上下文压缩:任务摘要 - 文件摘要 - 过程笔记

一句话:上下文压缩不是在上下文窗口里「减少字数」,而是用结构化摘要把冗余信息提炼成可检索、可回溯、不丢失关键信息的压缩态——做完一道菜,不是扔掉菜谱,而是把关键步骤抄到自己的小本本上。


一、为什么需要压缩?以及为什么不能随便压?

压缩的目标不是「删掉更多内容」,而是提高信息的信噪比。同样 1000 token 的空间,原始对话可能只有 10% 的关键信息,而一段好的摘要可以达到 80%。

graph TD
    A["10000 token 原始对话"] --> B["压缩"]
    B --> C["800 token 结构化摘要"]

    A2["10000 token 原始对话"] --> D["截断(不压缩)"]
    D --> E["丢弃 9000 token,只保留最近 1000"]

    C --> F["保留关键语义 ✓"]
    E --> G["可能丢失关键指令 ✗"]

    style F fill:#e8f5e9
    style G fill:#ffcdd2

压缩最大的风险:信息丢失

好的压缩像「脱水蔬菜」——营养还在,体积小了;差的压缩像「扔菜」——把关键信息扔了还不自知。所以压缩后的验证和压缩本身同等重要。


二、三种压缩形态

压缩类型压缩对象保留什么适用时机
任务摘要当前任务的执行轨迹目标、已完成步骤、当前进度、关键决策窗口达到阈值时触发
文件摘要被读取的外部文件/资源文件用途、关键函数、入口逻辑、依赖关系读取文件后立即生成
过程笔记推理中的洞察和教训踩过的坑、有效的策略、放弃的方案任务间歇时主动记录

类比:三种笔记的差别

任务摘要 = "今天这道红烧肉,焯水→炒糖色→加酱油→小火炖了40分钟,出锅前大火收汁"
文件摘要 = "这本食谱第三章讲的是川菜的麻辣调味,核心调料是花椒+豆瓣酱+干辣椒"
过程笔记 = "下次记住:炒糖色火不能太大,冒烟就苦了;炖肉40分钟刚好不烂不柴"

三、任务摘要(Task Summary)

任务摘要是把 Agent 执行的轨迹压缩成结构化总结,让 Agent 在上下文窗口紧张时能「回头看自己在干什么」。

from dataclasses import dataclass, field, asdict
from typing import Optional
 
@dataclass
class TaskSummary:
    """任务摘要数据结构"""
    goal: str                          # 用户最初的目标
    progress: str                      # 当前进度("已完成3/5步")
    completed_steps: list[str] = field(default_factory=list)
    pending_steps: list[str] = field(default_factory=list)
    key_findings: list[str] = field(default_factory=list)
    errors_encountered: list[str] = field(default_factory=list)
    decisions_made: list[str] = field(default_factory=list)
 
    def to_prompt_snippet(self) -> str:
        """转成注入 LLM 上下文的一段文本"""
        parts = [
            f"## 任务目标\n{self.goal}",
            f"## 当前进度\n{self.progress}",
        ]
        if self.completed_steps:
            parts.append("## 已完成步骤\n" + "\n".join(f"- {s}" for s in self.completed_steps))
        if self.pending_steps:
            parts.append("## 待完成任务\n" + "\n".join(f"- {s}" for s in self.pending_steps))
        if self.key_findings:
            parts.append("## 关键发现\n" + "\n".join(f"- {s}" for s in self.key_findings))
        if self.errors_encountered:
            parts.append("## 遇到过的错误(不要再踩)\n" + "\n".join(f"- {s}" for s in self.errors_encountered))
        if self.decisions_made:
            parts.append("## 已做决策\n" + "\n".join(f"- {s}" for s in self.decisions_made))
        return "\n\n".join(parts)
 
 
# ========== LLM 驱动的摘要生成 ==========
 
TASK_SUMMARY_PROMPT = """你是一个执行摘要专家。根据以下 Agent 执行记录,生成结构化摘要。
 
## 要求
1. 目标:用户到底想干什么?(一句话)
2. 进度:完成到什么程度了?(如"3/5 步骤完成")
3. 已完成步骤:每步用 15 字以内概括
4. 待完成步骤:还差什么没做?
5. 关键发现:执行中发现了什么重要信息?
6. 遇到的错误:踩了什么坑?(最多列 3 条,无则省略)
7. 已做决策:做了什么关键决策?
 
## 输出格式(JSON)
```json
{
    "goal": "...",
    "progress": "...",
    "completed_steps": [...],
    "pending_steps": [...],
    "key_findings": [...],
    "errors_encountered": [...],
    "decisions_made": [...]
}
```text
 
## 执行记录
{execution_log}
"""
 
def generate_task_summary(
    execution_log: str,
    llm_call: callable,  # 实际项目里换成你的 LLM 调用函数
) -> TaskSummary:
    """把执行日志压缩成结构化任务摘要"""
    prompt = TASK_SUMMARY_PROMPT.format(execution_log=execution_log)
    response = llm_call(prompt)
    # 解析 JSON 响应
    data = json.loads(response)
    return TaskSummary(**data)

四、文件摘要(File Summary)

Agent 读取外部文件时,如果把整个文件内容塞进上下文,窗口消耗极快。文件摘要把「读文件」变成「读摘要」,省下大量空间。

@dataclass
class FileSummary:
    """文件摘要"""
    file_path: str
    purpose: str                      # 这个文件是干什么的
    key_exports: list[str] = field(default_factory=list)  # 核心函数/类
    dependencies: list[str] = field(default_factory=list)  # 依赖什么
    entry_points: list[str] = field(default_factory=list)  # 入口逻辑
    notes: str = ""                   # 补充说明
 
    def to_context_block(self) -> str:
        """生成一个紧凑的上下文块,替代原始文件内容"""
        lines = [
            f"[文件摘要] {self.file_path}",
            f"用途: {self.purpose}",
        ]
        if self.key_exports:
            lines.append(f"核心导出: {', '.join(self.key_exports)}")
        if self.entry_points:
            lines.append(f"入口逻辑: {', '.join(self.entry_points)}")
        if self.dependencies:
            lines.append(f"依赖: {', '.join(self.dependencies)}")
        if self.notes:
            lines.append(f"备注: {self.notes}")
        return "\n".join(lines)
 
 
FILE_SUMMARY_PROMPT = """你是一个代码分析专家。用最简洁的语言总结以下文件。
 
## 要求
- purpose(一句话):这个文件的核心职责
- key_exports(最多 5 个):最需要关注的核心函数/类/模块
- dependencies:依赖的外部模块(看 import 语句,只列最重要的 3-5 个)
- entry_points:外部调用的入口点(public API、exports、main 函数等)
- notes:代码风格、设计模式、或任何不寻常的地方(可选,不超过 2 句)
 
## 文件内容
文件路径: {file_path}

{file_content}

 
## 输出格式(JSON)
```json
{{
    "purpose": "...",
    "key_exports": [...],
    "dependencies": [...],
    "entry_points": [...],
    "notes": "..."
}}
```text
"""
 
def summarize_file(file_path: str, file_content: str, llm_call: callable) -> FileSummary:
    """把文件内容压缩成结构化的文件摘要"""
    prompt = FILE_SUMMARY_PROMPT.format(
        file_path=file_path,
        file_content=file_content[:8000]  # 截断过长的文件
    )
    response = llm_call(prompt)
    data = json.loads(response)
    return FileSummary(file_path=file_path, **data)

五、过程笔记(Process Notes)

过程笔记是 Agent 自己写给自己的备忘录——在推理过程中主动记录「刚才这个思路不行,为什么」「发现了一个有用的模式」等洞察,帮助后续推理不走弯路。

@dataclass
class ProcessNote:
    """过程笔记"""
    step_id: int
    timestamp: str
    note_type: str   # insight / dead_end / pattern / decision / todo
    content: str
 
    def to_context_line(self) -> str:
        """转成一行笔记,可以密集排列"""
        emoji_map = {
            "insight": "[发现]",
            "dead_end": "[死路]",
            "pattern": "[模式]",
            "decision": "[决策]",
            "todo": "[待办]",
        }
        prefix = emoji_map.get(self.note_type, "[笔记]")
        return f"{prefix} Step{self.step_id}: {self.content}"
 
 
class ProcessNotebook:
    """过程笔记本:Agent 边推理边记录"""
 
    def __init__(self, max_notes: int = 20):
        self.notes: list[ProcessNote] = []
        self.max_notes = max_notes
        self.step_counter: int = 0
 
    def record(self, note_type: str, content: str):
        """记录一条笔记"""
        self.step_counter += 1
        self.notes.append(ProcessNote(
            step_id=self.step_counter,
            timestamp=datetime.now().isoformat(),
            note_type=note_type,
            content=content,
        ))
 
        # 笔记太多了也做一次压缩
        if len(self.notes) > self.max_notes:
            self._compact()
 
    def _compact(self):
        """把老笔记压缩合并"""
        old_notes = self.notes[:-10]  # 保留最近 10 条
        self.notes = self.notes[-10:]
 
        summary_lines = []
        for note in old_notes:
            summary_lines.append(note.to_context_line())
 
        # 在笔记最前面插入一条合并摘要
        self.notes.insert(0, ProcessNote(
            step_id=0,
            timestamp=datetime.now().isoformat(),
            note_type="insight",
            content=f"历史笔记摘要: {'; '.join(summary_lines[-5:])}",
        ))
 
    def get_context_block(self) -> str:
        """生成给 LLM 注入的笔记块"""
        if not self.notes:
            return ""
        lines = ["## 推理过程笔记"]
        for note in self.notes:
            lines.append(note.to_context_line())
        return "\n".join(lines)
 
 
# ========== 使用示例 ==========
notebook = ProcessNotebook()
 
# Agent 在推理过程中记录
notebook.record("dead_end", "正则匹配报错,改用 AST 解析更可靠")
notebook.record("insight", "用户偏好:每个回答结尾要一个可操作建议")
notebook.record("pattern", "tool_read_file 返回超过 5000 字符时,一定要做文件摘要")
notebook.record("decision", "放弃方案A(递归遍历),改方案B(BFS + 缓存)")
 
print(notebook.get_context_block())

六、压缩质量校验:怎么知道没压坏?

这是压缩中最容易被忽视的一环。压完了不等于压对了

def validate_summary(
    original: str,
    summary: str,
    key_entities: list[str],  # 必须出现在摘要中的关键实体
    llm_call: callable,
) -> dict:
    """
    校验摘要质量:确保压缩没有破坏关键信息。
 
    返回:{passed: bool, issues: list[str], score: float}
    """
    issues = []
 
    # 校验1:关键实体是否都在摘要或原始文本中?
    for entity in key_entities:
        if entity not in summary and entity not in original:
            issues.append(f"关键实体丢失: {entity}")
 
    # 校验2:用 LLM 做关键事实一致性检查
    consistency_prompt = f"""检查以下摘要是否与原文的关键事实一致。
列出摘要中与原文矛盾的陈述(没有就回答"无矛盾")。
 
原文(截取):
{original[:3000]}
 
摘要:
{summary}
 
矛盾之处(若有):"""
 
    consistency_check = llm_call(consistency_prompt)
    if "无矛盾" not in consistency_check:
        issues.append(f"事实矛盾: {consistency_check}")
 
    # 校验3:检查压缩比是否合理
    original_chars = len(original)
    summary_chars = len(summary)
    ratio = summary_chars / max(original_chars, 1)
 
    if ratio > 0.8:
        issues.append(f"压缩比过高 ({ratio:.1%}),几乎没有压缩效果")
    elif ratio < 0.01:
        issues.append(f"压缩比过低 ({ratio:.1%}),可能过度压缩")
 
    passed = len(issues) == 0
    return {
        "passed": passed,
        "issues": issues,
        "compression_ratio": ratio,
        "score": 1.0 - len(issues) * 0.25,  # 简单评分
    }

校验的三个维度

校验维度方法防什么
实体完整性检查关键实体(人名/变量名/数字)是否还在防止把核心信息压没了
事实一致性LLM 检查摘要与原文事实是否矛盾防止「张冠李戴」
压缩比合理性摘要字数 / 原文字数防止压了等于没压,或压太狠

七、过度压缩的风险与护栏

压缩不是越狠越好。下面列出过度压缩的常见后果和护栏:

风险表现护栏
数字精度丢失”85.32%” → “大约八成”数字类实体加入 key_entities 白名单
否定语义翻转”不要删主分支” → “删主分支”否定句禁止压缩,原文保留
因果链断裂只记「做了什么」,忘了「为什么做」任务摘要中 decisions_made 必须包含原因
累积误差多次摘要叠加后与原意越来越远设置最大压缩代数(如最多摘要两次)
# 否定句保护
def protect_negation(text: str) -> list[str]:
    """识别否定句,返回不应被压缩的关键句"""
    negation_patterns = ["不要", "禁止", "不能", "严禁", "勿", "不可", "千万别"]
    protected = []
    for sentence in text.split("。"):
        if any(p in sentence for p in negation_patterns):
            protected.append(sentence.strip())
    return protected
 
# 最大压缩代数限制
class GuardedCompressor:
    def __init__(self, max_generations: int = 2):
        self.max_generations = max_generations
        self.generation_count: dict[str, int] = {}
 
    def compress(self, content_id: str, content: str, summarize_fn: callable) -> str:
        """带代数限制的压缩"""
        self.generation_count.setdefault(content_id, 0)
        if self.generation_count[content_id] >= self.max_generations:
            print(f"[GUARD] {content_id} 已达最大压缩代数,不再压缩")
            return content
        self.generation_count[content_id] += 1
        return summarize_fn(content)

▶ 对应原理:41-上下文压缩三级策略

速记卡(面试闪卡)

Q1:一句话讲清「上下文压缩:任务摘要 - 文件摘要 - 过程笔记」到底是什么? A:上下文压缩用结构化摘要提纯信噪比,而非简单截断丢信息。

Q2:一、为什么压、为什么不能乱压 —— 怎么理解? A:像脱水蔬菜而非扔菜:好压缩营养还在体积小,差压缩把关键指令丢了还不自知——压完必须校验,验证和压缩同等重要。英文:Signal-to-Noise Ratio。

Q3:二、三种压缩形态:任务/文件/过程 —— 怎么理解? A:像做菜的三本笔记:任务摘要记”做到哪步了”、文件摘要记”这本食谱讲啥”、过程笔记记”下次别再炒糊”——对象不同、时机不同。英文:Task / File / Process Summary。

Q4:三、任务与文件摘要:把轨迹和外部文件压成块 —— 怎么理解? A:像给 Agent 配”便利贴”:TaskSummary 存目标进度与踩坑,FileSummary 存用途与核心导出——读文件变成读摘要,窗口瞬间省一大截。英文:Structured Summary。

Q5:四、质量校验与护栏:压坏了早发现 —— 怎么理解? A:像出货前质检三关:查关键实体在不在、查事实有无张冠李戴、查压缩比合不合理;否定句禁压、设最大压缩代数防累积误差。英文:Validation Guardrail。

Q6:核心速记主线有哪些?

  • 三种形态:任务摘要(进度)/ 文件摘要(用途)/ 过程笔记(洞察)
  • 核心目标:提高信噪比,不是单纯删字数
  • 校验三关:实体完整、事实一致、压缩比合理
  • 护栏:否定句禁压、最大压缩代数、数字入白名单

口诀 A:上下文压提纯汤, 任务文件过程装; 三种摘要分场合, 校验护栏防走样。

相关链接


技术学习路线图 > 记忆与上下文

相关链接