记录由这篇课程得到的一些关于RAG的启发。

What RAG

RAG:检索增强,就像他的名字一样,RAG的过程大概就分为两个部分

  1. Retrive(检索)
    我们知道LLM的原理就是通过你输入的Prompt不断预测下一个词,而且这个词的概率会随着**上下文(Context)**的变化而变化,RAG就是通过改变Prompt或者说Context的方式,影响模型的回答。
  • 向量数据库已经被认为是比传统数据库更高效的一种检索库
    在我们输入一段初始的Prompt,模型在处理上下文的时候,系统会先通过某个编码器(Embedding)将文本convert成向量,然后去向量数据库(Vector SQL)检索(Retrive)一些相近的知识/上下文,将其填充到Prompt,然后再调用大模型生成回答
  1. Augmented(增强)
    在检索回文档后,我们将检索信息和原来的提示词拼接起来,我们称这个过程为增强(Augmented),下面有一个提示词模版,可以塞给大模型让他生成,这样对比结果会更加明显
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
你是一个项目问答助手。请对比展示「未使用 RAG 增强」和「使用 RAG 增强」时,回答质量有什么不同。

用户问题:
{{USER_QUESTION}}

一、未使用 RAG 增强时

请在不读取任何外部资料、不访问项目文档、不使用知识库检索结果的情况下回答用户问题。
如果信息不足,请明确说明“当前只能基于通用知识回答”。

请输出:
【增强前回答】
{{ANSWER_WITHOUT_RAG}}

二、使用 RAG 增强后

下面是从知识库中检索到的资料片段,请严格基于这些资料回答用户问题。
如果资料片段中没有提到某些信息,请不要编造。

【检索信息】
{{RETRIEVED_CONTEXT}}

其中,每条检索信息的格式如下:

[资料片段 {{INDEX}}]
来源:{{SOURCE_TITLE}}
更新时间:{{UPDATED_AT}}
相关度:{{RELEVANCE_SCORE}}
内容:
{{CHUNK_CONTENT}}

请基于以上检索信息回答用户问题。

请输出:
【增强后回答】
{{ANSWER_WITH_RAG}}

三、对比总结

请从以下角度对比增强前和增强后的区别:
1. 回答是否更具体
2. 是否能引用项目中的真实信息
3. 是否减少了模型编造
4. 是否能说明信息来源
5. 是否能发现资料缺失

请输出:
【差异对比】
{{COMPARISON_SUMMARY}}

Why RAG

RAG的出现在大方向上说,是因为模型的微调/再训练成本太高,如果只是一些最新/专业领域的知识,对于模型来说他们不是没有回答能力,他们只是不知道这些事,在这种情况下,重新训练的成本太高了,不如在输入阶段直接把这些信息塞进去,虽然比较粗暴但确实很实用,但总不能在每次输入的时候都把这些信息带入吧,RAG就是专门干这事的,下面把这些信息划分:

  1. 众所周知大模型的训练都是基于一些历史数据,即使参数量再大,他也不知道一些最新的数据/信息,比如你问大模型
1
詹姆斯为什么转会76人?

他压根不知道这个信息,只会生成一些非常通用的回答,甚至生成一些幻觉,会把别的球星的转会原因告诉你

  • Q:这里肯定有人要问,主播主播,直接用Tool Calling调用搜索工具不行吗?
    A:他们看似都在解决大模型处理不了最新知识的问题,但这两种方案本质上就不一样,调工具本质上没有做加强提示词这一步,而是在大模型不断Thinking的时候出现这个Calling,然后才会调用工具,注入上下文。而RAG是在输入Prompt的时候进行增加,在大模型首轮Thinking的时候,他就已经知道相关的最新知识。所以Calling并没有将该问题的解决闭环在系统设计(虽然有时候RAG也会被设计成Tool)中,而是依赖LLM的Tool Calling能力。
  1. 众所周知有一些内容只有特定领域,甚至企业内部才会用到,比如当你问:
1
字节跳动的员工请假一般都要走哪些流程,在哪可以请假

这些内部知识就需要专门搭建的RAG知识库提供帮助,比如在知识库里放入一些企业文档,在某个文档下就会有关于这个问题的答案,然后在检索过程中被塞入上下文

How RAG

这里我会分为三部分介绍:

How RAG execute

这张图大致展现了检索的大致过程,我会由易到难,介绍一些每一层的设计

  1. Metadata Filter
    最简单的一层,和真正进行业务语义检索的Search过程不同,这个Filter要干的事非常清晰且明确,通过一些非常基础的信息对文档进行过滤,例如:
    1. Date(时间)
      比如我的提示词中明确带有所需文章的准确时间
1
请帮我检索字节跳动*2025-2026*季度的报销政策,每个季度都要
1. Author(作者)

比如我的提示词中清晰的写着只要某些作者的文档

1
请帮我检索*m1kasaz*该作者的所有文档
1. …(something like this)

通过这些例子,我们对这层的设计已经有一个大概的了解,视频中有一句话说得特别好,这个过程并不是在进行检索,他只是帮你缩小检索的范围,这个缩小既包括正向又包括反向,可以很形象的理解为SQL中的WHERE语句

1
2
3
4
SELECT title
FROM articles
WHERE create_time BETWEEN '${startdate}' AND '${enddate}'
AND author LIKE '%${author}%';

这一层的设计真的非常简单,但它的效果也非常好,仅仅通过一些简单过滤就可以缩小检索范围

  1. Keywrod Search
    听名字也能猜个大概设计,通过关键词(Keyword)进行文章的匹配(我在想这里用匹配一词可能会比较贴切,因为这一层本质也没有直接的检索过程),以下是一些常见的Keyword层设计
    假设一段提示词:
1
Making pizza without a pizza oven

现在RAG向量库中有很多文档(doc1、doc2、doc3.…),每个文档都维护了一个稀疏矩阵,用于揭示每个文档中,每个单词都出现频率,或者称为积。
然后我们通过提示词中的单词去映射对应的矩阵,这一过程称为倒排索引,因为我们在通过提示词本身进行映射,然后我们就能得到差不多这样的矩阵,有点像图论里面的连通表

可以通过简单计算每个关键词的积分,求一下总和,就可以得到一个文档中某个关键词出现的大致频率,但是计算过程普遍没有这么简单,在建立倒排索引时往往会遇到一些问题:

  1. 这种朴素算法似乎无法解决长文本的天然优势

  2. 一些常用语,比如the、a、of 在这种朴素算法下有着天然优势
    这里引入两种优化算法:

  3. TF-IDF
    简单来说,先计算一下每个词在文本中的占比(该词数/总词数),然后取对数,这样就能计算每个词的稀有值,然后在为矩阵中的每个数都*稀有值 ,就能解决1和2问题,这一过程叫做TF-IDF,这个稀有值叫做每个词的IDF ,这个矩阵叫做TF-IDF矩阵

  4. BM25(Best Matching25)
    目前最流行的优化算法。其核心思想和TF-IDF差不多
    - 惩罚长文本
    - 惩罚频率过高的单词
    - 惩罚力度可以通过调整算法中的kb进行控制

  5. Sementic Search
    语义检索,通过提示词和文档的语义关联度对文档进行检索。其检索过程和Transfomer的机制很相似,先对propmtdoc 进行词嵌入,转化为向量,然后在一个向量空间中计算相似度。常见的相似度算法有:

  6. 余弦相似度(Cosine Similarity)
    只看“方向”是否一致,弱化模长影响,文本/语义 embedding 最常用。
    $$\operatorname{cos}(x,y)=\frac{x\cdot y}{\lVert x\rVert,\lVert y\rVert}$$

  7. 点积/内积(Dot Product / Inner Product)
    同时受方向和模长影响,推荐/双塔召回常用。
    $$\operatorname{cos}(x,y)=x\cdot y$$

  • 向量不归一化时,模长大的向量更容易得到更高分
  1. 欧氏距离(L2 Distance)
    衡量空间几何距离,常用于聚类/最近邻、特征有明确尺度含义的场景。
    $$d_2(x,y)=\lVert x-y\rVert_2=\sqrt{\sum_i (x_i-y_i)^2}$$
  • 补充内容:通过Keywrod和Sementic计算出的结果需要通过一个优化算法计算出最终的排名
    • k 用于控制排名的影响力
    • beta 用于控制Keywrod SearchSementic Search的影响力

How RAG insepct

这一部分主要讨论我们如何**检验(insepct)**我们通过上面的方法召回文档的准确率。首先最重要的一件事,你得知道一份标准的正确答案,也就是该提示词在RAG之后应该召回的文档。目前主流的评估标准包括:
设:

  • $R_k$:模型返回的前 $k$ 个文档集合,$|R_k|=k$
  • $G$:该查询的相关文档集合(Ground Truth)
  1. Precision&Map(准确率),正确召回的文档数/总召回数
    $$\operatorname{Precision@}k=\frac{\lvert R_k \cap G \rvert}{k}$$

  2. Recall(召回率),正确召回的文档数/总相关文档数(也称为Top-k)
    $$\operatorname{Recall@}k=\frac{\lvert R_k \cap G \rvert}{\lvert G \rvert}$$

  3. MRR(倒数排名),正确召回的文档的平均排名。
    设:

  • $N$:测试集里查询(query)的总数
  • $\operatorname{rank}_i$:第 $i$ 个查询中,第一个相关文档在返回结果里的排名位置(从 1 开始)
  • 若前列表中没有任何相关文档,则 $\dfrac{1}{\operatorname{rank}i}=0$
    $$\operatorname{MRR}=\frac{1}{N}\sum
    {i=1}^{N}\frac{1}{\operatorname{rank}_i}$$

How improve RAG

先介绍三种主流快速检索相似向量的算法,主要用于文档召回

  1. KNN
    针对向量生成后,对比向量相似度的过程。简单来说就是将所有文档向量化以后,计算和提示词的相似度,这种全量计算会遇到几个问题
  2. 全量检索的复杂度是线性的,在文档较多的情况下性能很差
  3. 白白计算了很多不需要的相似度(前k个以外)
  4. ANN
    相比KNN算法,ANN不断检索局部最优解,初始化了一个包含多个(n个)节点的邻接图,然后随机以某个节点出发,不断向目标节点的方向遍历,直至找到最近的节点
    1. 复杂度可控,限制在n个节点范围内
    1. 每个节点的对比都更有意义,每次对比都能更接近目标节点
    1. 由于邻接图的随机性,这个算法对于同样k个文档的召回率没有KNN这么高
  5. HNSW
    这个算法的思路是从局部最优推到全局最优,这个算法将节点分为多个层级,对Layer1包含所有的节点,向上递减。从最高层的临接图出发,不断使用ANN算法,在每层中都选出一个最优解。具体包括:
    1. 随机从最顶层选择一个节点,找到顶层最优解
    1. 以上一层的最优解开始,遍历这一层的邻接图
    这个算法的成效非常显著,每一层都能过滤大量的节点,降低复杂度的同时召回率也能得到保证,复杂度的增长率接近对数函数

    下面是一些常见的分块策略,分块(chunking)是一种简单并且能显著提高召回率的方式,简单来说就是将一个文档切碎,以文档中的内容进行向量划分
  6. Fixed Size
    最简单的分块方式,完全不考虑任何语义信息,非常容易产生一些可读性很差的分块
  7. Overlapping
    仍以固定长度分块,但分块之间存在重合部分,重合部分可以为分块提高关联性
  8. Recursive Character Splitting
    递归分块,以某个特定单词进行动态分块,如\n 换行符等
  9. Sementic Chunking
    语义分块,将每个句子向量化,计算相邻句子的相关性,如果相关性比较强(向量相近),就把他们归为一块
  10. LLM Chunking
    使用LLM分块,给出大致的分块策略,使用大模型进行智能分块,虽然本质是一个黑盒策略,但随着大模型的调用方式变得廉价,这种方式变得非常具有性价比
  11. Context Aware Chunking
    这是一种可以叠加在任何分块策略上的优化策略,利用LLM为一些没有语义信息的分块增加上下文信息,增加其被召回的概率

    下面介绍一种提示词重写策略,一般使用这种策略即可满足优化需求,一些高级方式如HIDE,Named Entity Reconization
  12. Query Rewriting
    通常用于重写用户输入的提示词,使之成为大模型更容易理解的形式,具体策略简单来说就是,先理解用户的原始提示词,然后再利用自己在这一领域的专业知识,重新将提示词用更专业的话术重写

    下面介绍一些常用的Encoder,通过更合理的方式,将Prompt与文档向量化并计算他们的相似度
  13. Bi-Encoder
    一种预处理的Encoder,文档的向量化和Prompt的向量化是分开的,两者单独计算向量后再求相似度,而且这里处理的向量都是降维的,为了迎合效率
    - 预处理,真正检索的时候速度快
    - 丢失精度,Prompt和文档向量两者并没有深层次的交互
  14. Cross-Encoder
    一种将Prompt和文档一起向量化的Encoder,两者进行更深层次的交互。具体的交互方式可以参考Transfomer架构,其中的多头注意力机制就是这个算法的关键
    - 不能预计算:文档向量没法提前存好,每来一个新 query 都要和候选文档重新一起过一遍模型
    - 计算量大:所以通常不用来做全量检索(从几百万文档里找候选),而是用作重排(rerank)阶段——先用便宜的 Bi-Encoder/BM25 粗筛出几十到几百个候选,再用 Cross-Encoder 精细打分排序,兼顾效果和效率。
  15. ColBERT
    一种预处理的Encoder,虽然是预处理,但其粒度更细,将每个文档的语料全部向量化,然后通过与Prompt中所有语料进行对比得出最终相似度
    - 相比Bi-Encoder,ColBERT生成的是原始向量,纬度更高,细节更丰富