很多 RAG 系统都有过这样的升级过程:
TopK=3,担心漏资料TopK=10,看起来更保险TopK=30,反正模型上下文够长结果却很奇怪:Token 用得更多,响应更慢,答案反而开始漏条件、引用旧版本,甚至把两个不同产品的规则拼在一起。
问题不是大模型“装不下”。能塞进去,与能稳定利用,是两回事。
上下文窗口是容量上限,不是有效注意力的保证。
一、更多上下文为什么会伤害答案
假设用户问:“企业版 4.2 如何关闭自动续费?”检索结果里有:
企业版 4.2 自动续费关闭步骤; 个人版取消订阅流程; 企业版 3.8 续费规则; 退款政策; 支付失败排查; 自动续费功能介绍; 海外版订阅条款。
这七段都与“续费”相关,但真正回答问题的可能只有第一段。其余内容会带来几类噪声:
- 主题噪声:同领域,但不回答当前问题;
- 版本冲突:旧规则与新规则同时出现;
- 实体冲突:个人版、企业版、海外版混在一起;
- 重复信息:相邻 Chunk 反复出现同一句话;
- 指令干扰:文档中包含看起来像 Prompt 的文本;
- 位置问题:关键证据埋在长上下文中间。
生成模型不仅要找到正确句子,还要判断哪些条件适用、哪些已经过期。上下文越杂,这项工作越难。
二、Lost in the Middle 到底是什么
Lost in the Middle 常被翻译成“中间信息丢失”。它描述的是一种常见现象:在很长的输入中,模型对开头和结尾的信息利用得更稳定,而对中间位置的重要信息可能不够敏感。
它不是说模型完全看不到中间内容,也不是所有模型、所有任务都呈现相同曲线。更准确的理解是:信息位置会影响被使用的概率,超长且充满噪声的上下文尤其明显。

Lost in the Middle 位置效应
RAG 中最危险的情况是:正确证据虽然被召回,却被十几段相似内容夹在中间。模型最后根据更靠前的旧规则,或更靠后的通用总结作答。
所以“正确文档在上下文里”只是必要条件,不是充分条件。我们还要关心:
它放在哪里; 周围是否有冲突内容; 它是否包含完整条件; 模型是否能快速识别来源和边界。
三、上下文压缩不是简单做摘要
Contextual Compression 的目标是:保留回答当前问题所需的证据,删除无关和重复内容。
它至少有四种层次。
1. 文档级过滤
先判断整个 Chunk 是否值得进入上下文。Rerank 后取 TopN 就属于这一层。
优点是快、结构稳定;缺点是一个 Chunk 可能只有两句话有用,其余仍然占 Token。
2. 句子级抽取
从 Chunk 中抽取与问题直接相关的原句,同时保留来源。
原 Chunk:共 600 字,包含功能介绍、适用范围、操作步骤和注意事项压缩后:保留“适用于企业版 4.2”及三条关闭步骤,共 120 字抽取比自由摘要更适合需要引用和审计的场景,因为证据仍是原文。
3. 查询相关摘要
对较长文档生成围绕当前问题的摘要。它能显著缩短上下文,但可能遗漏限定条件或引入改写偏差。
高风险业务中,不建议只保留摘要而丢掉原文定位。至少保留 document_id、页码、段落和被摘要的原始片段。
4. 结构化压缩
对表格、合同、API 文档等内容,与其生成自然语言摘要,不如抽取与问题相关的字段、行或章节。
例如用户问某个型号的最大功率,就从表格中保留表头、目标型号行和单位,而不是把整张表转成一段散文。
四、一条实用的上下文组装流水线
推荐按下面顺序处理:
多路召回 ↓权限与版本过滤 ↓Rerank ↓按来源去重、合并相邻 Chunk ↓句子抽取或查询相关压缩 ↓冲突检测与顺序调整 ↓Token 预算内组装上下文第一步:合并相邻 Chunk
如果 Top 结果来自同一文档相邻位置,可以先还原成更完整的段落,避免重复标题和半句话。
def merge_adjacent(chunks):chunks = sorted(chunks,key=lambda x: (x.metadata["document_id"], x.metadata["start_offset"]),)merged = []for chunk in chunks:if not merged:merged.append(chunk)continueprevious = merged[-1]same_doc = (previous.metadata["document_id"]== chunk.metadata["document_id"])close_enough = (chunk.metadata["start_offset"]<= previous.metadata["end_offset"] + 100)if same_doc and close_enough:previous.page_content += "\n" + chunk.page_contentprevious.metadata["end_offset"] = chunk.metadata["end_offset"]else:merged.append(chunk)return merged
真实项目中不要原地修改缓存里的 Document,可以复制后再合并。这里为了突出逻辑做了简化。
第二步:抽取证据,而不是让模型重写答案
给压缩模型的指令要非常克制:
根据用户问题,从候选片段中原样抽取能够支持答案的句子。必须保留条件、否定、版本、单位和例外。不要回答问题,不要补充片段中不存在的信息。如果片段不包含有效证据,返回 EMPTY。输出最好结构化:
{"chunk_id": "billing-guide-4.2#7","evidence": ["企业版 4.2 管理员可在账单设置中关闭自动续费。","关闭操作必须在下个计费周期开始前 24 小时完成。"]}
第三步:按 Token 预算组装
上下文不应该把模型窗口全部用满。还要给系统指令、历史对话、用户问题和答案预留空间。
def select_with_budget(items, max_tokens, count_tokens):selected = []used = 0for item in items:size = count_tokens(item["text"])if used + size > max_tokens:continueselected.append(item)used += sizereturn selected
这里按 Rerank 后的优先级选取,但还可以增加来源多样性和每篇文档上限,避免一份资料独占整个上下文。
五、证据应该按什么顺序放
没有适用于所有模型的万能顺序,但可以从三个原则出发。
最强证据优先
把最直接、版本最匹配、能完整回答问题的证据放在前面。不要让通用背景压过操作结论。
冲突内容相邻
如果必须保留新旧规则,放在一起并清楚标注适用版本,不要让它们散落在上下文两端。
重要证据不要只放中间
对特别关键的规则,可以在上下文开头放证据摘要,后面附原始片段和来源。注意摘要仍不能代替原文证据。
一个清晰的上下文格式比单纯调位置更可靠:
[证据 1]来源:企业版账单手册版本:4.2状态:当前有效内容:……[证据 2]来源:自动续费常见问题版本:2026-06状态:当前有效内容:……六、压缩会不会把关键内容压没
会,而且这是上下文压缩最需要防的风险。
尤其容易丢失:
“不”“禁止”“除非”等否定和例外; 数值、币种、单位、日期; 版本和地区限制; 表格的表头与脚注; 操作步骤之间的顺序; 引用来源。
因此评测不能只看 Token 减少比例,还要看“证据保真”。可以把压缩前的必需事实标出来,计算压缩后保留了多少。
Evidence Recall = 压缩后保留的必需事实数 / 全部必需事实数同时观察:
上下文 Token 数; Context Precision 与 Context Recall; 答案正确率和 Faithfulness; 引用正确率; 压缩模型增加的延迟和成本。
一个方案把上下文从 8000 Token 压到 1500 Token,却丢了有效期条件,不能算优化成功。
七、什么时候不要用 LLM 压缩
候选本来就短且准确; 需要逐字引用的法规、合同条款; 表格结构比自然语言更重要; 延迟预算极紧; 压缩模型无法在本地合规部署; 还没有解决召回和权限问题。
这时可以优先用确定性方法:Metadata Filter、Rerank、相邻片段合并、按标题截取、正则抽取错误码、按表格行过滤。
上下文压缩不是必选组件,而是一种用额外计算换取更少噪声的手段。
八、别凭感觉上线,做一组上下文 A/B 实验
准备同一批困难问题,同时运行三组链路:原始 TopK、只做确定性去重、去重后再做 LLM 压缩。三组使用相同生成模型和 Prompt,避免把模型波动误认为压缩收益。
实验需要回答四个问题:
- 证据是否还完整:否定、条件、版本、数值、单位和原文定位有没有丢失;
- 答案是否真的变好:不仅看简洁程度,还要比较正确率、Faithfulness 和引用准确率;
- 资源是否值得:记录上下文 Token、压缩延迟和单次请求成本;
- 哪些问题应该绕过压缩:短问题、结构化查询和逐字引用场景是否直接走短链路更稳。
只有当质量收益在固定评测集上持续存在,并且超过新增延迟与成本,压缩组件才值得进入默认链路。
少给模型一点,反而需要更多工程判断
RAG 的目标不是把更多文字交给模型,而是把更少、更准、更完整的证据交给模型。TopK 决定候选规模,Rerank 调整优先级,上下文压缩负责把噪声真正拿掉。
夜雨聆风