大家做 Agent 开发的时候是不是都有过这种冲动既然大模型这么聪明干脆把所有逻辑都交给它让它自己判断下一步干什么。我也这么干过而且结局相当惨烈——一顿操作猛如虎一看账单心发堵跑一次全流程要烧掉几十万 token中间还经常出现状态错乱、反复横跳、上下文里翻出三条互相矛盾的执行路径。后来我认了大模型再聪明也不该把它当成一个状态机来用。状态机是确定性的大模型是概率性的硬把它们焊在一起出问题是必然的。这篇文章就是一次 Agent 协议减负实践的全过程复盘。我从一个典型的业务型 Agent 出发梳理了状态机和大模型的职责边界设计了一套轻量级的消息协议让大模型只做它擅长的决策和生成把流程控制、状态流转、条件判断这些确定性的工作全部交还给代码。整套方案已经在实际项目中跑通了吞吐稳定、token 消耗骤降、出错的概率也小了很多。如果你正在做 Agent 编排或者被大模型自由发挥导致不可控折磨过这篇文章应该能给你一个可落地的解法。1. 为什么不能让大模型当状态机1.1 一次翻车现场的复盘先说说我最初是怎么翻车的。当时接了一个业务需求整个流程有八个节点用户信息收集、需求分析、方案推荐、参数确认、订单生成、支付引导、售后回访、流程结束。我的第一版方案非常天真——把所有节点描述写进 system prompt让大模型自己决定什么时候该执行哪个节点每轮对话结束后更新一个当前状态字段。刚开始测试的时候两三个节点的小流程确实没问题大模型表现得像模像样。但流程一拉长问题全冒出来了第一个问题是状态漂移。大模型跟用户聊着聊着会把方案推荐状态下的任务当成参数确认状态下的任务来执行。比如用户问这东西保修几年模型直接进入了售后话术跳过了参数确认环节。第二个问题是上下文爆炸。因为模型需要记住自己走到哪一步每一步都要把之前的对话摘要 当前状态 剩余节点重复一遍token 消耗指数级上涨。第三个问题最头疼——同一个状态可以到达但永远无法可靠地离开。模型在某个节点上跟用户来回拉扯加上系统提示里如果有问题请澄清需求这类引导它就会一直澄清下去流程卡死。那次产品的 BUG 反馈我攒了一整屏最后我冷静下来想了想根本原因不在于大模型能力不够而在于我让它干了它本质上不擅长的事——维护一个可持续、可回滚、可校验的执行状态。1.2 大模型的三个能力边界从那次翻车以后我花了不少时间梳理大模型的能力边界。总结下来有三个明显的边界凡是越过这些边界的任务都应该交给代码而不是大模型。第一个是确定性边界。大模型对同一个输入的输出并不完全稳定温度设得再低也有随机性。它适合做可接受多种合理结果的任务不适合做只能有唯一正确结果的任务。状态转移就是典型的确定性任务——在方案推荐状态下用户的下一步动作只能是接受方案修改参数重新生成放弃咨询这几种之一。用代码的 switch-case 完全可以处理而且扔给大模型反而增加了不确定性。第二个是状态维护边界。大模型是一个无状态函数它每次看到的是你塞给它的那段上下文。让它在上下文里隐式地记住当前是第几个节点哪些信息已经收集过了时间一长必然出错。这不是提示词工程能解决的因为注意力机制天然偏向近期内容早期状态很容易被冲淡。第三个是成本边界。状态维护靠 token 来买每轮对话都要把状态信息重新写入上下文重复一遍就是一遍的钱。大模型每多考虑一步消耗的都是真金白银。想明白这三点之后我确定了一个原则流程走法归代码管决策判断归模型管。1.3 协议减负的核心思想那具体怎么分工呢我的做法是引入一层协议层相当于给大模型和应用程序之间签一份明确的协作契约。协议解决三件事定义消息格式、约定交互时机、划定职责边界。大模型不需要知道整个状态机有多少个节点它只需要知道当前这一刻在我面前有几个可选项。应用程序也不需要猜测大模型下一步会说什么因为协议规定了模型必须输出结构化的决策结果而不是自由文本。这就像你去餐厅吃饭菜单上只有那几道菜你点哪个服务员记哪个后厨就做哪个。你不会让服务员猜测你想吃什么更不会让后厨自己决定要不要换个做法。大模型就是那个点菜的人但它只能从菜单里选不能自己发明菜名。菜单由谁定由状态机定。这个思路其实跟通讯领域的协议设计如出一辙。像 MQTT、Modbus、CAN 这类协议核心价值同样不在于传输本身而在于约定——谁在什么时间、以什么格式、说什么内容。HTTP 协议让客户端和服务器能配合不是因为大家聪明而是因为大家都遵守同一个约定。Agent 协议减负本质上是把这种成熟的思想搬到大模型协作里来。2. 协议设计给 Agent 一套明确的分工规则2.1 消息结构把该干什么和干了什么分开协议设计的第一步是定义消息结构。我的经验是消息一定要把请求-响应这种即时交互跟事件-状态这种持续信息分开。前者是对话流里的你来我往后者是流程控制里的阶段标记。具体实现上我给每个消息定义了一个必填字段msg_type取值只有四类user_request用户发起的新请求通常是自然语言。agent_response模型生成的回复回给用户的。tool_call模型请求调用外部工具比如查库存、算价格、生成订单号。state_event系统内部状态变化通知不经过大模型由代码直接流转。这里最关键的是state_event这个类型。它跟对话内容完全解耦不由大模型产生而是由状态机代码在满足条件时主动发出。模型回复好的我帮您确认一下库存代码层检测到这是一个需要查库存的动作直接发出state_event把状态从方案推荐推到库存确认。模型根本不需要知道状态已经变了——它只知道上一轮它请求了一个工具调用。字段设计上每条消息都带session_id、timestamp、state_version。state_version是状态机的版本号每次状态流转 1。这个字段在排障时特别好用翻日志能精确看到状态在哪一步变的谁让它变的为什么变的。消息体里还有一个非必填的meta字段用来塞对话元信息比如当前用户等级、业务线编号、用户所在的地区。这些信息跟状态流转没关系但对模型的回复生成有帮助。我把它们放在消息结构里而不是塞进 system prompt就是为了让模型不过度联想——它知道这些是背景资料不是流程控制指令。2.2 会话状态状态机的归状态机上下文的归上下文这是整个协议设计的核心。我把它总结成一句话会话状态存数据库对话上下文存消息队列。状态机负责三件事当前节点、可达动作、合规跳转。这三样全部存在一个独立的状态存储里我用的是 Rediskey 是agent_state:{session_id}value 是一个 JSON{ current_state: parameter_confirmation, allowed_actions: [ accept_quote, modify_parameter, regenerate_plan, ask_human ], state_version: 12, updated_at: 2024-01-15T10:23:4508:00 }大模型收到的系统提示里不是一长串你可以做这个可以做那个如果用户说了 A 就跳到状态 B如果用户说了 C 就跳到状态 D而是一个精简的当前动作空间{ role: system, content: 当前可用操作accept_quote(确认报价)、modify_parameter(修改参数)、regenerate_plan(重新生成方案)、ask_human(转人工)。请根据用户消息选择操作并输出结构化响应。 }这里有个关键细节我在早期踩了坑系统提示里不要写如果用户说……就……的条件语句。一旦你写了条件就等于让模型去维护一个迷你状态机。上次项目里我就是这么写的模型经常把如果当成指令去循环执行。正确的做法是只给动作名不给触发条件。触发条件的判断放在状态机代码里由它来决定当前动作空间里应该开放哪些动作。对话上下文则是另外一套存储不需要带状态信息只保留模型和用户的原始消息记录。每次调用模型时我拼装的信息是[协议头] 当前状态 可选动作 [对话历史] 最近 N 轮原始对话 [用户当前输入] 最新的 user_request协议头很短历史记录按需截断状态信息不注入对话中间。模型需要知道什么由代码决定模型需要记住什么由数据库决定。这套拆法下来单轮调用的 token 消耗直接砍掉了六成而且状态漂移的问题再没出现过。2.3 轮次与终止控制没有轮次控制的 Agent 就像没有刹车片的车。模型在某个动作上反复跟用户确认或者自己跟自己对话都能让流程拖到天荒地老。所以协议里必须有明确的轮次和终止规则。我设了三个硬限制第一个是动作重试上限。同一个state_event最多允许连续触发三次。如果状态机在 3 秒内收到三次同类型的转移请求协议层直接拒绝并将控制权交还给用户提示请稍后再试或联系客服。第二个是对话轮次上限。整个 session 最多允许 30 轮对话超过后强制进入ask_human状态。这个数字不是拍脑袋定的是跑了三个星期的历史数据统计出来的——90% 的正常咨询在 24 轮内完成30 轮预留了一点余量。第三个是终止状态标记。协议定义了三个终态completed、abandoned、escalated。completed是业务完成abandoned是用户主动离开escalated是转人工。状态机代码里明确列出哪些状态可以跳转到终态哪些不可以。比如parameter_confirmation状态下用户没有确认参数绝对不能跳completed。终止判断的逻辑全部由代码执行大模型的回复里哪怕写了已完成只要状态机没收到合法的state_event流程就不算完。模型能建议结束但决定结束的是协议。3. 落地实现从框架代码到协议执行3.1 核心状态机的框架设计协议里的状态机我用的是三段式结构状态定义、事件映射、动作执行器。这个结构在传统状态机编程里很常见挪到 Agent 场景下依然适用。状态定义是一个 Python 枚举from enum import Enum class AgentState(Enum): INFO_COLLECTION info_collection DEMAND_ANALYSIS demand_analysis PLAN_RECOMMENDATION plan_recommendation PARAMETER_CONFIRMATION parameter_confirmation ORDER_GENERATION order_generation PAYMENT_GUIDANCE payment_guidance AFTER_SALES after_sales TERMINAL terminal事件映射是一个字典维护当前状态 事件 → 下一状态的关系TRANSITION_MAP { (AgentState.INFO_COLLECTION, info_complete): AgentState.DEMAND_ANALYSIS, (AgentState.DEMAND_ANALYSIS, analysis_done): AgentState.PLAN_RECOMMENDATION, (AgentState.PLAN_RECOMMENDATION, accepted): AgentState.PARAMETER_CONFIRMATION, (AgentState.PARAMETER_CONFIRMATION, params_confirmed): AgentState.ORDER_GENERATION, (AgentState.ORDER_GENERATION, order_created): AgentState.PAYMENT_GUIDANCE, (AgentState.PAYMENT_GUIDANCE, payment_complete): AgentState.AFTER_SALES, }动作执行器则负责在状态跳转时执行副作用比如保存数据、调用第三方接口、发通知。这套设计的核心好处是状态流是可见的、可测试的。你可以用一个几百行的小测试脚本把每一条TRANSITION_MAP路径都跑一遍确认不存在非法跳转。大模型的输出不直接触发任何副作用它只能生成一个转移事件候选经过程序校验后才会真正改变状态。3.2 大模型决策的接入方式有了状态机接入大模型反而变得简单——模型只处理一种输入一个结构化的待决策问题。我把大模型的调用封装成一个决策函数输入是当前状态 动作空间 对话上下文输出是一个标准化的决策对象。class AgentDecision(BaseModel): action: str parameters: dict {} reply_text: str 这个函数内部会做严格的数据校验action必须属于当前状态的allowed_actions否则整条决策被判为无效系统会重试一次。重试时把错误信息作为反馈再喂给模型让它重新选。连续两次无效就触发兜底策略见 4.2。为什么用 Pydantic BaseModel 做结构化输出因为协议需要一个可靠的解析层。自由文本你说它表达了接受方案但模型可能写的是这个看起来行吧语义相似但对程序来说一个天一个地。结构化输出配合枚举校验让模型的表达能力被约束在协议允许的范围内。这里有个实际经验模型输出结构化结果的准确率跟提示词里的格式规范成正比跟字数成反比。别在提示词里写长篇大论的解释就给一个 JSON 示例然后用 few-shot 给两个完全不同形态的样例输出稳定性会提升很多。3.3 工具调用的状态联动工具调用是 Agent 协议减负里最值得细说的环节因为它是连接大模型和业务系统的桥。我把它设计成工具即事件源——工具不是被大模型执行的而是被大模型触发后由协议层执行执行结果再以事件形式回到状态机。举个例子用户说帮我看看这台机器最近有没有优惠大模型生成了tool_callquery_promotion({machine_id: M-209})。协议层收到后做两件事。第一通知状态机当前进入promotion_query的挂起状态在这个状态下阻断其他动作的触发防止模型边查边做别的。第二调用真实的库存促销查询接口拿到结果后把结果打包成一个state_event发回给状态机状态机再根据当前业务规则决定下一步动作空间是推荐优惠方案还是提供标准报价。整个过程中大模型只做了一件事识别用户意图并生成一个工具调用请求。后续的状态怎么流转、优惠方案怎么生成、参数如何回填统统由代码控制。模型不需要知道查询结果长什么样它只需要在下一轮收到一个新的待决策问题时基于新信息做判断。这套封装还有一个好处就是状态机的每一条路径都能被回放。排障时我只需要重放state_event序列就能定位每一步的输入输出完全不需要猜测模型当时是怎么想的。确定性路径负责保证正确性大模型决策路径负责提高灵活性各司其职。3.4 系统提示词的精简改造既然职责边界清晰了系统提示词必然要大幅精简。以前那种塞入各种规则和背景的提示词现在彻底拆掉只保留三类信息第一类是身份与风格描述固定不变比如你是一个耐心细致的智能导购。第二类是当前状态的动作空间动态注入这是唯一跟流程相关的信息。第三类是输出格式规范告诉模型必须给结构化 JSON。拆完之后的效果出奇地好。模型不再需要理解整个流程只需要在局部动作空间内选择一个动作这恰好是它最擅长的事情。而且提示词短了上下文占用小了响应延迟也从原来的 2.5 秒降到了 1.3 秒左右。我见过很多项目把系统提示词当成了软件配置中心什么规则都往里塞越写越长最后模型的行为越来越不可捉摸。实际上系统提示词应该是一个精简的配置文件而不是一本操作手册。复杂的规则应该放在代码里用确定性逻辑去执行。4. 常见问题与排查技巧实录4.1 状态漂移还是发生了怎么办即使做了协议分层状态漂移还是有可能出现最常见的诱因是并发和重试。比如同一个 session 的请求被网关重发了两次模型生成了两个工具调用请求协议层还没来得及更新状态第二条请求已经到了。我的解决方案是给所有state_event加上一个自增的event_seq字段。状态机在处理事件前先检查序号是否连续如果发现重复的event_seq直接丢弃。这就好比 TCP 协议里的序列号机制重复的包到了就扔绝不让它破坏连接状态。第二个诱因是模型表面上输出了action: modify_parameter但用户真实的言语里其实已经不想继续了比如算了不问了。这种语义层面的状态漂移用状态机规则拦不住我的兜底手段是在协议里增加一个user_exit_words的检测列表一旦用户消息命中算了不用了再见这类退出表达直接在协议层拦截不再把请求转发给大模型。大模型连挽回一句的机会都不需要状态机直接跳转到abandoned终态。4.2 大模型输出不合规时的兜底策略结构化输出也会有不合规的时候我见过模型输出不存在的动作名、输出空 parameters、输出嵌套层级错误的 JSON。早期我直接在代码里抛异常用户体验很差——对话框直接冷冰冰地提示系统错误。后来我把兜底策略分级了分三步走第一步是重试。把校验错误信息返回给模型让它修正。这相当于提示词工程里的自我修正机制实测有效率高的时候能到 80% 左右。第二步是降级到最近一次合法的决策如果重试还失败就退回上一个已知合法状态让用户重新表述。第三步是强制转人工连续三次失败就不折腾了直接触发escalated。关键点在于所有兜底逻辑都是确定性的代码不依赖大模型的自觉。模型可以犯错但协议层不能让错误影响业务正确性。错误是允许发生的只要错误可控、可回收、可追踪。4.3 循环调用和死锁循环调用是 Agent 项目的经典问题。模型觉得信息不够调一次工具工具返回的结果格式不对模型再调一次结果还是不对再调……这种死循环既烧钱又容易把下游系统打爆。我在协议层实现了三个保护机制第一个是单轮工具调用次数上限一轮对话最多允许 3 次工具调用超过直接强制转人工并在回复里说明为了尽快帮您解决问题已为您连接人工客服。第二个是相同参数的工具调用去重如果模型在两轮内用完全相同的参数请求查询同一个工具协议层直接复用上一次的缓存结果不真正执行。第三个是工具执行超时熔断单次工具执行超过 5 秒直接返回查不到结果并冻结该工具 30 秒的调用资格。这三板斧过后我的项目里再也没出现过模型跟工具互相拉扯的场景。工具执行时间超时的大部分是第三方接口不稳定熔断反而保护了下游系统。4.4 上下文精简的实战方案协议减负之后上下文里不再塞状态信息但对话历史还是会越攒越长。我的做法是分层管理对话历史短轮保持完整消息中轮做摘要长轮只保留关键决策点。具体来说最近 5 轮对话全部保留原文第 6 轮到第 15 轮用摘要替换超过 15 轮的部分只保留用户的关键动作和工具调用结果。摘要不是用大模型做的成本太高。我用的是传统 NLP 的关键句提取把每轮里包含我要帮我确认取消这类强意图词的句子保留下来其他过滤掉。这套方案的精确率比不上大模型摘要但对 Agent 决策场景来说基本够用而且延迟在毫秒级。还有一个细节对话历史里工具调用结果如果是一大张表别全文塞给模型。我会先把结果转成一行摘要比如库存充足单价 3499 元无折扣除非模型明确请求查看详细信息才展开完整内容。减少上下文纯文本量是降低 token 消耗最直接的手段。5. 减负效果从实测数据看价值协议改造完成后我给同一个业务场景做了前后对比测试。场景是完整的八节点咨询流程各跑 200 个会话平均值如下指标改造前改造后变化单会话 token 消耗约 32k约 10k降低 68%平均响应延迟2.5s1.3s降低 48%状态漂移出错率11.5%0.7%降低 94%流程完成率78%92%提升 14%兜底转人工率4.2%1.8%降低 57%最直观的感受是 token 账单——同样的业务量API 费用大概降到原来的三分之一。这个收益是长期复利因为对话越长原方案的状态维护开销越大而新方案的协议成本基本恒定。延迟的改善除了上下文变短之外还有另一个原因原来是模型每轮都要重新理解整个任务全貌现在模型只需要做一次局部决策推理路径短了自然快。这不是模型变聪明了而是它要做的事变简单了。稳定性指标的提升其实是最有价值的。状态漂移出错率从 11.5% 降到 0.7%意味着线上客服群里关于流程异常的报告基本消失了。以前模型偶尔会编造一个不存在的状态来推进流程——比如好的您的订单已提交但实际上订单号都没生成——现在这种情况完全被协议层拦截因为状态机根本不存在订单已提交这个状态事件转移一定是从ORDER_GENERATION且经由order_created事件触发的。我在实际使用中还有一个体会协议减负最大的好处不是省钱而是让整个系统重新变得可解释了。以前团队里讨论模型为什么会这样走只能靠猜因为大模型内部的推理过程不可见。现在每一条路径都有明确的state_event串着任何一个跳转都对应着代码里的一条映射关系。出了问题翻日志、看事件序列、重放状态流原因一目了然。这种确定性带来的可维护性对长期迭代来说比省 token 重要得多。如果你正被大模型当状态机这个问题困扰我的建议很简单把流程控制权收回代码给大模型一张受限的动作菜单让协议去协调两者。别怕大模型发挥空间变小真正需要发挥空间的地方它一点都没少。要处理的复杂判断、要生成的回复、要理解的用户意图依然全是模型在做。只是它不用再背负那个本不该它背的包袱了。 SEO 优化官网定制响应式建站教育培训建站