1. 为什么上下文工程成了 Agent 落地的分水岭
过去一年我帮几个团队做过 AI Agent 的落地评审,发现一个很反直觉的现象:同一个模型、同一套工具、同一个业务场景,两个团队做出来的 Agent 效果能差出三倍以上。一个能稳定完成多步任务,另一个跑三步就开始胡言乱语、忘记目标、重复调用同一个工具。排查到最后,问题几乎从来不在模型本身,而在一个被大多数人忽略的环节——上下文工程(Context Engineering)。
很多人把上下文工程等同于"写 Prompt",这是最大的误解。Prompt 是单次输入的一段文字,而上下文是模型在某一时刻能"看到"的全部信息总和:系统指令、历史对话、工具返回结果、检索到的文档片段、当前任务状态、甚至包括 token 预算的分配方式。Agent 和普通对话机器人的本质区别在于,Agent 是多轮、有状态、会调用外部工具的,这意味着它的上下文是动态增长的、会被工具输出污染的、会随着步数累积而爆炸的。你不管它,它就会自己把自己淹死。
这篇文章我想把上下文工程这件事拆透。它适合已经跑通过一个 Demo、但发现 Agent 一上真实任务就崩的人;也适合正在设计 Agent 中台、需要给多个业务方提供稳定能力的架构同学。我会讲清楚上下文到底由哪几块组成、每一块该怎么管、token 预算怎么算、工具返回结果怎么裁剪、长任务怎么压缩记忆,以及那些只有真正踩过坑才知道的细节。全文基于我在实际项目里的做法,不是理论推演。
先说一个我自己的判断:2026 年之后,Agent 的竞争力不在模型选型,而在上下文管理。模型能力会趋同,API 会越来越便宜,但"如何在有限窗口里塞进最有价值的信息"这件事,是工程问题,是每个团队必须自己解决的。下面进入正题。
2. 拆开 Agent 的上下文:它到底由哪几块拼成
2.1 上下文不是一段文字,而是五个来源的叠加
我习惯把 Agent 单次推理时喂给模型的上下文拆成五个来源,这样排查问题时能快速定位是哪一块出了问题:
- 系统层(System):角色设定、行为约束、输出格式要求、可用工具清单。这部分相对静态,但最容易写臃肿。
- 任务层(Task):当前要完成的目标、拆解后的子任务、验收标准。这是 Agent 的"北极星",丢了它 Agent 就迷路。
- 记忆层(Memory):历史对话摘要、长期偏好、之前步骤的关键结论。注意是"摘要"不是"原文"。
- 工具层(Tool I/O):每一次工具调用的入参和返回结果。这是上下文膨胀的头号元凶。
- 检索层(Retrieval):从知识库、文档、数据库里捞出来的相关片段,也就是常说的 RAG 部分。
这五块加起来才是模型真正看到的全部。很多人的 Agent 出问题,是因为只优化了系统层(把 Prompt 写得花里胡哨),却对工具层和记忆层放任不管。结果就是:Prompt 写得再漂亮,第 8 步工具返回了一坨 3000 token 的 JSON,直接把前面的指令挤出了有效注意力范围。
2.2 为什么工具返回结果是上下文污染的重灾区
我拿一个真实场景举例。你让 Agent 去查一批订单状态,工具返回的原始 JSON 可能是这样的:
{ "code": 0, "message": "success", "data": { "total": 128, "list": [ {"order_id": "SO20260101001", "status": "shipped", "create_time": "2026-01-01 10:23:11", "logistics": {...}, "items": [...]}, ... ] } }128 条订单,每条还带物流轨迹和商品明细,轻松突破 2 万 token。模型真正需要的可能只是"128 条订单中,有 3 条处于异常状态"。你把原始 JSON 全塞进去,等于让模型在垃圾堆里找针,既浪费预算又降低准确率。
我的做法是在工具层和模型层之间加一个"结果整形器":工具返回原始数据,整形器负责提取关键字段、聚合统计、截断超长列表,只把模型决策需要的最小信息集传下去。这一步看起来简单,但它对 Agent 稳定性的提升,比换一个更强的模型还明显。
2.3 上下文窗口的"有效区"远比标称值小
现在动辄 128K、200K 甚至 1M token 的窗口,让很多人产生了"窗口够大就不用管上下文"的错觉。实测下来完全不是这么回事。模型对上下文的注意力是不均匀的:开头(系统指令)和结尾(当前问题)的注意力权重高,中间部分容易被稀释,这就是常说的"lost in the middle"现象。
我做过一个粗糙但有效的测试:把关键约束放在上下文的第 1 段、中间、最后一段,分别测 Agent 的遵守率。结果是首尾放置时遵守率能到 90% 以上,放中间时掉到 60% 左右。所以哪怕你有 1M 窗口,关键信息也必须往首尾放,中间塞的应该是可容忍丢失的背景材料。
提示:不要因为窗口大就无脑堆内容。窗口大小决定的是"能不能塞下",而上下文工程决定的是"塞进去有没有用"。这两件事完全不同。
3. Token 预算:给上下文做一份"财务规划"
3.1 先算清楚你的预算上限
做上下文工程的第一步不是写代码,是算账。假设你用的是 128K 窗口的模型,你不能真的用满 128K,因为:
- 输出也要占额度,通常要预留 4K 到 8K;
- 工具调用的 schema 定义本身占额度;
- 留 10% 到 20% 的缓冲,防止某一步突然超长。
我一般的分配是这样的(以 128K 为例):
| 上下文分区 | 预算占比 | 对应 token 数 | 说明 |
|---|---|---|---|
| 系统指令 + 工具定义 | 10% | 约 12K | 静态,尽量精简 |
| 任务目标 + 当前状态 | 10% | 约 12K | 必须常驻,不可裁剪 |
| 历史记忆摘要 | 20% | 约 25K | 滚动压缩 |
| 工具返回结果 | 30% | 约 38K | 动态,需整形 |
| 检索片段 | 20% | 约 25K | 按相关度排序 |
| 输出预留 + 缓冲 | 10% | 约 12K | 不可挪用 |
这张表不是死的,但它给了我一个硬约束:任何一块超了,就必须从这块内部裁剪,而不是去挤占别的分区。很多 Agent 崩掉就是因为工具返回结果无限膨胀,把任务目标挤没了。
3.2 用 tokenizer 精确计数,别靠字符数估算
我见过太多人用"字符数除以 4"来估算 token,中文场景下这个误差能到 50% 以上。正确做法是用对应模型的 tokenizer 精确计数。以常见的开源 tokenizer 为例:
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("your-model-tokenizer") def count_tokens(text: str) -> int: return len(tokenizer.encode(text, add_special_tokens=False)) # 在拼接上下文前,逐块计数 blocks = { "system": system_prompt, "task": task_state, "memory": memory_summary, "tool_result": tool_output, "retrieval": retrieved_docs, } total = sum(count_tokens(v) for v in blocks.values()) print(f"当前上下文占用: {total} tokens")这个计数函数要在每次拼接上下文前调用,而不是只在开发时测一次。因为工具返回是动态的,你必须实时知道当前用了多少预算,才能决定下一步是压缩还是截断。
3.3 预算超了怎么办:三级降级策略
当计数发现超预算时,我一般按这个顺序降级:
- 裁剪工具结果:只保留关键字段,长列表截断到前 N 条 + 统计摘要。这一步通常能省下最多。
- 压缩历史记忆:把更早的对话轮次合并成摘要,用模型自己生成一段"到目前为止发生了什么"。
- 降低检索片段数量:从 top-10 降到 top-3,或者只保留每段的前 200 字。
注意顺序:先动工具结果,再动记忆,最后动检索。因为工具结果的信息密度最低(大量冗余字段),记忆次之,检索片段通常是精挑细选过的,最不该动。
注意:降级策略必须是自动触发的,不能靠人工判断。我一般设两个阈值:用到 70% 预算时开始轻度压缩,用到 90% 时强制降级。这样能避免"跑着跑着突然爆窗"。
4. 工具返回结果的整形:把垃圾挡在模型之外
4.1 整形器的三个职责
工具结果整形器(Result Shaper)是我认为上下文工程里性价比最高的一个组件。它干三件事:
- 提取:从原始返回里挑出模型决策真正需要的字段;
- 聚合:把列表类数据转成统计摘要,比如"共 128 条,异常 3 条,异常订单号是……";
- 截断:对超长文本做首尾保留 + 中间省略,或者按相关度排序后取前 N。
我举个具体例子。原始工具返回 128 条订单,整形后传给模型的是:
订单查询结果摘要: - 总数:128 - 状态分布:已发货 120,待发货 5,异常 3 - 异常订单:SO20260101007(物流停滞超 48h)、SO20260101042(地址异常)、SO20260101103(支付失败) - 时间范围:2026-01-01 至 2026-01-15从 2 万 token 压到 100 token 以内,信息量却几乎没损失。模型拿到这个摘要,完全能做出"优先处理这 3 条异常订单"的决策。
4.2 整形规则要按工具类型定制
不同工具的返回结构差异很大,整形规则不能一刀切。我一般按工具类型分几类处理:
| 工具类型 | 典型返回 | 整形策略 |
|---|---|---|
| 列表查询类 | 订单、用户、日志列表 | 统计摘要 + 异常项全量 + 正常项截断 |
| 详情查询类 | 单个对象的完整字段 | 白名单字段提取,丢弃无关字段 |
| 文本生成类 | 长文档、报告 | 首尾保留 + 中间省略,或按段落相关度筛选 |
| 状态查询类 | 布尔值、状态码 | 直接透传,无需整形 |
| 错误返回类 | 异常堆栈、错误码 | 提取错误类型 + 关键信息,丢弃堆栈细节 |
这张表是我踩了很多坑总结出来的。比如错误返回,一开始我把完整堆栈传给模型,结果模型被一堆技术细节带偏,反而忘了原始任务。后来改成只传"错误类型 + 一句话描述",模型就能正确判断是重试还是换方案。
4.3 一个容易忽略的点:工具入参也要管
大家都在管工具返回,但工具入参同样会污染上下文。尤其是当 Agent 需要调用一个参数复杂的工具时,它自己生成的入参 JSON 可能很长。如果这个入参在后续轮次里一直被保留在上下文中,就会持续占用预算。
我的做法是:工具调用完成后,把入参压缩成一行摘要,比如把{"query": {...}, "filters": {...}, "pagination": {...}}压成查询订单(筛选:异常状态,分页:第1页)。这样既保留了"我做过什么"的信息,又省下了大量 token。
5. 记忆管理:让 Agent 记住该记的,忘掉该忘的
5.1 短期记忆和长期记忆要分开处理
Agent 的记忆我分成两层:
- 短期记忆:当前任务内的对话历史、已完成的步骤、中间结论。任务结束就丢弃。
- 长期记忆:跨任务的用户偏好、领域知识、历史决策。需要持久化存储。
这两层的管理策略完全不同。短期记忆的核心是滚动压缩,长期记忆的核心是按需检索。很多人把两者混在一起,结果要么短期记忆太长爆窗,要么长期记忆检索不准。
5.2 短期记忆的滚动压缩怎么做
短期记忆不能无限增长,我一般用"滑动窗口 + 摘要"的组合:
- 保留最近 N 轮(比如 5 轮)的完整对话;
- 更早的轮次合并成一段摘要,由模型生成;
- 摘要里必须包含:已完成的关键步骤、当前任务状态、未解决的问题。
生成摘要的 Prompt 我一般这么写:
请把以下对话历史压缩成一段不超过 300 字的摘要,必须保留: 1. 已经完成的关键操作及其结果 2. 当前任务的进展状态 3. 尚未解决的问题或待确认的信息 不要保留:寒暄、重复的确认、已被推翻的中间结论。 对话历史: {history}关键是那句"不要保留什么"。不明确告诉模型丢弃什么,它会把所有东西都塞进摘要,压缩就白做了。
5.3 长期记忆的检索要带"时效衰减"
长期记忆检索最容易犯的错是只看相关度,不看时间。用户三个月前说过的偏好,可能早就变了。我的做法是在检索打分里加一个时效衰减因子:
最终得分 = 相关度得分 × 衰减系数 衰减系数 = exp(-λ × 距今天数)λ 的取值看业务,快变的偏好(比如当前项目)λ 取大一点,稳定的知识(比如用户所在行业)λ 取小一点。这样能保证检索出来的记忆既相关又新鲜。
提示:长期记忆一定要有"遗忘机制"。我见过一个 Agent 因为记住了用户半年前的一次错误操作,之后每次都在错误的路径上打转。记忆不是越多越好,是越准越好。
6. 长任务下的上下文压缩实战
6.1 什么时候该压缩,什么时候不该
压缩是有代价的:每次压缩都会损失信息,而且压缩本身要消耗一次模型调用。所以不是所有长任务都值得压缩。我的判断标准是:
- 任务步数超过 10 步,且每步都有工具调用 → 必须压缩;
- 任务步数少但单步返回巨大 → 优先整形,不一定要压缩;
- 任务需要严格追溯每一步细节(比如审计场景)→ 压缩要谨慎,改用外部存储 + 按需检索。
6.2 分层压缩:把不同信息用不同力度压
我一般把压缩分成三档:
- 轻压缩:只去掉冗余字段和重复内容,保留原文结构。适合最近几轮。
- 中压缩:合并同类项,生成结构化摘要。适合中间轮次。
- 重压缩:只保留结论和状态,丢弃过程。适合很早的轮次。
这样做的原因是:越近的信息越可能被用到,压缩力度要轻;越远的信息越可能只是背景,可以压得狠一点。一刀切地压缩所有历史,会导致近期关键信息也被损失。
6.3 压缩后的验证不能省
压缩完不能直接用,必须验证。我一般做两个检查:
- 关键信息是否还在:把压缩前的关键实体(订单号、用户 ID、任务目标)列出来,检查压缩后是否都还在。
- 压缩比是否合理:如果压缩后 token 数没降多少,说明压缩没起作用;如果降得太狠(比如降了 95%),要警惕信息损失过多。
def validate_compression(original: str, compressed: str, key_entities: list) -> bool: # 检查关键实体是否保留 for entity in key_entities: if entity not in compressed: print(f"警告:关键实体丢失 -> {entity}") return False # 检查压缩比 ratio = count_tokens(compressed) / count_tokens(original) if ratio > 0.8: print(f"警告:压缩效果不足,压缩比 {ratio:.2f}") if ratio < 0.05: print(f"警告:压缩过狠,可能丢失信息,压缩比 {ratio:.2f}") return True这个验证函数我几乎每个项目都会加,它帮我抓到过好几次"压缩把关键订单号弄丢了"的事故。
7. 那些只有踩过才知道的上下文工程细节
7.1 系统指令要"短而硬",不要"长而软"
我早期写系统指令喜欢面面俱到,写个两三千字,结果模型反而抓不住重点。后来改成只保留硬约束:角色、必须遵守的规则、输出格式。软性的风格描述能删就删。系统指令越短,模型对它的遵守率越高,因为它不会被自己的其他描述稀释。
7.2 工具定义本身也是上下文负担
每个工具的 schema 定义都要占 token。如果你给 Agent 挂了 30 个工具,光工具定义就可能吃掉上万 token。我的做法是按任务动态加载工具:当前任务只需要 5 个工具,就只挂这 5 个,其余的不进上下文。这一招对多工具 Agent 的效果提升非常明显。
7.3 上下文里的顺序会影响模型决策
同样的信息,放在不同位置,模型的决策可能不同。我一般遵循这个顺序:系统指令 → 任务目标 → 当前状态 → 工具结果 → 检索片段 → 当前问题。把"当前问题"放最后,是因为模型对结尾的注意力最高,能确保它聚焦在"现在要干什么"上。
7.4 别让 Agent 自己管理上下文
有些框架让 Agent 自己决定"要不要压缩记忆""要不要丢弃历史",听起来很智能,实测很不稳。Agent 在任务中途很难准确判断哪些信息未来会用到。我的做法是把上下文管理做成框架层的确定性逻辑,Agent 只负责决策任务本身,不负责管理自己的记忆。职责分离之后,稳定性提升一大截。
7.5 监控上下文占用,像监控内存一样
上线之后,我会给每个 Agent 加一个上下文占用的监控指标,记录每一步的 token 使用量。一旦发现某个任务的上下文占用持续逼近上限,就说明整形或压缩策略需要调整。这个监控帮我提前发现过好几次"某个工具返回突然变大"的问题。
8. 一个可复用的上下文组装流程
把上面所有东西串起来,我实际项目里的上下文组装流程大致是这样的:
- 加载静态部分:系统指令 + 当前任务需要的工具定义;
- 注入任务状态:当前目标、已完成步骤、待办事项;
- 拼接记忆:最近 N 轮完整对话 + 更早轮次的摘要;
- 整形工具结果:对最近一次工具返回做提取、聚合、截断;
- 检索相关片段:按相关度和时效打分,取 top-K;
- 计算总 token:用 tokenizer 精确计数;
- 触发降级:超预算则按"工具结果 → 记忆 → 检索"顺序压缩;
- 按固定顺序拼接:系统 → 任务 → 状态 → 工具 → 检索 → 当前问题;
- 记录监控指标:把本次 token 占用写入监控。
这套流程看起来步骤多,但每一步都是确定性的,不依赖模型判断,所以非常稳。我把它封装成一个ContextBuilder类,所有 Agent 共用,改一处就能全局生效。
class ContextBuilder: def __init__(self, tokenizer, budget=128000): self.tokenizer = tokenizer self.budget = budget def build(self, system, task, memory, tool_result, retrieval, question): blocks = [system, task, memory, tool_result, retrieval, question] total = sum(self._count(b) for b in blocks) if total > self.budget * 0.9: tool_result = self._shrink_tool(tool_result) memory = self._compress_memory(memory) retrieval = self._trim_retrieval(retrieval) return self._assemble(system, task, memory, tool_result, retrieval, question)这个类的核心思想就是:把上下文管理从"每次手写"变成"统一组装"。一旦统一,你就能对它做监控、做优化、做回归测试。
9. 我在这件事上的几点个人体会
做了这么多 Agent 项目,我最大的体会是:上下文工程是一个"减法"的活,不是"加法"的活。新手总想着往上下文里塞更多信息,觉得信息越多模型越聪明;老手想的是怎么把不必要的信息拿掉,让模型只看到它真正需要的。这个思维转变,是从"能跑通 Demo"到"能上生产"的关键。
第二个体会是,上下文工程没有银弹,只有针对具体场景的调优。工具返回长什么样、任务有多少步、用户对延迟多敏感,这些都会影响你的策略。所以别指望抄一套配置就万事大吉,你得自己测、自己调、自己监控。
第三个体会是,把上下文管理做成确定性逻辑,比让模型自己管要靠谱得多。模型擅长的是理解和生成,不擅长的是资源管理和状态维护。把后者交给代码,前者交给模型,各司其职,Agent 才能稳。
最后分享一个我常用的小技巧:给上下文每个分区打标签。比如用[SYSTEM]、[TASK]、[TOOL]、[MEMORY]这样的标记把不同来源的内容隔开。这样不仅模型更容易区分信息边界,你自己排查问题时也能一眼看出是哪块内容出了问题。这个习惯帮我省下了大量调试时间。