一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

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

时间:2026-07-16 08:00:56 编辑:袖梨 来源:一聚教程网

RAG检索优化实战:从60%到90%准确率的3个关键技巧


一、背景

上一篇我们把 RAG 系统的向量数据库从 ChromaDB 迁移到了 PgVector,系统已经能工作了。

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

但跑了一段时间后发现一个问题:检索准确率只有 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 原理

混合检索的核心思想:让向量检索和关键词检索各自发挥优势,合并结果

 复制代码用户查询: "v5.0.0有什么新功能?"向量检索 (语义理解):
  → 召回 Top-10: 包含"版本"、"更新""功能"等语义相关的文档BM25 检索 (关键词匹配):
  → 召回 Top-10: 包含"v5.0.0"、"新功能"等精确关键词的文档合并去重:
  → 向量优先 + BM25 补充 = Top-5 最终结果

Java 类比:就像同时查两个索引——Elasticsearch 的全文索引 + 向量索引,然后合并结果。

2.2 实现

 复制代码# 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 对比实验

 复制代码查询: "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.30.7用户问"v5.0.0版本号"
语义理解为主0.70.3用户问"kkFileView能干什么"
均衡0.50.5通用场景
 复制代码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 生成多个相关查询,扩大检索范围:

 复制代码expand_prompt = PromptTemplate.from_template(
    "你是一个搜索优化助手。用户想查询关于 kkFileView 文档的信息。n"
    "原始问题: {query}nn"
    "请生成 3-5 个相关的扩展查询,帮助更全面地检索文档。n"
    "扩展查询应该包含:同义词、近义词、相关术语。nn"
    "只返回查询列表,每行一个,不要其他内容: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(后退一步)

生成一个更宽泛的问题,帮助检索更广泛的相关内容:

 复制代码stepback_prompt = PromptTemplate.from_template(
    "你是一个搜索优化助手。用户想查询关于 kkFileView 文档的信息。n"
    "原始问题: {query}nn"
    "请生成一个更宽泛、更通用的问题,帮助检索更广泛的相关内容。n"
    "例如:n"
    "  - 'v5.0.0新功能' → 'kkFileView版本更新'n"
    "  - '支持哪些3D格式' → 'kkFileView支持的格式'nn"
    "只返回宽泛问题,不要其他内容: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)

生成用户可能用不同方式表达的相同意图:

 复制代码hypothetical_prompt = PromptTemplate.from_template(
    "你是一个搜索优化助手。用户想查询关于 kkFileView 文档的信息。n"
    "原始问题: {query}nn"
    "请生成 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 用改写后的查询做检索

 复制代码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 效果对比

 复制代码原始查询: "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 的回答基于什么文档。

没有引用的回答:

有引用的回答:

Java 类比:就像日志框架的 logger.info("xxx", source="UserService.java:42"),出了问题能追溯到代码。

4.2 实现方式

在 Prompt 中要求 LLM 标注来源:

 复制代码prompt_with_citation = PromptTemplate.from_template(
    "请严格依据下面参考资料回答用户问题,给出回答的依据文件,不要编造内容:n"
    "参考资料:{context}n"
    "用户问题:{input}nn"
    "回答格式要求:n"
    "1. 先给出回答n"
    "2. 在每个事实陈述后标注来源:[来源: {source_file}, 第{n}条]n"
    "3. 如果参考资料中没有相关信息,请明确说明n"
)

4.3 效果

 复制代码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 完整优化流程

 复制代码用户查询 → 查询改写(扩展/后退/假设)
              ↓
         混合检索(向量 + BM25)
              ↓
         合并去重 → Top-K 文档
              ↓
         引用溯源(标注来源)
              ↓
         LLM 生成回答 + 引用

5.3 Java 类比

 复制代码用户查询
  │
  ▼
┌─────────────────────────────────────────┐
│  QueryRewriteService                    │
│  ├─ expandQuery()    // 关键词扩展        │
│  ├─ stepBackQuery()  // 后退一步          │
│  └─ hypotheticalQ()  // 假设性问题        │
└─────────────────────────────────────────┘
  │
  ▼
┌─────────────────────────────────────────┐
│  HybridRetrievalService                 │
│  ├─ vectorSearch()   // PgVector 向量检索 │
│  ├─ bm25Search()     // BM25 关键词检索   │
│  └─ mergeResults()   // 合并去重          │
└─────────────────────────────────────────┘
  │
  ▼
┌─────────────────────────────────────────┐
│  CitationService                        │
│  ├─ extractSource()  // 提取来源         │
│  └─ formatReference() // 格式化引用       │
└─────────────────────────────────────────┘
  │
  ▼
  LLM 生成回答 + 引用

六、知识地图回顾

期数知识点类比(Java)
Day 1-2Python 速学 + 环境搭建学会新语言
Day 3-5API 调用 + Prompt 工程学会跟 AI 对话
Day 5-7Embedding + ChromaDB + RAG学会让 AI 读你的文档
Day 8-10多格式解析 + 分割策略让 AI 读懂各种格式
Day 11-12PgVector 向量数据库从原型到生产
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)

热门栏目