之前在做一个内部 Agent 项目时,最头疼的往往不是模型能力,而是“下一步到底该怎么做决策”。用户说一句“帮我查一下最近三个月的订单”,系统要先判断用户身份是否可信、当前是闲聊还是真实业务请求,再决定调用订单查询工具还是先走身份核验。这些分支逻辑一开始全部写在 if-else 里,半年之后变成了一条上百行的判断链,改一个策略要重新编译、重新发布,线上出了误判还得回滚代码。
后来我们把“决策策略”从代码里抽出来,统一交给 Laya 决策模型管理,这才真正把流程理顺。这篇文章就围绕 Laya 决策模型,整理一份从零跑通第一个决策的完整教程,包含环境准备、模型文件怎么写、运行时怎么加载和执行、如何验证结果,以及怎样把这个决策模型接入 Agent 决策流程。无论你是刚接触决策模型的新手,还是想给现有系统引入策略编排能力的开发者,都可以按这篇文章的步骤完整地搭一遍。
1. 背景与核心概念
1.1 什么是 Laya 决策模型
先放到业务场景里解释。所谓“决策模型”,就是把“什么条件下做什么事”这件事,从业务代码中独立出来,变成一份可加载、可解释、可替换的结构化描述。它一般包含三个要素:
- 输入特征(比如订单金额、用户等级、风险分);
- 决策规则或决策表(比如“风险分大于 80 就拒绝”);
- 输出动作(比如 PASS、REVIEW、REJECT)。
Laya 决策模型的特点在于:模型以文件形式存在,运行时由统一的引擎负责加载和分析,业务代码只需要把输入特征传进去,引擎就会按照规则命中逻辑返回一个决策结果。这样策略调整就变成了“改模型文件 + 发布新模型”,而不再是“改代码 + 重新发版”。
从专业角度讲,Laya 决策模型是一种面向 JVM 环境的轻量级决策建模与执行方案。它借鉴了规则引擎和决策表的思路,但比传统规则引擎更轻:不需要独立部署服务,可以以库的形式嵌入到 Kotlin/Java 应用中,也可以在 Agent 运行流程中作为决策组件被反复调用。
1.2 它解决什么问题
在没有决策模型之前,业务策略通常散落在代码里。常见的问题有三类。
第一,策略与代码强耦合。一个判断条件改动,哪怕只是把阈值从 80 改成 75,也需要开发改代码、走构建、发版本。第二,规则之间互相影响,难以排查。多个 if-else 嵌套在一起,线上出现了非预期结果,很难定位到底是哪个分支出了问题。第三,测试成本高。策略逻辑没有独立的结构,测试用例只能从接口层反复造数据,效率很低。
Laya 决策模型把这些痛点集中解决掉:策略集中在模型文件中,版本清晰;命中规则有明确优先级,可以输出“哪条规则命中”便于排查;决策模型本身可以单独做单元测试,甚至可以在不发布代码的情况下做灰度验证。
1.3 决策模型与 Agent 的关系
社区里讨论 Laya 决策模型时,经常和 Agent 放在一起说,比如“adk.dev 的 Kotlin 快速上手在 JVM 上跑通一个 agent”这类话题。Agent 运行起来之后,模型负责理解用户意图,而“下一步该执行哪个工具、该走哪条分支”本质上就是一个决策问题。
所以一个很常见的做法,是把 Laya 决策模型作为 Agent 的“决策大脑”:Agent 拿到用户输入后,先做意图识别和特征提取,然后把特征交给决策模型,模型输出一个 action,Agent 再根据 action 选择对应的工具或流程。这种“决策模型先行”的模式,社区里也常叫它 laya 模式。它让 Agent 的分支逻辑变得可配置、可观测、可测试,比在 Agent 主循环里写一长串 if-else 要干净得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与版本说明
2.1 环境清单
本文的示例基于 JVM 环境,推荐使用以下环境:
- 操作系统:Windows / macOS / Linux 均可;
- JDK 17 或更高版本;
- Kotlin 2.x;
- Gradle 8.x,或 Maven 3.8+;
- IDE 推荐 IntelliJ IDEA,社区版即可。
需要说明的是,Laya 决策模型在不同项目里的版本和 API 可能不同。本文的重点是讲清楚“建模 + 加载 + 执行”的思路,代码属于示例性质,具体类名、包名、方法名请以你实际引入的版本为准。如果你是第一次接触,建议先按本文的结构把代码搭起来,跑通之后再去查自己所用版本的精确 API。
2.2 示例项目结构
为了后面操作方便,我们先把项目目录规划好。建议目录结构如下:
text复制laya-decision-demo/
├── build.gradle.kts
├── settings.gradle.kts
├── models/
│ └── order_risk_decision_v1.json
└── src/main/kotlin/com/example/laya/
├── Main.kt
├── model/LayaModel.kt
└── runtime/LayaDecisionEngine.kt
目录分工很清晰:models 目录放决策模型文件,model 包放模型对应的数据结构,runtime 包放模型加载和执行的引擎逻辑,Main.kt 作为程序入口。这样划分的好处是,后续把决策模型接入真实业务系统时,只需要替换模型文件和修改调用方式,不需要动引擎代码。
3. 理解 Laya 决策模型的核心组成
3.1 模型文件的三要素
一个标准的 Laya 决策模型文件,通常包含三块:输入描述(inputSchema)、输出描述(outputSchema)和决策表(decisionTable)。
输入描述用来声明这个模型接收哪些特征字段,以及字段的类型和约束。它相当于对外的一个“接口契约”,调用方必须按照这个契约传入数据。输出描述声明决策完成之后返回什么结构。决策表则是一组规则的集合,每条规则包含“命中条件”和“输出动作”。
这里用一个订单风控决策模型的 JSON 文件来演示。文件路径为 models/order_risk_decision_v1.json:
json复制{
"modelId": "order_risk_decision",
"version": "1.0.0",
"inputSchema": {
"type": "object",
"properties": {
"orderAmount": { "type": "number" },
"userLevel": { "type": "string", "enum": ["NEW", "NORMAL", "VIP"] },
"riskScore": { "type": "number", "minimum": 0, "maximum": 100 }
},
"required": ["orderAmount", "userLevel", "riskScore"]
},
"outputSchema": {
"type": "object",
"properties": {
"action": { "type": "string", "enum": ["PASS", "REVIEW", "REJECT"] },
"reason": { "type": "string" }
},
"required": ["action"]
},
"decisionTable": [
{
"name": "高风险直接拒绝",
"priority": 1,
"when": { "riskScore": { "gte": 80 } },
"then": { "action": "REJECT", "reason": "风险分过高" }
},
{
"name": "VIP大额订单通过",
"priority": 2,
"when": {
"userLevel": "VIP",
"orderAmount": { "gte": 10000 }
},
"then": { "action": "PASS", "reason": "VIP高价值用户" }
},
{
"name": "默认人工审核",
"priority": 100,
"when": {},
"then": { "action": "REVIEW", "reason": "未命中特殊策略" }
}
]
}
这个模型描述的业务含义是:如果一个订单触发风控规则,首先判断风险分是否大于等于 80,如果是,直接拒绝;其次判断用户是不是 VIP 且订单金额达到 10000 以上,如果是,直接通过;如果前面都没有命中,走人工审核兜底。
3.2 规则命中机制
决策表执行的时候,引擎会按照 priority 从小到大的顺序匹配规则。priority 数字越小,优先级越高。也就是说,上面的模型中第一条规则优先级最高,会先被检查;如果命中,就直接返回结果,不再继续向下匹配,这就是常见的“首命即止”模式。
这种模式的好处是清晰可控:特殊策略放在前面,通用兜底策略放在最后,保证任何输入都能得到一个结果。日常使用中需要注意“规则顺序影响结果”的问题,这也是决策模型开发中最容易踩的坑。比如高风险规则和 VIP 规则同时命中时,必须明确谁先谁后,否则同样的输入在不同版本的模型里可能得到不一致的决策。
3.3 执行器核心流程
Laya 决策模型运行时执行一次决策的流程可以简单描述为:
text复制读取模型文件 -> 校验输入Schema -> 按优先级遍历规则 ->
匹配条件 -> 命中则返回输出 -> 未命中走下一规则 -> 兜底规则返回
从开发角度看,核心就是“加载”和“执行”两个动作。加载负责把 JSON 文件解析成内存对象,并预编译条件表达式;执行则是接收输入、遍历规则、输出结果。之所以要预编译条件,是为了避免每条输入都去重新解析 JSON 字符串,这是提升性能的关键。
4. 从零跑通第一个决策
4.1 创建项目结构
先在本地创建一个空目录 laya-decision-demo,然后建立上面规划好的目录结构。如果是用 IDEA,可以直接新建一个 Kotlin/JVM 项目,生成默认的 build.gradle.kts 和 settings.gradle.kts。
settings.gradle.kts 内容非常简单:
kotlin复制rootProject.name = "laya-decision-demo"
4.2 添加构建依赖
build.gradle.kts 中我们需要 Kotlin 插件、应用插件,以及 JSON 解析和日志依赖。具体版本可以按你的实际环境调整,这里给出示例配置:
kotlin复制plugins {
kotlin("jvm") version "2.0.21"
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
implementation("org.slf4j:slf4j-api:2.0.13")
implementation("ch.qos.logback:logback-classic:1.5.6")
testImplementation(kotlin("test"))
}
application {
mainClass.set("com.example.laya.MainKt")
}
其中 Jackson 用来解析 JSON 模型文件和输入输出结构,SLF4J 负责日志输出。如果你用的是 Maven 项目,等价的依赖也可以按这个名字去查询。版本号不要照抄,建议打开 Maven Central 搜索对应依赖的最新稳定版,避免版本冲突。
4.3 定义模型数据结构
模型文件是 JSON,运行时需要对应的 Kotlin 数据结构来承接。在 src/main/kotlin/com/example/laya/model/LayaModel.kt 中创建数据类:
kotlin复制package com.example.laya.model
// 决策规则:一条规则由名称、优先级、条件和输出组成
data class DecisionRule(
val name: String,
val priority: Int,
val whenCondition: Map<String, Any>,
val thenOutput: Map<String, Any>
)
// 决策模型:模型ID、版本、输入输出契约以及决策表
data class LayaModel(
val modelId: String,
val version: String,
val inputSchema: Map<String, Any>,
val outputSchema: Map<String, Any>,
val decisionTable: List<DecisionRule>
)
这里把 schema 先按 Map<String, Any> 处理,好处是模型文件变更时不需要频繁改动数据结构。如果团队对类型安全要求高,可以再为 schema 定义更严格的类型,但初学阶段没必要引入额外复杂度。
4.4 编写加载与执行逻辑
在 src/main/kotlin/com/example/laya/runtime/LayaDecisionEngine.kt 中编写引擎代码:
kotlin复制package com.example.laya.runtime
import com.example.laya.model.DecisionRule
import com.example.laya.model.LayaModel
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.io.File
data class DecisionResult(
val action: String,
val reason: String?,
val matchedRule: String?,
val modelVersion: String
)
class LayaDecisionEngine {
private val mapper: ObjectMapper = jacksonObjectMapper()
// 加载模型文件
fun loadModel(path: String): LayaModel {
val json = File(path).readText()
return mapper.readValue<LayaModel>(json)
}
// 执行一次决策
fun evaluate(model: LayaModel, input: Map<String, Any>): DecisionResult {
val sortedRules = model.decisionTable.sortedBy { it.priority }
for (rule in sortedRules) {
if (matchRule(rule, input)) {
val action = rule.thenOutput["action"]?.toString() ?: "UNKNOWN"
val reason = rule.thenOutput["reason"]?.toString()
return DecisionResult(
action = action,
reason = reason,
matchedRule = rule.name,
modelVersion = model.version
)
}
}
return DecisionResult("UNKNOWN", "未命中任何规则", null, model.version)
}
// 判断单条规则是否命中
private fun matchRule(rule: DecisionRule, input: Map<String, Any>): Boolean {
// 空条件视为兜底规则,永远命中
if (rule.whenCondition.isEmpty()) return true
for ((key, condition) in rule.whenCondition) {
val actualValue = input[key]
if (actualValue == null && condition !is Map<*, *>) {
if (actualValue != condition) return false
}
// 处理简单的比较运算符,例如 gte
if (condition is Map<*, *>) {
for ((op, expected) in condition) {
if (!compare(actualValue, op.toString(), expected)) return false
}
} else if (actualValue != condition) {
return false
}
}
return true
}
private fun compare(actualValue: Any?, op: String, expected: Any): Boolean {
if (actualValue !is Number) return false
val actual = actualValue.toDouble()
val base = (expected as Number).toDouble()
return when (op) {
"gte" -> actual >= base
"gt" -> actual > base
"lte" -> actual <= base
"lt" -> actual < base
"eq" -> actual == base
else -> false
}
}
}
说明几点。matchRule 方法遍历规则中的每个条件键,从输入里取对应值做比较;gte 这类运算符统一转成 Double 比较,避免 int 和 long 混用。这里为了演示方便,比较器只实现了数字运算,真实项目中你还需要支持字符串等于、枚举列表包含、正则匹配等能力,可以作为扩展点继续完善。
这段代码的核心思路是通用的:无论底层引擎 API 叫什么名字,“加载模型”和“匹配规则”这两个职责是决策模型运行时必须具备的。
4.5 编写主程序并运行
在 src/main/kotlin/com/example/laya/Main.kt 中编写入口:
kotlin复制package com.example.laya
import com.example.laya.runtime.LayaDecisionEngine
import com.example.laya.runtime.evaluate
fun main() {
val engine = LayaDecisionEngine()
val model = engine.loadModel("models/order_risk_decision_v1.json")
val input = mapOf(
"orderAmount" to 12000,
"userLevel" to "VIP",
"riskScore" to 65
)
val result = engine.evaluate(model, input)
println("action = ${result.action}")
println("reason = ${result.reason}")
println("matchedRule = ${result.matchedRule}")
println("modelVersion = ${result.modelVersion}")
}
在项目根目录执行:
bash复制./gradlew run
预期输出类似:
text复制action = PASS
reason = VIP高价值用户
matchedRule = VIP大额订单通过
modelVersion = 1.0.0
4.6 结果说明
为什么这个输入得到 PASS?我们来手动走一遍匹配过程。输入特征是 orderAmount=12000、userLevel=VIP、riskScore=65。
引擎首先检查优先级最高的“高风险直接拒绝”规则,条件是 riskScore >= 80。当前风险分是 65,不满足。然后是“VIP大额订单通过”规则,userLevel=VIP 且 orderAmount=12000 >= 10000,两个条件都满足,于是命中,输出 PASS。
如果把 riskScore 改成 85 再跑一次,第一条规则就会命中,输出直接变成 REJECT。这就是决策模型相比 if-else 的直观优势:不需要改任何代码,只改模型文件或输入数据,决策结果就会按预定策略变化。
5. 进阶:把决策模型接入 Agent 流程
5.1 场景说明
跑通单次决策之后,下一步通常是把它放进真实的业务流程。这里给一个典型的 Agent 场景:Agent 根据用户意图选择调用哪个工具。
假设一个客服 Agent 面对三种典型请求:
- 用户咨询产品资料,需要调用
search_web_tool; - 用户查询自己的订单,需要调用
db_query_tool; - 用户闲聊或意图不明确,走
default_reply_tool。
如果用 if-else 写,很快又会变成一条又长又乱的判断链。更好的方式是建一个“工具选择决策模型”,把“什么意图调用什么工具”的规则收进模型文件。
5.2 工具选择模型设计
新增模型文件 models/tool_decision_v1.json:
json复制{
"modelId": "tool_decision",
"version": "1.0.0",
"inputSchema": {
"type": "object",
"properties": {
"intent": { "type": "string" },
"needOrderInfo": { "type": "boolean" }
},
"required": ["intent"]
},
"outputSchema": {
"type": "object",
"properties": {
"action": { "type": "string" },
"reason": { "type": "string" }
},
"required": ["action"]
},
"decisionTable": [
{
"name": "订单查询走数据库工具",
"priority": 1,
"when": {
"intent": "order_query",
"needOrderInfo": true
},
"then": { "action": "db_query", "reason": "用户需要查询订单" }
},
{
"name": "资料咨询走搜索工具",
"priority": 2,
"when": { "intent": "product_consult" },
"then": { "action": "search", "reason": "用户咨询产品资料" }
},
{
"name": "默认走兜底回复",
"priority": 100,
"when": {},
"then": { "action": "fallback", "reason": "意图不明确" }
}
]
}
5.3 在 Agent 中调用决策模型
在 src/main/kotlin/com/example/laya/agent/ToolSelector.kt 中,把决策模型封装成工具选择器:
kotlin复制package com.example.laya.agent
import com.example.laya.model.LayaModel
import com.example.laya.runtime.LayaDecisionEngine
class ToolSelector(
private val engine: LayaDecisionEngine,
private val toolDecisionModel: LayaModel
) {
fun selectTool(intent: String, needOrderInfo: Boolean): String {
val input = mapOf(
"intent" to intent,
"needOrderInfo" to needOrderInfo
)
val decision = engine.evaluate(toolDecisionModel, input)
return when (decision.action) {
"db_query" -> "db_query_tool"
"search" -> "search_web_tool"
else -> "default_reply_tool"
}
}
}
然后在 Agent 主循环的开始阶段调用 selectTool,拿到工具名后再执行对应工具。如果你正在用 ADK,也就是 Agent Development Kit,可以参考 adk.dev 上的 Kotlin 快速上手文档,先把一个 Agent 在 JVM 上跑通,然后把 ToolSelector 插入到 Agent 的决策节点中。这样 Agent 的“下一步做什么”就不是写死的代码,而是可配置的 Laya 决策模型,后续调整工具选择策略也不需要重新发布 Agent 服务。
5.4 模型文件的来源与管理
另一个常被问到的问题是:“laya 模型从哪里来?”模型文件可以是自己根据业务编写的,也可以从团队内部的模型仓库或官方发布页下载基础模型后二次修改。如果需要下载模型,注意确认模型版本、输入输出契约和依赖的运行环境,模型文件最好和业务代码放在同一个版本仓库中管理,保证“代码 + 模型 + 配置”三者版本一致。
6. 常见问题与排查思路
6.1 高频问题对照表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 模型加载失败,提示文件不存在 | 模型文件路径不对 | 检查路径,建议使用绝对路径或 classpath 资源路径 |
| JSON 解析报错 | 模型文件格式不合法 | 用 JSON 校验工具检查,注意逗号、引号和注释不能混用 |
| 决策结果为空,返回 UNKNOWN | 所有规则都没命中,且没有兜底规则 | 增加 priority 最大、when 条件为空的兜底规则 |
| 条件判断结果和预期不一致 | 输入字段类型与模型 schema 不匹配 | 打印输入 Map,确认字段名和类型统一 |
| 多次调用性能差 | 每次调用都重新加载模型文件 | 初始化时加载一次,缓存 LayaModel 实例 |
| 线上模型和代码不同步 | 模型变更未走发布流程 | 模型文件入库,和代码一起走版本管理 |
| 规则同时命中但结果不对 | 规则优先级设计不合理 | 明确首命即止语义,检查 priority 顺序 |
6.2 排查建议
遇到决策结果不符合预期时,不要直接去翻代码,先看三样东西:输入特征、模型版本、命中规则。输入特征要确认字段名有没有拼错,类型是不是字符串和数字混用;模型版本要确认当前加载版本是不是你想要的那一版;命中规则最直接,一般运行时对象里会返回 matchedRule,拿到这个字段基本就能定位问题。
一个实用的小技巧是,在模型执行入口打印完整的输入和输出 JSON。决策模型之所以比 if-else 好排查,就是因为“输入 + 模型 + 输出”这三段都是可见、可复现的,记录下来之后即使模型后面改过,也能拿旧数据重新计算对比。
7. 最佳实践与工程建议
7.1 先定义输入输出契约,再写规则
建模之前先想清楚模型的输入特征有哪些、输出动作有哪些,这比先写规则更重要。输入输出契约定了,调用方和模型维护者才有一个稳定的沟通边界。实际项目中建议为每个模型维护一份字段说明文档,字段来源、类型、取值范围都写清楚,避免不同团队因为字段名理解不一致反复返工。
7.2 规则优先级要显式、冲突要可检测
决策表里每条规则都必须有明确的优先级,不要让引擎的默认顺序成为隐式规则。规则多起来之后,可以在模型发布前跑一遍“冲突检测”:把所有规则两两对比,找出条件存在交集、输出却不同的规则对,人工确认是否合理。这一步能省掉很多线上诡异问题。
7.3 模型加载一次,执行多次
把模型加载放到应用启动阶段或初始化阶段,不要在每次请求时重新读文件和解析 JSON。如果模型文件本身很大,还可以考虑预编译条件表达式,把字符串条件转成内存中的比较闭包,执行时只做数值比较,性能会好很多。
7.4 日志记录要包含可解释信息
每次决策都应当记录模型 ID、模型版本、输入特征、输出动作、命中规则。这样一方面方便排错,另一方面可以为后续的模型评估提供真实样本数据。如果用户投诉某个决策不合理,你能立刻知道这条决策命中了哪条规则、为什么命中,而不是对着代码猜测。
7.5 模型发布要走配置化流程
生产环境下,模型文件的修改建议走配置中心或模型管理平台,而不是直接改服务器上的文件。改动前先在测试环境用历史数据回归,再小流量灰度,最后全量发布。因为决策模型往往直接影响用户可见行为,一旦误判就是线上事故,所以必须有回滚预案。
7.6 安全边界不能丢
模型文件本身是数据,虽然不直接拼接 SQL 或执行脚本,但如果模型中允许自定义条件表达式,就要考虑表达式注入的风险。对从外部获取的模型文件,要做完整性校验和签名验证,防止被恶意替换。决策模型只应负责“决策”,不负责执行高权限操作,输出 Action 后,真正的工具调用和数据库操作仍然要走原有的鉴权链路。
7.7 用测试用例把模型固下来
决策模型非常适合做表格化测试。把典型输入和期望输出整理成用例集,每次模型变动都跑一遍全量用例。新增规则要配套新增测试用例,删掉旧规则要确认没有线上流量依赖它。模型测试和代码测试一样重要,不能因为模型不是代码就跳过测试。
8. 总结与实践路线
这一路下来,你已经把 Laya 决策模型的完整链路走了一遍:理解了决策模型解决的问题,搭好了 Kotlin/JVM 项目环境,写了一个订单风控决策模型,用运行时加载并执行出了正确的决策结果,还把它接入了 Agent 工具选择的场景。下面这个顺序可以作为后续深入学习的参考。
先把第 4 章的示例完整跑通,确保自己对“加载模型、匹配规则、输出结果”这个过程有手感。然后试着修改模型,比如增加一个“普通用户大额订单走审核”的规则,观察优先级和命中规则的变化。接着给 matchRule 增加字符串、枚举、正则等比较能力,把决策引擎补全到能应对真实业务。最后再回到 Agent 场景,把 ADK 和决策模型结合起来,在 JVM 上完整跑通一个会根据用户意图切换工具的 Agent。
如果本文帮你解决了问题,可以收藏备用,也欢迎在实际跑通之后把你踩到的坑记录下来,决策模型看起来简单,但真正用到生产环境时,规则冲突、模型版本、灰度发布这些细节才是真正考验工程能力的地方。
