同一个模型,同一个工具列表,两个 Agent 在第十轮时看起来差不多,到第五十轮却可能完全不同。
一个还能准确说出"刚才修改了哪些文件、测试为什么没过、下一步要做什么";另一个开始重复读文件、忘记用户约束,甚至修改已经确认过的代码。
很多时候,这不是模型智商突然掉了,而是上下文管理出了问题。
模型的能力由模型提供商决定,开发者真正能控制的,是每一轮到底把什么信息放进上下文,以及哪些信息应该留在上下文之外。上下文工程不是写一段更长、更复杂的 Prompt,它更像 Agent 的供给系统:什么时候把信息送进去,送多少,旧的怎么清理,需要时又从哪里取回来。
一、先别急着微调,Agent 的问题往往不在模型
看到 Agent 表现不好,常见反应有两个:训练自己的模型,或者等产品稳定后用强化学习优化。
这两件事都不便宜,也不应该成为默认选项。
如果 Agent 记不住上一轮读过哪个文件,可能需要的是状态管理;如果它找不到项目里的某个实现,可能需要的是更好的检索工具;如果它在第 30 轮开始无视早期约束,可能需要的是上下文压缩和重注入。
这些问题里,只有一部分和模型参数有关。大部分是上下文供给问题。
可以用五个动作来整理:
- Offload:信息太挤,先搬到文件或数据库;
- Reduce:上下文太长,压成摘要、占位符或更短的表示;
- Retrieve:需要的信息不在窗口里,按需检索回来;
- Isolate:一个窗口装不下,就拆成多个独立上下文;
- Cache:重复计算很贵,就复用已有的计算结果。
它们不是五选一,而是一套组合拳。
二、Offload:文件系统比上下文窗口便宜
上下文窗口很贵,而且满了之后会影响模型对其他信息的关注。文件系统、数据库、对象存储便宜得多。
所以第一优先级不是"怎么把更多东西塞进去",而是"有什么东西根本不需要一直待在里面"。
工具执行结果就是典型例子。一次 Grep 可能返回几百行日志,一次网页读取可能有几万字符。Agent 当前只需要知道"在哪些文件里命中了什么",完整内容可以写到磁盘,等需要时再通过 Read 读取。
这类操作叫 Context Offloading:把信息从模型窗口中移走,在外部保存,需要时再恢复。
Offload 做得好,Agent 的上下文会保持干净。它不需要记住几万行日志,只需要记住日志文件在哪里,以及下一步为什么需要它。
三、JIT 和全量预填充是两种完全不同的思路
把所有可能有用的文档、代码和工具结果全部放进上下文,叫全量预填充。它简单,但成本高,而且噪声很大。
另一种方式是 Just-in-time:不提前塞,等 Agent 真正需要时再加载。
比如排查"登录后没有跳回原页面"的 Bug,最省钱的过程不是一上来读取整个项目,而是:
- 用 Glob 按文件名找和登录、路由、中间件相关的文件,只返回路径;
- 用 Grep 搜索
returnTo、redirect等关键词,返回命中的行; - 最后用 Read 读取最可疑的文件。
这是按成本递增来使用工具:先做便宜、范围大的搜索,再做昂贵、范围小的读取。
RAG 和 Agentic Search 都是 JIT 的一种。区别在于,RAG 通常由系统在一次生成前检索相关片段;Agentic Search 则把"要不要搜、搜什么、结果够不够"交给 Agent,在循环中动态决定。
当上下文里只有真正相关的信息时,模型的注意力不会浪费在大量无关内容上。
四、Reduce:先轻量清理,实在不行再摘要
无论前置管理做得多好,长任务跑几十轮之后,工具结果和对话历史仍然会堆积。压缩不是一次操作,而应该分成几层。
Microcompact
Microcompact 不改变消息结构,也不删除整段对话,只处理旧的工具结果。
比如第三轮读取文件得到 3000 token,到了第 30 轮,这段内容已经没人再用。可以把原文替换成:
text
[Old tool result content cleared]
消息还在,对话轮次仍然可追踪,但昂贵的工具输出被释放了。
Snip
Snip 更激进,直接从历史对话头部删除老消息。被删的内容永久丢失,后面无法恢复。
它适合确实已经不重要、而且不会影响后续决策的历史。代价是容易把某条关键约束一起剪掉,所以需要状态系统标记哪些消息不能删。
Auto-compact
Auto-compact 用 LLM 生成摘要,把一大段历史替换成结构化总结。
有些实现不会等上下文真正爆满才触发,而是预留一段安全空间。例如:
text
触发阈值 = effectiveContextWindow - 13k
这里的 13k 是给下一轮回答、工具调用和恢复过程留下的余量。等到窗口 100% 再压缩,通常已经来不及。
摘要也有格式。一个可用的压缩模板会覆盖这些信息:
- 用户真正想要什么;
- 涉及哪些技术概念;
- 修改了哪些文件;
- 修过哪些错误;
- 已经解决了什么问题;
- 用户提出过哪些约束;
- 还有哪些待办;
- 当前正在做什么;
- 下一步准备做什么。
压缩之后,还要恢复关键文件。比如附上最近读过的五个文件,避免摘要生成完,Agent 马上又把它们读一遍。
最终的消息结构可以简化成:
text
[压缩边界]
[结构化摘要]
[恢复的关键文件]
[最近几轮对话]
这里有个取舍:Microcompact 信息损失小,但释放的 token 有限;Snip 释放得多,却不可恢复;Auto-compact 释放最多,但摘要器可能漏掉关键细节。顺序应该是先做轻量处理,再考虑摘要,而不是每轮都直接重写全部历史。
五、Prompt 也要模块化,不是一大段文字
Agent 的 System Prompt 和 Chatbot 的 Prompt 目标不同。
Chatbot 更在意一次回答的质量。Agent 可能连续跑 50 轮,每一轮都要作出正确决策,所以它更需要稳定性和可维护性。
把所有规则写成一个巨大字符串,很快就会遇到问题:改输出风格时碰坏身份设定,加一个工具说明时让整个前缀缓存失效,用户自定义规则又不知道该插在哪里。
更合理的做法是按职责拆模块:
ts
const systemPrompt = [
identitySection(),
systemRulesSection(),
taskGuidelinesSection(),
riskGuidelinesSection(),
toolUsageGuide(tools),
outputStyle(),
// 以下是会话级动态信息
envInfo(cwd, gitStatus),
userConfig(claudeMd),
languagePreference(lang),
memoryContext(memories),
];
静态部分尽量稳定,可以复用来提升前缀缓存命中率。动态部分每会话甚至每轮都会变化,应该放在后面,避免影响前面的缓存。
用户自定义规则通常遵循两个原则:
- 越通用的配置优先级越低;
- 相同冲突下,更具体的规则后加载。
模型对 Prompt 末尾的信息通常更敏感。全局规范放前面,项目规则和当前任务约束放后面,实际效果会更稳定。
至于当前时间、IDE 打开的文件、相关 Skill 这类高频变化信息,不一定要改 System Prompt,可以直接注入到用户消息附近:
text
<system-context>
当前打开文件:src/auth.ts(第 42 行)
相关 Skill:security-review
当前日期:2026-09-12
</system-context>
帮我看看 auth.ts 有什么问题
这样既保留了上下文,又不破坏静态前缀。
六、Cache:KV Cache、Prompt Cache 和 Context Collapse 不是一回事
缓存有三个层次,很容易混在一起。
KV Cache
KV Cache 在模型推理层,缓存的是 attention 计算过程中的 Key 和 Value。生成长文本时,前面的 token 不需要重复计算。开发者通常无法直接控制它,但模型服务商会利用它降低推理成本。
Prompt Cache
Prompt Cache 在 API 层,开发者可以通过缓存标记控制。它适合缓存稳定的系统 Prompt、工具说明和公共上下文前缀。
需要澄清一点:Prompt Cache 通常缓存的是前缀对应的计算结果,不等于"同一个问题问两次,直接返回上次答案"。后者是语义缓存,属于另一个系统,不能用 Prompt Cache 来代替。
Context Collapse
Context Collapse 处理的是历史消息。
与其把旧消息直接删除,不如把它们折叠进外部存储,上下文里只保留一个标记。后续真需要时,可以再展开或检索回来。
它和 Auto-compact 的差别在于:Auto-compact 用自然语言摘要替代历史,Context Collapse 更像可恢复的引用。
七、Isolate:上下文不够用,未必要继续压缩
有些任务不适合把所有信息放进同一个上下文,比如主 Agent 正在改代码,同时需要分析一个上万行的模块。
继续压缩可能丢细节,这时可以把任务拆给子 Agent。
多 Agent 的上下文隔离通常有两种方式:
- 消息传递:主 Agent 给子 Agent 发任务,只拿回结果,不共享内部过程;
- 上下文共享:子 Agent 复制主 Agent 的上下文,在副本上继续处理。
前者 token 成本低,隔离彻底,但子 Agent 缺少背景;后者背景完整,但复制上下文很贵,而且容易让两边状态分叉。
这里要注意,子 Agent 不是越多越好。每增加一个 Agent,就增加一组交互路径、共享状态和排查成本。只有任务边界清楚、结果可以独立验收时,拆分才划算。
八、Memory 不等于 Context
Context 是本次会话的工作台,Memory 是跨会话留下来的长期信息。
Claude Code 一类编码 Agent 常用文件式记忆:
MEMORY.md作为索引,记录用户偏好、项目规范等简短条目;- 每条记忆单独存文件,用 Frontmatter 描述类型和用途;
- 每轮只把索引放进 System Prompt,详细内容按需读取。
一条好的记忆不只是一句结论,还应该说明原因和适用方式:
md
用 pnpm,不要用 npm。
Why: 团队统一规范,锁文件和 CI 都基于 pnpm。
How to apply: 所有 install/add 命令使用 pnpm。
记忆少、单人使用时,文件方案足够。记忆多、多人协作时,数据库更容易做权限、版本和冲突处理。
但记忆系统会失效:
- 把推测当事实记录,污染记忆;
- 只写不删,低质量内容越来越多;
- 项目结构变化后,旧记忆过期;
- 新旧规则冲突,没有明确覆盖关系;
- 新项目没有项目级记忆,Agent 只能靠猜;
- 外部内容进入记忆,导致记忆投毒。
所以 Memory 不是"记得越多越好"。它同样需要写入标准、过期策略和清理机制。
九、RAG 的瓶颈通常是数据,不是模型
RAG 适用于模型没训练过的企业私有数据,也适用于 Agent 运行时需要查的外部资料。
一条完整管线包括:文档加载、分块、Embedding、向量存储、检索、注入上下文。
其中最容易被低估的是前两步。
Markdown 对 RAG 很友好。PDF 很麻烦,纯文本提取容易丢表格和标题结构,需要更强的版面解析。代码也不适合按照固定 token 硬切,因为一个函数从中间切断,两个 chunk 都可能失去意义。代码检索有时更适合 Glob、Grep、Read 这种 Agentic Search,而不是全部向量化。
分块策略也有差别:
- 固定大小最简单,但经常切断语义;
- 递归分块按段落、句子、token 逐级拆,通常更稳;
- 语义分块用 Embedding 判断相邻段落是否相关,成本更高。
检索也不是"语义相似就一定能回答"。向量召回和关键词召回各有盲区,混合检索往往更可靠。用户问题不适合直接搜索时,还可以做 Query 改写;返回一堆重复内容时,可以用 MMR 去重。
最后,RAG 可以做成一个循环:Agent 先判断要不要搜,搜完判断信息够不够,不够就换 Query 或调用其他工具,直到足够再生成答案。
十、上下文工程的检查顺序
遇到长任务效果下降,我通常会按这个顺序排查:
- 有没有把大结果 Offload,而不是一直塞进上下文;
- Prompt 是否拆成了稳定前缀和动态信息;
- 是否使用 JIT,只在需要时加载文件和文档;
- 压缩是否分层,是否保留了边界、摘要和关键文件;
- 一个上下文是否装得下任务,需不需要拆给子 Agent;
- 跨会话信息是否进入 Memory,而不是指望模型自然记得;
- 检索数据是否干净,分块是否保留了结构。
上下文工程没有一种万能模板。模型、任务长度、工具数量和数据形态都会改变答案。
但目标始终一样:让模型在需要做决定的那一刻,看到最相关的信息,同时不被历史噪声拖住。