1. 为什么我想把决策权从LLM手里拿回来最近半年我一直在折腾 Agent 编排陆续用主流大模型 API 搭过客服工单处理、数据分析助手、内容自动回复这类内部工具。项目越做越多一个感觉越来越强烈让 LLM 直接做决策短期看很惊艳长期看是灾难。于是我在一个内部实验项目里换了一条路项目代号叫 Embabel核心就一句话把 Agent 的决策权从 LLM 手里拿回来。这篇文章是我的完整学习笔记从动机、设计、实操到踩坑全写在这里给同样被 Agent 不可控问题困扰的开发者做个参考。先说清楚我说的决策权到底指什么。在一个典型的 Agent 链路里模型不仅要负责生成回复还要负责选择调用哪个工具、决定流程下一步怎么走、甚至在多个分支里自己判断该往哪条路走。这些都属于决策。而 Embabel 的思路正好反过来让 LLM 只负责生成内容所有下一步做什么的决定都由代码控制。听起来像退化但实际跑完一轮对比实验之后我得出的结论是——很多时候退化才是正路。1.1 决策型 Agent 的三宗罪幻觉、失控、成本先讲三个最让我头疼的问题。第一是幻觉驱动决策。LLM 在事实不确定的情况下依然会用非常自信的语气给出一个决定。比如一个工单分类任务模型对某个模糊描述拿不准按概率分布硬选了一个分类结果还选错了。如果只是生成回复说错了用户还能纠正但如果是决策说错了就意味着整条流程走偏后面每一步都在错误基础上继续放大。第二是失控。模型自主调用工具时很容易陷入循环查了一次结果不满意又查一次换了关键词再查一次同一个动作反复执行直到把 token 耗尽或触发超时。我在日志里看到过一次循环同一个查询工具被连续调用了 5 次中间没有任何收敛迹象感觉就像模型自己跟自己较劲。第三是成本。决策层每多一次判断都要把工具返回结果重新拼进上下文再发一次模型请求。按输入 token、输出 token、工具返回 token 三部分叠加计算一次带工具调用的任务成本往往是不带工具调用的 2 到 3 倍。我统计过某个实验任务决策型 Agent 平均单任务发起了 2.3 次模型调用而规则路由只需要 0.2 次——光这一项就差了 10 倍以上。这三件事本质上是同一个根因决策这件事天然要求可解释、可复现、可回滚恰好是概率模型最不擅长的事。生成一段文案模型发挥不确定性没关系但决定这笔钱该不该退没人敢接受随机性。1.2 一个真实到让人尴尬的工单场景我最初下定决心做 Embabel是因为一次很尴尬的线上事故。当时我负责的客服工单 Agent 在处理用户申请退款这类请求时LLM 负责两件事给工单分类以及决定是否调用客户订单查询工具。结果模型把一张退款工单错误路由到了技术支持分组并且开始反复调用订单查询工具。每次查询返回之后模型总觉得信息不够又发起下一次查询。我在日志里看到同一句话对应的 Prompt 被完整执行了 5 遍工具返回的数据一次次被拼接、丢弃、再拼接直到超时。最后是靠人工介入才把流程拽回来。更让我无法接受的是我事后追问模型为什么这么决策它给出的解释和它实际做的不完全一致。那一刻我意识到如果一个决定错了之后连原因都没法准确复盘那就不该让模型来定。Embabel 的起点就是这次事故。我给自己定了个规矩模型可以建议但是不可以决定。所有路由、流转、动作选择的最终拍板权交给代码。2. Embabel 的核心理念决策层与生成层分离Embabel 不是要把 LLM 从架构里踢出去而是重新划分职责。整个系统分三层策略层、控制层、生成层。策略层是大脑全部由确定性代码构成包括 if else 规则、决策树、有限状态机。控制层是手负责把策略层的决定翻译成具体动作索引管理状态转移并调用外部服务。生成层才是 LLM 待的地方它只接收已经被策略层切好的小块任务做摘要、写回复、提取字段然后交出结构化结果。最关键的一点是调用关系被反转了。传统 Agent 架构里LLM 拿着工具清单自己决定调用哪个函数Embabel 里LLM 手里什么工具都没有它只是被控制层调用的一块内容生成服务。2.1 LLM 只负责说不负责决定的分层设计用一个生活化的类比来解释以前你请了个代理人把银行卡、钥匙、公章全交给他让他替你做所有决定。结果他乱花钱、走错门、盖错章你只能在旁边干着急。Embabel 的做法是把他手里的所有权限收回来只留给他一支笔和一张表——他负责填表你负责审批。在 Embabel 中LLM 的笔是输出特定格式的 JSON表是控制层定义好的任务规范。比如客服工单场景里模型不需要思考该调哪个工具它只需要回答这段用户诉求应该归到哪个候选分类且分类列表是代码里显式给出的。能不能调用工具、调用哪个工具、调用之后能不能进入下一步全部由策略层决定。这个设计的直接收益是决策路径变成了一条可以打印、可以回放、可以断言的链路。每次决策进入哪个分支为什么进入代码里一目了然。出问题时我能拿出日志说它因为命中规则 27 所以进了退款流程而不是模型说它觉得应该在退款流程。2.2 策略路由与模型路由的分界一张表讲清楚很多团队纠结要不要用 LLM 做 Agent 决策其实本质上是没分清策略路由和模型路由的边界。我把两者的差异整理成了一张表维度策略路由确定性决策模型路由LLM 决策决策依据代码规则、状态机、显式策略概率分布、上下文语义可解释性每步可打印、可断言需要 Prompt 解释不稳定可复现性同输入必得同输出温度 0 也只是相对稳定成本接近 0每次决策消耗 tokens维护方式人工梳理规则、测试覆盖改 Prompt、反复试错适合场景高频、高风险、流程固定低频、开放、语义模糊分类主要风险规则僵化、覆盖不全幻觉、死循环、越权调用我的经验公式是把 80% 的高频确定性决策交给策略层剩下 20% 的模糊判断交给模型做候选排序但最终决定权还是回到策略层。也就是说模型可以给出这个工单更像是退款还是投诉的概率分布但路由到哪个分组、触发什么动作这件事依然由代码根据分布结果做最终裁决。3. 在 Embabel 里拿回决策权的四步实操理论讲完直接上实操。我总结了四步按顺序做完基本上就能把一个模型想干嘛就干嘛的 Agent改造成模型只能在管道里干活的系统。3.1 第一步把动作空间收窄成显式动作表第一步是重新定义 Agent 能做什么。传统 Agent 会把工具函数直接暴露给模型这让模型拥有任意调用的权力。Embabel 的做法是把工具变成一张动作表表中每一行都是一个动作包含动作 ID、描述、输入结构、前置条件、允许执行的上下文。{ action_id: query_order, description: 查询订单状态, input_schema: { order_id: string }, preconditions: [user_verified], allowed_roles: [customer_service] }动作表有两个作用。第一个是限制模型根本无法触达表中不存在的动作想越权也没有入口。第二个是归一化所有动作入口都是统一的 ID控制层拿到 ID 后先去校验校验通过才执行。动作表本质上是一份白名单它把模型能做什么从模糊的能力空间变成了明确的、可枚举的集合。我在第一版实现里曾经偷懒让 LLM 直接输出动作名称字符串想着反正模型能看懂。结果模型输出了好几个不存在的动作 ID控制层直接懵了。后来改成从列表中选一个的枚举输出并加上严格校验这类错误才归零。3.2 第二步用确定性规则引擎替换让模型想一下动作表定义好了之后第二步是把决策过程从调用模型判断改成执行规则引擎。def decide_routing(ticket): if ticket.refund_requested and ticket.amount 0: return route_to_refund if ticket.priority urgent: return route_to_manual_approval if keyword_hit(ticket.content, [技术, 接口, 500]): return route_to_tech return ask_llm_for_category这段代码看起来朴素但它代表了一个关键的转向原来这些判断要花一次模型调用现在只需要几毫秒和零 token。规则是确定性的可以写单元测试可以覆盖边界条件。为什么不像很多人那样让 LLM 直接做分类因为规则能覆盖的场景没有必要交给概率模型。退款金额、优先级字段、关键词命中这些判断完全可以通过代码表达。只有规则覆盖不到、语义模糊、需要权衡多种信号的场景才值得动用模型。我给自己定了一条原则每新增一条规则必须写明它覆盖的场景。当规则开始互相重叠、难以解释时就该引入决策树或者专用规则引擎而不是继续堆 if else。3.3 第三步关键节点插入人工确认与状态机光靠规则还不够真正把决策权锁死的关键是引入状态机。我在 Embabel 里给退款流程定义了一张状态表所有工单严格按照状态迁移规则流转当前状态触发条件下一状态允许执行的动作open创建工单awaiting_classification生成分类建议awaiting_classification分类确认awaiting_approval生成退款建议awaiting_approval外部审批通过approved执行退款approved退款结果返回done发送回执open任意时刻canceled停止处理这张表的重点是LLM 建议立即退款并不能让工单进入执行状态。它只是生成了一段文字建议真正的状态迁移必须由外部审批信号触发。没有人工确认工单永远停在 awaiting_approval。这等于把最后拍板权从模型手里硬生生抽了出来放到业务规则和人工操作里。状态机还有一个好处它让整个流程的合法性可以被检查。我可以断言状态 X 不可能直接跳到状态 Z一旦日志里出现非法迁移说明控制层有 bug而不是模型抽风。这种把不确定性锁在格子间里的设计是 Embabel 最核心的安全机制。3.4 第四步为 LLM 输出加校验-重试-降级链路最后一步是给 LLM 的生成能力装上约束。模型即使只负责生成也可能输出格式错误、缺少字段、甚至干脆拒绝回答。Embabel 的做法是给每个生成任务配一段校验-重试-降级链路。for attempt in range(max_retries): content llm_generate(task) if validate(content): return parse(content) return {status: degraded, fallback: manual_reply}校验部分用结构化 Schema 定义字段必填、类型、枚举范围允许值只能来自动作表。重试次数我最终设为 2原因后面踩坑章节会细讲。降级路径完全不依赖 LLM固定文案加转人工。也就是说就算模型彻底罢工Agent 也不会卡死而是走兜底通道。这一步极其重要因为它是拿回决策权的最后一道保险。哪怕前面所有规则都失效、状态机被绕过至少还有一层校验把模型的坏输出挡在系统之外。4. 实测数据成本、延迟与成功率的变化理论说得再好落地还得看数据。我把同一套客服工单处理任务跑成了一组对比实验在完全相同的语料和指标口径下比较三种配置的差异。4.1 实验环境与三种对比配置实验语料是 1000 条模拟工单包含退款、技术故障、投诉、普通咨询和无法归类的垃圾信息。判定成功的标准是工单最终进入正确状态组并且用户诉求得到了有效答复。比如退款工单必须走完审批通过并执行退款技术工单必须被路由到技术组并使用状态查询动作。三种配置分别是配置 A纯 LLM 决策。模型自主判断分类自主调用工具自主决定流程下一步。配置 BLLM 决策加规则兜底。模型先给分类若命中高风险场景再由规则纠正。配置 CEmbabel 策略主导。规则和状态机承担全部决策LLM 只做回复生成和模糊分类建议。所有配置都使用同一个主模型 API温度设为 0。成本以配置 A 的单任务平均消耗为基准折算成百分比避免涉及具体金额。4.2 三组配置的指标对比配置任务成功率平均单任务耗时单任务 tokens 成本人工介入率决策可解释性纯 LLM 决策86.2%4.8 秒100%基准12%低LLM 决策 规则兜底92.5%3.1 秒65%7%中Embabel 策略主导97.4%1.2 秒24%3%高三个数字最让我意外。第一是成本配置 C 的 token 消耗只有基线的 24%约等于省了四分之三。原因很简单绝大多数工单走了规则路径一次模型调用都没发生。第二是延迟平均单任务从 4.8 秒降到 1.2 秒体感差距极大。第三是成功率不是持平而是从 86% 涨到了 97%说明约束不仅没让 Agent 变笨反而减少了错误决策对流程的破坏。人工介入率从 12% 降到 3%这里有个隐藏信号原本需要人工兜底去纠正模型错误现在人工只需要处理真正的业务异常。团队里最贵的人力终于从给模型善后变成了处理真正有价值的问题。4.3 代价与边界不是所有场景都该把决策权拿回来数据好看不代表这条路适合所有项目。Embabel 拿回决策权的前提是你非常清楚自己的业务流程能画出状态图能列出动作表。如果你的场景连人自己都说不清下一步该往哪走那规则也无从写起。不适合的场景主要有三类。第一类是开放式创意生成比如让模型写营销文案这本来就不需要路由和决策。第二类是高度非结构化的语义理解比如意图识别中同时存在几十种细分类且互相交叉规则很难覆盖。第三类是业务规则每天都在变规则体系维护成本会迅速反超收益。针对第三类我目前的习惯是规则覆盖 80%、模型兜底 20%的比例不变但定期复盘高频模型调用日志。如果一个分支频繁触发模型兜底说明规则该补了反过来如果一个规则长期命中率极低说明该删。拿回决策权不是一锤子买卖而是一个持续双向调节的过程。5. 我在学习 Embabel 过程中踩过的坑这一节全是实际操作里的教训。Embabel 的设计思路不复杂真正难的是各种边边角角的坑每一个我都花了不少时间才爬出来。5.1 过度规则化把 Agent 写成了死脚本第一次搭策略层时我犯了一个典型错误想用规则覆盖所有情况一口气堆了 200 多条 if else。结果很快出现规则互相覆盖某工单同时命中退款规则和技术规则代码按顺序执行命中了先写的那条而先写的那条恰好是不合适的。工单又被路由错了。后来我意识到规则系统不是越多越好而是越有结构越好。我把 200 多条平铺规则按业务意图重组为决策树每条路径只有一个出口并为存在冲突的规则显式设置优先级和互斥条件。重构成决策树之后规则总量下降到 60 条左右覆盖率没有下降反而因为去掉了互相覆盖的冗余逻辑准确率提升了。血泪经验规则超过 50 条且有交叉逻辑时别再用 if else 硬堆果断上决策树或状态机。判断标准是——当你需要给同事解释某条规则为什么会生效时结构化的表达会永远比平铺的 if else 好讲得多。5.2 结构化输出解析里的隐性错误LLM 即使只负责生成也会在结构化输出上捣乱。最常见的是 JSON 解析失败多了逗号、注释、markdown 代码块包裹、中英文引号混用、字段名里莫名出现的空格。普通解析器一遇到这种东西就抛异常整条链路直接崩溃。我的解法是加一个宽容解析层先尝试标准解析失败则做一轮清洗——去掉注释、规整引号、剥离 markdown 标记然后再解析。即便如此实测仍有大约 5% 的生成结果需要二次清洗且这个比例在不同模型之间差异很大。另一个隐蔽问题是模型偶尔会输出合法但语义不对的值。比如要求枚举动作 ID模型输出了一个不在动作表里的字符串但格式完全合法Schema 校验不会拦截。这时候必须加业务规则过滤不仅格式要对值还必须存在于白名单中。总之解析层永远不要把格式合法当成内容可靠。5.3 状态机与外部信号对接时的竞态问题这个坑花了我最长时间定位症状很诡异同一张工单偶尔会重复进入执行状态导致退款动作执行两次。排查到最后问题出在外部审批信号的重复推送。控制层收到一次确认信号就触发状态迁移但如果信号源因为网络重试、重复点击、消息队列重复消费等原因发了两次状态机就会把同一个合法迁移执行两遍。解决方案不复杂给每条外部信号加一个全局事件 ID并在状态迁移前做幂等检查同一事件 ID 只允许触发一次迁移。同时状态迁移函数必须先判断当前状态是否允许该迁移不允许就直接拒绝并记录日志。改完之后这类重复执行问题彻底消失。这个教训让我记住了状态机的严谨性不只在状态定义更在于迁移动作的幂等性。5.4 对不做决定的兜底模型沉默比乱说更可怕LLM 除了乱输出还会拒绝输出。某些情况下模型会以安全理由拒绝生成回复或者干脆返回空内容、超时。在传统 Agent 里模型沉默只是少一段字在 Embabel 里如果生成层卡住整个流程就卡在等待状态比乱决策还难办因为你连日志里该记什么都没得记。我的处理分两条线。第一条线是控制层超时机制LLM 生成超过 15 秒没返回直接放弃并走降级通道不让它无限拖。第二条线是降级内容降级不是简单地说一句系统繁忙而是给用户一个明确标准回复并把工单标记为需要人工处理插入待办队列。这样系统至少能保证流程闭环不会因为生成层故障导致工单消失。说到底拿回决策权不只是为了防模型乱来更是为了确保整个系统在模型故障时还能保持基本可用。最后再说一点我的体会整个 Embabel 实验做完我最深的一个感受是拿回决策权并不是否定 LLM 的价值而是重新给它在系统里定位。LLM 依然负责生成内容、提炼摘要、给出候选建议它仍然是不可或缺的生产力工具但它不再是那个承担责任的代理人。责任应该由代码、状态机和业务规则来承担因为只有这些确定性因素才能在出错时被追溯、被修复、被测试。如果你也想在项目里做类似的改造我建议按这个顺序动手先梳理动作清单再画状态机最后才去调 Prompt。很多人一上来就优化提示词希望模型更听话但问题的根源往往不在提示词而在流程本身没有边界。把边界画清楚之后你会发现模型根本不需要那么聪明它只需要稳定地做一个输出部件就好。最后分享一个小技巧在控制层给每条决策路径生成一个唯一日志 ID把工单为什么走进这条路完整打出来。几次迭代之后你会发现真正的分歧点从来不在模型能力而在业务规则没理清。规则理清了LLM 的决策权交不交出来都是一件很自然的事——反正大多数时候你根本不需要它来拍板。