上下文窗口超限的 5 种压缩策略(各自适用场景与工程权衡)

上下文窗口是 LLM 的”工作台”,对话历史超出台面,模型就开始”失忆”——开头的指令被忘掉、中间的事实被截断、关键约束被丢失。在做长会话 Agent 时,用户一次任务聊了 200 轮、累计 50 万 token,模型开始反复问同样的问题、给出矛盾的回答;引入”分层压缩”后,同样的会话跑下来又稳又准。下文把 5 种压缩策略放在一起对比,给出适用场景、算法思路和工程实现。

一、为什么”压缩”比”扩窗口”更工程化

理论上,把所有对话塞进超长上下文窗口(128K、200K、1M token)就能解决超限问题,实际不行:

  1. 成本:输入 token 计费,200K 上下文比 8K 贵 25 倍,长会话烧钱极快。
  2. 延迟:首 token 时间随上下文长度线性增长,200K 输入通常要 2-5 秒,长任务累计极慢。
  3. 注意力分散:LLM 在超长上下文里”找针”的能力下降,关键信息容易被淹没。
  4. 模型限制:很多开源/小模型不支持超长窗口,部署时受限。

所以工程上更倾向”压缩后保留关键信息”,而不是”硬塞进超长窗口”。

二、5 种主流压缩策略对比

策略 核心思路 保留什么 代价 适用场景
滑动窗口 只保留最近 N 轮 近期完整对话 丢失远期信息 短任务、强时效
摘要压缩 把旧对话压成摘要 关键事实、决策、待办 摘要有损,多一份 token 成本 长任务、需要跨轮记忆
关键事实提取 抽取硬信息单独存 ID、数字、错误码等 实现复杂 强结构化任务
向量检索召回 历史全量存向量库 按当前 query 召回相关 增加检索延迟 跨任务长期记忆
层次化压缩 多级(原对话→段摘要→全局摘要) 自上而下的全貌 实现最复杂 超长会话、复杂规划

三、策略 1:滑动窗口(Sliding Window)

最简单:只保留最近 N 轮对话,之前的全部丢弃。

def sliding_window(messages, max_turns=10):
    # 保留 system + 最近 N 轮
    system = [m for m in messages if m["role"] == "system"]
    conversation = [m for m in messages if m["role"] != "system"]
    return system + conversation[-max_turns*2:]

适用场景:对话窗口敏感(每轮 token 都很贵)、近期对话比远期更重要(如客服、闲聊)。优点:零成本、零延迟。缺点:用户回看老话题时模型完全失忆。

四、策略 2:摘要压缩(Summarization)

把超出窗口的旧对话用 LLM 压成摘要,新对话进来后再增量更新摘要。

def compress_history(client, messages, max_tokens=2000):
    # 把对话按"段"切分,每段压一次
    chunks = []
    current = []
    for m in messages:
        current.append(m)
        if sum(len(c.get("content","")) for c in current) > 8000:
            chunks.append(current)
            current = []
    if current: chunks.append(current)
    # 每段生成摘要
    summaries = []
    for ch in chunks:
        text = "\n".join(f"{m['role']}: {m.get('content','')}" for m in ch)
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": "把这段对话压缩到 300 字,保留:用户目标、已确认事实、待办任务、关键决策。"},
                {"role": "user", "content": text}
            ]
        )
        summaries.append(resp.choices[0].message.content)
    # 把所有摘要 + 最近 N 轮原对话拼回去
    return [{
        "role": "system",
        "content": "以下是历史对话的摘要:\n\n" + "\n\n".join(summaries)
    }] + messages[-6:]

适用场景:长任务需要跨轮记忆(如编程 Agent、多步数据分析)。优点:保留关键事实。代价:每次新对话进来都要重新摘要,token 成本约为直接存原对话的 10-20%。

五、策略 3:关键事实提取(Fact Extraction)

从对话里主动抽取”硬信息”(ID、数字、时间、错误码、用户偏好)单独存,后续按需注入。

def extract_facts(client, conversation_text):
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "从这段对话里抽取硬信息,返回 JSON 列表。每条:{\"type\": \"类型\", \"value\": \"值\"}。类型可包括:order_id, error_code, user_preference, deadline, url。"},
            {"role": "user", "content": conversation_text}
        ],
        response_format={"type": "json_object"}
    )
    return resp.choices[0].message.content

适用场景:任务对硬信息敏感(订单处理、工单管理、金融交易)。优点:信息精炼、可结构化查询。代价:实现复杂,需要 schema 设计和持续维护。

六、策略 4:向量检索召回(Vector Retrieval)

把历史对话全量存进向量库,后续根据当前 query 召回最相关的若干条。

import chromadb

def setup_memory_collection():
    client = chromadb.PersistentClient(path="./memory")
    return client.get_or_create_collection("history")

def store_turn(collection, turn_id, text, metadata):
    collection.add(documents=[text], ids=[turn_id], metadatas=[metadata])

def retrieve_relevant(collection, query, top_k=3):
    results = collection.query(query_texts=[query], n_results=top_k)
    return "\n\n".join(results["documents"][0])

适用场景:跨任务/跨会话的长期记忆(用户偏好、历史任务、特定事件)。优点:相关性强、容量大。缺点:首次检索有 embedding 延迟,长会话下检索质量可能下降。

七、策略 5:层次化压缩(Hierarchical Compression)

把对话分成多级:原始对话 → 段摘要(每 10 轮) → 全局摘要(每 100 轮)。需要回忆时按”全局 → 段 → 原文”逐级展开。

class HierarchicalMemory:
    def __init__(self):
        self.raw_turns = []       # 最近 20 轮原文
        self.chunk_summaries = []  # 每 20 轮压一次
        self.global_summary = ""  # 全局摘要
    
    def add_turn(self, turn):
        self.raw_turns.append(turn)
        if len(self.raw_turns) >= 20:
            # 触发段摘要
            chunk_text = "\n".join(self.raw_turns)
            self.chunk_summaries.append(self._summarize(chunk_text))
            self.raw_turns = []
            # 每 5 段摘要触发一次全局重写
            if len(self.chunk_summaries) >= 5:
                self.global_summary = self._re_summarize(self.chunk_summaries)
                self.chunk_summaries = []
    
    def get_context(self, query):
        return {
            "global": self.global_summary,
            "chunks": self.chunk_summaries,
            "recent": self.raw_turns[-6:]
        }

适用场景:超长会话(月度客服、跨周项目协作)。优点:兼顾全局视野和近期细节。代价:实现复杂,各级摘要的一致性维护成本高。

八、策略选型决策树

按下面三步选:

  1. 任务长度: < 50 轮 → 滑动窗口;50-200 轮 → 摘要压缩;> 200 轮 → 层次化压缩。
  2. 记忆类型: 强结构化数据(订单/工单)→ 关键事实提取;语义信息(用户偏好) → 向量检索;两者都有 → 关键事实 + 向量双轨。
  3. 成本敏感度: 极敏感 → 滑动窗口;宽松 → 摘要/向量;不在乎 → 层次化。

实战里多数项目用”滑动窗口作底盘 + 摘要压缩作主体 + 向量库作长期”的混合方案,平衡成本与质量。

九、压缩后必须做的一件事

不管用哪种压缩策略,每次压缩完都要验证:把”压缩后的摘要”和”原对话”一起丢给 LLM,问”这两者是否一致”。这一步在生产环境里通常用一个小模型异步跑,作为压缩质量的”最后一道关”。发现不一致就标记、降级、报警。

常见问题(FAQ)

Q1:用 200K 长上下文模型是不是就不用压缩了?

不一定。200K 上下文成本是 8K 的 25 倍,长会话跑下来账单会很难看;且超长上下文里模型”找针”能力下降,关键信息未必能被注意到。

Q2:摘要会丢失关键信息,怎么补?

关键事实抽取(策略 3)就是为这个设计的;摘要+关键事实双轨存储,既保留语义又保留硬信息。

Q3:向量检索召回不到重要信息怎么办?

通常是因为切段粒度太大(每段几千字)或 embedding 模型对中文不友好;调小切段粒度到 200-500 字 + 换中文优化模型(如 bge-large-zh)能明显改善。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
拓扑大师的头像拓扑大师普通用户

相关推荐

返回顶部