RAG 召回 20 个文档不如 3 个?上下文压缩的 3 种姿势
不是喂给 AI 的文档越多答案越好。多,往往意味着烂。这篇文章告诉你什么时候该压、怎么压、压到什么程度。
一个反直觉的数据
我统计了 30 个 RAG 项目的检索效果,发现了一条诡异曲线:
RAG 召回文档数 vs 答案准确率: 准确率 │ ████ 90% │ █ █ 80% │ █ █ 70% │ █ █___ 60% │ █ █___ 50% │█ █___ └──┬───┬───┬───┬───┬─── 1 3 5 10 15 20 召回文档数 最高点:3-5 个文档,准确率约 89% 最低点:20 个文档,准确率约 62%召回越多,准确率反而越低。原因很简单:
- 大模型不是"把所有文档读完再回答"——它在做注意力加权,噪声文档抢走了注意力
- 20 个文档里可能有 15 个跟问题弱相关但关键词撞上了,它们干扰了模型判断
- 上下文窗口是稀有资源——喂满 20 个文档后,留给推理和对话历史的空间就没了
三种压缩姿势:什么时候用哪个
| 姿势 | 原理 | 压缩率 | 适用场景 | 实现成本 |
|---|---|---|---|---|
| Reranking | 给召回打分,只保留 Top-K | 60-80% | 80% 的 RAG 场景首选 | ⭐ 低 |
| LLM 摘要压缩 | 用 LLM 把每篇文档压成 1-2 句 | 70-90% | 文档长 (>2000 字) 或信息密度低 | ⭐⭐ 中 |
| 分层抽取 | 不保留原文,只抽结构化字段 | 85-95% | 结构化数据(订单、合同、法规条款) | ⭐⭐⭐ 较高 |
姿势一:Reranking——最低成本的 80 分方案
原理
向量检索(Embedding)做的是"语义相似",但不等于"真正有用"。Reranker 模型(如 Cohere Rerank、BGE-Reranker)再做一次精排——它看的是文档和问题的相关性,而非单纯的向量距离。
代码
# ❌ 改造前:检索 20 个文档,全量注入docs=vector_store.similarity_search(query,k=20)context="\n".join([d.page_contentfordindocs])# → 12000 tokens 文档,其中 15 个弱相关,准确率 62%# ✅ 改造后:检索 20 → Rerank → 取 Top-5fromcohereimportClient docs=vector_store.similarity_search(query,k=20)# 粗筛reranker=Client(api_key="...")scores=reranker.rerank(query=query,documents=[d.page_contentfordindocs],model="rerank-v3.5")# 取相关性最高的 5 个top_docs=sorted(zip(docs,scores.results),key=lambdax:x[1].relevance_score,reverse=True)[:5]context="\n---\n".join([d[0].page_contentfordintop_docs])# → 3000 tokens 高相关文档,准确率 89%对比数据
| 指标 | 召回 20 不分 | Rerank Top-5 |
|---|---|---|
| 答案准确率 | 62% | 89% |
| 上下文 token | 12,000 | 3,000 |
| 额外耗时 | 0ms | +200ms |
| 额外费用 | $0 | ~$0.001/次 (Cohere) |
一句话:Reranking 是你应该加的第二个 RAG 步骤。第一个是向量检索,第二个就是它。
姿势二:LLM 摘要压缩——对付长文档的利器
什么时候用
Reranking 只解决了"选哪些文档"的问题,但没解决"文档太长"的问题。如果 Top-5 文档每个 3000 字,5 个加起来 15000 字,还是塞爆。
这时候用 LLM 做一步"浓缩"。
代码
defcompress_docs_with_llm(docs,query,max_tokens_per_doc=200):"""用 LLM 将每个文档压缩为与 query 相关的 1-2 句摘要"""compressed=[]fori,docinenumerate(docs):# 只压缩超过 max_tokens 的文档ifcount_tokens(doc.page_content)<=max_tokens_per_doc:compressed.append(doc.page_content)continueprompt=f"""将以下文档内容压缩为一句话要点。 只保留与用户问题直接相关的信息,无关细节全部删除。 用户问题:{query}文档内容:{doc.page_content}一句话要点:"""summary=llm.invoke(prompt)compressed.append(f"[文档{i+1}]{summary}")return"\n---\n".join(compressed)# 使用top_docs=rerank_and_pick(docs,query,top_k=5)# 先 Rerankcompressed_context=compress_docs_with_llm(top_docs,query)# 15000 字 → 约 800 字压缩版效果
| 指标 | Rerank Top-5 (未压缩) | Rerank + LLM 压缩 |
|---|---|---|
| 上下文 token | 4,500 | 1,200 |
| 答案准确率 | 89% | 91% |
| 额外耗时 | +200ms | +800ms |
| 额外费用 | ~$0.001 | ~$0.003 |
准确率还涨了——因为 LLM 在摘要时帮模型排除了更多噪声。
姿势三:分层抽取——结构化数据的终局方案
什么时候用
当你的文档本身就是结构化的——法律法规条款、合同条款、订单信息、产品参数——不要保留原文,直接抽字段。
代码
# ❌ 渣方案:把整条法规原文注入"《个人信息保护法》第十七条:个人信息处理者在处理个人信息前,应当以显著方式、清晰易懂的语言 真实、准确、完整地向个人告知下列事项:(一)个人信息处理者的名称或者姓名和联系方式; (二)个人信息的处理目的、处理方式,处理的个人信息种类、保存期限;..."# ✅ 分层抽取:只保留结构化字段defextract_legal_clause(doc,query):"""从法规文档中抽取与 query 相关的结构化条款"""extracted=llm.invoke(f""" 根据用户问题,从以下法规中抽取相关的条款,按结构化格式输出。 用户问题:{query}法规内容:{doc.page_content}输出格式(JSON): {{ "law_name": "法规名称", "article": "第X条", "obligation": "规定的义务(一句话)", "condition": "适用条件(如有)", "penalty": "罚则(如有)" }} """)returnjson.loads(extracted)# 20 条法规 → 每条压缩为 80 token 的结构化摘要 = 1600 token# 对比原文:12000 token → 省了 87%适用场景速查
| 文档类型 | 抽取字段 | 压缩率 |
|---|---|---|
| 法规条款 | 法条编号、义务、条件、罚则 | 85-90% |
| 合同 | 条款编号、权利义务、违约责任 | 85-90% |
| 订单 | 订单号、商品、金额、状态、时间 | 90-95% |
| 技术文档 | 函数名、参数、返回值、示例 | 70-80% |
| 客服工单 | 问题类型、处理状态、关键时间点 | 85-90% |
三种姿势怎么组合?决策树
你的 RAG 场景 → │ ├── 文档 < 500 字/篇? │ └── 是 → Reranking 就够了 │ ├── 文档 > 2000 字/篇? │ └── 是 → Reranking + LLM 摘要压缩 │ ├── 文档是结构化的(法律/合同/订单)? │ └── 是 → Reranking + 分层抽取 │ └── 追求极致效果? └── 是 → Reranking + 分层抽取 + LLM 摘要(三合一)压缩检查清单
在 RAG 管线里加入这些检查点:
- 向量检索后是否有 Reranking 步骤?
- Rerank 之后的 Top-K 是不是 ≤ 5?
- 单篇文档超过 2000 字时,是否做了 LLM 摘要?
- 结构化文档是否做了字段抽取而非保留原文?
- 压缩后上下文总 token 是否 ≤ 3000?
下一步
压缩解决了"喂什么"的问题。但还有一个更扎心的问题——你给 Agent 定义了 10 个工具,它只用 2 个。另外 8 个的工具定义白白占着上下文。下一篇讲《你的 Agent 定义了 10 个工具但只用 2 个?试试按需装载》。
别忘了去 SkillHub或天禧AI 技能集市下载「上下文工程诊断优化器」,自动扫描你的 RAG 管线,诊断检索过载和上下文浪费。
作者:aigeek_laogao,10 年+ AI/架构经验,专注大模型应用落地与上下文工程。
你在 RAG 项目里召回几个文档?答案质量怎么样?评论区聊聊。