Demo能跑、上线就崩,几乎是每个做Agent的团队都会撞上的墙。我在过去一年多里搭过好几个Agent原型项目,从最早的单机ReAct循环,到后来带记忆、带工具调度的多Agent系统,最深的感受是:跑通一个Demo只需要一个晚上,让它稳定扛住生产流量却需要一整个架构级的重构。
这篇文章是Agent进阶系列的第一课,我会结合自己做过的模拟项目X和几轮重构经验,把Demo到生产之间那几道最容易被忽视的隐性壁垒拆开来讲。适合那些已经能用LangChain或写原生代码跑通Agent Demo、但还没上过线的开发者,也适合被线上事故反复折腾过、想系统梳理一遍架构思路的读者。
1. 现象背后:Demo到生产之间到底藏着什么
1.1 从“能跑”到“能扛”的距离
先给一个我自己的判断:Demo的本质是证明“可行”,生产的本质是保证“可控”。这两个字之差,体现在代码上可能是十倍的复杂度。
比如说,你本地跑一个Agent测试题,给模型配上两个工具函数,问它“帮我查一下昨天的销售数据并生成总结”,它能顺滑地调用工具、拼装上下文、输出答案。整个过程不超过10秒,看着非常丝滑。但同样的逻辑搬上线,面对的是多用户并发、外部API延迟抖动、上下文积累导致token爆炸、模型偶发格式错乱……这些在Demo阶段都遇不到,或者偶尔遇到你也不会在意。
我见过不止一个团队,Demo阶段花了三天,上线调试却拖了三周。不是模型能力不行,而是工程化的功课没做够。所以我们先要把“能跑”和“能扛”之间那几道壁垒一条条掰开来看。
1.2 Demo环境的“温室效应”有哪些
类比一下,Demo环境就像温室大棚,生产环境就像野外露天。大棚里你控制温度、湿度、光照,苗长得挺好;搬出去之后,风霜雨雪虫害全来了。
具体到Agent开发里,这个大鹏效应体现在几个方面:
- 网络环境:本地调用模型、调用工具API都是低延迟,甚至内网直连,但生产环境里外部服务的p99延迟可能翻好几倍。
- 数据规模:Demo语料和query可能就那么几千条,你甚至把测试样例背下来了。生产环境的请求分布千奇百怪,Prompt里什么样的话都有。
- 并发模型:本地是单线程串行调,一次一个请求。生产是几十上百并发,模型API有速率限制,工具服务有容量上限。
- 状态持久性:Demo里每一次调试都是全新会话,Agent不记得上次说了什么也无所谓。但生产场景下,用户的中途离开、断线重连、多轮纠缠后恢复上下文,都是刚性需求。
这四条是天然的牵制,基本决定了Agent系统在上线前必须补上哪些底层能力。
2. 第一道壁垒:状态管理从“隐形”到“显形”
2.1 无状态假设是最常见的翻车点
很多人在写Agent Demo时,代码里根本没有“状态”这个东西。每次对话进来,把消息拼成上下文,丢给LLM,得到回复就返回。就算有记忆,也只是会话窗口内的滑动messages列表。这在Demo阶段完全够用,因为你的用户就是你自己。
可一旦上生产,状态问题立刻浮出水面。举个例子:用户和Agent聊到一半,因为网络波动断开重连,服务重启了或者会话路由到了另一台机器,Agent彻底忘了之前聊了什么。用户问“刚才你给我的那个方案,再细化一下第二点”,Agent一脸茫然。这种体验,用户只会觉得你做了个假AI。
我自己的模拟项目X早期就吃过这个亏。当时把messages存在内存里,一重启什么都没了。后来不得不引入外部存储,把会话历史、用户偏好、任务上下文全部持久化,才算稳住。
2.2 生产级状态管理的三要素
要支撑生产环境,Agent的状态管理至少要覆盖三个维度:
- 会话状态(Session State):当前对话的上下文、用户意图拆解、正在执行的任务流程。建议结构化存储,而不是只存一段JSON。任务阶段、已选参数、下一步动作这些字段要可查询。
- 用户状态(User State):长期不变或低频变化的偏好、权限、历史结论。这部分要和会话状态分离,避免每个请求都重复加载。
- 全局状态(Global State):比如Agent内部维护的定时任务、跨会话共享的缓存、编排调度用的队列状态。
这三元组的区分不只是存储层面的,更是逻辑层面的。会话状态要快,用户状态要准,全局状态要稳。
2.3 事件溯源与快照策略
如果你做的是复杂业务Agent,只存最终状态是不够的。因为LLM的每一步推理都可能出错,一旦出错,你需要能回滚到某个中间阶段重新执行。
我在项目X里采用了“事件溯源(Event Sourcing)”的变体:把Agent每次动作(工具调用、消息回复、状态转移)都追加到事件日志里,然后定期打快照。回答用户时读的是快照,但排查问题时看的是完整事件流。
这样做最大的好处是“可回放”。线上出了事故,我能拉出那一次会话的事件流,一步一步看模型到底执行了什么,在哪一步开始跑偏。没有这套东西,排查Agent问题基本靠猜。
提示:事件日志是无脑的append-only,不要做原地修改。快照频率建议根据事件量级设定,单会话超过50步就强制打包一次快照,否则后续恢复成本会很高。
3. 第二道壁垒:工具调用的不确定性处理
3.1 Demo里“永远成功”的工具
Demo中的工具调用基本上都是预埋好的返回,你让模型查天气,天气函数必然返回晴天;你让模型调订单接口,订单接口必然返回201。但真实世界的工具调用,失败才是常态。
- 第三方API超时
- 上游HTTP 500
- 参数进入数据库后被拒绝
- 权限token过期
模型遇到这些错误时,如果工具返回的是一段晦涩的异常堆栈,它往往会“编造”一个合理的结果来填坑。这是LLM的天性——它不是诚实面对未知,而是生成最合乎逻辑的下一段文本。
3.2 超时、重试、降级三板斧
生产级Agent要过工具调用这一关,至少得给工具调用套上三层逻辑:
- 超时控制:每一次工具调用都必须有明确超时上限,不要让Agent无限等一个上游响应。比如默认5秒,超过就返回一个“工具超时”的占位结果,然后让Agent判断下一步是重试、改路还是结束。
- 指数退避重试:对于短暂故障,比如503、限流,可以做重试,但必须带退避,否则故障期会把上游打得更惨。常用策略是1s、2s、4s、8s,最多重试3次。
- 降级方案:重试还不行的,要设定降级路径。比如查不了实时订单状态,就返回最近一次缓存的状态;调用不了推荐模型,就返回基于规则的结果。Agent的Prompt要训练它理解“降级结果”不等于“真实结果”,上下文里要显著标注[降级]标识,避免它把降级数据当成真相去推导。
我看到很多团队在工具定义里只写了“工具返回什么”,完全不写“工具可能怎么失败”。这是对LLM最不负责任的做法。
3.3 工具协议与Schema的契约管理
另一个隐蔽的坑是工具Schema的漂移。Demo阶段你可能就三个工具,改起来随意。生产环境工具几十上百个,总有人动了某个工具的入参结构忘了改Agent的Function Calling Schema,结果线上模型偶尔能唤起、偶尔唤起错,错误率极难排查。
我用了一个土办法:每个工具的Schema在CI阶段做一次“契约测试”,模拟十几种典型输入和失败输入,断言模型的调用格式是否正确。说白了就是让Agent工具调用也过TestCase,而不是靠上线后拿真实流量测试。
注意:工具返回结构务必设计成“错误码 + 数据和错误”的统一格式,而不要每个工具自造错误格式。否则模型在编排层的错误处理逻辑会乱成一团。
4. 第三道壁垒:上下文窗口与记忆机制
4.1 上下文不是越长越好
这是很多新手最常踩的坑:为了让Agent“记得更多”,把所有历史消息一股脑儿塞进上下文。结果就是:
- Token账单暴涨
- 模型注意力被无关信息稀释
- 回答质量不升反降,甚至开始遗忘早期关键信息
我在某次压力测试里遇到过:一次会话跑了80轮,上下文从几k膨胀到60k,模型对前面预设的目标约束视而不见,开始对用户偏好跑偏。这不是模型变笨了,而是信息信噪比太低了。
生产级的上下文管理,核心原则是“按需加载,而不是全量携带”。
4.2 记忆分层:会话级、用户级、知识级
我把Agent的记忆分成了三个层次,分别是:
- 会话级记忆:这一轮对话的流程、用户刚说的需求。这个必须全程携带,因为这是当前任务的原料。
- 用户级记忆:用户的偏好、常用选择、历史结论。比如用户说过“我偏好保守的投资策略”,这个应该落地到用户画像存储里,而不是每轮对话都占用上下文。
- 知识级记忆:系统知识库、条令条例、产品文档。只在Agent判断需要时,通过检索拉取相关片段。
这三个层次对应三种不同的承载方式:会话级用消息队列+KV存储,用户级用数据库,知识级用向量库。
4.3 压缩与检索的配合
当会话长度超过阈值(我自己常用10轮)时,就触发压缩策略:把前几轮的对话内容交给LLM做一次摘要,摘要之后的内容才继续放入上下文。
但摘要一定会损失细节。所以我在项目X里加了一层“关键信息槽位”机制:先把本轮对话里提到的关键信息(时间、地点、金额、约束)抽出来填入结构化槽位,永远保留在上下文中;然后剩余的对话再压缩成摘要。这样就算摘要丢细节,结构化槽位也不会丢关键参数。
配合这块的是检索增强。用户级记忆和知识级记忆不能靠猜,必须在Agent动手之前先做一次embedding检索,把Top-K相关的记忆片段喂到上下文里。
建议:压缩操作本身要有“元认知”指示——明确告诉模型“以下内容是历史摘要,如果用户提到的信息和摘要冲突,以用户最新输入为准”。否则模型容易把摘要里的过时信息当成最新状态。
5. 第四道壁垒:并发、性能与成本控制
5.1 限流、排队与背压
Demo阶段你只有一个用户,那就是你自己。生产环境一上来全是并发请求,模型API的速率限制、上游服务的容量保护、甚至单机进程的内存上限,都会成为瓶颈。
我做过的项目里,控制并发最稳妥的方案是:每一个Agent任务进来,先进队列,然后由调度器控制并发度,并发度直接对齐模型API的RPM限制。
这里有个关键点:Agent任务不像普通HTTP请求那样可以在几百毫秒内结束,一个复杂任务可能持续几十秒甚至几分钟,中间还要多次调用模型。所以单纯的请求级限流远远不够,你要做的是任务级并发控制,甚至要细分到“当前正在启动的Llm调用数”。
背上压是必须的:当队列满时,新的请求不能无限排队,要直接返回“当前繁忙,请稍后再试”。否则排队时间过长,用户那头超时断开,你这边还在傻傻跑任务,纯粹浪费钱。
5.2 Token成本模型与预算控制
很多团队上线Agent之后,最大的意外不是功能不行,而是账单爆炸。原因很简单:你在Demo阶段看的是“单次调用的token数”,而生产环境是“所有用户的所有调用的token数”。
我自己会为每个任务做一次预算封顶。比如一个任务最多跑30次模型调用,总token不超过200k(不同模型按自己的计价方式换算)。一旦到达上限,Agent必须切换成“保守模式”:只做必要步骤,跳过可选的旁路探索,直接给用户阶段结论。
这个预算机制要放在编排层强制,不能只写在Prompt里让模型自觉控制,模型根本不会自觉。
5.3 缓存策略
Agent生产环境里最容易忽略的优化就是缓存。Demo阶段对话都是新的,缓存不存在也没感觉。生产里如果你想省钱,至少要建三级缓存:
- Prompt片段缓存:系统提示、工具描述、长文本知识片段,这些基本不变,做K-V缓存就行。
- 记忆检索缓存:向量检索的结果可以在短时间内缓存,尤其是用户的偏好信息,没必要每轮对话都重新检索。
- 模型调用缓存:在参数、上下文哈希一致的情况下,可以直接复用上一次的response。适合处理高频重复查询,不过要小心“时间敏感型”问题不能缓存,比如实时报价。
缓存本来是为降本,但如果缓存的是过期结果,引发用户难以接受的延迟,反而变成事故源。所以时间敏感类Agent的缓存TTL要设得保守一些。
6. 第五道壁垒:可观测性与调试
6.1 日志只是最低标准
Agent和传统服务最大的区别是:它不仅执行逻辑,还在“生成”逻辑,这导致调试难度指数级上升。传统服务出问题,看个报错堆栈基本就定位了;Agent出问题,你看日志只会看到一堆“模型调了工具、模型返回了一段话”的流水账,很难直接判断是哪一步决策错了。
所以Agent日志不能只是结构化文本,至少要记录:
- 每一次模型调用的完整输入(Prompt、工具调用记录)
- 模型的原始返回(Raw Output)
- 解析后结构化数据
- 上下文压缩前后的状态对比
这些都是调“模型抽风”问题的基础材料。没有完整输入输出,你在排查时只能一遍遍回放prompt猜,效率极低。
6.2 Trace链路与回放
我在项目X里给每个Agent任务生成了一个全局TraceId,贯穿所有内部模块和外部工具调用,这让我能在UI上看到完整的时间线。某个任务卡了10秒、超时、工具调用失败、又重试成功,都一目了然。
比Trace更进一步的是“会话回放”。我做了个调试面板,可以选中任意一条线上任务,把当时的完整事件流拉出来,逐步查看模型的每一步推理和动作。遇到事故的时候,直接回放比对着日志猜快很多倍,基本能把定位时间从“天”压缩到“分钟”。
说得严格点:没有回放能力的Agent系统,本质上不具备线上可维护性。你代码写得多好,上线运营维护的时候都会变成“盲飞”,因为LLM的结果不可预期,必须靠Trace回放兜底。
提示:回放数据存储建议做冷热分离,热数据存3天,冷数据压缩归档到对象存储,保存至少一个月。毕竟线上Agent事故,经常要翻一周之前的记录对比。
7. 实操篇:三步从Demo走向生产
7.1 第一步:把“Demo外壳”和“生产内核”从代码上分开
不需要重写整个项目,但至少在架构上先把这两层分开。Demo里你可能是直接在回调函数里调LLM,但生产环境要在这个链路上插入:队列、限流、持久化、Trace采集。
具体分法:
- 外层编排层:负责接收请求、入队、调度、超时控制、返回结果。
- 内核Agent层:专注决策、工具调用、上下文管理,不考虑“外部的网络和别人”。
- 基础设施层:提供存储、缓存、向量库、Trace、日志。
我甚至建议直接按“依赖倒置”的方式把内核写成纯业务接口,不依赖任何具体实现,这样Demo和生产可以共用内核,只替换外壳。
7.2 第二步:为每一个不确定性建立闸门
Agent生产环境的本质,是对“模型不确定”的工程化兜底。每多一层不确定性,就要多一层闸门。
- 模型输出可能不合法 -> 加Schema校验和自动修复重试
- 工具调用可能失败 -> 超时/重试/降级
- 上下文可能膨胀 -> 压缩/摘要/槽位
- 并发可能爆 -> 限流/排队/背压
- 成本可能失控 -> 预算封顶
- 出错可能无法定位 -> 持久化事件流
这六个闸门其实对应了前面几节的核心议题。你可以照着这个清单自查:自己的系统离“上线及格线”还差几个闸门。
7.3 第三步:建立生产环境的“金丝雀”验证
最后一步也很容易被忽视:上线不等于结束,你要建立持续验证的机制。
我在项目X里做了一个“影子模式”:把生产流量复制一份,打到新版本的Agent上,但结果不返回给用户,只回流到分析系统里对比新旧版本的响应质量。持续跑几天,看有没有退化、有没有崩溃、token用量有没有异常。
这个影子模式是我觉得性价比最高的上线安全网。它不需要你担惊受怕做全量发布,也不需要在新版本出问题时紧急回滚,它就是那个提前发现问题的探针。
8. 常见问题与排查技巧实录
下面整理几个我在实际项目中高频遇到、以及周围同事经常问我的问题,每个都附上排查思路。
8.1 Agent突然“不听话”了,指向了过时的工具参数
现象:线上某一天开始,模型开始用旧版本的工具调用格式,死活改不过来。
排查思路:先查工具Schema的版本是否一致,再查模型端的Function Calling配置缓存。很多情况下是因为你改了工具定义的字段,但线上缓存没刷新。另外,如果在Prompt里让模型“参考上一次工具调用格式”,它也很容易固化旧格式。解决办法:要么每次刷新缓存,要么在工具说明里写清楚“必须根据最新Schema,禁止参考当前会话内旧格式”。
8.2 工具调用明明成功了,但Agent回答却说“失败”
现象:日志显示工具返回200和正常数据,但Agent输出“抱歉,我暂时无法处理”。
排查思路:大概率是工具返回的内容超出了模型在那个步骤的上下文长度限制,或者工具返回格式模型没有正确解析。建议把工具返回设计成结构化JSON,并在Prompt里明确告知模型“工具结果字段为result.data,状态以status字段为准”。我发现很多团队的工具返回是个长字符串,模型根本看不出哪段是状态码哪段是数据,自然就误解了。
8.3 日志与Trace齐全,但怎么都复现不了线上问题
现象:线上用户抱怨某个问题,你回放事件流,发现步骤都和预期一致,但复现不出来。
排查思路:多半是外部依赖的“状态”被漏掉了。比如某个第三方接口当时返回了一个特殊状态码,你的日志里没记录这个字段;又比如当时数据库内容已经变了,你的回放库是历史版本。解决办法:把所有外部依赖的原始响应(哪怕是原文)也连同Trace一起存储,回放的时候才能“复现当时的世界”。
8.4 Token成本飞速上涨,却不知道是谁在烧钱
现象:月底看账单,Agent服务的token消耗是预估值的好几倍。
排查思路:给每一类任务打上用途标签(bind:意图分类、RAG检索、总结摘要、工具调用),按Tag聚合分析。通常问题出在两类:一类是工具调用失败后的无脑重试,每次重试都带着一大段上下文;另一类是RAG检索回来的Top-K文档全是无关内容,模型每次都在“硬读”。前者需要加熔断和预算,后者需要调检索阈值和重排策略。
9. 写在最后:一些真实的体会
做Agent生产化这一年半,我最深的一个体感是:把Agent当“传统程序”来开发会撞墙,但当“新的分布式系统”来开发又过重。它更像一个带有“强不确定性执行单元”的中间态系统。
我踩过的坑里,最不值得的是“过度迷信模型能力”和“过度准备工程化”。最好的节奏是:先跑通核心链路,然后把不确定性逐层加闸,再用影子模式持续验证。不要指望一步到位,上生产本身就是一次持续调参的过程。
最后分享一个小技巧:给Agent代码里所有涉及“计划下一步动作”的地方,都埋一个“策略版本号”的字段。这样当线上策略变更出现问题时,你能立刻知道是哪些版本在生效,回滚也只是改配置。这个小习惯在早期帮我省了无数次眼急手快的紧急回滚。
Agent的复杂在一开始会让人望而却步,但拆除这些壁垒本身就是一种乐趣——每当线上稳定跑过一周零事故,那种成就感是写普通CRUD接口完全比不了的。希望这篇文章能帮你少走几趟弯路,下一课我们再接着深入一个具体的模块,拆开讲讲。