RAG 检索到的上下文太长怎么办?上下文压缩怎么做?
结论:上下文过长有三宗罪——贵、慢、中间被忽略。压缩(Contextual Compression)的目标不是"塞得更多", 而是"用更少的 token 保住回答所需的证据"。手段分四类:去重、重排后截断、句子级抽取、摘要与 token 级压缩。 顺序永远是先重排再压缩,且压缩后必须还能标出引用来源。
1. 代价先算清
| 上下文 token | 相对费用 | 首字延迟 | 证据利用率 |
|---|---|---|---|
| 2k | 1× | 快 | 高 |
| 8k | 4× | 中 | 中,中部开始被忽略 |
| 32k | 16× | 慢 | 低,且不一定比 8k 更准 |
费用随 token 线性上涨,而准确率不随上下文变长而上涨:注意力呈 U 形分布,开头结尾记得牢,中间容易漏。 所以"把 top-50 全塞进去"是最贵且最不划算的做法。
2. 四类压缩手段
| 方法 | 做法 | 压缩比 | 风险 |
|---|---|---|---|
| 去重 | 内容哈希 / MinHash 合并近似重复块 | 10%~40% | 低,几乎无副作用 |
| 重排 + 截断 | 只保留 top-n(3~8 条) | 50%~80% | 边界证据被砍掉 |
| 抽取式 | 按句打分,只留与问题相关的句子 | 60%~90% | 句子被切碎,指代丢失 |
| 摘要式 | 让 LLM 概括每块或合并多块 | 70%~95% | 引入失真,引用难对上 |
| token 级 | 删低信息量 token(如 LLMLingua) | 2~20× | 可读性差,领域词易误删 |
选择原则:需要给出处就用抽取式,摘要式只适合"总结这份文档"类任务。
3. 压缩在链路中的位置
召回 50 条
│ rerank
▼
12 条(分数过阈值)
│ 去重(同一文档相邻块合并)
▼
8~10 条
│ 句子级抽取(每块保留 ≤6 句)
▼
按 token 预算装箱
▼
prompt(含 [n] 编号与来源)
4. 代码:抽取 + 预算装箱
import re
import numpy as np
def split_sentences(text: str) -> list[str]:
# 中文句末标点后切,保留标点
return [s for s in re.split(r"(?<=[。!?;])", text) if len(s.strip()) > 8]
def compress_doc(qvec: np.ndarray, doc: dict, max_sents: int = 6) -> dict:
sents = split_sentences(doc["text"])
if len(sents) <= max_sents:
return doc
vecs = np.array(embed(sents))
scores = vecs @ qvec # 句向量已归一化
keep = sorted(np.argsort(scores)[::-1][:max_sents]) # 取高分但保持原文顺序
doc["text"] = "".join(sents[i] for i in keep)
doc["compressed"] = True
return doc
def fit_budget(docs: list[dict], max_tokens: int = 2500, chars_per_token: float = 1.4) -> list[dict]:
"""按预算装箱:高分文档优先,装不下就截断到剩余额度。"""
budget = int(max_tokens * chars_per_token) # 中文 1 token ≈ 1.4 字符的粗估
out = []
for d in docs:
if budget <= 200:
break
text = d["text"][:budget]
out.append({**d, "text": text})
budget -= len(text)
return out
装不下时截断比丢弃更安全:留下的半段仍可能含关键数字,但记得在截断处加省略号,并在日志里记录截断率。
5. 压缩后如何保住引用
| 做法 | 说明 |
|---|---|
保留 source、page、offset | 压缩只改文本,不动元数据 |
块内编号 [n] 在压缩前分配 | 压缩后编号不重排,避免引用错位 |
| 抽取式保留原句原文 | 不要改写成"大意是……",否则校验对不上 |
| 摘要式单独标记 | 摘要不参与引用校验,只作背景 |
6. 常见追问
Q:模型支持 128k 上下文,还需要压缩吗? 需要。一是费用与延迟,二是长上下文的中部召回会衰减,三是上下文里噪声越多,模型越容易被带偏。
Q:压缩会降低召回率吗? 不会影响召回,只影响生成可见的证据。前提是压缩发生在重排之后——先压后排,等于把好证据提前删了。
Q:摘要和抽取怎么选? 要"逐句有出处"选抽取;要"这段在讲什么"选摘要。二者也可叠加:摘要做导航,抽取做证据。
Q:token 级压缩能上生产吗? 可以用,但要按领域验证:它删的是低信息量 token,中文与专业术语容易被误删,务必在评估集上对比忠实度。
小结
压缩的本质是预算分配:把有限的 token 花在最能支撑答案的句子上。先重排、再去重、再抽取、最后才考虑摘要——顺序错了,压缩就变成丢证据。