Agent 上下文管理实现详解:从窗口工程到长期记忆

30 阅读15分钟

核心判断: 上下文窗口是 RAM,不是硬盘。窗口越大≠效果越好——研究里叫 context rot(上下文腐化):token 越多,模型对中间信息的召回精度越下降。所以主流 Agent 的工程目标不是"塞下更多",而是每一步只塞最小的高信号 token 集。这篇文档拆的就是这件事在 Claude Code、Cline、Aider、LangGraph、OpenAI 这些产品里具体怎么落地。

暂时无法在飞书文档外展示此内容

一、为什么上下文管理是 Agent 工程的第一问题

普通聊天是一问一答,上下文一两轮就清空。Agent 是循环——模型自己调工具、看结果、再调下一个,一个任务跑几十上百轮。每轮都产生新的 token:用户消息、模型推理、工具调用 JSON、工具返回结果。窗口很快就会撑满。

撑满之后不是简单的"报错",而是四种隐性失败,每一种都会让 Agent 悄悄变差:

失败模式含义典型症状
Context Poisoning 上下文中毒早期一次幻觉被塞进了历史,后面所有轮次都基于这个错误前提推理。Agent 越跑越偏,跟用户说的事实对不上。
Context Distraction 上下文分心窗口里塞了太多无关信息,模型注意力被稀释,抓不住重点。明明给了正确线索,模型却忽略了。
Context Confusion 上下文混淆历史里有多条相似但不同的信息,模型混淆了哪条是当前任务要用的。用了上一个任务的文件路径/参数。
Context Clash 上下文冲突历史里两条信息互相矛盾(用户先说 A 后说 B),模型不知道听谁的。输出前后矛盾,或莫名其妙取了旧值。

叠加 context rot(长窗口召回精度自然下降)和成本/延迟随 token 线性上涨,结论很直接:不管窗口多大(128k、200k、1M),都必须主动做上下文管理。Anthropic 自己的说法是——"把上下文当成有限资源,而不是可以无限灌的桶"。

二、一个典型 Agent 的上下文到底由什么组成

把一次 LLM 调用的输入拆开看,按顺序通常是这样:

组成部分内容是否跨轮变化典型占比
System Prompt角色、行为准则、输出格式、拒绝策略、工具用法示例。基本不变5%~10%
工具定义所有可用工具的 name + JSON Schema + description。基本不变(除非动态增减工具)5%~15%
长期记忆 / 项目文件CLAUDE.md、rules 文件、用户画像、关键事实笔记。偶尔更新3%~8%
Message History对话历史:用户/助手/工具角色的消息列表。每轮追加20%~40%
Tool Results最近几次工具调用的返回结果(文件内容、搜索结果、SQL 输出)。每轮追加15%~30%
当前用户消息这一轮用户的输入。这一轮才有1%~5%

关键观察:真正每轮变化的只是后 50%~70% 。前面那 30%~40%(system + tools + memory file)在多轮之间是固定的——这就是 Prefix Cache 的物理基础,后面会专门讲。

上下文管理的四件事——Write / Select / Compress / Isolate——本质就是在管这张表里的每一行:哪些要预加载、哪些要按需取、哪些要压掉、哪些要拆出去。

三、策略一:Write —— 把信息写到窗口外面

核心思想:窗口里只放"这一步必须用的",其他信息写到外部存储(文件、数据库、state object),需要时再用工具读回来。

3.1 Scratchpad(草稿纸)

让 Agent 自己维护一个工作笔记,跨轮持久化。常见实现:

  • 文件即笔记:Claude Code 的 TODO list、项目根目录的 NOTES.md / progress.md。Agent 用 Edit/Write 工具更新,下一轮用 Read 读回来。
  • State object 字段:LangGraph 的 state schema 里加一个 notes 字段,每轮由模型写入、系统自动持久化,不用工具调用。
  • 专门的 Memory 工具:Anthropic 在 Sonnet 4.5 上线了 memory 工具(beta),就是把"存笔记/读笔记"抽象成两个 function call,模型自己决定何时写、何时读。

Anthropic 拿 Claude 玩 Pokémon 做过实验:不开记忆,跑几百步就忘自己在干嘛;开了结构化笔记,Agent 自己在笔记里维护"已探索区域地图 / 已解锁道具 / 战斗策略",跨几千步还能连贯推进。

3.2 Procedural Memory(程序性记忆):CLAUDE.md 这一类

不是事实,而是"做事的规则"。典型形式:

  • Claude Code 的 CLAUDE.md(项目根目录)和用户级 ~/.claude/CLAUDE.md。
  • Cursor 的 .cursorrules、Windsurf 的 .windsurfrules。
  • Cline 的 Memory Bank:在项目里建一组 markdown(projectBrief.md、techContext.md、productContext.md 等),每次启动自动加载。

这些文件每轮都被塞进上下文(属于"长期记忆"那一段),所以要克制——它们是 system prompt 的延伸,不是日志。Anthropic 的建议是"minimal but complete":够指导行为,又不冗余。

3.3 Long-term Memory(长期记忆):跨会话的用户画像

跨会话记住"用户是谁、偏好什么、之前做过什么"。专门做这一层的产品:

产品核心机制写入/召回方式
Mem0从对话里抽事实,自动去重、打分、存向量库。用户和助手每轮对话后异步抽 memory;召回用 embedding + 时间衰减。
Letta (MemGPT)把 LLM 当 CPU,显式划分 main context(RAM)和 external context(磁盘),模型自己决定 page in/page out。通过 core_memory(窗口内)和 archival_memory(外部)两个工具手动管理。
Zep时序知识图谱,把记忆建成图(实体 + 关系 + 时间)。不是简单向量检索,而是图查询——能回答"用户上次说的那个项目用的什么技术栈"。
ChatGPT 自动记忆对话中自动识别值得记住的事实,存到用户级 memory。召回靠 embedding;问题是会误召回(Simon Willison 举过例子:自己要求生成图片,结果模型把"他在哪个城市"也从记忆里拉进去了)。

工程经验:写入要异步(不要让用户等抽 memory);召回要按用户隔离(A 的偏好不能给 B 用);过期事实要有 TTL 或被新事实覆盖。

四、策略二:Select —— 每轮只选相关的进来

Write 解决了"存哪儿",Select 解决"每轮拿什么"。不是所有信息都该进窗口。

4.1 预加载 vs 即时加载

策略做法适用
预加载 (eager)启动时就把 system prompt、工具定义、CLAUDE.md、最近一次任务状态塞进窗口。小项目、规则稳定、首轮延迟不敏感。
即时加载(lazy / just-in-time )窗口里只放轻量标识符(文件路径、URL、query),模型用 Read/grep/HTTP 工具按需取。大代码库、长文档、动态数据。Claude Code 就是这种:CLAUDE.md 预加载,其他文件靠 glob + grep + Read 现取。

Anthropic 把后者叫 "just-in-time context":别预计算塞进来,给模型工具让它自己导航。好处是不用维护复杂的索引,坏处是多了一轮工具往返。生产里通常是混合:稳定的规则预加载,动态的内容即时取。

4.2 RAG:按相关度检索

知识库大到塞不进窗口,就做检索。工程细节:

  • Chunking:按语义边界切(代码用 AST 切函数/类,文档按段落 + overlap),不要按固定字符数硬切。
  • 混合检索:embedding(语义)+ BM25(关键词)两路召回,再用 cross-encoder re-rank 取 Top-K。纯 embedding 在代码库上不可靠(Windsurf 工程博客原话)。
  • 上下文拼接模板:召回结果不是直接拼进 history,而是包成一个 <context>... 块放在用户消息前面,并标注来源,方便模型区分"这是检索来的参考"还是"用户原话"。

4.3 工具 RAG:工具太多时也要选

当工具数量超过 ~20 个,工具定义本身就能吃掉 10k+ token,而且模型会因为描述相似而选错工具。做法:把所有工具描述做 embedding,每轮根据用户消息语义检索 Top-K 个相关工具,只把这几个塞进 system。LangGraph 的 BigTool 库就是干这个的,论文里说工具选择准确率提升 ~3 倍。

五、策略三:Compress —— 窗口要爆时压掉

前面是预防,Compress 是兜底。触发时机通常是"窗口用了 80%~95%"。

5.1 Tool Result Clearing(最轻量)

最安全的压缩:不改对话,只把旧工具结果替换成占位符。比如第 5 轮那个 grep 结果,到第 30 轮已经没用了,换成:

[Tool result cleared: grep returned 12 matches in src/; see original output at turn 5]

Anthropic 在 Claude Developer Platform 上线的 "tool result clearing" 就是这个——不动模型行为,只省 token。这是第一档该做的事。

5.2 Auto-Compact(全轨迹摘要)

窗口接近满时(Claude Code 是 95%),把整个对话历史喂给模型,让它输出一段摘要,然后用"摘要 + 最近 N 轮原始消息"重启一个新窗口。

关键是摘要 prompt 里要明确保留什么、丢弃什么。Claude Code 的做法:

  • 保留:架构决策、未解决的 bug、实现细节、用户偏好、当前任务目标。
  • 丢弃:冗余的工具输出、重复的推理过程、已经完成的子任务细节。
  • 附加:压缩后再拼上最近访问的 5 个文件,让模型有即时上下文。

工程经验:先调 recall(确保摘要不丢关键事实),再调 precision(去掉冗余)。压缩太狠会丢"现在看起来没用、后面才发现关键"的细节。

5.3 递归 / 分层摘要

超长任务(几小时的迁移、几十轮调研)不会只压一次。做法:

  • 每过 N 轮做一次局部摘要(这一段的工具调用和结果)。
  • 窗口再满时,把多个局部摘要再压成一个全局摘要。
  • 形成"摘要的摘要"的树状结构,类似于人类的会议纪要分层。

5.4 Trimming(硬切)

最暴力的方式:直接丢掉最旧的 K 条消息。优点是零成本、确定性;缺点是可能丢掉早期重要约束(用户第一轮说的"不要用 X 库")。工程上一般配合"系统级消息保护":system prompt 和前几条关键用户消息永远不切。

六、策略四:Isolate —— 拆窗口,不要一个 Agent 扛所有事

有些任务本身就需要探索("帮我调研一下 A、B、C 三个方案"),让一个 Agent 在一个窗口里搜三个方案,结果就是互相干扰。拆成子 Agent 是最干净的解法。

6.1 Sub-Agent(子代理)

主 Agent 只做规划和综合,子 Agent 各拿一个独立窗口去深潜:

  • 每个子 Agent 有自己的 system prompt、工具集、消息历史。
  • 它可以用几万 token 去搜、去读文件、去跑命令。
  • 完成后只回传 1k~2k token 的摘要给主 Agent。

Anthropic 的 multi-agent research system 论文里,这种架构在复杂调研任务上明显优于单 Agent。代价是 token 消耗可能是单 Agent 的 15 倍——所以只在并行探索有价值时用。

6.2 Sandbox(沙箱隔离大对象)

CodeAgent(HuggingFace deep researcher 用的那种):模型输出一段 Python 代码,在沙箱里执行。大对象(图片、音频、CSV、中间结果)直接作为 Python 变量留在沙箱内存里,不进 LLM 上下文。回传给模型的只有 print 出来的几行结果或变量引用。这是隔离"重数据"的标准做法。

6.3 State Schema 隔离

LangGraph 式的状态对象:schema 里有很多字段,但只有 messages 这一个字段被暴露给 LLM。其他字段(工具结果缓存、中间计算、用户画像)留在 state 里,需要时由代码决定何时注入。这样 LLM 看到的永远是干净的消息流,不会被中间状态污染。

暂时无法在飞书文档外展示此内容

七、主流产品的具体实现拆解

7.1 Claude Code(Anthropic 官方 CLI)

  • 预加载:项目根目录 CLAUDE.md + 用户级 ~/.claude/CLAUDE.md + 系统 prompt。
  • 即时加载:内置工具 Read / Glob / Grep / Bash,模型自己导航文件系统。不做 codebase 索引。
  • 压缩:Auto-compact 在 95% 窗口时触发,模型输出摘要 + 最近 5 个文件重启。
  • 记忆工具:Sonnet 4.5 起有专门的 memory 工具(写文件到 memory 目录)。
  • 子代理:可以 spawn 子 Agent,主 Agent 只拿摘要。

7.2 Cline(VS Code 编码 Agent)

  • 不做 codebase 索引:官方博客专门解释——RAG 索引在大代码库上不可靠,而且会过期。让模型用 Read/Grep 工具即时探索更准。
  • Memory Bank:项目里预建一组 markdown 文件(projectBrief / techContext / productContext / activeContext),每次会话启动加载,作为程序性记忆。
  • 渐进式披露:工具返回大文件时不一次性塞,而是分页、截断、显示目录树,让模型自己决定要不要继续读。

7.3 Aider(终端 pair-programming)

  • Repo Map:用 tree-sitter 解析整个代码库,抽出每个文件的类/函数/方法,生成一个 symbol map。
  • 重要性打分:按 import 关系、被引用次数、与当前编辑文件的距离打分,只把 top-N 的符号塞进上下文(典型几百行)。
  • 好处:模型不用读整个文件就能知道"项目里有哪些函数、它们在哪",改代码时不会瞎编 API。

7.4 LangGraph(编排框架)

  • 短期记忆:checkpointer 自动持久化每一步的 state(SQLite/Postgres),相当于自动 scratchpad。
  • 压缩节点:在 graph 里加一个 summarization node,条件边判断"消息历史超过 N token 就走压缩分支"。
  • 长期记忆:LangMem / Store API,支持 embedding 检索和按用户分库。
  • 隔离:state schema 设计 + 子图(subgraph)天然隔离不同阶段的上下文。

7.5 OpenAI Responses API / Assistants

  • Thread:服务端维护对话历史,客户端每次只传新增消息,不用自己管 messages 数组。
  • 自动截断:超过模型上下文窗口时,服务端自动丢旧消息(具体策略不透明)。
  • 内置工具:web_search / file_search / code_interpreter 都是服务端执行,大结果不会回到上下文。

7.6 长期记忆专门层:Mem0 / Letta / Zep

不自己造轮子时直接用:Mem0 适合"用户画像 + 偏好",Letta 适合"Agent 自主管理记忆",Zep 适合"需要实体关系推理"的场景。它们都是独立的 memory service,Agent 通过 API 读写,不占 LLM 上下文。

八、Prefix Cache 工程:被低估的成本杠杆

前面说过,system prompt + tools + memory file 这一段在多轮间是固定的。主流推理服务(OpenAI、Anthropic、火山方舟)都支持 Prefix Caching:这段固定前缀只算一次,后续轮次命中缓存直接复用,输入 token 价格打 1~2 折,首 token 延迟大幅下降。

工程上的硬规则:

  • 固定前缀放在最前面:system prompt → tools → memory file → 最近消息。顺序不能变。
  • 同一会话内不要改 system prompt / 工具列表 / thinking 配置——改一处整段缓存失效。调试期可以接受,线上必须冻结。
  • 用户消息和历史放在前缀之后,这样它们变化不会影响前缀缓存命中。
  • 多轮 Agent 的成本大头其实是缓存命中后的输入 token,不是输出。一个长会话不做 prefix cache,账单可能差 5~10 倍。

九、工具结果怎么处理:最容易被忽略的一块

工具结果是上下文膨胀的最大来源——一次搜索返回 10 条结果、一次 Read 读一个 500 行文件、一次 SQL 返回 1000 行。直接全塞窗口,3 轮就满。

工具类型处理策略
文件读取超过 ~200 行只返回目录/前 N 行 + 文件大小提示,让模型决定要不要 offset 继续读。
搜索结果只返回 title + URL + snippet,不要全文;re-rank 后取 Top-5。
SQL / API限制返回行数(LIMIT 20),返回 schema + 前几行样本,不要全量 dump。
命令输出stderr 全保留,stdout 截断到 ~2000 token,附"输出被截断,需要更多请用 head/tail 工具"。

核心原则:工具返回给 LLM 的是"索引和样本",不是"数据本身" 。数据留在工具侧,模型要更多就再调一次工具。

十、上线前的上下文管理检查清单

问题合格标准
固定前缀占比多少?system + tools + memory file 合计 ≤ 窗口的 30%,且多轮不变(prefix cache 友好)。
工具结果怎么截断?大结果有明确的截断策略和"继续读"的工具,不是全量塞。
窗口到 80% 时怎么办?有自动压缩(tool result clearing → summarization),不是硬切或报错。
压缩时保留什么?有明确的 summary prompt,保留任务目标、未解决问题、用户偏好;丢弃冗余工具输出。
跨会话记忆存哪?独立 memory service / 文件,按用户隔离,有过期策略。
工具超过 20 个怎么办?做工具描述语义检索,每轮只塞相关 Top-K。
长任务怎么拆?子 Agent 独立窗口,只回摘要;大对象走 sandbox 不进上下文。
怎么验证上下文管理有效?有离线 case 库,改压缩策略后跑回归,对比任务成功率不下降。

资料来源

本文为工程综述,具体阈值(如 95% auto-compact、Top-K 取值)来自各产品公开文档与工程博客,实际项目需按自身任务和模型窗口实测调整。