Skip to content

上下文工程

上下文压缩与摘要

摘要、抽取、分层压缩——上下文工程的核心动作。把 100K 上下文压到 10K 还保留 90% 有效信息,是长 Agent 必备能力。

适合阶段:上下文工程进阶核心:摘要 / 抽取 / 分层 / 滚动面试重点:和 prompt-level 压缩的区别

:本文聚焦"上下文级别"的压缩——摘要、抽取、滚动等架构层手段。LLMLingua、token-level 压缩等技术细节见 Prompt 压缩

面试官想考什么

读完这篇你要能正面回答下面这些题。每题后面括号里是面试官真正想看你答出什么。

上下文压缩和 prompt compression 是同一回事吗?考概念分层,前者偏架构后者偏算法。
Map-Reduce、Refine、Stuff 三种摘要策略各自适合什么场景?考 LangChain 经典三策略。
多轮对话历史用滚动摘要 (rolling summary),关键信息怎么不丢?考工程细节,能不能讲清"分层记忆"思路。
抽取式摘要 vs 生成式摘要在 Agent 场景哪个更合适?考权衡,事实保真度 vs 信息密度。
压缩是用 LLM 还是用 embedding?两种各自的代价?考成本意识。
压缩后怎么验证信息没丢?怎么 debug?考评估方法论。
Claude Code 用什么策略管理长 session 的上下文?考实战观察,了解主流 Agent 怎么做。

为什么上下文压缩独立成一章

Prompt compression(如 LLMLingua)和上下文压缩是两个不同层次:

维度Prompt Compression上下文压缩
层次token 级(删除冗余 token)信息级(保留语义)
方法算法(perplexity、classifier)LLM 摘要、规则抽取、分层管理
适用单次长 prompt多轮 Agent / 长 session
粒度字符串变换语义重组
典型场景长文档塞 promptAgent 多轮对话历史管理

简单说:Prompt compression 是把"一坨字"压短,上下文压缩是把"对话/任务历史"提炼成精华

Agent 长 session 的真实挑战:跑 50 步任务后,前面的工具调用结果、用户反馈、中间思考过程加起来可能几十 K token。直接塞进每一步的 prompt——成本爆炸 + Lost in the Middle + KV Cache 爆显存。

上下文压缩就是 Agent 的"记忆 GC"机制——决定哪些过去要保留全文、哪些可以摘要、哪些可以扔。


三大经典压缩策略

LangChain 早期总结的三种文档摘要策略,至今仍是上下文压缩的基础范式:

策略 1:Stuff(直接塞)

把所有内容拼接,一次性扔给 LLM 摘要:

prompt = """
请总结以下所有内容:

{doc1}

{doc2}

...
"""
summary = llm.chat(prompt)
  • ✅ 简单、一次调用
  • ❌ 长度有上限(受 context window 限制)
  • ❌ Lost in the Middle 风险

适合:内容总长 < 模型上下文一半时。

策略 2:Map-Reduce

分两阶段:

  1. Map:每个文档/段落独立摘要
  2. Reduce:把所有摘要再综合成一个
python
def map_reduce_summary(docs, llm):
    # Map
    summaries = [llm.chat(f"摘要:{d}") for d in docs]
    # Reduce
    combined = "\n\n".join(summaries)
    return llm.chat(f"综合以下摘要写一个最终总结:\n{combined}")
  • ✅ 能处理任意长度(map 阶段独立)
  • ✅ map 阶段可以并行(加速)
  • ❌ 失去跨段落的关联信息
  • ❌ 调用次数多(成本)

适合:长文档摘要、多文档综合。LangChain 的 MapReduceDocumentsChain 实现。

策略 3:Refine(迭代细化)

从第一段摘要开始,每读一段就细化一次:

python
def refine_summary(docs, llm):
    summary = llm.chat(f"摘要:{docs[0]}")
    for d in docs[1:]:
        summary = llm.chat(f"""
当前摘要:{summary}
新内容:{d}
请基于新内容更新摘要:""")
    return summary
  • ✅ 保留上下文关联(每步知道之前所有内容)
  • ❌ 必须串行(不能并行)
  • ❌ 后面段落对早期段落影响弱

适合:连续性强的内容(剧情、推理过程)、需要保持"叙事一致"的场景。

三者对比

策略调用次数并行连贯性长度上限
Stuff1N/A最好受 context 限制
Map-ReduceN+1中等无限
RefineN无限

Agent 场景的滚动摘要 (Rolling Summary)

多轮 Agent 会话的典型压缩模式:

[System Prompt]
[Old conversation summary]   ← 把早期对话压缩成摘要
[Last N turns]                ← 保留最近 N 轮原文
[Current user message]

每隔几轮触发一次"压缩旧轮次":

python
def manage_chat_history(messages, llm, max_recent=10):
    if len(messages) < max_recent + 5:
        return messages   # 短对话不压

    system = messages[0]
    recent = messages[-max_recent:]
    old = messages[1:-max_recent]

    summary = llm.chat(f"""把下面的对话历史压缩成 200 字以内的摘要。
保留:用户的偏好、已确定的事实、未解决的问题。
省略:寒暄、重复的内容。

{format_messages(old)}""")

    return [
        system,
        {"role": "system", "content": f"先前对话摘要:{summary}"},
        *recent
    ]

滚动摘要的关键设计

1. 保留什么、丢什么

保留(高价值):
- 用户明确表达的偏好("我只喜欢温和的语气")
- 已确认的事实(订单号、用户身份)
- 未完成的任务(待办)
- 关键约束(用户要求过的拒绝项)

丢弃(低价值):
- 寒暄
- 已解决的问题
- 重复确认
- 模型的冗长解释

2. 分层时间策略

Tier 1: 最近 5 轮 - 完整原文
Tier 2: 5-20 轮  - 每 5 轮摘要一次
Tier 3: 20+ 轮  - 整体摘要 + 关键事实列表

不同时间段用不同压缩粒度——近的精细、远的粗略。

3. 关键事实独立存储

不要靠摘要保留所有事实——用结构化字段提取:

python
chat_state = {
    "user_facts": {
        "name": "小明",
        "language_preference": "中文",
        "verified_orders": ["ABC-1234"]
    },
    "pending_tasks": ["处理退款 XYZ-5678"],
    "constraints": ["不接受邮件提醒"],
    "recent_messages": [...]  # 最近 N 轮原文
}

每轮把 chat_state 序列化后注入 prompt,比纯摘要可靠得多。


抽取式 vs 生成式压缩

生成式:让 LLM 重写成摘要(前面所有例子)

  • ✅ 信息密度高
  • ❌ 可能改写/丢失/扭曲原文
  • ❌ 每次调用都有成本

抽取式:从原文里挑出关键句子/段落

  • ✅ 100% 保真(直接原文)
  • ✅ 不需要 LLM(可以用 embedding + 重要性打分)
  • ❌ 压缩率低(受原文表达限制)
  • ❌ 抽取出的句子可能不连贯

抽取式压缩实现

python
from sentence_transformers import SentenceTransformer
import numpy as np

embedder = SentenceTransformer("BAAI/bge-large-zh-v1.5")

def extractive_compress(text, query, ratio=0.3):
    sentences = split_sentences(text)
    sent_vecs = embedder.encode(sentences, normalize_embeddings=True)
    query_vec = embedder.encode([query], normalize_embeddings=True)

    # 按相关性给每句打分
    scores = (sent_vecs @ query_vec.T).flatten()

    # 选前 ratio 比例的句子(保持原顺序)
    n_keep = max(1, int(len(sentences) * ratio))
    top_idx = sorted(np.argsort(-scores)[:n_keep])

    return "\n".join(sentences[i] for i in top_idx)

Agent 场景的实战推荐

  • 事实型上下文(订单、用户档案、检索文档)→ 抽取式(保真重要)
  • 过程型上下文(中间思考、工具调用序列)→ 生成式(密度重要)

Claude Code 等主流 Agent 的做法

观察主流编程 Agent(Claude Code、Cursor、Cline)的上下文管理实现,有一些共同模式:

1. 工具调用结果的截断

工具调用返回 5000 行日志 → 只保留 head 100 + tail 100 + "... (4800 lines omitted) ..."

工具返回经常是巨大的——日志、目录列表、grep 结果。完整保留浪费 token,且模型经常只需要"判断成功/失败 + 关键信息"。

2. 文件内容的"按需重读"

不要把所有读过的文件持续保留在 context。Agent 应该:

  • 第一次读文件,原文进入 context
  • 经过若干步,文件原文从 context 移除
  • 需要时重新读(用 read_file 工具)

牺牲一次工具调用,换 context 不无限增长。

3. Auto-Compact / "压缩点"

Claude Code 等在 context 接近上限时触发自动压缩:

  • 让 LLM 生成"过去工作的摘要"
  • 摘要 + 系统状态 + 最近几步替换原 context

用户经常看到"compacting context"——就是这个机制。

4. 优先级标记

不同信息有不同保留优先级:

P0(永远保留): System prompt、用户最终任务、关键约束
P1(保留摘要): 中间步骤、决策点、失败和反思
P2(截断): 工具大输出、重复确认
P3(丢弃): 寒暄、过期讨论

压缩时按优先级倒序丢弃。


实战:完整的 Agent 上下文管理器

python
from dataclasses import dataclass, field
from typing import Literal

@dataclass
class ContextManager:
    system_prompt: str
    max_tokens: int = 100000
    recent_window: int = 10            # 保留最近 N 轮原文
    facts: dict = field(default_factory=dict)   # 抽取的关键事实
    history: list = field(default_factory=list)
    old_summary: str = ""

    def add_message(self, role, content):
        self.history.append({"role": role, "content": content})

        # 触发压缩
        if self._estimate_tokens() > self.max_tokens * 0.8:
            self._compact()

    def build_messages(self):
        """返回喂给 LLM 的 messages 列表"""
        msgs = [{"role": "system", "content": self.system_prompt}]

        # 注入摘要 + 关键事实
        if self.old_summary or self.facts:
            ctx = self._format_context()
            msgs.append({"role": "system", "content": ctx})

        # 最近原文
        msgs.extend(self.history[-self.recent_window:])
        return msgs

    def _compact(self):
        """压缩超出窗口的旧消息"""
        old = self.history[:-self.recent_window]
        if not old:
            return

        # 抽取关键事实
        new_facts = self._extract_facts(old)
        self.facts.update(new_facts)

        # 生成摘要(旧摘要 + 新增)
        prompt = f"""压缩成 200 字以内摘要,保留任务进展和未决问题。

旧摘要: {self.old_summary}
新内容: {self._format_messages(old)}

新摘要:"""
        self.old_summary = call_llm(prompt)

        # 截断 history
        self.history = self.history[-self.recent_window:]

    def _format_context(self):
        parts = []
        if self.old_summary:
            parts.append(f"先前对话摘要: {self.old_summary}")
        if self.facts:
            parts.append("已确认事实:")
            for k, v in self.facts.items():
                parts.append(f"  - {k}: {v}")
        return "\n".join(parts)

    def _estimate_tokens(self):
        # 粗略估算
        total = len(self.system_prompt) + len(self.old_summary)
        total += sum(len(str(self.facts)))
        total += sum(len(m["content"]) for m in self.history)
        return total // 3   # 中文大致 1 token / 1.5 字符

这个类管理:

  • System prompt(永久保留)
  • 老对话摘要(动态更新)
  • 关键事实(结构化)
  • 最近 N 轮(原文)

压缩动态触发,对外接口(add_messagebuild_messages)简单。


常见陷阱

陷阱 1:摘要把关键事实写错

摘要是 LLM 生成的,本质上有幻觉风险。"用户的订单号是 ABC-1234" 可能被压缩成 "用户的订单号是 ABC-1243"——一个字符错就翻车。

对策:关键事实(订单号、用户名、金额、时间)不要靠摘要保留,用规则抽取存到结构化字段

陷阱 2:压缩太激进丢失关键信息

为省 token 把摘要压到 50 字 → 信息密度过高 → 模型读了等于没读。

经验值:每轮对话压缩比 5-10x,最终上下文不要 < 1000 token。

陷阱 3:压缩破坏了 prompt cache

摘要是动态生成的——每次内容都不同 → prefix cache 全失效。

对策

  • System prompt 保持稳定(最前面),摘要插在中间
  • 摘要触发频率不要太高(每 5-10 轮才更新一次)
  • 用 cache key 设计让"未压缩部分"仍能命中

详见 上下文缓存

陷阱 4:不区分"事实"和"过程"

messages 里同时包含:用户事实、Agent 思考、工具结果、错误回滚——一锅炖摘要会把它们混淆。

对策:按类型独立管理:

state = {
    "facts": {...},          # 结构化事实
    "task_progress": "...",  # 任务进度的自然语言描述
    "recent_messages": [...] # 最近原文
}

陷阱 5:摘要后没法 debug

模型出错时回溯历史,发现原文已经被压缩——无法判断错在哪一步。

对策:所有原始消息保留到独立 log(不进入 context,但可查询),便于 debug。压缩后的 context 是"模型看到的",原始 log 是"真实发生的"——分开存。

陷阱 6:压缩频率不对

  • 太频繁(每轮都压)→ 调用次数爆炸、性能下降
  • 太稀疏(context 满了才压)→ 中间已经触发 Lost in the Middle 或被截断

经验:当 context 占用达 70-80% max 时触发压缩。


面试题深度解析

Q: Map-Reduce vs Refine vs Stuff 摘要策略怎么选?

30 秒版本:(1) Stuff:所有内容拼一起一次调 LLM,简单但受 context 长度限制,适合短到中等长度;(2) Map-Reduce:每段独立摘要再综合,能处理任意长度且 map 阶段可并行,但丢失跨段落关联,适合多文档独立内容综合;(3) Refine:从第一段开始迭代细化,保留连贯性,但必须串行且后面影响前面弱,适合叙事性内容(剧情、推理过程)。实战推荐:长文档 + 独立性强 → Map-Reduce;连续叙事 → Refine;短内容 → Stuff。

追问:那对话历史压缩用哪种? 对话历史一般用滚动摘要 + Refine 思路——旧对话的摘要随新对话不断更新(refine),不是每次从零摘要。具体说:new_summary = LLM("旧摘要 + 新增对话 → 更新后的摘要")。这种增量更新比每次 Map-Reduce 重新摘要省成本,且保持对话连贯性。

Q: Claude Code 这类长 session Agent 怎么管理上下文?

30 秒版本:观察到几个共同模式:(1) 工具调用结果截断——大输出(日志、文件内容)只保留 head+tail;(2) 文件按需重读——读过的文件不持续占 context,需要时重新调 read_file 工具;(3) Auto-Compact——context 接近上限时触发自动摘要,用 "过去工作的摘要 + 最近 N 步" 替换原 context;(4) 优先级丢弃——系统 prompt 和用户最终任务永远保留,中间过程优先级低被先丢。Claude Code 用户经常看到 "Compacting conversation..." 就是这个机制在跑。

追问:那压缩后模型怎么知道之前做了什么? 靠"工作摘要" + "结构化状态"。摘要是自然语言("已读取 3 个文件、修改了 auth.py 的 login 函数、运行 tests 发现一个失败"),状态是结构化(已读文件列表、已修改文件列表、待办事项)。两者结合让模型有"我现在在哪一步、之前做了什么"的认知。这比"原文滚动"省 token 多得多,且关键信息不丢。

Q: 抽取式 vs 生成式压缩,Agent 场景怎么选?

30 秒版本:分内容类型:(1) 事实型上下文(订单号、用户档案、检索文档)→ 抽取式,保真度优先,不能让摘要篡改数字 / ID / 关键事实;(2) 过程型上下文(中间思考、工具调用序列、错误反思)→ 生成式,信息密度优先,原文太冗余。生产 Agent 通常两者混用:关键事实独立存为结构化字段(抽取式)+ 过程内容用生成式摘要 + 最近几步保留原文。

追问:那抽取式压缩用什么算法? 两种主流:(1) 基于 embedding 的句子重要性打分——把每句嵌入,按与"任务目标"的相似度排序,挑前 K 个;(2) 基于规则的实体抽取——用 NER 或 regex 提取订单号、日期、金额等结构化信息存为字段。生产里两者结合:embedding 抽出相关段落 + 规则抽出关键实体存档。

Q: 怎么验证压缩后信息没丢?

30 秒版本:四种方法:(1) 对照评估——准备"理想回答"评估集,对比压缩前后下游任务的准确率;(2) 关键事实回溯——压缩后让 LLM 回答"用户的订单号是什么"等关键事实,看能否准确回答;(3) 原文 vs 摘要对比——人工抽样检查摘要是否准确反映原文;(4) 生产监控——压缩后的会话有没有出现"忘了用户身份"、"重复问已答过的问题"这类症状。关键原则:任何压缩策略上线前必须有评估集 baseline,任何参数调整后必须重测——压缩是高风险优化,不能凭感觉。

追问:那 LLM-as-judge 能不能自动评估摘要质量? 能做粗筛但不能完全替代人工。LLM judge 评摘要的常见问题:(1) 风格偏好——judge 偏爱长摘要 / 结构化输出;(2) 事实校验弱——judge 自己也是 LLM,可能漏掉事实错误;(3) 不一致——同一摘要不同 judge 给分差距大。生产做法:LLM-as-judge 做 80% 自动评估 + 关键 case 人工抽查 10% + 真实用户反馈兜底 10%。


延伸阅读

  • LangChain 文档:Document Chains (python.langchain.com/docs/tutorials/summarization) Stuff / Map-Reduce / Refine 三策略的官方实现。读源码能掌握每种策略的细节。

  • 博客:Claude Code Internals (Anthropic 的工程博客) 搜 "Claude Code agentic context management",能看到他们对长 session 上下文管理的设计思路。

  • 论文:MemGPT (arxiv 2310.08560) Packer et al. 2023。把 OS 的虚拟内存思想引入 LLM 上下文管理——"主存"(in-context)+ "外存"(外部存储)+ 自动 page swap。读它会建立"context 是稀缺资源、需要主动管理"的直觉。

  • 论文:RAPTOR (arxiv 2401.18059) Sarthi et al. 2024。分层摘要的检索增强思路——把长文档递归摘要成树,按需检索不同层级。是分层压缩的代表方法。

  • 配套阅读Prompt 压缩 — token 级压缩算法(LLMLingua)。记忆系统 — 上下文的"外存",长期记忆设计。会话历史管理 — 多轮对话的实操细节。

基于 MIT 协议开源