当所有人都在讨论如何提升召回率时,真正的战场已经转移到了召回之后。
一、为什么召回率 95%,大模型还是答错?
几乎所有 RAG 项目的第一步都是提升召回率:调 Embedding 模型 、换向量数据库、优化 Chunk 策略……一套操作下来,Recall@10 到了 95%,团队觉得稳了。
系统上线后,用户问了个问题,大模型却给了个似是而非的答案。回头查召回的文档——明明挺相关,Top-10 里 8 条都相关,怎么就答错了?
问题不在召回,在召回之后。
召回率陷阱
有个现象挺有意思:当 Recall@5 从 70% 提升到 85% 时,最终问答准确率只提升了 5 个百分点;而当 Recall@10 从 85% 提升到 95% 时,准确率反而下降了 2 个百分点。
Recall@K 这个指标只关心"前 K 条里有多少相关文档",不关心文档排得对不对,也不关心里面混了多少噪音。
K 变大,相关文档确实多了,噪音也跟着多了。更要命的是,向量检索的排序是按相似度来的,但这个相似度是 Embedding 模型理解的"相似"——不等于"对回答问题有帮助"。
向量检索的局限
向量检索把语义相近的内容映射到向量空间里的相近位置。这里有两个损失:
1. 信息压缩必然丢失细节
一段 500 字的文档被压缩成 1024 维的向量,这个过程注定要丢东西。Embedding 能抓住大意,但抓不住细节;能识别主题,但识别不了用户真正的意图。
2. 语义相似不等于问答相关
用户问"如何优化 SQL 查询性能",Embedding 可能把"SQL 注入防护"也召回——因为它们都含"SQL",在向量空间里距离近。
用户问"苹果公司最近的财报怎么样",可能召回"苹果种植技术"——Embedding 分不清这个"苹果"是水果还是公司。
用户问"2024 年 AI 大模型的最新进展",可能召回"2023 年的报告"——语义相近,但过时了。
向量检索的任务应该是:找到一批可能相关的文档,别漏掉重要的。
排序和治理的任务则是:从这批文档里找出真正能回答问题的,排出优先级,过滤噪音。
这是两件事,得用不同的方法。
RAG 没有二次确认机会
这和传统搜索不一样。
用 Google 搜索时,你输入 query,看到 10 条结果,会扫一眼标题和摘要,决定点哪个、不点哪个——你有二次确认的机会。
但 RAG 系统里,召回的内容直接拼成 Prompt 喂给大模型。大模型没机会"浏览一下召回结果,挑最相关的看",它只能看到你塞给它的东西。
如果召回的前 3 条里,2 条是噪音,1 条相关,大模型会怎么回答?
它会倾向于"综合"所有召回的内容,给出一个四不像的答案,或者被噪音带偏。
所以召回后的治理比召回本身更重要。召回是"把东西拿进来",治理是"把东西挑干净、排好序"。
多轮对话:噪音累积
多轮对话里,问题更明显。
每一轮对话,新召回的内容都会注入上下文。如果不做治理,噪音会不断累积,最终淹没有用的信息。
这个问题在检索层面无解——召回再多相关文档,不做排序、过滤、上下文管理,结果只会越来越差。
二、两阶段检索:从粗筛到精排
为什么向量相似度不够?
向量检索的排序依据是全局 语义相似度 ,不是"对回答这个问题有帮助"。
比如用户问:“Python 里如何实现多线程?”
- 文档 A:“Python 的 GIL 机制导致多线程无法真正并行,这是 Python 的设计缺陷。”
- 文档 B:“Python 提供了 threading 模块来实现多线程编程,常用方法包括 Thread、Lock、Queue。”
从语义相似度看,文档 A 可能和"多线程"、"Python"这些关键词更匹配。
但从"回答问题"的角度,文档 B 才是真正有用的。
向量检索不知道用户想问什么,只知道文档大概说了什么。
两阶段架构
既然向量检索是"粗筛",那就需要一个"精排"环节。
Query → 向量召回(Top-100)→ 重排模型(Top-10)→ 大模型生成答案
第一阶段:向量召回(Bi-Encoder)
- 用 Bi-Encoder 把 Query 和文档分别编码成向量
- 在向量空间里做最近邻搜索(ANN)
- 速度快,适合大规模召回
- 目标:别漏掉相关文档
第二阶段:重排(Rerank)
- 用更精准的模型对候选集重新排序
- Query 和文档一起输入,考虑交互
- 目标:把最有用的文档排到最前面
这个架构的核心就一点:召回追求"全",排序追求"准"。
Bi-Encoder vs Cross-Encoder
重排模型为什么比向量检索准?区别在架构。
Bi-Encoder(双编码器):
Query → [Encoder] → 向量 Q ─┐
├→ 余弦相似度 → 得分
Doc → [Encoder] → 向量 D ─┘
- Query 和文档分别通过同一个 Encoder,独立编码成向量
- 相似度计算在编码之后,用余弦相似度或点积比较
- 优势:文档向量可以预先计算并建索引,查询时只需编码 Query,速度极快
- 劣势:Query 和文档在编码阶段没有交互,无法捕捉细粒度的语义关联
Cross-Encoder(交叉编码器):
Query + Doc → [拼接] → [Transformer Encoder] → 相关性分数
- Query 和文档拼接后一起输入,在 Transformer 每一层通过 Self-Attention 交互
- 相似度计算在编码过程中完成,模型能看到完整上下文
- 优势:能捕捉词与词之间的精细关联,判断更准确
- 劣势:每对 Query-Doc 都要过一次完整模型,无法预计算,速度慢
直观类比:
- Bi-Encoder 像"相亲看简历":先把每个人写成简历(编码),然后比较匹配度。效率高,但简历可能丢失细节。
- Cross-Encoder 像"相亲看真人":让两个人直接见面聊天,当场判断。更准,但没法批量处理。
为什么 Self-Attention 让 Cross-Encoder 更准?
在 Transformer 的每一层,Self-Attention 让每个 token 都能"看到"其他所有 token。这意味着:
- Query 里的"苹果",能注意到 Doc 里的"公司"、“财报”,从而判断是苹果公司而非水果
- Query 里的"优化",能关注到 Doc 里的"性能提升 30%“,而不是泛泛而谈的"优化方法”
这种细粒度的交互,Bi-Encoder 分别编码后做向量点积做不到。
| 维度 | Bi-Encoder | Cross-Encoder |
|---|---|---|
| 速度 | 快(可预计算) | 慢(每对单独过模型) |
| 精度 | 中等 | 高 |
| 适合规模 | 百万/千万级文档 | 百/千级候选集 |
| 典型用途 | 第一阶段召回 | 第二阶段精排 |
效果数据
在一个面向技术文档的 RAG 系统里,研究者做了个实验:
| 方案 | Top-1 准确率 | Top-3 准确率 | Top-5 准确率 |
|---|---|---|---|
| 仅向量召回 | 45% | 62% | 73% |
| 向量召回 + Cross-Encoder Rerank | 78% | 89% | 93% |
加入 Rerank 后,Top-1 准确率从 45% 提升到 78%。
原来问 10 个问题只能准确回答 4.5 个,现在能回答 7.8 个。
主流 Rerank 模型
| 模型 | 类型 | 语言支持 | 优势 | 适用场景 |
|---|---|---|---|---|
| BGE-Reranker | Cross-Encoder | 中英 | 开源免费,中文优化 | 通用场景,预算有限 |
| Cohere Rerank | API | 多语言 | 精度高,开箱即用 | 英文为主,追求效果 |
| Jina Reranker | Cross-Encoder | 多语言 | 长文档友好,支持 8K 上下文 | 技术文档、法律文档 |
| bge-reranker-v2 | Cross-Encoder | 中英 | 轻量级,延迟低 | 对延迟敏感的场景 |
| LLM Rerank | 大模型 | 任意 | 推理能力强,可解释 | 高价值场景,复杂 query |
选型建议:
- 中文场景:BGE-Reranker 系列(bge-reranker-large / v2)
- 英文场景:Cohere Rerank 或 Jina
- 长文档:Jina Reranker(支持 8K 上下文)
- 高价值场景:LLM Rerank(法律、医疗、金融)
LLM Rerank
除了 Cross-Encoder,还可以让大模型直接判断文档和 Query 的相关性。
优势:
- 能理解复杂的语义关系
- 能做多跳推理
- 不需要额外训练 Rerank 模型
劣势:
- 慢:每条文档都要过一次大模型
- 贵:API 调用成本是 Cross-Encoder 的 10-100 倍
实战用法:先用 Cross-Encoder 过滤到 Top-3,再用 LLM Rerank 做最终排序。这样既能用上大模型的推理能力,又能控制成本。
参数建议
召回阶段:Top-50 到 Top-100
- 太少可能漏掉长尾 Query
- 太多会增加 Rerank 的延迟和成本
- Top-50 比较平衡
Rerank 阶段:Top-5 到 Top-10
- 重排后的结果送入大模型作为上下文
- 5-10 条足够,太多会稀释关键信息
延迟预算:
- 向量召回:10-50ms
- Cross-Encoder Rerank:50-200ms
- 大模型生成:1-10s
总延迟控制在 200-300ms 以内,用户体验会比较流畅。
混合检索
单一向量检索有局限,可以组合多种方式:
Query
├── BM25 召回(关键词匹配)→ Top-20
├── 向量召回(语义匹配) → Top-20
└── 知识图谱召回(实体关联)→ Top-10
↓
融合 + 去重 → Top-50
↓
Rerank(精排) → Top-5
↓
大模型生成答案
BM25 负责精确匹配,向量检索负责语义扩展,知识图谱负责关联推理。三者互补,最后统一用 Rerank 做最终排序。
三、召回后治理:7 个处理环节
3.1 去重
从多个渠道召回文档时,第一件事是去重。
为什么需要去重:
- 同一篇文章的不同 URL(官网、备份站、转载站)
- 同一个文档的不同版本(v1.0、v2.0、v3.0)
- 内容高度相似的多篇文档
不去重的话,大模型会看到重复内容,可能重复引用同一个观点,或者浪费上下文窗口。
常用算法:
-
SimHash:Google 提出的局部敏感哈希算法。文档生成 64 位指纹,汉明距离小于 3 视为相似。
-
MinHash:适合计算 Jaccard 相似度。
-
Embedding 相似度:用 Embedding 向量计算余弦相似度,阈值 0.95 以上视为重复。
做法:
- 第一层:URL 去重(exact match)
- 第二层:SimHash 快速过滤
- 第三层:Embedding 相似度精细筛选
三层去重后,通常能减少 30-50% 的冗余。
3.2 上下文压缩
去重之后是压缩。
大模型的上下文窗口有限。召回 10 条文档,每条 5000 字,加起来 5 万字,但大模型可能只能处理 8000 字。
得选择保留什么、丢弃什么。
截断策略:
-
固定截断:直接取前 N 个字。简单,但可能截掉关键信息。
-
语义切分:用模型判断文档的自然段落或句子边界,在语义完整的地方切分。
-
滑动窗口:按固定步长(如 500 字)滑动,每个窗口保留重叠(如 100 字)。关键信息不会被完全切掉。
-
关键信息提取:用 LLM 或提取模型,从长文档里提取"核心观点"、“关键数据”、“结论”。
经验:
- 文档结构清晰(标题、段落):用语义切分
- 长篇大论:用滑动窗口 + 重叠
- 精度要求高:用关键信息提取(成本更高)
3.3 时效性加权
信息有"保质期"。
- 用户问"最新发布的 iPhone 怎么样",召回一篇 2022 年的评测,不合适
- 用户查"2024 年 AI 大模型排名",给一篇 2023 年的报告,准确度打折扣
时间衰减函数:
score = base_score * e^(-λ * days_since_publish)
- λ 越大,旧文档衰减越快
- 新闻类 λ=0.1(半年衰减 80%),知识类 λ=0.01(一年衰减不到 4%)
做法:
- 在向量数据库里给文档加
publish_time字段 - 召回后用时间衰减函数重新加权
- 时效性敏感的场景(新闻、行情、政策),强制过滤掉 N 天前的文档
3.4 多样化排序
向量检索和 Rerank 都倾向于召回"最相似的",但最相似的往往也是最同质化的。
比如用户问"Python 和 JavaScript 的区别",可能召回:
- 文档 1:Python 基础语法
- 文档 2:Python 高级特性
- 文档 3:Python 与 Java 对比
- 文档 4:Python Web 开发
Top-4 全是 Python,JavaScript 被淹没了。
解决方案:MMR(Maximal Marginal Relevance)
MMR 在相关性和多样性之间做平衡:
MMR = α * relevance(query, doc) - (1-α) * max_similarity(doc, selected_docs)
- α 越大,越偏向相关性
- α 越小,越偏向多样性
做法:
- 第一遍:按相关性排序,选出 Top-20
- 第二遍:用 MMR 重排,保证 Top-10 里至少有 3 个不同主题
3.5 上下文窗口管理
多轮对话 RAG 的核心挑战。
每一轮对话,大模型看到的上下文包括:
- 系统提示词(System Prompt)
- 历史对话记录
- 本轮召回的文档
GPT-4 Turbo 是 128K tokens,Claude 3.5 是 200K tokens——看起来大,但实际上:
- 系统提示词占 2-3K
- 历史对话(10 轮)占 5-10K
- 召回文档占 50-80K
一不小心就满了。
治理策略:
-
选择性遗忘:只保留最近 N 轮对话
-
摘要压缩:把历史对话压缩成摘要
-
按需加载:只在当前 Query 涉及历史内容时才加载
-
Token 预算分配:给对话历史、召回文档、系统提示分配固定预算
核心:只保留对当前问题有用的上下文。
3.6 置信度判断
不是所有问题都能回答。
- 用户问的内容超出知识库范围
- 召回的文档相关度都很低
- 用户问题太模糊
这时候硬要回答,大模型会"编"答案,这就是幻觉(Hallucination)。
置信度判断方案:
-
召回相关性阈值:Top-5 文档平均相关性低于 0.5,触发拒答或反问
-
LLM 自我评估:让大模型在生成答案前评估"我有足够信心吗?"
-
一致性检测:用多个不同 Prompt 生成答案,结果不一致说明置信度低
拒答策略:
- 保守:直接回复"抱歉,这个问题我无法从知识库中找到答案"
- 中间:只回答确信的部分,不确定的承认不知道
- 激进:告诉用户"知识库里没有直接答案,以下是推测……"
阈值参考:
- 客服场景:0.4-0.5(宁可拒答,不要乱答)
- 内部知识库:0.3-0.4(允许容错)
- 创意生成:0.2-0.3(鼓励发散)
3.7 人工审核
再好的系统也会有漏网之鱼。
- 召回可能漏文档
- Rerank 可能排错顺序
- 置信度可能误判
- 生成可能产生幻觉
人工审核机制:
-
bad case 收集:用户标记"回答不对"时自动记录
-
归因分析:判断是召回、Rerank 还是生成的问题
-
定期 review:每周/每月 review 一批 bad case,找出系统性问题
-
持续优化:
- 召回漏了 → 补充知识库或调整 Chunk 策略
- Rerank 排错 → 调整模型权重
- 幻觉 → 调低置信度阈值或增强 prompt
案例:某公司 RAG 系统上线后满意度 65%,建立了"答案评审"机制(用户点赞/踩,踩的进入审核池,每周 review 20 个 bad case),三个月后满意度升到 88%。
四、落地实践
参数速查表
| 环节 | 推荐配置 | 备注 |
|---|---|---|
| 召回 Top-K | 50-100 | 长尾 Query 取大值 |
| Rerank Top-K | 5-10 | 超过 10 条收益递减 |
| 去重阈值 | 0.95 | Embedding 余弦相似度 |
| 置信度阈值 | 0.4-0.5 | 根据业务调整 |
| 时间衰减 λ | 0.01-0.1 | 新闻类取大值,知识类取小值 |
| MMR α | 0.7-0.9 | 平衡相关性与多样性 |
| 延迟预算 | 200-300ms | 不含大模型生成 |
案例一:电商客服 RAG
背景:
- 日均咨询 10 万+
- 主要问题:退换货政策、订单查询、促销活动
- 痛点:政策频繁更新,旧答案误导用户
问题:
- 召回率 92%,答案准确率只有 68%
- 用户投诉:按系统回答操作,实际政策已变更
- bad case 分析:70% 时效性问题,20% 重复内容
改进:
第一步:时效性治理
- 给政策文档加
effective_date和expire_date字段 - 召回后强制过滤过期文档
- 时间衰减 λ 设为 0.2
第二步:去重优化
- 发现同一政策有"官网版"、“APP 版”、"客服版"三个版本
- 建立"唯一可信源",只保留官方最新版
- 去重阈值从 0.95 调到 0.9
第三步:置信度 + 人工审核
- 置信度阈值 0.45,低于此值转人工
- 新政策发布后,旧答案自动标记"待审核"
效果:
- 3 个月后,准确率从 68% 提升到 89%
- 投诉下降 76%
- 人工介入率从 35% 降到 18%
案例二:技术文档 RAG
背景:
- API 文档平台,50 万+ 技术文档
- 痛点:代码示例被切分,召回后无法理解
问题:
- 用户问"如何调用 XX 接口",召回的代码缺少上下文
- Chunk 策略是固定 512 tokens,代码被拦腰斩断
- 多轮对话下,历史内容堆积,新内容被稀释
改进:
第一步:Chunk 策略重构
- 代码文档:按函数/类切分,保证每个 Chunk 完整
- API 文档:按标题层级切分,每个 endpoint 独立
- 增加"代码上下文"字段:附带文件路径、导入语句
第二步:混合检索
- BM25:精确匹配 API 名称、参数名
- 向量:语义匹配功能描述
- 融合:BM25 前 20 + 向量前 30 → 去重 → Rerank
第三步:上下文压缩
- 多轮对话用摘要压缩历史
- 只保留最近 3 轮完整内容
效果:
- 代码问题准确率从 52% 提升到 81%
- 平均对话轮次从 4.2 降到 2.8
- NPS 从 23 提升到 41
五、常见误区
误区一:只盯着召回率
Recall@K 只关心"有没有召回相关文档",不关心"排序对不对"。
Recall@10 是 90%,意味着 10 条里召回 9 条相关。但如果排在前面的 9 条都是噪音,只有最后 1 条相关,回答质量照样差。
除了 Recall@K,还要看:
- Hit@K:Top-K 里有多少次真正命中正确答案
- MRR:第一个相关文档出现的位置,越靠前越好
- NDCG:综合考虑相关性和排序位置
误区二:RAG = 向量数据库
很多团队觉得 RAG 就是"搭个向量 数据库 ,接上大模型"。
RAG 的完整流程:
文档 → Chunk → Embedding → 向量索引 → 检索 → 排序 → 上下文压缩 → 生成答案
向量检索只是其中一步。Chunk、Embedding、排序、压缩、生成,每个环节都会影响最终效果。
误区三:Chunk 策略随便搞
很多人用"512 tokens 切一刀"的固定策略。
但这样会切坏语义。一段话被拦腰斩断,召回的内容可能不完整。
代码文档更明显。一段代码被切成三段,用户问"参数校验逻辑是什么",召回了第二段,上下文没了,看不懂。
正确做法:
- 结构化文档(技术文档、API 文档):按标题层级切
- 代码文档:按函数/类切
- 纯文本:按句子或段落切,用滑动窗口保留重叠
误区四:Embedding 模型乱选
Embedding 是向量检索的"原材料",选错了后续优化都是事倍功半。
常见问题是盲目追求"最新最大"。OpenAI 的 text-embedding-3-large(3072 维)确实强,但在中文垂直领域,可能不如专门的模型(如 BGE、M3E)。
选型参考:
| 场景 | 推荐模型 |
|---|---|
| 通用中文 | BGE-large-zh |
| 英文为主 | text-embedding-3-large |
| 代码检索 | CodeBERT |
| 跨语言 | text-embedding-3-large、LabSE |
| 垂直领域 | 领域微调的 BGE |
选好模型后,在自己的数据集上做离线评测,别只看官方榜单。
误区五:知识库不更新
知识库会"过期"。
- 产品文档更新了,向量数据库里还是旧版本
- 政策文件替换了,旧版本还在被检索
- 新闻过时了,依然被当作有效信息召回
做法:
- 给文档加时间戳(创建时间、更新时间)
- 超过 N 天的文档自动降权或下架
- 知识库变更时,触发向量索引增量更新
- 强时效性内容(新闻、促销、政策)单独建索引
误区六:不上监控
RAG 系统上线后,最大的风险是"不知道出了问题"。
常见问题:
- Embedding 模型更新了,向量索引没重建,召回质量下跌,没人发现
- 大模型版本升级,回答风格变了,原来的 prompt 不适用了,没人知道
- 知识库被污染,低质量文档混入,召回质量下降
监控体系:
- 召回指标监控:每小时计算 Recall@K、Hit@K,下跌超过 10% 报警
- 回答质量抽检:每天随机抽样 100 个回答,人工评估
- Bad case 告警:用户踩了自动记录并触发审核
- 端到端监控:模拟真实用户 query,定期跑自动化测试
六、总结
回到开篇的问题:为什么召回率很高,大模型还是答错?
因为真正的挑战不在检索,在召回后的治理。
几点结论:
-
向量检索是"粗筛",任务是别漏掉相关文档。召回率再高,Top-K 里都是噪音,大模型也答不好。
-
Rerank 是必要的。两阶段检索(向量召回 + Rerank 精排)是主流方案,能带来明显提升。
-
召回后处理是系统工程。去重、压缩、时效性加权、多样化排序、上下文管理、置信度判断、人工审核,每个环节都重要。
-
避开常见坑:别只盯着召回率,别把 RAG 等同于向量检索,别忽视 Chunk 策略,别不上监控。
如果你正在搭建 RAG 系统,可以按这个顺序推进:
- 第一周:统计当前召回率、准确率,分析 50 个 bad case,确定优先级
- 第二周:上线 Rerank(推荐 BGE-Reranker),配置召回 Top-50 → Rerank Top-5,A/B 测试
- 第三周:加上去重、时效性加权、置信度判断
- 第四周:建立 bad case 收集机制,每周 review,持续优化
检索是基础能力,每家公司都能做。治理才是差异化竞争力。
关于作者:欢迎关注微信公众号「小小寰宇」,获取更多 AI 工程化、大模型落地实战干货。