如何让 RAG 系统更可靠地找到所需资料

Page6 · RAG 学习笔记

如何让 RAG 系统更可靠地找到所需资料

上一篇《一个DEMO认识最小RAG》里,我们从一个最小向量 RAG Demo 出发,编写了基本流程。

资料切块并向量化准备可检索的资料索引
→
问题转成向量使用同一个嵌入模型
→
余弦相似度检索排序并取 Top K 片段
→
生成回答把问题和命中片段交给模型

这条流程构成了最小 RAG 的基础。接下来,我们继续看一个实际问题:当简单的向量检索没能稳定找到所需资料时,可以怎样改进?查询改写、复杂问题拆解、混合检索和重排序分别处理不同环节,检索评估则帮助判断改动是否有效。

四种改善检索的方法

1. 查询改写:让搜索词表达得更清楚

用户的提问可能很口语、依赖上下文,或者没有包含资料里的关键词。查询改写会在保持原意的前提下,把问题整理成更适合搜索的完整表达。

例子:对话中用户问“那酒店呢?”,系统可以结合前文改写成“公司差旅政策对酒店住宿报销有什么规定?”。这样检索时不必猜“酒店”指什么。

适合:问题简短、指代不清、上下文省略,或用户用词与资料用词不同的情况。改写后仍然是同一个查询意图。

技术实现思路

  1. 在调用现有 retrieve() 前,增加 rewriteQuery(question, history)。这个函数通过聊天 API,把当前问题和必要的最近对话发送给同一个大语言模型。
  2. 在这次 API 请求的提示词中,明确要求模型只返回一个独立、明确的检索问题,并保留人名、产品名、编号、日期和数字,不要补造用户没说过的条件。
  3. 用改写后的问题做检索,但最终生成回答时仍传入用户的原问题和检索片段。
  4. 如果改写为空、超时或格式异常,就回退到原问题,保证检索主流程可以继续。
// rewriteQuery 内部调用聊天 API,返回模型改写后的检索问题
String searchQuery = rewriteQuery(question, recentHistory);
List<Hit> hits = retrieve(searchQuery, chunks);
// 回答仍使用用户原问题,并附上检索到的资料
String answer = askModel(question, formatContext(hits));

LangChain4j 中可用查询转换组件处理这一步,例如把依赖对话上下文的追问压缩成独立查询。组件负责组织流程,具体改写仍由所配置的模型完成。

2. 复杂问题拆解:分别搜索问题中的多个方面

一个问题如果同时询问多个对象、条件或步骤,单次搜索可能只照顾到其中一部分。拆解会把它转成多个独立、完整的子问题,分别检索,再把证据合并起来回答原问题。

例子:“比较 Milvus 和 PostgreSQL 做混合检索时的检索能力、改造成本和运维成本”,可以拆成检索能力、接入现有 Demo 的改造量、部署运维差异三个子问题。

与查询改写的区别:改写通常产生一个意思相同、表达更清楚的查询;拆解会产生多个不同但彼此相关的子查询,每个子查询负责找到原问题的一部分证据。

技术实现思路

  1. 先判断问题是否包含多个目标;可以用简单规则做初筛,再让模型输出结构化的子问题列表。
  2. 要求每个子问题脱离原句也能看懂,并限制数量,例如最多 3~5 个;简单问题就只返回原问题或一个改写问题。
  3. 相互独立的子问题并行调用检索;有先后依赖时按顺序检索。
  4. 把各路结果按片段 ID 去重,再用 RRF 等方式融合排名;最终用原始问题和合并后的证据生成回答。
List<String> queries = decompose(question); // 结构化子问题
List<List<Hit>> resultLists = retrieveInParallel(queries, index);
List<Hit> evidence = rrfMergeAndDeduplicate(resultLists);
String answer = askModel(question, formatContext(evidence));

LangChain4j 的 QueryTransformer 可以把一个查询转换成一个或多个查询;检索编排器再把多路结果交给聚合器。子问题并行会缩短等待时间,但会增加检索调用量。

3. 混合检索:同时照顾语义和关键词

向量检索擅长找到意思相近的内容;关键词检索擅长匹配产品型号、政策编号、人名等精确词语。混合检索把两种结果合并,减少只依赖其中一种方式带来的漏检。

例如搜索“报销上限”,语义检索可能找到标题为“差旅费用标准”的片段;如果用户问的是型号“X200”,关键词检索又能帮忙精确命中包含该型号的资料。

结果怎么合并?可以用 Reciprocal Rank Fusion(RRF)等方法综合不同检索列表的排名。直观地说,一个片段如果在多个列表里都排得靠前,合并后通常也会靠前。

技术实现思路

  1. 索引中同时保留片段原文和向量。检索时分别执行关键词检索(如 BM25)和向量检索,各自先取一批候选,例如各取前 20 条。
  2. 用 RRF 合并两个排名列表。对片段 d,累加它在各列表中的 1 / (c + rank) 分数;没出现的列表不计分。c 是平滑常数。
  3. 按融合分数排序并去重,再交给后续重排序或直接取最终 Top K。
向量检索 Top 20 ─┐
                  ├─ RRF 合并、去重 ─→ 候选结果
BM25 检索 Top 20 ─┘

在我们当前的本地 Demo 里,可以先分别实现 vectorSearch() 和 keywordSearch(),再实现 rrfMerge()。生产环境可以把检索存储替换成支持混合检索的 Milvus,再通过 Java SDK 执行稠密向量和 BM25 搜索;Milvus 的混合搜索接口支持用 RRF 或加权排序器融合结果。LangChain4j 可以负责查询转换、检索器和结果聚合等流程编排,底层数据库仍由对应的存储连接器负责。

4. 重排序:把最相关的候选资料放前面

初次检索的目标是尽量找全候选资料;重排序则对候选结果逐条评估它们与当前问题的相关程度,再重新排列。之后系统只取前几段加入提示词。

比如初次检索拿到 20 段,重排序后把真正解释“酒店报销上限”的片段放在最前,而把只提到“差旅”的泛相关片段排后。

注意:重排序只能调整已经召回的候选项。如果正确资料根本没有进入候选列表,重排序无法把它找回来。

技术实现思路

  1. 先用向量、关键词或混合检索快速召回较宽的一批候选,例如前 20~50 段。
  2. 把原问题与每个候选片段一起交给重排序模型(常见做法是 Cross-Encoder / Reranker),让模型直接判断“这段资料对回答这个问题有多相关”。
  3. 按相关性分数重新排序,必要时设最低分阈值,再只取前几段(如 3~5 段)进入最终提示词。
候选片段 + 原问题
        ↓ Reranker 逐段打分
按相关性排序 → 过滤低分 → 取 Top K

LangChain4j 提供 ReRankingContentAggregator,可接入实现了 ScoringModel 的评分 / 重排序模型。RRF 与 Reranker 的工作不同:RRF 融合多个检索列表的名次;Reranker 再用查询与片段的匹配程度精排。重排序能提升精排质量,但会增加一次模型调用的延迟和费用,所以通常只处理有限候选集。

检索评估:检查系统到底有没有找对

检索评估主要用于离线检查和迭代,不是每次用户提问时必需的一步。先准备一组代表性问题,并由人标注每个问题应该命中的资料,再让系统检索,比较实际结果与标注结果。

指标 直白解释 常用于发现
Recall@K 应该找到的相关资料,有多少出现在前 K 条里? 重要资料有没有漏掉
Precision@K 前 K 条结果中,有多少确实相关? 结果里无关内容是否太多
MRR 第一条相关资料排得有多靠前? 最有用证据是否容易被模型优先看到

例如,测试集标注了 2 段相关资料,系统返回前 3 段并命中其中 1 段,那么 Recall@3 = 1/2。如果最相关的资料排在第二位,那么该问题的 MRR = 1/2。

技术实现思路

  1. 给索引片段分配稳定 ID(例如来源路径加章节 / 片段标识),建立固定测试集;每项记录问题、应命中的片段 ID,以及可选的问题类型标签。
  2. 在固定语料和相同 Top K 下,批量运行检索,记录每个问题实际返回的片段 ID 和名次。
  3. 用人工标注的相关片段计算 Recall@K、Precision@K、MRR;对有多个相关片段且关心整体排序的场景,也可加 NDCG@K。
  4. 对比基线向量检索与“改写 / 拆解 / 混合检索 / 重排序”版本的分数,同时记录延迟和调用成本,查看具体失败案例。
{
  "question": "酒店住宿每晚最多能报销多少?",
  "relevantChunkIds": ["travel-policy#hotel-limit"]
}

对当前 Java Demo,第一版可以自己写一个离线评测入口:读取 JSON 测试集,调用 retrieve(),按相关片段 ID 计算指标并输出结果表。这样不需要先引入评估框架,也能直接比较每次检索改动的影响。以后如果需要评估最终回答的忠实度、相关性和引用质量,再引入 Ragas 等面向 RAG 的评测工具;评估框架不能替代高质量的人工测试集。

常见评估框架怎么选?

框架 / 平台 主要用途 适合什么情况
Ragas 面向 RAG 的评估库。提供 Context Precision、Context Recall 等检索相关指标,也有 Faithfulness、Response Relevancy 等回答评估指标。 希望用一套 RAG 指标检查检索上下文和最终回答;它以 Python 为主,Java 项目可导出评测数据后单独运行评估。
LangSmith 管理测试集、运行实验和对比版本,也能结合规则评估、人工标注或 LLM-as-judge。 需要持续记录调用过程、追踪失败案例,并比较不同模型、提示词或检索配置。
Microsoft Foundry Evaluators 提供 Document Retrieval 等 RAG 评估器;有人工相关性标签时可评估检索排名,没有标签时也可用模型评估检索上下文相关性。 系统已经使用 Microsoft Foundry,希望在该平台上运行和查看评测结果。
LangChain4j 主要用于 Java 应用中的 LLM 与 RAG 流程构建,包括查询转换、检索和结果聚合;评测指标可由应用自行实现或接入其他评估工具。 Java 开发者希望在代码里组织 RAG 流程;不要把它与专门的 RAG 评估平台混为一谈。
对当前 Demo 的建议:先用 Java 自己计算 Recall@K、Precision@K 和 MRR,弄清“实际命中”和“人工标注”如何比较;之后若想同时评估上下文质量和答案忠实度,可以把同一份测试数据导出给 Ragas。若还需要管理多轮实验和线上运行记录,再考虑 LangSmith 这类平台。

评测集怎么准备?

  1. 收集真实用户可能会问的问题,覆盖常见问法和不同难度。
  2. 为每个问题标注应该命中的资料片段,或至少标注相关来源文件。
  3. 运行检索,记录候选结果、排名和分数。
  4. 计算指标,并抽查失败案例,判断是查询、召回还是排序出了问题。
  5. 调整一个环节后重新评测,比较改动前后的结果。

如果按来源文件而不是片段标注,指标只能反映“找对了来源”,无法精确判断是否命中了来源中的正确段落。随着评测成熟,可以逐步改为片段级标注。

把五项内容放回一张图里

方法 解决的问题 作用位置
查询改写 问题表达不清或缺少检索所需信息 搜索之前
复杂问题拆解 一个问题包含多个检索目标 搜索之前
混合检索 只用语义或只用关键词都可能漏掉结果 召回候选资料时
重排序 初次检索结果中相关资料排序不理想 初次召回之后
检索评估 不清楚系统是否找对、改动是否有效 离线验证与迭代

可以把前四项看作检索链路中的改进工具,把检索评估看作检查这些工具是否有效的反馈办法。系统不必无条件启用全部方法:简单问题可能只需原始查询和向量检索;问题有多个方面时再拆解;精确词和语义都重要时用混合检索;候选较多或顺序不理想时再重排序。

最重要的判断

检索的目标不是拿到最多资料,而是找到足以支撑回答的相关证据。
如果评估显示相关资料没有被召回,优先检查查询处理和召回方式;如果资料已经召回但排得靠后,再检查排序和重排序。这样才能针对实际问题改进,而不是盲目叠加技术。

把方法串成一条检索链路

对于一条复杂查询,在线流程可以概括为:

查询改写 / 拆解
  → 每个查询分别执行向量检索 + BM25
  → RRF 合并并去重
  → Reranker 精排
  → 取 Top K 片段,与原问题一起交给生成模型

在 Java 项目中,这些组件可以按职责组合:LangChain4j 用于编排查询转换、多个检索器和结果聚合,并可接入重排序模型;Milvus 负责存储文本与向量,并执行向量和 BM25 检索,Java 应用可通过 Milvus Java SDK 或相应适配器调用;RRF 用于融合多个检索列表的排名;评测代码则读取带有相关片段标注的数据集并计算指标。LangChain4j 与 Milvus 的具体连接方式取决于所选版本和集成模块。学习阶段也可以先用 Java 自行实现这些步骤,再逐步替换为框架和数据库组件。

参考文档

Page6 · RAG 学习笔记

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注