1. 上下文管理为什么成了Agent开发的分水岭
1.1 从一个真实困境说起
但凡用过OpenCode、Codex或者Claude Code这类终端Agent工具超过一周的人,大概率都遇到过同一个问题:对话轮次一多,模型开始"失忆"。前面刚说过的文件路径,后面就记混了;明明已经确认过的接口定义,改到第三轮又变回原来的样子。这不是模型变笨了,而是上下文窗口被塞满了。
上下文管理,说白了就是解决"Agent在有限记忆容量下如何不丢关键信息"这件事。它听起来像是个纯工程问题,但实际做下来你会发现,它同时牵扯到Token预算分配、信息压缩策略、状态持久化、检索召回四个层面。任何一个环节没处理好,Agent的表现就会断崖式下跌。
我自己的体感是:一个Agent项目能不能从Demo走向可用,上下文管理是那条最明显的分水岭。Demo阶段对话三五轮就结束,根本暴露不出问题;一旦接入真实工作流,动辄几十轮交互、多个文件来回修改,上下文管理没做好的Agent基本就废了。
1.2 上下文窗口到底在装什么
很多人对上下文窗口的理解停留在"对话历史"这个层面,实际上一个成熟的Agent在单次请求里塞进去的东西远比想象中复杂。以Claude Code这类工具为例,一次请求的上下文通常包含以下几类内容:
- 系统提示词:定义Agent的角色、行为边界、工具使用规范,这部分通常固定且不可压缩
- 工具定义:每个可用工具的schema描述,工具越多这部分占用越大
- 对话历史:用户输入和模型回复的完整记录
- 文件内容:被读取或引用的代码文件、配置文件
- 工具调用结果:命令执行输出、搜索结果、报错信息
- 记忆/摘要:之前轮次压缩后的关键信息
这六类内容共享同一个Token预算。问题在于,文件内容和工具调用结果往往是不可预测的——你读一个文件可能200行,也可能2000行;跑一条命令可能返回三行,也可能刷屏几百行。这种不确定性正是上下文管理最棘手的地方。
1.3 不同工具的上下文策略差异
OpenCode、Codex、Claude Code这几个工具在上下文管理上的思路其实有明显差异,理解这些差异对选型和二次开发都很关键。
| 工具 | 上下文策略特点 | 适合场景 |
|---|---|---|
| Claude Code | 自动压缩+手动compact,对长对话有内置摘要机制 | 长周期重构、多文件联动修改 |
| Codex | 偏向单次任务,上下文窗口利用相对保守 | 短平快的代码生成与修复 |
| OpenCode | 开源可定制,上下文策略可自行调整 | 需要深度定制Agent行为的场景 |
Claude Code的自动压缩机制是我用过最省心的,它在接近窗口上限时会自动把早期对话摘要化,保留关键决策和文件状态。但这个机制也有代价——摘要过程本身要消耗Token,而且摘要质量不稳定,有时候会把一些细节丢掉。Codex则更倾向于让你手动控制,每次任务尽量独立,减少上下文污染。OpenCode因为开源,你可以自己决定什么时候压缩、压缩到什么程度,灵活度最高但也最费心思。
提示:不要迷信任何工具的自动上下文管理。自动摘要能兜底,但关键信息该手动固定就得手动固定,比如把核心接口定义写进一个专门的记忆文件。
2. 上下文管理的四个核心机制拆解
2.1 Token预算分配:先算账再动手
做上下文管理的第一步不是写代码,而是算账。你得先搞清楚自己的Token预算怎么分。假设你用的是某个上下文窗口为200K Token的模型,实际可用预算通常要打个八折,因为输出也要占额度。
我的经验分配比例大致是这样的:
- 系统提示词+工具定义:固定占用,通常5K-15K,工具多的话可能到20K
- 对话历史:动态,建议控制在总预算的30%以内
- 文件内容:动态,建议不超过40%
- 工具调用结果:动态,建议不超过20%
- 预留缓冲:至少留10%应对突发
这个比例不是死的,但核心原则是任何一类内容都不能无限膨胀。我见过最典型的翻车场景是:Agent读了一个巨大的日志文件,直接把上下文撑爆,导致后续所有对话都被截断,模型完全不知道自己在干什么。
具体怎么控制?最直接的办法是在读取文件或执行命令前做预判。比如读文件前先看文件大小,超过一定行数就只读关键片段;执行命令时加上输出限制,比如管道接head或tail。这些操作看起来琐碎,但能极大延长Agent的可用轮次。
2.2 信息压缩:摘要不是万能药
信息压缩是上下文管理的核心手段,但很多人对它的理解过于简单,以为"把老对话总结一下"就完事了。实际上压缩策略要分层次:
第一层是对话摘要。把早期多轮对话压缩成一段关键信息摘要。这里的关键是摘要要保留什么——我的做法是保留三类信息:已确认的决策、当前任务状态、待办事项。至于中间的探索过程、试错记录,全部丢弃。
第二层是文件状态快照。不要保留文件的完整内容,而是保留"文件当前状态+关键改动点"。比如一个配置文件改了三个参数,你只需要记录这三个参数的新值和位置,不需要把整个文件塞进上下文。
第三层是工具结果提炼。命令执行结果往往包含大量噪音,比如编译输出几百行只有几行是错误。这时候要做的是提取关键行,而不是原样保留。
# 一个简单的工具结果提炼示例 def compress_tool_output(output, max_lines=50): lines = output.split('\n') if len(lines) <= max_lines: return output # 优先保留包含错误关键词的行 keywords = ['error', 'failed', 'warning', 'exception'] important = [l for l in lines if any(k in l.lower() for k in keywords)] # 保留头部和尾部 head = lines[:10] tail = lines[-10:] result = head + ['... (中间省略) ...'] + important[:20] + tail return '\n'.join(result)这段代码逻辑很简单,但效果立竿见影。实测下来,一个动辄几百行的构建输出,压缩后通常能控制在50行以内,关键信息一个不丢。
2.3 状态持久化:让Agent有"长期记忆"
上下文窗口是短期记忆,一旦超出就丢失。要解决这个问题,必须引入外部持久化存储。这也是Agent和普通聊天机器人的本质区别——Agent需要跨会话记住项目状态。
持久化的形式可以很简单,一个Markdown文件就够了。我通常会在项目根目录建一个.agent-memory.md,里面记录:
- 项目基本信息(技术栈、目录结构约定)
- 已确认的关键决策
- 当前进行中的任务和进度
- 已知的坑和注意事项
每次Agent启动时,先读这个文件;每次会话结束前,把新的关键信息追加进去。这样即使上下文完全清空,Agent也能快速恢复状态。
注意:持久化文件本身也要控制大小。我建议单个记忆文件不超过500行,超了就拆分或归档。否则每次读取都要消耗大量Token,得不偿失。
2.4 检索召回:按需加载而非全量塞入
当项目规模变大,把所有信息都塞进上下文既不现实也不经济。这时候就需要检索召回机制——根据当前任务动态加载相关上下文。
最简单的实现是基于文件路径的检索:Agent当前在处理哪个模块,就只加载那个模块相关的文件。进阶一点可以用向量检索,把代码和文档切片后建索引,根据语义相似度召回。
不过我要泼一盆冷水:向量检索在代码场景下的效果远没有宣传的那么好。代码的语义相似度和功能相关性经常不一致,检索出来的片段可能看起来相关但实际没用。我的建议是,代码场景优先用结构化检索(按文件、按函数、按依赖关系),向量检索作为补充。
3. 实操:从零搭建一套上下文管理方案
3.1 环境准备与基础配置
假设你用的是OpenCode这类可定制的工具,搭建上下文管理方案的第一步是搞清楚它的配置文件结构。通常配置文件会定义模型参数、工具列表、系统提示词等。你需要重点关注两个地方:上下文窗口大小和压缩触发阈值。
以常见的配置为例,关键参数大概长这样:
{ "model": { "context_window": 200000, "max_output_tokens": 8000, "compression_threshold": 0.75 }, "context": { "max_file_lines": 500, "max_tool_output_lines": 100, "history_keep_rounds": 10 } }这里的compression_threshold是压缩触发阈值,0.75表示上下文用到75%时开始压缩。这个值不能设太高,否则压缩来不及;也不能太低,否则频繁压缩浪费Token。我的经验是0.7到0.8之间比较合适。
history_keep_rounds是保留最近多少轮完整对话,超出的部分才进入压缩流程。这个值设太小会导致Agent"记性差",设太大又浪费预算。10轮左右是个比较平衡的值。
3.2 核心模块实现:压缩器与记忆管理器
上下文管理的核心代码通常分两个模块:压缩器和记忆管理器。压缩器负责把超出的上下文压缩,记忆管理器负责持久化关键信息。
压缩器的实现思路是分类型处理:
class ContextCompressor: def __init__(self, model_client, threshold=0.75): self.model = model_client self.threshold = threshold def should_compress(self, current_tokens, max_tokens): return current_tokens / max_tokens > self.threshold def compress_history(self, history): # 保留最近N轮,压缩更早的 recent = history[-10:] older = history[:-10] if not older: return history summary = self._summarize(older) return [{"role": "system", "content": f"历史摘要:{summary}"}] + recent def _summarize(self, messages): prompt = "请将以下对话压缩为关键信息摘要,保留:已确认的决策、当前任务状态、待办事项。丢弃探索过程和试错记录。\n\n" text = "\n".join([f"{m['role']}: {m['content']}" for m in messages]) return self.model.generate(prompt + text)记忆管理器的实现更简单,核心就是读写一个Markdown文件:
class MemoryManager: def __init__(self, memory_file=".agent-memory.md"): self.memory_file = memory_file def load(self): if os.path.exists(self.memory_file): with open(self.memory_file, 'r') as f: return f.read() return "" def append(self, section, content): with open(self.memory_file, 'a') as f: f.write(f"\n## {section}\n{content}\n") def update_section(self, section, content): # 读取全文,替换对应section,再写回 pass这两个模块加起来不到100行代码,但能解决80%的上下文管理问题。剩下的20%是调优和边界情况处理。
3.3 参数调优:几个关键数字的来龙去脉
参数调优这块我想展开讲讲,因为很多人直接抄配置但不知道数字怎么来的。
压缩阈值0.75怎么来的?假设上下文窗口200K,输出预留8K,实际可用192K。压缩本身要调用模型,一次压缩大概消耗5K-10K Token。如果阈值设0.9,触发压缩时只剩19K空间,压缩请求可能都塞不进去。0.75意味着触发时还有48K空间,足够完成压缩操作并留有余地。
文件读取上限500行怎么来的?一行代码平均10-15个Token,500行大约5K-7.5K Token。一个任务通常涉及3-5个文件,加起来25K-37K,占总预算的15%左右,比较合理。如果设成2000行,一个文件就吃掉30K,多读几个文件上下文就满了。
工具输出上限100行怎么来的?工具输出通常信息密度低,100行里真正有用的可能就10行。设100行是给提炼留空间,提炼后实际保留的可能就20-30行。
这些数字不是绝对的,但背后的计算逻辑是通用的:先算总预算,再按比例分配给各类内容,最后反推具体限制。
3.4 实操现场:一次多文件重构的完整上下文流转
光讲理论太干,我拿一次实际的多文件重构来演示上下文是怎么流转的。
任务背景:把一个项目的配置加载逻辑从硬编码改成配置文件驱动,涉及5个文件。
第一轮:Agent读取主入口文件,识别出配置相关的代码位置。此时上下文占用约15K。
第二轮到第五轮:逐个读取涉及的5个文件,每读一个就分析改动点。到第五轮时上下文占用约60K,其中文件内容占了大头。
第六轮:开始实际修改。每改一个文件,就把该文件的完整内容从上下文中移除,只保留改动摘要。这一步很关键——改完的文件不需要再保留全文,因为改动已经落盘了。
第七轮:运行测试,输出报错。工具输出被压缩到30行以内,只保留错误堆栈的关键部分。
第八轮:根据报错修复,再次运行测试通过。此时上下文占用回落到40K左右。
第九轮:更新记忆文件,记录本次重构的关键决策和改动点。会话结束。
整个流程下来,上下文峰值控制在60K左右,远低于200K上限。如果没有做文件内容的及时清理,峰值可能到150K以上,接近危险线。
4. 常见问题与排查技巧实录
4.1 上下文管理高频问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方法 |
|---|---|---|---|
| Agent突然"失忆",忘记之前确认的信息 | 上下文被截断或压缩过度 | 检查Token占用和压缩日志 | 调低压缩阈值,关键信息写入记忆文件 |
| 响应变慢,每轮耗时明显增加 | 上下文过大,模型处理时间长 | 查看每轮Token消耗 | 及时清理已完成的文件内容 |
| 压缩后信息丢失严重 | 摘要提示词不够精确 | 检查摘要输出质量 | 优化摘要提示词,明确保留项 |
| 工具输出刷屏导致上下文爆掉 | 未限制工具输出长度 | 检查工具调用配置 | 加输出行数限制,做结果提炼 |
| 跨会话状态丢失 | 未做持久化 | 检查记忆文件是否存在 | 建立记忆文件读写机制 |
| 压缩频繁触发浪费Token | 阈值设太低 | 查看压缩触发频率 | 适当提高阈值到0.75-0.8 |
4.2 几个我踩过的坑
坑一:过度依赖自动压缩。早期我完全信任Claude Code的自动压缩,结果有一次它把"不要修改某个核心文件"这个约束给摘要没了,导致Agent把不该动的文件改了。从那以后,所有硬约束我都写进记忆文件,不依赖摘要。
坑二:记忆文件无限增长。有段时间我的记忆文件涨到了2000多行,每次读取都消耗大量Token,反而成了负担。后来改成定期归档,只保留最近一个月的活跃记忆,历史记忆移到单独文件按需加载。
坑三:工具输出没做提炼。有一次跑测试,输出了几百行日志,直接把上下文撑到接近上限。后面几轮Agent完全不在状态。加了输出提炼后,同样场景上下文占用降了80%。
坑四:压缩提示词太笼统。一开始我的摘要提示词就一句"总结一下",结果摘要出来的东西又长又没用。后来改成明确列出要保留的类别,摘要质量明显提升。
提示:压缩提示词的质量直接决定上下文管理的效果。建议花时间打磨这个提示词,把它当成一个独立的小项目来对待。
4.3 进阶技巧:分层记忆与按需加载
当项目复杂到一定程度,单一记忆文件不够用了,就需要分层记忆。我的做法是分三层:
- 全局记忆:项目级信息,所有会话都加载,控制在200行以内
- 模块记忆:按模块划分,处理哪个模块加载哪个,每个100行以内
- 临时记忆:当前任务的细节,会话结束即丢弃
按需加载的关键是判断当前任务涉及哪些模块。最简单的判断依据是文件路径——Agent在操作哪个目录下的文件,就加载对应模块的记忆。这个逻辑实现起来不难,但效果很好,能把每次加载的记忆量控制在合理范围。
另外一个小技巧是记忆文件的格式要结构化。不要写成大段散文,而是用固定的小标题和列表。这样Agent读取时更容易定位关键信息,压缩时也更容易保留重点。
5. 不同Agent工具的上下文管理适配
5.1 Claude Code的上下文管理实践
Claude Code在上下文管理上做得最完善,内置了自动压缩和手动compact两套机制。我的使用习惯是:日常对话靠自动压缩兜底,但在关键节点手动触发compact,确保压缩质量。
具体操作上,我会在完成一个阶段性任务后主动compact一次,把这一阶段的成果固化下来。这样下一阶段开始时上下文是干净的,不会被上一阶段的细节干扰。
另外Claude Code支持通过CLAUDE.md文件注入项目级上下文,这个文件相当于全局记忆。我会把项目约定、常用命令、注意事项写进去,每次会话自动加载。
5.2 Codex的上下文策略调整
Codex的上下文管理相对保守,更适合短任务。用Codex时我的策略是任务拆分——把大任务拆成多个独立小任务,每个任务上下文尽量干净。这样虽然会话数多了,但每个会话的质量更稳定。
Codex的配置文件里可以调整上下文相关参数,但可调项比Claude Code少。实际使用中,我更依赖任务拆分而不是参数调优来控制上下文。
5.3 OpenCode的定制化上下文方案
OpenCode因为开源,上下文管理可以深度定制。我在这上面花的时间最多,也踩坑最多。定制化的核心是压缩策略可插拔——不同任务用不同的压缩策略。
比如代码生成任务,压缩时重点保留接口定义和类型信息;文档写作任务,压缩时重点保留大纲和关键论点。这种按任务类型定制压缩策略的做法,效果比通用策略好很多。
OpenCode的另一个优势是可以自己控制压缩时机。我实现了一个基于Token增速的触发机制——当Token增长速度超过阈值时提前压缩,而不是等到接近上限才压缩。这样压缩更从容,质量也更高。
5.4 跨工具的记忆文件兼容
如果你像我一样同时用多个工具,记忆文件的兼容性就是个问题。我的做法是用纯Markdown格式,不依赖任何工具特有的语法。这样Claude Code、Codex、OpenCode都能读写同一个记忆文件。
具体约定是:用二级标题分section,用列表记录要点,用引用块标注重要提示。这套格式所有工具都能解析,迁移成本最低。
注意:不同工具对记忆文件的读取时机不同。有的启动时读一次,有的每轮都读。如果你的记忆文件较大,要注意读取频率带来的Token消耗。
6. 上下文管理的效果评估与持续优化
6.1 怎么判断上下文管理做得好不好
评估上下文管理效果,我主要看三个指标:
任务完成率:同样一批任务,上下文管理优化前后完成率的变化。这个最直接,但需要足够多的样本才有统计意义。
平均轮次:完成同类任务平均需要多少轮对话。轮次越少说明上下文利用效率越高,Agent越"聪明"。
Token效率:完成任务消耗的总Token数。这个指标反映的是经济性,同样的任务消耗Token越少越好。
我自己的实测数据是:优化上下文管理后,多文件重构类任务的平均轮次从18轮降到11轮,Token消耗降低约40%。这个提升幅度相当可观。
6.2 持续优化的几个方向
上下文管理不是一劳永逸的事,需要持续优化。我目前主要在探索这几个方向:
动态预算分配:根据任务类型动态调整各类内容的预算比例。比如代码任务给文件内容更多预算,文档任务给对话历史更多预算。
压缩质量评估:建立一个压缩质量的自动评估机制,压缩后检查关键信息是否保留,不达标就重新压缩。
记忆自动整理:定期自动整理记忆文件,合并重复内容,归档过期信息,保持记忆文件精简。
跨会话状态同步:多个会话并行时,如何同步状态避免冲突。这个在多Agent协作场景下尤其重要。
这些方向都还在探索阶段,没有成熟方案。但我觉得上下文管理的天花板远不止现在这样,随着Agent承担的任务越来越复杂,上下文管理的重要性只会越来越高。
最后分享一个我最近在用的技巧:给记忆文件加时间戳和版本号。每次更新记忆时记录时间和版本,这样出问题时可以回溯到之前的版本,也能清楚看到信息是什么时候加入的。这个习惯帮我排查了好几次"Agent为什么突然改变行为"的问题,强烈推荐你也试试。