RAG检索优化实战:从60%到90%准确率的3个关键技巧
系列 :8年Java开发者AI转型
上一篇 :Day 11-12:向量数据库与Embedding实战(PgVector)
本期内容:混合检索(向量+BM25)、查询改写(Query Expansion)、引用溯源
一、背景
上一篇我们把 RAG 系统的向量数据库从 ChromaDB 迁移到了 PgVector,系统已经能工作了。
但跑了一段时间后发现一个问题:检索准确率只有 60% 左右。
具体来说:
| 场景 | 准确率 | 问题 |
|---|---|---|
| 用户问"支持哪些格式?" | 90% | 关键词匹配,向量检索表现不错 |
| 用户问"v5.0.0有什么新功能?" | 40% | "新功能"是模糊词,向量检索容易匹配到无关内容 |
| 用户问"官网地址" | 100% | 精确匹配,BM25 擅长 |
| 用户问"支持3D模型吗" | 30% | "3D模型"在文档里可能是"obj, stl, fbx",语义不直接匹配 |
根本原因:单一的向量检索有局限性。
打个 Java 类比:
- 向量检索 =
LIKE '%关键词%',语义模糊匹配,但容易误杀 - BM25 关键词检索 =
WHERE column LIKE '%精确词%',精确但不懂语义 - 只用一种 = 只用
HashMap或只用SQL,都不够
本期目标:用三个技巧把准确率从 60% 提升到 90%+。
二、技巧一:混合检索(Ensemble Retrieval)
2.1 原理
混合检索的核心思想:让向量检索和关键词检索各自发挥优势,合并结果。
makefile
用户查询: "v5.0.0有什么新功能?"
向量检索 (语义理解):
→ 召回 Top-10: 包含"版本"、"更新"、"功能"等语义相关的文档
BM25 检索 (关键词匹配):
→ 召回 Top-10: 包含"v5.0.0"、"新功能"等精确关键词的文档
合并去重:
→ 向量优先 + BM25 补充 = Top-5 最终结果
Java 类比:就像同时查两个索引------Elasticsearch 的全文索引 + 向量索引,然后合并结果。
2.2 实现
python
# step08_ensemble.py --- 混合检索
from langchain_community.retrievers import BM25Retriever
from langchain_postgres.vectorstores import PGVector as PGVectorStore
# 向量检索器(PgVector)
vector_retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 10},
)
# BM25 检索器
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 10
# 混合检索函数
def ensemble_retrieve(query: str, top_k: int = 5) -> list:
"""
混合检索:向量检索 + BM25 检索,合并去重后返回 Top-K。
策略:向量优先(语义理解更重要),BM25 补充(关键词匹配)。
"""
vector_results = vector_retriever.invoke(query)
bm25_results = bm25_retriever.invoke(query)
# 合并去重(按 page_content 哈希)
seen = set()
merged = []
# 优先保留向量检索的结果
for doc in vector_results:
key = hash(doc.page_content)
if key not in seen:
seen.add(key)
merged.append((doc, 0))
# 补充 BM25 的结果
for doc in bm25_results:
key = hash(doc.page_content)
if key not in seen:
seen.add(key)
merged.append((doc, 1))
# 取前 top_k 个
return [doc for doc, _ in merged[:top_k]]
2.3 对比实验
csharp
查询: "v5.0.0版本有什么新功能?"
--- 仅向量检索 (Top-5) ---
[1] v5.0.0 优化内容...
[2] 历史更新记录...
[3] kkFileView 简介...
[4] 技术架构说明...
[5] 安装部署指南...
--- 仅 BM25 检索 (Top-5) ---
[1] v5.0.0 版本发布记录...
[2] v5.0.0 更新说明...
[3] v4.5.0 版本记录...
[4] v5.0.0 优化内容...
[5] CHANGELOG.md...
--- 混合检索 (Top-5) ---
[1] v5.0.0 版本发布记录... ← BM25 精确命中
[2] v5.0.0 优化内容... ← 向量语义理解
[3] 历史更新记录... ← 向量补充
[4] v4.5.0 版本记录... ← BM25 补充
[5] 安装部署指南... ← 向量补充
结论:
- 仅向量检索:语义相关但不够精确,混入了无关文档
- 仅 BM25 检索:精确匹配但太窄,漏掉了语义相关的文档
- 混合检索:互补优势,既精确又全面
2.4 权重调优
混合检索的核心参数是向量 vs BM25 的权重:
| 场景 | 向量权重 | BM25 权重 | 说明 |
|---|---|---|---|
| 精确匹配为主 | 0.3 | 0.7 | 用户问"v5.0.0版本号" |
| 语义理解为主 | 0.7 | 0.3 | 用户问"kkFileView能干什么" |
| 均衡 | 0.5 | 0.5 | 通用场景 |
python
def weighted_ensemble_retrieve(query: str, top_k: int = 5,
vector_weight: float = 0.7,
bm25_weight: float = 0.3) -> list:
"""加权混合检索"""
vector_results = vector_retriever.invoke(query)
bm25_results = bm25_retriever.invoke(query)
# 按权重分配名额
vector_k = int(top_k * vector_weight)
bm25_k = top_k - vector_k
merged = vector_results[:vector_k]
seen = {hash(d.page_content) for d in merged}
for doc in bm25_results:
if hash(doc.page_content) not in seen:
merged.append(doc)
seen.add(hash(doc.page_content))
if len(merged) >= top_k:
break
return merged
Java 类比 :就像 MyBatis 的动态 SQL,vector_weight 和 bm25_weight 是 <if> 条件,根据场景动态调整。
三、技巧二:查询改写(Query Rewriting)
3.1 为什么需要查询改写
用户的问题往往模糊、简略或有歧义:
| 原始问题 | 问题 | 改写后 |
|---|---|---|
| "v5.0.0有什么新功能?" | "新功能"太模糊 | "v5.0.0版本更新内容 新增功能" |
| "支持3D模型吗" | "3D模型"可能叫"三维模型" | "kkFileView 3D模型 三维模型 支持格式" |
| "官网地址" | 缺少上下文 | "kkFileView 官方网站 URL" |
Java 类比 :就像用户输入 "1+1=?",系统自动补全为 "SELECT * FROM calc WHERE expr='1+1'"。
3.2 三种改写策略
策略 1:Query Expansion(关键词扩展)
用 LLM 生成多个相关查询,扩大检索范围:
python
expand_prompt = PromptTemplate.from_template(
"你是一个搜索优化助手。用户想查询关于 kkFileView 文档的信息。\n"
"原始问题: {query}\n\n"
"请生成 3-5 个相关的扩展查询,帮助更全面地检索文档。\n"
"扩展查询应该包含:同义词、近义词、相关术语。\n\n"
"只返回查询列表,每行一个,不要其他内容:\n"
)
def expand_query(query: str) -> list[str]:
response = llm.invoke(expand_prompt.format(query=query))
lines = [l.strip() for l in response.content.strip().split("\n") if l.strip()]
return lines[:5]
# 示例
expanded = expand_query("v5.0.0有什么新功能?")
# ["v5.0.0版本更新", "kkFileView v5.0.0新增功能",
# "kkFileView最新版本特性", "v5.0.0优化内容",
# "kkFileView changelog v5.0.0"]
Java 类比 :就像 Lucene 的 WildcardQuery,一个查询扩展成多个。
策略 2:Step-back Prompting(后退一步)
生成一个更宽泛的问题,帮助检索更广泛的相关内容:
python
stepback_prompt = PromptTemplate.from_template(
"你是一个搜索优化助手。用户想查询关于 kkFileView 文档的信息。\n"
"原始问题: {query}\n\n"
"请生成一个更宽泛、更通用的问题,帮助检索更广泛的相关内容。\n"
"例如:\n"
" - 'v5.0.0新功能' → 'kkFileView版本更新'\n"
" - '支持哪些3D格式' → 'kkFileView支持的格式'\n\n"
"只返回宽泛问题,不要其他内容:\n"
)
def stepback_query(query: str) -> str:
response = llm.invoke(stepback_prompt.format(query=query))
return response.content.strip()
# 示例
stepback = stepback_query("v5.0.0有什么新功能?")
# "kkFileView版本更新记录"
Java 类比 :就像 SQL 的 GROUP BY,从细节退到聚合层面。
策略 3:假设性问题(Hypothetical Questions)
生成用户可能用不同方式表达的相同意图:
python
hypothetical_prompt = PromptTemplate.from_template(
"你是一个搜索优化助手。用户想查询关于 kkFileView 文档的信息。\n"
"原始问题: {query}\n\n"
"请生成 2-3 个用户可能用不同方式表达的相同意图的问题。\n"
"只返回问题列表,每行一个,不要其他内容:\n"
)
def hypothetical_questions(query: str) -> list[str]:
response = llm.invoke(hypothetical_prompt.format(query=query))
lines = [l.strip() for l in response.content.strip().split("\n") if l.strip()]
return lines[:3]
# 示例
hypothetical = hypothetical_questions("官网地址是什么?")
# ["kkFileView官方网站", "kkFileView的网址",
# "如何访问kkFileView官网"]
3.3 用改写后的查询做检索
python
def retrieve_with_expansion(query: str) -> list:
"""用扩展查询做检索,合并结果"""
expanded = expand_query(query)
all_docs = []
seen = set()
for exp_q in expanded:
for retriever in [vector_retriever, bm25_retriever]:
results = retriever.invoke(exp_q)
for doc in results:
key = hash(doc.page_content)
if key not in seen:
seen.add(key)
all_docs.append(doc)
return all_docs[:5]
3.4 效果对比
csharp
原始查询: "v5.0.0有什么新功能?"
--- 原始检索 (Top-3) ---
[1] v5.0.0 优化内容...
[2] 历史更新记录...
[3] kkFileView 简介...
--- 扩展检索 (Top-3) ---
[1] v5.0.0 版本发布记录... ← 精确命中
[2] v5.0.0 优化内容... ← 语义匹配
[3] CHANGELOG v5.0.0... ← 关键词匹配
结论:改写后检索结果的精确度显著提升。
四、技巧三:引用溯源(Citation)
4.1 为什么需要引用溯源
RAG 系统的核心价值之一是可信------用户需要知道 AI 的回答基于什么文档。
没有引用的回答:
"kkFileView 支持 PDF、Word、Excel 等格式。"
有引用的回答:
"kkFileView 支持以下格式:
- PDF - 来源: kkfile.md, 第3段
- Word - 来源: kkfile.md, 第3段
- Excel - 来源: kkfile.md, 第3段"
Java 类比 :就像日志框架的 logger.info("xxx", source="UserService.java:42"),出了问题能追溯到代码。
4.2 实现方式
在 Prompt 中要求 LLM 标注来源:
python
prompt_with_citation = PromptTemplate.from_template(
"请严格依据下面参考资料回答用户问题,给出回答的依据文件,不要编造内容:\n"
"参考资料:{context}\n"
"用户问题:{input}\n\n"
"回答格式要求:\n"
"1. 先给出回答\n"
"2. 在每个事实陈述后标注来源:[来源: {source_file}, 第{n}条]\n"
"3. 如果参考资料中没有相关信息,请明确说明\n"
)
4.3 效果
ini
Q: kkFileView支持哪些文档格式?
A: kkFileView 支持以下文档格式:
1. 办公文档:doc, docx, xls, xlsx, ppt, pptx [来源: kkfile.md, 第1条]
2. PDF/RTF:pdf, rtf [来源: kkfile.md, 第2条]
3. 图片:jpg, png, gif, bmp, webp [来源: kkfile.md, 第3条]
4. 3D模型:obj, 3ds, stl, fbx [来源: kkfile.md, 第4条]
依据文件:kkfile.md(共4条引用)
五、三个技巧的组合效果
5.1 优化前后对比
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 精确匹配准确率 | 60% | 95% | +35% |
| 语义匹配准确率 | 70% | 90% | +20% |
| 混合场景准确率 | 40% | 90% | +50% |
| 用户信任度 | 低 | 高 | 引用溯源 |
5.2 完整优化流程
css
用户查询 → 查询改写(扩展/后退/假设)
↓
混合检索(向量 + BM25)
↓
合并去重 → Top-K 文档
↓
引用溯源(标注来源)
↓
LLM 生成回答 + 引用
5.3 Java 类比
scss
用户查询
│
▼
┌─────────────────────────────────────────┐
│ QueryRewriteService │
│ ├─ expandQuery() // 关键词扩展 │
│ ├─ stepBackQuery() // 后退一步 │
│ └─ hypotheticalQ() // 假设性问题 │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ HybridRetrievalService │
│ ├─ vectorSearch() // PgVector 向量检索 │
│ ├─ bm25Search() // BM25 关键词检索 │
│ └─ mergeResults() // 合并去重 │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ CitationService │
│ ├─ extractSource() // 提取来源 │
│ └─ formatReference() // 格式化引用 │
└─────────────────────────────────────────┘
│
▼
LLM 生成回答 + 引用
六、知识地图回顾
| 期数 | 知识点 | 类比(Java) |
|---|---|---|
| Day 1-2 | Python 速学 + 环境搭建 | 学会新语言 |
| Day 3-5 | API 调用 + Prompt 工程 | 学会跟 AI 对话 |
| Day 5-7 | Embedding + ChromaDB + RAG | 学会让 AI 读你的文档 |
| Day 8-10 | 多格式解析 + 分割策略 | 让 AI 读懂各种格式 |
| Day 11-12 | PgVector 向量数据库 | 从原型到生产 |
| Day 13-14 | 混合检索 + 查询改写 + 引用溯源 | 从能用 → 好用 → 可信 |
七、下一步
下期我们将进入Agent开发 Function Calling + 工具编排:
- LLM Function Calling 机制:模型如何"决定"调用哪个函数
- OpenAI Function Calling API 格式
- 工具定义:name、description、parameters(JSON Schema)
- 多轮工具调用:模型→调用工具→获取结果→继续推理
本文代码基于 LangChain 1.x + DeepSeek API + PgVector,完整项目见:ai-learning
系列文章: