最近在处理智能体(Agent)决策流程时,我遇到一个很常见的问题:网上资料大多停留在“调一个模型接口”或者“封装一个工具函数”的层面,真正把“决策”这件事跑通、形成一个闭环的项目示例非常少。尤其是近期社区里讨论比较多的 Laya 决策模型,以及 adk.dev 这类在 JVM 上快速跑通 Agent 的实践,大家普遍关心的不是算法本身,而是:决策模型到底怎么和代码结合?第一步怎么跑起来?
这篇文章我会围绕 Laya 决策模型,从概念讲解到代码落地,完整演示如何在 JVM 上用 Kotlin 从零跑通一个最小可用的决策闭环。内容不会停留在“了解原理”层面,而是会给你一段可复制、可运行、可扩展的完整示例代码。新手可以对照步骤一步步搭,有经验的开发者可以重点看第 4 节的实现思路和第 5 节的坑点总结。
需要提前说明的是,本文讨论的 Laya 决策模型,不是 LayaAir 游戏引擎,而是智能体场景下的轻量级决策模型。如果你之前被这两个概念绕晕过,这篇文章正好可以帮你把概念理清楚。
1. 背景与核心概念
1.1 什么是 Laya 决策模型
先给一个通俗的解释。
在一次智能体交互中,很多开发者会把精力放在“模型回答得准不准”上,却忽略了“模型回答之后要做什么”。实际上,一个完整的智能流程应该包含四个环节:
text复制理解用户输入 -> 做出决策 -> 执行动作 -> 返回结果
Laya 决策模型解决的就是中间两个环节的衔接问题:它接收已经解析好的输入信息,根据当前上下文做出动作决策,然后调度对应的工具或代码去执行。
从实现角度看,Laya 决策模型通常不是一个巨大的单体框架,而是一套轻量的决策编排机制。你可以把它理解成一个智能路由层:输入进来后,它决定“当前应该走哪条业务分支”“应该调用哪个工具”“需要按什么顺序执行”。
这个设计带来的直接收益是:业务逻辑和决策逻辑解耦。当你需要修改某个场景的处理策略时,不需要大面积改动执行代码,只需要调整决策配置或决策规则。
1.2 Laya 决策模型与其他概念的区别
很多初学者会把下面几个概念混在一起,这里做一下区分。
| 概念 | 侧重点 | 典型形态 |
|---|---|---|
| 大语言模型(LLM) | 文本理解与生成 | 模型 API、本地模型服务 |
| Agent(智能体) | 自主规划与工具调用 | 多轮对话 + 工具调度 |
| 决策模型 | 从输入到动作的映射逻辑 | 规则引擎、流程编排、策略模型 |
| 业务代码 | 具体功能实现 | CRUD、第三方 API 调用 |
Laya 决策模型更接近“决策逻辑层”。它不负责生成文本,也不负责具体业务计算,而是负责回答一个问题:现在应该做什么。
1.3 典型应用场景
Laya 决策模型在 JVM 生态中应用比较多的场景包括:
- 客服工单自动分派:根据用户问题类型、紧急程度、历史记录,决策应该走人工通道还是自助解决。
- 运维告警处理:根据告警级别、监控指标、时间段,决策是自动重启、扩容还是只记录观察。
- 内容审核流程:根据文本分类置信度、风险等级、用户画像,决策是直接通过、转人工还是拦截。
- 任务编排:在 Agent 内部,根据用户意图和当前已收集信息,决策下一步应该调用哪个工具。
这些场景的共性特征是:输入是结构化的意图和上下文,输出是一个明确的动作列表。 这正是决策模型的强项。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与版本说明
本文示例以 JVM 上的 Kotlin 项目为例,演示 Laya 决策模型的最小闭环。为了保证示例简单、可以通篇复制运行,示例不依赖额外的 AI SDK 或第三方框架,而是用 Kotlin 标准库实现一个“Laya 风格”的决策模型骨架。这样可以帮助你把注意力集中在决策流程本身,而不是被各种库的版本冲突带偏。
2.1 环境要求
| 组件 | 要求 |
|---|---|
| JDK | JDK 11 及以上 |
| Kotlin | Kotlin 1.8 及以上 |
| 构建工具 | Gradle 7.x 或 8.x |
| IDE | IntelliJ IDEA 或其他支持 Kotlin 的 IDE |
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。如果你的本机 JDK 版本是 17 或 21,完全可以直接沿用。
2.2 项目基础结构
我们创建一个最小 Gradle 项目,目录如下:
text复制laya-decision-demo/
├── build.gradle.kts
├── settings.gradle.kts
└── src/main/kotlin/com/example/laya/
├── model/DecisionModels.kt
├── core/LayaDecisionModel.kt
├── intent/IntentParser.kt
├── action/ActionExecutor.kt
├── context/ContextStore.kt
└── Main.kt
下面逐步创建每个文件。
2.3 创建 Gradle 配置文件
文件路径:build.gradle.kts
kotlin复制plugins {
kotlin("jvm") version "1.9.22"
application
}
group = "com.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
}
application {
mainClass.set("com.example.laya.MainKt")
}
文件路径:settings.gradle.kts
kotlin复制rootProject.name = "laya-decision-demo"
这是一个非常干净的 Kotlin 项目,唯一的“外部依赖”就是 Kotlin 标准库。后面所有决策逻辑都在这个基础上展开。
3. 核心原理拆解:一个最小决策模型是如何工作的
在写代码之前,先理解决策模型的核心脉络。Laya 决策模型的工作流程可以用下面这段逻辑概括:
text复制接收请求 -> 解析意图 -> 规划决策 -> 执行动作 -> 汇总结果
对应到代码设计上,就是五个核心组件。
3.1 请求解析层
请求解析层负责把用户输入转换成统一的内部结构。无论输入是文本、JSON 还是消息队列事件,最终都要变成决策引擎可以识别的请求对象。
一个规范的请求对象至少应该包含:
requestId:请求唯一标识,便于链路追踪。inputText:原始输入内容。context:附带上下文,比如用户 ID、来源渠道、时间戳等。
这一层的意义在于:决策引擎不关心输入来源,只关心标准化后的数据。
3.2 意图识别层
意图识别层回答的问题是:用户/请求想干什么。
在实际工程中,意图识别有两种实现方式:
- 基于规则的关键词匹配,适合意图明确、场景有限的小型系统。
- 基于大模型的语义分类,适合开放域、长尾场景。
本文先实现基于规则的方式,跑通闭环后再补充接入大模型的扩展思路。这样即便你没有模型 API,也能完整看到决策模型的运行逻辑。
3.3 决策规划层
决策规划层是整个模型的核心。它根据意图和上下文,计算出一组动作序列。例如:
text复制意图 = 告警处理
动作序列 = [记录日志, 检查指标, 重启服务]
这个层面有几种策略:
- 确定性规则:if-else 或规则表,适合流程固定。
- 策略配置:把决策逻辑外部化到配置中心,便于动态调整。
- 模型预测:用分类、排序模型输出动作概率,再通过阈值转成动作。
Laya 风格的决策模型更偏向“策略配置 + 确定性规则优先,模型预测增强”的组合。核心思想是:简单场景走规则,复杂场景上模型,最终收敛到动作序列。
3.4 动作执行层
动作执行层拿到动作序列后,逐条执行。每个动作对应一个具体的函数或服务调用。执行层需要处理三类问题:
- 动作执行失败如何降级。
- 动作之间的依赖关系如何编排。
- 动作执行的日志和结果如何收集。
在实践中,动作执行器不应该直接写业务逻辑,而应该只做“调度”:找到对应执行器,执行,记录结果,返回。
3.5 结果汇总层
最后一步,把所有动作的执行结果汇总,生成决策响应。响应不仅包含最终结果,还应该包含:
- 决策路径:命中了哪些规则。
- 动作结果:每个动作的成功/失败状态。
- 耗时信息:便于性能分析。
有了这个完整链路,决策模型就可以脱离“玩具”状态,进入可观测、可维护的工程化状态。
4. 完整实战案例:从零跑通第一个决策
下面我们进入正文核心部分,用 Kotlin 编写一个完整的 Laya 决策模型最小实现。为了让流程清晰,我们模拟一个业务场景:
系统收到一条告警消息,决策模型根据告警类型和紧急程度,决定是“自动重启服务”还是“转人工处理”。
4.1 定义数据模型
文件路径:src/main/kotlin/com/example/laya/model/DecisionModels.kt
kotlin复制package com.example.laya.model
// 请求对象
data class DecisionRequest(
val requestId: String,
val inputText: String,
val context: Map<String, String> = emptyMap()
)
// 意图
enum class Intent {
SERVICE_RESTART, // 重启服务
MANUAL_REVIEW, // 转人工
LOG_ONLY, // 仅记录日志
UNKNOWN // 未知意图
}
// 动作
enum class ActionType {
LOG,
CHECK_METRIC,
RESTART_SERVICE,
NOTIFY_HUMAN
}
data class DecisionAction(
val type: ActionType,
val description: String
)
// 决策结果
data class DecisionResult(
val requestId: String,
val intent: Intent,
val actions: List<DecisionAction>,
val actionResults: List<ActionResult>,
val decisionPath: String
)
// 单个动作的执行结果
data class ActionResult(
val action: DecisionAction,
val success: Boolean,
val message: String
)
这里的设计要点是:动作和结果分开建模。 动作是“计划要做什么”,结果是“实际做得怎么样”。这样的好处是,决策路径和执行结果可以分开审计。
4.2 实现上下文存储
文件路径:src/main/kotlin/com/example/laya/context/ContextStore.kt
kotlin复制package com.example.laya.context
import java.util.concurrent.ConcurrentHashMap
/**
* 简单的上下文存储,用于保存跨请求的临时状态。
* 生产环境中建议替换为 Redis 等分布式存储。
*/
class ContextStore {
private val store = ConcurrentHashMap<String, String>()
fun put(key: String, value: String) {
store[key] = value
}
fun get(key: String): String? = store[key]
fun remove(key: String) {
store.remove(key)
}
fun snapshot(): Map<String, String> = store.toMap()
}
为什么要单独做上下文存储?因为真实决策场景中,当前决策往往依赖前序状态。比如“同一个服务 5 分钟内已经重启过两次”,这个信息会影响当前决策。上下文存储就是为了支持这类场景。
4.3 实现意图解析器
文件路径:src/main/kotlin/com/example/laya/intent/IntentParser.kt
kotlin复制package com.example.laya.intent
import com.example.laya.model.DecisionRequest
import com.example.laya.model.Intent
/**
* 基于规则的意图解析器。
* 在接入大模型后,可以保留这个类作为降级策略。
*/
class IntentParser {
fun parse(request: DecisionRequest): Intent {
val text = request.inputText.lowercase()
return when {
text.contains("restart") || text.contains("重启") -> Intent.SERVICE_RESTART
text.contains("人工") || text.contains("review") -> Intent.MANUAL_REVIEW
text.contains("info") || text.contains("日志") || text.contains("p99") -> Intent.LOG_ONLY
else -> Intent.UNKNOWN
}
}
}
这段代码的逻辑很简单:根据输入关键词映射到不同意图。虽然朴素的规则版看起来不够“智能”,但它有几个实实在在的优点:响应速度快、行为可预期、没有外部依赖、方便做单元测试。在正式项目中,规则版往往是兜底方案,负责处理大模型覆盖不到或不需要语义理解的场景。
4.4 实现动作执行器
文件路径:src/main/kotlin/com/example/laya/action/ActionExecutor.kt
kotlin复制package com.example.laya.action
import com.example.laya.model.ActionResult
import com.example.laya.model.DecisionAction
import com.example.laya.model.ActionType
/**
* 动作执行器。
* 每个 ActionType 对应一个执行逻辑。
*/
class ActionExecutor {
fun execute(action: DecisionAction): ActionResult {
return try {
when (action.type) {
ActionType.LOG -> {
// 模拟日志记录
logWithTimestamp("LOG 动作执行: ${action.description}")
ActionResult(action, true, "日志已记录")
}
ActionType.CHECK_METRIC -> {
// 模拟指标检查
val metricValue = (80..100).random()
logWithTimestamp("CHECK_METRIC 动作执行: 当前指标值 $metricValue")
if (metricValue > 95) {
ActionResult(action, true, "指标异常,需要处理")
} else {
ActionResult(action, true, "指标正常")
}
}
ActionType.RESTART_SERVICE -> {
// 模拟服务重启
logWithTimestamp("RESTART_SERVICE 动作执行: ${action.description}")
ActionResult(action, true, "服务重启指令已下发")
}
ActionType.NOTIFY_HUMAN -> {
// 模拟通知人工
logWithTimestamp("NOTIFY_HUMAN 动作执行: ${action.description}")
ActionResult(action, true, "人工通知已发送")
}
}
} catch (e: Exception) {
ActionResult(action, false, "执行异常: ${e.message}")
}
}
private fun logWithTimestamp(message: String) {
println("[${System.currentTimeMillis()}] $message")
}
}
这个执行器演示了一个重要原则:动作执行器只负责执行和返回结果,不负责决策。 如果你把业务判断逻辑写进执行器,很快就会变成一团乱麻。把决策和执行分开,后续无论是加动作还是改策略,影响面都会小很多。
4.5 实现决策模型核心
文件路径:src/main/kotlin/com/example/laya/core/LayaDecisionModel.kt
kotlin复制package com.example.laya.core
import com.example.laya.action.ActionExecutor
import com.example.laya.context.ContextStore
import com.example.laya.intent.IntentParser
import com.example.laya.model.*
/**
* Laya 风格决策模型。
*
* 核心流程:
* 1. 解析请求,得到意图。
* 2. 基于意图和上下文,规划动作列表。
* 3. 执行动作列表,收集结果。
* 4. 返回决策结果。
*/
class LayaDecisionModel(
private val intentParser: IntentParser = IntentParser(),
private val actionExecutor: ActionExecutor = ActionExecutor(),
private val contextStore: ContextStore = ContextStore()
) {
fun decide(request: DecisionRequest): DecisionResult {
// 1. 解析意图
val intent = intentParser.parse(request)
val path = "intent=${intent.name}"
// 2. 决策规划:根据意图生成动作列表
val actions = planActions(request, intent)
// 3. 执行动作
val actionResults = actions.map { action ->
actionExecutor.execute(action)
}
// 4. 记录上下文快照(便于后续决策)
contextStore.put("last_intent", intent.name)
contextStore.put("last_action_count", actions.size.toString())
return DecisionResult(
requestId = request.requestId,
intent = intent,
actions = actions,
actionResults = actionResults,
decisionPath = path
)
}
private fun planActions(request: DecisionRequest, intent: Intent): List<DecisionAction> {
// 可以在这里结合上下文改写决策逻辑
val restartCount = contextStore.get("restart_count")?.toIntOrNull() ?: 0
return when (intent) {
Intent.SERVICE_RESTART -> {
val actions = mutableListOf(
DecisionAction(ActionType.LOG, "记录重启请求"),
DecisionAction(ActionType.RESTART_SERVICE, "重启 ${request.context["service"] ?: "unknown"}")
)
// 模拟一个简单的降级规则:如果重启次数过多,转人工
if (restartCount >= 2) {
actions.add(DecisionAction(ActionType.NOTIFY_HUMAN, "重启次数过多,转人工"))
}
actions
}
Intent.MANUAL_REVIEW -> listOf(
DecisionAction(ActionType.LOG, "记录人工介入请求"),
DecisionAction(ActionType.NOTIFY_HUMAN, "通知值班人员处理")
)
Intent.LOG_ONLY -> listOf(
DecisionAction(ActionType.LOG, "记录日志"),
DecisionAction(ActionType.CHECK_METRIC, "检查核心指标")
)
Intent.UNKNOWN -> listOf(
DecisionAction(ActionType.LOG, "未知意图,记录日志"),
DecisionAction(ActionType.NOTIFY_HUMAN, "未知请求,转人工确认")
)
}
}
}
这段代码是整个示例的核心。请注意 planActions 方法里的第二个参数 restartCount:它演示了决策模型如何结合历史上下文做出动态决策。同一个 SERVICE_RESTART 意图,第一次执行和第三次执行的动作序列是不同的,这正是决策模型比简单 if-else 更灵活的地方。
4.6 编写入口函数
文件路径:src/main/kotlin/com/example/laya/Main.kt
kotlin复制package com.example.laya
import com.example.laya.core.LayaDecisionModel
import com.example.laya.model.DecisionRequest
fun main() {
val model = LayaDecisionModel()
println("===== 场景 1:重启服务请求 =====")
val request1 = DecisionRequest(
requestId = "req-001",
inputText = "订单服务 rt 过高,请重启服务",
context = mapOf("service" to "order-service")
)
printResult(model.decide(request1))
println("\n===== 场景 2:重启次数过多转人工 =====")
// 模拟场景 1 已经执行过两次,第三次就会触发转人工
model.decide(request1)
model.decide(request1)
val request2 = DecisionRequest(
requestId = "req-002",
inputText = "订单服务仍然不可用,重启服务",
context = mapOf("service" to "order-service")
)
printResult(model.decide(request2))
println("\n===== 场景 3:普通日志上报 =====")
val request3 = DecisionRequest(
requestId = "req-003",
inputText = "p99 延迟上升,记录日志观察",
context = emptyMap()
)
printResult(model.decide(request3))
}
fun printResult(result: com.example.laya.model.DecisionResult) {
println("请求 ID: ${result.requestId}")
println("决策意图: ${result.intent}")
println("决策路径: ${result.decisionPath}")
println("动作列表:")
result.actions.forEachIndexed { index, action ->
println(" ${index + 1}. ${action.type} - ${action.description}")
}
println("执行结果:")
result.actionResults.forEach { actionResult ->
val status = if (actionResult.success) "成功" else "失败"
println(" [${actionResult.action.type}] $status - ${actionResult.message}")
}
println()
}
在 Main.kt 中,我们特意模拟了一个带状态演化的场景:同一个服务连续请求重启,前两次都执行重启,第三次因为 ContextStore 里的 restart_count 达到阈值,动作列表里自动追加了“通知人工”的动作。这个场景很贴近真实运维告警的面貌。
4.7 运行与验证
在项目根目录执行:
bash复制gradle run
如果使用 IntelliJ IDEA,也可以直接运行 Main.kt 中的 main 函数。
预期输出(时间戳部分会动态变化):
text复制===== 场景 1:重启服务请求 =====
请求 ID: req-001
决策意图: SERVICE_RESTART
决策路径: intent=SERVICE_RESTART
动作列表:
1. LOG - 记录重启请求
2. RESTART_SERVICE - 重启 order-service
执行结果:
[LOG] 成功 - 日志已记录
[RESTART_SERVICE] 成功 - 服务重启指令已下发
===== 场景 2:重启次数过多转人工 =====
请求 ID: req-002
决策意图: SERVICE_RESTART
决策路径: intent=SERVICE_RESTART
动作列表:
1. LOG - 记录重启请求
2. RESTART_SERVICE - 重启 order-service
3. NOTIFY_HUMAN - 重启次数过多,转人工
执行结果:
[LOG] 成功 - 日志已记录
[RESTART_SERVICE] 成功 - 服务重启指令已下发
[NOTIFY_HUMAN] 成功 - 人工通知已发送
到这里,你已经成功跑通了第一个 Laya 风格决策模型。整个过程没有引入任何复杂框架,核心代码完整可见,适合作为后续扩展的基座。
4.8 接入大模型意图识别的扩展思路
上面的示例用的是关键词规则,如果你的业务需要开放域意图识别,可以在 IntentParser 中增加一个基于 LLM 的实现。核心思路是:保留现有接口,新增一个 LlmIntentParser 类,内部调用模型 API 完成文本分类。
kotlin复制class LlmIntentParser(
private val apiEndpoint: String,
private val apiKey: String
) : IntentParserBase {
override fun parse(request: DecisionRequest): Intent {
// 1. 构造 Prompt,告知模型输出固定枚举值
// 2. 调用模型 API
// 3. 解析返回结果并映射到 Intent 枚举
// 4. 如果模型调用失败,降级到规则解析
}
}
这里不绑定具体的模型 API,是因为各家接口差异较大。你需要根据自己当前使用的模型服务调整。在实际工程中,大模型负责“理解”用户意图,决策模型负责“决定”动作序列,两者合作才能形成完整的 Agent 能力。
5. 常见问题与排查思路
跑通示例只是第一步,真实项目中一定会在环境、编码、依赖、并发等层面踩坑。下面整理几个高频问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
Gradle 运行报错 Unresolved reference: kotlin |
Kotlin 插件版本与 Gradle 版本不兼容 | 检查 build.gradle.kts 中 Kotlin 插件版本,降级或升级到官方兼容版本 |
| 中文乱码 | 控制台编码不是 UTF-8 | 在 build.gradle.kts 中配置 systemProperty("file.encoding", "UTF-8"),并确认 IDE 编码设置为 UTF-8 |
| 决策结果与预期不符 | 关键词规则过于简单或上下文状态被污染 | 为 IntentParser 增加单元测试用例,覆盖每个正则关键词;检查 ContextStore 中的状态是否在错误时机写入 |
| 并发请求导致上下文错乱 | ConcurrentHashMap 无法保证跨请求的业务隔离 |
为 requestId 增加独立的上下文 key 前缀,或引入基于 TraceId 的上下文传递 |
| 动作执行部分失败但整体流程不停止 | 决策模型没有定义失败降级策略 | 在 ActionExecutor 中定义失败动作的重试、跳过或终止策略 |
| 决策逻辑越写越复杂 | 所有规则堆在 planActions 中 |
将规则表外部化到统一配置,例如使用 JSON 定义“意图 -> 动作列表”的映射 |
下面是两个比较典型的排查场景展开说明。
场景 A:一切编译正常,但是输出中文全部是乱码。
这种问题大多数出在编码配置上。Windows 环境默认编码可能是 GBK,而 Kotlin 源码和 Gradle 都默认 UTF-8。建议在 build.gradle.kts 中添加如下配置:
kotlin复制tasks.withType<JavaExec> {
systemProperty("file.encoding", "UTF-8")
}
同时也检查 IDE 右下角文件编码是否为 UTF-8。
场景 B:连续执行多次,决策结果出现预料之外的变化。
大概率是上下文存储没有按请求维度隔离。在我们示例里,ContextStore 的 restart_count 是全局的,所有请求共享。真实项目中应该按服务维度、用户维度或请求 ID 维度独立存储,避免不同业务的上下文互相干扰。比如把 store 的 key 设计成:
kotlin复制contextStore.put("${serviceName}:restart_count", restartCount.toString())
这样才能做到业务隔离。
6. 最佳实践与工程建议
示例代码跑通后,接下来要思考的是如何把 Laya 决策模型应用到生产环境。下面这些建议来自我在业务系统里沉淀下来的经验,优先级从高到低排列。
6.1 决策与执行严格分层
决策模型的代码仓库中,我会用包名强制约束层次:
text复制core/ # 决策编排,只负责“做什么”
action/ # 动作执行,只负责“怎么做”
intent/ # 意图识别,只负责“想做什么”
任何跨越层级的逻辑都视为坏味道。比如,在 ActionExecutor 里直接判断 intent,或者在 IntentParser 里写业务计算,都应该被拆回正确的位置。分层清晰之后,决策模型的单元测试会好写很多。
6.2 决策过程和执行结果必须可观测
线上排查问题时,最痛苦的就是“不知道为什么走了这个分支”。所以在设计决策结果时,至少包含以下字段:
- 决策路径
decisionPath:命中了什么规则。 - 动作明细
actions:规划了哪些动作。 - 执行结果
actionResults:每个动作的成功/失败情况。 - 耗时信息:从接收请求到完成处理的耗时。
对应的日志建议采用结构化格式,比如 JSON。一旦接入日志平台,就可以直接搜索某个 requestId 的完整决策链路。
6.3 规则配置外部化
planActions 方法中的 if-else 在小规模场景下没问题,但一旦决策分支超过十个,代码就会变得难维护。建议把规则表抽成配置文件,例如:
json复制{
"SERVICE_RESTART": [
{ "action": "LOG", "description": "记录重启请求" },
{ "action": "RESTART_SERVICE", "description": "重启服务" }
],
"MANUAL_REVIEW": [
{ "action": "LOG", "description": "记录人工介入请求" },
{ "action": "NOTIFY_HUMAN", "description": "通知值班人员" }
]
}
运行时由配置中心动态加载。这样当产品经理要求调整某个场景的流程时,不需要发版,只需要变更配置。
6.4 定义降级策略
任何外部依赖都可能故障,所以决策模型必须有降级路径。我有三条原则:
- 大模型不可用时,降级到关键词规则。
- 规则也不命中时,走默认的
UNKNOWN分支,转人工兜底。 - 所有降级事件都要有日志和监控告警,不能静默失败。
这与我们示例中的设计是一致的:UNKNOWN 意图会转人工,而不是直接丢给用户一个错误。
6.5 做好上下文隔离与生命周期管理
上下文存储是决策模型最容易被忽略的隐患。要特别注意:
- 必须设置过期时间,防止内存无限增长。
- 必须按业务维度隔离 key,防止跨业务污染。
- 请求处理结束后,确认哪些上下文需要保留、哪些需要清理。
最稳妥的方案是引入分布式缓存(比如 Redis)并设置 TTL,而不是像示例这样使用进程内 Map。示例只是为了跑通流程,不作为生产方案。
6.6 为决策模型编写单元测试
决策模型非常适合做单元测试,因为它的输入输出都是结构化的。下面给出一个示例测试代码的位置:
文件路径:src/test/kotlin/com/example/laya/LayaDecisionModelTest.kt
kotlin复制package com.example.laya
import com.example.laya.core.LayaDecisionModel
import com.example.laya.model.DecisionRequest
import com.example.laya.model.Intent
import kotlin.test.Test
import kotlin.test.assertEquals
class LayaDecisionModelTest {
@Test
fun `重启服务请求应该产生 RESTART 动作`() {
val model = LayaDecisionModel()
val request = DecisionRequest(
requestId = "test-1",
inputText = "order-service 响应缓慢,需要重启服务",
context = mapOf("service" to "order-service")
)
val result = model.decide(request)
assertEquals(Intent.SERVICE_RESTART, result.intent)
assertEquals(2, result.actions.size)
}
}
有了这样的测试,后续修改决策逻辑时,就能快速判断是否破坏了原有行为。建议把每个意图分支、每种边界情况都覆盖到。
7. 总结与学习路线
这篇文章围绕 Laya 决策模型,完整走了一遍“概念理解 -> 环境准备 -> 原理拆解 -> 代码实现 -> 运行验证 -> 问题排查 -> 工程建议”的流程。核心收获可以归纳为四点:
第一,决策模型的本质是“输入到动作序列的映射”,它介于意图理解和业务执行之间,属于智能体系统里的编排层。
第二,一个最小可用的决策闭环必然包含请求解析、意图识别、决策规划、动作执行、结果汇总五个环节。每个环节的职责边界要清晰,不要混写。
第三,示例代码中用上下文状态演示了动态决策:同样的意图在不同上下文下会产出不同动作序列。这是决策模型比固定 if-else 更有价值的关键点。
第四,生产环境落地时,优先关注可观测性、规则外部化、降级策略和上下文隔离,而不是一味追求模型能力强弱。决策链路跑得稳,比跑得快更重要。
下一步,你可以尝试把示例中的 IntentParser 替换成大模型接口,体验一次“LLM 理解用户意图 + 决策模型编排工具调用”的完整 Agent 流程。也可以把规则表迁移到配置中心,实现决策逻辑的动态发布。这两个方向是当前 JVM 生态中比较热门的实践路径。
如果这篇文章对你有帮助,建议自己动手把代码敲一遍,尤其是 planActions 里的动态决策逻辑,只有亲手跑过一遍,才能真正理解决策模型的运转方式。
