1. 复盘主题(系统问题)
为什么 Agent 不能完全依赖 LLM 的自由推理循环?
具体表现:在 FitMind 的 ReAct 实现中,单纯让 LLM 决定"下一步做什么"会引发路由崩坏、步骤死锁与副作用误触发,最终必须在 Planner 与 Router 之间嵌入多层确定性保护,才能让多工具 Agent 安全上线。
2. 背景问题(Why)
FitMind 是一个面向个人健康管理的 AI 助手,用户输入极其口语化且意图混杂,比如:
- "感觉今天吃咸了,会不会水肿?"
- "帮我把刚刚那顿记一下,大概一碗米饭、两个鸡腿。"
- "练完腿之后膝盖不舒服,正常吗?"
系统需要完成的动作跨越:
- 自然语言查询(检索健康知识)
- 结构化数据查询(查运动/饮食日志、用户档案)
- 数值计算(BMI、热量估算)
- 写库操作(记录饮食/运动)
显然,单一工具无法覆盖所有请求,必须采用多步推理与工具组合。ReAct(Reasoning + Acting)范式天然适合:让模型在循环中思考(Thought)、选工具(Action)、读结果(Observation),直到形成最终答案。
但问题在于:用户请求的开放性与系统状态的严肃性之间存在根本矛盾。我不能允许模型随意调用写库工具,也不能让一个简单的 BMI 查询陷在 5 步推理中消耗大量 Token 和时间。
3. 原始方案(Initial Design)
最初设计极其"信任 LLM":
- 所有用户消息首先经过一个 LLM Router,它判断"是否需要多步处理"以及"路由类型(顺序/并行)"。
- 如果需要,就交给 ReAct Planner ,Planner 内部维持一个无约束的 Thought-Action-Observation 循环,直到模型输出
final_answer。 - 唯一的安全措施是一个较大的
MAX_STEPS = 10,用来防止真的跑飞。
架构草图(原始):
User Input → LLM Router → [Simple / Complex]
↓(Complex)
ReAct Planner (LLM 自由循环)
↓
ToolRegistry → 真实工具执行
我当时的假设:
- LLM Router 能准确理解用户的真实意图。
- ReAct 循环中,模型会合理利用 Observation,不会卡死或重复。
- 工具本身都是幂等的或可重放,风险可控。
4. 遇到的问题(Core Problem)
问题 1:路由误判触发写操作
用户输入:
"昨天我吃够 2000 卡了吗?"
Router(LLM)将其分类为 需要多步处理 ,因为句子包含"吃"和卡路里数值,触发了"记录饮食"的倾向。进入 ReAct 循环后,模型第一步就调用了 log_diet,试图写入一条只有日期和卡路里目标但没有具体食物的记录。这导致了脏数据,并且用户实际想问的是"查询昨天的饮食记录并汇总热量"。
危险原因:写操作被无确认执行,且 Router 的错误判断将查询意图扭曲为修改意图。
问题 2:ReAct 循环中的步骤死锁
用户在对话中途问:
"那我现在的 BMI 是多少?"
Planner 生成的步骤:
- Thought: 需要身高体重 → Action:
get_user_profile→ Observation: {height: 175, weight: 80} - Thought: 现在可以计算 BMI → Action:
calculate_bmi→ Observation: {bmi: 26.1} - Thought: 嗯,我还需要确认一下最新的体重,可能变化了 → Action:
get_user_profile→ Observation: (同前) - Thought: 好,再算一次 → Action:
calculate_bmi→ Observation: (同前)
然后循环,直到 MAX_STEPS 耗尽。
危险原因 :模型在获得足够信息后,没有推进到 final_answer,反而陷入无意义的重复查询。虽然数据没坏,但延迟巨大,用户体验极差,Token 成本剧增。
问题 3:简单的单一工具查询走了长链推理
用户问:"帮我算下 BMI"。
Router 判定为复杂问题,启动 ReAct。Planner 执行了:
- Thought: 需要身高体重 →
get_user_profile - Thought: 得到数据了,计算 →
calculate_bmi - Thought: 现在可以回答了 →
final_answer
总共 3 步,但其实整个流程本可以压缩成一次直接工具调用,无需语言模型反复思考。这导致响应时间从 1 秒拉长到 5 秒,且浪费计算。
5. 问题根因分析(Root Cause)
表面看是 prompt 不够好或者模型能力不足,但本质是架构层面的信任边界错误。
LLM 本质是概率性的下一个 token 生成器,它没有内建的状态机来保证:
- 步骤 唯一性(是否已经执行过同一动作)
- 进展性(每一步是否推动任务更接近目标)
- 副作用安全(是否应该执行不可逆操作)
在标准 ReAct 论文中,这些可以通过环境给予的 reward 或强大的基础模型隐式解决,但在真实产品中,我无法承受概率性的失败。尤其是写库、发送通知等有副作用的动作,必须由确定性代码把关。
因此,问题不是"LLM 不够聪明",而是架构错误地将流程决策权完全交给了概率模型。
6. 最终方案(Final Design)
我重构为 "双层路由 + 带围栏的 ReAct 循环"。
架构图
User Input
│
▼
┌───────────────┐
│ Rule Router │ ← 确定性层:关键词/模式匹配
│ (deterministic)│
└───────┬───────┘
│ 未命中规则
▼
┌───────────────┐
│ LLM Router │ ← 概率层:语义分类 (SEQUENTIAL/PARALLEL/DIRECT)
└───────┬───────┘
│ 需要多步推理
▼
┌──────────────────────────────┐
│ ReAct Planner (受控循环) │
│ - executed_sigs 去重检查 │ ← 确定性保护
│ - MAX_STEPS=5 │
│ - JSON 容错退出 │
└──────────────────────────────┘
│
▼
ToolRegistry (含副作用工具隔离)
各层职责
Rule Router(确定性层)
根据预定义模式直接分流:
- 匹配 "算下BMI" → 直接调用
calculate_bmi并返回,不启动 ReAct。 - 匹配 "记录饮食"+ 明确食物 → 可进入确认流程,但不进入自由 ReAct。
这样保证了高频明确请求的响应速度和安全性。
LLM Router(语义层)
仅当规则无法覆盖时才启用,利用 LLM 理解模糊意图,返回路由决策。但即便路由到 ReAct,其后续步骤依然受保护。
ReAct Planner 内嵌围栏
- 步骤去重 :
executed_sigs记录已经执行过的action:params签名,若检测到完全相同的调用,立即中止循环并返回"检测到重复操作,请重新组织请求"。 - 硬性步数限制 :
MAX_STEPS = 5,足够覆盖"查询档案→计算→搜索知识→合成答案"的典型链,同时避免无限徘徊。 - 输出合规性检查:要求 LLM 输出严格 JSON,解析失败则记录错误并安全退出,不执行任何动作。
为什么这样设计?
- 确定性层兜底:把最危险和最确定的路径固化,不受模型波动影响。
- 概率层处理长尾:保持灵活性,但同时被围栏约束,不会失控。
- 分层解耦:Router 与 Planner 的保护机制独立演进,互不干扰。
7. Trade-off(代价)
新方案显著提升了稳定性和安全,但引入了明显的代价:
| 维度 | 代价 |
|---|---|
| 维护成本 | Rule Router 需要持续更新关键词库,容易出现冲突或遗漏,需要定期分析线上日志来补全规则。 |
| 灵活性受限 | 严格的步数限制和去重机制可能阻断某些真正需要重试的场景(如"刚才网络错误,再查一次"会被误判为重复动作)。 |
| 规则与 LLM 冲突 | 某些模糊表达可能被 Rule Router 强行截胡,导致本应由 LLM 深入理解的需求被简单化处理。 |
| 调试复杂度 | 多层决策导致 trace 变长,定位问题需要同时检查规则命中日志和 LLM 决策日志。 |
这些代价都是为了换取确定性必须支付的,而且从线上稳定性来看,完全值得。
8. 为什么不用其他方案(架构判断)
-
为什么不用纯 LLM Router + 无保护 ReAct?
前面问题已证明:不可控。副作用安全、死循环、成本三方面都无法接受。产品级系统不能建立在"模型应该不会出错"的假设上。
-
为什么不用纯规则系统?
FitMind 的核心价值在于理解"今天随便吃了点"这样的自然语言,纯规则无法穷举饮食表达的多样性。语义歧义必须用 LLM 解码。
-
为什么不用固定 workflow(比如 LangChain 预设链)?
用户意图无法在编码时预判全部。固定链在处理组合查询时(如同时需要知识检索和用户数据)显得僵化,且会退化成一个巨大的 if-else,最终不可维护。
所以混合路由 + 受控 ReAct 是当前条件下最平衡的方案。
9. 和 AI 系统设计的关系(高价值抽象)
这次重构让我深刻意识到一个普遍法则:
在 AI 原生系统中,LLM 的最佳角色是"策略提议者",而不是"状态控制器"。
任何涉及关键状态转换、事务性写入、安全边界的环节,都必须由确定性代码来最终拍板。这与自动驾驶中的"感知-规划-控制"分层类似:LLM 可以作为"规划层"提供候选动作,但"控制层"必须是确定性的,它根据规则和 safety monitor 来决定是否真的执行。
这也是为什么在 Agent 架构中,我们常会看到Guardrails、沙箱、审批流、human-in-the-loop 等机制------它们的本质都是为了给概率性推理穿上确定性的铠甲。
10. 我的新理解(认知变化)
以前我以为:
Agent 开发的核心是选好模型、写好 prompt、把工具描述清楚,剩下的让 ReAct 循环自己跑就行。
现在我发现:
真正困难的不是让 LLM 生成 Thought/Action,而是如何设计一套不依赖模型自控力的安全网。包括:
- 哪些路径可以走捷径?
- 如何防止模型在循环中迷失?
- 写操作如何与人确认?
- 失败如何降级而不是硬崩?
Agent 的工程难点,80% 在于"约束 LLM",而不是"释放 LLM"。
11. 如果重新设计
如果今天从头开始,我会做以下几项调整:
-
在 Router 层引入置信度评分
让 LLM Router 输出一个 confidence,低于阈值时主动转向澄清反问,而不是硬着头皮走进 ReAct。
-
将步骤去重升级为语义去重
当前的
executed_sigs只是字符串匹配,如果模型换了等价参数(如weight=80vsweight=80.0)就会绕过。应该用工具定义的幂等键来做更智能的去重。 -
提前设计带外审批的写操作
一开始就把
log_diet等工具做成二段式:生成记录 → 用户确认 → 真实写入。目前是重构后才加上的,成本更高。
12. 后续优化方向
- 长期记忆压缩:当对话较长时,Observation 历史堆叠导致 prompt 膨胀,模型更容易忽略早期信息。计划加入记忆摘要模块,定期将旧 trace 压缩成简明的状态描述。
- 离线路由评估:收集线上 Router 决策和最终用户反馈,定期分析路由错误率,驱动规则库更新。
- 更细粒度的状态机:将 ReAct 循环变成显式状态机(如:收集信息→推理→确认动作→执行→合成答案),每个状态对应不同的 prompt 和安全规则,进一步降低模型越权风险。
13. 一句话总结
这是"确定性工作流与概率性推理的协同问题"------可靠 Agent 的关键,不是 LLM 有多聪明,而是我为它的不确定性修建了多少条确定性的沟渠。