RAG-Core
记录由这篇课程得到的一些关于RAG的启发。
What RAG
RAG:检索增强,就像他的名字一样,RAG的过程大概就分为两个部分
- Retrive(检索)
我们知道LLM的原理就是通过你输入的Prompt不断预测下一个词,而且这个词的概率会随着**上下文(Context)**的变化而变化,RAG就是通过改变Prompt或者说Context的方式,影响模型的回答。
- 向量数据库已经被认为是比传统数据库更高效的一种检索库
在我们输入一段初始的Prompt,模型在处理上下文的时候,系统会先通过某个编码器(Embedding)将文本convert成向量,然后去向量数据库(Vector SQL)检索(Retrive)一些相近的知识/上下文,将其填充到Prompt,然后再调用大模型生成回答
- Augmented(增强)
在检索回文档后,我们将检索信息和原来的提示词拼接起来,我们称这个过程为增强(Augmented),下面有一个提示词模版,可以塞给大模型让他生成,这样对比结果会更加明显
1 | 你是一个项目问答助手。请对比展示「未使用 RAG 增强」和「使用 RAG 增强」时,回答质量有什么不同。 |
Why RAG
RAG的出现在大方向上说,是因为模型的微调/再训练成本太高,如果只是一些最新/专业领域的知识,对于模型来说他们不是没有回答能力,他们只是不知道这些事,在这种情况下,重新训练的成本太高了,不如在输入阶段直接把这些信息塞进去,虽然比较粗暴但确实很实用,但总不能在每次输入的时候都把这些信息带入吧,RAG就是专门干这事的,下面把这些信息划分:
- 众所周知大模型的训练都是基于一些历史数据,即使参数量再大,他也不知道一些最新的数据/信息,比如你问大模型
1 | 詹姆斯为什么转会76人? |
他压根不知道这个信息,只会生成一些非常通用的回答,甚至生成一些幻觉,会把别的球星的转会原因告诉你
- Q:这里肯定有人要问,主播主播,直接用Tool Calling调用搜索工具不行吗?
A:他们看似都在解决大模型处理不了最新知识的问题,但这两种方案本质上就不一样,调工具本质上没有做加强提示词这一步,而是在大模型不断Thinking的时候出现这个Calling,然后才会调用工具,注入上下文。而RAG是在输入Prompt的时候进行增加,在大模型首轮Thinking的时候,他就已经知道相关的最新知识。所以Calling并没有将该问题的解决闭环在系统设计(虽然有时候RAG也会被设计成Tool)中,而是依赖LLM的Tool Calling能力。
- 众所周知有一些内容只有特定领域,甚至企业内部才会用到,比如当你问:
1 | 字节跳动的员工请假一般都要走哪些流程,在哪可以请假 |
这些内部知识就需要专门搭建的RAG知识库提供帮助,比如在知识库里放入一些企业文档,在某个文档下就会有关于这个问题的答案,然后在检索过程中被塞入上下文
How RAG
这里我会分为三部分介绍:
How RAG execute
这张图大致展现了检索的大致过程,我会由易到难,介绍一些每一层的设计

- Metadata Filter
最简单的一层,和真正进行业务语义检索的Search过程不同,这个Filter要干的事非常清晰且明确,通过一些非常基础的信息对文档进行过滤,例如:- Date(时间)
比如我的提示词中明确带有所需文章的准确时间
- Date(时间)
1 | 请帮我检索字节跳动*2025-2026*季度的报销政策,每个季度都要 |
1. Author(作者)
比如我的提示词中清晰的写着只要某些作者的文档
1 | 请帮我检索*m1kasaz*该作者的所有文档 |
1. …(something like this)
通过这些例子,我们对这层的设计已经有一个大概的了解,视频中有一句话说得特别好,这个过程并不是在进行检索,他只是帮你缩小检索的范围,这个缩小既包括正向又包括反向,可以很形象的理解为SQL中的WHERE语句
1 | SELECT title |
这一层的设计真的非常简单,但它的效果也非常好,仅仅通过一些简单过滤就可以缩小检索范围
- Keywrod Search
听名字也能猜个大概设计,通过关键词(Keyword)进行文章的匹配(我在想这里用匹配一词可能会比较贴切,因为这一层本质也没有直接的检索过程),以下是一些常见的Keyword层设计
假设一段提示词:
1 | Making pizza without a pizza oven |
现在RAG向量库中有很多文档(doc1、doc2、doc3.…),每个文档都维护了一个稀疏矩阵,用于揭示每个文档中,每个单词都出现频率,或者称为积。
然后我们通过提示词中的单词去映射对应的矩阵,这一过程称为倒排索引,因为我们在通过提示词本身进行映射,然后我们就能得到差不多这样的矩阵,有点像图论里面的连通表

可以通过简单计算每个关键词的积分,求一下总和,就可以得到一个文档中某个关键词出现的大致频率,但是计算过程普遍没有这么简单,在建立倒排索引时往往会遇到一些问题:
-
这种朴素算法似乎无法解决长文本的天然优势
-
一些常用语,比如
the、a、of在这种朴素算法下有着天然优势
这里引入两种优化算法: -
TF-IDF
简单来说,先计算一下每个词在文本中的占比(该词数/总词数),然后取对数,这样就能计算每个词的稀有值,然后在为矩阵中的每个数都*该稀有值,就能解决1和2问题,这一过程叫做TF-IDF,这个稀有值叫做每个词的IDF,这个矩阵叫做TF-IDF矩阵

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


-
Sementic Search
语义检索,通过提示词和文档的语义关联度对文档进行检索。其检索过程和Transfomer的机制很相似,先对propmt和doc进行词嵌入,转化为向量,然后在一个向量空间中计算相似度。常见的相似度算法有: -
余弦相似度(Cosine Similarity)
只看“方向”是否一致,弱化模长影响,文本/语义 embedding 最常用。
$$\operatorname{cos}(x,y)=\frac{x\cdot y}{\lVert x\rVert,\lVert y\rVert}$$ -
点积/内积(Dot Product / Inner Product)
同时受方向和模长影响,推荐/双塔召回常用。
$$\operatorname{cos}(x,y)=x\cdot y$$
- 向量不归一化时,模长大的向量更容易得到更高分
- 欧氏距离(L2 Distance)
衡量空间几何距离,常用于聚类/最近邻、特征有明确尺度含义的场景。
$$d_2(x,y)=\lVert x-y\rVert_2=\sqrt{\sum_i (x_i-y_i)^2}$$
- 补充内容:通过Keywrod和Sementic计算出的结果需要通过一个优化算法计算出最终的排名
k用于控制排名的影响力beta用于控制Keywrod Search和Sementic Search的影响力

How RAG insepct
这一部分主要讨论我们如何**检验(insepct)**我们通过上面的方法召回文档的准确率。首先最重要的一件事,你得知道一份标准的正确答案,也就是该提示词在RAG之后应该召回的文档。目前主流的评估标准包括:
设:
- $R_k$:模型返回的前 $k$ 个文档集合,$|R_k|=k$
- $G$:该查询的相关文档集合(Ground Truth)
-
Precision&Map(准确率),正确召回的文档数/总召回数
$$\operatorname{Precision@}k=\frac{\lvert R_k \cap G \rvert}{k}$$ -
Recall(召回率),正确召回的文档数/总相关文档数(也称为Top-k)
$$\operatorname{Recall@}k=\frac{\lvert R_k \cap G \rvert}{\lvert G \rvert}$$ -
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
先介绍三种主流快速检索相似向量的算法,主要用于文档召回
- KNN
针对向量生成后,对比向量相似度的过程。简单来说就是将所有文档向量化以后,计算和提示词的相似度,这种全量计算会遇到几个问题 - 全量检索的复杂度是线性的,在文档较多的情况下性能很差
- 白白计算了很多不需要的相似度(前k个以外)

- ANN
相比KNN算法,ANN不断检索局部最优解,初始化了一个包含多个(n个)节点的邻接图,然后随机以某个节点出发,不断向目标节点的方向遍历,直至找到最近的节点
1. 复杂度可控,限制在n个节点范围内
1. 每个节点的对比都更有意义,每次对比都能更接近目标节点
1. 由于邻接图的随机性,这个算法对于同样k个文档的召回率没有KNN这么高

- HNSW
这个算法的思路是从局部最优推到全局最优,这个算法将节点分为多个层级,对Layer1包含所有的节点,向上递减。从最高层的临接图出发,不断使用ANN算法,在每层中都选出一个最优解。具体包括:
1. 随机从最顶层选择一个节点,找到顶层最优解
1. 以上一层的最优解开始,遍历这一层的邻接图
这个算法的成效非常显著,每一层都能过滤大量的节点,降低复杂度的同时召回率也能得到保证,复杂度的增长率接近对数函数

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

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

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




