1. 从"第20轮失忆"说起:Agent 上下文管理的真实痛点
如果你正在做 Agent 开发,大概率遇到过这个场景:前几轮对话还挺聪明,到第十几轮、二十轮之后,它开始答非所问,忘了用户最开始说的约束条件,甚至把之前确认过的结论推翻重来。很多人第一反应是"模型不行",换个更大的模型,结果发现该忘还是忘。
问题不在模型本身,而在于你喂给它的**上下文(Context)**已经变成了一锅粥。大模型的上下文窗口是有限的,哪怕标称 1M token 的窗口,塞进去的东西越多,注意力被稀释得越厉害,关键信息被淹没在大量冗余历史里,模型自然就"失忆"了。
这里要先厘清一个概念上的分水岭:管理历史和管理上下文是两回事。
- 管理历史:把每一轮对话原封不动地存下来,按时间顺序拼进 prompt。这是大多数初学者的做法,本质上是"日志式"思维。
- 管理上下文:把历史当作原材料,经过筛选、压缩、结构化、分层之后,只把当前这一步真正需要的信息放进窗口。这是"工程式"思维。
标题里说的"高手管理的是上下文",指的就是后者。这篇文章我会把 Agent 上下文管理的几个核心机制——Context Editing(上下文编辑)、Compaction(压缩)、Memory Tool(记忆工具)——从原理到落地拆开讲,结合我在实际项目里踩过的坑,给出一套可以直接抄作业的方案。适合正在做 Agent 开发、被上下文超长折磨过的同学,也适合刚入门想搞清楚"上下文到底怎么管"的新手。
先说结论:上下文管理不是某一个 API 调用能解决的,它是一套分层策略。你得先想清楚哪些信息该留在窗口里、哪些该压缩、哪些该外置到记忆里,然后才是选工具、写代码。
2. 上下文窗口到底是怎么被"撑爆"的
2.1 一个真实的 token 消耗账本
很多人对 token 消耗没有概念,觉得"不就是几段文字吗"。我拿一个典型的 Agent 会话给你算笔账。
假设你做一个客服类 Agent,系统提示词(System Prompt)里包含角色设定、工具说明、输出格式约束,大概 1500 token。每轮用户输入平均 100 token,Agent 回复平均 300 token。如果 Agent 每轮还要调用工具,工具返回结果平均 500 token。
那么第 N 轮结束时,累积的上下文大致是:
| 轮次 | 累积 token(粗略) | 说明 |
|---|---|---|
| 第 1 轮 | 1500 + 100 + 300 + 500 = 2400 | 还很清爽 |
| 第 5 轮 | 2400 + 4 × 900 = 6000 | 开始有压力 |
| 第 10 轮 | 2400 + 9 × 900 = 10500 | 注意力开始分散 |
| 第 20 轮 | 2400 + 19 × 900 = 19500 | 关键信息基本被淹没 |
| 第 50 轮 | 2400 + 49 × 900 = 46500 | 接近很多模型的窗口上限 |
这还只是保守估计。如果工具返回的是长文档、网页内容、代码文件,单轮就能吃掉几千甚至上万 token。我见过一个做代码分析的 Agent,读一个中等规模的仓库文件,单次工具返回就 3 万 token,三轮下来窗口就满了。
注意:token 数不是线性影响效果的。窗口用到 50% 和用到 90%,模型的表现可能差出一个档次。不是"没超就行",而是"越满越糊"。
2.2 为什么"塞满"反而变笨
这里涉及一个常被忽略的机制:注意力稀释。
Transformer 的注意力机制本质上是给上下文里每个 token 分配权重。当上下文很短时,关键信息(比如用户的核心诉求)能拿到很高的权重。但当上下文里塞了几万 token 的闲聊、重复的工具返回、已经过时的中间结论时,这些噪声会分走注意力,关键信息的相对权重被压低。
打个比方:你在一个安静的房间里跟人说话,对方听得清清楚楚。但如果房间里同时有 50 个人在聊天,你说同样的话,对方就得费劲去分辨。模型也是一样,它不是"记不住",而是"分不清哪个重要"。
更麻烦的是**中间遗忘(Lost in the Middle)**现象。研究发现,模型对上下文开头和结尾的信息记得比较牢,中间部分容易被忽略。而按时间顺序拼接的历史,最重要的信息(比如用户最初的约束)往往就在开头,中间全是过程性内容,结尾是最近的对话——结果就是开头和结尾还行,中间一塌糊涂。
2.3 常见的三种错误应对
我见过太多团队在这上面走弯路,总结下来有三种典型错误:
第一种:无脑截断。窗口快满了就把最早的历史删掉。这会导致用户最开始说的约束条件丢失,Agent 开始违反最初的需求。比如用户一开始说"所有金额用人民币",聊到第 30 轮截断后,Agent 又开始用美元了。
第二种:无脑摘要。把历史全部丢给模型做摘要,然后只保留摘要。问题是摘要会丢细节,而且摘要本身也可能失真。更坑的是,摘要做多了会"摘要的摘要",信息层层衰减,最后剩下一堆正确的废话。
第三种:换大窗口模型。以为 1M 窗口就能解决一切。实际上窗口越大,注意力稀释越严重,而且成本飙升。1M 窗口的模型跑一次,费用可能是普通模型的几十倍,延迟也高得离谱。
这三种做法的共同问题是:没有区分信息的"生命周期"。有些信息是永久有效的(用户身份、核心约束),有些是阶段性的(当前任务的中间结果),有些是一次性的(某个工具的原始返回)。把它们一视同仁地处理,必然出问题。
3. Context Editing:不是删历史,是重新编排
3.1 Context Editing 的核心思路
Context Editing 这个词听起来玄乎,其实核心就一句话:在把上下文送进模型之前,动态地重新编排它。
注意关键词是"动态"和"重新编排",不是简单的增删。它包含几个动作:
- 筛选:从完整历史里挑出当前这一步真正相关的部分。
- 重排:把最重要的信息放在开头或结尾(避开中间遗忘区)。
- 替换:把冗长的原始内容替换成结构化摘要或引用。
- 注入:把外置记忆里相关的片段拉进当前上下文。
这跟传统的"历史管理"最大的区别是:历史是只读的存档,上下文是每轮重新生成的视图。同一份历史,在不同轮次可以生成完全不同的上下文。
3.2 一个可落地的分层结构
我在项目里用的是一套四层结构,你可以直接参考:
| 层级 | 内容 | 生命周期 | 处理方式 |
|---|---|---|---|
| 固定层 | 系统提示词、角色设定、工具定义 | 永久 | 每轮原样注入,放在最前 |
| 约束层 | 用户核心诉求、硬性约束、已确认结论 | 会话级 | 结构化存储,每轮注入 |
| 工作层 | 当前任务的中间结果、最近几轮对话 | 任务级 | 保留最近 N 轮,超出则压缩 |
| 归档层 | 更早的历史、工具原始返回 | 永久存档 | 外置存储,按需检索注入 |
固定层和约束层是"永远在场"的,工作层是"滚动窗口",归档层是"按需调取"。这样设计的好处是:无论会话多长,模型每轮看到的上下文都是结构清晰、重点突出的,而不是一坨越来越大的历史。
3.3 约束层怎么提取和维护
约束层是这套结构里最容易被忽略、但价值最高的部分。它的作用是:把用户在整个会话里说过的"硬性要求"抽出来,单独维护,每轮都注入。
具体怎么做?我的做法是:
- 首次提取:会话开始时,用一次轻量模型调用,从用户首轮输入里抽取约束,存成结构化 JSON。
- 增量更新:每轮对话后,判断用户是否新增或修改了约束,如果有就更新。
- 冲突检测:如果新约束和旧约束冲突,主动向用户确认,而不是默默覆盖。
举个例子,用户说"帮我写个爬虫,只要标题和链接,不要正文,输出成 CSV"。约束层就是:
{ "task": "写爬虫", "fields": ["标题", "链接"], "exclude": ["正文"], "output_format": "CSV" }之后无论聊到第几轮,这段 JSON 都会出现在上下文里。模型就不会因为中间聊了别的内容,突然又开始抓正文了。
提示:约束层不要用自然语言存,用结构化格式(JSON/YAML)。自然语言容易被模型"重新解读",结构化数据更稳定。
3.4 重排策略:把关键信息放在"黄金位置"
前面提到中间遗忘现象,所以重排很重要。我的经验是:
- 开头放:系统提示词、约束层。这是模型注意力最集中的区域之一。
- 结尾放:当前用户输入、最近一轮的工具返回。这是另一个高注意力区。
- 中间放:工作层的中间结果。这部分即使被部分忽略,影响也相对小。
如果某轮任务特别依赖某个中间结果,我会把它从中间"提"到结尾,紧挨着用户输入。这个动作看起来小,但实测对准确率提升明显。
4. Compaction:压缩不是摘要,是有损但可控的降维
4.1 Compaction 和普通摘要的区别
很多人把 Compaction 等同于"让模型总结一下历史",这是误解。普通摘要是无差别压缩,Compaction 是有策略的有损压缩。
区别在哪?普通摘要会把所有内容揉成一段话,细节全丢。Compaction 会保留结构,只压缩冗余部分。比如工具返回了一个 5000 token 的 JSON,Compaction 不是把它总结成"工具返回了一些数据",而是提取出关键字段,丢掉重复的元数据,压成 200 token 的结构化结果。
我常用的 Compaction 策略有三种:
- 结构化提取:从冗长的工具返回里抽出关键字段,丢掉包装层。
- 对话折叠:把多轮"确认-回复"折叠成一条结论。
- 引用替换:把长内容替换成一个引用 ID,需要时再展开。
4.2 触发时机:什么时候该压缩
压缩不是越早越好,也不是越晚越好。触发太早,信息还没用上就被压没了;触发太晚,窗口已经爆了。
我的做法是设双阈值:
- 软阈值(比如窗口的 60%):开始对归档层做后台压缩,不影响当前对话。
- 硬阈值(比如窗口的 85%):强制压缩工作层,把最老的部分折叠掉。
这样设计的好处是压缩动作是"渐进"的,不会在某一轮突然大改上下文导致模型行为突变。
4.3 压缩的"保真度"控制
压缩最大的风险是丢关键信息。我的经验是给压缩加一个保真度检查:
压缩完成后,用一次轻量调用判断"压缩后的内容是否还能支撑当前任务"。如果判断为"信息不足",就回退到更保守的压缩策略,或者从归档层补拉原始内容。
这个检查看起来多花了一次调用,但比起因为压缩丢信息导致任务失败重来,成本低得多。
4.4 一个压缩前后的对比实例
假设工具返回了这样一段(简化版):
{ "status": "success", "request_id": "abc-123-def-456", "timestamp": "2024-01-15T10:30:00Z", "data": { "user": {"id": 1001, "name": "张三", "email": "zhangsan@example.com", "created_at": "2023-05-01"}, "orders": [ {"order_id": "O001", "amount": 299, "status": "paid", "items": 3}, {"order_id": "O002", "amount": 158, "status": "pending", "items": 1} ] }, "meta": {"page": 1, "total": 2, "elapsed_ms": 45} }压缩后:
{ "user": "张三(1001)", "orders": [ {"id": "O001", "amt": 299, "st": "paid"}, {"id": "O002", "amt": 158, "st": "pending"} ] }从约 400 token 压到约 60 token,关键信息(用户、订单、金额、状态)全保留,丢掉的是 request_id、timestamp、email、meta 这些当前任务用不上的字段。这就是"有损但可控"。
5. Memory Tool:把"记不住"变成"查得到"
5.1 记忆工具解决的是什么问题
Context Editing 和 Compaction 解决的是"窗口内"的问题,但有些信息天然就不该常驻窗口——比如用户三个月前的偏好、上一个会话的结论、知识库里的领域知识。这些信息应该外置存储,按需检索。这就是 Memory Tool 的定位。
Memory Tool 的本质是给 Agent 配一个"外部大脑",它包含两个动作:
- 写入(Write):把值得长期保留的信息存进去。
- 检索(Retrieve):在当前任务需要时,把相关片段拉进上下文。
5.2 什么信息该写进记忆
不是所有信息都值得存。我的判断标准是三条:
- 跨会话有效:这个信息在下一次会话里还有用吗?如果只在当前会话有用,放工作层就行。
- 稳定不变:用户偏好、身份信息、长期约束,这些相对稳定。频繁变化的信息不适合长期存。
- 检索可命中:存进去的信息要能被语义检索到。如果存的时候没有好的描述,检索时也找不到。
按这个标准,适合存记忆的有:用户画像、历史结论、领域知识、常用配置。不适合的有:临时中间结果、一次性工具返回、当前会话的闲聊。
5.3 检索注入的时机和粒度
记忆检索最容易犯的错是"每次都全量拉"。这等于把外置记忆又变成了窗口负担。
我的做法是按需检索 + 粒度控制:
- 按需:只在当前任务确实需要外部信息时才检索,不是每轮都查。
- 粒度:检索返回的是片段,不是整条记忆。比如用户偏好有 20 条,只返回和当前任务相关的 3 条。
具体实现上,我会在每轮开始前做一次轻量的"意图判断",决定这轮要不要查记忆、查什么。这个判断可以用规则(关键词匹配)也可以用模型(轻量分类),看你的成本预算。
5.4 记忆的更新和遗忘
记忆不是只写不删的。我见过一些项目,记忆库越堆越大,检索越来越慢,命中率越来越低。所以记忆需要更新和遗忘机制:
- 更新:同一类信息有新版本时,覆盖旧的,而不是追加。比如用户换了偏好,旧偏好要标记失效。
- 遗忘:长期未被检索命中的记忆,降低权重或归档。可以用"最后命中时间"做淘汰依据。
- 冲突处理:检索到互相矛盾的记忆时,以时间最新的为准,并在上下文里标注冲突。
注意:记忆的写入最好也走一次"价值判断",不是什么都说"记住这个"。我见过 Agent 把用户的每句话都存进记忆,结果检索出来的全是噪声。
6. 三者怎么配合:一套完整的上下文流水线
6.1 每轮对话的完整流程
把 Context Editing、Compaction、Memory Tool 串起来,一轮对话的上下文处理流程大致是:
- 接收用户输入。
- 意图判断:这轮要不要查记忆?要不要更新约束?
- 记忆检索:如果需要,从 Memory Tool 拉相关片段。
- 约束更新:如果用户新增/修改了约束,更新约束层。
- 上下文组装:固定层 + 约束层 + 工作层 + 检索到的记忆 + 当前输入。
- 阈值检查:如果组装后超过软阈值,触发 Compaction。
- 调用模型。
- 结果处理:把值得长期保留的信息写入 Memory Tool。
- 工作层滚动:把本轮加入工作层,超出窗口的部分移入归档层。
这套流程看起来步骤多,但大部分是轻量操作,真正花 token 的只有第 7 步。实测下来,整体延迟增加在可接受范围内。
6.2 不同场景的策略侧重
不是所有场景都需要全套。我按场景给个侧重建议:
| 场景 | 侧重机制 | 原因 |
|---|---|---|
| 短会话客服 | Context Editing | 会话短,压缩和记忆收益低 |
| 长会话助手 | Compaction + 约束层 | 历史长,需要持续压缩 |
| 跨会话个人助理 | Memory Tool | 核心价值在跨会话记忆 |
| 代码分析 Agent | Compaction + 归档检索 | 工具返回巨大,必须压缩 |
| 多轮任务编排 | 全套 | 约束多、历史长、需跨任务记忆 |
6.3 成本与效果的平衡
上下文管理本身也有成本。每次压缩、每次记忆检索都要花 token 和时间。所以要做收益判断:
- 如果会话预计很短(比如 5 轮内结束),别上全套,简单截断就行。
- 如果任务对准确性要求极高(比如金融、医疗),压缩要保守,宁可多花 token。
- 如果任务对延迟敏感(比如实时对话),压缩和检索要异步做,别阻塞主流程。
我的经验是:先上约束层和基础 Compaction,这两个投入产出比最高。Memory Tool 等到确实有跨会话需求时再加,别一上来就搞复杂。
7. 踩过的坑和实测有效的技巧
7.1 坑一:压缩把"否定约束"压没了
这是我最惨的一次。用户说"不要用红色,不要用圆角",压缩时模型觉得"不要"是冗余,压成了"配色和圆角待定"。结果 Agent 后面用了红色圆角,用户直接炸了。
教训:否定性约束(不要、禁止、避免)在压缩时必须原样保留,不能改写。我后来在压缩 prompt 里明确加了规则:"所有否定性表述必须逐字保留"。
7.2 坑二:记忆检索命中了过时信息
用户半年前说"我偏好简洁风格",三个月前改成了"我喜欢详细解释"。记忆库里两条都在,检索时命中了旧的那条,Agent 又开始简洁了。
教训:记忆必须有时间戳和失效机制。同类信息新版本写入时,旧版本要标记失效或降权。检索时优先返回最新的。
7.3 坑三:约束层和实际对话冲突
约束层说"输出 JSON",但用户中途说"这次用表格给我看"。如果约束层不更新,Agent 会继续输出 JSON,用户觉得它不听人话。
教训:约束层要支持"临时覆盖"。可以给约束加一个"作用域"字段,区分"会话级"和"本轮级"。本轮级的约束优先级更高,用完即弃。
7.4 实测有效的三个技巧
技巧一:给上下文加"锚点"。在每层内容前加一个简短标签,比如[约束]、[最近对话]、[记忆]。模型对结构化标签的识别能力比纯文本强,实测能减少"张冠李戴"。
技巧二:压缩后做一次"自检"。让模型回答"压缩后的上下文是否还包含完成任务所需的所有约束",如果回答"否",就回退。这个自检成本很低,但能拦住大部分压缩事故。
技巧三:工作层保留"最近 N 轮 + 关键轮"。不是简单保留最近 N 轮,而是最近 N 轮加上被标记为"关键"的历史轮次(比如用户确认需求的轮次)。这样既控制了长度,又不丢关键节点。
7.5 关于工具选型的一点看法
市面上做上下文管理的工具和框架不少,我的建议是:别一上来就上重框架。先把约束层和基础 Compaction 用几十行代码实现出来,跑通了再考虑引入专门的记忆工具。
原因很简单:上下文管理的核心是策略,不是工具。策略想清楚了,用什么工具都能实现;策略没想清楚,上再好的工具也是白搭。我见过团队花两周接入某个记忆框架,结果发现核心问题(约束没维护好)根本没解决。
如果你确实需要现成的记忆工具,选型时重点看三点:检索是否支持语义、是否支持时间衰减、是否支持结构化字段过滤。这三点决定了记忆能不能真正用起来。
8. 写在最后的一点个人体会
做 Agent 这几年,我最大的感受是:上下文管理是 Agent 工程里最不性感、但最决定成败的部分。模型能力大家都能买到,工具调用大家都能接,真正拉开差距的,是你怎么组织喂给模型的那几千几万 token。
标题说"高手管理的是上下文",我理解这句话的深层含义是:高手把上下文当成一个需要持续经营的资源,而不是一个被动堆积的容器。每一轮都在问自己:这一步模型真正需要知道什么?哪些可以压缩?哪些该外置?哪些必须原样保留?
这套思路不依赖特定模型,也不依赖特定框架。你换成任何模型、任何框架,这套分层策略都成立。这大概就是它值得花时间的原因。
如果你现在正被"第 20 轮失忆"折磨,我的建议是从约束层开始改。先把用户的核心诉求结构化地维护起来,每轮注入,你会发现很多"失忆"问题其实不是记忆问题,而是重点没有被持续强调。这一步做完,再考虑压缩和记忆工具,路会顺很多。