1. 上下文管理为什么成了Agent开发的分水岭
做Agent开发的人,迟早会撞上同一堵墙:模型本身够聪明,工具链也搭好了,但对话轮次一多,它就开始胡言乱语、忘记关键约束、重复调用同一个工具,甚至把早前明确否定的方案又捡回来执行。这不是模型退化了,而是上下文管理没做好。
上下文管理,说白了就是决定“每一轮请求里,到底该把哪些信息塞给模型,哪些该压缩、哪些该丢弃、哪些该外置存储”。它直接决定了Agent能不能在长任务里保持稳定。我见过太多项目,提示词写得漂亮,工具封装也干净,但一跑长流程就崩,最后定位下来全是上下文膨胀导致的注意力稀释。
这篇文章面向的是已经在用或准备用OpenCode、Codex、Claude Code这类Agent工具链的开发者,也适合正在自研Agent架构、想搞清楚上下文该怎么设计的人。我会从整体设计思路讲到具体实现细节,把参数计算、压缩策略、工具返回值的处理方式都拆开说,最后给一份常见问题速查表。全文基于我在多个Agent项目里踩过的坑整理,能直接抄作业。
2. 上下文管理的整体设计与核心思路拆解
2.1 上下文到底由哪几块拼成
很多人以为上下文就是“聊天记录”,这个理解太窄了。一个成熟Agent的上下文窗口里,通常同时塞着五类东西:
- 系统提示词:角色定义、行为约束、输出格式要求,这部分基本固定,但往往最长。
- 工具定义:每个可调用工具的schema描述,工具越多,这块占用越大。
- 历史对话:用户输入和模型回复的累积,随轮次线性增长。
- 工具调用结果:每次工具执行返回的内容,可能是几百字,也可能是几万字的文件内容。
- 外部注入信息:检索到的文档、记忆库召回、当前环境状态等。
这五块加起来,很容易在十几轮之后就撑爆窗口。关键在于,这五块的“信息密度”差异极大。系统提示词和工具定义是高频复用的,历史对话里大量是寒暄和确认,工具结果里经常混着大段无关内容。上下文管理的核心任务,就是按信息密度重新分配这块有限的预算。
2.2 为什么不能简单粗暴地截断
最直觉的做法是“超了就删最早的”。我早期也这么干过,结果非常惨:Agent把最初设定的关键约束忘了,比如“不要修改生产环境配置”,然后照改不误。
截断的问题在于,它假设信息价值随时间线性衰减,但实际不是。系统提示词里的约束永远有效,用户第一轮说的核心目标可能贯穿全程,而中间某轮的工具返回可能只是一次性参考。所以正确的思路是分层管理,而不是一刀切。
我现在的做法是把上下文分成三层:
| 层级 | 内容 | 处理策略 | 是否可压缩 |
|---|---|---|---|
| 固定层 | 系统提示词、工具定义 | 常驻,优化措辞 | 谨慎压缩 |
| 活跃层 | 最近N轮对话、当前任务状态 | 完整保留 | 不压缩 |
| 归档层 | 早期对话、历史工具结果 | 摘要或外置 | 可压缩 |
固定层要尽量精简,工具描述能短则短;活跃层是模型当前推理的直接依据,必须完整;归档层才是压缩的主战场。
2.3 压缩策略的选型逻辑
压缩不是简单删字,常见的有四种手段,各有适用场景:
- 摘要压缩:把多轮对话交给模型总结成一段。适合历史对话,但会丢失细节,且摘要本身也要花token。
- 外置存储:把工具返回的大块内容存到文件或向量库,上下文里只留引用ID和简短描述。适合文件读取、网页抓取这类场景。
- 滑动窗口:只保留最近K轮。实现简单,但会丢早期约束,必须配合固定层兜底。
- 关键信息提取:从历史里抽取实体、决策、待办,结构化存储。适合任务型Agent。
实测下来,单一策略都不够用。我通常组合使用:固定层常驻,活跃层滑动窗口,归档层做摘要加外置。这样既控制了token,又保住了关键约束。
注意:摘要压缩有个隐蔽的坑——如果摘要模型和被压缩的对话是同一个模型,它可能把错误信息也“总结”进去,导致错误被固化。建议摘要时用更保守的提示词,明确要求“只保留事实和决策,不要推断”。
3. 核心细节解析与实操要点
3.1 Token预算怎么算才不翻车
先明确一个数:不同模型的上下文窗口不一样,但可用预算永远要留出余量。我的经验是,实际使用不超过窗口的70%,剩下30%留给模型输出和突发内容。
假设窗口是128K token,那输入侧控制在90K以内比较稳。这90K怎么分配?我给一个参考比例:
- 系统提示词加工具定义:不超过15K
- 活跃对话:30K到40K
- 归档摘要:10K到15K
- 工具结果引用:10K以内
- 预留缓冲:10K
这个比例不是死的,工具特别多的项目,工具定义那块会涨,那就得从归档摘要里省。关键是每次请求前都要估算,而不是等报错了才处理。
估算token有个粗略办法:中文大约1个字1.5到2个token,英文大约1个词1.3个token,代码和JSON更密。更准的做法是用对应模型的分词器,但工程上没必要每轮都精确算,按字符数乘系数估个大概就够触发压缩逻辑了。
3.2 工具返回值是上下文膨胀的头号元凶
我统计过自己项目里的token消耗,工具返回值占了将近一半,而且大部分是浪费。比如读一个配置文件,返回几千行,模型真正需要的可能就其中十几行。
处理工具返回值,我总结了三道关:
- 工具侧裁剪:在工具实现里就限制返回量。比如读文件支持offset和limit参数,默认只返回前200行,需要更多让模型显式请求。
- 返回后过滤:拿到结果先做一次轻量处理,去掉空行、注释、重复内容,再决定是否入上下文。
- 外置加引用:超过阈值的内容直接落盘,上下文里只放“已保存到xxx,摘要如下”加一段简短摘要。
这里有个细节:外置存储的引用ID要稳定且可追溯,否则模型想回看时找不到。我一般用“文件路径加行号范围”作为引用,模型需要时可以用工具重新读取指定片段。
3.3 系统提示词的瘦身技巧
系统提示词是最容易被忽视的膨胀源。很多人写提示词像写文档,越写越长,最后光提示词就占了几万token。
瘦身有几个实用手法:
- 合并同类约束:把“不要做A”“不要做B”“不要做C”合并成“禁止以下操作:A、B、C”。
- 用示例代替描述:与其用三段话描述输出格式,不如给一个标准示例,模型模仿能力很强。
- 工具描述精简:每个工具的description只写“什么时候用”和“关键参数”,详细用法放到工具报错信息里按需返回。
- 动态加载:不是所有工具每轮都需要,可以按当前任务阶段动态挂载工具子集。
我做过对比,同样的功能,提示词从8000token压到3000token,Agent的表现反而更稳定,因为干扰信息少了。
3.4 多轮对话里的状态管理
长任务里,Agent需要记住“当前做到哪一步了”。这个状态如果全靠对话历史承载,很快就会乱。更好的做法是维护一个显式的任务状态对象,每轮更新,然后以结构化形式注入上下文。
比如一个代码修改任务,状态对象可以是:
{ "task": "重构用户模块", "current_step": "修改数据访问层", "completed": ["分析现有代码", "确定重构方案"], "pending": ["修改数据访问层", "更新单元测试", "回归验证"], "constraints": ["不改变对外接口", "保持向后兼容"] }这个对象每轮都完整注入,比让模型从几十轮对话里自己回忆靠谱得多。而且它体积小,压缩时优先保留。
4. 实操过程与核心环节实现
4.1 搭一个最小可用的上下文管理器
下面用一个Python伪代码演示核心逻辑,思路可以直接迁移到任何Agent框架。
class ContextManager: def __init__(self, max_tokens=90000, reserve=10000): self.max_tokens = max_tokens self.reserve = reserve self.fixed_layer = [] # 系统提示词、工具定义 self.active_layer = [] # 最近对话 self.archive_layer = [] # 归档摘要 self.state = {} # 任务状态对象 def estimate_tokens(self, messages): # 粗略估算,中文按1.8系数 total = 0 for m in messages: total += len(str(m)) * 1.8 return int(total) def build(self, new_message): self.active_layer.append(new_message) # 先尝试直接组装 ctx = self.fixed_layer + self.archive_layer + self.active_layer if self.estimate_tokens(ctx) < self.max_tokens - self.reserve: return ctx # 超预算,触发压缩 self.compress() return self.fixed_layer + self.archive_layer + self.active_layer def compress(self): # 把活跃层最老的一半对话摘要进归档层 half = len(self.active_layer) // 2 old = self.active_layer[:half] summary = self.summarize(old) self.archive_layer.append(summary) self.active_layer = self.active_layer[half:] # 归档层也超了就再压 if self.estimate_tokens(self.archive_layer) > 15000: self.archive_layer = [self.summarize(self.archive_layer)]这段代码的关键点在于:压缩是分级的,先压活跃层,再压归档层,固定层基本不动。summarize函数需要调用模型,提示词要明确要求保留决策、约束和待办。
4.2 摘要提示词怎么写才不丢信息
摘要质量直接决定压缩后Agent还能不能正常工作。我用的提示词模板大致是这样:
请将以下对话压缩为结构化摘要,严格保留: 1. 用户提出的所有明确要求和约束 2. 已经做出的决策及其理由 3. 当前任务进度和待办事项 4. 涉及的具体文件、函数、参数名 不要添加任何推断内容,不要省略任何约束条件。 对话内容: {content}重点是“不要推断”和“不要省略约束”。我踩过的坑是,早期摘要提示词太宽松,模型把“用户可能想要X”这种猜测也写进去,结果后续Agent把猜测当成了确定需求。
4.3 工具结果外置的落地方式
以文件读取工具为例,我的实现逻辑是:
def read_file(path, offset=0, limit=200): with open(path) as f: lines = f.readlines() total = len(lines) chunk = lines[offset:offset+limit] content = "".join(chunk) if total > limit: # 内容较大,外置存储 ref_id = save_to_store(path, offset, limit, content) return { "ref": ref_id, "summary": f"文件{path}第{offset}到{offset+limit}行,共{total}行", "preview": content[:500] } return {"content": content}这样模型拿到的是引用加预览,需要完整内容时再用另一个工具按ref读取。实测token消耗能降60%以上,而且模型对“有引用可查”这件事理解得很好,不会因为看不到全文就卡住。
4.4 参数选择与阈值设定
几个关键阈值我反复调过,给一组参考值:
| 参数 | 建议值 | 说明 |
|---|---|---|
| 窗口使用率上限 | 70% | 超过就触发压缩 |
| 活跃层保留轮数 | 8到12轮 | 太少丢上下文,太多占预算 |
| 归档摘要上限 | 15K token | 超了做二级摘要 |
| 工具结果外置阈值 | 2000字符 | 低于此值直接入上下文 |
| 单次工具返回上限 | 5000字符 | 工具侧硬限制 |
这些值不是绝对的,任务越复杂,活跃层可以适当多留;工具调用越频繁,外置阈值要调低。
提示:阈值一定要做成配置项,不同任务类型用不同配置。代码任务和客服任务对上下文的需求完全不同,一套参数打天下必然出问题。
5. 常见问题与排查技巧实录
5.1 典型问题速查表
| 现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| Agent忘记早期约束 | 归档压缩丢了约束 | 检查摘要是否保留约束 | 摘要提示词强调保留约束,或约束放固定层 |
| 重复调用同一工具 | 工具结果被压缩后模型以为没执行 | 检查工具结果是否入上下文 | 工具调用记录单独维护,不参与压缩 |
| 响应变慢、成本飙升 | 上下文膨胀 | 统计各层token占比 | 定位膨胀源,外置或裁剪 |
| 模型输出格式错乱 | 系统提示词被稀释 | 检查提示词位置和长度 | 提示词前置,精简工具定义 |
| 长任务中途跑偏 | 任务状态丢失 | 检查状态对象是否每轮注入 | 显式维护状态对象,优先保留 |
| 摘要后信息矛盾 | 摘要模型产生幻觉 | 对比摘要与原文 | 换更保守的摘要提示词,或人工校验关键摘要 |
5.2 几个容易忽视的坑
坑一:工具定义重复注入。有些框架每轮都把全部工具定义塞进去,工具一多就是灾难。正确做法是工具定义只在系统层出现一次,或者按需动态挂载。
坑二:错误信息无限累积。工具报错后,错误信息进入上下文,模型重试又报错,错误信息越堆越多。我的做法是同类错误只保留最近一条,历史错误折叠成“已尝试X次,均失败”。
坑三:摘要的摘要。归档层超限后做二级摘要,信息损失会叠加。我一般限制最多两级,再超就考虑把部分内容彻底外置,只留引用。
坑四:忽略输出预留。只算输入不算输出,结果模型刚要生成就被截断。预留至少10%给输出,长输出任务要留更多。
5.3 我的调试习惯
每次Agent行为异常,我第一件事是打印当前上下文的各层token占比,而不是急着改提示词。十次里有八次问题出在上下文结构上,而不是模型能力上。
具体做法是在每轮请求前打一条日志:
[CTX] fixed=8200 active=31000 archive=9000 tools_ref=4000 total=52200/90000这条日志能快速看出是哪一层在膨胀。如果active涨得特别快,说明对话轮次太多或单轮内容太长;如果archive一直涨,说明摘要没压住。
另外我会定期做“上下文回放”:把某次失败任务的完整上下文导出,手动删减不同部分,看模型表现如何变化。这个方法很笨,但能精准定位到底哪块信息是关键的、哪块是冗余的。
6. 上下文管理的进阶思路
6.1 按任务阶段动态调整策略
一个长任务通常分几个阶段:理解需求、制定方案、执行、验证。每个阶段对上下文的需求不同。
理解阶段需要完整的需求描述和历史讨论;执行阶段需要当前步骤的详细信息和工具结果;验证阶段需要原始需求和执行结果的对比。与其用一套固定策略,不如按阶段切换配置。
我的做法是给每个阶段定义一套上下文配置,阶段切换时重新组装上下文。这样既省token,又让模型每轮看到的都是当前最相关的信息。
6.2 记忆库与上下文的配合
上下文是短期记忆,记忆库是长期记忆。两者配合的关键是召回时机和召回量。
召回太频繁,上下文被无关记忆污染;召回太少,Agent又显得“没记性”。我的经验是:只在任务开始和关键决策点召回,每次召回不超过3条,且必须带相关性评分,低于阈值的直接丢弃。
召回内容也要压缩,不能把整篇文档塞进去。通常召回的是“结论加引用”,需要细节时再让模型主动查询。
6.3 多Agent场景下的上下文隔离
多个Agent协作时,上下文不能共享,否则互相干扰。每个Agent维护自己的上下文,Agent之间通过结构化消息通信,消息里只传必要信息,不传完整上下文。
我见过一个反例:两个Agent共享同一个对话历史,结果A的工具调用结果被B当成了自己的,行为完全乱套。正确做法是每个Agent有独立的上下文管理器,通信走明确的消息协议。
7. 一些实操后的个人体会
上下文管理这件事,本质上是在“信息完整性”和“注意力集中度”之间找平衡。给得太多,模型抓不住重点;给得太少,模型缺关键信息。没有一劳永逸的配置,只有针对具体任务不断调优的过程。
我现在做新Agent项目,第一版一定先把上下文管理框架搭好,而不是先写业务逻辑。因为业务逻辑可以慢慢加,但上下文结构一旦定型,后面改起来伤筋动骨。工具返回值的外置、任务状态对象的维护、摘要策略的分级,这三件事在项目初期就定下来,能省掉后面大量的返工。
最后分享一个我常用的自检问题:如果把这轮上下文砍掉一半,模型还能不能完成任务?如果答案是能,说明上下文里有大量冗余,该压了;如果答案是绝对不能,那要检查是不是关键信息没有被结构化保留,而是散落在对话历史里靠模型自己找。把关键信息显式化,是上下文管理最核心的一条原则。