8年Java开发者AI转型第二周:RAG检索优化-从60%到90%准确率的3个关键技巧 (Day 13-14)

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_weightbm25_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 支持以下格式:

  1. PDF - 来源: kkfile.md, 第3段
  2. Word - 来源: kkfile.md, 第3段
  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

系列文章:

Day 1-2 从零搭建RAG文档问答系统

Day 3-4 从零搭建RAG文档问答系统

Day 5-7 从零搭建RAG文档问答系统

Day 8-10 RAG系统深入 + 向量数据库实战

Day 11-12 向量数据库与Embedding实战(PgVector)

相关推荐
空中湖1 小时前
AI是如何思考的|AI的边界与人机共生:在狂热中保持清醒
人工智能
weixin_446260851 小时前
SportMV-Agent:面向多机位体育视频的智能体多视角推理框架 & SportMV-Bench评测基准
人工智能·计算机视觉·音视频
u0132508611 小时前
标定(Calibration)00-3:自动驾驶常用传感器Camera / LiDAR / Radar / IMU / GNSS 各自解决什么问题?
人工智能·机器学习·自动驾驶
自动化和Linux1 小时前
【pytest在pycharm中运行与CMD命令运行区别一】
python·pycharm·pytest
ksueh2 小时前
AI写小说工具怎么选?2026年最新实测数据展示
人工智能·ai写作·ai助手
大鱼>2 小时前
多宠物家庭智能管理平台:云端架构与多设备协同实战
python·算法·iot·宠物
自动驾驶之心2 小时前
ResWorld:端到端自动驾驶的时序残差世界模型(北航&中关村实验室)
人工智能·机器学习·自动驾驶
Li Ming&2 小时前
Python办公自动化:利用Python批量将PDF转换为图片文件
python·pdf·pip
墨染天姬2 小时前
【AI】Hermes Agent 提示词优化与闭环机制
人工智能