AI应用高级工程师 · 第 05 / 12 章

检索、混合搜索、Rerank与召回评测

检索不是“向量库返回几个相似片段”就结束。本章把用户问题怎样经过过滤、关键词搜索、向量搜索、结果融合、精排和离线评测完整串起来。

上一章
已经把原始文档加工成带版本、权限和向量的Chunk。
→
本章解决
怎样稳定找到真正能回答问题的片段,并证明检索效果变好了。
→
下一章
找到资料后,让模型安全查询订单、创建工单和执行真实操作。
学完后的结果:你能为企业知识库设计一条可评测的检索链,知道各阶段为什么存在、参数怎样选、错了怎样定位。

本章目录

阅读过程中可按 Ctrl + Z 快速返回当前页面的本章目录;也可以点击右下角按钮。

本章思维导图

知识流向:先完成左侧基础,再进入中间机制,最后用右侧工程控制把它变成可上线能力。节点可直接点击跳转。

MODULE 01

查询理解与范围过滤

按照本模块顺序完成以下知识点,再进入下一模块。

Q01先给当前项目的默认检索方案

退款客服默认:先用tenant_id、权限、status=active和生效时间做确定性过滤;再同时做向量检索和BM25关键词检索;用RRF融合;取约20个候选交给Reranker精排到5个;低于阈值或证据冲突就拒答或转人工。数值只是起点,必须用真实评测调整。

用户问题
→
权限/版本过滤
→
向量+关键词召回
→
融合+Rerank
→
证据或拒答
生活中的例子:像去大型医院看病:先按科室和挂号权限筛选,再由初诊广泛排查,最后让专家精确判断。

Q02查询理解与安全改写

后台检索步骤 查询改写是程序调用规则或模型,把口语问题变成更适合检索的表达。例如“钱咋还没回来”可补成“退款到账时间”。输入是原问题和少量对话上下文,输出是检索查询,不是最终答案。

可做

补全对话中的指代、同义词、产品名称;生成关键词查询与语义查询两个版本。

不能做

擅自加入用户没说的订单状态或日期。改写错会让后面搜索得再准也找错方向。

用户:它什么时候回来?
对话已确认“它”指退款款项
检索查询:退款审核通过后多久到账
生活中的例子:顾客对前台说“那个钱怎么还没来”,前台结合上一句知道“那个钱”指退款,再把它写成清楚的查询单。

Q03Metadata 预过滤

过滤条件来自登录身份、租户、角色、当前时间和发布系统,不由LLM猜。它先缩小允许搜索的集合,既防止越权,也避免旧政策占据Top-k。

filter = {
 tenant_id: 当前登录公司的ID,
 access_groups: 包含当前角色,
 status: "active",
 effective_at: 小于等于当前时间,
 expires_at: 不存在或大于当前时间
}

错误后果:先全库向量搜索、取回文本后才检查权限,敏感内容可能已经进入日志、Prompt或模型服务,属于数据泄露,不只是“结果不好”。

生活中的例子:像公司档案室先刷门禁卡:没有权限的柜子根本不进入搜索范围,而不是拿出文件后才说不能看。
MODULE 02

多路召回与排序

按照本模块顺序完成以下知识点,再进入下一模块。

Q04向量检索、BM25 与混合检索

Dense Retrieval

稠密向量检索算法

用Embedding比较语义,适合“退钱”和“退款”这种不同词同意思;弱点是精确编号、罕见缩写可能不敏感。

BM25

关键词相关性算法

根据词是否出现、出现频率和词的稀有程度打分。适合订单编号、产品型号、条款名;不擅长完全不同说法的同义表达。

Hybrid Search(混合检索)就是同时拿两路候选再融合。不是把两种分数直接相加,因为分数量纲通常不同。

生活中的例子:图书管理员一人按书名关键词找书,另一人按“这本书大概讲什么”找书,两份结果合起来更不容易漏。

Q05RRF 排名融合

排名融合算法 RRF是Reciprocal Rank Fusion,中文可理解为“倒数排名融合”。它更看重候选在每一路排第几,而不是原始分数是多少,因此适合融合BM25与向量检索。

向量排名:A第1,B第2,C第3
关键词排名:B第1,D第2,A第3

RRF后:A和B因两路都靠前而优先;D和C作为单路候选保留

程序执行RRF,输入是多份有序ID列表,输出是融合排名。失败常见原因是每路召回太少、文档ID无法对齐,或过滤条件不一致。

生活中的例子:像两位评委各自排名,RRF更看候选在两张榜单上的名次,而不是比较两位评委完全不同的打分尺度。

Q06Rerank 到底怎样做:模型、输入、打分与重排

第二阶段相关性排序模型 Rerank(重新排序)通常确实要调用一个模型。它不是 Milvus 自动完成的动作,也不是把向量相似度再排序一次。后端先从向量检索、BM25 和 RRF 得到约 20 个候选,再把“同一个完整问题 + 每个候选片段”交给专用 Reranker,得到新的相关性分数。

退款客服默认方案:先用托管的多语言 Rerank API 快速上线;如果政策文本不能出网,再在内网部署多语言 Cross-Encoder,例如 BAAI/bge-reranker-v2-m3。输入先取约 20 个候选,精排后保留约 5 个;20、5 和阈值都必须通过真实评测调整。

1. 最常用的 Reranker 是什么模型

Cross-Encoder(交叉编码器)

专用排序模型 把问题和一段候选文字拼成一对,一起送进 Transformer。问题里的词能直接与文档里的词互相注意,最后输出一个相关性分数。这是本章默认所说的 Reranker。

Embedding 双编码器

第一阶段召回模型 问题和文档分别提前编码成向量,再比较距离。速度快、可以搜索海量文档,但问题与文档在编码时没有逐词共同阅读,所以细节判断通常较粗。

生活中的例子:Embedding 像先分别给“求职者”和“岗位”贴标签,再快速筛出20份简历;Cross-Encoder 像面试官拿着同一份岗位要求逐份阅读这20份简历,重新排出前5名。

2. 一次 Rerank 的完整过程

取得候选:向量检索、BM25 和 RRF 返回约20条记录,每条至少有 chunk_id 与正文 text。
组成文本对:程序为每个候选构造 [用户完整问题, 候选片段]。不是只给模型一个向量,也不是把20段一次塞给聊天模型随意评判。
分词并共同阅读:Reranker 的 tokenizer 把问题和片段转成模型输入;Cross-Encoder 让两段文字在同一次前向计算中发生交互。
输出分数:每个“问题—片段”对得到一个 logit 或 relevance score。分数越大通常越相关,但不同模型的分数范围不能横向照搬。
程序排序:后端把分数写回对应的 chunk_id,从高到低排序,保留前5条。
进入安全检查:程序再做去重、阈值、版本冲突和证据充分性判断,最后才把少量证据放进 LLM Prompt。
RRF候选20条
→
20个
问题+片段对
→
Reranker
逐对打分
→
按分数降序
→
前5条证据

3. 用退款问题看见“重排”发生了什么

用户问题:“退款审核已经通过,钱什么时候回来?”

RRF初排候选片段Rerank示意分精排后为什么
1签收七日内可以提交退货申请0.313同属退款,但回答的是申请期限
2退款审核通过后,三个工作日内原路退回0.961同时匹配“审核通过”和“何时到账”
3退款失败时请联系人工客服0.184用户没有说退款失败
4银行卡退款可能受银行处理进度影响0.722可作为到账时间的补充证据

这些分数只是演示,不是固定阈值。0.96 不能解释成“96%保证正确”。Reranker 的分数要在自己的评测集上观察和校准。

4. 可选模型与各自优缺点

方案例子怎样使用优点缺点
托管 Rerank APICohere rerank-v3.5、Voyage rerank-2.5 等后端把 query 与 documents 列表发给独立 Rerank 接口接入快,不管理GPU;通常直接返回排序下标和相关性分文本可能出网、按量费用、网络延迟和限流;型号会更新
本地 Cross-EncoderBAAI/bge-reranker-v2-m3 等多语言模型下载权重,在自己的CPU/GPU或推理服务运行数据留在内网,可固定版本需要负责模型加载、显存、批处理、并发、监控和升级
聊天大模型打分让LLM比较候选或输出排名Prompt中放问题和候选,要求模型判断规则灵活,可解释复杂偏好慢、贵、输出波动和Prompt注入风险更高;不作为普通RAG默认方案

5. API 调用时,数据放在哪里

# Cohere风格的真实调用结构;具体SDK和模型名以后以官方文档为准
import cohere

co = cohere.ClientV2(api_key=COHERE_API_KEY)
documents = [item["text"] for item in candidates]

response = co.rerank(
    model="rerank-v3.5",
    query="退款审核已经通过,钱什么时候回来?",
    documents=documents,
    top_n=5,
)

# result.index 指向原 candidates 中的下标
ranked = [
    {
        **candidates[result.index],
        "rerank_score": result.relevance_score,
    }
    for result in response.results
]

API请求里的位置:model 选择排序模型,query 是完整检索问题,documents 是第一阶段召回的候选正文,top_n 是最多返回几个。API输出不是最终答案,而是候选下标和相关性分数。

6. 本地模型怎样做同一件事

from FlagEmbedding import FlagReranker

# 有兼容GPU时可评估 use_fp16=True;CPU先使用False
reranker = FlagReranker(
    "BAAI/bge-reranker-v2-m3",
    use_fp16=False,
)

query = "退款审核已经通过,钱什么时候回来?"
pairs = [[query, item["text"]] for item in candidates]

scores = reranker.compute_score(pairs, normalize=True)

ranked = sorted(
    [
        {**item, "rerank_score": score}
        for item, score in zip(candidates, scores)
    ],
    key=lambda item: item["rerank_score"],
    reverse=True,
)[:5]

代码发生的位置:这段逻辑运行在AI应用后端的检索服务里,顺序在RRF之后、把资料交给聊天模型之前。输入是文本对,输出是一组分数;排序动作由普通程序代码完成。

7. 为什么不能把候选从20无限提高到200

Cross-Encoder要对每个候选分别运行一次“问题+片段”联合计算。候选越多,模型计算、Token、延迟和费用大致都会增加。通常让便宜的召回阶段负责“别漏掉”,让昂贵的Reranker只负责“小范围排好顺序”。

失败边界:Rerank只能重排已经召回的候选。如果正确片段没进前20,它无法凭空找回来;候选文本被错误截断、语言不适配、模型版本变化,也会导致精排变差。因此先看 Recall@20,再看 nDCG、MRR 和最终答案质量。

生活中的例子:海选让20人进入决赛,Reranker是决赛评委。评委能仔细重排这20人,但不能从没报名或海选漏掉的人里找回冠军。

Q07Top-k、阈值与无答案策略

Top-k是最多取几个候选,不代表这几个都正确。k太小容易漏,太大增加延迟、重复和Prompt噪声。阈值是低于某个检索或精排分数时不采用,但不同模型、语料的分数不可照抄。

先准备真实问题和标准相关片段。
观察不同k值下正确片段是否进入候选。
观察低分区间的误召回,选择拒答阈值。
上线后监控无答案率、误答率和人工转接率。
生活中的例子:Top-k像渔网开口:太小会漏鱼,太大把水草也捞上来;阈值像最小合格尺寸。
MODULE 03

检索评测与验证

按照本模块顺序完成以下知识点,再进入下一模块。

Q08召回评测指标从零解释

指标小白解释退款案例
Recall@k前k个候选里有没有把应该找到的资料找回来100个问题中,前5命中正确条款92个,Recall@5约92%
Precision@k返回的k个里有多少真相关返回5段,只有2段相关,精度偏低
MRR第一个正确答案排得越靠前越好正确条款经常第1名,MRR较高
nDCG考虑多个结果的相关程度及排名位置高度相关条款排前,弱相关补充排后

这些指标用人工标注或可信业务规则产生的标准答案计算,不让被评测的同一个系统自己宣布成功。

生活中的例子:Recall@5像问“正确资料是否进入前5名”;MRR像看“第一个正确结果究竟排第几”。

Q09真实检索评测集怎样建立、运行并证明方案变好了

离线测试数据集 检索评测集不是保存起来给人看的问题清单,而是一批“输入条件 + 人工确认的正确结果”。评测程序会把每条输入重新交给检索系统,再把系统返回的 Chunk 排名与正确结果自动对比。

默认做法:从脱敏后的真实客服问题、历史工单和搜索日志中抽样,由熟悉退款政策的人标注正确 Chunk;另外补充新政策、错别字、越权、无答案等高风险案例。固定一份测试集,同时运行旧方案和新方案,比较同一批问题的指标与错误明细。不能只展示几个“看起来成功”的例子。

1. 每条记录里的字段到底给谁用

字段记录什么评测时怎么用缺少会怎样
case_id案例唯一编号定位失败、关联日志、修复后回归同一句话多次出现时难以追踪
query用户真实问法原样送入被评测的检索链系统没有测试输入
conversation理解指代所必需的少量历史测试查询改写,例如“它多久回来”中的“它”无法公平复现多轮问题
tenant_id / role当时的租户和权限生成与线上一致的 metadata 过滤;检查越权资料是否被返回可能把越权命中误判成检索成功
as_of问题发生的业务时间决定当时哪一版政策有效新旧政策没有统一判断基准
expected_chunk_ids专家确认必须或可以命中的正确片段与系统返回的 ranked Chunk IDs 比较,计算 Recall、MRR、nDCG没有“标准答案”,无法自动评分
acceptable_alternatives同样能够回答问题的替代片段避免只认一个 ID,把同义有效证据错判为失败指标会低估真实效果
should_answer这个问题是否应该从知识库得到答案测试系统应回答还是拒答;越权和库外问题通常为 false系统乱答也可能被算成“有结果”
tags错别字、新政策、短问句、越权等类别分组统计,发现总体分数掩盖的局部退化只知道总分变化,不知道哪类用户变差

2. 一条可执行的评测记录

{
  "case_id": "refund-arrival-001",
  "query": "钱咋还没回来",
  "conversation": [
    {"role": "user", "content": "退款已经审核通过了"}
  ],
  "tenant_id": "company-a",
  "role": "customer-service",
  "as_of": "2026-09-15T10:00:00+08:00",
  "expected_chunk_ids": ["refund-v4-arrival-time"],
  "acceptable_alternatives": ["refund-v4-bank-delay"],
  "should_answer": true,
  "tags": ["口语", "短问句", "退款到账"]
}

这里的 expected_chunk_ids 不是模型生成的,而是业务专家阅读政策后确认的 Ground Truth(人工可信标准)。如果政策更新导致正确答案变化,评测集也要经过评审后更新版本,不能悄悄改答案来迎合新系统。

生活中的例子:评测记录像驾照考试题:题目、当时交通条件和标准答案都提前确定。考生答完后才能客观判分,不能让考生自己宣布标准答案。

3. “真实评测集”从哪里来

真实分布:从线上搜索日志、客服工单、用户反馈中脱敏抽样,保留真实口语、缩写、错别字和短问句。
专家标注:让懂业务规则的人查阅当时有效政策,标注正确 Chunk、可接受替代证据和是否应该回答。
主动补难例:增加新旧政策冲突、多租户同名政策、越权请求、知识库没有答案、编号和极短查询。
双人复核:高风险案例最好由第二位标注者复核;有分歧时先统一业务标准。
冻结测试集:调参数用开发集,最终报告用没有参与调参的测试集,避免把参数“背”在测试题上。

“真实”不等于只复制生产日志:生产日志能代表常见分布,但罕见的越权和旧政策事故可能样本很少,必须主动补充。纯让LLM生成的问题也不能单独代表真实用户。

4. 评测程序实际怎样跑

读取一条案例
→
按租户/权限/时间
运行完整检索链
→
得到有序
Chunk IDs
→
与 Ground Truth
逐条比较
→
汇总指标和失败清单
def evaluate(retrieve, cases, k=5):
    rows = []

    for case in cases:
        results = retrieve(
            query=case["query"],
            conversation=case.get("conversation", []),
            tenant_id=case["tenant_id"],
            role=case["role"],
            as_of=case["as_of"],
            top_k=k,
        )

        returned_ids = [item["chunk_id"] for item in results]
        relevant_ids = set(
            case["expected_chunk_ids"]
            + case.get("acceptable_alternatives", [])
        )

        ranks = [
            index + 1
            for index, chunk_id in enumerate(returned_ids)
            if chunk_id in relevant_ids
        ]

        hit_at_k = 1 if ranks else 0
        reciprocal_rank = 1 / min(ranks) if ranks else 0

        rows.append({
            "case_id": case["case_id"],
            "returned_ids": returned_ids,
            "hit_at_k": hit_at_k,
            "reciprocal_rank": reciprocal_rank,
            "tags": case["tags"],
        })

    recall_at_k = sum(r["hit_at_k"] for r in rows) / len(rows)
    mrr = sum(r["reciprocal_rank"] for r in rows) / len(rows)
    return {"recall_at_k": recall_at_k, "mrr": mrr, "rows": rows}

这段程序并不调用 LLM 来给自己打分。它读取已经人工确认的 Chunk IDs,用普通确定性代码判断正确片段是否进入前 k,以及第一次出现在第几名。这样同一输入反复运行能得到一致的判分规则。

5. 一个案例是怎样算分的

标准相关片段:
["refund-v4-arrival-time", "refund-v4-bank-delay"]

系统返回前5:
1. refund-v4-apply-deadline
2. refund-v4-arrival-time       ← 第一个正确结果
3. refund-v4-contact-service
4. payment-v2-card-binding
5. refund-v3-arrival-time

Hit / Recall@5:命中了正确片段 → 记 1
Reciprocal Rank:第一个正确结果排第2 → 1 / 2 = 0.5
安全检查:第5条是过期v3 → 另记“版本过滤失败”,即使已命中也不能忽略

如果一条问题必须同时找到多个互补片段,Recall@k 应使用“命中的相关片段数量 ÷ 全部必需片段数量”,而不只是有没有命中一个。课程中的单答案案例可以先用 Hit@k 简化理解。

6. 怎样用同一评测集证明新方案更好

假设固定 500 条测试案例,不改问题、不改标准答案,分别运行:

对比项旧方案 A:纯向量 Top-5新方案 B:过滤+混合召回+RRF+Rerank怎样判断
Recall@582.0%91.4%正确证据进入前5的案例增加
MRR0.610.79第一个正确证据整体排得更靠前
旧政策误召回27例2例版本过滤明显改善
越权结果3例0例安全门槛必须为0,不能用平均分抵消
P95检索延迟180ms310ms效果提高但延迟上升,需要确认仍符合SLA
无答案正确拒答率64%88%系统更少在无证据时乱答

“证明变好”不是只看一个总分:新方案应达到预先规定的上线门槛,例如 Recall@5 ≥ 90%、越权命中 = 0、旧政策误召回 ≤ 2、P95 ≤ 400ms,而且关键标签分组不能明显退化。安全指标不允许用其他指标的提升抵消。

生活中的例子:比较两种教学方法,要让两组参加同一套考试并看各题型成绩;不能让旧方法做难题、新方法做简单题,也不能只挑新方法答对的三道题展示。

7. 指标提高后为什么还要看失败清单

总指标只告诉你整体变化,rows 中的失败案例才告诉你该修改哪一段。评测程序应保存每个案例的查询改写、过滤条件、BM25/向量候选、RRF排名、Rerank分数和最终Chunk IDs,再分类定位:

错误类别现象优先修改
查询改写错把“它”理解成错误对象对话截取和改写规则
过滤错正确政策被过滤或旧政策进入metadata、权限、时间条件
召回漏正确片段没有进入Rerank候选切块、Embedding、BM25、候选数
融合错某一路好结果被RRF压低各路候选数和融合策略
Rerank错正确片段已召回却被精排压低Rerank模型、输入长度和top_n
阈值错有证据却拒答,或无证据仍放行用开发集重新选择阈值

防止数据泄漏:某个坏案例加入训练或调参开发集后,不能继续把它当完全独立的最终测试证据。应保留一份未参与调参的测试集,并持续补充新的线上坏案例。

Q10本章完整生产链

原问题+必要历史
→
改写与过滤
→
两路召回
→
融合与精排
→
去重/阈值/冲突检查
→
引用或拒答

记住:Embedding负责扩大语义召回,BM25保护精确词,RRF融合排名,Reranker提高前几名质量,评测集证明系统是否真的改善。

生活中的例子:像快递分拣流水线:地址检查、粗分、合并、精分、异常退回,每一站都有明确职责。

本章总结:从哪里来,解决什么,为什么继续

上一章留下的问题怎样从知识库找到可靠证据
本章形成的能力先过滤,再用语义与关键词多路召回,融合并精排,最后用真实评测集证明结果。
下一章必须继续解决找到了政策后怎样查询实时订单并执行操作

本章参考的权威资料

课程讲的是跨厂商通用工程方法;具体参数会更新,真正开发时再以所选厂商最新官方文档为准。

← 上一章RAG数据工程下一章 →工具调用、Function Calling与MCP
↑ 返回本章目录Ctrl+Z