最近总有朋友拿着自己做的AI Agent来给我看,演示的时候一切“乖巧”,任务拆解清晰、工具调用流畅、回答有逻辑,看起来比不少商业产品还像样。但问一句“上线了吗”,气氛就会突然安静下来。这大概是当下做Agent最普遍的一种状态:Demo能跑、上线就崩。不是代码写得不好,也不一定是大模型选错了,而是我们在演示阶段根本没遇到生产环境里那些专门等着你的底层问题。这篇是Agent进阶系列的第一课,核心做的事情就是把“Demo能跑”和“上线不崩”之间那层看不见的壁垒拆开揉碎,讲讲为什么本地跑通的Agent一到真实场景就原形毕露,以及那些被大多数人忽略的上下文、工具调用、状态治理问题到底出在哪。适合已经能写出一个Agent原型、但正被生产化折磨的开发者,也适合想提前避开这些坑的入门者。1. Demo能跑到底证明了什么:先厘清“能跑”的假象1.1 单路径成功不等于系统可靠很多人口中的“Demo能跑”,其实只是在一个受控环境里,把某一条固定路径走通了一次。用户输入什么、工具返回什么、上下文长什么样,几乎都是“友善”的。用户在演示现场提一个问题,Agent把问题拆成两步,第一步查资料,第二步总结,顺利输出一段答案,全场掌声。但这只能证明一件事:这个模型在给定的某一条路径下没有崩,仅此而已。生产环境完全不是这个概念。真实用户不会按照你预设的句式说话,外部API可能返回异常数据,知识库里同一个问题有十种说法,上下文可能累积到几十万token。在这种开放输入空间里,Agent面对的是近乎无限的路径组合,每条路径都可能出现异常分支。只要其中一条路径没兜住,对那个用户来说就是“你的Agent是坏的”。Demo验证的是“存在一条可行路径”,生产需要的是“所有可预见路径都被覆盖”,这两者之间的差距,就是所谓的壁垒。1.2 Agent是决策系统,不是查询接口很多刚入门的同学会把Agent理解成一个“高级点的接口”:用户提问,模型回答,无非是多调几个工具。但从系统设计的角度看,Agent和普通查询接口有本质区别——它是一个自主决策系统。普通的查询接口是确定性的:同样的输入,必然走同样的逻辑,输出完全可预期。而Agent的核心是一个大模型在动态决定,每一步该调用什么工具、采纳什么信息、如何组织回复。这个决策过程依赖上下文、依赖模型当时的“状态”,还依赖外部工具返回的结果。也就是说,系统的行为不是一个纯函数,而是一条随时间展开的决策链。这意味着什么?意味着你不能用传统软件的测试思路来验证Agent。传统软件测试讲究“用例覆盖”,但Agent的决策路径几乎不可能穷举。一个Demo里你只验证了“天气晴好时怎么走”,生产里会遇到“天气接口挂了”“天气接口返回了晴转多云但体感温度字段缺失”“用户非要问三天后的天气而你的工具只支持两天”这些情况。这些不是代码bug,而是决策空间突然变大之后,原有的逻辑根本没有对应分支。1.3 所谓核心壁垒,其实是三层:不确定性、无边界、易污染如果把Agent生产化的核心壁垒提炼一下,我的看法是三层:不确定、无边界、易污染。不确定:模型输出是概率性的,同样的Prompt可能得到不同结果,工具调用可能时好时坏。无边界:用户输入、外部数据、上下文累积,这些都是没有明确边界的,不主动管控就会被无限撑大。易污染:上下文里一旦混入无关信息、错误信息、历史残留,模型的判断就会逐渐偏移,最后整个决策链崩掉。这三层问题在Demo阶段几乎完全不存在。演示时上下文干净,工具返回可控,输入有限。上线之后,三个问题会同时爆发,互相叠加,最终以“用户觉得你东西是坏的”这种形式表现出来。后面几个章节,就是围绕这三层展开的。2. 上下文与记忆:上线后第一道暗槛2.1 上下文一长,模型就开始“选择性失聪”我先问一个扎心的问题:你在Demo里,上下文的token量级是多少?我见过的绝大多数原型,一个会话下来,几百到两三千token已经顶天了。但上线之后,真实对话的上下文完全不是这么回事。用户可能会问十几个问题,每个问题背后都带着工具返回的大段结果,知识检索可能一次性插入三四段文档,再加上Agent内部执行多轮工具调用产生的中间过程……几个来回之后,上下文轻松突破两万token。如果你把长期记忆、外部知识库片段也塞进去,五万、十万token是家常便饭。问题来了:上下文越长,模型的指令遵循能力越差。这不是我编的,是很多模型评测和实际部署中都会观察到的现象。系统Prompt里的约束,比如“只根据检索内容回答”“不要编造数据”,在上下文里塞了一堆无关信息后,会被模型“选择性失聪”直接忽略。更麻烦的是,长上下文还会带来注意力分散的问题,模型开始对前面的关键指令“印象模糊”,反而被后面插入的大量工具返回带偏。我自己在项目里遇到过最典型的场景:Agent本来被要求“如果数据库没有该用户数据,必须明确告知用户,禁止猜测”,结果上下文里堆了很多历史问答和工具返回之后,模型面对一个查无此人的请求,居然自动编了一个“该用户近期活跃度中等”的结论出来。这种问题在Demo里根本不可能出现,因为上下文太短,系统指令永远占据绝对权重。2.2 记忆放哪里:窗口、外部存储还是混合既然把一切塞进上下文很危险,那记忆到底该放哪?这是Agent架构设计里绕不开的一个问题。我见过最朴素的方案是:所有历史对话全部拼进上下文,不做任何裁剪或摘要。这让Agent在短会话里表现很好,因为信息齐全。但会话一长,上面说的“选择性失聪”就开始发威,而且在成本上也非常不划算——每轮对话的token消耗都是全线性的,上下文长度翻倍,费用基本也跟着翻倍。稍微讲究一点的方案是引入“上下文预算”:给系统Prompt、历史对话、工具返回、检索内容各分配一个token上限,超出的部分做裁剪或摘要。比如我会用一个简单的预算结构:系统指令占5%,最近几轮对话占20%,工具返回占30%,知识检索占30%,剩余的预留。一旦某一部分超出预算,就对较早的对话做摘要压缩,而不是原样保留。这个方案的好处是上下文稳定在一个可控范围内,模型的指令遵循能力不会因为上下文膨胀而急速退化。长期记忆则不应该放在上下文中,而应该放到外部存储里,比如向量库或键值库。只有当某个记忆片段与当前问题相关时,才通过检索拉回上下文。把长期记忆全部铺进上下文,是最容易让Agent“精神分裂”的做法——模型会分不清哪些是当前任务的输入,哪些是过期状态的残留。我之前在某个内部知识问答Agent里实践过混合记忆方案:短期对话用“滚动窗口+自动摘要”,长期记忆放向量库按需检索。整体稳定性和回答准确性明显比“全量拼接上下文”高出一个档次。这也是为什么我一直强调,上下文管理不是优化项,而是Agent从Demo走向生产的及格线。2.3 多用户下的状态隔离:一个串号事故的现场还有一个在Demo阶段根本不会暴露、上线必炸的问题:会话状态隔离。本地跑Demo的时候,整个世界只有你一个用户,所有全局变量、上下文缓存、内存状态,都是你的。但生产环境里同时在线几十上百个用户,每个用户都有自己的对话历史、身份信息、权限范围。如果你在设计时没有显式隔离状态,就会出现一个非常经典的串号事故。我印象很深的一次排查:某公司内部部署了一个助手类Agent,上线第二天就收到投诉,说A用户问的问题,B用户居然能看到上下文里的历史痕迹。查了半天,代码里根本没有session维度的状态隔离,所有用户的对话记录都被塞进了一个全局的上下文缓冲区。这在Demo里当然没有任何问题——因为只有一个用户在那里测来测去。上线后,多用户并发读写同一个缓冲,数据直接乱套。正确的做法是,从一开始就把会话状态做成显式的维度。最基础的是给每个会话分配一个session_id,所有上下文、记忆、缓存都带这个ID,按ID隔离存取。更进一步,每一次请求都要做“状态快照”,确保一次完整的决策链依赖的状态是可回溯的,而不是共享的可变全局变量。这个设计听起来简单,但能救你于水火之中。3. 工具调用:模型输出与客观世界之间的裂缝3.1 工具描述越长,模型越容易选错、漏参Agent之所以是Agent,很大程度是因为它能调用工具。但工具调用正是“上线就崩”的重灾区,而且这个坑在Demo阶段几乎不会暴露。原因很简单:Demo里你只会给Agent配一两个工具,而且工具描述往往是你自己精心写过的,模型很容易做选择。生产环境里,工具数量可能变成十几个、几十个,每个工具的描述又多又长——各种参数、格式、示例、注意事项。这时候模型的工具选择能力会肉眼可见地下降:该用工具A的时候选了工具B,调用的时候漏了必填参数,甚至编造出工具里根本不存在的参数。我见过最夸张的一次,模型调用一个“发送消息”工具时,自动在参数里加了一个“优先级=P1”的字段,但实际接口根本没有这个字段。这就是所谓的参数幻觉。模型不是故意骗你,而是它在海量工具描述中“联想”出了这个参数,然后真的把它塞进了调用请求里。解决思路有两条线。第一是控制工具描述的数量和长度,每增加一个工具、每多写一句描述,都会提高模型出错的概率。尽量精简参数说明,把“可选参数”在运行时处理,而不是全堆在描述里让模型自己悟。第二是在工具调用入口处加一个结构化的校验层,用JSON Schema或者手写的校验函数,对模型输出的参数做校验,不合格的直接拦截并告知模型重试或选择其他路径。这一层就是你给“不确定的模型输出”套上的“确定性约束网”。3.2 真实API的三座大山:延迟、错误、不确定性模型能输出正确的工具调用参数,只是万里长征第一步。接下来要面对的是真实世界API的“三座大山”:延迟不稳定、错误码混乱、结果不确定。先说延迟。Demo里调API,本地网络好、服务也空,响应可能十几毫秒到几十毫秒。生产环境下,真实第三方接口的延迟波动非常大,几百毫秒是常态,遇到高峰期三五秒也不稀奇。如果你给Agent里的每个工具调用都设置了默认超时,那么一个多步骤任务里但凡有一个步骤卡住,整个决策链就会停滞。更麻烦的是,大模型自身是有响应时间要求的,你不可能让用户等一个Agent思考五分钟。所以工具调用的超时与重试策略,必须单独设计,而不是沿用通用HTTP客户端的默认配置。再说错误码。Demo里你可能只测了“成功”路径和“明确失败”路径。生产环境的API错误是千奇百怪的:有时返回200但内容是错误页面,有时返回500但隔一秒重试就好,有时返回400但错误信息里什么都没说。Agent如果对每个错误都做同样的处理,就会出现“明明可以降级处理却直接崩掉”或者“明明不该重试却反复打一个必然失败的接口”的尴尬局面。最后说结果不确定。工具返回的数据结构,在文档里写得很清楚,但在真实世界里,你可能会遇到字段缺失、类型不对、内容为空等情况。Demo里你定义了一个“解析上一个工具返回的结果,提取金额字段”的步骤,上线后真实接口返回的字段名从amount变成了totalAmount,你的Agent可能就懵了,然后开始“合理猜测”,给用户编造一个数字。这个场景特别典型。我现在的做法是,对工具的输入和输出都做“契约校验”。输入侧做参数Schema校验,输出侧做关键字段存在性检查,一旦发现结构不匹配,立即把原始返回交给一个确定的“降级处理逻辑”而不是让模型自由发挥。3.3 参数幻觉与幂等性:Agent乱调接口得有人擦屁股前面说到参数幻觉,这里展开讲一下,因为这是Agent工具调用里最隐蔽也最危险的问题。模型可能为了满足某个任务目标,给工具调用补上一些它认为“合理”的参数。这在构造没有副作用时还好,但如果工具是“下单”“扣款”“发送短信”“修改数据”,一个幻觉参数就能造成真实世界的后果。因此,对所有有副作用的工具,必须增加“人工确认”或者“二次确认”的环节。我的经验是,Agent可以自动构造调用参数,但执行前强行插入一个确认步骤:把参数摘要反馈给用户确认,确认通过才真正调用接口。这听起来会降低“自动化”的酷炫程度,但生产系统里,可靠性永远优先于效率。还有一个与工具调用强相关的刺:幂等性。大模型在工具调用失败后,经常干一件事就是“再试一次”。如果没有幂等机制,重试就可能变成重复扣款、重复发送。这个问题在Demo里测不出来,因为你不会真的去重复下单。上线后一旦出现网络超时后自动重试,而你的重试没有带幂等键,麻烦就来了。正确的做法是:为每一个涉及写操作的Agent任务生成一个唯一的request_id,在调用接口时把它作为幂等键传给下游。这样即使Agent因为超时而重试,下游也能识别出“这个请求之前已经处理过”,直接返回上一次的结果,而不是再执行一次。这是Agent生产化时必须补上的基本功。4. 从能跑到能扛:可观测、可恢复、可治理三板斧4.1 只做Trace不够,需要“决策审计”很多Agent项目上线后的第一反应是加日志。但普通日志对Agent这种系统来说远远不够,因为Agent的核心是“决策过程”,如果你不知道模型为什么在那个时间点选了那个工具,出了问题时你就只能瞪着报错日志干猜。我给Agent项目用的是一套“决策审计”日志结构。每一轮决策,记录以下信息:当前会话ID、决策步骤编号、模型观察到的输入(或至少是输入的摘要)、选择的动作(工具名或回复)、动作的原因(模型给出的理由)、调用结果、耗时的token数、延迟。这套日志的价值在于,当用户反馈“Agent刚才回了一个很奇怪的结果”时,你可以把这个会话的决策审计调出来,逐帧回放它是怎么一步步走到错误结论的。这比单纯记录“某工具调用失败”有用得多。很多Agent的崩溃不是显式的异常,而是“看似正常但结论错误”。这时候没有决策审计,你根本无从下手。我在实践中的做法是把决策审计和传统Trace并行:Trace管链路有没有断,决策审计管链路为什么这么走。二者结合,才是完整的可观测性。4.2 可控退化:给模型一条不崩的退路生产系统的铁律之一是:你的Agent一定会遇到处理不了的情况。区别在于,好的系统遇到意外时能优雅降级,差的系统直接崩给用户看。这里要引入一个概念,就是“可控退化”。简单说,就是为Agent可能遇到的每一种典型异常,预先设计好一条确定性的兜底路径,而不是让模型在失败中自由发挥。举个例子,工具调用超时了,与其让模型用“我猜大概是XXX”来搪塞用户,不如在代码层直接捕获超时,返回一个标准的占位信息“该服务暂时不可用,请稍后再试”,并把这个信息作为新的观察传给模型,让模型基于这个确定性的状态继续决策。再比如,知识检索返回为空,不要期待模型“凭借自己的知识”回答——因为那正是幻觉的高发期。应该让Agent明确回答“未检索到相关信息”,而不是硬编一个答案。可控退化的本质是减少模型的自由度为代价换取可靠性。所有你能在代码层确定下来的异常行为,就不要丢给模型去“随机应变”。模型只负责在你无法预先设计的决策空间里做判断,至于那些你已经预见到的失败,全部用确定性逻辑处理。4.3 无状态化与配额:多人使用的前提从一个用户到多用户,Agent架构必须做一次根本性的调整:从有状态进程变成无状态工作节点。Demo阶段,Agent的所有状态可能都保存在进程内存里,比如对话历史、工具返回缓存、当前任务进度。上线后,这种设计几乎注定要出问题。最典型的就是进程重启后,所有会话状态全部丢失,用户聊到一半,突然发现Agent失忆了。更好的做法是让Agent的工作节点保持无状态,所有会话状态统一放到外部的存储服务里,每个请求进来时通过session_id把状态加载出来,处理完再写回去。这样任何一台节点都可以承接任意用户的请求,节点可以随意扩容缩容、重启升级,都不会丢状态。这本质上就是把Agent从“单机单会话”模型改造成“分布式多会话”模型。除了状态隔离,多用户下的配额控制也必须在闭环里做。一个不受控的Agent任务可能因为模型判断失误,在短时间内疯狂调用外部工具,既产生高昂成本,也可能触达第三方接口的限流阈值。所以要从架构上给每个用户、每个会话设置配额:每轮任务的工具调用次数上限、单次任务的token消耗预算、单位时间内的并发限制。一旦超过配额,强制中断任务并提示用户重试。这些控制逻辑虽然无聊,但它们才是Agent在生产环境里活下去的保障。5. 一次“上线即崩”的完整复盘:排查链路与修复过程5.1 现象:同一个问题,时好时坏前面讲了不少理论和方案,现在用一次真实的排查过程来串一遍。某次我给一个内部知识库问答Agent做生产化部署,上线第一天就收到大量反馈:用户问同一个问题,有时候能正常回答,有时候回答得驴唇不对马嘴,还有一部分请求直接超时,前端显示“服务无响应”。这个现象的诡异之处在于:它不是“完全不可用”,而是“时好时坏”。如果Agent完全不能用,那反而好定位,但这种间歇性失败最磨人——因为它在Demo环境里从来没有发生过。我第一反应是去看API调用日志,确认外部大模型服务是否稳定。看完之后更迷惑:API服务方的状态面板一切正常,我们的调用也没有收到显式的错误码。唯一异常的指标是,出问题的那批请求,上下文token总量都特别大,普遍在3万token以上。5.2 下钻链路:token用量、上下文构造、工具返回有了“上下文特别大”这个线索,我立刻开始查上下文构造的代码。然后发现了第一个问题:历史消息被无限制地拼接到了上下文里。会话持续二十分钟后,前面的所有内容,包括用户闲聊、上一轮的思考过程、工具调用中间输出,全部原样保留。这就直接导致token量膨胀。继续往里追,又发现第二个问题:工具返回结果也被原样存进了上下文。知识库检索工具一次可能返回三五个相关片段,每个片段几百上千字,这些内容全被一股脑塞进去。如果用户在连续提问后触发多次检索,上下文里就会堆积大量重复、冗余的检索片段。模型面对这些内容,根本分不清哪些是当前问题真正需要的依据,哪些是之前问题的残留答案。第三个问题出现在超时设置上。工具调用的超时时间设置得太长,而且失败后的重试是“连续重试”模式。当一个步骤超时后,Agent会自动重试同一个工具,而重试期间的思考过程也会被记入上下文,进一步推高token量,形成恶性循环。某些请求可能在整个链路中反复超时多次,最终超出整体响应时间预算,用户那边看到的就是“无响应”。5.3 真正根因:上下文污染与检索膨胀把三个问题串起来看,根因其实不是一个“bug”,而是一套组合病:上下文缺乏预算管理,导致会话越长,上下文越庞大;工具返回缺乏去重和筛选,导致检索片段不断重复累积;失败时缺乏确定性降级路径,导致模型陷入无意义的反复重试,而每次重试都在继续往上下文里写垃圾信息。我把这种状态称为“上下文污染”,它是对Agent生产化威胁最大的隐形杀手。污染一旦发生,模型的行为会越来越偏离预期:开始忽略系统指令,开始把历史残留信息当成当前任务依据,甚至逐渐在回答里掺入幻觉内容。最严重的情况下,上下文窗口被无关内容塞满,模型无法再写入任何有效的新信息,只能输出毫无意义的空响应或固定话术。这里也顺便回答了一个很多人困惑的问题:为什么同一个Prompt,早上跑得好好的,下午就崩了?大概率不是模型变笨了,而是到了下午,你的上下文里已经默默累积了一堆上午的残留污染。Demo里你永远保持半页纸的对话,自然什么都看不出来。5.4 修复验证与后续习惯知道了根因,修复方案就很明确了。我做了四件事:第一,给上下文加上预算管理,历史对话超出预算的部分自动做摘要压缩,不再原样保留。第二,工具返回做字段裁剪与去重,检索片段每轮最多保留最相关的几个,去除与当前问题无关的内容。第三,把工具超时设置调到合理值,并且重试策略改为“最多重试一次”,重试前强制等待一小段间隔,避免连续打接口。第四,增加一个“兜底回复”分支:当Agent检测到自己无法从检索内容中得到依据时,直接返回“知识库中未找到相关信息”,而不是让模型自行编造。修复之后,我把之前出问题的那些会话重新跑了一遍,错误率基本清零,响应时间也都回到了正常范围。后来我养成了几个工作习惯,在这里分享给读者:第一,任何Agent任务上线前,先设定上下文的预算上限,并按预算上限跑一遍长会话压测;第二,在日志里记录每一轮的工具调用次数和token消耗,一旦出现异常增长就是上下文污染的信号;第三,强制设计至少一条“失败时不必调用模型”的确定性退路,不要把所有情况都丢给大模型应对。这三个习惯看起来不起眼,但正是它们把一个又一个Agent项目从“Demo能跑”推向了“生产能扛”的阶段。希望这篇拆解也能给你一些启发,等你自己被Agent的上下文问题折磨过一轮之后,或许会回来验证我说的这些经验。
最近总有朋友拿着自己做的AI Agent来给我看,演示的时候一切“乖巧”,任务拆解清晰、工具调用流畅、回答有逻辑,看起来比不少商业产品还像样。但问一句“上线了吗”,气氛就会突然安静下来。这大概是当下做Agent最普遍的一种状态:Demo能跑、上线就崩。不是代码写得不好,也不一定是大模型选错了,而是我们在演示阶段根本没遇到生产环境里那些专门等着你的底层问题。这篇是Agent进阶系列的第一课,核心做的事情就是把“Demo能跑”和“上线不崩”之间那层看不见的壁垒拆开揉碎,讲讲为什么本地跑通的Agent一到真实场景就原形毕露,以及那些被大多数人忽略的上下文、工具调用、状态治理问题到底出在哪。适合已经能写出一个Agent原型、但正被生产化折磨的开发者,也适合想提前避开这些坑的入门者。1. Demo能跑到底证明了什么:先厘清“能跑”的假象1.1 单路径成功不等于系统可靠很多人口中的“Demo能跑”,其实只是在一个受控环境里,把某一条固定路径走通了一次。用户输入什么、工具返回什么、上下文长什么样,几乎都是“友善”的。用户在演示现场提一个问题,Agent把问题拆成两步,第一步查资料,第二步总结,顺利输出一段答案,全场掌声。但这只能证明一件事:这个模型在给定的某一条路径下没有崩,仅此而已。生产环境完全不是这个概念。真实用户不会按照你预设的句式说话,外部API可能返回异常数据,知识库里同一个问题有十种说法,上下文可能累积到几十万token。在这种开放输入空间里,Agent面对的是近乎无限的路径组合,每条路径都可能出现异常分支。只要其中一条路径没兜住,对那个用户来说就是“你的Agent是坏的”。Demo验证的是“存在一条可行路径”,生产需要的是“所有可预见路径都被覆盖”,这两者之间的差距,就是所谓的壁垒。1.2 Agent是决策系统,不是查询接口很多刚入门的同学会把Agent理解成一个“高级点的接口”:用户提问,模型回答,无非是多调几个工具。但从系统设计的角度看,Agent和普通查询接口有本质区别——它是一个自主决策系统。普通的查询接口是确定性的:同样的输入,必然走同样的逻辑,输出完全可预期。而Agent的核心是一个大模型在动态决定,每一步该调用什么工具、采纳什么信息、如何组织回复。这个决策过程依赖上下文、依赖模型当时的“状态”,还依赖外部工具返回的结果。也就是说,系统的行为不是一个纯函数,而是一条随时间展开的决策链。这意味着什么?意味着你不能用传统软件的测试思路来验证Agent。传统软件测试讲究“用例覆盖”,但Agent的决策路径几乎不可能穷举。一个Demo里你只验证了“天气晴好时怎么走”,生产里会遇到“天气接口挂了”“天气接口返回了晴转多云但体感温度字段缺失”“用户非要问三天后的天气而你的工具只支持两天”这些情况。这些不是代码bug,而是决策空间突然变大之后,原有的逻辑根本没有对应分支。1.3 所谓核心壁垒,其实是三层:不确定性、无边界、易污染如果把Agent生产化的核心壁垒提炼一下,我的看法是三层:不确定、无边界、易污染。不确定:模型输出是概率性的,同样的Prompt可能得到不同结果,工具调用可能时好时坏。无边界:用户输入、外部数据、上下文累积,这些都是没有明确边界的,不主动管控就会被无限撑大。易污染:上下文里一旦混入无关信息、错误信息、历史残留,模型的判断就会逐渐偏移,最后整个决策链崩掉。这三层问题在Demo阶段几乎完全不存在。演示时上下文干净,工具返回可控,输入有限。上线之后,三个问题会同时爆发,互相叠加,最终以“用户觉得你东西是坏的”这种形式表现出来。后面几个章节,就是围绕这三层展开的。2. 上下文与记忆:上线后第一道暗槛2.1 上下文一长,模型就开始“选择性失聪”我先问一个扎心的问题:你在Demo里,上下文的token量级是多少?我见过的绝大多数原型,一个会话下来,几百到两三千token已经顶天了。但上线之后,真实对话的上下文完全不是这么回事。用户可能会问十几个问题,每个问题背后都带着工具返回的大段结果,知识检索可能一次性插入三四段文档,再加上Agent内部执行多轮工具调用产生的中间过程……几个来回之后,上下文轻松突破两万token。如果你把长期记忆、外部知识库片段也塞进去,五万、十万token是家常便饭。问题来了:上下文越长,模型的指令遵循能力越差。这不是我编的,是很多模型评测和实际部署中都会观察到的现象。系统Prompt里的约束,比如“只根据检索内容回答”“不要编造数据”,在上下文里塞了一堆无关信息后,会被模型“选择性失聪”直接忽略。更麻烦的是,长上下文还会带来注意力分散的问题,模型开始对前面的关键指令“印象模糊”,反而被后面插入的大量工具返回带偏。我自己在项目里遇到过最典型的场景:Agent本来被要求“如果数据库没有该用户数据,必须明确告知用户,禁止猜测”,结果上下文里堆了很多历史问答和工具返回之后,模型面对一个查无此人的请求,居然自动编了一个“该用户近期活跃度中等”的结论出来。这种问题在Demo里根本不可能出现,因为上下文太短,系统指令永远占据绝对权重。2.2 记忆放哪里:窗口、外部存储还是混合既然把一切塞进上下文很危险,那记忆到底该放哪?这是Agent架构设计里绕不开的一个问题。我见过最朴素的方案是:所有历史对话全部拼进上下文,不做任何裁剪或摘要。这让Agent在短会话里表现很好,因为信息齐全。但会话一长,上面说的“选择性失聪”就开始发威,而且在成本上也非常不划算——每轮对话的token消耗都是全线性的,上下文长度翻倍,费用基本也跟着翻倍。稍微讲究一点的方案是引入“上下文预算”:给系统Prompt、历史对话、工具返回、检索内容各分配一个token上限,超出的部分做裁剪或摘要。比如我会用一个简单的预算结构:系统指令占5%,最近几轮对话占20%,工具返回占30%,知识检索占30%,剩余的预留。一旦某一部分超出预算,就对较早的对话做摘要压缩,而不是原样保留。这个方案的好处是上下文稳定在一个可控范围内,模型的指令遵循能力不会因为上下文膨胀而急速退化。长期记忆则不应该放在上下文中,而应该放到外部存储里,比如向量库或键值库。只有当某个记忆片段与当前问题相关时,才通过检索拉回上下文。把长期记忆全部铺进上下文,是最容易让Agent“精神分裂”的做法——模型会分不清哪些是当前任务的输入,哪些是过期状态的残留。我之前在某个内部知识问答Agent里实践过混合记忆方案:短期对话用“滚动窗口+自动摘要”,长期记忆放向量库按需检索。整体稳定性和回答准确性明显比“全量拼接上下文”高出一个档次。这也是为什么我一直强调,上下文管理不是优化项,而是Agent从Demo走向生产的及格线。2.3 多用户下的状态隔离:一个串号事故的现场还有一个在Demo阶段根本不会暴露、上线必炸的问题:会话状态隔离。本地跑Demo的时候,整个世界只有你一个用户,所有全局变量、上下文缓存、内存状态,都是你的。但生产环境里同时在线几十上百个用户,每个用户都有自己的对话历史、身份信息、权限范围。如果你在设计时没有显式隔离状态,就会出现一个非常经典的串号事故。我印象很深的一次排查:某公司内部部署了一个助手类Agent,上线第二天就收到投诉,说A用户问的问题,B用户居然能看到上下文里的历史痕迹。查了半天,代码里根本没有session维度的状态隔离,所有用户的对话记录都被塞进了一个全局的上下文缓冲区。这在Demo里当然没有任何问题——因为只有一个用户在那里测来测去。上线后,多用户并发读写同一个缓冲,数据直接乱套。正确的做法是,从一开始就把会话状态做成显式的维度。最基础的是给每个会话分配一个session_id,所有上下文、记忆、缓存都带这个ID,按ID隔离存取。更进一步,每一次请求都要做“状态快照”,确保一次完整的决策链依赖的状态是可回溯的,而不是共享的可变全局变量。这个设计听起来简单,但能救你于水火之中。3. 工具调用:模型输出与客观世界之间的裂缝3.1 工具描述越长,模型越容易选错、漏参Agent之所以是Agent,很大程度是因为它能调用工具。但工具调用正是“上线就崩”的重灾区,而且这个坑在Demo阶段几乎不会暴露。原因很简单:Demo里你只会给Agent配一两个工具,而且工具描述往往是你自己精心写过的,模型很容易做选择。生产环境里,工具数量可能变成十几个、几十个,每个工具的描述又多又长——各种参数、格式、示例、注意事项。这时候模型的工具选择能力会肉眼可见地下降:该用工具A的时候选了工具B,调用的时候漏了必填参数,甚至编造出工具里根本不存在的参数。我见过最夸张的一次,模型调用一个“发送消息”工具时,自动在参数里加了一个“优先级=P1”的字段,但实际接口根本没有这个字段。这就是所谓的参数幻觉。模型不是故意骗你,而是它在海量工具描述中“联想”出了这个参数,然后真的把它塞进了调用请求里。解决思路有两条线。第一是控制工具描述的数量和长度,每增加一个工具、每多写一句描述,都会提高模型出错的概率。尽量精简参数说明,把“可选参数”在运行时处理,而不是全堆在描述里让模型自己悟。第二是在工具调用入口处加一个结构化的校验层,用JSON Schema或者手写的校验函数,对模型输出的参数做校验,不合格的直接拦截并告知模型重试或选择其他路径。这一层就是你给“不确定的模型输出”套上的“确定性约束网”。3.2 真实API的三座大山:延迟、错误、不确定性模型能输出正确的工具调用参数,只是万里长征第一步。接下来要面对的是真实世界API的“三座大山”:延迟不稳定、错误码混乱、结果不确定。先说延迟。Demo里调API,本地网络好、服务也空,响应可能十几毫秒到几十毫秒。生产环境下,真实第三方接口的延迟波动非常大,几百毫秒是常态,遇到高峰期三五秒也不稀奇。如果你给Agent里的每个工具调用都设置了默认超时,那么一个多步骤任务里但凡有一个步骤卡住,整个决策链就会停滞。更麻烦的是,大模型自身是有响应时间要求的,你不可能让用户等一个Agent思考五分钟。所以工具调用的超时与重试策略,必须单独设计,而不是沿用通用HTTP客户端的默认配置。再说错误码。Demo里你可能只测了“成功”路径和“明确失败”路径。生产环境的API错误是千奇百怪的:有时返回200但内容是错误页面,有时返回500但隔一秒重试就好,有时返回400但错误信息里什么都没说。Agent如果对每个错误都做同样的处理,就会出现“明明可以降级处理却直接崩掉”或者“明明不该重试却反复打一个必然失败的接口”的尴尬局面。最后说结果不确定。工具返回的数据结构,在文档里写得很清楚,但在真实世界里,你可能会遇到字段缺失、类型不对、内容为空等情况。Demo里你定义了一个“解析上一个工具返回的结果,提取金额字段”的步骤,上线后真实接口返回的字段名从amount变成了totalAmount,你的Agent可能就懵了,然后开始“合理猜测”,给用户编造一个数字。这个场景特别典型。我现在的做法是,对工具的输入和输出都做“契约校验”。输入侧做参数Schema校验,输出侧做关键字段存在性检查,一旦发现结构不匹配,立即把原始返回交给一个确定的“降级处理逻辑”而不是让模型自由发挥。3.3 参数幻觉与幂等性:Agent乱调接口得有人擦屁股前面说到参数幻觉,这里展开讲一下,因为这是Agent工具调用里最隐蔽也最危险的问题。模型可能为了满足某个任务目标,给工具调用补上一些它认为“合理”的参数。这在构造没有副作用时还好,但如果工具是“下单”“扣款”“发送短信”“修改数据”,一个幻觉参数就能造成真实世界的后果。因此,对所有有副作用的工具,必须增加“人工确认”或者“二次确认”的环节。我的经验是,Agent可以自动构造调用参数,但执行前强行插入一个确认步骤:把参数摘要反馈给用户确认,确认通过才真正调用接口。这听起来会降低“自动化”的酷炫程度,但生产系统里,可靠性永远优先于效率。还有一个与工具调用强相关的刺:幂等性。大模型在工具调用失败后,经常干一件事就是“再试一次”。如果没有幂等机制,重试就可能变成重复扣款、重复发送。这个问题在Demo里测不出来,因为你不会真的去重复下单。上线后一旦出现网络超时后自动重试,而你的重试没有带幂等键,麻烦就来了。正确的做法是:为每一个涉及写操作的Agent任务生成一个唯一的request_id,在调用接口时把它作为幂等键传给下游。这样即使Agent因为超时而重试,下游也能识别出“这个请求之前已经处理过”,直接返回上一次的结果,而不是再执行一次。这是Agent生产化时必须补上的基本功。4. 从能跑到能扛:可观测、可恢复、可治理三板斧4.1 只做Trace不够,需要“决策审计”很多Agent项目上线后的第一反应是加日志。但普通日志对Agent这种系统来说远远不够,因为Agent的核心是“决策过程”,如果你不知道模型为什么在那个时间点选了那个工具,出了问题时你就只能瞪着报错日志干猜。我给Agent项目用的是一套“决策审计”日志结构。每一轮决策,记录以下信息:当前会话ID、决策步骤编号、模型观察到的输入(或至少是输入的摘要)、选择的动作(工具名或回复)、动作的原因(模型给出的理由)、调用结果、耗时的token数、延迟。这套日志的价值在于,当用户反馈“Agent刚才回了一个很奇怪的结果”时,你可以把这个会话的决策审计调出来,逐帧回放它是怎么一步步走到错误结论的。这比单纯记录“某工具调用失败”有用得多。很多Agent的崩溃不是显式的异常,而是“看似正常但结论错误”。这时候没有决策审计,你根本无从下手。我在实践中的做法是把决策审计和传统Trace并行:Trace管链路有没有断,决策审计管链路为什么这么走。二者结合,才是完整的可观测性。4.2 可控退化:给模型一条不崩的退路生产系统的铁律之一是:你的Agent一定会遇到处理不了的情况。区别在于,好的系统遇到意外时能优雅降级,差的系统直接崩给用户看。这里要引入一个概念,就是“可控退化”。简单说,就是为Agent可能遇到的每一种典型异常,预先设计好一条确定性的兜底路径,而不是让模型在失败中自由发挥。举个例子,工具调用超时了,与其让模型用“我猜大概是XXX”来搪塞用户,不如在代码层直接捕获超时,返回一个标准的占位信息“该服务暂时不可用,请稍后再试”,并把这个信息作为新的观察传给模型,让模型基于这个确定性的状态继续决策。再比如,知识检索返回为空,不要期待模型“凭借自己的知识”回答——因为那正是幻觉的高发期。应该让Agent明确回答“未检索到相关信息”,而不是硬编一个答案。可控退化的本质是减少模型的自由度为代价换取可靠性。所有你能在代码层确定下来的异常行为,就不要丢给模型去“随机应变”。模型只负责在你无法预先设计的决策空间里做判断,至于那些你已经预见到的失败,全部用确定性逻辑处理。4.3 无状态化与配额:多人使用的前提从一个用户到多用户,Agent架构必须做一次根本性的调整:从有状态进程变成无状态工作节点。Demo阶段,Agent的所有状态可能都保存在进程内存里,比如对话历史、工具返回缓存、当前任务进度。上线后,这种设计几乎注定要出问题。最典型的就是进程重启后,所有会话状态全部丢失,用户聊到一半,突然发现Agent失忆了。更好的做法是让Agent的工作节点保持无状态,所有会话状态统一放到外部的存储服务里,每个请求进来时通过session_id把状态加载出来,处理完再写回去。这样任何一台节点都可以承接任意用户的请求,节点可以随意扩容缩容、重启升级,都不会丢状态。这本质上就是把Agent从“单机单会话”模型改造成“分布式多会话”模型。除了状态隔离,多用户下的配额控制也必须在闭环里做。一个不受控的Agent任务可能因为模型判断失误,在短时间内疯狂调用外部工具,既产生高昂成本,也可能触达第三方接口的限流阈值。所以要从架构上给每个用户、每个会话设置配额:每轮任务的工具调用次数上限、单次任务的token消耗预算、单位时间内的并发限制。一旦超过配额,强制中断任务并提示用户重试。这些控制逻辑虽然无聊,但它们才是Agent在生产环境里活下去的保障。5. 一次“上线即崩”的完整复盘:排查链路与修复过程5.1 现象:同一个问题,时好时坏前面讲了不少理论和方案,现在用一次真实的排查过程来串一遍。某次我给一个内部知识库问答Agent做生产化部署,上线第一天就收到大量反馈:用户问同一个问题,有时候能正常回答,有时候回答得驴唇不对马嘴,还有一部分请求直接超时,前端显示“服务无响应”。这个现象的诡异之处在于:它不是“完全不可用”,而是“时好时坏”。如果Agent完全不能用,那反而好定位,但这种间歇性失败最磨人——因为它在Demo环境里从来没有发生过。我第一反应是去看API调用日志,确认外部大模型服务是否稳定。看完之后更迷惑:API服务方的状态面板一切正常,我们的调用也没有收到显式的错误码。唯一异常的指标是,出问题的那批请求,上下文token总量都特别大,普遍在3万token以上。5.2 下钻链路:token用量、上下文构造、工具返回有了“上下文特别大”这个线索,我立刻开始查上下文构造的代码。然后发现了第一个问题:历史消息被无限制地拼接到了上下文里。会话持续二十分钟后,前面的所有内容,包括用户闲聊、上一轮的思考过程、工具调用中间输出,全部原样保留。这就直接导致token量膨胀。继续往里追,又发现第二个问题:工具返回结果也被原样存进了上下文。知识库检索工具一次可能返回三五个相关片段,每个片段几百上千字,这些内容全被一股脑塞进去。如果用户在连续提问后触发多次检索,上下文里就会堆积大量重复、冗余的检索片段。模型面对这些内容,根本分不清哪些是当前问题真正需要的依据,哪些是之前问题的残留答案。第三个问题出现在超时设置上。工具调用的超时时间设置得太长,而且失败后的重试是“连续重试”模式。当一个步骤超时后,Agent会自动重试同一个工具,而重试期间的思考过程也会被记入上下文,进一步推高token量,形成恶性循环。某些请求可能在整个链路中反复超时多次,最终超出整体响应时间预算,用户那边看到的就是“无响应”。5.3 真正根因:上下文污染与检索膨胀把三个问题串起来看,根因其实不是一个“bug”,而是一套组合病:上下文缺乏预算管理,导致会话越长,上下文越庞大;工具返回缺乏去重和筛选,导致检索片段不断重复累积;失败时缺乏确定性降级路径,导致模型陷入无意义的反复重试,而每次重试都在继续往上下文里写垃圾信息。我把这种状态称为“上下文污染”,它是对Agent生产化威胁最大的隐形杀手。污染一旦发生,模型的行为会越来越偏离预期:开始忽略系统指令,开始把历史残留信息当成当前任务依据,甚至逐渐在回答里掺入幻觉内容。最严重的情况下,上下文窗口被无关内容塞满,模型无法再写入任何有效的新信息,只能输出毫无意义的空响应或固定话术。这里也顺便回答了一个很多人困惑的问题:为什么同一个Prompt,早上跑得好好的,下午就崩了?大概率不是模型变笨了,而是到了下午,你的上下文里已经默默累积了一堆上午的残留污染。Demo里你永远保持半页纸的对话,自然什么都看不出来。5.4 修复验证与后续习惯知道了根因,修复方案就很明确了。我做了四件事:第一,给上下文加上预算管理,历史对话超出预算的部分自动做摘要压缩,不再原样保留。第二,工具返回做字段裁剪与去重,检索片段每轮最多保留最相关的几个,去除与当前问题无关的内容。第三,把工具超时设置调到合理值,并且重试策略改为“最多重试一次”,重试前强制等待一小段间隔,避免连续打接口。第四,增加一个“兜底回复”分支:当Agent检测到自己无法从检索内容中得到依据时,直接返回“知识库中未找到相关信息”,而不是让模型自行编造。修复之后,我把之前出问题的那些会话重新跑了一遍,错误率基本清零,响应时间也都回到了正常范围。后来我养成了几个工作习惯,在这里分享给读者:第一,任何Agent任务上线前,先设定上下文的预算上限,并按预算上限跑一遍长会话压测;第二,在日志里记录每一轮的工具调用次数和token消耗,一旦出现异常增长就是上下文污染的信号;第三,强制设计至少一条“失败时不必调用模型”的确定性退路,不要把所有情况都丢给大模型应对。这三个习惯看起来不起眼,但正是它们把一个又一个Agent项目从“Demo能跑”推向了“生产能扛”的阶段。希望这篇拆解也能给你一些启发,等你自己被Agent的上下文问题折磨过一轮之后,或许会回来验证我说的这些经验。