Agent工程化实战:从Demo到生产的状态机重构与可观测性设计
做 AI Agent 最迷惑人的一句话就是“它已经能跑起来了。”很多项目死在演示阶段不是因为模型能力不够而是因为Demo和交付之间隔着一条巨大的工程鸿沟。这次“Agent v2”的迭代本质上就是一次被真实业务逼着从“原型思维”切换进“工程思维”的完整记录。如果你也在做 Agent 相关的东西正好卡在“能跑”和“能用”之间那这篇实录应该能给你一些参考。先说背景v1 版本是我花了一周做的原型用来验证“大模型自动拆解任务并调用工具”这条链路是否可行结果非常理想——模型能理解需求、能规划步骤、能调API、能输出结果演示效果相当惊艳。但当我试图把它推上真实环境面对多用户并发、长时间运行、异常恢复这些问题时v1 几乎是瞬间崩塌的。于是才有了 v2 这轮重构。与其说 v2 是功能的升级不如说是一次彻底的“账务清算”把过去那些“先能跑再说”的债一次性还完。这篇文章会把拆解思路、架构调整、关键实现细节、踩坑记录全部整理出来适合那些正在把 Agent 从一个 Demo 推向生产环境的人。1. 为什么「能跑」突然变得不够用了1.1 从“能跑”到“能交付”之间的那条鸿沟如果你只在一对一演示环境里验证过 Agent大概率会觉得一切都很顺利输入一个需求Agent 按步骤调用工具最后给出一个正确的回答。但真实的用户场景是另一回事——他们会输入模糊到极致的指令、会在中途反复修改需求、会在任务执行到一半直接关闭页面甚至同时开几个会话让多个 Agent 任务并发跑。我的 v1 是典型的“只管主流程”实现。它假设模型一定会按照预期调用工具工具一定会正常返回用户一定会等待结果——这等于默认系统永远不会出错。真实环境把这些假设全部击碎了模型偶尔会不按预定的工具格式输出第三方 API 偶尔会超时甚至直接挂掉用户的输入偶尔会触发模型陷入自我循环。每一条“偶尔”在生产环境里都会被放大成“常态”。所以 v2 要回答的不是“模型能不能完成任务”而是在模型答错、工具失败、用户乱操作这些情况下系统如何保持可控、可追踪、可恢复。这个转变决定了整个架构都会推翻重来。1.2 v1 时代欠下的那些“技术债”谈 v2 之前我先把 v1 的几个致命问题摊开说这些都是促使我转向工程化的直接原因所有状态都在内存里。一个会话的所有上下文都存在一个全局字典里进程一重启全部丢光。单用户测试没问题一旦用户量上来内存直接被撑爆而且任务执行到一半如果服务重启用户只能重新开始。上下文无边界膨胀。每轮对话都把完整历史送给模型没有截断、没有摘要、没有窗口控制。前几轮还能正常跑第 20 轮对话的时候一次请求可能要送几万个 token响应速度慢得离谱单次调用的成本也成倍上涨。没有失败恢复机制。工具调用一旦超时整个流程直接抛异常用户看到的就是一句“系统错误请重试”。没有重试策略、没有降级方案、没有检查点任务全部要推倒重来。任何形式的可观测性都为零。日志靠 print调用链根本看不出问题出在“模型理解错了”还是“工具执行失败了”。一旦出错排查基本靠猜。2. Agent v2 的整体设计思路一次性把地基打对2.1 架构选型从“单函数调用链”到“可观测的状态机”v2 架构上最大的变化是把原来的“单线程调用链”彻底改成了显式状态机。这是我认为 Agent 工程化最关键、也最容易被低估的一步。为什么状态机合适因为 Agent 的本质是一个循环感知输入-模型决策-调用工具-观察结果-再次决策。这个循环天然适合用状态节点来表示。用状态机有两个直接的好处第一每一轮执行到哪个节点一目了然运维同学不需要理解大模型也能从状态转移看出系统正在做什么第二状态节点之间可以挂载超时、重试、回调、监控等机制系统能力可以很方便地在状态层面扩展而不是侵入到业务代码里。我设计的核心状态大致如下IDLE等待用户输入初始状态。PLANNING模型正在解读输入内容生成执行计划。EXECUTING逐个执行计划中的工具调用步骤。OBSERVING观察工具返回结果判断下一步动作。REASONING模型根据观察结果进行推理决定继续执行还是收尾。RETRYING工具调用失败后的等待重试状态带退避策略。FINISHED正常完成产出最终回答。FAILED多次重试仍失败进入终态。TERMINATED用户主动取消或系统强制中止。每个状态之间的转移条件都是显式声明的。比如EXECUTING超时 30 秒没有返回就自动跳转到RETRYING重试三次仍然失败则进入FAILED。这套设计带来的直接效果是整个系统行为从“不可预期的黑盒”变成了“可以任意观测的白盒”我可以在任何节点插入日志、指标采集和故障恢复逻辑而不需要把代码翻个底朝天。2.2 工具层设计三个关键取舍v2 里我把工具层彻底重写了核心围绕三个原则第一工具协议标准化。v1 时代每个工具都有自己的入参格式和返回结构模型经常在“读不懂另一个工具的输出”上翻车。v2 统一用 JSON Schema 定义每个工具的入参工具的返回值也强制统一成一个包裹结构{success: true/false, data: {...}, error: {code: ..., message: ...}}。这一步看起来是形式主义但实际作用非常大模型在决策时面对的是一套统一格式犯错的概率大幅下降排查问题的时候所有失败案例都能用同一套逻辑去分析。第二工具必须做到单一职责。v1 里我写过一个“全能工具”它接收一个 action 参数内部用一堆 if-else 分派到不同的逻辑。设想很好实际执行的时候模型经常传错参数因为一个工具承载的意图太多了。v2 把这些拆成独立的工具函数让模型的选择空间更小、每个选择的目标更明确准确率立刻就有了可感知的提升。第三工具结果必须裁剪。模型对超长工具返回的处理能力是有限的还把几万字的原始数据直接塞给模型既浪费 token 又容易产生注意力漂移。v2 在每个工具层加了一个结果后处理钩子强制对返回内容做摘要、截断或结构化压缩再喂给模型继续推理。2.3 数据隔离与并发控制多用户场景下的第一道坎v1 的全局内存字典架构在并发场景下基本等于裸奔用户 A 的上下文可能被用户 B 的请求覆盖多个任务同时执行时状态互相污染。v2 把数据层拆成三个独立维度会话隔离每个会话有独立的上下文空间和状态机实例互不读取、互不影响。会话数据定期持久化保证进程重启后可以恢复。任务隔离同一个会话内的多个任务比如用户同时让 Agent 做两件独立的事情各自维护自己的执行状态只有共享的会话上下文是并发的交叉点。资源配额在会话和用户层面分别做并发限制防止某一个用户的任务占满所有计算资源拖垮整个服务。并发控制上我用了类似令牌桶的思路控制同一时刻活跃的执行单元数量。超出阈值的请求进入队列排队。这套机制保证的是在高峰流量到来时系统表现为“排队等待”而不是“直接雪崩”。3. 核心工程环节的实操拆解3.1 异步任务队列与流式输出的落地v1 是纯同步的实现请求进来阻塞等待模型返回再返回给前端。这种方式在本地看起来没问题但它一次性阻塞的时间可能长达几十秒而且超时即失败前端体验极差。v2 改成了两段式结构提交接口 执行引擎。提交接口收到请求后立即返回一个task_id执行引擎拿到任务后放入队列后台异步消费执行。前端通过 WebSocket 订阅任务状态变更实时收到每个执行阶段的事件流。执行引擎内部有核心的几个组成部分消息队列不要把队列想得太复杂早期直接用基础设施里现成的能力即可比如数据库表里的任务状态加上轮询或者用现成消息队列核心是保证消息不丢、可重试。工作进程池固定数量的 worker 并发消费队列每个 worker 处理一个任务时按状态机推进。事件发布器每个状态迁移都发布一个事件任务开始、模型调用中、工具调用中、工具返回、完成、失败前端和监控系统都订阅事件流。检查点机制每个关键状态执行成功后就持久化一次任务快照进程崩溃后下次启动可以恢复到最近的进度点而不是全部重新跑。流式输出这里有一个容易被忽略的坑不只是把模型生成的文字流式推给前端工具调用的状态变化也要实时推送。用户看到“正在调用天气查询工具...”“工具返回了以下数据...”这些过程信息比干等三十秒然后刷出一段完整回答的体验好非常多而且能让用户在长任务中感知到系统“确实在干活”。3.2 提示词与上下文管理的工程化改造这个环节是 v2 改造中性价比最高的部分。v1 把提示词直接写在代码里每次调整都要发一次版。v2 我把它拆成了三个层次系统级提示词System Prompt定义 Agent 的基本人设、能力边界和工作纪律单独维护不许内联到业务代码里。场景级模板Scenario Template根据不同的任务类型问答、工具调用、代码生成、总结分析预置不同的模板结构包含该场景特有的输出格式和要求。动态上下文Dynamic Context运行时根据会话历史动态生成的上下文内容是唯一会随请求变化的部分。上下文长度的管理是 Agent 工程化绝不开的一道坎。我的策略是分级处理小窗口内用完整原文中窗口用压缩摘要大窗口只保留关键事实。具体流程如下会话开始把用户最近几轮完整对话加入上下文。一旦累计 token 数超过阈值触发对“较早历史”的自动摘要用摘要块替换原始文本。摘要本身再超限则把更早期的摘要做二次压缩只留核心事实列表。所有上下文块都带上产生时间模型回答时明确要求引用信息来源防止摘要后出现事实漂移。这套机制跑了一个多月效果非常明显上下文的 token 开销下降了约 55%长会话场景下任务完成率反而有所上升因为模型聚焦在有效信息上不再被冗长的历史记录干扰。3.3 可观测性与日志链路Debug 的救命稻草这是 v2 工程化改造中我最后悔没有早一点做的事。因为 Agent 的每一个决策都是模型“猜”的出了问题你很难说是猜错了、工具坏了还是数据脏了。没有好的观测手段排查问题就像在迷宫里找出口。我落地了两层观测能力。第一层链路追踪。每个任务从创建开始生成一个全局唯一的trace_id这个 ID 贯穿所有日志、事件、耗时统计和费用记账。执行引擎里每经过一个状态节点就记录一条带trace_id的结构化日志。日志里至少包含节点名称、触发条件、耗时、模型调用信息模型名、输入摘要、输出摘要、token 用量、工具调用信息工具名、参数、返回状态码、关键结果摘要。第二层关键指标采集。每个节点都要记录耗时和成功率。这些指标汇总来看你能非常清楚地发现瓶颈比如某个工具的平均调用耗时明显比其他工具高比如特定类型任务的失败率集中在某个状态转移上。有一次我发现用户抱怨任务经常卡住从指标上看是OBSERVING节点平均耗时在增长追下去发现是一个第三方接口在晚高峰时段响应变慢——如果没有指标这种问题很难从分散的日志里定位出来。另外v2 我加了一个非常有用的调试功能复盘回放。每个任务结束之后把完整的决策轨迹按时间线呈现出来——模型每一步看到了什么、选择了什么、工具返回了什么——以类似回放的方式逐帧查看。开发时联调、上线后查问题这个功能救了我太多次了。3.4 成本控制与模型路由策略Agent 和普通 ChatBot 在成本上有本质区别一轮任务可能触发十几次模型调用而且全部是长输入长输出。如果不做控制单个任务的 API 费用可以是普通问答的数十倍。v2 在这块做了三个层面的设计第一模型分级路由。根据任务复杂度决定用哪个模型。简单分类任务、抽取任务用低成本高速模型复杂推理、长文生成才用顶级模型。判断方式不是硬编码而是在提示词里要求模型先输出一个difficulty评估字段系统据此做模型路由当然前端也可以主动指定越级调用。第二上下文瘦身前置。所有将要发送给模型的文本在发送前都要经过压缩层。工具返回结果是大头前置裁剪规则在工具层已经强制生效。这部分测算下来节省了大约 35%-40% 的 token 开销。第三预算配额与熔断机制。每个会话和每个用户都设了 token 预算上限超出后自动降级为简化模式比如禁用工具调用、只能走简单问答或者直接拒绝并提示联系管理员。同时对接了费用账单接口做实时监控费用异常增长时第一时间收到告警。4. 实测中踩过的坑与排查技巧实录4.1 典型故障案例超时、幻觉与死循环案例一工具超时进程直接崩溃。上线后没几天有个依赖外部服务的工具突然响应变慢单次调用超过了 60 秒。v1 架构是同步的主线程直接抛出超时异常任务全部被终结。v2 里我在EXECUTING状态加了超时保护和重试机制状况立刻得到控制。这类问题让我认识到Agent 的“健康”不能依赖底层工具“稳定”只能默认工具会挂然后在状态层做防护。案例二模型在循环里反复调用同一个工具。某次实测用户问的产品问题比较模糊模型返回一个计划后又反复调用某个搜索工具每次结果都差不多但模型就是停不下来token 消耗蹭蹭往上涨。v2 我在状态机里加了个“循环检测器”如果模型连续 3 次调用同一个工具且入参完全相同就强制中断并让模型重新思考当前路径是否有效。这个规则不需要复杂算法就是简单的暴力检测但效果显著。案例三上下文被工具错误输出污染导致幻觉。一次排查时发现模型把某个工具返回的异常提示一段错误堆栈当成了正常业务数据在此基础上继续推理产出了一段看起来很自信、实际完全错误的回答。根本原因是工具的返回值里混入了错误信息模型理解不了“这是我该展示给用户的错误”还是“这是我该用于推理的事实”。修复方案也很直接在工具的返回结构里把success标志和错误信息做成强约束的独立通道模型只基于data字段推理错误信息单独展示给用户不进推理上下文。4.2 常见问题速查表症状可能原因排查路径解决方案任务卡在某个状态不推进工具调用超时、模型响应挂起查看该节点耗时指标、链路日志给状态节点加超时和重试确认无限等待的根因模型反复执行相同操作上下文里没有明确“停止条件”回放决策轨迹检查指令中是否有明确的终止条件提示词里强制声明循环检测规则 代码层检查重复调用回答出现事实性错误工具错误信息混入推理上下文检查工具返回结构看 data 是否干净错误信息与推理数据强隔离模型只拿 data 做推理长会话后响应越来越慢上下文无收敛的无限膨胀检查每次请求的 token 数量和响应耗时落地摘要压缩和滑窗裁剪策略高并发时系统整体卡顿没有全局配额控制查看 worker 负载和队列堆积引入令牌桶和队列限流隔离大任务费用某个时段异常上涨存在死循环或未限制的单任务 token 消耗按 trace_id 分析单任务 token 稳定性加循环检测 会话级费用熔断几条经验后续扩展空间比较大。比如模型路由这层后续可以引入动态反馈根据历史执行质量自动调整不同任务的路由策略再比如上下文摘要这块可以采用多级摘要甚至向量化记忆让长线信息真正能跨会话复用。工具层则可以做插件式的热更新让新工具的接入不需要停机发布。最后再分享一个个人的深刻教训一个 Agent 系统如果你的第一版设计里没有专门考虑“出错了怎么办”那这个系统大概率活不过真实环境里的一周。踩过这一轮坑之后我的一个原则成了“把 Agent 当社会人来看它随时会做出你意想不到的事情而工程的任务不是期待它不发生而是提前想好它发生了你怎么反应。”把可观测性做在前面把失败当作正常路径来设计后续的每一次迭代都会轻松很多。这就是 Agent v2 这轮重构我想最值得分享的心得。