ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型状态机失控?用协议层为Agent减负的重构方案

大模型状态机失控?用协议层为Agent减负的重构方案 最近重构了一个智能客服 Agent起因是一条线上告警。用户在会话里反复追问“刚才说的优惠券到底什么时候过期”Agent 却回答“我没有提到过优惠券”。实际日志显示优惠券有效期在 12 轮之前就已经给出模型也答对了但第 13 轮提问时它突然“失忆”。类似的失控见得太多了。状态记岔、行为回退、上下文越滚越长最后整个 Agent 像是喝醉了酒在跟用户聊天。问题其实不在模型而在架构设计本身——我们让大模型硬生生去当状态机用。状态机的核心是精确的状态流转、确定的事件响应而大模型的核心能力是语义理解与生成。让一个概率系统去精确维护有限状态属于典型的“任务错配”。这篇博文把当时的重构思路、协议设计细节和踩坑过程完整写出来包括为什么用协议给模型“减负”、状态层如何与模型层解耦、以及线上实际效果。正在做 Agent 后端、或者在为智能体任务编排发愁的朋友这篇内容可以直接当一份参考方案。1. 为什么大模型当不好状态机1.1 状态机的本质是什么大模型的本质又是什么状态机编程在嵌入式领域和传统后端里太常见了。它的核心是三要素状态、事件、状态转移表。比如一个订单流程created状态收到pay事件变成paidpaid状态收到ship事件变成shipped。每一步都是确定性的同一个状态输入同一个事件输出永远一致不存在“可能”这种说法。我在做设备通信协议的时候J-TAG 状态机、CAN 总线状态处理都是这么玩的要求的就是铁一般的确定性。大模型完全不是这么运作的。它的本质是“根据上文预测下一个 Token 的概率分布”每一次生成都带有随机性。你问同一个问题两次得到的措辞会有轻微差异这无所谓但状态机要求的是精确、不可有二义性比如“状态 A 事件 E 状态 B”这没法用概率生成来保证。这么说可能更直白让大模型当状态机相当于让一位文笔极好的作家去兼任记账会计。写文章、做总结、理解复杂意图这确实很擅长但让他用自然语言维护几十个状态的精确流转时间一长一定会出乱子因为自然语言本身就有歧义和冗余。1.2 项目中常见的三个失控表现第一个失控表现是状态遗忘。客服 Agent 要为每个用户维护“当前在聊什么、已经解决到哪一步”的信息但如果这些信息全都靠模型从历史对话里“回忆”随着上下文变长注意力会分散。比如会话超过 20 轮模型已经很难准确意识到“上一个状态是等待用户确认收货地址”。这不是模型笨是任务的难度随着上下文长度指数级上升。第二个失控表现是状态回跳。用户说“我现在不想改地址了”Agent 回复“好的那不改了”但下一轮用户再说“还是改吧”模型可能直接跳回“修改地址”流程的初始状态也可能跳到“退货”流程——因为它对“当前状态”的理解是基于语义相似度而非状态枚举。这种不确定性在真实业务里非常致命用户会被反复询问同样的问题。第三个失控表现是上下文膨胀。为了让模型不忘记状态我们一开始的做法是把整个会话历史都塞进 Prompt再额外加一段“当前状态xxx”。结果每轮 token 消耗越来越高延迟越来越明显而且状态信息淹没在大段历史文本里模型根本不知道哪些 Token 是“状态定义”、哪些是“用户闲聊”。成本翻倍效果反而更差。2. 协议减负的整体设计思路2.1 设计原则把确定性的留给系统把不确定性的留给模型重构前我反复想一个问题Agent 系统里哪些环节是确定性的哪些环节是真正需要模型智能的把这两者分开架构就清晰了。确定性的部分是状态流转规则。比如“用户要求修改订单 - Agent 必须确认新信息 - 确认后调用改单接口 - 返回结果给用户”这个流程是产品定义的不需要模型去创造。它在任何情况下都应该是稳定执行的。不确定性的部分是“用户这句话到底是什么意思”例如用户说“那个东西我不想要了”需要判断是取消订单、退货、还是仅表达不满。这一步必须交给大模型因为它需要理解上下文、语气和业务知识。所以设计原则很简单系统用协议和状态机保证“不会错”模型用能力保证“懂得快”。协议不是限制模型的自由度而是帮它排除那些不需要思考的确定性决策让它把注意力放在真正需要语义理解的部分。2.2 三层架构状态层、协议层、模型层重构之后我把 Agent 拆成了三个层级。状态层是最底层维护所有业务会话状态。它就是一个标准状态机状态枚举、事件枚举、转移表全部写死在代码里由后端服务维护。每个用户会话对应一个状态实例状态变化有日志、可回放、可审计。协议层是中间层负责状态层与模型层之间的消息通信。它不关心模型具体怎么推理只关心消息格式是否合法、事件是否能触发、超时和重试怎么处理。大模型的输出必须经过协议层校验才能触发状态转移。模型层是最上层只做两件事理解用户意图生成动作建议。它输入是用户消息和当前状态摘要输出是一个结构化动作比如action: confirm_change_address, params: {...}不直接改状态也不直接调用业务接口。真正改状态是协议层校验通过之后才执行。2.3 为什么选择轻量协议而不是重型编排引擎一开始也有人建议用工作流引擎或者流程编排框架。我认真对比过最后还是选择了轻量的自定义协议内部定义一个 JSON 格式的消息契约走 HTTP 回调。原因有三个。第一开发成本低。工作流引擎往往自带一套 DSL 和可视化界面学习成本高调试起来要跨好几个系统。我们团队当时只有三个人没有精力去维护一套重型引擎。第二可观测性好。协议消息就是纯文本 JSON打印出来就能看明白模型理解成了什么、协议层拒绝了什么、状态转移到了哪里。这对排查问题太重要了。第三跨语言友好。另一端将来如果接入新的模型平台或者新的业务系统只要遵循同一份协议文档不依赖任何特定框架。如果你的场景是超大规模分布式系统每秒上万次状态流转那可以用消息队列或者 Redis Streams 来替代 HTTP 回调。但绝大多数 Agent 项目的规模根本到不了那个量级轻量协议足够稳还更容易调试。3. 落地一次协议减负实践3.1 业务背景与状态定义拿客服这个场景具体举例。业务是订单售后用户可能咨询物流、申请退款、修改地址、开发票等。重构前这些能力全部靠模型自由发挥重构后我们显式定义了五个主状态状态枚举含义可进入的事件created会话创建初始化awaiting_intent等待识别用户意图intent_identified/intent_timeoutcollecting_params收集业务所需参数param_collected/param_missingexecuting执行业务动作action_approvedcompleted本次意图服务结束action_successfailed本次意图服务失败action_failure/action_rejected每个状态都对应模型可以执行的动作白名单。比如在collecting_params状态模型能做的动作只有request_param向用户询问缺失参数和cancel_intent主动终止当前意图。模型绝对不能做的事情是直接调用改单接口那是executing状态才有的权力。3.2 协议消息结构用事件通知代替状态记忆协议层的核心设计是用事件驱动替代“让模型记住状态”。状态层和模型层之间的消息结构如下{ session_id: 7f3a2c9e-1d4e-4a5f-9c2b-0e6a2d4b7c81, event: intent_identified, state: { from: awaiting_intent, to: collecting_params, intent: change_address }, payload: { collected_params: { new_address: 杭州市西湖区文三路 138 号 }, missing_params: [receiver_phone] }, model_action: { type: request_param, params: { param_name: receiver_phone } } }这个协议的消息含义是模型已经识别出用户想改地址并且拿到了一部分参数还需要手机号所以向用户发起了request_param动作。状态层收到这条消息后校验当前状态确实是collecting_params动作request_param在动作白名单内参数receiver_phone确实缺失——全部通过于是消息进入业务执行环节。如果模型试图在collecting_params状态下直接调用update_order接口协议层直接拒绝并返回一个action_not_allowed错误。协议层不信任模型的任何“记忆”它只根据消息里的 state 字段和业务层真实状态做比对。状态一致性由状态机保证模型只需要在当下这一刻给出正确的判断即可。3.3 参数选择轮询周期、超时、重试、上下文协议是双向的模型可以向状态层发送动作请求状态层也可以向模型发起事件通知。在实现上模型服务通过 HTTP 接口等待新事件用长轮询实现。参数按实际业务量来定长轮询间隔设置为 500ms。也就是说模型服务在没有事件时最多挂起 500ms然后重新请求。根据线上压测单任务平均状态停留时间为 5 秒单轮模型推理耗时约 2 秒500ms 的轮询间隔完全不会成为瓶颈。如果并发会话数达到 1000每秒钟新增轮询请求约 2000 次单次状态查询接口响应时间小于 2ms完全扛得住。单次事件处理超时设置为 30 秒。超过 30 秒模型没有返回合法动作状态层标记为timeout事件自动转入人工队列。这个值不是拍脑袋定的线上统计显示95% 的模型单轮响应都在 10 秒以内30 秒的阈值给长上下文场景留了充足余量。重试参数采用“2 次立即重试 1 次延迟重试延迟 5 秒”。如果协议层返回校验错误模型有最多 3 次重新生成动作的机会。超过 3 次直接转人工并且记录一个model_failure日志。上下文注入采用“状态摘要 必要业务数据”方式而不是把完整历史对话全部塞进去。例如在collecting_params状态系统只向模型注入当前意图、已收集参数、缺失参数。上一轮用户说了什么废话、语气词全部不进入模型输入。这个改动让单次请求的 token 消耗直接降了约 34%因为模型不再需要从几千字的会话历史里找状态信息。3.4 模型层只保留哪些能力模型层经过裁剪最终只保留三类能力。意图识别。给定用户消息和当前状态枚举模型输出一个意图标签比如change_address、refund_request、check_logistics。输出必须是枚举值不允许自由发挥。参数抽取。模型从用户对话中提取业务参数例如“新地址”“手机号”输出为结构化字段。字段的 schema 由协议层预先定义模型必须按 schema 输出。动作生成。模型根据当前状态和意图从动作白名单里选择一个动作执行。例如状态是collecting_params缺失参数是receiver_phone用户说“我手机号是 138xxxx”模型输出action: submit_param, param: receiver_phone, value: 138xxxx。这个动作会被协议层直接消费。模型不再负责“记住这个会话进行到哪一步”也不再负责“下一步该走到哪个状态”。状态转移完全由协议层根据校验结果触发。模型从“全知全能的大脑”变成了“专职的语义解析器”它的压力和出错率自然下降。4. 实战中的坑与排查技巧4.1 坑一模型总是绕过协议把状态写在自由文本里重构刚上线时模型还是偶尔会把状态信息写进回复正文。比如协议要求输出 JSON 动作模型偏偏在文本里说“好的用户现在的状态是等待确认收货地址”。人眼能看懂但协议层完全无法解析直接抛错。后来排查发现这是 Prompt 措辞导致的。之前的系统提示语是“你需要记住用户当前的状态”这相当于鼓励模型把状态放在自己的记忆里。改成“你不需要知道当前状态系统会告诉你当前状态是什么你只需要输出动作 JSON”之后问题基本消失了。模型是高度受提示影响的架构上要求它不记状态提示词也绝不能让它“心里有数”。4.2 坑二任务链断了到底谁负责重试有一次线上出现了一个诡异的现象用户在改地址流程中连续三次输入新地址Agent 始终没有触发修改。排查日志后发现协议层每次收到动作都校验失败原因是业务接口返回了一个“地址库无匹配”的错误。模型倒是很诚实每次都把用户输入的原样提交没有做任何标准化处理。这个问题的本质是协议层只校验“动作是否合法”却没有校验“动作能否执行成功”。所以重试逻辑需要分两层协议层负责重试网络异常比如超时、连接失败这类问题立即重试两次业务层负责重试“业务校验失败”的情况比如地址库无匹配此时不能盲目重试而应该触发一个clarification_required事件让模型重新向用户提问。这两个重试机制必须分开混在一起会导致重复提交和状态错乱。4.3 坑三事件乱序导致状态回跳某个下午出现了连续三单报错用户明明已经完成退款申请点进会话又看到 Agent 在问“请问您需要处理什么问题”。回头看日志发现是异步回调的时序出了问题。模型在收到action_success事件之前抢先提交了下一个意图的intent_identified事件状态机于是把会话从completed拉回到awaiting_intent。这个问题的解决方案是给协议消息加上expected_state字段。协议层处理每条消息前先比对expected_state与会话当前真实状态。如果不一致直接丢弃并返回state_mismatch错误由发送方重新获取最新状态。这相当于给状态转移加了一道“乐观锁”代价是模型偶尔要多一次重试但换来了状态不可能回跳的保证。4.4 坑四协议层的规则与模型自由度如何平衡协议如果定得太死模型没有发挥空间定得太松模型又容易越界。这里面有一个取舍值得说清楚我最终的做法是把规则分成两层硬规则和软规则。硬规则包括状态转移表、动作白名单、必填参数校验。这些写死在代码里没有任何商量余地。软规则包括模型在回复用户时的语气风格、解释方式、推荐策略。比如用户问“改地址支持哪些快递”模型可以自由组织语言只要最终动作是request_param或answer_query内容完全自由。实践结论是硬规则要尽量多软规则要尽量少。协议层多限制一个自由度模型就少一个出错的可能。业务层应该主动把那些确定性的判断都写成代码而不是留给模型现场发挥。协议层不是模型的对立面而是模型的护栏。调整完这套架构后线上异常率从重构前的 13% 降到了 0.6%模型 token 消耗下降约 37%用户“重复提问”的会话比例从 8.4% 降到 1.1%。我认为关键并不在于用了多高级的模型而在于把不该模型做的事情全部拿掉了。还有一个小技巧想分享给大家给协议里的每条消息都增加一个全局递增的sequence_id即使是同一个会话里的两条消息也不要复用序列号。排查乱序问题时只需要按序列号排序就能还原真实的事件流比看时间戳靠谱得多。这套“协议减负”思路还可以继续扩展。比如将来如果要接入新的模型不需要修改状态机只需要让新模型适配协议层的数据格式即可如果要把能力开放给第三方系统协议文档可以直接作为 API 契约。稳定的 Agent 系统不是让模型记住一切而是让模型做判断、让系统做确保。
返回列表