Agent Harness系列(二):上下文管理的4种策略
上一篇拆了 Agent Harness 的 5 层架构。这篇聚焦第 2 层——上下文管理。这一层直接决定模型"看到什么",对输出质量的影响在 20-40% 之间。4 种主流实现策略各有适用场景,选错了代价很大。附架构对比和实现代码。
上下文管理解决什么问题
模型是无状态的——每次 API 调用它都从零开始。让它"记得"之前说过什么、项目用什么规范、工具返回了什么结果,全靠你在每次请求里塞进去的上下文。
上下文管理就是决定"每次调用模型时,发送哪些信息、按什么顺序、占多少 token"的工程。
这个工程做得好不好,直接体现在三个指标上:
| 指标 | 上下文管理差 | 上下文管理好 |
|---|---|---|
| 指令遵从度 | 经常偏离项目规范 | 严格遵循编码风格 |
| 多轮连贯性 | 第 4 轮开始答非所问 | 第 8 轮仍保持连贯 |
| 工具使用准确率 | 调错工具,传错参数 | 精确选择和调用 |
据 Anthropic 官方指南,仅仅是把文档和问题的顺序换一下(文档放前面、问题放后面),就能提升回答质量约 30%。这是上下文管理里最简单的一个优化,已经有 30% 的差距了。
4 种策略的架构对比
策略 1:全量注入(Naive Full Context)
做法:每次调用模型时,把所有历史消息、所有工具结果、所有系统提示全部发送。
def build_context_naive(system_prompt, messages, tool_results):
context = [{"role": "system", "content": system_prompt}]
context.extend(messages) # 全部历史
context.extend(tool_results) # 全部工具结果
return context
优点:实现最简单,不丢失任何信息。
缺点:上下文会无限膨胀。根据跑 Agent 的实际经验,8 轮对话后上下文通常膨胀到 15-20K token。如果中间有工具调用,可能飙到 50K+。
更严重的问题不是 token 数量,而是信噪比下降。一次 Web 搜索返回 3000 token,有用的可能就 2-3 句话。10 次工具调用后,上下文里堆了 30K token 的工具输出噪音,模型的注意力被严重稀释。
适用场景:对话轮次 ≤ 5 轮、无工具调用的简单场景。超过这个范围,质量会明显下降。
策略 2:固定窗口(Sliding Window)
做法:只保留最近 N 轮对话,更早的直接丢弃。
def build_context_window(system_prompt, messages, window_size=6):
context = [{"role": "system", "content": system_prompt}]
# 只保留最近 N 轮(每轮 = 1 user + 1 assistant)
recent = messages[-(window_size * 2):]
context.extend(recent)
return context
优点:上下文长度恒定,不会膨胀。实现简单。
缺点:会丢失关键信息。 如果用户在第 2 轮说"记住这个项目叫 Phoenix",滑过窗口后这条信息就消失了。模型会"忘记"你之前说过的重要事情。
定量影响:据多个团队的分享,固定窗口策略下,跨窗口的信息回忆率随间隔轮次指数下降:
| 信息位置 | 当前窗口内 | 刚滑出窗口(1-2 轮前) | 远离窗口(5+ 轮前) |
|---|---|---|---|
| 回忆率 | ~95% | ~40% | ~0% |
适用场景:纯聊天场景,不需要长期记忆。每轮对话相对独立,不依赖之前说过的信息。
策略 3:滚动窗口 + 摘要(Window + Summary)
做法:近期对话完整保留,远期对话压缩成摘要。兼顾精度和长度控制。
def build_context_summarized(system_prompt, messages, tool_results,
current_turn, keep_recent=3):
context = [{"role": "system", "content": system_prompt}]
# 远期对话 → 用轻量模型压缩成摘要
if current_turn > keep_recent:
old_messages = messages[1:current_turn - keep_recent]
summary = summarize_with_llm(old_messages, max_tokens=300)
context.append({
"role": "system",
"content": f"[之前 {len(old_messages)//2} 轮对话摘要] {summary}"
})
# 近期对话 → 完整保留
context.extend(messages[max(1, current_turn - keep_recent):])
# 工具输出 → 也做摘要
for result in tool_results[-3:]: # 只保留最近 3 次
context.append({
"role": "tool",
"content": summarize_tool_output(result, max_tokens=200)
})
return context
这是当前最主流的策略。 OpenClaw、Claude Code、大部分生产级 Agent 都用这个方案。
优点:上下文长度可控,远期信息不完全丢失(保留在摘要里),近期信息精度不损失。
缺点:
-
摘要有信息损失。 压缩过程必然丢细节。如果远期对话里有一个关键的数字(比如"预算不超过 $5000"),摘要可能会保留,也可能会丢——取决于摘要模型的判断。
-
摘要需要额外的模型调用。 每次做摘要要调一次轻量模型,增加延迟(200-500ms)和成本(几美分/次)。
-
摘要质量依赖模型能力。 用太便宜的模型做摘要,可能漏掉关键信息。
工具输出摘要的必要性:这一点很多实现忽略了。原始工具输出(尤其是 Web 搜索结果)的信噪比很低。据对比分析,对工具输出做摘要后,第 6 轮的回答质量可以提升约 30%——因为减少了 60% 的上下文噪音。
def summarize_tool_output(raw_output, user_question, max_tokens=200):
"""用轻量模型把工具返回值压缩到关键信息"""
resp = client.chat.completions.create(
model="qwen/qwen3.5-9b", # 用最便宜的模型做摘要
messages=[
{"role": "system", "content": "提取以下内容中与用户问题相关的关键信息,压缩到3句话以内。去掉无关的 HTML、导航、广告内容。"},
{"role": "user", "content": f"用户问题:{user_question}\n\n原始内容:{raw_output[:3000]}"}
],
max_tokens=max_tokens
)
return resp.choices[0].message.content
适用场景:大部分生产级 Agent。需要多轮对话、有工具调用、但不需要精确回忆 50 轮前的细节。
策略 4:语义检索注入(RAG-style Context)
做法:不按时间顺序保留历史,而是按语义相关性从历史中检索最相关的片段注入上下文。
def build_context_rag(system_prompt, current_message,
memory_store, top_k=5):
context = [{"role": "system", "content": system_prompt}]
# 用当前消息做 embedding,从记忆库里检索最相关的历史片段
query_embedding = embed(current_message)
relevant_memories = memory_store.search(query_embedding, top_k=top_k)
# 注入相关的历史上下文
if relevant_memories:
memory_text = "\n".join([m.content for m in relevant_memories])
context.append({
"role": "system",
"content": f"[相关历史信息]\n{memory_text}"
})
# 当前消息
context.append({"role": "user", "content": current_message})
return context
优点:不受时间顺序限制——100 轮前说过的关键信息,只要和当前问题相关就能被检索出来。上下文长度恒定(只注入 top_k 个结果)。
缺点:
-
需要 Embedding 基础设施。 每条消息要做 embedding 存入向量库,每次查询要做语义检索。增加了系统复杂度。
-
检索准确率不是 100%。 语义检索可能会漏掉重要信息,也可能拉入不相关的内容。
-
丢失了对话的时间顺序。 检索出来的片段是按相关性排的,不是按时间排的。模型可能搞不清"这条信息是什么时候说的"。
-
冷启动问题。 前几轮对话记忆库几乎是空的,检索没用。
适用场景:长期运行的个人助手(跑了几个月,积累了大量历史对话),需要从海量历史中精准回忆特定信息。不适合短对话或新建的 Agent。
4 种策略的决策矩阵
| 策略 | 实现复杂度 | 信息保留 | 上下文长度 | 额外成本 | 适用场景 |
|---|---|---|---|---|---|
| 全量注入 | ⭐ | 100% | 无限膨胀 | 无 | ≤5 轮简单对话 |
| 固定窗口 | ⭐ | 近期高/远期丢 | 恒定 | 无 | 无长期记忆需求 |
| 窗口+摘要 | ⭐⭐⭐ | 近期高/远期中 | 可控 | 低 | 大部分生产场景 |
| 语义检索 | ⭐⭐⭐⭐ | 按相关性 | 恒定 | 中 | 长期个人助手 |
我的建议:先用"窗口+摘要"跑起来。 80% 的场景它都够用。等你发现"Agent 经常忘记两周前说过的事"时,再加语义检索层——大部分 Agent 跑不到那个阶段。
一个被忽视的设计问题:注入顺序
4 种策略都在回答"放什么进上下文",但还有一个同样重要的问题:放进去的东西按什么顺序排?
据 Anthropic 官方指南:
“Queries at the end can improve response quality by up to 30%.”
模型对上下文的注意力分布不均匀:开头和结尾的内容被重视,中间的内容容易被忽视(Lost in the Middle 效应)。
最优的注入顺序:
1. 系统提示(CLAUDE.md / AGENTS.md) ← 开头,高注意力
2. 长文档 / 参考资料 ← 紧跟系统提示
3. 历史对话摘要 ← 中间,注意力较低
4. 工具调用结果(摘要后的) ← 中间
5. 最近 2-3 轮的完整对话 ← 接近结尾,注意力恢复
6. 当前用户消息 ← 结尾,最高注意力
关键原则:把最重要的信息放在开头和结尾,把可以容忍信息损失的内容放在中间。
系统提示(你的项目规范)和当前用户消息分别占据开头和结尾——这两个位置注意力最高。历史摘要放在中间——即使模型"看得不仔细",也不影响当前任务的完成。
组合方案:我在用的实现
实际生产中,我用的是窗口+摘要 + 工具输出摘要 + 注入顺序优化的组合:
def build_production_context(
system_prompt: str, # CLAUDE.md 内容
messages: list, # 完整对话历史
tool_results: list, # 工具调用结果
current_turn: int,
keep_recent: int = 3,
max_tool_results: int = 3
):
context = []
# ① 开头:系统提示(最高注意力位置)
context.append({"role": "system", "content": system_prompt})
# ② 远期摘要(中间位置,可容忍信息损失)
if current_turn > keep_recent:
old = messages[1 : current_turn - keep_recent]
summary = summarize_with_llm(old)
context.append({"role": "system",
"content": f"[对话摘要] {summary}"})
# ③ 工具输出摘要(中间位置)
recent_tools = tool_results[-max_tool_results:]
for result in recent_tools:
context.append({"role": "tool",
"content": summarize_tool_output(result)})
# ④ 近期完整对话(接近结尾,注意力恢复)
recent_messages = messages[max(1, current_turn - keep_recent):]
context.extend(recent_messages)
# ⑤ 当前消息已经在 recent_messages 的最后一条里
return context
这个方案的 token 消耗是可预测的:
系统提示: ~500-1500 token(固定)
远期摘要: ~300 token(固定上限)
工具输出摘要: ~600 token(3 × 200)
近期对话: ~2000-4000 token(变化,但有上限)
─────────────────────────────
总计: ~3400-6400 token(可控范围)
不管对话进行了多少轮,上下文总量始终在 3400-6400 token 之间。 既不浪费上下文窗口,也不会因为膨胀导致质量下降。
常见问题
Q: 摘要用什么模型做?用主模型还是便宜的?
A: 用最便宜的模型。摘要是"提取关键信息"的任务,不需要高级推理能力。Qwen 3.5 9B($0.10/MTok)或类似的轻量模型完全够用。用主模型做摘要既慢又贵,不值得。通过 API 网关可以把摘要请求路由到便宜模型,和主 Agent 的请求走不同的路由规则。
Q: keep_recent 设多少合适?
A: 3 是大部分场景的最优值。设 2 太少——当前轮的上下文不够。设 5 太多——近期对话太长。3 轮 = 最近 3 次用户提问和 3 次 Agent 回复 ≈ 2000-4000 token,是精度和长度的平衡点。
Q: 语义检索和窗口+摘要可以混合用吗?
A: 可以,而且效果很好。做法是:窗口+摘要处理近期上下文,语义检索处理长期记忆。近期的信息靠摘要保留时间顺序,远期的信息靠检索按相关性召回。两者互补而非替代。但实现复杂度也是最高的。
更多推荐


所有评论(0)