跳转到内容

8.1.5 检索策略

Hybrid Search 与 Rerank 流程图

完成本节后,你将能够:

  • 理解检索策略为什么直接决定 RAG 质量
  • 分清关键词检索、向量检索和混合检索
  • 理解 rerank、查询改写 这些常见增强手段
  • 用一个可运行例子体验混合检索的思路

一、检索不是“只有一个 top-k”

Section titled “一、检索不是“只有一个 top-k””

关键词检索更像“查目录”。

它擅长:

  • 精确术语
  • 产品名
  • 报错码
  • 法条编号

例如用户问:

“错误码 403 是什么?”

这种场景关键词检索往往很强。

向量检索:适合找语义相近内容

Section titled “向量检索:适合找语义相近内容”

向量检索更像“按意思找相似”。

它擅长:

  • 同义表达
  • 改写后的问题
  • 用户口语化提问

例如:

“怎么退课?”

和:

“课程购买后 7 天内可申请退款”

虽然词不一样,但向量检索有机会把它们连起来。


二、为什么很多项目最后都走向混合检索?

Section titled “二、为什么很多项目最后都走向混合检索?”

只用关键词:

  • 容易漏掉语义近但措辞不同的内容

只用向量:

  • 有时会忽略特别关键的专有词

所以很多系统会做:

关键词分数 + 向量分数 = 混合分数

人类找资料时也会这样:

  • 先看有没有明确关键词
  • 再判断是不是在说同一件事

混合检索就是把这两种判断合起来。

关键词检索与向量检索盲区图

检索策略对比图

BM25 是一种经典的关键词排序方法。很多混合检索都会把 BM25 风格的分数和向量相似度结合起来,再交给 rerank 做最终排序。


三、一个最小可运行的混合检索示例

Section titled “三、一个最小可运行的混合检索示例”

下面这个例子里:

  • keyword_score 模拟关键词匹配
  • vector_score 模拟语义相似度
  • 最后把两者做加权组合
import math
import re
from collections import Counter
import numpy as np
docs = [
{
"id": "d1",
"text": "课程购买后 7 天内可申请退款",
"vector": np.array([0.95, 0.10, 0.05])
},
{
"id": "d2",
"text": "完成所有项目并通过测试后可获得证书",
"vector": np.array([0.10, 0.95, 0.10])
},
{
"id": "d3",
"text": "建议先学 Python,再学机器学习和深度学习",
"vector": np.array([0.20, 0.30, 0.95])
}
]
query = "怎么申请退课退款"
query_vector = np.array([0.90, 0.10, 0.10])
def tokenize(text):
words = re.findall(r"[a-zA-Z0-9_]+", text.lower())
cjk_chars = re.findall(r"[\u4e00-\u9fff\u3040-\u30ff]", text)
cjk_bigrams = ["".join(cjk_chars[i:i + 2]) for i in range(len(cjk_chars) - 1)]
return words + cjk_bigrams
def keyword_score(query, text):
q = Counter(tokenize(query))
t = Counter(tokenize(text))
return sum(min(q[k], t[k]) for k in q)
def cosine_similarity(a, b):
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
results = []
for doc in docs:
kw = keyword_score(query, doc["text"])
vec = cosine_similarity(query_vector, doc["vector"])
hybrid = 0.4 * kw + 0.6 * vec
results.append((hybrid, kw, vec, doc["id"], doc["text"]))
for hybrid, kw, vec, doc_id, text in sorted(results, reverse=True):
print(doc_id, "hybrid=", round(hybrid, 4), "kw=", kw, "vec=", round(vec, 4), "->", text)

预期输出:

Terminal window
d1 hybrid= 1.799 kw= 3 vec= 0.9983 -> 课程购买后 7 天内可申请退款
d3 hybrid= 0.1977 kw= 0 vec= 0.3295 -> 建议先学 Python,再学机器学习和深度学习
d2 hybrid= 0.1337 kw= 0 vec= 0.2228 -> 完成所有项目并通过测试后可获得证书

这个例子虽然简化,但已经很接近真实系统的核心思路。


四、Rerank:先粗召回,再精排序

Section titled “四、Rerank:先粗召回,再精排序”

很多系统不会一开始就追求“第一次就排准”,而是:

  1. 先用较便宜的方法召回一批候选
  2. 再用更强但更贵的方法重排

这就叫 rerank。

像找工作时:

  • 第一轮按关键词筛简历
  • 第二轮再认真看候选人是否真的合适

RAG 也是一样。


五、查询改写:用户问题往往不够“适合检索”

Section titled “五、查询改写:用户问题往往不够“适合检索””

用户可能会说:

“我这个情况还能退吗?”

但知识库里写的是:

“购买后 7 天内且学习进度低于 20% 可退款”

这时系统常常会先把问题改写得更适合检索。

def rewrite_query(query):
rewrite_rules = {
"怎么退课": "退款政策 课程取消",
"退掉课程": "退款政策 课程取消",
"我这个情况还能退吗": "退款条件 购买时间 学习进度",
"我想拿证": "证书要求 完成项目 通过测试",
"毕业证": "证书要求 完成项目 通过测试",
}
for phrase, retrieval_query in rewrite_rules.items():
if phrase in query:
return retrieval_query
return query
queries = ["怎么退课", "我想拿证", "我这个情况还能退吗"]
for q in queries:
print(q, "->", rewrite_query(q))

预期输出:

Terminal window
怎么退课 -> 退款政策 课程取消
我想拿证 -> 证书要求 完成项目 通过测试
我这个情况还能退吗 -> 退款条件 购买时间 学习进度

注意,改写后的查询不一定要是一句漂亮的自然语言。它的任务是变成更适合检索的关键词组合。

真实系统里,query rewrite 可能由 LLM 来完成。

查询改写 与 Rerank 双阶段漏斗图


六、还有哪些常见检索增强策略?

Section titled “六、还有哪些常见检索增强策略?”

把一个问题改写成多个等价问法,再分别检索,合并结果。

先按业务条件缩小范围,再做语义检索。

父子块检索(Parent-child retrieval)

Section titled “父子块检索(Parent-child retrieval)”

先检索小 chunk,再回到更大块或原文段落。

自查询检索(Self-查询 retrieval)

Section titled “自查询检索(Self-查询 retrieval)”

让模型自动判断需要哪些过滤条件和检索字段。


更要重视:

  • 关键词检索
  • 混合检索
  • 元数据过滤

更要重视:

  • 向量检索
  • 查询改写
  • rerank

可以考虑:

  • 先路由
  • 再定向检索
  • 最后重排

八、如果你的目标是“知识库驱动的 SOP 文档助手”,检索策略该怎么想?

Section titled “八、如果你的目标是“知识库驱动的 SOP 文档助手”,检索策略该怎么想?”

这类项目里,检索不只是“找到相关内容”, 而是更像在做两层选择:

  1. 先决定从内部资料找,还是从外部资料补
  2. 再决定要找政策、处理案例,还是复核清单

所以很适合先把检索条件写成这种样子:

条件它在帮你控制什么
topic当前主题
content_type政策 / 案例 / 清单
source_origin内部资料 / 外部资料
team支持团队或适用对象

你可以先把这条线记成一句话:

SOP 文档项目里的检索,不只是“找相关”,而是“按栏目找对证据”。

一个最小过滤示例可以先写成:

items = [
{"topic": "退款升级", "content_type": "policy", "source_origin": "internal", "text": "重复扣费退款必须带交易证据升级处理。"},
{"topic": "退款升级", "content_type": "case", "source_origin": "internal", "text": "客服核对失败结账后的两笔成功扣款,并将案例升级给 billing。"},
{"topic": "退款升级", "content_type": "note", "source_origin": "external", "text": "外部资料补充:支付渠道可能显示临时授权占用。"},
]
hits = [
x for x in items
if x["topic"] == "退款升级" and x["content_type"] in {"policy", "case"}
]
for hit in hits:
print(hit)

预期输出:

Terminal window
{'topic': '退款升级', 'content_type': 'policy', 'source_origin': 'internal', 'text': '重复扣费退款必须带交易证据升级处理。'}
{'topic': '退款升级', 'content_type': 'case', 'source_origin': 'internal', 'text': '客服核对失败结账后的两笔成功扣款,并将案例升级给 billing。'}

这个例子特别适合新人,因为它会让你先看到:

  • metadata filter 往往比“换更大的模型”更先见效

只测向量检索,不测关键词检索

Section titled “只测向量检索,不测关键词检索”

很多企业场景里,关键词检索并不弱,甚至是基础盘。

建议先从:

  1. 一个 基线
  2. 一个明确评估集
  3. 一次只改一个策略

开始。

检索分数高,不代表最终答案一定更好。 因为生成阶段也会影响表现。


调检索时,不要只说“效果不好”,要把现象映射到可以调整的杠杆。

现象优先调整为什么
明确术语、报错码搜不到增加关键词检索或混合检索向量检索可能弱化精确词
用户口语化问题搜不到查询改写、multi-查询、向量检索用户表达和文档表达不一致
top-k 里相关内容排得靠后rerank粗召回能找到,但排序不够准
检索结果主题对但版本错metadata filter需要按版本、日期、来源缩小范围
答案需要跨多个片段parent-child retrieval 或更合理的 chunk小 chunk 命中但上下文不足

这张表适合和评估集一起使用。每次只改一个策略,然后记录 Hit@k、MRR、引用质量和失败样本变化。

  • baseline:关键词检索,top-k=3,不 rerank。能命中精确词,但漏掉同义问法,适合报错和术语。
  • exp-1:向量检索,top-k=3,不 rerank。同义问法更好,但专有词有时不准,需要保留关键词通道。
  • exp-2:混合检索,top-k=5,加 rerank。整体质量最好,但延迟增加;如果延迟可接受,可以作为标准版本。

检索优化的关键不是一次找到完美策略,而是让每次改动都有记录、有指标、有失败样本。

学完这一页,至少保留这张证据卡:

查询
一个用户问题或测试用例
已检索分块
分块 ID、分数和来源标题
答案
带引用或来源说明的最终回答
失败检查
缺少证据、切分错误、文档过时或论断无依据
下一步动作
分块、embedding、重排、Prompt 或评估改动

这节课最关键的认识是:

RAG 的“找资料”不是机械步骤,而是一个可以不断设计和优化的系统环节。

很多时候,检索策略升级带来的收益,比换一个更大的模型还直接。


  1. 修改混合检索示例里的权重,比较关键词权重更高和向量权重更高时排序变化。
  2. 给文档再加一条包含“退课”字样的句子,观察关键词检索优势。
  3. 自己设计一个更丰富的 rewrite_query() 规则表。
参考实现与讲解
  1. 关键词权重更高时会偏向精确词匹配;向量权重更高时会偏向语义相似。更好的设置取决于用户是使用精确产品术语,还是使用模糊自然语言提问。
  2. 当精确短语、ID、课程名、政策术语或错误码很重要时,关键词检索很强。它能抓住 embedding 可能“抹平”的细粒度词。
  3. 有用的 rewrite 表应规范同义词、扩展缩写、把用户说法映射到领域术语,并避免改变用户意图。Rewrite 必须记录日志,因为错误 rewrite 会悄悄毁掉检索。