1. 半年卡壳的真相:AI Agent 从 Demo 到生产之间隔着什么
去年秋天我接了一个内部工具项目,目标很明确:做一个能自动处理工单的 AI Agent,用户提交问题后,Agent 自主判断类型、检索知识库、调用内部 API 完成操作、最后回复用户。Demo 阶段一切顺利,LangChain 串起来跑得挺欢,本地测试通过率看着也不错。我当时觉得这事儿稳了,顶多再花两周做做工程化就能上线。
结果这一卡就是半年。
问题不是出在模型能力上,而是出在从“能跑”到“敢用”之间那条看不见的鸿沟。Demo 里 Agent 调错一次 API 无所谓,重跑就行;生产环境里 Agent 调错一次,可能就是一条错误数据写进数据库,或者一个不该发的通知发给了客户。Demo 里并发是 1,生产环境里并发是几十上百,Agent 的状态管理、超时控制、重试策略全都要重新设计。Demo 里工具只有三五个,生产环境里工具可能有几十个,Agent 怎么在众多工具里选对那个正确的,本身就是个难题。
这半年我踩的坑大致可以归成几类:并发下的状态污染、工具调用的可靠性、多轮对话的上下文管理、可观测性缺失导致排查困难、成本失控。每一个单独拎出来都不算特别难,但它们叠在一起,就形成了一个让人头疼的系统工程问题。我翻了不少文档,看了不少开源项目,但总感觉缺一个能把这些问题串起来讲清楚的地方——大多数资料要么停留在“怎么搭一个 Demo”,要么直接跳到“我们内部怎么做的”语焉不详。
所以当我看到 iRTE2026 的议程里有一整条关于 AI Agent 工程化的 track 时,我几乎没有犹豫就决定必须去。不是为了听概念,而是想看看那些真正把 Agent 跑在生产环境里的团队,是怎么处理这些我卡了半年的问题的。下面我把这半年卡壳的具体位置、我尝试过的方案、以及我期待在 iRTE2026 上找到答案的方向,完整地梳理一遍。
2. 并发场景下 Agent 状态管理的三种翻车方式
2.1 共享内存状态:最隐蔽的并发杀手
我最初的设计很自然:Agent 对象在应用启动时初始化一次,所有请求复用同一个实例。本地测试完全没问题,因为同一时间只有一个请求。上线压测的第一天就炸了——两个用户同时提问,Agent 的对话历史串了,A 用户看到了 B 用户的上下文。
根因很直接:Agent 内部维护了一个conversation_history列表,每次请求往里追加消息。并发请求进来时,两个线程同时读写这个列表,轻则上下文错乱,重则列表在迭代时被修改直接抛异常。更麻烦的是,有些工具调用会修改 Agent 的内部状态(比如“当前步骤索引”),并发时这个索引会被互相覆盖,导致 Agent 执行到一半跳到了另一个请求的步骤上。
我试过的修复方案:
- 每次请求新建 Agent 实例:最直接,但初始化开销大,尤其是需要加载工具描述、连接向量库的时候,单次初始化可能就要几百毫秒。
- 用线程局部存储隔离状态:能解决线程安全问题,但在异步框架下(比如 FastAPI 的 async 路由)线程模型和协程模型混用,容易出更诡异的问题。
- 把状态外置到 Redis:每次请求从 Redis 读状态、处理后写回。这个方案最稳,但引入了序列化开销和网络延迟,而且需要处理并发写冲突。
最后我采用的是“请求级 Agent 实例 + 连接池复用底层资源”的方案:Agent 对象本身轻量化,每次请求新建,但向量库连接、HTTP 客户端这些重资源通过连接池共享。这样既隔离了状态,又避免了重复初始化的开销。实测下单次请求的额外开销从 300ms 降到了 20ms 以内。
这里有个容易忽略的点:即使 Agent 实例是请求级的,如果工具函数里用了全局变量或者类变量来缓存数据,并发时照样会出问题。我后来养成了一个习惯,所有工具函数的实现里,只要涉及写操作,一律先问自己“这个变量在并发下安全吗”。
2.2 异步任务的状态追踪:谁在什么时候改了状态
工单处理场景里,有些操作是异步的——比如 Agent 调用了一个“提交审批”的 API,这个 API 返回“已受理”,但实际审批结果要几分钟后才出来。这时候 Agent 不能干等,得先把当前对话挂起,等审批结果回来后再继续。
我一开始用数据库存了一个pending_tasks表,Agent 把任务 ID 写进去,然后有个后台 worker 轮询审批结果,拿到结果后更新任务状态,再触发 Agent 继续执行。听起来没问题,但实际跑起来发现:Agent 恢复执行时,它需要的上下文可能已经变了。比如用户在这几分钟里又发了新消息,或者 Agent 之前调用的另一个工具的结果已经过期了。
更麻烦的是状态机的管理。Agent 的执行流程不是线性的,有分支、有循环、有并行。我用一个state字段来标记当前处于哪个阶段,但很快这个字段就变成了一个巨大的枚举,每加一个功能就要加几个状态,状态之间的转换逻辑散落在各处,改一处忘一处,bug 层出不穷。
后来我参考了LangGraph的思路,把 Agent 的执行流程显式建模成一张状态图:节点是执行步骤,边是转换条件,状态是一个结构化的对象而不是一个枚举字段。这样每个节点的输入输出都明确,状态转换由图的定义统一管理,排查问题时可以直接把当前状态 dump 出来看卡在哪个节点。这个改造花了我将近三周,但改完之后,异步任务相关的 bug 减少了大概八成。
2.3 多轮对话的上下文窗口:截断策略决定用户体验
多轮对话是 Agent 的标配,但上下文窗口是有限的。我最初的做法很简单:超过 token 限制就从最老的消息开始删。结果用户发现 Agent “失忆”了——前面刚说过的约束条件,聊了几轮之后 Agent 就忘了。
我试过几种策略:
| 策略 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 滑动窗口 | 保留最近 N 轮 | 实现简单 | 早期关键信息丢失 |
| 摘要压缩 | 把早期对话用模型总结成一段话 | 保留关键信息 | 摘要本身消耗 token,且可能丢细节 |
| 关键信息抽取 | 从对话中抽取结构化信息单独存储 | 精准 | 需要定义抽取规则,维护成本高 |
| 混合策略 | 近期原文 + 中期摘要 + 远期关键信息 | 平衡 | 实现复杂 |
我最后用的是混合策略:最近 5 轮保留原文,5 到 20 轮用模型压缩成摘要,20 轮以上的只保留抽取出来的关键实体和约束条件。这个策略在实测中效果不错,但摘要的生成时机很关键——不能每轮都重新生成,那样太贵;也不能等窗口满了才生成,那样会丢信息。我的做法是每 5 轮触发一次增量摘要,把新产生的对话合并到已有摘要里。
这里有个坑:摘要本身也是模型生成的,如果摘要里出现了幻觉,这个错误会被后续所有轮次继承。我的应对方式是摘要生成时要求模型只做“压缩”不做“推理”,并且保留原始对话的引用 ID,出问题时可以回溯。
3. 工具调用可靠性:从“能调”到“调对”的距离
3.1 工具描述的质量直接决定调用准确率
我一开始写工具描述很随意,比如一个查询订单的工具,描述就写“查询订单信息”。结果 Agent 经常在用户问“我的快递到哪了”的时候调用这个工具,然后发现返回的是订单状态而不是物流信息。后来我把描述改成了“根据订单号查询订单的支付状态、金额、创建时间,不包含物流信息。查询物流请使用 query_logistics 工具”,调用准确率立刻上去了。
这件事让我意识到,工具描述不是给人看的文档,是给模型看的 prompt。它的写法直接决定了模型能不能在正确的时机选对工具。我后来总结了几条经验:
- 描述里要明确说明“这个工具做什么”和“不做什么”,尤其是容易混淆的工具之间要互相引用。
- 参数描述要写清楚格式和约束,比如“订单号必须是 18 位数字字符串”,而不是“订单号”。
- 如果工具调用有副作用(比如写操作),描述里要明确标注,让模型知道这个操作不可逆。
3.2 工具调用的超时、重试与幂等性
生产环境里,工具调用失败是常态而不是异常。网络抖动、下游服务限流、数据库连接池满,这些都会导致工具调用失败。我最初的做法是失败就重试三次,结果发现有些写操作重试后产生了重复数据。
后来我把工具分成了三类:
- 只读工具:失败后可以安全重试,设置指数退避策略,最多重试 3 次。
- 幂等写工具:调用时需要传入一个幂等键,下游服务根据幂等键去重,可以重试。
- 非幂等写工具:失败后不能自动重试,必须把错误抛给 Agent,由 Agent 决定是告知用户失败还是尝试补偿操作。
超时设置也是个学问。我一开始统一设了 30 秒超时,结果发现有些查询工具正常就要 20 多秒,经常触发超时重试,反而加重了下游负担。后来我按工具类型分别设置:缓存类查询 2 秒,数据库查询 10 秒,外部 API 调用 15 秒,批量操作 60 秒。这个配置不是拍脑袋定的,是统计了每个工具 P99 耗时之后再加 50% 余量算出来的。
3.3 工具返回结果的结构化处理
工具返回的结果格式五花八门,有的是 JSON,有的是纯文本,有的是 HTML。Agent 拿到这些结果后需要理解并决定下一步。我最初直接把原始返回塞给模型,结果模型经常被无关信息干扰,比如 HTML 里的样式代码。
后来我加了一层“结果规范化”处理:每个工具都配一个结果解析器,把原始返回转换成统一的结构化格式(通常是 JSON),并且只保留关键字段。比如物流查询工具返回一大坨 JSON,我只提取“当前状态”“当前位置”“预计到达时间”三个字段给模型。这样不仅减少了 token 消耗,也提高了模型的理解准确率。
这里有个细节:规范化处理本身也可能失败,比如下游返回了非预期的格式。我的做法是规范化失败时,把原始返回截断到前 500 字符直接给模型,同时在结果里标注“解析失败,以下是原始返回”,让模型自己判断。这比直接抛异常要好,因为有时候模型能从原始文本里提取出有用信息。
4. 可观测性:Agent 出问题时你怎么知道发生了什么
4.1 传统日志在 Agent 场景下的不足
我一开始用标准的应用日志,每个步骤打一行 log。出问题时去翻日志,发现根本串不起来——一个请求的日志散落在几十行里,中间还夹杂着其他请求的日志。更麻烦的是,Agent 的决策过程是“模型想了什么”,这个信息在传统日志里完全没有。
后来我引入了Trace ID,每个请求生成一个唯一 ID,所有相关日志都带上这个 ID。这样至少能把一个请求的日志串起来了。但还不够,因为 Agent 的决策链路很长,我需要知道每一步的输入输出是什么、模型为什么选了那个工具、工具返回了什么、模型下一步又决定了什么。
4.2 构建 Agent 专属的追踪体系
我最终的方案是记录一个结构化的“执行轨迹”,每个步骤包含:
- 步骤类型(模型推理 / 工具调用 / 结果处理)
- 输入(模型看到的完整 prompt 或工具收到的参数)
- 输出(模型的原始返回或工具返回的结果)
- 耗时
- token 消耗
- 如果出错,错误信息和堆栈
这个轨迹存到 MongoDB 里,每个请求一条文档。排查问题时,直接按 Trace ID 查,整条链路一目了然。我还做了一个简单的可视化页面,把轨迹按时间轴展示出来,一眼就能看出卡在哪一步、哪一步消耗的 token 最多。
这套东西做下来大概花了两周,但带来的收益是巨大的。以前排查一个线上问题可能要一两个小时,现在通常十分钟内就能定位到根因。而且有了这些数据,我还能做优化——比如发现某个工具的调用频率特别高但成功率很低,就可以针对性地优化那个工具。
4.3 关键指标监控:不只是成功率
除了追踪,我还建了一套监控指标:
- 任务完成率:用户请求最终被成功处理的比例。
- 平均步数:完成一个任务平均需要多少步,步数突然增加往往意味着模型开始“绕路”。
- 工具调用分布:各个工具的调用次数和成功率,能发现异常调用模式。
- Token 消耗分布:按请求类型统计 token 消耗,找出“贵”的请求类型。
- 端到端延迟:P50、P95、P99 延迟,以及各步骤的耗时占比。
这些指标里,我觉得最有价值的是“平均步数”。正常情况下,处理一个工单平均需要 4 到 6 步。如果某天这个数字突然涨到了 10 步以上,说明模型在某个环节卡住了,可能是工具返回了非预期结果,也可能是 prompt 被改坏了。这个指标帮我提前发现了好几次潜在问题。
5. 成本控制:Agent 跑起来之后账单有多吓人
5.1 Token 消耗的主要来源
我统计了一下,token 消耗的大头有三个:
- 系统 prompt:包括 Agent 的角色定义、工具描述、约束条件。这部分每次请求都要带上,如果工具多、描述长,光系统 prompt 就可能上千 token。
- 对话历史:多轮对话下,历史消息会不断累积。
- 工具返回结果:尤其是返回大量文本的工具,比如文档检索。
我最初系统 prompt 写了将近 2000 token,后来精简到了 800 左右,主要是把一些不必要的形式化描述去掉了,工具描述也从“完整句子”改成了“关键词 + 约束”的紧凑格式。这一项就省了大概 30% 的 token。
5.2 模型选择的性价比权衡
不是所有步骤都需要用最强的模型。我把 Agent 的执行分成了两类步骤:
- 决策步骤:需要模型理解复杂意图、选择工具、规划下一步,用能力强的模型。
- 格式化步骤:比如把工具返回的 JSON 转成自然语言回复,用便宜的小模型就够了。
实测下来,这种混合策略能省 40% 到 60% 的成本,而用户体验几乎没有差别。因为用户感知到的“智能”主要来自决策步骤,格式化步骤只要不出错就行。
5.3 缓存策略:哪些结果可以复用
有些工具调用的结果是相对稳定的,比如“查询产品目录”“获取帮助文档”,这些结果可以在短时间内缓存。我加了一层缓存,key 是工具名 + 参数哈希,TTL 根据数据更新频率设置。产品目录缓存 1 小时,帮助文档缓存 24 小时。这一项又省了大概 15% 的工具调用次数。
但缓存也有坑:如果缓存了用户相关的数据,比如“查询我的订单”,那就不能跨用户复用。我的做法是给每个工具打一个“是否用户相关”的标签,只有非用户相关的工具才走缓存。
6. iRTE2026 上我想找到答案的几个问题
6.1 多 Agent 协作的工程化实践
单 Agent 的场景我算是摸清楚了,但多 Agent 协作——比如一个负责理解意图、一个负责执行、一个负责审核——在生产环境里怎么落地,我还没有太多头绪。Agent 之间怎么通信、怎么共享状态、怎么处理一个 Agent 失败的情况,这些都是我期待在 iRTE2026 上听到实战分享的方向。
6.2 Agent 的评测与回归测试
Agent 的行为是概率性的,同样的输入可能产生不同的输出。这就带来一个问题:怎么评测一个 Agent 好不好?怎么保证改了 prompt 或换了模型之后,原有功能没有退化?我现在用的是“黄金测试集”——收集一批典型请求和预期结果,每次改动后跑一遍看通过率。但这个方法覆盖有限,而且预期结果本身也不好定义。我很好奇其他团队是怎么做 Agent 评测的。
6.3 安全与权限控制
Agent 能调用工具,就意味着它能执行操作。怎么确保 Agent 不会执行越权操作?怎么在 Agent 调用工具前做权限校验?怎么审计 Agent 的每一个操作?这些在企业级场景里是绕不开的问题。我目前的方案是在工具层面做权限校验,每个工具调用前检查当前用户的权限。但这还不够细,比如“查询订单”和“修改订单”可能需要不同的权限级别,而 Agent 可能在一次任务里连续调用这两个工具。我期待在 iRTE2026 上看到更系统的方案。
7. 去之前我做的准备:带着具体问题去,别只听热闹
上次参加技术会议,我犯了一个错误:听了很多场分享,觉得每个都很有道理,但回来之后发现什么都没落地。这次我提前做了准备,把半年卡壳的问题整理成了一个清单,每个问题都写清楚了“我试过什么”“卡在哪里”“期望听到什么”。这样在会场的时候,我可以有针对性地选择场次,并且在 Q&A 环节直接问具体问题,而不是泛泛地听。
另外我也准备了一些自己的数据——比如并发场景下的性能对比、不同摘要策略的效果差异、成本优化的前后对比。这些数据在和其他参会者交流的时候很有用,因为大家都是在做实际项目的人,有具体数据更容易聊到点子上。
如果你也在做 AI Agent 相关的项目,并且遇到了类似的工程化问题,我建议你去之前也把自己的问题整理一下。不用很正式,就是列一个清单:我现在卡在哪、我试过什么、我还想试什么。带着这个清单去,收获会比漫无目的地听要大得多。
最后说一个我自己的体会:AI Agent 这个领域,Demo 和生产的差距比大多数技术领域都要大。因为 Agent 的行为是概率性的,而生产环境要求的是确定性。这个矛盾没有银弹,只能靠工程手段一点点磨。iRTE2026 对我来说最大的价值,就是能看到其他人是怎么磨的,少走一些弯路。