系统设计复盘:为什么 Agent 的 ReAct 循环必须内嵌确定性保护层——以 FitMind 健康助手的路由与步骤控制为例
1. 复盘主题系统问题为什么 Agent 不能完全依赖 LLM 的自由推理循环具体表现在 FitMind 的 ReAct 实现中单纯让 LLM 决定“下一步做什么”会引发路由崩坏、步骤死锁与副作用误触发最终必须在 Planner 与 Router 之间嵌入多层确定性保护才能让多工具 Agent 安全上线。2. 背景问题WhyFitMind 是一个面向个人健康管理的 AI 助手用户输入极其口语化且意图混杂比如“感觉今天吃咸了会不会水肿”“帮我把刚刚那顿记一下大概一碗米饭、两个鸡腿。”“练完腿之后膝盖不舒服正常吗”系统需要完成的动作跨越自然语言查询检索健康知识结构化数据查询查运动/饮食日志、用户档案数值计算BMI、热量估算写库操作记录饮食/运动显然单一工具无法覆盖所有请求必须采用多步推理与工具组合。ReActReasoning Acting范式天然适合让模型在循环中思考Thought、选工具Action、读结果Observation直到形成最终答案。但问题在于用户请求的开放性与系统状态的严肃性之间存在根本矛盾。我不能允许模型随意调用写库工具也不能让一个简单的 BMI 查询陷在 5 步推理中消耗大量 Token 和时间。3. 原始方案Initial Design最初设计极其“信任 LLM”所有用户消息首先经过一个LLM Router它判断“是否需要多步处理”以及“路由类型顺序/并行”。如果需要就交给ReAct PlannerPlanner 内部维持一个无约束的 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 卡了吗”RouterLLM将其分类为需要多步处理因为句子包含“吃”和卡路里数值触发了“记录饮食”的倾向。进入 ReAct 循环后模型第一步就调用了log_diet试图写入一条只有日期和卡路里目标但没有具体食物的记录。这导致了脏数据并且用户实际想问的是“查询昨天的饮食记录并汇总热量”。危险原因写操作被无确认执行且 Router 的错误判断将查询意图扭曲为修改意图。问题 2ReAct 循环中的步骤死锁用户在对话中途问“那我现在的 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_profileThought: 得到数据了计算 →calculate_bmiThought: 现在可以回答了 →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_STEPS5 │ │ - 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只是字符串匹配如果模型换了等价参数如weight80vsweight80.0就会绕过。应该用工具定义的幂等键来做更智能的去重。提前设计带外审批的写操作一开始就把log_diet等工具做成二段式生成记录 → 用户确认 → 真实写入。目前是重构后才加上的成本更高。12. 后续优化方向长期记忆压缩当对话较长时Observation 历史堆叠导致 prompt 膨胀模型更容易忽略早期信息。计划加入记忆摘要模块定期将旧 trace 压缩成简明的状态描述。离线路由评估收集线上 Router 决策和最终用户反馈定期分析路由错误率驱动规则库更新。更细粒度的状态机将 ReAct 循环变成显式状态机如收集信息→推理→确认动作→执行→合成答案每个状态对应不同的 prompt 和安全规则进一步降低模型越权风险。13. 一句话总结这是“确定性工作流与概率性推理的协同问题”——可靠 Agent 的关键不是 LLM 有多聪明而是我为它的不确定性修建了多少条确定性的沟渠。