本章目录
阅读过程中可按 Ctrl + Z 快速返回当前页面的本章目录;也可以点击右下角按钮。
本章思维导图
知识流向:先完成左侧基础,再进入中间机制,最后用右侧工程控制把它变成可上线能力。节点可直接点击跳转。
查询理解与范围过滤
按照本模块顺序完成以下知识点,再进入下一模块。
Q01先给当前项目的默认检索方案
退款客服默认:先用tenant_id、权限、status=active和生效时间做确定性过滤;再同时做向量检索和BM25关键词检索;用RRF融合;取约20个候选交给Reranker精排到5个;低于阈值或证据冲突就拒答或转人工。数值只是起点,必须用真实评测调整。
Q02查询理解与安全改写
后台检索步骤 查询改写是程序调用规则或模型,把口语问题变成更适合检索的表达。例如“钱咋还没回来”可补成“退款到账时间”。输入是原问题和少量对话上下文,输出是检索查询,不是最终答案。
可做
补全对话中的指代、同义词、产品名称;生成关键词查询与语义查询两个版本。
不能做
擅自加入用户没说的订单状态或日期。改写错会让后面搜索得再准也找错方向。
用户:它什么时候回来? 对话已确认“它”指退款款项 检索查询:退款审核通过后多久到账
Q03Metadata 预过滤
过滤条件来自登录身份、租户、角色、当前时间和发布系统,不由LLM猜。它先缩小允许搜索的集合,既防止越权,也避免旧政策占据Top-k。
filter = {
tenant_id: 当前登录公司的ID,
access_groups: 包含当前角色,
status: "active",
effective_at: 小于等于当前时间,
expires_at: 不存在或大于当前时间
}错误后果:先全库向量搜索、取回文本后才检查权限,敏感内容可能已经进入日志、Prompt或模型服务,属于数据泄露,不只是“结果不好”。
多路召回与排序
按照本模块顺序完成以下知识点,再进入下一模块。
Q04向量检索、BM25 与混合检索
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无法对齐,或过滤条件不一致。
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 双编码器
第一阶段召回模型 问题和文档分别提前编码成向量,再比较距离。速度快、可以搜索海量文档,但问题与文档在编码时没有逐词共同阅读,所以细节判断通常较粗。
2. 一次 Rerank 的完整过程
chunk_id 与正文 text。[用户完整问题, 候选片段]。不是只给模型一个向量,也不是把20段一次塞给聊天模型随意评判。chunk_id,从高到低排序,保留前5条。问题+片段对
逐对打分
3. 用退款问题看见“重排”发生了什么
用户问题:“退款审核已经通过,钱什么时候回来?”
| RRF初排 | 候选片段 | Rerank示意分 | 精排后 | 为什么 |
|---|---|---|---|---|
| 1 | 签收七日内可以提交退货申请 | 0.31 | 3 | 同属退款,但回答的是申请期限 |
| 2 | 退款审核通过后,三个工作日内原路退回 | 0.96 | 1 | 同时匹配“审核通过”和“何时到账” |
| 3 | 退款失败时请联系人工客服 | 0.18 | 4 | 用户没有说退款失败 |
| 4 | 银行卡退款可能受银行处理进度影响 | 0.72 | 2 | 可作为到账时间的补充证据 |
这些分数只是演示,不是固定阈值。0.96 不能解释成“96%保证正确”。Reranker 的分数要在自己的评测集上观察和校准。
4. 可选模型与各自优缺点
| 方案 | 例子 | 怎样使用 | 优点 | 缺点 |
|---|---|---|---|---|
| 托管 Rerank API | Cohere rerank-v3.5、Voyage rerank-2.5 等 | 后端把 query 与 documents 列表发给独立 Rerank 接口 | 接入快,不管理GPU;通常直接返回排序下标和相关性分 | 文本可能出网、按量费用、网络延迟和限流;型号会更新 |
| 本地 Cross-Encoder | BAAI/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 和最终答案质量。
Q07Top-k、阈值与无答案策略
Top-k是最多取几个候选,不代表这几个都正确。k太小容易漏,太大增加延迟、重复和Prompt噪声。阈值是低于某个检索或精排分数时不采用,但不同模型、语料的分数不可照抄。
检索评测与验证
按照本模块顺序完成以下知识点,再进入下一模块。
Q08召回评测指标从零解释
| 指标 | 小白解释 | 退款案例 |
|---|---|---|
| Recall@k | 前k个候选里有没有把应该找到的资料找回来 | 100个问题中,前5命中正确条款92个,Recall@5约92% |
| Precision@k | 返回的k个里有多少真相关 | 返回5段,只有2段相关,精度偏低 |
| MRR | 第一个正确答案排得越靠前越好 | 正确条款经常第1名,MRR较高 |
| nDCG | 考虑多个结果的相关程度及排名位置 | 高度相关条款排前,弱相关补充排后 |
这些指标用人工标注或可信业务规则产生的标准答案计算,不让被评测的同一个系统自己宣布成功。
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. “真实评测集”从哪里来
“真实”不等于只复制生产日志:生产日志能代表常见分布,但罕见的越权和旧政策事故可能样本很少,必须主动补充。纯让LLM生成的问题也不能单独代表真实用户。
4. 评测程序实际怎样跑
运行完整检索链
Chunk IDs
逐条比较
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@5 | 82.0% | 91.4% | 正确证据进入前5的案例增加 |
| MRR | 0.61 | 0.79 | 第一个正确证据整体排得更靠前 |
| 旧政策误召回 | 27例 | 2例 | 版本过滤明显改善 |
| 越权结果 | 3例 | 0例 | 安全门槛必须为0,不能用平均分抵消 |
| P95检索延迟 | 180ms | 310ms | 效果提高但延迟上升,需要确认仍符合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提高前几名质量,评测集证明系统是否真的改善。
本章总结:从哪里来,解决什么,为什么继续
本章参考的权威资料
课程讲的是跨厂商通用工程方法;具体参数会更新,真正开发时再以所选厂商最新官方文档为准。