检索和生成的权衡——RAG 的本质是什么
检索和生成的权衡——RAG 的本质是什么
适读人群:做 RAG 系统的工程师,对 LLM 应用架构感兴趣的人 | 阅读时长:约14分钟 | 核心价值:从设计哲学层面理解 RAG,而不只是技术实现步骤
大概一年半前,我第一次搭 RAG 系统的时候,理解非常浅——就是把文档拆碎,存进向量库,查询时候检索出来,拼进 Prompt 里,然后让 LLM 生成答案。这个流程图我能画出来,代码也能写出来,但我真的不理解我在做什么。
有一次系统上线后,用户反馈回答质量很差:有时候回答的是文档里的内容但和用户的问题对不上,有时候回答的东西文档里根本没有。
排查了很久,我才意识到问题出在我对 RAG 设计的根本理解上。我在写 RAG,但我不知道 RAG 的本质是在权衡什么。
理解了本质,很多工程决策就清晰了。
RAG 解决的根本问题
LLM 有两个核心能力:
推理能力:基于给定信息进行理解、总结、推断、生成。这是模型训练出来的,是模型的"大脑"。
记忆能力:知道什么是什么,事物的关系,领域知识。这存在于模型参数里,是训练时"记住"的东西。
LLM 的记忆有几个明显的局限:
- 知识有截止日期(训练数据的时间范围)
- 私有数据不在训练集里
- 参数记忆会"遗忘"或混淆细节
- 无法追溯来源
RAG 的本质,是用外部检索来补充和替代模型的参数记忆,同时保留模型的推理能力。
这个理解很重要:RAG 不是让模型变聪明,而是给模型提供它需要的信息,让它用已有的推理能力处理这些信息。
所以 RAG 系统的设计,本质上是在回答这个问题:在"检索"和"生成"之间,这个任务的瓶颈在哪里,如何在两者之间分配资源和精力?
检索的边界
检索能做什么,不能做什么?
检索擅长:
- 从大量文档里找出与查询最相关的片段
- 基于语义相似性的模糊匹配
- 跨文档的信息召回
检索不擅长:
- 理解查询的意图("这个人问的是 A 还是 B?")
- 处理需要跨多个概念推理的复杂问题
- 处理用户问题和文档语言风格差异很大的情况
- 判断检索到的内容是否真的与问题相关
用一个实际例子:
用户问:"我们的产品支持批量导入吗?如果支持,每次最多几条?"
这是一个复合问题,包含两个子问题。向量检索会找到语义最相似的文档片段,但:
- 可能返回"支持批量操作"但没提到数量限制的片段
- 可能返回"最大 1000 条"但是讲另一个功能的
- 可能两个子问题的答案分散在不同文档里,单次检索无法同时找到
这时候问题不在生成端,在检索端。加强检索(比如:把复合问题拆开分别检索)才能真正解决。
生成的边界
生成能做什么,不能做什么?
生成擅长:
- 基于给定信息回答问题
- 整合多个信息片段,生成连贯的回答
- 推理、分析、总结
- 格式化和语言风格调整
生成不擅长:
- 对抗"检索噪声"——如果检索结果里有误导性信息,模型往往会被带偏
- 知道自己"不知道"——LLM 有生成倾向,当检索结果不足时,容易补充自己的参数记忆(幻觉)
- 处理矛盾信息——如果检索到的两个片段说法不一致,模型的处理往往不可预测
权衡的关键:瓶颈在哪里
理解了两者的边界,就能更清晰地定位 RAG 系统的瓶颈。
在我实际接触的 RAG 项目里,大概有这样的分布:
70% 的问题出在检索端
- 向量检索召回了相关性不够的文档
- 复合问题没有被拆解,单次检索信息不完整
- 查询和文档的表达方式差异大,语义匹配失效
- Chunk 分割方式不合理,把本来应该在一起的信息切断了
20% 的问题出在生成端
- Prompt 设计不当,模型没有被正确引导
- 检索结果太长,超过了模型的有效注意力范围
- 模型被噪声信息带偏,生成了与主要内容相悖的答案
10% 是系统级问题
- 没有兜底策略,检索为空时模型胡编
- 延迟问题,多次检索让响应太慢
这个分布告诉我们:大多数 RAG 系统的改进空间在检索端,而不是生成端。
但很多人的直觉是反过来的——他们会第一个去改 Prompt,改完发现效果没太大变化。正确的排查顺序应该是:先看检索结果的质量,再看生成的质量。
加强检索的几种方式
当瓶颈在检索端,有这几个方向可以考虑:
一、查询改写(Query Rewriting)
用户的问题往往不是适合向量检索的形式。可以先用 LLM 把用户问题改写成更适合检索的形式。
def rewrite_query(user_query: str) -> list[str]:
"""把用户问题改写成多个检索子查询"""
prompt = f"""
将以下用户问题改写为 2-3 个更具体的检索查询,
每个查询针对问题的一个方面,输出 JSON 数组。
用户问题:{user_query}
输出格式:["查询1", "查询2", "查询3"]
"""
response = llm.generate(prompt)
return json.loads(response)
# 示例
user_query = "我们的产品支持批量导入吗?每次最多几条?"
queries = rewrite_query(user_query)
# 可能得到:["产品批量导入功能", "批量导入数量限制", "批量操作最大条数"]二、混合检索(Hybrid Search)
纯向量检索对精确术语匹配比较弱。比如用户问"OrderService 这个类的实现",向量检索可能找不到,因为向量空间里语义最近的不一定是包含精确类名的文档。
混合检索结合向量检索(语义相关性)和 BM25 关键词检索(精确匹配),然后用 RRF(Reciprocal Rank Fusion)合并结果。
三、分层检索(Hierarchical Retrieval)
不是直接检索 Chunk,而是先检索文档级摘要,找到相关文档后,再在文档内部做精细检索。
这对长文档特别有效——单个 Chunk 可能不包含完整信息,但文档级摘要能帮助先过滤出正确的文档。
四、调整 Chunk 策略
这是最底层的改进,也是经常被忽视的。
常见的 Chunk 问题:
- Chunk 太小:上下文不足,模型难以理解
- Chunk 太大:包含太多不相关信息,干扰检索
- 边界切割不当:把一个完整的概念切成两半
一般规律:
- 技术文档:512-1024 tokens,按段落/章节切割
- 对话记录:按轮次切割,保留上下文
- 代码:按函数/类切割,不要按行数切割
- FAQ:问题+答案作为一个 Chunk加强生成的几种方式
当瓶颈在生成端:
一、改进 Prompt 结构
最有效的单点改进通常是:给模型明确的"不知道就说不知道"的指令,以及提供检索内容的使用规范。
RAG_PROMPT = """
你是一个知识库助手,只根据下方提供的文档内容回答问题。
规则:
1. 如果文档中有直接相关信息,给出具体回答,并指出信息来自哪个文档
2. 如果文档中的信息不足以完整回答,说明哪些部分有信息,哪些部分不在文档范围内
3. 不要基于常识或自己的知识补充文档中没有的信息
4. 如果多个文档说法不一致,明确指出差异,不要擅自选择一个版本
文档内容:
{context}
问题:{question}
回答:
"""二、控制 Context 窗口
不是检索到的内容越多越好。检索结果太多,模型的注意力会被稀释。
一般来说,3-5 个高质量的 Chunk 比 10 个参差不齐的 Chunk 效果更好。宁可检索的 top-k 小一些,也不要放太多低相关性的内容进去。
三、Self-RAG
让模型先判断是否需要检索,然后在生成过程中动态决定何时触发检索,以及检索到的内容是否有用。
这比简单的"先检索后生成"更灵活,但实现复杂度高,适合成熟阶段的优化,不适合第一版就上。
RAG 设计的几个原则
把上面的东西提炼成几个可以直接用的原则:
原则一:先评估检索质量,再优化生成
在优化 Prompt 之前,先看看检索结果:把 top-k 的检索结果打印出来,人工判断这些结果是否真的回答了用户的问题。如果检索结果本身不对,改什么 Prompt 都没用。
原则二:用简单基线测复杂方案
很多人上来就用 HyDE、Self-RAG、Multi-Query 等高级方案。先跑最简单的基线(直接检索+直接生成),评估它在你的数据上的效果,再决定在哪里改进。复杂方案增加的不只是效果,还有维护成本。
原则三:兜底比优化更重要
先把"无法回答的情况"处理好,再考虑优化"可以回答的情况"。一个能准确说"我不知道"的 RAG 系统,比一个偶尔给出高质量回答但有时候胡说的系统,在生产里更可靠。
原则四:检索和生成分别评估
不要只看最终答案的质量,分别评估:检索的召回率是否足够,以及在完美检索结果的情况下,生成的答案是否正确。这两个维度分开测,才能定位瓶颈。
RAG 是目前把 LLM 实际落地的最成熟的技术方案之一,但它不是把向量库和 LLM 接起来那么简单。
理解检索和生成的边界,理解你的系统瓶颈在哪里——这比学会用 LangChain 的 RAG 组件,更重要。
