☰
AI Agent 从“能跑”到“能用”:状态机、上下文与成本治理实录
2026/10/22 4:44:04 网站建设 项目流程

这个系列写到现在,终于到了要承认“能跑”这件事并不体面的阶段。v1 版本的 Agent 当时是真能跑:能接大模型 API,能调几个工具,能对着一个简单问题给出步骤和结果。可在实际流程里跑了两个月,我发现它其实一直在旁边“表演能跑”——让它连续处理二十个真实任务,第二个就开始跑偏;让用户多问两句,它就把上文忘得干干净净;日志拉出来几十行,却根本看不出是哪一步把整个流程带崩的。

这一篇是整个系列里最难写的一篇,因为 v2 遇到的问题不再是“模型不够聪明”或“提示词不够精细”,而是把 Agent 从一次演示的玩具,变成一台可以被业务反复使用的机器。这个过程会逼着你去碰状态管理、上下文预算、成本核算、异常恢复、可观测性这些平时写 demo 根本不会碰的工程深水区。如果你现在也在做 Agent,而且已经受够了“demo 很流畅、一上真实数据就翻车”的状态,这篇应该能帮上忙。

1. 为什么“能跑”不再够用:v1 的体面与代价

1.1 v1 的真实样子:它只是“演示态”的 Agent

先回顾一下 v1 的架构,说穿了就是一个循环:把用户消息塞进 prompt,调用大模型,解析模型输出,判断是否要调用工具,执行工具再把结果拼回去,继续下一轮。整个过程两三百行就能写完,一个 while 循环加一个工具注册表,demo 跑起来非常好看。

但“演示态”和“生产态”之间隔着一道巨大的鸿沟。演示的时候你选的是最理想的输入,模型状态最好的时候,工具也都通着。真实业务里没有这种运气:用户输入是残缺的,模型输出偶尔不按约定来,外部工具会超时、会返回意料之外的错误,甚至整个循环执行到一半,进程被杀掉后没有任何恢复手段。

我记得最清楚的一次,v1 被扔到一个模拟工单系统里做自动分拣测试。前期几条工单处理得漂亮,到了第四条,模型的输出格式里少了一个花括号,解析层直接抛异常,整个任务中止。放在演示里我会重开一轮,但在生产环境里,这条工单一卡,后面的队列全堵住了。那一刻我意识到:不是模型不行,是我根本没有为“出错”这件事做过任何设计。

1.2 v1 暴露的四个真实炸点

用一张表来复盘 v1 的问题,比用语言描述更直观:

问题具体表现后果
输出不稳定模型偶尔不按 JSON Schema 返回,字段缺失或类型不对流程直接中断,需要人工介入
上下文失控长对话不管理上下文,历史全量塞进 prompttoken 消耗剧增,且超出窗口后模型开始“失忆”
成本不可控长任务每轮都带完整历史,调用次数随问题复杂度膨胀单任务成本远超预期,无法批量使用
黑盒运行只有 print 级别日志,无法还原执行链路排查问题基本靠猜,回归测试无从谈起

这四个问题互相纠缠。上下文失控会放大成本问题,输出不稳定让用户在真实任务上寸步难行,黑盒运行让我连问题在哪都定位不到。只靠“把提示词写得更细”解决不了任何一项,因为它们都发生在工程层,不是模型层。

2. 从“提示词编排”到“代码编排”:v2 的架构重构思路

2.1 第一步:把“编排”从提示词里搬到代码里

v1 的思路是尽可能把规则写进提示词,让模型去“理解”整个任务流程。这有点像一个从没带过团队的资深工程师,把所有事情都扛在自己身上,最后一定被细节压垮。

v2 的第一刀,就是把任务流程的编排逻辑从提示词中剥离出来,放回代码里。模型仍然负责“决策”:面对当前状态,下一步应该调用哪个工具、还是直接输出最终回答。但流程控制、状态转移、终止条件这些,全部由代码决定。

举个例子,v1 里为了阻止模型在任务没完成时提前结束,我会在提示词里反复强调“你必须完成任务才能停止”。效果不稳定。v2 里我直接写进状态机:当前阶段如果是“收集资料”,状态机根本不允许结束,模型必须输出工具调用;只有状态机转移到“完成”阶段,Agent 才会生成最终回答。模型负责内容,代码负责规则,边界清晰以后,稳定性一下就上来了。

2.2 Agent 拆成三件套:状态机、工具层、资源管理层

重构后的 v2 核心由三部分组成,每部分只做一件事:

状态机是流程的中枢,负责定义每个阶段的合法转移路径。它不关心模型说什么,只关心当前状态 + 事件后应该跳到哪个状态。工具层是 Agent 的手脚,所有外部能力统一在这里注册,统一做参数校验、超时控制、异常处理和结果规范化。资源管理层是 Agent 的钱包和记忆,负责 token 预算的分配、历史上下文的压缩策略、成本配额的管控和并发请求的调度。

用 Python 来描述,三个模块的职责边界大概是这样的:

class AgentV2: def __init__(self, state_machine, tools, resource_manager): self.fsm = state_machine # 状态机:流程合法性 self.tools = tools # 工具层:所有外部能力 self.resource = resource_manager # 资源层:上下文、token、成本 self.events = [] # 事件日志,追加式记录 def run(self, user_input): self.fsm.reset() self.resource.begin_session(user_input) while not self.fsm.is_terminal(): stage = self.fsm.current_stage() prompt = self.resource.build_prompt(stage, self.events) response = self._call_model(prompt) event = self.tools.execute(response.tool_call, request_id=self.uid) self.fsm.transition(event.status) self.events.append(event) self.resource.record_tokens(...) return self.fsm.result()

这样改完以后,模型不再面对一个混杂了流程规则、工具描述、历史记录的超长提示词。它每一轮看到的,是按当前阶段裁剪好的结构化上下文,决策的压力小了很多。

2.3 一条关键取舍:哪些交给模型,哪些必须交给代码

这是整个重构里最值得反复琢磨的问题。我的原则总结成一句话:凡是影响正确性和成本的环节,必须代码控制;凡是只有模型能做的环节,才交给模型。

影响正确性的包括:状态转移是否合法、工具参数格式是否正确、最终结果是否满足业务约束。这些如果让模型自由发挥,就是拿随机性去赌业务结果。影响成本的包括:历史上下文怎么裁剪、每一轮给模型看多少东西、同一个工具调用结果是否重复利用。这些如果让模型自己决定,它天然倾向于“看得越多越安全”,成本就失控了。

真正留给模型的只有三类能力:理解用户意图、规划下一步动作、生成最终表达。这三件事代码做不了,也是模型的价值所在。把这条边界画清楚,Agent 的工程结构会豁然开朗。

3. 最硬的三个骨头:状态、上下文和成本治理实录

3.1 状态管理:用“事件日志”替代“不断被覆盖的记忆”

v1 的状态管理方式是简单粗暴的:把历史中间结果全塞进下一轮 prompt,让模型自己从里面找信息。问题在于,模型在长文本里找信息的能力并不可靠,而且拼接过程会不断重复、遗漏,最后 prompt 膨胀到失控。

v2 换成了事件溯源思路。我不再维护一个不断被覆盖的“当前记忆”,而是只做追加式的完整事件日志。每执行一步,就往日志里追加一条不可变的事件记录。构造 prompt 时,由资源管理层结合当前阶段,从事件日志中提取信息,重新组织成模型需要看的摘要和关键变量。

事件记录的最小结构长这样:

{ "event_id": "evt_0007", "request_id": "req_2f9c4d", "stage": "collect_matcherial", "agent_action": { "type": "tool_call", "tool_name": "search_docs", "arguments": {"keyword": "退款政策"} }, "tool_result": { "status": "success", "summary": "共命中三条文档,核心规则是7天无理由退款", "raw_size": 240 }, "tokens_spent": 3120, "created_at": "2025-01-04T12:31:08Z" }

关键变量独立存储在状态对象里,比如当前已确认的信息、待确认的字段、最终答案的草稿。这些变量由代码维护,不依赖模型在长对话里自我记忆。模型只负责产出增量信息,代码负责把增量信息合并进业务状态。状态正确性不再随上下文长度衰减,这是 v2 最重要的进步。

3.2 上下文治理:预算分槽、滑动窗口与压缩三层策略

上下文问题的本质是:模型窗口有限,而任务过程的信息量没有上限。v2 的做法是引入一套“预算分槽”的机制,把每一轮 prompt 的总 token 预算拆成四个槽位:

总预算:8K tokens ├── 系统指令槽:1.2K(固定,包含角色与输出格式) ├── 工具描述槽:1.8K(动态裁剪,只保留本阶段可能用到的工具) ├── 历史摘要槽:2.5K(根据阶段从事件日志生成) └── 当前任务槽:2.5K(本阶段的关键变量与用户当前输入)

四个槽位互不挤占,任何一个槽位超限,都不会去偷别的槽的空间,而是触发各自的压缩策略。系统指令槽超限就去精简固定规则;工具描述槽超限就按阶段过滤掉不可能用到的工具;历史摘要槽超限,就要进入下一层处理。

历史摘要槽是最容易超的,实测中最有效的三层策略分别是:先截断最老的工具调用日志,只保留结论摘要;然后对连续出现的同类型事件做折叠合并,比如十次搜索记录合并成一条“已完成对六个来源的检索,结论为……”;最后如果还不够,就调用一次轻量模型把摘要再压缩一版。

关于压缩策略有一个需要重点提醒的地方:不要只截断不总结。我做过对比测试,同样的任务,直接截断老日志的成功率比用摘要替换低不少,因为很多关键信息其实散落在老日志里,摘要至少能把高价值的结论保留下来。摘要是用一次模型的成本,换取后续多轮稳定性和 token 节省,这笔账是划算的。

3.3 成本治理:按环节计费、模型分级和结果缓存

v1 对成本的态度是“算不清就假装它不存在”。v2 把成本量化成了每个执行周期的硬指标。第一步就是按环节计费,把一次 Agent 任务拆成规划、工具调用、摘要压缩、最终生成四个环节,分别记录 token 消耗和费用。这样一跑完就能看出来钱花在哪了。

第二步是模型分级。不是所有环节都需要旗舰模型。规划环节需要最强的理解和推理能力,用最贵、性能最好的模型;工具调用结果的摘要压缩,可以用便宜的小模型替代;最终生成环节如果要求高,再把旗舰模型请出来。实测下来,一个中等复杂度任务里,压缩摘要环节占了不小比例的 token,换成便宜模型后,成本直接砍掉一部分,而输出质量几乎无损。

第三步是结果缓存。同一个工具,在相同参数下返回的结果,在会话级甚至全局级做缓存。比如查询用户订单状态,短时间内不会变化,第二次又查到同一个单,直接命中缓存,不重复调用大模型去理解工具结果。这里的缓存不是模型输出缓存,而是工具结果缓存,能省掉一整轮工具调用的大模型理解成本。

我给一个自己的成本数据对比。v1 阶段,一个典型多步骤任务跑下来,单任务约 12 到 15 次模型调用,消耗 6 万到 9 万 token,成本居高不下。经过分槽预算、模型分级和结果缓存之后,同样的任务降到 5 到 7 次模型调用,单任务消耗稳定在 2.5 万 token 左右,成本降了一半以上。这个数据不是模型变便宜了,而是工程层把浪费堵住了。

4. 我在 v2 工程化路上最常翻的五个坑(附排查方法)

4.1 结构化输出解析失败:模型不听话时的兜底方案

v1 被这句害死过:模型说好的返回 JSON,结果乱加 Markdown 标记,或者字段类型不对。v2 里的第一道防线不是要求模型“必须遵守”,而是写一个足够健壮的解析器。

我的解析器核心逻辑是三层兜底:先做标准 JSON 解析,失败后尝试提取代码块中的 JSON 片段,再失败就做一轮最小修复,比如补全缺失的右花括号、去掉注释、修正裸字符串。如果修复后仍失败,就触发一次“针对性重生成”:把上一次的原始输出和具体报错信息拼回去,请模型重新输出。两次仍失败的,直接把这个事件标记为异常,状态机走人工处理分支,绝不让整个任务卡死。

def safe_parse_agent_output(text): # 第一层:直接解析 try: return json.loads(text) except Exception: pass # 第二层:提取代码块中的 JSON match = re.search(r"```(?:json)?\\s*(.*?)\\s*```", text, re.S) if match: try: return json.loads(match.group(1)) except Exception: pass # 第三层:最小修复 try: return json.loads(minimal_fix_json(text)) except Exception: return None

这个函数看着简单,但价值极高。以前一碰到解析失败,整个 Agent 就崩,现在大多数情况一层就过了,最差的也在两层内解决。唯一要注意的是,第三层的最小修复要克制,只修确定性错误,不要尝试理解语义,否则会修出更隐蔽的问题。

4.2 工具调用超时与限流:外部接口没你想象的那么稳

把 Agent 接上真实外部接口以后,你会发现最不可靠的不是模型,是那些第三方 API。有些接口平均响应两秒,偶尔给你拖三十秒;限流策略各不相同,有的按 QPS,有的按并发。

工具层对每个外部调用统一套上了三层防护:超时控制、重试策略、熔断机制。超时设置必须分阶段,连接超时设置成 3 秒,读超时设置成 5 秒,总超时 10 秒。重试采用指数退避加抖动,第一次等 1 秒,第二次 2 秒,第三次 4 秒,最多三次。连续失败五次以上的工具,直接熔断十秒,期间所有对这个工具的调用立即返回失败,不再白白等待。

限流处理上用了一个简单令牌桶。每个工具独立配额,比如某查询接口 QPS 限制是 5,令牌桶就按每秒 5 个的速率补充。超出配额时,工具层不直接丢弃任务,而是把这个事件标记为“等待重试”,状态机转到延时重试分支。这样做的好处是,模型不会因为限流事件把流程误判为失败。

4.3 重试带来的重复副作用:幂等键是必需品

重试机制解决了稳定性,也带来一个大坑:工具执行成功了,但响应在返回时超时,重试就会把同一个操作执行两遍。这个副作用在查询类工具上问题不大,但在消息发送、订单创建这类操作上是致命的。

v2 在工具合同里加了一个强制约定:所有具备副作用的工具,都必须支持幂等键。每次调用分配唯一的 request_id,工具内部用这个 ID 做去重。如果重试时带上同一个 request_id,工具要能识别并返回第一次执行的结果,而不是再执行一次。

调用侧也要配合:每次模型决定调用某个工具时,这个 request_id 从生成到重试全程不变;只有确认前一次请求彻底失败,才生成新的 request_id。这个机制写起来不复杂,但能让重试策略放心用到三倍,不至于把系统搞出事故。

4.4 回归测试搞起来:Agent 没崩溃不等于没坏

工程化之前,我对 Agent 的验证方式是“跑两个例子看看效果”。这在 demo 阶段没问题,但到了 v2,一个改动可能看着没问题,却让某个任务环节成功率悄悄掉下去。没有回归测试,你根本发现不了这种隐性退化。

我的做法是维护一个小型评测集,初始不用多,三五十条就够。每条用例包含:输入、期望的执行路径、期望的最终结果、关键约束项。比如有一条工单分拣用例,预期操作是调用“分类工具”并转到“已完成”状态,最终结果必须包含分拣理由。每次架构变更后,先跑整个评测集,对比成功率、平均调用轮数、平均 token 消耗三个指标,任何一项发生明显下降,都要追问原因。

评测集跑起来大概花费不多,但提供的信息量很大。它让我从一个“靠感觉判断 Agent 有没有变好”的状态,变成了“看数据说话”。

4.5 可观测性:把每轮决策过程变成结构化日志

v1 的日志是用 print 打的,出问题以后只能看到模型回复了一大段文本,完全不知道内部状态长什么样。v2 给每个执行周期补上了结构化 Trace。

每条事件日志都包含当前阶段、模型输入摘要、模型输出类型、工具名、工具参数、工具结果状态、耗时、token 消耗、状态机转移结果。所有日志带上同一个 request_id,就能把一次任务的完整链路串起来。排查问题不再靠猜,直接按 request_id 拉出整条执行链,一步一眼就能定位到是模型决策错误、工具异常还是上下文裁剪把关键信息丢了。

[req_2f9c4d] stage=collect_matcherial | tool=search_docs | status=success | args={"keyword":"退款政策"} | duration=1.82s | tokens=3120 | transition=collect_matcherial -> draft_answer [req_2f9c4d] stage=draft_answer | model_output=final_answer | status=ok | duration=4.35s | tokens=2100 | transition=draft_answer -> finalized

按这套日志结构走下来,Agent 的每次运行都能复盘,每个失误都能定位到具体环节。工程化后的 Agent 不应该是黑盒,它的每一步决策都应该可以被追溯。

5. 升级后的量化对比与我的整体体会

5.1 一组真实的升级前后对比数据

不看指标的工程化都是自嗨。我把 v1 和 v2 在同一批真实业务用例上的表现放在一起对比:

指标v1v2变化
多步骤任务成功率约 62%约 91%明显提升,失败可恢复
单任务平均模型调用轮数12~15 次5~7 次减少一半
单任务平均 token 消耗6~9 万2.2~2.8 万下降约六成
单任务平均成本基准下降一半以上代码编排让浪费变少
中断后可恢复性无,从头再来支持基于事件日志恢复生产可用关键能力
问题定位时间小时级,靠猜分钟级看 trace可观测性生效

这些数据不是凭空得来的,都是同一批任务在 v1 和 v2 架构上各跑一轮之后汇总的结果。需要说明的是,我这里的指标只针对自己的业务场景,不同场景数值会有差异,但趋势是一致的:代码编排带来的稳定性和成本收益,远超多写几段提示词。

5.2 如果再让我做一次 v2,我会更早开始的三件事

第一件事,先搭评测集再动手改架构。我是在 v2 已经改了差不多一半的时候才补上评测集,导致最开始几个改动没有数据反馈,是摸着石头过河。如果你现在正准备从 v1 升 v2,先花半天把评测集框起来,后面的每一步都能看得见影响。

第二件事,状态机先于提示词工程。v1 时代我花了大量时间调提示词,试图让模型“别乱来”。但问题出在流程上没有硬约束。把状态机的合法路径画出来之后,提示词只需要做一件事:告诉模型在某个阶段能做什么。提示词的复杂度降低了,稳定性反而提升了。

第三件事,把工具层优先做成强契约结构。工具的数量会膨胀,每个工具的输入参数、超时、幂等要求、返回结构都应该在注册时定义清楚。先定契约,再往上堆能力,而不是边写工具边补协议。v1 里那些用着用着就崩的工具调用,根本原因都是契约没定明白。

5.3 什么情况下你不需要学到这个程度

坦白说,不是每个 Agent 项目都需要这套工程深度。如果你的场景是内部单用户使用,任务失败的影响很小,模型调用次数也少,那 v2 这套状态机、预算、幂等、评测体系确实会显得过度设计。我自己的判断标准是:当 Agent 开始被多个真实用户反复使用,或者它的任务结果会直接进业务系统,或者失败后需要人工善后成本很高时,才值得投入这一整套工程改造。

如果你的 Agent 还在“跑给你看”的阶段,先把评测集搭起来,等数据告诉你“能跑”确实不够用了,再顺着这篇里的架构去重构也不迟。

最后说一点个人的体会。v1 升 v2 的过程,表面上看是技术重构,实际上是一次思维方式的转变:从“相信模型能自己搞定一切”到“让模型在明确边界内发挥能力”。模型的能力边界会继续扩展,但工程上的状态管理、成本控制、异常恢复这些约束永远存在。你要做的,是为模型搭好那个可靠的框架,然后放心大胆地把决策权放进框架里。这条路,越早走越稳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询