上一篇拆了 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 都用这个方案。

优点:上下文长度可控,远期信息不完全丢失(保留在摘要里),近期信息精度不损失。

缺点

  1. 摘要有信息损失。 压缩过程必然丢细节。如果远期对话里有一个关键的数字(比如"预算不超过 $5000"),摘要可能会保留,也可能会丢——取决于摘要模型的判断。

  2. 摘要需要额外的模型调用。 每次做摘要要调一次轻量模型,增加延迟(200-500ms)和成本(几美分/次)。

  3. 摘要质量依赖模型能力。 用太便宜的模型做摘要,可能漏掉关键信息。

工具输出摘要的必要性:这一点很多实现忽略了。原始工具输出(尤其是 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 个结果)。

缺点

  1. 需要 Embedding 基础设施。 每条消息要做 embedding 存入向量库,每次查询要做语义检索。增加了系统复杂度。

  2. 检索准确率不是 100%。 语义检索可能会漏掉重要信息,也可能拉入不相关的内容。

  3. 丢失了对话的时间顺序。 检索出来的片段是按相关性排的,不是按时间排的。模型可能搞不清"这条信息是什么时候说的"。

  4. 冷启动问题。 前几轮对话记忆库几乎是空的,检索没用。

适用场景:长期运行的个人助手(跑了几个月,积累了大量历史对话),需要从海量历史中精准回忆特定信息。不适合短对话或新建的 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: 可以,而且效果很好。做法是:窗口+摘要处理近期上下文,语义检索处理长期记忆。近期的信息靠摘要保留时间顺序,远期的信息靠检索按相关性召回。两者互补而非替代。但实现复杂度也是最高的。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐