这是《LLM 应用开发 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 向量检索擅长语义相似,但对错误码、型号、人名、版本号等精确信号不稳定;关键词检索相反。混合检索把多种召回路径组合起来,提高复杂查询的覆盖率。
11.1 单路召回的盲区
| 查询 | 向量检索 | 关键词检索 |
|---|---|---|
| 如何申请退款 | 好 | 一般 |
| 错误 1045 怎么处理 | 可能偏语义 | 好 |
| ORD-10001 状态 | 可能不准 | 好 |
| 账号被锁 | 好 | 一般 |
| MySQL 8.0.36 参数 | 语义近似错版本 | 好 |
| 退货和换货区别 | 好 | 一般 |
因此生产 RAG 通常至少包含:
dense embedding
sparse keywords / BM25
metadata filter
database exact lookup
11.2 检索架构
Query Understanding
|-- rewrite
|-- keyword extraction
|-- language / tenant / acl
|
+-- parallel retrieval
|-- vector top K1
|-- keyword top K2
+-- exact lookup
|
+-- fusion
-> dedupe
-> rerank
-> context build
每路召回取更多候选,再交给 rerank 精排,通常优于只调大单路 top K。
11.3 BM25
BM25 是经典关键词排序算法,核心思想:
- 查询词在文档中出现越多越相关;
- 罕见词权重更高;
- 长文档做归一化;
- 词频收益有上限。
score(query, doc)
= sum IDF(term) * TF(term, doc) / (TF + length normalization)
适合:
- 精确术语;
- 错误码;
- 产品型号;
- 函数名;
- 合同编号;
- 版本号。
11.4 关键词抽取
规则优先处理强信号:
import re
PATTERNS = {
"order_id": r"\b[A-Z]{2,4}-\d{4,12}\b",
"error_code": r"\b(?:0x)?[A-Z]{2,6}-?\d{3,6}\b",
"version": r"\bv?\d+\.\d+(?:\.\d+)?\b",
}
def extract_keywords(text: str):
found = {}
for name, pattern in PATTERNS.items():
values = re.findall(pattern, text)
if values:
found[name] = values
return found
强信号可直接查数据库或倒排索引,不一定要经过语义模型。
11.5 融合策略
分数归一化
normalized = (score - min) / (max - min)
不同来源的分数分布不同,简单加权容易失真。
RRF
def reciprocal_rank_fusion(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, item in enumerate(ranking, start=1):
scores[item] = scores.get(item, 0) + 1 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)
RRF 只用排名,不用原始分数,工程上更常用。
加权
final_score = 0.6 * dense + 0.3 * sparse + 0.1 * recency
权重应通过评测集调优,不能拍脑袋固定。
11.6 过滤与排序
过滤分两类:
| 类型 | 示例 | 时机 |
|---|---|---|
| 硬过滤 | 租户、权限、状态 | 检索前或检索中 |
| 软排序 | 新鲜度、权威度、来源 | 融合或重排 |
示例:
def apply_business_score(item, now):
score = item["retrieval_score"]
if item["source"] == "official":
score += 0.05
if item["effective_time"] > now:
return None
age_days = (now - item["updated_at"]).days
score -= min(age_days / 365, 0.05)
return score
权威度调整要谨慎,避免让高权威但无关的文档压过真正答案。
11.7 多查询召回
复合问题应拆解:
用户:退款多久到账,运费谁承担?
子查询 1:退款到账时间
子查询 2:退货运费承担规则
实现方式:
- 规则拆分问句;
- 模型生成子查询;
- 保留原始查询;
- 每个子查询独立检索;
- 合并去重;
- 按子问题组织上下文;
- 生成时分别回答。
拆分要设置数量上限,防止查询爆炸。
11.8 Elasticsearch 示例
{
"query": {
"bool": {
"must": {
"match": {
"content": "MySQL 1045 access denied"
}
},
"filter": [
{"term": {"tenant_id": "tenant-a"}},
{"term": {"active": true}}
]
}
},
"size": 20
}
向量检索与 BM25 可并行执行,再由应用层或引擎做融合。
11.9 评估
| 指标 | 说明 |
|---|---|
| Dense Recall@K | 向量召回 |
| Sparse Recall@K | 关键词召回 |
| Hybrid Recall@K | 融合召回 |
| Exact Hit@1 | 精确信号命中 |
| Latency | 检索总延迟 |
| Noise Rate | 无关候选比例 |
| Context Tokens | 上下文成本 |
必须按查询类型分组评估,例如普通语义类、编号类、版本类、多子问题类。
11.10 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 精确编号查不到 | 向量弱化符号 | 关键词/数据库 |
| 关键词结果太死 | 同义表达缺失 | 加向量召回 |
| 融合后旧文档靠前 | 新鲜度未参与 | 时间软排序 |
| 多租户串数据 | 过滤缺失 | 硬过滤 |
| 召回多但答案差 | 候选噪声高 | rerank |
| 查询延迟高 | 串行检索 | 并行化 |
本章小结
混合检索通过向量、关键词、元数据和精确查询互补,覆盖语义相似与精确匹配两类需求。融合常用 RRF,硬过滤必须前置,业务权重需通过评测调优。召回阶段追求覆盖,排序质量交给 rerank。
思考题
- BM25 和向量检索各自适合什么查询?
- RRF 为什么比直接混合分数更稳定?
- 硬过滤和软排序的区别是什么?
- 多子问题检索如何控制成本?
- 如何评估混合检索是否优于单路召回?