上下文管理是 Harness 的第一根缰绳。原因很简单:LLM 的上下文窗口是有限的——128K、200K 甚至 1M Token,听上去很大,但一轮多工具调用的 Agent 对话里,几千字的系统 Prompt + 不断累积的工具返回结果 + 历史对话,会以你想象不到的速度填满这个窗口。
更重要的是,填满窗口不是最糟糕的事。最糟糕的是重要信息被淹没在无关内容中——模型在长上下文中的注意力分布并不均匀,放在开头和结尾的内容被关注的概率远高于中间(Lost in the Middle 问题)。你塞进去的东西越多,模型真正用到的比例反而越低。
先看清现实:模型的上下文窗口不是"大"的问题,而是"管理"的问题。
| 模型 | 窗口大小 | Agent 场景下的实际可用量 |
|---|---|---|
| GPT-4o | 128K | ~30K(越界后性能下降) |
| Claude Sonnet 4 | 200K | ~60K(Lost in the Middle) |
| DeepSeek-V4 | 128K | ~40K(同) |
| Gemini 1.5 Pro | 1M | ~200K(同,但范围更大) |
注意"实际可用量"这个列——模型声称的窗口大小和它能有效利用的大小是两回事。大量研究表明,超过一定阈值后,增加上下文长度并不会线性提升任务表现,甚至可能因干扰而下降。
一个典型的 Agent 对话中,上下文由以下几部分组成:
Agent 上下文构成(按体积排序):
1. 系统 Prompt(固定部分) ≈ 2-5K Token
└─ 角色定义、行为规则、输出格式
2. 工具描述(工具数量 × 描述长度) ≈ 1-10K Token
└─ 每个工具的 schema、用途、参数说明
└─ 每多一个工具,多约 200-500 Token
3. 对话历史(轮数 × 每轮长度) ≈ 10-200K Token
└─ 用户输入 + Agent 回复 + 工具调用
└─ 每一轮 Agent 思考 + 工具返回 = 2-10K Token
4. RAG 检索结果 ≈ 2-20K Token
└─ 每次检索注入的相关文档段落
5. 记忆/知识上下文 ≈ 1-5K Token
└─ 跨会话持久化的用户偏好、项目信息 这里最危险的是"对话历史"——它在 Agent 的多轮交互中以最快的速度增长。一个执行了 5 轮工具调用的 Agent 对话,仅仅工具返回的结果就可能消耗 10K 以上 Token。
既然窗口有限,压缩就是必然选择。以下是实测有效的策略:
最简单的策略:只保留最近 N 轮对话,丢弃最早的。问题在于"只保留最近"可能会丢失关键的上下文——比如用户在 10 轮前设置的筛选条件。
改进版:保留系统 Prompt + 最近的 N 轮 + 标记为"关键"的历史轮次(比如用户设置了某个参数、确认了某个决策)。
用一个轻量模型定期对历史对话做压缩摘要:
// 对话历史压缩策略
function compressHistory(messages, threshold = 20K):
if totalTokens(messages) < threshold:
return messages // 不需要压缩
// 保留最近的 M 轮完整消息
recent = messages.slice(-5)
// 对更早的消息做摘要
early = messages.slice(0, -5)
summary = callLLM("请用一段话总结以上对话的核心信息")
// 用摘要替代早期消息
return [summary, ...recent] 实测效果:一轮压缩可以将 40K Token 的历史压缩到 5K 以内,关键信息保留率约 90%。
不是所有历史都值得保存。Agent 可以主动标记哪些信息是"当前不需要的"并丢弃:
系统 Prompt 和工具描述在每一轮对话中是固定的。利用 LLM 的 Prompt Caching 机制,这部分不会重复消耗 Token 预算,只有新增的对话内容计入。这个优化在大部分主流 API 中已自动支持。
上下文管理的更高阶形态是记忆分层——不是简单的一刀切压缩,而是让不同类型的记忆有不同的生命周期和管理策略。
记忆分层模型:
┌────────────────────────────────────────────┐
│ L1:工作记忆(当前对话) │
│ ├─ 用户当前输入 │
│ ├─ 最近 5-10 轮对话 │
│ └─ 当前正在执行的工具调用结果 │
│ 特征:高精度、短生命周期、全部保留 │
├────────────────────────────────────────────┤
│ L2:情景记忆(一次会话) │
│ ├─ 压缩后的历史对话摘要 │
│ ├─ 用户在本轮对话中设置的关键信息 │
│ └─ 已完成步骤的结果摘要 │
│ 特征:有损压缩、中等生命周期 │
├────────────────────────────────────────────┤
│ L3:语义记忆(跨会话) │
│ ├─ 用户偏好和常规设置 │
│ ├─ 项目相关的持久化知识 │
│ └─ 从经验中提取的可复用模式 │
│ 特征:高度抽象、长生命周期、固化存储 │
└────────────────────────────────────────────┘ 三个层级的数据来源不同、用途不同、管理策略也不同:
基于以上原理,一个生产级 Agent 的上下文管理流程大致如下:
每一轮对话的处理流程:
用户输入到达
↓
Step 1: 注入 L3 语义记忆(从持久化存储检索)
↓
Step 2: 注入 L2 情景记忆(上一轮的压缩摘要)
↓
Step 3: 注入 L1 工作记忆(最近的 N 轮完整对话)
↓
Step 4: 注入本轮用户输入
↓
Step 5: 检查总 Token 数是否超过阈值
├─ 未超过 → 直接调用 LLM
└─ 超过 → 触发压缩:
├─ 丢弃非关键的工具返回
├─ 压缩早期对话历史
└─ 如仍超过 → 丢弃最旧的非关键轮次
↓
LLM 生成回复 + 工具调用
↓
Step 6: 将本轮结果追加到 L1 工作记忆
Step 7: 将关键信息同步到 L2/L3 记忆层 上下文管理的三条原则:
1. 窗口再大也是有限的
不要被 200K Token 的规格迷惑
模型真正能有效利用的远小于标称值
2. 少即是多
上下文质量 > 上下文数量
放对的东西比放多的东西重要得多
3. 分层管理
工作记忆 → 情景记忆 → 语义记忆
不同生命周期用不同策略
不要把所有记忆塞进同一个桶里 上下文管理是 Agent 工程中最容易被低估的基础能力。很多人把精力花在选模型、调 Prompt 上,却忽略了每一轮对话中模型实际"看到"的是什么。一个好的上下文管理策略,可以在不换模型、不加预算的前提下,显著提升 Agent 的表现一致性——这就是 Harness 第一根缰绳的威力。
💬 评论