拿回Agent决策权:用状态机与护栏让LLM不再失控

发布时间:2026/10/14 23:55:46
拿回Agent决策权:用状态机与护栏让LLM不再失控
最近在折腾一个叫 Embabel 的小项目核心就一句话把 Agent 的决策权从 LLM 手里拿回来。说得再直白一点就是不再让大模型自由决定“下一步干什么”而是把流程路由、状态迁移、动作合法性这些关键判断全部收回到确定性代码里。LLM 只负责它擅长的部分理解用户意图、抽取关键信息、生成自然回复。这个方向跟我平时做 Agent 时的体感高度一致——凡是让 LLM 全权做决策的 Agent大概率会在某个意想不到的环节跑飞。这篇学习笔记会从“为什么要拿回决策权”讲起拆一拆 Embabel 的核心机制再给一段完整实操记录。适合正在做 Agent 应用、被模型“自作主张”坑过、或者想搞懂 LLM 之上到底该套什么控制逻辑的开发者。1. 先想明白Agent 为什么会失控Embabel 的解题思路是什么1.1 失控的根源不是“模型笨”而是它不适合做全局决策先回忆一下典型的 Agent 跑飞现场用户说了一句“帮我查一下 A 供应商的资料”模型理解成“用户想下单”直接把采购订单给创建了又或者流程走到第 5 步模型突然绕过了第 3 步的身份确认直接调了支付接口。这些问题不是模型能力不够而是 LLM 天生就不适合承担“全局决策”这个职责。原因有几个。第一LLM 是概率生成模型它输出的不是“正确动作”而是“概率上比较像的动作”。在开放对话里这种模糊性没问题但放到业务系统里一次低概率但致命的输出就是事故。第二上下文窗口是有限的模型一次只能看到局部状态它根本不知道整个流程已经走到哪一步、哪些前置条件已经满足。第三越长的对话上下文越容易发生注意力漂移前面明确说过的“必须确认客户身份”这个硬规则很可能在后续交互中被模型遗忘。我用一个具体例子复盘过一个售后工单处理 Agent模型在某个会话中完全跳过了“校验客户是否在服务期内”这个环节直接给了“免费换新”的承诺。业务侧事后追责时发现模型没有任何“决策记录”没人能说清楚它当时为什么这么判断。这就是把决策权完全交给 LLM 的代价——它给不出可解释、可回放、可校验的决策链路。1.2 Embabel 的解题思路把决策拆成三层而不是一刀切Embabel 的做法不是“完全不用 LLM”而是把决策分层。类比一下公司运作老板定战略方向经理在规则范围内做局部判断员工执行具体动作。把这三层混在一起让一个人既定方向又做执行必然出乱子。具体到 Agent 系统三层划分是这样的战略层规则与人工定义目标、约束、状态机。比如“下单前必须验证用户身份”“单笔金额超过 5000 元必须转人工审批”。这些是不可协商的硬约束由代码实现。战术层LLM 受限发挥面对模糊的用户输入模型负责理解意图、抽取参数、生成回复。但它不能直接决定“要不要下单”“要不要支付”它的输出被限制成“只能在当前状态下可选动作里选一个并填充参数”。执行层函数与工具实际的外部操作比如查资料、发通知、写数据库。这一层只接受经过校验的指令。这种分层的核心价值在于模型永远没有“最终拍板权”只有“提案权”。真正的决策动作状态迁移、副作用调用必须经过确定性代码的校验才能放行。拿回的不是所有判断而是那些从概率模型视角看“赌不起”的判断。1.3 什么算“决策权”具体要拿回哪些我把 Agent 系统里常见的决策点列了一个清单分两类必须拿回交给确定性逻辑的流程路由下一步走哪个分支不能由模型临场发挥状态迁移从“待确认”到“已下单”这种状态变化必须符合状态机定义权限与合规校验能不能访问某个数据、能不能执行某个副作用动作终止条件什么时候结束任务、什么时候转人工需要明确阈值资源访问控制模型不能决定“我能看哪些数据”这由代码控制可以放心留给 LLM 的用户意图理解把自然语言映射成结构化意图信息抽取从对话里提取订单号、日期、数量等参数内容生成回复话术、摘要、润色局部推理在给定约束的小范围内做语义判断这个清单是 Embabel 设计的基础。判断标准很简单如果这个决策错了会造成不可逆的副作用钱、权限、数据、承诺就必须拿回来如果只是影响表达质量可以留给模型。2. Embabel 核心机制拆解动作白名单、状态机与护栏2.1 动作白名单LLM 只能做选择题不能做填空题之外的发挥Embabel 的第一个关键机制是“动作白名单”。传统的 Agent 实现里模型可以自由决定调用哪个工具这等于让一个充满想象力的人随便使用一屋子危险工具。Embabel 的做法是给当前状态定义一个“可执行动作集合”模型只能从中选择并且选择结果在后端还要再过一遍校验。举个例子一个“文档问答代下单” Agent 的动作白名单可能长这样ACTIONS_BY_STATE { intent_collecting: [search_docs, ask_for_more, verify_user], user_verified: [search_docs, create_order, send_notice], order_confirming: [confirm_order, abort_order, send_notice], executing: [send_notice, mark_done], }模型输出动作时先经过一层“枚举校验”这个动作必须在当前状态的白名单里。如果模型输出delete_order而当前状态是intent_collecting直接拒绝并让模型重新生成。为什么这层校验特别重要因为提示词约束只是“软约束”模型完全可能在一次输出里产生动作幻觉。我在实操时遇到过模型在无任何前兆的情况下输出一个从未定义过的动作后端的白名单校验直接拦住了避免了事故发生。白名单之外还有一个关键设计每个动作都带前置条件guard和后置钩子hook。前置条件用来校验“现在能执行这个动作吗”后置钩子用来在动作执行后做状态清理或日志记录。这样动作执行被双保险包裹模型只是启动了流程真正的判断在代码层。2.2 有限状态机接管流程路由每一步都有确定的下一跳第二个核心机制是状态机。Embabel 把 Agent 的整个生命周期定义成一个有限状态机FSM每个状态都规定了合法的迁移路径。这样做的好处非常直接整个流程在任意时刻都知道“我在哪、我能去哪、我不能去哪”而不是把所有流程知识塞进模型的上下文里。我用状态迁移表来说明。以下是一个简化版本当前状态合法下一状态触发条件initialintent_collecting用户输入到达intent_collectinguser_verified / human_review用户身份校验通过 / 无法确认身份user_verifiedorder_confirming用户表达下单意图且参数完整order_confirmingexecuting / aborted / human_review用户确认 / 用户取消 / 金额超限executingdone / human_review执行成功 / 执行异常human_reviewdone / aborted人工处理完成模型在这个过程中扮演的角色是什么它不会直接输出“进入 order_confirming”而是输出“用户想下单参数是……”。然后代码层根据当前状态和模型输出的动作决定是否发生状态迁移。如果模型输出的期望动作为create_order但当前状态是intent_collecting状态机就会拦截迁移请求要求模型先推进到身份校验状态。这种设计的另一个好处是可测试性。状态迁移表本身就是一种可以写单元测试的契约。我把整个 Agent 的状态迁移路径全部用测试用例覆盖输入同样的状态和模型输出永远得到同样的迁移结果。这在纯 LLM 决策方案里是不可想象的。2.3 护栏机制决策前必须过校验而不是决策后补救Embabel 的第三个机制是护栏Guardrail也就是在每个关键动作执行前做强制校验。我把它分成硬校验和软校验两种。硬校验是纯规则判断不掺入任何模型判断。比如“当前用户是否登录”“订单金额是否小于限额”“动作是否在白名单内”。这些校验直接写在代码里速度极快也不依赖模型输出。凡是涉及钱、权限、身份的操作必须过硬校验。硬校验失败时动作会被直接拦截并产生一条告警。软校验是模型加规则的混合方案。比如“用户这句话是否包含威胁性内容”“这个请求是否属于高风险场景”这些语义层面的事情没法用纯规则覆盖但仍然不能让模型“自由决定是否放行”。Embabel 的做法是模型对语义风险进行打分规则设定阈值。得分低于阈值正常放行超过阈值自动进入人工复核队列连续 N 次触发阈值直接熔断整个 Agent 暂停只允许人工介入。这里有一个容易遗漏的细节校验失败后不要原地重试太多次。我在使用中给自己定的规则是同一决策点最多重试 2 次第 3 次仍失败就转人工。否则会出现“模型反复生成、代码反复拦截”的死循环Token 消耗成倍增加用户体验还极差。2.4 决策留痕没有日志的决策等于没有决策最后一个重要机制是“决策留痕”。每次模型的决策输出、每次护栏校验结果、每次状态迁移全部写进不可变日志。日志至少包含当前状态、模型输出的原始内容、校验结果、命中或拦截原因、决策时间戳、上下文快照或上下文哈希。为什么这个机制很重要因为拿回决策权之后Agent 的行为必须能做到事后可回放。哪天线上出问题了你可以顺着日志还原整个决策链路“模型在这个状态输出了 X护栏因为 Y 条件拦截了它然后流程进入了 human_review。”这种确定性带来的安全感和可维护性是纯 LLM 决策方案永远给不了的。我在做线上问题排查时80% 的疑案都是靠日志回放解决的而不是靠“重新问一遍模型”。3. 实操把“文档问答自动执行” Agent 改造成可控决策链路3.1 场景设定与改造目标下面用一段完整实操来展示 Embabel 的具体落地过程。场景是一个“资料查询代下单” Agent用户可以向它咨询产品资料也可以在确认信息后代为创建采购订单。改造前这个 Agent 是典型的全 LLM 决策模型直接决定调哪个工具、什么时候调。问题很明显——出现过几次“用户还没表达下单意愿模型就把订单创建了”的低级错误。改造目标很明确只有在“用户明确表达下单意图 身份已确认 金额在限额内”这三个条件全部满足时才允许调用下单接口其余情况下Agent 只能在查资料、追问、请求身份验证这些动作里选一个改造思路就是套用 Embabel 的“白名单 状态机 护栏”组合拳。3.2 定义状态机与动作集首先定义状态机。为了让读者可以直接抄作业我给出一个精简版实现用 Python 枚举和字典来配置from enum import Enum class State(Enum): INITIAL initial INTENT_COLLECTING intent_collecting USER_VERIFIED user_verified ORDER_CONFIRMING order_confirming EXECUTING executing DONE done HUMAN_REVIEW human_review ABORTED aborted class Action(Enum): SEARCH_DOCS search_docs ASK_FOR_MORE ask_for_more VERIFY_USER verify_user CREATE_ORDER create_order CONFIRM_ORDER confirm_order ABORT_ORDER abort_order SEND_NOTICE send_notice MARK_DONE mark_done然后配置“每个状态允许的动作集合”和“下一跳映射”VALID_ACTIONS { State.INITIAL: [Action.SEARCH_DOCS], State.INTENT_COLLECTING: [Action.SEARCH_DOCS, Action.ASK_FOR_MORE, Action.VERIFY_USER], State.USER_VERIFIED: [Action.SEARCH_DOCS, Action.CREATE_ORDER, Action.SEND_NOTICE], State.ORDER_CONFIRMING: [Action.CONFIRM_ORDER, Action.ABORT_ORDER, Action.SEND_NOTICE], State.EXECUTING: [Action.SEND_NOTICE, Action.MARK_DONE], } NEXT_STATE { (State.INTENT_COLLECTING, Action.VERIFY_USER): State.USER_VERIFIED, (State.USER_VERIFIED, Action.CREATE_ORDER): State.ORDER_CONFIRMING, (State.ORDER_CONFIRMING, Action.CONFIRM_ORDER): State.EXECUTING, (State.ORDER_CONFIRMING, Action.ABORT_ORDER): State.ABORTED, (State.EXECUTING, Action.MARK_DONE): State.DONE, }注意几个设计要点INITIAL状态只开放查询动作用户在还没有表达明确意图时无论如何不可能触发下单状态迁移只能由代码层根据当前状态和合法动作来推进涉及订单创建的动作不直接在INTENT_COLLECTING状态里开放必须经过身份校验状态之后才可能出现。3.3 接入 LLM 决策模块的完整流程接下来是核心部分LLM 输出如何接入这套框架。这里的难点是“既要让模型处理自然语言又不让它越权决策”。我用分段梳理的方式讲整个流程。第一步组装受限提示词。提示词模板里明确写出当前状态、可用动作、约束条件并要求模型只输出结构化结果。示例模板如下system_prompt f 你是一个受限决策模块。当前状态{state.value} 当前状态允许的动作{[a.value for a in valid_actions]} 用户输入{user_input} 历史摘要{context_summary} 请输出 JSON不要输出其他内容 {{intent: 用户意图简述, action: 从允许动作中选择一个, params: {{关键参数}}, confidence: 0.0-1.0}} 如果用户输入不足以执行任何动作请选择 ask_for_more 并给出需要补充的问题。 第二步模型输出解析与结构校验。模型返回的文本必须被解析成 JSON并做结构校验。如果解析失败、缺少字段、或者 action 不在允许列表内视为一次无效决策。无效决策不直接放行而是给模型一次“纠错重发”的机会把错误信息返回给模型要求重新生成。但这里要遵守前面定的规矩最多重试两次。第三步护栏校验。模型输出合法、action 也在白名单内还不能直接执行。每个动作还绑定了 guard 函数比如CREATE_ORDER的 guard 是“用户已确认下单内容且金额未超限”。全部校验通过后才真正执行动作并推进状态。下面给一个核心路由函数的实现示意这是整个 Embabel 框架里最关键的一段代码from dataclasses import dataclass dataclass class RouteResult: ok: bool next_state: State reason: str def route(state: State, llm_output: dict, guards: dict) - RouteResult: action llm_output.get(action) params llm_output.get(params, {}) # 校验1动作是否为合法枚举 try: action Action(action) except ValueError: return RouteResult(False, state, reasoninvalid_action) # 校验2动作是否在当前状态白名单内 if action not in VALID_ACTIONS[state]: return RouteResult(False, state, reasonaction_not_allowed_in_state) # 校验3动作前置条件是否满足 if action in guards: guard guards[action] if not guard(params, state): return RouteResult(False, state, reasonguard_failed) # 校验4状态迁移是否合法 if (state, action) not in NEXT_STATE: return RouteResult(False, state, reasontransition_not_allowed) return RouteResult(True, NEXT_STATE[(state, action)], reasonok)这段代码体现了 Embabel 的核心哲学模型输出只是一个需要被多轮验证的“提案”而真正的决策权在一系列确定性校验函数手上。每个校验点都返回结构化的拒绝原因方便后期分析和告警。3.4 改造前后的对比效果改造完成后我用同一批历史会话做了回放测试。下表是一个简化的效果对比用数据说话指标改造前LLM 全权决策改造后Embabel 分层决策非法下单调用次数测试集 200 会话11 次0 次平均单次决策响应耗时1.8 秒1.2 秒单会话平均 Token 消耗24501680人工介入率6%3.5%Token 消耗下降的原因很简单状态机把很多路由判断从模型身上拿走了模型只需要输出动作和参数不需要反复解释“为什么选这个步骤”。响应耗时下降是因为很多意图分流不再需要完整生成过程规则层直接判定了。而人工介入率下降是因为之前很多“模型犯傻被业务打回”的情况消失了流程变得可控业务侧反而更信任 Agent。3.5 护栏与人工兜底的具体配置最后一个实操环节是配置护栏参数。我自己常用的配置模板是这样的金额阈值单笔订单超过 5000 元直接转 human_review不做任何自动放行身份校验下单前必须校验成功校验失败转人工不重试置信度阈值模型输出的 confidence 低于 0.6转入 human_review熔断规则连续 3 次 guard 失败整个 Agent 暂停并告警这些参数不是什么魔法数字而是根据业务风险承受力调出来的。我给的建议是核心业务动作的 guard 越“笨”越好用最朴素的 if 判断不要用模型做二次判断。只有人工兜底这一步才需要人的参与。4. 踩坑实录与常见问题排查技巧4.1 模型仍然输出白名单之外的 action这是最常见的坑。提示词里明明写了“只能从列表中选择”模型还是可能在输出里出现一个从未定义过的动作。我的排查经验是不要指望提示词约束来保障安全必须靠结构校验和“重新生成一次”的兜底。实操方案解析模型输出时如果 action 非法把合法动作列表和错误信息拼接成一条修正提示让模型重新生成。如果重试两次后依然非法直接转人工。这条规则写死以后线上非法动作的发生率基本归零。记住模型输出是概率分布任何单次输出都有可能偏离预期但你的代码可以把偏离的影响限制在可控范围内。4.2 状态机太严格导致流程僵化把决策权拿回来之后另一个极端出现了状态机设计太严格用户稍微绕了一下话题Agent 就频繁转人工体验变得很生硬。举个例子用户问完产品资料后随口说“你们会不会有优惠活动”如果状态机强制要求先迁移到“身份校验”才能回答就会显得非常呆板。我的解法是区分“硬状态”和“软状态”。核心业务动作下单、支付、权限变更必须走硬状态机一步都不能跳而普通对话、查资料、闲聊回复放在软状态里不需要严格的迁移约束。通俗点说流程的关键节点要硬但非关键路径要宽容。状态机的目的是控制风险不是限制表达能力。4.3 每次决策都调 LLM成本和延迟翻倍有些场景其实是规则就能搞定的没必要每次都动用模型。比如用户输入“你好”或者“查一下 A 产品的价格”这些高确定性意图可以直接用规则分流。Embabel 的改进方案是加一个前置路由层先跑规则引擎命中确定性规则就直接走对应处理逻辑只有规则无法判断时才调用模型做意图理解。我测下来这种“规则优先、模型兜底”的策略能把模型调用次数减少 40% 左右响应速度也明显更快。另一个优化点是给决策结果做缓存同一用户、同一状态、同一种输入在一定时间窗口内直接复用上一次的决策结论减少重复计算。4.4 如何验证“决策权真的拿回来了”一个很实用的验证方法是“决策回放差异报告”。把历史会话记录重新喂给新老两套策略对比关键决策点的行为差异。我在实操中比较关注三个指标非法动作触发次数、状态迁移不符合预期的次数、以及转人工比例的变化。如果新策略在这些指标上都明显改善说明决策权确实收回来了如果指标没变化说明你的状态机设计和原方案在行为层面没有本质区别需要重新审视状态划分。这种方法比单纯看线上效果更靠谱因为线上环境的随机因素太多。回放测试可以保证同时期、同输入下新旧策略的可比性是验证这类系统改造效果的有效手段。4.5 常见问题速查表问题现象排查方向推荐解法模型输出了非法动作动作白名单是否覆盖当前状态后端正则校验 重试一次 转人工兜底状态机频繁转人工状态划分是否过细引入软状态放宽非关键路径Token 消耗居高不下LLM 调用是否过于频繁前置规则分流 决策缓存线上行为不一致决策日志是否完整完善决策留痕做回放对比模型低置信度导致误判guard 阈值设置不合理分场景调整阈值核心动作阈值从严5. 延伸思考什么时候可以放开 LLM什么时候必须拿回决策权5.1 适合完全交 LLM 的低风险、重表达、无副作用不是所有 Agent 都需要 Embabel 这套东西。如果场景是纯内容生成、纸面方案的头脑风暴、闲聊陪伴没有外部副作用没有资源和资金损耗那么让 LLM 全权决策反而是更优解。自由度和创造力在这种场景里是收益不是风险。我自己的判断标准是“三无原则”无真实资源消耗、无不可逆操作、无权限边界跨越。三者都满足就让模型自由发挥任何一个不满足就需要引入确定性决策链路。5.2 必须拿回决策权的钱、权、承诺、数据凡是涉及以下高危要素的场景决策权必须回到代码手里资金操作下单、支付、退款、优惠计算权限变更账号权限修改、数据访问授权对外承诺客服机器人向用户承诺解决方案或赔偿敏感数据访问、导出、删除用户数据状态持久化修改数据库里的关键业务字段为什么这些必须拿回来因为这些操作的错误代价不可逆。LLM 的单次输出错误率可能只有 1%但乘以每天百万次请求就是一万次事故。而确定性代码加状态机可以把这 1% 的错误在到达业务系统之前拦截掉。需要强调一句1% 的错不是“模型不够聪明”而是概率生成的固有特性你不可能靠换一个更大的模型来消除它。5.3 决策权分配是可以动态调整的我最后想说的是一种更细腻的实践决策权分配不一定要“全有或全无”。Embabel 这套体系的优势在于你可以给不同场景配置不同的决策策略。比如在灰度期只把下单动作收回到代码层其余动作仍然放给模型等到验证稳定了再把更多高风险动作收回来。也可以给不同用户配置不同阈值新用户的下单限额低一些老用户的信任级别高一些。这种动态调节能力需要决策链路本身就具备可配置性最好是像前面实现那样把状态和动作的定义写成配置而不是硬编码在业务流程里。最后分享一点个人体会把决策权从 LLM 手里拿回来之后Agent 看起来确实“不聪明”了不再像以前那样能各种旁路操作。但有意思的是业务方反而更喜欢这种“不聪明”的 Agent因为它可以解释、可以测试、可以追责。模型该发挥创造力的地方我们在单个局部节点里仍然放开给它但涉及核心业务动作必须让流程秘书严格把关。如果你也想改造手里的 Agent我建议不要一次性推翻原有方案而是先挑一个风险最高的动作比如下单、删除、付款把它收回到代码层配上状态机和护栏跑一段时间看效果。一次只收一个动作容易控制风险也容易总结经验。等这一套跑顺了再逐步扩大范围。决策权拿回来之后你会发现 Agent 反而变得更可靠、更好维护也更像一个能被业务信任的工具。