Agent决策权从LLM手里拿回来:状态机+规则引擎的硬骨架实践
如果把Agent的最终决策权交给LLM你迟早会在某个深夜收到一条线上事故告警。我在自己的Agent项目里踩过这个坑一个看起来简单的意图路由场景LLM上一轮还在规规矩矩执行工作流下一轮就开始自由发挥把本该转人工的高风险客诉转给了商品推荐技能还一本正经地给出了一段道歉话术。那次之后我彻底想明白了一件事Agent的决策链路需要一条显式的轨道规则先于模型状态先于生成。这也是我整理Embabel学习笔记的起点——把Agent的决策权从LLM手里拿回来。这套思路不是要废掉LLM而是要把它按回合适的位置。LLM仍然负责理解、生成和部分开放域判断但在关键的路由、状态流转、风险拦截、资源分配这些决策点上用显式的规则和流程结构来接管。说白了就是给Agent装上硬骨架让它在既定轨道里发挥而不是靠一次生成碰运气。这篇文章不会讲太多抽象概念更多是我在项目里落地的过程为什么做、怎么做、踩了哪些坑以及路线图的取舍。适合谁来看如果你正在做Agent相关的工程化项目被LLM的“随机发挥”搞得头疼或者你想在团队里推动一套更可控的Agent架构这篇笔记应该能给你一些实际参考。1. 决策权外移Agent链路里最难缠的问题先说结论LLM生成能力越强你越要小心它被用在决策路径上。很多人搭Agent的第一版就是“把所有输入扔给一个强模型让模型决定下一步”。这看起来灵活实际上是在用模型的下一次采样赌业务正确性。1.1 LLM做决策的三个结构性弱点LLM做决策的第一问题是不可复现。同一个Prompt温度调到0也只代表大概率稳定不代表每一次都稳定。业务场景里一旦涉及数字、顺序、多条件组合模型很容易在边界条件上给出不同答案这在决策链路里是要命的。第二个问题更隐蔽——上下文污染。拿决策来说模型读到一段充满负面情绪的用户输入时生成的下一步动作很容易被情绪带偏。客服场景里用户语气不好时模型倾向转人工语气温和但实际是高危投诉时模型反而倾向于安抚话术多聊几句。这种“看人下菜”在对话场景看着有温度在决策场景就是事故温床。第三个问题是决策依据不可审计。LLM的推理链条是隐式的即便你让它输出思考过程那也只是模型对自己决策的一种事后合理化解释和实际影响决策的权重未必一致。一旦项目需要追责、复盘、合规审查你拿不出一份能说服人的决策记录。我那个客诉转接事故复盘时唯一能确认的是模型觉得“用户没骂人所以不需要转人工”但这个判断依据没法落到规则表里下次它又可能觉得“用户提了退款就该转人工”。1.2 决策链路的坐标系确定性、可观测性、成本把Agent的决策问题拆开看其实就三个维度确定性同一输入是否得到同一决策、可观测性决策依据能否被审查、成本每次决策调用LLM的Token开销。LLM直控决策在三个维度上的表现都不理想确定性靠温度和Few-shot撑着随时会破防可观测性靠Prompt约束约束不住模型的内部推理成本则随调用次数线性上涨。而规则引擎和状态机在这三个维度上是天然占优的。确定性来自代码逻辑可观测性来自规则命中的Log记录成本则是本地计算几乎为零。不过规则方案也有明显短板适配不了开放式场景。用户随口一句“这破手机充不进去电”你怎么写规则这时候还是要LLM来做意图理解和信息抽取。所以正确的姿势是分层——LLM负责“听懂”和“生成”规则层负责“决定往哪走”。Embabel给我的启发就是把这句话落成工程结构听和做分离决策从模型采样里抽出来。1.3 什么场景必须把决策权拿回来不是所有Agent场景都需要这套改造。如果你的Agent只是玩具Demo怎么折腾都行。但以下几个信号一旦出现你就该认真考虑决策权外移了第一个信号是事故可解释性不足。出了线上问题只能回滚Prompt或微调数据拿不出明确的责任边界。第二个信号是可操作性差。业务要加一个“晚上十点之后不再处理改签请求”的策略结果你发现改Prompt并不能稳定生效因为模型记不住你说的时间限制只对这条生效。第三个信号是状态管理混乱。Agent多轮任务里已经完成节点和待处理节点搞不清全靠Prompt里的历史摘要硬撑一旦摘要超长或遗漏整个流程就断。我自己的经验是一旦Agent要对接真实业务系统工单、订单、库存、支付决策权必须从模型手里拿回来。业务系统要求的是确定性、可回滚、可追踪这和LLM的概率本质天然冲突。把两者之间加一层显式的决策中间层才是工程上稳的做法。2. Embabel的核心设计硬编排与软决策双轨Embabel带给我的第一个认知冲击是它没有把决策逻辑做成一套庞大的集中式规则库而是做成了“硬编排 软决策”的双轨结构。这个设计和传统工作流引擎很不一样值得展开聊聊。2.1 双轨制的思路拆解所谓硬编排指的是Agent的主流程骨架用DAG有向无环图或状态机来表达。每个节点是什么类型、节点之间的流转条件、哪些节点允许并行、哪些节点是串行屏障这些全部是显式定义写死在配置里不走模型生成。这部分解决的是流程确定的“主干道”问题。软决策则是留给LLM的弹性空间。硬编排定义了大方向但在某些节点里需要一些开放判断时由LLM产出候选选项再由决策层用规则做最终确认或否决。比如意图分类节点可以从五六个候选意图里让模型做初步筛选但命中“高危投诉”“退款纠纷”这类意图时规则层直接强制路由模型的选择被覆盖。这个双轨设计的好处是责任清晰流程骨架错了改配置即可模型判断错了查规则决策记录即可。不会出现那种“流程和模型纠缠在一起出了问题都不知道该改哪一边”的情况。类比一下硬编排好比地铁轨道软决策好比车厢里的乘客选座位——乘客可以选坐左边还是右边但决定不了地铁下一站往哪开。2.2 状态机不是所有状态都该让LLM驱动在Embabel的思路里状态机的状态迁移有两类来源一类是显式事件触发比如用户点击“确认支付”按钮后系统收到支付回调状态从“待支付”迁移到“支付中”另一类是自然语言理解触发比如用户说“我要退款”LLM抽取出退款意图然后系统置为“退款处理中”。第一类迁移完全不需要LLM参与第二类则需要LLM先做意图理解但迁移动作本身仍然由规则层执行。我最早踩的坑就是把第二类做成了“LLM直接返回新状态”结果模型偶尔返回一个状态机里根本不存在的状态比如把“退款处理中”写成了“退款处理终态没有下一步”状态机直接原地死锁。后来参照Embabel的做法把所有状态迁移的合法性判断收拢到一处LLM可以提议迁移但迁移是否合法由状态表决定。状态表里定义了每个状态允许的下一状态集合不在集合内的迁移请求一律拒绝并回退同时记录一次模型异常输出。这样即便模型的意图理解出错也不会把整个流程带崩。2.3 决策点抽象把“判断”变成可配置项Embabel第二个启发点是决策点的抽象方式。它把流程中每一个需要判断的地方都抽象成独立的决策点每个决策点有输入参数、判断规则、输出分支运行时统一走一个决策服务。拿订单售后Agent举例里面就有几个典型的决策点意图分类决策、风险等级评定决策、自动处理还是转人工决策、退款金额是否在授权范围内决策。把这些拆成独立的决策点之后每个点的逻辑可以单独测试和优化不用每次改一个判断逻辑就把整个Agent重新验证一遍。做成配置的好处还在于业务人员能参与进来。运营同学可以直接改配置表里的“自动退款上限金额”而不需要理解Agent是怎么调LLM的。这个对项目长期迭代的帮助非常大团队协作的边界一下子清晰起来。同时每个决策点跑完都会落一条记录输入参数、命中规则、最终分支、是否覆盖过模型结果。审计复盘时拉起这条链路就一目了然。3. 实操落地给Agent装上外部决策骨架这部分直接进入实操。我用一个模拟的售后客服Agent来演示怎么把决策权从LLM手里拿回来。整体结构是状态机管流程、显式规则管关键判断、LLM只做理解和生成。下面的代码不是完整生产实现但骨架可以直接参考。3.1 定义决策状态与流程骨架先把流程状态设计和代码结构拆分看。完整的实现里状态定义、流程定义、决策规则定义分别是三个文件这里为了可读性合并展示。# states.py from enum import Enum class OrderAgentState(str, Enum): INIT init # 初始化 INTENT_CLARIFY intent_clarify # 意图确认 RECEIVED received # 已受理 REFUND_REVIEW refund_review # 退款审核 HUMAN_HANDOFF human_handoff # 转人工 CLOSED closed # 已完结 ESCALATED escalated # 已升级 # transitions.py ALLOWED_TRANSITIONS { OrderAgentState.INIT: {OrderAgentState.INTENT_CLARIFY, OrderAgentState.RECEIVED, OrderAgentState.HUMAN_HANDOFF}, OrderAgentState.INTENT_CLARIFY: {OrderAgentState.RECEIVED, OrderAgentState.HUMAN_HANDOFF}, OrderAgentState.RECEIVED: {OrderAgentState.REFUND_REVIEW, OrderAgentState.HUMAN_HANDOFF, OrderAgentState.CLOSED}, OrderAgentState.REFUND_REVIEW: {OrderAgentState.CLOSED, OrderAgentState.HUMAN_HANDOFF, OrderAgentState.ESCALATED}, OrderAgentState.HUMAN_HANDOFF: {OrderAgentState.CLOSED, OrderAgentState.ESCALATED}, OrderAgentState.ESCALATED: {OrderAgentState.CLOSED}, OrderAgentState.CLOSED: set(), }注意到INTENT_CLARIFY状态的设计当LLM对用户意图的置信度不够高时Agent不是直接猜测而是进入一个“意图澄清”状态用一次反问圈定边界。这个小改动把LLM误判意图的风险挡在流程入口处。状态迁移表里每个状态的可达后续状态都是预先定义好的LLM没有任何权力把一个状态随便迁移到另一个状态。3.2 决策规则优先LLM输出只做候选核心的决策服务在拿到LLM输出后不是直接执行而是先过规则层。这里给出一个意图决策的实现示意# intent_decider.py from dataclasses import dataclass dataclass class DecisionResult: decision: str ruled_by: str # llm_candidate | hard_rule detail: str def decide_intent(user_input: str, llm_candidate: str, profile: dict) - DecisionResult: # 1. 绝对规则优先命中高危词直接覆盖 HIGH_RISK_KEYWORDS [投诉, 起诉, 媒体曝光, 12315, 报警] for kw in HIGH_RISK_KEYWORDS: if kw in user_input: return DecisionResult( decisionhuman_handoff, ruled_byhard_rule, detailf高危关键词命中: {kw} ) # 2. 业务策略规则金额相关走强约束 REFUND_TRIGGER_WORDS [退款, 退钱, 退货退款] if any(w in user_input for w in REFUND_TRIGGER_WORDS): # 强制进入退款审核不被LLM候选干扰 return DecisionResult( decisionrefund_review, ruled_byhard_rule, detail退款意图关键词命中 ) # 3. 置信度门槛LLM候选意图低于阈值时不接受其自主决策 confidence profile.get(confidence, 0.0) if confidence 0.65: return DecisionResult( decisionintent_clarify, ruled_byhard_rule, detailf候选置信度{confidence:.2f}低于阈值进入澄清流程 ) # 4. 其余情况接受LLM候选但仍走白名单校验 if llm_candidate not in {close_order, general_query, product_info}: return DecisionResult( decisionhuman_handoff, ruled_byhard_rule, detailfLLM候选不在白名单内: {llm_candidate} ) return DecisionResult(decisionllm_candidate, ruled_byllm_candidate)这段代码体现了“软决策”和“硬规则”的合作方式LLM仍然在跑但它的每一步输出都要过规则闸门。ruled_by字段用来记录最终决策是谁定的方便事后审计。规则责任边界在这里非常清晰——高危词命中、退款意图识别这类重要节点规则直接拍板不给LLM发挥的空间只有低风险通用查询才放行给LLM候选。我实际跑这个方案时最先体验到的改善是排障效率。以前出问题要翻Prompt、翻模型输出猜测是哪里被带偏了现在直接看决策日志命中哪条规则一目了然。有一次排查到凌晨三点最后发现是规则库里退款关键词的匹配范围写窄了漏掉“我不想要了”这种口语退款表达改一行关键词配置就解决完全不用动模型。3.3 让LLM输出结构化并接受否决决策权外移不代表LLM变成一个纯粹的文本生成器它在链路里仍然有不可替代的作用。关键是要强迫模型输出结构化数据并且让下游规则层具备否决权。# llm_extract.py def extract_intent_with_llm(user_input: str, candidate_enum: list[str]) - dict: prompt f你是意图识别模块只输出JSON不要多余解释。 候选意图: {candidate_enum} 用户输入: {user_input} 输出格式: {{intent: 意图标识, confidence: 0.0, reason: 一句话依据}} 只允许从候选意图中选择无法判断时输出: {{intent: unknown, confidence: 0.0, reason: 信息不足}} # 调用模型后解析并校验JSON结构 raw_output call_llm(prompt) result parse_and_validate_json(raw_output) return result # 使用侧 candidate_intents [close_order, refund_request, general_query, human_handoff, product_info] llm_result extract_intent_with_llm(user_input, candidate_intents) decision decide_intent(user_input, llm_result[intent], llm_result)这段实现里最关键的是把模型的作用限制为“抽取候选”而非“做决定”。即便模型输出了强烈倾向“直接退款”的意图规则层也会先检查用户输入是否真的包含退款触发词没有就不放行。模型可以解释它为什么这么判断但解释改变不了规则的结果。实测下来用这种方式还有一个附加好处用户的投诉倾向不会被模型“温和化”掉。LLM天然有“顺应对话语气”的倾向用户好好说话它就倾向低风险哪怕实际诉求是高危的。规则层直接根据关键词匹配结果强行转人工彻底绕开这种“语气误判”问题。3.4 完整链路里的LLM调用边界状态机和决策权外移之后LLM的调用点其实只剩下三类入口意图候选抽取、会话回复生成、特殊字段抽取。用一个调度循环把它们串起来# agent_loop.py def agent_loop(session_ctx, user_input): current_state session_ctx.current_state while True: # 状态机校验当前状态允许的动作 if current_state OrderAgentState.INIT or current_state OrderAgentState.INTENT_CLARIFY: llm_proposal extract_intent_with_llm(user_input, candidate_intents) decision decide_intent(user_input, llm_proposal[intent], llm_proposal) if decision.ruled_by hard_rule: log_decision(session_ctx.session_id, decision) next_state ACTION_TO_STATE[decision.decision] if next_state not in ALLOWED_TRANSITIONS[current_state]: # 规则说可以但状态机不允许强制回退澄清 next_state OrderAgentState.INTENT_CLARIFY session_ctx.current_state next_state current_state next_state continue if current_state OrderAgentState.RECEIVED: # 只做回复生成不做任何决策判断 reply generate_reply_with_llm(session_ctx, user_input) return reply if current_state OrderAgentState.REFUND_REVIEW: # 退款金额校验是规则判断 if is_amount_authorized(session_ctx.refund_amount): current_state OrderAgentState.CLOSED return 您的退款申请已通过预计3个工作日内到账。 else: current_state OrderAgentState.HUMAN_HANDOFF return 您的退款金额超出自动处理范围我来为您转接人工专员。这个循环把LLM的调用边界卡得很死。LLM永远不能直接改变状态只能给候选状态迁移的表结构、触发关键词、金额阈值全都落在代码和配置里。回复生成虽然还是LLM来做但那是在状态定下来之后执行的训练数据再怎么散漫影响也只是话术差异不会带偏引擎。这套结构跑了一百多天最明显的变化是对模型的信任边界清晰了模型只能在授权范围内发挥超出即否决。4. 选型与避坑哪些方案看起来能用但千万别上头写到这里分享几类实际踩过或见人踩过的替代方案思路。它们单看都有自己的合理性但放到“收回决策权”这个目标下一对比短板就很明显。4.1 方案对比状态机、规则引擎、树模型、Fine-tuning第一类是纯规则引擎全家桶所有东西都写代码。这种方案的最大问题是难以应对灵活表达。用户说“我手机坏了想退货”你以为规则能命中“退货”换个说法“这东西不行我不要了”规则就傻眼。光靠正则和关键词做一个Demo没问题覆盖真实流量会很吃力。第二类是Fine-tuning模型来学决策。不少人觉得“我直接把决策逻辑攒成几千条样本微调一下让模型自己学会规则不就行了”。这个方案看起来保留了LLM的灵活度但代价是决策逻辑被揉进了模型参数里业务改一条规则就等于重新训练一轮迭代成本不可控可解释性也大幅下降。作为实验可以放进生产链路等着挨打。第三类是树模型和评分卡方案。把决策特征抽出来之后用GBDT之类的模型做优先级排序这适用于风险决策场景比如判断退款单是不是高危单。但它和LLM的前置理解能力是割裂的要额外做特征工程运维成本高。Agent场景下我还是更倾向“状态机硬规则白名单”的组合因为Agent的决策核心是流程编排不是概率排序。第四类就是回到纯LLM决策加一堆Prompt约束和Few-shot。这种方案看起来最轻量但确定性无法兜底。只要LLM输出一次不合法的状态迁移或一个白名单外的分支选择整个流程就断给你看。Prompt写得再严也拦不住模型的自由发挥。决策方案确定性可观测性迭代便捷性适合场景状态机硬规则高高高业务Agent主干流程规则引擎全家桶极高高中可穷举判断的垂直场景树模型/评分卡高中中风险排序、额度审批Fine-tuning模型低低低实验性开放场景LLMPrompt直控低低高玩具Demo、开放闲聊我的结论是对决策主链路用状态机硬规则开放场景理解用LLM候选风险排序类需求再叠加传统模型。不要试图用一个方案吃掉所有决策事项职责分离反而让每个环节都简单可靠。4.2 实操过程中最容易翻车的地方翻车点一状态机的闭环校验缺失。只定义了允许迁移却没有在运行时校验模型返回了一个非法状态后本地测试环境跑不出来线上一压测就现原形。我后来强制在状态迁移入口加了断言只要出现白名单外的迁移就直接抛异常宁可通过报警暴露问题也不让脏状态在系统里蔓延。翻车点二规则太大而全上下文缺失。规则只是匹配关键词但不看对话上下文会出现用户补一句“我不是要投诉只是问一下流程”也触发高危转人工的尴尬情况。我在规则层只对强触发词做分流把弱信号交给LLM置信度来判断同时把“用户是否有过投诉关键词”作为特征与历史会话状态合并判断命中才转人工。这个改动大幅减少了误转率。翻车点三LLM候选没用白名单约束。有些实现里模型可以输出任意新意图标识状态机为了兼容搞了一堆“灵活配置”结果灵活变成了灾难。我的做法是白名单之外只要出现一次模型异常输出就回归检查一次Prompt而不是靠概率去容忍它。翻车点四决策日志没有记录候选。决策日志只记最终决策和规则命中但没记录LLM当时的候选意图。排查问题时发现规则层莫名其妙覆盖了模型结果却修正不出模型到底说了什么。后来每次决策都全量记录输入、候选、置信度、最终决策、耗时回头排查省了一半时间。4.3 补充一个小经验把模型异常输出当特征喂回去这个是我自己摸索很久才试出来的一个土办法。规则层拦截了模型输出之后不要只是简单地把异常结果丢弃或直接转人工可以把异常特征记录到一个上下文变量里作为下一轮会话的输入。比如模型连续两次把普通咨询分类为“投诉”就把“模型疑似过度识别投诉倾向”作为提示词特征反馈给下一轮生成让模型在回复话术里更谨慎一点。这个做法的本质是在LLM和规则层之间建立了一个“异常反馈回路”模型输出越界规则层就记录并纠正下一次调用会带着纠正信号走。它不是让规则去替代生成而是让生成更有上下文敏感性。实际上在线上跑的时候这个机制确实让模型对争议表达的判断收敛了不少越界次数从每周几十次降到了个位数。5. 常见问题与排查技巧实录整理一下做决策权外移改造时最常被问到的几个问题都来自我实际和同行交流时的真实经历。5.1 LLM输出偶发越界怎么办问得最多的就是“规则拦住之后LLM还是偶尔会输出奇怪选择正常吗”正常LLM本来就做不到百分百稳定你的目标不是杜绝偶发而是让偶发不产生实际影响。规则层拦住越界输出后还要做两件事记录异常样本沉淀回测试集下次回归时验证同时在前台给一个兜底回复不能让用户感觉到流程断了。我建议每隔两周做一次异常样本的回归分析统计哪些Prompt场景下模型越界率偏高针对性调整候选枚举和Prompt措辞。比如发现“用户提到时间敏感词但没说清订单号”的场景下模型最喜欢乱猜就在提取阶段增加一个“缺少关键参数”的候选枚举让模型可以合规地说“信息不足”。这比靠规则硬切体验好很多因为模型不再被逼到“非黑即白”的表达空间里。5.2 规则之间冲突怎么排优先级规则多了之后一定会出现冲突。比如“用户提到退款”被规则强约束进入退款审核但同时用户输入里也包含“投诉”关键词两条规则都会命中。我的做法是给每条规则打优先级高危类和合规类规则永远放在最高优先位普通业务策略规则次之LLM候选兜底最后。运营想调整业务策略改配置就可以但高危规则改了要发审批。实际操作中我建议在规则引擎里加一层优先级数字字段数值越小优先级越高同一个决策点只会取最高优先级的规则结果。做一个规则命中展示面板运营和研发都能看到“这条请求为什么走了这个分支”谈优先级争议时直接指面板说话省去大量沟通成本。5.3 决策日志到底要记哪些字段决策日志别只记最终结论我踩过这个坑。最少要记六类字段会话标识、用户原始输入、LLM候选输出、置信度、决策点名称、最终决策结果、规则命中的具体规则ID。有条件再加耗时和Token消耗。前面几项是为了复盘最后一项是为了成本控制。日志落库后按会话ID聚合能串起整条决策链路线上出问题直接链式拉取能快速定位到具体决策点和规则。5.4 状态机状态膨胀怎么办状态机设计得太细状态超过三十个之后维护成本会明显上升。我的经验是分层全局状态保持粗粒度不追求把所有细节都建模成状态。比如“退款审核中”是一个全局状态但内部是否超过金额上限、是否需要财务复核用子状态或业务字段来表示不单独作为全局状态迁移的理由。这样状态迁移表不会失控业务流程的可读性也大幅提升。5.5 成本优化不是所有请求都要过LLM决策权外移之后还有一个自然而然的好处很多决策根本不需要调用LLM。用户输入命中规则关键词后直接走规则分支连LLM候选提取都不用做。我在售后Agent上实测每天超过40%的请求在规则层就完成了路由决策根本轮不到模型出场Token成本下降了三分之一。具体优化做法是前置一道规则分类快速通道先用一批高频关键词和正则对所有输入做快速分类命中明确分支的直接走规则流程只有“无法明确匹配”的输入才进入LLM候选提取流程。这样既不影响处理效果又能把没必要的LLM调用省掉。成本这件事背着背着就显性了不是每轮对话都值得花一次模型调用的钱。6. 这条路走下来我的实际体会把Agent的决策权从LLM手里拿回来本质上是一场“架构信仰”的调整从“模型足够聪明可以自己搞定”切换到“模型很有用但不该替我做主”。这套改造带来的收益不是立竿见影的指标提升而是长期可靠性的质变——线上事故少了责任边界清晰了业务协作顺畅了模型升级也不那么惊心动魄了。如果你打算做类似改造我的建议是从一条最痛的流程开始比如客服Agent里的风险路由或交易流程里的异常单处理不要一上来就重构整个系统。先在一个决策点引入状态机约束和硬规则覆盖跑通之后再逐步铺开。每增加一个受控决策点系统就稳定一分LLM的自由度就收缩一分但它能发挥的“有效自由度”反而更强。最后分享一个小技巧给规则层做一份“决策点地图”把Agent主流程里每一个判断点列出来标注当前是硬规则还是LLM候选哪些已经收权、哪些还在裸奔。这份地图每两周更新一次你会发现自己对系统的掌控感远超从前。这才是把决策权拿回来之后最实在的回报。我在实际使用中还有一个体感很深的细节当规则层真正成熟之后LLM升级给你带来的焦虑会小很多。以前每次模型版本更新都要重新做回归担心新模型把决策带偏。现在决策规则不受模型参数影响模型升级只需要关注理解能力和生成自然度是否提升核心业务逻辑纹丝不动。这种感觉比调Prompt促成一次完美输出踏实太多了。