先聊一件让我头疼了两个月的事。上个月我们上线了一个贷款咨询的Agent,功能不复杂:用户进来问利率、算月供、查审批进度,偶尔问问材料清单。第一天测试群里全是好评,大家都在夸回答够快、语气也自然。第三天开始,有人发现Agent回答同一个问题时,语气和准确性开始漂移。上午还在坚持某套产品口径,下午就自己改了说法。我拉日志一看,上下文窗口里的历史对话已经滚了快一百轮,早期的业务规则早被挤出去了;最要命的是某次工具调用返回了一份两千多字的PDF解析结果,直接把后面用户的真实意图挤到了注意力边缘。
这就是上下文工程(Context Engineering)要解决的问题。过去我们聊提示词工程,是在跟模型对话的开头把话说清楚;而上下文工程处理的是Agent在长时间运行过程中,怎么在每个时刻都准确地让模型看到“此刻该看到的东西”。说直白点,它是一门关于注意力资源分配的手艺。这篇文章我想结合自己从0到1搭建几个Agent踩过的坑,把上下文工程的核心思路、技术方案和避坑经验一次讲透,给准备入坑或者已经在被上下文折磨的朋友一点参考。
1. 上下文工程到底是什么:一次线上事故让我重新认识它
1.1 一次上下文挤爆的真实案例
上面说的贷款咨询Agent只是引子,真正让我把上下文工程当回事的,是另一个更典型的案例。
当时做一个数据洞察Agent,它的任务是从用户上传的Excel里读取数据,然后回答各种统计问题。流程很简单:解析文件,抽几行样本数据,塞进提示词,让模型分析。初期一切正常,直到有用户传了一个带72列、三千多行的销售明细表。为了“让模型充分理解数据”,我们当时的设计是把前200行完整数据全部放进去。光这200行,转成Markdown表格就差不多25k token。
然后灾难来了。
第一轮问答还正常,第二轮用户追问“按地区拆一下华东大区的月度趋势”,Agent为了回答这个问题,又调了一次工具去重新读取数据。新工具结果又被完整追加进上下文。这一轮下来,上下文总token数飙到接近60k。接着模型开始“胡言乱语”,把不同月份的数字算错,甚至开始引用早期样本里根本不存在的数据。
当时我第一反应是模型能力不行,后来把完整请求日志打出来逐个字段排查,才发现问题出在上下文本身上:模型收到的内容里,有效信息占比不到三成,其余全是重复的原始表格、历史对话和无意义的中间步骤。这就是典型的上下文管理失能——窗口还够大,但注意力早被稀释了。
1.2 提示词工程与上下文工程的本质区别
很多人会问,提示词工程和上下文工程有什么区别?我拿自己的理解做个划分。
提示词工程解决的是“开局怎么把话说清楚”。你在系统提示词里定义角色、目标、约束、输出格式,这是静态的,写一次基本不动。它解决的问题是模型首轮输出的质量。
上下文工程解决的是“每一轮开局之后,模型眼前到底应该摆什么”。一个Agent每轮推理时,模型看到的其实是一个拼接起来的完整文本:系统提示词、历史对话、工具返回结果、用户当前输入、可能的中间推理状态。这个拼接的过程,每一轮都在动态变化。怎么决定哪些保留、哪些丢弃、哪些压缩、哪些检索回来,这就是上下文工程的核心。
我见过不少团队,系统提示词写了一版又一版,各种few-shot示例层层叠加,但Agent跑几轮之后依然变蠢。原因很简单:他们只优化了“开局”,没有管理“过程”。一个真实的Agent,通常跑个三五轮就会开始处理工具结果,十轮以上历史对话就开始膨胀,二十轮以上如果还没有任何压缩和淘汰策略,LLM的能力衰减会以肉眼可见的速度发生。这就是模型注意力机制的一个特性:Transformer的self-attention对位置有天然偏好,上下文中间位置的信息最容易丢失,这个现象在学术界叫“lost in the middle”。你做上下文工程的时候,必须时刻记住这个底层规律——关键指令放开头,最新信息放末尾,中间位置留给冗余容忍度高的内容。
2. 上下文窗口的结构化设计:给Agent搭一张工作台
2.1 五个区块的划分与职责
我后来把Agent每轮推理时看到的上下文,强制划分成了五个逻辑区块,像给Agent搭了一张固定工作台。这五个区块各有职责,不能混:
- 系统指令区:角色定义、业务目标、安全边界、回复风格。这部分是“宪法”,几乎固定不变。
- 工具说明区:每个工具的用途、参数、什么情况下调用。这部分是“工具箱使用手册”。
- 动态状态区:当前任务的目标、已有结论、执行进度。这部分是“工作台上的便签”。
- 观察记录区:最近几轮的工具调用结果、关键数据快照。这部分是“刚做完的实验记录”。
- 对话历史区:用户和Agent最近的问答对。这部分是“和用户聊天的录音”。
五层分完,你会发现很多问题暴露出来了。比如我们之前的Agent把所有东西混在一起,工具说明散落在对话历史中间,模型经常在需要调用工具时找不到正确的工具描述,转而自己瞎编一个工具名或者干脆不调工具。
这里要特别说明一点:不同的模型,对这五个区块的敏感度不一样。我在GPT-4级别的模型上测试过,系统指令区对行为的控制力最强;但换成某些开源模型,工具说明区的权重反而更高,因为它们的指令跟随能力相对弱,需要靠完整的工具描述来引导。所以区块划分是通用思路,具体权重分配要根据你的模型来调,没有一劳永逸的方案。
2.2 Token预算怎么分:一张可落地的配置表
上下文窗口再大,也不能真的把它当无限容量用。这里有个关键认知:窗口大小和有效注意力不是一回事。即使你用的是128k窗口的模型,当上下文里填充了太多无关内容,模型对关键信息的关注度会急剧下降,而且每次请求的延迟和成本都会随着token数线性上涨。我从实际项目中总结了一套token预算分配方案,适合大部分业务型Agent,给你做个参考:
| 区块 | 预算占比 | 参考值(以32k窗口为例) | 管理策略 |
|---|---|---|---|
| 系统指令区 | 8% | 2-3k | 固定不变,不随对话增长 |
| 工具说明区 | 3% | 1k | 按需注入,动态裁剪 |
| 动态状态区 | 15% | 4-5k | 每轮覆写,只留最新结论 |
| 观察记录区 | 25% | 8k | 保留最近2轮工具结果,旧结果做摘要 |
| 对话历史区 | 35% | 10-12k | 滑动窗口+摘要压缩 |
| 预留/用户输入 | 14% | 4-5k | 保证当前输入一定有空间 |
这个比例不是拍脑袋定的,核心逻辑是:对话历史是吃token的大头,但不能让它无限膨胀,所以要给它设一个硬上限,同时用摘要兜底;观察记录区只留最近两轮,是为了保证工具调用的链式推理不断裂,再老的记录就提炼成结论放进动态状态区。
你可能会问,那32k窗口的模型是不是必须把窗口用满才划算?恰恰相反,我实际生产中都建议把目标占用控制在窗口的60%-70%。原因有两个:第一,留出余量给模型生成输出,避免达到硬上限被截断;第二,集中注意力比扩大视野更重要,你把一个新用户的问题加一个精简的上下文压到10k以内,效果往往比塞满25k的背景资料好得多。
2.3 工具调用结果:最容易被忽略的“太肥”源头
聊到Token消耗,很多人惯性思维是“对话历史太长”,但实际上,在Agent场景里,真正把上下文撑爆的往往不是对话,而是工具返回结果。一个接口返回几千字JSON太常见了,一段网页正文动不动5k-10k token,一次PDF解析可能直接塞进20k。
我见过一个最极端的情况:有个Agent需要查询企业工商信息,工具返回的数据包里有股东、变更记录、行政处罚等十几个字段的完整JSON。开发人员图省事,直接把整个JSON塞进上下文。结果这个Agent单轮对话耗掉40k token,每次请求光模型费用就够喝一壶的。
这里有几个处理原则,都是我实测后沉淀下来的:
- 工具侧做字段裁剪:在调用工具前就定义好返回结构,只让Agent拿到当前任务真正需要的字段。例如查工商信息时按需指定只要“统一社会信用代码、注册资本、成立日期”,不要整包返回全部登记信息,从源头控制体积。
- 结果侧做截断和摘要:如果工具返回确实没办法裁剪,那就对返回内容做策略截断,保留首尾、去重,或者用一个小模型把结果先归纳成要点。
- 代码执行类工具的返回治理:比如Agent写代码去跑数据,返回的可能是一大段stderr日志。这类返回经常充斥着无意义警告,直接丢进去就是纯污染。可以在代码解释器的输出环节做处理,只保留最后一行exit code和有限长度的核心输出。
我自己后来封了一个工具结果处理器,每个工具返回前都必须过一遍:先按规则截断到2k token以内,再标记是否包含需要“长期记住”的结论,如果需要,就同步生成一条摘要存进状态区。这套改造完成后,同样一个销售分析Agent,单轮上下文token数从60k降到了12k,响应速度提升了将近一倍,而且准确率反而更高了,因为模型终于能看清数据了。
3. 上下文管理的三项核心技术:截断、压缩与检索
3.1 滑动窗口截断:先保住最近N轮
截断是上下文管理最基础的手段,也是最容易做错的。很多人简单粗暴地把对话历史从最前面砍掉,结果模型失去了关键背景信息。正确做法是滑动窗口加楼层保护。
我的配置方式是:对话历史保留最近6轮完整内容,再往前的内容全部收进摘要。这里有个细节——6轮这个数字不是随便定的,它是跟Agent的典型任务链长度强相关的。如果Agent经常需要多步工具调用,比如问完城市再问天气再定行程,这个过程可能要4-5轮才能完成,窗口小了就顶不住。而如果只是普通FAQ式问答,3轮就够了。你需要根据自己Agent最常跑的链式长度来定,然后留50%余量。
另外,截断不是只在接近上限时才做,而是每一轮都应该对历史队列做一次“过期淘汰”。我见过一些设计,等上下文快爆了才触发清理,结果一次性把前面的重要信息全砍了,Agent当场“失忆”。好的策略是把清理动作做成常态,永远保持上下文处在有序的紧凑状态。
3.2 摘要压缩:让Agent记住结论而不是原文
截断解决的是“太多了放不下”的问题,但单纯截断会丢信息。所以需要一个配套动作:对即将被挤出窗口的内容做信息蒸馏,并把蒸馏结果留在上下文里。
这其实就是摘要压缩。常见的做法有两种,一种是基于规则的——只保留关键字段,比如用户原始诉求、最终结论、重要数字;另一种是基于模型的——定期调用LLM,把历史对话浓缩成一段结构化摘要。
我实操下来的建议是:能用规则不要用模型,能用小模型不要用大模型。因为基于模型的摘要不仅花钱,还会引入幻觉。比如之前销售分析Agent,历史对话里有一轮用户提到“华东区三月的退货率大约2.3%”,模型做摘要时把这个数字写成了“2.3”,看起来差不多,但后面所有计算全部跟着错。为了防这种坑,我的摘要模板会强制要求数字必须原样引用,不允许改写;依然由模型压缩的话,压缩完我再跑一道规则校验,把关键数字和原对话做比对,不一致就回滚,改由截断方案兜底。
纯规则能解决大部分场景。把每轮对话抽取出三个字段——用户核心诉求、Agent给出的结论、未决的遗留事项——拼成一个状态块,这个状态块就是Agent后续推理的“记忆锚点”。这比把十几轮冗长对话全塞进去,效果要好得多。
3.3 向量检索召回:让长期记忆变成外挂
如果Agent要服务的场景跨越很长时间,比如用户每天来问一次,持续一个月,摘要也会越来越长,最终还是会突破窗口极限。这时候就需要第三件武器——向量检索召回。
你可以把向量检索理解为给Agent配了一个外置硬盘,平时不占用工作内存,需要时再把相关的内容加载回来。具体流程是:每一轮对话结束后,把该轮内容切片、向量化、写入向量数据库;下一轮新问题进来时,先用用户的问题去向量库里做相似度检索,把最相关的历史对话记录捞回来,拼进上下文。
这套方案在需要长期记忆的场景非常有用,比如私人财务助理、健康管理Agent、陪伴型Agent。但要注意,向量检索不是银弹,它有召回不准确的风险,检索回来的内容可能跟当前任务毫无关系,反而造成新的上下文污染。所以设计时要有阈值过滤:相似度低于某个值就不召回,并显式告诉Agent“没有找到相关历史,按新问题处理”。
3.4 混合策略怎么搭:一个值得参考的配比方案
这三种技术不是互斥的,生产级Agent通常是把它们组合在一起,形成一个分层记忆体系:
| 层级 | 技术 | 生命周期 | 典型容量 |
|---|---|---|---|
| 工作记忆 | 完整上下文窗口 | 当前轮次 | 5-20k token |
| 短期记忆 | 滑动窗口 + 摘要压缩 | 最近N轮对话 | 10-30k token |
| 长期记忆 | 向量检索召回 | 天/周/月级 | 数百万token |
我们内部把这套结构叫“三层记忆模型”,实现了之后Agent的稳定性确实有了质的提升。工作记忆承担当前推理的直接输入;短期记忆保存最近几轮的原始细节,同时用摘要保住更早的关键信息;长期记忆则负责把真正有价值的历史信息按需捞回来。三者配合,既保证了窗口不爆,也尽量减少了信息丢失。
这里要提醒一句:三层记忆模型不是一上来就该做的。如果你的Agent只是单轮问答、无状态调用,那连短期记忆都不需要,加了反而累赘。我见过不少团队刚开始就上复杂的记忆系统,结果排错排到怀疑人生。我的建议是,先跑通核心流程,在真实日志里观察上下文膨胀速率,再决定在哪个层级补强。
4. 多Agent协作中的上下文隔离与传递
4.1 会话级与任务级:上下文要分层
如果你的系统是单Agent跑完整条链路,上文那些基本够用了。但如果你在做多Agent协作——比如一个主管Agent调度几个专家Agent,就需要考虑上下文的隔离问题。
刚开始搭多Agent时,我踩过一个很蠢的坑:为了让每个专家Agent都能理解全局,我把主Agent的完整上下文复制了一份传给每个子Agent。结果就是每个子Agent回复时都“非常全面”,全面到把用户真正要问的事情淹没在了背景信息里。而且多个专家都拿着相同的历史对话,回答的口径反而各种冲突。
后来想明白了一个道理:上下文必须按职责分层,而不是按职位复制。会话级上下文是全链路共享的,只保存用户核心目标、关键约束、已完成步骤的结论;任务级上下文是每个子Agent私有的,只装完成当前子任务所需的信息。
举个实际例子,一个研究型Agent被用户要求对比三款云服务商的定价。主管Agent会拿到会话级上下文,包括用户预算、地区、需求规模,然后分配三个调研子任务给三个专家Agent。每个专家只拿到一个服务商的官方定价文档地址和调研要求,这就是任务级上下文。专家们各自干活,把结论汇总回来,主管再做最后综合。这样一来,每个Agent看到的上下文都很干净,注意力自然集中。
4.2 父子Agent之间的信息损耗怎么控制
多Agent协作有个绕不开的问题:子Agent的执行结果,要怎样传回父Agent,才能尽量减少信息损耗?
我的经验是,让子Agent的输出遵循固定结构,并且强制做三层拆分:
- 结论层:直接回答分配给它的子任务,给出结论性描述,比如“A服务商的按量付费比B贵12%”。
- 证据层:给出支撑结论的关键数据点,比如具体价格、生效条件,控制在有限字数以内。
- 原始材料层:如果是需要长期存档的完整资料,写入临时存储,只返回引用地址。
父Agent在汇总时只需要读取前两层,原始材料层按需再取。这个设计实际上是把上下文工程里的“压缩”原则应用在Agent与Agent之间的通信协议上,效果非常明显——父Agent的上下文不会再被几十页原始报告撑爆,子Agent的产出也真正地“被看到”了。
另外,父Agent给子Agent下发任务时,也要尽量少带无关历史。只传任务本身和目标约束,不要传用户跟父Agent闲聊的记录。子Agent需要保持专注,用户昨天抱怨过一句“最近带宽不太稳定”,跟今天的定价对比没有任何关系,传到子Agent那里只会造成注意力干扰。
4.3 上下文漂移:一个容易忽视的故障源
多Agent系统里还有一个很隐蔽的故障源——上下文漂移。我说的不是模型幻觉,而是指Agent系统在长时间运行后,各Agent对同一件事的理解出现偏差。
举一个真实场景:一个销售辅助Agent系统,主管Agent在会话级上下文中记录了一条规则:“给客户报价不能低于8折”。这条规则在会话开始时被正确写入了每个子Agent的任务指令里。但随着对话推进,某个子Agent在工具调用结果中看到一条“历史成交价7.5折”的信息,它就把7.5折当成新规则了。后续所有报价都按新规则走,而主管Agent并不知情,因为那个子Agent的私有上下文并没有被主管同步到。
解决漂移问题,我的做法是让子Agent在每次任务的结尾,回传一个“理解确认”字段——确认它最终执行时使用的规则是什么。父Agent拿到这个字段后跟自己的规则基线做比对,发现不一致就触发纠正。这个设计并不复杂,但能把漂移问题从“随机出现”变成“可监测、可干预”。
如果你不想做得这么重,至少要在系统里埋观察点:每个Agent在关键决策后,把它的决策依据写进日志,方便事后复盘“它是基于什么做出这个决定的”。没有这个埋点,上下文漂移即使发生了你也很难定位。
5. 避坑实录:上下文污染是最贵的隐形开销
5.1 四个常见的上下文污染来源
做了这么多项目,我总结出上下文污染的四个主要来源,每一个都是花真金白银买来的教训:
- 无关历史残留:对话轮次多了,模型把早期用户随口说的一句话当成当前指令。比如用户在开场说“等下我可能要问很多问题”,这句话一直被保留在上下文里,结果每轮问答模型都以为用户要连续追问,回答风格变得越来越发散。
- 工具结果的冗余:前面说过,这是大头。JSON字段多、日志噪声大、PDF全文解析后没有精细化提取,全都是污染源。
- 错误信息的持续放大:某轮工具调用返回了一个过期报价,模型当成事实记录进上下文,之后每一轮都基于这个错误报价计算,越跑越偏。这种污染是“传染性”的,危害极大。
- 隐含状态的错乱:多个异步任务交错进行时,上下文里的状态信息互相覆盖。比如用户同时问了两件事,Agent在处理第二件事时把第一件事的中间状态当成了当前状态。
前两种可以通过规则处理,后两种则需要靠状态管理机制来兜底。状态信息一定要显式声明它的适用范围,例如用“当前活跃目标”来标注当下正在处理的任务,用“已完成目标”来区分历史状态,模型才不会把旧的当成新的。
5.2 一套可以照着做的上下文治理清单
我给你列一份上下文质量的检查清单,做Agent开发时逐条过一遍,能过滤掉大部分低级问题:
| 检查项 | 标准 | 不合格的后果 |
|---|---|---|
| 上下文总量 | 占窗口的60%-70%以下 | 成本飙升、注意力分散 |
| 系统提示词 | 无重复、无矛盾、不超过3k | 行为漂移、指令冲突 |
| 工具描述 | 每个工具描述不超过200 token | 工具误调用、漏调用 |
| 工具返回 | 已裁剪、已结构化、不超过2k | 上下文膨胀、关键信息被淹没 |
| 历史对话 | 保留最近N轮,超出部分已摘要 | 早期噪声一直干扰后续推理 |
| 关键数字 | 摘要与原对话比对一致 | 基于错误数字推理,结果全错 |
| 子Agent输出 | 三层拆分:结论+证据+材料引用 | 父Agent上下文被撑爆,汇总困难 |
| 状态标记 | 活跃目标与已完成目标显式区分 | 任务交错时状态错乱 |
5.3 技术栈选型与工程落地的几个参考
很多人在做上下文工程时,容易一上来就想用复杂的框架。以我个人的经验,工具选型还是“够用就好”。服务端用FastAPI作为Agent网关,把用户请求接入、工具调用编排、上下文组装放在一层;流程控制用LangGraph这类带状态管理的框架,它能帮你管理好各节点之间的状态流转,状态写入和读取都有清晰的钩子;会话存储用Redis,缓存最近对话和短期摘要;向量库我常用pgvector,直接复用Postgres,省掉多维护一套基础设施的麻烦。
前端界面上,如果你在调试阶段,强烈建议搞一个请求日志查看器,把每一轮的上下文结构可视化,哪一块占了多少token、被模型实际关注的重点有哪些,全部展示出来。我见过太多团队在“黑盒”状态下调试Agent,完全靠猜。有了可视化的上下文面板,很多问题一眼就能看出来。
另外我特别想聊一下并发场景。很多朋友问Agent怎么扛并发,其实上下文工程就是扛并发的前置条件。上下文体积直接决定了每次请求的token消耗和推理延迟。我们把单个请求的上下文从60k压到12k之后,同样的GPU资源吞吐量直接翻了接近三倍。如果你的上下文没有治理好就上高并发,只会放大成本问题,而不是解决并发问题。
最后再分享一个在实战中验证过的细节:上下文工程不会一蹴而就,它需要持续观测和调优。我会在每次版本迭代后跑一轮回归,用固定的测试问题集,对比上线前后的输出稳定性,重点盯上下文占用曲线和关键决策依据的保持率。Agent的能力上限由模型决定,但它的稳定下限,基本由上下文工程的质量决定。把这块打磨到位了,你会发现Agent的“智商”其实没有那么飘。