跳转至

RAG

Retrieval-Augmented Generation,检索增强生成

RAG 价值

LLM 知识冻结、无法覆盖私有数据和最新信息。

第一是知识时效性,LLM 训练完知识就固定了,训练截止日期之后发生的事它一无所知;第二是私有知识覆盖,公司内部文档、行业专有数据根本没有机会进训练集,LLM 对这些内容是空白的;第三是幻觉问题,没有知识依据时 LLM 容易「自己发挥」编出一个听起来合理但实际错误的答案,给了它参考资料之后幻觉就少很多。

知识可热更新、答案可溯源


至于微调,主要是成本太高,周期太长。

离线阶段:文档加载、文档切割、存入向量数据库

文档加载 → 切割(Chunking)→ 向量化(Embedding)→ 入库。

相对用户查询来说是离线阶段,不在每次提问时执行。

文档分割策略

策略一:固定大小切割(Fixed Size Chunking)

最简单的方式,按固定字符数或 token 数切割,不管语义边界在哪。优点是实现简单、chunk 大小可控;缺点是可能在句子中间截断,破坏语义完整性。

纯固定大小几乎不单独使用,通常会加上重叠(overlap)来缓解边界截断问题。你可能觉得这种方式太粗暴了,确实如此。重叠就像扫描仪扫跨页内容时特意扫两遍边缘区域,前一个 chunk 的末尾和下一个 chunk 的开头有一段重叠内容,确保跨边界的语义能被至少一个 chunk 完整覆盖。

比如 chunk_size=500、overlap=100,后一个 chunk 的前 100 个字符和前一个 chunk 的后 100 个字符是相同的。

策略二:Structure Based Chunking

按文档的自然语义边界来切,比如段落、句子、标题层级。核心思想是:不要在语义中间截断,找到文字天然的「断点」再切。

常见的做法是维护一个分隔符优先级列表,先尝试按段落切,切出来太大再按句子切,还是太大再按标点切,以此类推,直到满足 chunk_size 限制。

对于有明确标题结构的 Markdown 或 HTML 文档,按标题层级切是更优的选择:每个 chunk 对应整篇文档中的一个完整章节,metadata 里自动带上所属标题(比如「产品手册 > 退款政策 > 申请流程」),既语义独立,又方便过滤和溯源。

策略三:特殊内容专项处理

前面两种策略对普通文本够用了,但遇到代码文件和表格就会露馅,通用切割策略在这两种内容上效果很差,需要单独处理。

代码应该以函数或类为单位切割;表格应该按一整个表来切。

策略四:父子切割(Parent-Child Chunking)

同时兼顾检索精度和上下文完整

检索时用放大镜(小块,精准定位),返回时用全景图(大块,上下文完整)

存储时,同一段内容存两份。一份是细粒度的小 chunk(比如 200 token),专门用于向量检索,因为小 chunk 语义聚焦,围绕一个小话题,检索精度高。另一份是包含这个小 chunk 前后上下文的大 chunk(比如 1000 token),通过 ID 与对应的小 chunk 关联。检索时用小 chunk 找到精准的命中点,然后根据关联 ID 取出对应的大 chunk,把完整的上下文交给 LLM 阅读,生成质量更好。

选用什么 Embedding 模型

选模型的话,主要看三个维度:第一是中文支持,中文场景我会优先选 BGE 系列;第二是向量维度,维度越高一般精度越好,但存储成本也越大;第三是最大输入长度,这个决定了能处理多长的 chunk。


评估这块我的建议是不要只看通用排行榜,一定要在自己的业务数据上跑召回测试,那个才是真正有参考价值的。

跑 Hit@K 测试:在自己的业务数据上测:准备几百条业务相关的「问题 + 正确答案 chunk」对,分别用候选模型做检索,看正确的 chunk 有没有出现在前 K 条结果里。这个指标叫 Hit@K,Hit@5 = 0.8 的意思就是,80% 的问题,它对应的答案都出现在了检索结果的前 5 条里。

通常 Hit@5 低于 0.7 就要考虑换模型或者改进 Chunking 策略了。

向量数据库

向量数据库就是专门用来存储和检索「向量」的数据库。

这里的向量,指的是嵌入模型(Embedding Model)把文本、图片、音频这些内容转换成的一串浮点数,比如一句话可能被转换成 [0.12, -0.87, 0.34, ... ] 这样一个 768 维或 1024 维的数组。每一个向量代表了这段内容的「语义含义」,语义越相近的内容,它们的向量在高维空间里的距离就越近。


近似检索:向量数据库支持的核心操作叫做 近似最近邻搜索(ANN,Approximate Nearest Neighbor):给你一个查询向量,在库里找出和它最相似的 K 个向量,返回对应的原始内容。这正是 RAG 里「检索」那一步的底层支撑——用户问了一个问题,先把问题转成向量,再去向量数据库里找最相关的文档片段,最后喂给大模型生成回答。

在线阶段:用户提问时实时检索

Query 改写 → 向量检索(粗排)→ Rerank(精排),每次用户提问都要跑一遍。

问题改写

一是简单改写,把口语化问题改写成更正式、独立完整的检索句。表把口语化、模糊的问题改写成更精准、更书面化的检索表述,这就是最基础的改写方式。改写的本质是消除口语化表达和书面知识库表述之间的向量距离,「这东西咋退」和「申请退款的操作流程」在向量空间里不是同一个邻居,改写就是把前者的向量拉近到后者。同时还可以补全上下文,处理代词指代(「这个功能」改写成具体功能名称),让多轮对话中的模糊引用变成检索可识别的具体词汇。

二是 HyDE(Hypothetical Document Embeddings),让 LLM 先「假设」一个可能的答案,用这个假设答案的向量去检索。你可能会觉得奇怪,不用问题去搜反而用假设答案去搜?原因很简单:问题和答案的用词往往差异很大,但假设答案和真实答案的用词更接近,它们的语义空间天然更匹配,命中率往往更高。

还有一个 Step-back Prompting(后退提问)方法。

最后把这些都向量化。

粗排:向量检索、关键词检索、多路召回

关键词检索(BM25)靠词频统计,精确匹配强但同义词没辙;向量检索靠语义空间距离,语义理解强但精确词容易漏。

多路召回要使用 RRF 算法:多种不同结果度量衡不同,无法直接比较。所以 RRF 就是直接使用排名而不是原始分数,绕开了分数不可比的问题。

一个文档在多路检索里都排名靠前,它的 RRF 综合分就高,就像多位评委都给高分的选手,最后的综合排名就高。

Rerank

Rerank 常见实现是 Cross-encoder,把「query + chunk」拼成一对输入,让模型整体看这一对的相关性。Cross-encoder 能看到 query 中每个词对 chunk 的影响、chunk 里哪些词最能回答 query,相关性判断精度远高于 Bi-encoder。代价是每一个候选 chunk 都要单独跑一次 Cross-encoder,速度慢,所以只适合对小规模候选集做精排,不适合大规模召回阶段。

Generation:RAG 里的 G

RAG 全称是 Retrieval-Augmented Generation,所以从概念上说,它描述的是「用检索结果增强生成」这件事,而不是单纯的 search。

但工程实现时,要区分「RAG 范式」和「RAG 组件边界」。Generation 最终一定会发生,但它不一定要封装在检索组件内部。

如果系统是一个独立问答应用,它通常对外暴露的是 question -> answer。这时 RAG 链路内部会包含检索、上下文组装、答案生成、引用标注和拒答策略。Generation 就是最后一步:把检索出来的 chunk、来源信息和用户问题一起放进 prompt,让 LLM 基于这些参考资料回答。

如果 RAG 是作为 Agent 或上层应用的工具,它更适合只暴露 query -> evidence:输入查询,返回相关 chunk、来源、分数和 metadata。后续是否继续检索、如何组织证据、是否生成最终答案、如何标注引用、证据不足时是否拒答,都交给调用者决定。

所以可以这样区分:

语义搜索:query -> chunks
问答型 RAG 封装:question -> evidence -> answer
检索型 RAG 工具:query -> evidence,由调用者负责后续生成

这个区别很重要。RAG 的 G 是范式层面的最终生成环节,但组件设计上不必强制把 G 做进 search tool。尤其在 Agentic RAG 里,检索工具只返回证据往往更灵活;生成和纠错留在 Agent 主循环里,边界反而更清楚。

RAG 知识库更新

应该是要把那个文档对应的所有的 chunk 全都删除掉,然后换成新的。

如何感知数据源变动:计算 hash。 做法可以是在每个 chunk 的 metadata 里存上文档 ID 字段。