Agent 上下文管理
2026/7/1 2:17:51 网站建设 项目流程

Agent 上下文管理

目录

  • 上下文是什么
  • 上下文都去哪了
  • 上下文压缩
  • 上下文隔离
  • 上下文按需加载
  • 上下文持久化
  • 表格总结
  • 小结

LLM 每次调用都是无状态的。它不记得你上一轮说了什么,除非你把之前的对话历史重新发给它。Agent 框架做的事情,就是维护一个messages数组,每轮对话都把完整历史发给模型。

回到 AgentLoop 的核心结构:

messages=[{"role":"user","content":user_input}]whileTrue:response=call_llm(messages)messages.append(response)ifnotresponse.has_tool_use():breakresult=execute_tool(response)messages.append(result)

每一轮循环,messages都在增长。用户输入、模型回复、工具调用、工具结果,全部堆积在里面。假设一轮对话产生 4 条消息,每条平均 500 token,那 20 轮下来就是 40000 token。而且每一轮 API 调用,都要把这个 40000 token 的数组完整发送一遍——前面的内容反复发送,后面的内容不断累加。

问题来了:模型的上下文窗口是有限的。Claude 的上下文窗口是 200K token,听起来很大,但一个复杂的编码任务,读几个文件、跑几次测试、来回改几轮代码,很容易就会逼近上限。一旦超出,只能报错,或者被迫丢弃早期消息,而那些消息里可能包含你最初的需求描述和关键设计决策。

上下文管理要解决的就是这个问题:在有限的窗口里,让模型始终看到最有价值的信息。

上下文是什么

在讨论管理策略之前,先搞清楚上下文到底由哪些部分组成。

每次调用 LLM API,发送的内容大致是这样的:

System prompt 和工具定义是固定开销,通常占几千到一两万 token。真正快速增长的是 messages 数组——也就是对话历史。

对话历史是上下文里的大头,而且它的增长是线性的:每多一轮对话,就多几条消息,每条消息几百到几千 token。跑个十几轮,对话历史就能占据大半窗口。

工具输出是另一个容易被忽略的大头。Agent 读一个文件,几百行代码全部塞进上下文;跑一个测试,控制台输出可能几千行。这些原始数据里可能只有一小部分对决策有用,但全部都要算 token。

上下文都去哪了

来看一个具体例子。你让 Agent 帮你实现一个用户注册模块,它需要读现有代码、写新代码、跑测试。假设跑了 10 轮:

轮次 操作 新增 token(估算) ────────────────────────────────────────────────────── 1 用户输入需求 ~100 2 Agent 读了 3 个文件 ~3000(文件内容) 3 Agent 写了注册代码 ~800 4 Agent 跑测试,输出测试日志 ~2000 5 测试失败,Agent 读错误日志 ~1500 6 Agent 修改代码 ~600 7 Agent 再次跑测试 ~2000 8 测试通过,Agent 读了另一个文件 ~1500 9 Agent 做了一些重构 ~1000 10 Agent 输出最终总结 ~300 ────────────────────────────────────────────────────── 累计 ~12800 token

这只是一个相对简单的任务。如果任务更复杂,比如涉及多个模块、需要反复调试、中间有大量试错,token 消耗会轻松翻几倍。而且每一轮 API 调用,都要把这 12800 token 的历史完整发送一遍。第 10 轮调用时,第一轮的需求描述已经离"头部"很远了,但它依然占着 token。

更要命的是,早期那些消息里包含了大量"过程性"信息——测试失败的堆栈、读文件的原始输出、中间版本的代码。这些在任务完成后对模型决策几乎没有帮助,但它们一直待在那里,挤占空间。

所以问题的本质是:不是上下文不够大,是里面塞了太多"没用的东西"。管理上下文的核心,就是决定什么该留、什么该扔、什么该压缩。

上下文压缩

最直接的策略:当对话历史太长时,自动压缩它。

Claude Code 内置了一个叫auto-compact的机制。当 token 用量逼近窗口上限时,它会自动触发压缩,把早期的对话历史做一次总结,用一段精炼的文本替代原来的大段消息。

压缩前:

messages[0]: 用户需求(100 token) messages[1]: Agent 读文件 A 的完整内容(2000 token) messages[2]: Agent 读文件 B 的完整内容(1500 token) messages[3]: Agent 的分析(400 token) messages[4]: 工具调用结果(800 token) messages[5]: Agent 修改代码(600 token) messages[6]: 测试输出(2000 token) ...

压缩后:

messages[0]: "用户要求实现注册模块。已读取文件 A(UserService.java)和文件 B (UserController.java)。第一版代码已写好,测试失败原因是参数校验 逻辑有误,已修复并通过测试。"(~150 token) messages[n-2]: 最近的几条消息(保留原始内容) messages[n-1]: 最新消息

原来几千 token 的历史,被压缩成了一小段摘要。模型依然知道之前发生了什么,但不需要看那些冗长的原始数据。

压缩本质是用模型的理解能力换 token 空间。让模型读一遍早期消息,提炼出关键信息,用更少的文字表达同样的意思。代价是丢失一些细节,但对于大多数任务来说,那些细节本来也不需要了。

实际使用中,你可以在 Claude Code 里手动触发压缩:输入/compact就会执行一次。或者等它自动触发:当 token 用量超过 80% 时,Claude Code 会主动压缩。

上下文隔离

压缩解决的是"消息太长"的问题,但有一种情况压缩也帮不了:当 Agent 需要同时处理多个子任务时,所有子任务的中间过程都堆在同一个上下文里,互相挤占空间。

这就是 SubAgent 要解决的问题。

回忆一下 SubAgent 的核心思路:主 Agent 不亲自执行复杂子任务,而是派一个"下属"去做。这个下属有自己的上下文,干完活只把结论交回来,中间过程自己消化掉。

主 Agent(上下文保持精简) │ ├── SubAgent A → 独立上下文,执行完只返回结论 ├── SubAgent B → 独立上下文,执行完只返回结论 └── SubAgent C → 独立上下文,执行完只返回结论

来看一个对比。不用 SubAgent 的情况下,主 Agent 自己去读 5 个文件、写代码、跑测试:

主 Agent 上下文: [用户需求] [读文件 A 的完整内容] ← 2000 token [读文件 B 的完整内容] ← 1500 token [读文件 C 的完整内容] ← 1800 token [Agent 分析] [写代码] [测试输出] ← 2000 token [修改代码] [再次测试输出] ← 2000 token [最终结论] 总计:~12000 token

用 SubAgent 的情况下,主 Agent 把"实现注册模块"这个任务派给 SubAgent:

主 Agent 上下文: [用户需求] [SubAgent 返回的结论] ← 200 token 总计:~500 token SubAgent 上下文(独立的,执行完就销毁): [任务描述] [读文件 A/B/C] [写代码] [测试输出] [修改代码] [再次测试] [结论]

主 Agent 的上下文从 12000 token 降到了 500 token。那些文件内容、测试输出、代码迭代,全部留在 SubAgent 自己的上下文里,不会污染主 Agent。

SubAgent 同时做了两件事:隔离(中间过程不回流到主 Agent)和压缩(只返回精炼结论,不返回原始数据)。

上下文按需加载

前面讲的压缩和隔离,都是在"已经产生了大量上下文"之后的应对策略。有没有办法从源头上减少上下文的膨胀?

有。核心思路是:不要提前加载用不到的东西。

Skill 机制就是一个典型例子。如果你有 50 个 Skill,每个 Skill 的完整内容平均 2000 token,全塞进 system prompt 就是 100000 token——一半的窗口直接被吃掉了,而且大部分 Skill 在当前对话里根本用不到。

Skill 的做法是分两层:

第一层:目录(始终在 system prompt 里,~100 token/Skill) - code-review: 审查代码变更 - api-doc: 生成 API 文档 - db-migration: 数据库迁移 第二层:完整内容(按需通过工具调用加载,~2000 token/Skill) - 只有 Agent 决定使用某个 Skill 时,才加载它的完整内容

Agent 每轮都能看到"我有哪些技能可用",但只看到名字和描述。真正需要某个 Skill 时,调用load_skill工具把完整内容拉进来。这样 50 个 Skill 的目录只占 5000 token,而不是 100000 token。

CLAUDE.md 也是类似的道理。你每次对话都要告诉 Agent “我们用 SpringBoot”、“测试用 JUnit”、“接口风格是 RESTful”,这些信息每次都要说一遍,每次都要花 token。写进 CLAUDE.md 之后,它作为 system prompt 的一部分自动加载,你不用再重复。虽然单次省的不多,但几十轮对话下来,避免的重复 token 消耗是很可观的。

更重要的是,CLAUDE.md 减少了"模型忘了你的偏好 → 输出不符合要求 → 你纠正 → 模型重来"这种返工循环。每一次返工都在花费额外的 token,而且返工产生的中间消息还会进一步膨胀上下文。

上下文持久化

还有一种场景:你在对话中确定了一些重要信息——技术方案、架构决策、API 设计——这些信息在后续对话中需要反复引用。如果每次都要从对话历史里找,不仅浪费 token,还容易被压缩掉。

解决方案是把关键信息持久化到文件里

比如你让 Agent 设计一个微服务架构,讨论了半小时,确定了服务拆分方案、数据库选型、接口规范。这些决策如果只存在对话历史里,随着后续讨论越来越多,它们会被推到早期位置,可能在下一次压缩时被摘要掉。

更好的做法是让 Agent 把这些决策写进一个文件(比如DESIGN.mdARCHITECTURE.md)。后续对话中,Agent 需要参考这些决策时,读文件就行,不需要从对话历史里翻。

不用持久化: 对话历史里找决策 → 对话越长越难找 → 可能被压缩掉 → Agent"忘了"之前的决策 用持久化: 写进文件 → 需要时读文件 → 不依赖对话历史 → 压缩也不影响

表格总结

策略解决什么问题核心思路代价
auto-compact对话历史太长用 LLM 总结早期消息丢失细节
SubAgent多任务互相挤占独立上下文 + 只返回结论额外 API 调用
Skill 懒加载系统指令太多目录常驻,内容按需加载需要一次工具调用
CLAUDE.md重复配置浪费 token写一次,每次自动加载内容需要维护
文件持久化关键信息被压缩丢失写进文件,需要时读取需要主动管理文件

这五个策略本质上就做三件事:少发(Skill 懒加载、CLAUDE.md)、压缩(auto-compact)、隔离(SubAgent)。再加上持久化把关键信息从上下文里"搬"到文件里,就是一个完整的上下文管理方案。

小结

上下文管理的核心:在有限的窗口里,让模型始终看到最有价值的信息。

理解了上文中提到的这些策略,你就能理解为什么有时候 Agent 表现好、有时候表现差,不是模型变蠢了,在于它的上下文管理得好不好。当你发现 Agent 开始"忘事"或者重复劳动时,大概率是上下文膨胀了。这时候可以手动/compact一下,对于Agent开发者来说也要做好上下文管理的工作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询