如何让 RAG 系统更可靠地找到所需资料
上一篇《一个DEMO认识最小RAG》里,我们从一个最小向量 RAG Demo 出发,编写了基本流程。
这条流程构成了最小 RAG 的基础。接下来,我们继续看一个实际问题:当简单的向量检索没能稳定找到所需资料时,可以怎样改进?查询改写、复杂问题拆解、混合检索和重排序分别处理不同环节,检索评估则帮助判断改动是否有效。
四种改善检索的方法
1. 查询改写:让搜索词表达得更清楚
用户的提问可能很口语、依赖上下文,或者没有包含资料里的关键词。查询改写会在保持原意的前提下,把问题整理成更适合搜索的完整表达。
例子:对话中用户问“那酒店呢?”,系统可以结合前文改写成“公司差旅政策对酒店住宿报销有什么规定?”。这样检索时不必猜“酒店”指什么。
技术实现思路
- 在调用现有
retrieve()前,增加rewriteQuery(question, history)。这个函数通过聊天 API,把当前问题和必要的最近对话发送给同一个大语言模型。 - 在这次 API 请求的提示词中,明确要求模型只返回一个独立、明确的检索问题,并保留人名、产品名、编号、日期和数字,不要补造用户没说过的条件。
- 用改写后的问题做检索,但最终生成回答时仍传入用户的原问题和检索片段。
- 如果改写为空、超时或格式异常,就回退到原问题,保证检索主流程可以继续。
// rewriteQuery 内部调用聊天 API,返回模型改写后的检索问题 String searchQuery = rewriteQuery(question, recentHistory); List<Hit> hits = retrieve(searchQuery, chunks); // 回答仍使用用户原问题,并附上检索到的资料 String answer = askModel(question, formatContext(hits));
LangChain4j 中可用查询转换组件处理这一步,例如把依赖对话上下文的追问压缩成独立查询。组件负责组织流程,具体改写仍由所配置的模型完成。
2. 复杂问题拆解:分别搜索问题中的多个方面
一个问题如果同时询问多个对象、条件或步骤,单次搜索可能只照顾到其中一部分。拆解会把它转成多个独立、完整的子问题,分别检索,再把证据合并起来回答原问题。
例子:“比较 Milvus 和 PostgreSQL 做混合检索时的检索能力、改造成本和运维成本”,可以拆成检索能力、接入现有 Demo 的改造量、部署运维差异三个子问题。
技术实现思路
- 先判断问题是否包含多个目标;可以用简单规则做初筛,再让模型输出结构化的子问题列表。
- 要求每个子问题脱离原句也能看懂,并限制数量,例如最多 3~5 个;简单问题就只返回原问题或一个改写问题。
- 相互独立的子问题并行调用检索;有先后依赖时按顺序检索。
- 把各路结果按片段 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”,关键词检索又能帮忙精确命中包含该型号的资料。
技术实现思路
- 索引中同时保留片段原文和向量。检索时分别执行关键词检索(如 BM25)和向量检索,各自先取一批候选,例如各取前 20 条。
- 用 RRF 合并两个排名列表。对片段
d,累加它在各列表中的1 / (c + rank)分数;没出现的列表不计分。c是平滑常数。 - 按融合分数排序并去重,再交给后续重排序或直接取最终 Top K。
向量检索 Top 20 ─┐
├─ RRF 合并、去重 ─→ 候选结果
BM25 检索 Top 20 ─┘
在我们当前的本地 Demo 里,可以先分别实现 vectorSearch() 和 keywordSearch(),再实现 rrfMerge()。生产环境可以把检索存储替换成支持混合检索的 Milvus,再通过 Java SDK 执行稠密向量和 BM25 搜索;Milvus 的混合搜索接口支持用 RRF 或加权排序器融合结果。LangChain4j 可以负责查询转换、检索器和结果聚合等流程编排,底层数据库仍由对应的存储连接器负责。
4. 重排序:把最相关的候选资料放前面
初次检索的目标是尽量找全候选资料;重排序则对候选结果逐条评估它们与当前问题的相关程度,再重新排列。之后系统只取前几段加入提示词。
比如初次检索拿到 20 段,重排序后把真正解释“酒店报销上限”的片段放在最前,而把只提到“差旅”的泛相关片段排后。
技术实现思路
- 先用向量、关键词或混合检索快速召回较宽的一批候选,例如前 20~50 段。
- 把原问题与每个候选片段一起交给重排序模型(常见做法是 Cross-Encoder / Reranker),让模型直接判断“这段资料对回答这个问题有多相关”。
- 按相关性分数重新排序,必要时设最低分阈值,再只取前几段(如 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。
技术实现思路
- 给索引片段分配稳定 ID(例如来源路径加章节 / 片段标识),建立固定测试集;每项记录问题、应命中的片段 ID,以及可选的问题类型标签。
- 在固定语料和相同 Top K 下,批量运行检索,记录每个问题实际返回的片段 ID 和名次。
- 用人工标注的相关片段计算 Recall@K、Precision@K、MRR;对有多个相关片段且关心整体排序的场景,也可加 NDCG@K。
- 对比基线向量检索与“改写 / 拆解 / 混合检索 / 重排序”版本的分数,同时记录延迟和调用成本,查看具体失败案例。
{
"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 评估平台混为一谈。 |
评测集怎么准备?
- 收集真实用户可能会问的问题,覆盖常见问法和不同难度。
- 为每个问题标注应该命中的资料片段,或至少标注相关来源文件。
- 运行检索,记录候选结果、排名和分数。
- 计算指标,并抽查失败案例,判断是查询、召回还是排序出了问题。
- 调整一个环节后重新评测,比较改动前后的结果。
如果按来源文件而不是片段标注,指标只能反映“找对了来源”,无法精确判断是否命中了来源中的正确段落。随着评测成熟,可以逐步改为片段级标注。
把五项内容放回一张图里
| 方法 | 解决的问题 | 作用位置 |
|---|---|---|
| 查询改写 | 问题表达不清或缺少检索所需信息 | 搜索之前 |
| 复杂问题拆解 | 一个问题包含多个检索目标 | 搜索之前 |
| 混合检索 | 只用语义或只用关键词都可能漏掉结果 | 召回候选资料时 |
| 重排序 | 初次检索结果中相关资料排序不理想 | 初次召回之后 |
| 检索评估 | 不清楚系统是否找对、改动是否有效 | 离线验证与迭代 |
可以把前四项看作检索链路中的改进工具,把检索评估看作检查这些工具是否有效的反馈办法。系统不必无条件启用全部方法:简单问题可能只需原始查询和向量检索;问题有多个方面时再拆解;精确词和语义都重要时用混合检索;候选较多或顺序不理想时再重排序。
最重要的判断
如果评估显示相关资料没有被召回,优先检查查询处理和召回方式;如果资料已经召回但排得靠后,再检查排序和重排序。这样才能针对实际问题改进,而不是盲目叠加技术。
把方法串成一条检索链路
对于一条复杂查询,在线流程可以概括为:
查询改写 / 拆解 → 每个查询分别执行向量检索 + BM25 → RRF 合并并去重 → Reranker 精排 → 取 Top K 片段,与原问题一起交给生成模型
在 Java 项目中,这些组件可以按职责组合:LangChain4j 用于编排查询转换、多个检索器和结果聚合,并可接入重排序模型;Milvus 负责存储文本与向量,并执行向量和 BM25 检索,Java 应用可通过 Milvus Java SDK 或相应适配器调用;RRF 用于融合多个检索列表的排名;评测代码则读取带有相关片段标注的数据集并计算指标。LangChain4j 与 Milvus 的具体连接方式取决于所选版本和集成模块。学习阶段也可以先用 Java 自行实现这些步骤,再逐步替换为框架和数据库组件。