1.1 RAG基础与向量检索局限 本节摘要:检索增强生成(RAG)是大模型应用最普遍的落地形态——生成答案前先检索相关文档,把私有知识塞进上下文。本节拆解 RAG 的索引、检索、生成三阶段流程,重点剖析纯向量检索在语义模糊、多跳推理、实体关系查询上的三类硬伤。理解这些局限,是后续选择 GraphRAG 或混合检索的前提。 本节目标 阅读完本节,你应当能够: 画出 RAG 的三阶段流程图,说清每阶段输入输出 解释 embedding 相似度检索为什么会出现"语义相关但答非所问" 用一个多跳推理的例子说明向量检索为什么力不从心 列出至少三种缓解向量检索局限的工程手段 一、问题与直觉 假设你给团队上线了一个基于大模型的内部问答机器人,接入了公司的产品手册和工单库。
本节摘要:检索增强生成(RAG)是大模型应用最普遍的落地形态——生成答案前先检索相关文档,把私有知识塞进上下文。本节拆解 RAG 的索引、检索、生成三阶段流程,重点剖析纯向量检索在语义模糊、多跳推理、实体关系查询上的三类硬伤。理解这些局限,是后续选择 GraphRAG 或混合检索的前提。
阅读完本节,你应当能够:
假设你给团队上线了一个基于大模型的内部问答机器人,接入了公司的产品手册和工单库。上线第一天就有员工问:"上个月那个导致订单重复的 bug 是哪个服务引起的?"机器人淡定地回了一段关于"订单系统"的通用介绍——既没提具体 bug,也没指出来源服务。
你大概率会怀疑检索没命中。于是去翻日志,发现检索召回的 chunk 倒是挺像,都提到了"订单"和"服务",但没有任何一个 chunk 同时包含"那个 bug"和"根因服务"这两条信息。问题不在模型,而在检索这一步就丢了关键知识。
这就是 RAG 最典型的翻车场景。RAG(Retrieval-Augmented Generation,检索增强生成)的思路很直白:别让模型凭记忆乱编,生成答案前先把相关文档检索出来塞进上下文。它的好处是能用上最新的、私有的知识,还能让模型给出引用。但"检索"这一步如果质量不行,后面模型再强也是巧妇难为无米之炊。
我们这一节先把标准 RAG 的流程拆开看,再重点讲它最常用的向量检索到底会在哪些地方掉链子。
一个标准的 RAG 系统分三个阶段,前两个在离线做,最后一个在用户提问时实时做。
索引阶段把文档切成小块(chunk),每块用 embedding 模型转成高维向量,存进向量数据库。检索阶段把用户问题也转成向量,在库里找最相似的若干块。生成阶段把找到的块拼进提示词,让模型基于这些内容回答。
听起来简单,但每一步都有讲究。切 chunk 时块太大会稀释相似度、太小会割裂上下文;embedding 模型选得不好,相似度本身就不可靠;检索回来的块可能只是"看起来像"而不是"真的有用"。
向量检索的核心是余弦相似度——两个向量方向越接近,被认为越相似。这背后的假设是:语义相近的文本,在 embedding 空间里位置也相近。
这个假设在很多场景下成立,但"语义相近"和"能回答问题"之间有一道鸿沟。考虑下面的例子:
| 用户问题 | 检索命中的 chunk | 是否真能回答 |
|---|---|---|
| "iPhone 15 的发布时间?" | "苹果公司于2023年9月发布了新款手机,搭载A16芯片" | 命中但模糊,没直接说15 |
| "马斯克收购的社交平台月活多少?" | 一段讲马斯克收购的,一段讲某平台用户数的 | 两块都没串起来 |
| "A 服务依赖的 B 服务挂了会怎样?" | 一段讲A服务的,一段讲B服务的 | 缺少"依赖"这条关系 |
第一个例子是语义模糊:向量觉得"新款手机"和"iPhone 15"很像,但模型从 chunk 里拿不到"新款就是15"这个明确对应。第二个例子是多跳推理:答案需要先跳一步"马斯克收购的是Twitter",再跳一步"Twitter月活4.5亿",向量检索只能按整体相似度找,很难同时召回这两块。第三个例子是实体关系:A 依赖 B 这条边,在纯文本里可能分散在不同段落,向量检索捕捉不到这种结构化的依赖关系。
多跳推理是向量检索最难啃的骨头。一个多跳问题,它的每个"跳"在文档里都是一段独立文本,彼此之间靠实体或关系连接。
问题:"收购了某平台的那个人的公司市值多少?" 跳1: 某平台 → 被收购 → 收购方是谁 跳2: 收购方 → 名字 → 这个人 跳3: 这个人 → 创立 → 某公司 跳4: 某公司 → 市值 → 数字
向量检索是把整个问题作为一个向量去匹配,它没法"拆解"出这几跳,更没法按跳的顺序去检索。结果往往是:要么召回一堆和问题整体相似但其实无关的块,要么漏掉中间某跳的关键文档。这也是为什么复杂业务问答(走流程、查依赖、溯因果)上,纯向量 RAG 的准确率常常掉到五成以下。
很多人 RAG 效果差,根源在 chunk 切得不对。常见的切分策略和它们的取舍:
| 切分方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 固定长度 | 按字符数硬切 | 实现简单 | 会切断句子、割裂语义 |
| 按句子/段落 | 在自然边界切 | 保留语义完整 | 块大小不均 |
| 递归切分 | 先按段落,超长再按句子 | 兼顾语义和长度 | 需要调参 |
| 带重叠 | 相邻块重叠一部分 | 减少边界信息丢失 | 增加存储和冗余 |
我的经验是:先用递归切分配 128 到 256 token 的块、加 10% 到 20% 的重叠,跑一版基线,再根据 bad case 调。别一上来就追求最优切分,基线都没跑通时调切分参数是瞎忙。
💡 关键直觉:chunk 的粒度本质是在"检索精度"和"上下文完整性"之间取舍。小块检索准但信息碎,大块信息全但相似度被稀释。没有银弹,得看你的问题类型——事实型问答倾向小块,理解型问答倾向大块。
embedding 模型直接决定向量空间的语义质量。选型时关注三点:语言覆盖(中英文、多语言)、维度(影响存储和速度)、领域适配(通用 vs 医疗法律金融专用)。
一个常见误区是盲目追高维度。维度从 768 涨到 1536,存储翻倍、检索变慢,但语义质量的提升往往是边际的。除非你的数据量非常大、查询对细微语义差异敏感,否则中等维度够用。
⚠️ 常见坑:embedding 模型和生成模型最好语言一致。用一个纯英文训练的 embedding 去检索中文文档,相似度计算会很不准。跨语言场景务必用支持多语言的 embedding。
向量检索召回的 top-k 块,相似度高不代表相关性高。一个普遍有效的做法是加一层重排序(re-ranking):用更重型的 cross-encoder 模型对召回的候选逐一打分重排。
class HybridRetriever: def retrieve(self, query, top_k=10): # 第一阶段:向量检索快速召回较大候选集 candidates = self.vector_store.search( self.embed(query), top_k=top_k * 3 ) # 第二阶段:cross-encoder 重排,精选最相关的 ranked = self.reranker.rank(query, candidates) return ranked[:top_k]
这种"先粗排再精排"的两阶段检索,是大多数生产级 RAG 的标配。粗排用向量检索保证速度(从百万级文档里毫秒级筛出几十个),精排用 cross-encoder 保证精度(对这几十个做细致的相关性判断)。
针对多跳推理这个硬伤,工程上有几条路:
第一种是查询改写:把一个多跳问题拆成多个子问题分别检索,再合并。比如把"收购某平台的人的公司市值"拆成"谁收购了某平台""这个人创立了哪个公司""这个公司市值多少"。代价是要调用多次模型,延迟和成本上升。
第二种是迭代检索:第一轮检索生成中间答案,根据中间答案再检索第二轮,像滚雪球一样逼近最终答案。这种思路和后面的 GraphRAG 有相通之处。
第三种就是引入知识图谱,把实体和关系显式存下来,靠图遍历做多跳——这正是下一节 GraphRAG 要解决的问题。
很多人 RAG 效果不好,第一反应是换更强的模型或更复杂的检索算法。但更有效的做法是先做归因诊断——搞清楚 bad case 到底栽在哪一环。一个简单的诊断流程:把每个 bad case 按下面三个环节逐一排查。
第一步看检索有没有召回正确文档。把期望能回答问题的文档手动找出来,看它在不在线上检索的 top-k 里。如果不在,问题在召回——可能是切分把答案切断了、embedding 把它排到 top-k 外了、或者查询表述和文档表述差太远。修法是调切分、换 embedding、加查询改写。
第二步看召回的文档排在第几。如果正确文档召回了但排得很靠后(比如 top-20 的第 18 名),而模型上下文窗口只装 top-5,那它实际没进上下文。问题在排序——加 cross-encoder 重排序能把正确文档往前提。
第三步看模型有没有用召回的内容。如果正确文档进了上下文,模型还是答错或编造,问题在生成——可能是提示没约束模型基于检索内容答,或者文档和问题对应关系太隐晦模型没读懂。修法是改提示、或者说明这个文档本身不够用。
💡 关键直觉:RAG 效果诊断要分层归因——召回、排序、生成,三个环节各有各的修法。不归因就盲目调参,等于蒙眼治病。养成"每个 bad case 先归因再修"的习惯,能省掉大量无效调参。这个分层思路也适用于后面几节的所有检索技术。
入门:找一个你熟悉的文档集(比如产品 FAQ),用固定长度切分搭一个最小 RAG,观察它对事实型问题和非事实型问题的回答差异。
进阶:把固定切分换成递归切分加重叠,对比同一批测试问题的准确率变化。再试着加一层 cross-encoder 重排序,看 top-3 的相关性是否提升。
挑战:构造三个多跳推理问题,用你的 RAG 测试,记录它每次漏掉了哪一跳。然后试着用查询改写(手动拆子问题)来修复,体会"拆解"对多跳问题的价值。
下一节我们看 GraphRAG 怎么用知识图谱补上多跳推理和实体关系查询这两块短板,以及它的构建成本到底有多高。