Skip to content

10|压缩策略:长了怎么办

上一篇的组装管线解决了"放什么"。但有个问题它没解决:上下文会越来越长。

多轮对话累积,工具结果累积,检索材料累积,总有一天超出窗口。简单截断会丢关键信息,不截断会报上下文超长。本篇解决上下文太长时怎么办——从轻到重的四层压缩策略。

先算清预算

压缩之前先知道预算还剩多少。上下文窗口是输入+输出+推理的总容量上限(第 19 篇会展开三种 token 的关系),不是输入独占。

AIOps 概念图:先算 token 预算

python
def count_tokens(text: str, model: str = "gpt-4o") -> int:
    """粗略估算 token 数。model 仅用于选 tiktoken 编码器,不是真实推理模型名。"""
    import tiktoken
    encoder = tiktoken.encoding_for_model(model)
    return len(encoder.encode(text))

def context_token_count(messages: list[dict]) -> int:
    return sum(count_tokens(json.dumps(m, ensure_ascii=False)) for m in messages)

生产环境用 API 返回的 usage.input_tokens 更准,本地估算用 tiktoken。知道当前用了多少、上限多少,才能决定要不要压缩、压缩到什么程度。

四层从轻到重

压缩不是一上来就摘要。摘要要额外调用模型,成本高、延迟大。正确的做法是从轻到重,够用就停。

AIOps 概念图:四层压缩

python
def compact(messages, budget, client):
    # 第一层:轻量整理
    messages = light_trim(messages)
    if context_token_count(messages) < budget * 0.9:
        return messages

    # 第二层:上下文折叠
    messages = fold_messages(messages, budget)
    if context_token_count(messages) < budget * 0.9:
        return messages

    # 第三层:自动摘要
    messages = auto_compact(messages, client, budget)
    if context_token_count(messages) < budget * 0.9:
        return messages

    # 第四层:关键文件重注入
    messages = reinject_key_files(messages, budget)
    return messages

每一层试完都检查够不够,够了就停,不往下走。这是成本意识——轻量处理能释放足够空间时,不必触发昂贵的摘要。

第一层:轻量整理

最轻的处理,不改变实质信息,只清理格式噪声。去掉重复的空行、多余的 markdown 标记、工具结果里冗长的元数据。释放的 token 空间有限,但成本接近零。

python
def light_trim(messages):
    # 去掉空消息、重复内容、过期的时间戳标记
    return [m for m in messages if has_substance(m)]

第二层:上下文折叠

把详细内容折叠成摘要,保留关键字段。工具返回的 500 行日志,折叠成"14:32 出现 KEYS order:*,耗时 350ms"。早期几轮对话,折叠成"已确认:P99 升到 120ms,slowlog 出现 KEYS"。

折叠比轻量整理释放更多空间,但仍不调用模型,靠规则或模板。

第三层:自动摘要

前两层不够时,才调用模型做摘要。把早期对话历史和长文档喂给模型,让它生成结构化摘要:

python
def auto_compact(messages, client, budget):
    old = messages[:-6]   # 早期部分
    recent = messages[-6:]  # 保留最近几轮
    summary = client.responses.create(
        model=settings.model,
        instructions="把以下对话压缩成事实摘要,保留已确认事实、用户约束、待确认项。不要写过程。",
        input=json.dumps(old, ensure_ascii=False),
    ).output_text
    return [{"role": "system", "content": f"会话摘要:\n{summary}"}, *recent]

这一层成本最高——多一次模型调用,增加延迟和 token 消耗。所以放最后,前两层够了就不触发。

第四层:关键文件重注入

摘要会丢细节。如果当前决策依赖某个关键文件的具体内容(比如一份配置文件的精确字段),摘要后要把这个文件重新放回上下文。压缩时标记哪些是关键文件,压缩后按需重注入。

压缩必须保留用户消息

这是压缩策略最重要的一条。

AIOps 概念图:保留用户消息

摘要最容易丢失的是用户约束——"不要用某技术栈""只看最近一小时""不要建议重启"这类限制。一旦在摘要里漏掉,后续轮次就会被破坏。模型在压缩后的上下文里看不到这些约束,可能给出违反约束的回答。

压缩 prompt 必须明确要求保留用户消息和用户约束:

python
instructions = (
    "把以下对话压缩成事实摘要。"
    "必须保留:用户的明确约束、已确认事实、待确认项。"
    "可以丢弃:工具调用的中间过程、重复的确认消息。"
)

压缩不是丢信息,是换形式保存信息。被折叠或摘要的内容仍然保存在 trace 和持久化存储里,事后审计能完整恢复。压缩只影响模型当前看到的上下文,不影响系统的长期记忆。

够用就停

触发压缩的阈值不是固定值。成本敏感场景(大量自动化告警处理)可以把阈值设低,预算用到 60% 就开始压缩,尽早减少单次请求 token。质量敏感场景(人工复核的关键故障排查)把阈值设高,90% 再压缩,尽量保留完整上下文。

预处理不是每次请求都走完四层。固定流程——每次都整理、折叠、摘要——违背管线核心设计:够用就停。生产环境里多数请求只需要轻量整理,少数走到折叠,只有少数长会话才触发自动摘要。固定走完四层会让大部分请求多付不必要的成本。具体各层占比以本项目 eval 为准。

压缩之后

压缩解决了已有材料太长的问题。但有些材料不在手边——Runbook、历史案例、文档。这些知识没法预先放进上下文,要用的时候得去检索。下一篇讲 RAG:查外部知识。