Agent八股文-RAG篇
基础概念篇
什么是 RAG?详细描述一个完整 RAG 系统的详细工作流程?
检索增强生成,通过检索与问题相关的文档内容,依次作为内容补充增强结果的生成。静态处理 文档加载,文档切割,向量化,存入数据库,在线处理,问题向量化,粗排(查找相关的前10个数据),精排(用特殊的模型将问题和文档结合,查看匹配度,再取前3个数据),返回数据
我的回答里,静态处理缺少了文档的加载,在线处理缺少了第一步问题去口语化,完善问题内容,方便检索,
RAG
LLM由于知识固化无法获取私有数据和最新数据,每次传到文章又过于麻烦,所以希望有一个技术实现ai自动去数据库查询相关数据,因此就有了RAG,通过向量化实现语义之间相似度的判断,以此具备通过自然语言检索文本的能力。
RAG 的过程
离线阶段:
- 文档加载,读取文档
- 文档切割(chunking)小林给的范围在500-1000 前后100覆盖长度
- 向量化(embedding) 选取不同的embedding模型可以对同一chunk得到不同的高维向量
- 入库,将每一个chunk的向量和原始文本存入向量数据库
在线阶段
- 问题预处理 Query Rewrite.:将口语化的问题书面化,比如上一次的文档,通过历史对话完善内容,提高检索效率
- 预处理方式有:
- 简单改写,将口语化的描述写成书面文字
- HyDE,先给问题设立一个假设答案,然后构成一个陈述句,再去查询,这要比用问题去查询资料要更好,毕竟答案要比问题需要的资料更接近,命中率更高,本质是更改文体,将疑问句改成陈述句
- 后退提问 Step-back Prompting:可能知识库中没有相关问题的解答内容,但是有该问题的背景,前置知识,可以查找这些资料然后让模型推导得出内容,具体实现方式是先让LLM总结该问题所需的通用背景,然后查询
请将以下具体问题转化为一个更通用的背景问题,用于检索相关背景知识。 具体问题:{原始问题}背景问题:
- 多Query扩展:将原始问题改写成3-5个从不同角度的版本,然后区间所,可最终将所有的结果合并去重
- 问题向量化:向量化embedding模型的选取需要和离线处理知识库的模型一致,保持向量维度一致。
- 向量检索+多路召回(粗排):采用ANN搜索,近似最近搜索,找出余弦相似度高的 Top-K 个文档,但是实际工作中不单单只用向量检索的方式查找,因为向量检索是语义理解,对于型号和专有名词匹配效率查,还需要用关键词做到精确匹配,实现多路召回。
- 关键词匹配主要的算法有BM25,向量检索负责语义相似性,而BM25负责捕获关键词精确匹配,主要是应对对型号有严格要求的应用场景。
- 多路查询之后还需要进行结果的融合,采用的算法是 RRF 算法 倒数排名融合,每个文档在不同检索方式下有一个排名,两个排名倒数的加和按照从大到小排序,然后去Top-K
- Rerank(精排):选取Rerank模型(Cross-Encoder结构),将用户问题和每个候选的chunk拼在一起,判断二者的关联性,提取Top-3到Top-5,直接用该模型等于全表查询,时间效率很低
- Prompt 拼接:在将数据交给大模型之前需要额外提醒不可凭空编造,减少幻觉
- 大模型生成+溯源:在生成答案的时候还会标注哪些结论引用的是那些文档,方便跟踪借鉴文档。
大模型的 RAG 主要用来解决什么问题?
- 知识冻结:获取不到训练截止日期之后的任何数据
- 知识私有:公司内部私有文件不会进入训练模型内部,通过RAG可以实现知识私有
- 知识缺失——幻觉:模型没有理论依据会编造出看起来对但是错的答案,通过RAG提供充足的知识库可以减少幻觉的生成
相比于直接微调LLM,RAG解决了什么问题,微调和RAG各自的优势是什么?
按照现在我的记忆来回答,我会回答:首先微调模型本身是一件耗时耗材的事情,需要大量的时间文本去训练并且还需算力中心,单次训练成本大,相比之下RAG更具有性价比,除此之外,想不到其他优势了,
小林还给出亮点:数据更新频繁,不适合微调,其次就是训练黑盒,模型的训练成果很难追溯到哪些文本出现问题
微调和RAG的应用场景或者区别分别是什么
RAG 是为了创建知识库,让ai的回答有更多的理论依据,而微调的使用场景重在通过大量的文本数据锻炼ai在某领域的理解深度,无论从输出格式,行业语气,结果推断上更符合某领域专家风格。
简单的讲 微调解决怎么说的问题(风格),RAG解决说什么的问题(语言基础)
一些比较点
- 推理延迟:微调延迟更低,RAG更高
- 成本消耗:微调更高,RAG较低,上手容易
- 答案溯源:微调不可查,RAG可追溯到具体的chunk
- 知识上线:微调受限于训练质量,RAG受限于context和检索质量
BM25 实现原理
这里是为了重点区别关键词检索和向量检索的区别,采用倒排索引的方式
对于每一个关键词,作为一个桶标记,这个桶里包含了所有含有这个关键词的文章,这样就从原来的从文章中匹配关键字,变成了直接从关键字里去找文章,所以叫倒排索引
查找到含有关键词的文章后,需要对文章做排序,排序方式是 TF-IDF 打分机制
TF:词频,文档中某个词出现的次数,出现越多,越相关
IDF:稀缺度,这个词在所有文档中有罕见
为了去除常见的高频词,给常见词降权,给罕见词加权。
索引构建篇
RAG 中的文档是怎么存的?粒度是多大?详细说说文档切割(Chunking)策略?
我的文档先进行切割,向量化然后存入向量数据库,颗粒度在500-1000 token 过长内容过多,检索不够细致,过少容易拆分大部分内容,文档切割策略,在项目我用到的一个是按照md标题大小进行切割,就是#号切割,一个就是按照token长度切割,满1000就切割
很明显,上面有个问题,如果面试官问我为什么选择这样切割,以及如果按照长度切割,切断语义了怎么办
为什么文档不能直接存入向量数据库
- 向量模型有输入长度,一般是几百到几千token,一篇几千字的文档根本塞不进去
- 检索不细致,由于本质是检索然后加入prompt,在存入的时候没对内容做划分,那么检索的将是整片文章,比如你想找文章的作者,他直接把整个文章连同作者全回复了,检索效果不佳
每个chunk记录的结构组成
- 向量索引
- 原文
- Metadata:记录文件的路径,名称,页码,章节等等附加信息,便于跟踪路径和过滤非文本信息(只搜客服部门的文档),
文档切割策略
- 固定大小切割+重叠覆盖
- 语义边界切割——标点,标题层级,段落切割,详细切割策略,维护一个分隔符优先级列表,先尝试按照段落切割,切太大在按照句子切割,然后再按照标点切割,直到满足 chunk_size的限制
- 特殊内容专项处理
- 对于代码表格类就很难切割,具体策略
- 代码以函数和类为单位切割
- 表格整块保留,转换成md格式存储,这张表作为一个chunk
- 父子切割
- 固定切割导致语义割裂,语义切割粒度又不好把控,用父子切割,小chunk做检索,大chunk做原文,大chunk包含小chunk,一般是通过语义分割切割出小chunk,然后根据合适比例扩大成大chunk,小chunk的意义就类似于标题类,书签
如何规避语义被切割的问题
- 重叠切割,兜底策略,相邻chunk之间头尾有一部分是重叠的,这样无论切割到哪个中间语义,总有一个chunk内包含,初步设计范围在 chunk_size 10%-20%,如果过小,重叠区域覆盖不到中间的完整语义,如果过大,chunk内不同的部分占比过少,需要更多的chunk实现分割,增加存储成本。
- 按语义边界切割(多粒度分层索引),md文件可以直接按照标题层级划分,正常文本可以用NLP****工具(spacy/nltk)识别具体结束的位置,然后以句子,章节,段落为单位填充chunk,直到塞满为止,这样就不会存在语句被截断的情况。
- 句子窗口检索,不关注于如何切割,而是直接将chunk按照单个句子作为向量,检索命中一个句子的时候,返回的时候返回相邻的N个句子,形成一个上下文窗口,将内容LLM,好处颗粒度细,定位精确,相关性高,但是缺点是存储成本大
- 父子切割,存储时同一段存储两份,一份是小chunk一份是大chunk,两者用ID进行关联,检索的时候用小chunk,返回大chunk,用小chunk语义聚焦,召回精度高,返回大chunk上下文完整。
- 命题化切割(摘要索引),直接将文章给大模型,让他把文章总结成一个一个的命题,包含一个完整的陈述句,以及所需的全部上下文内容,单独拿出来也能看懂,不存在语义切割问题,每个命题成为一个chunk,精度高但是需要额外的LLM调用
- Contextual Retrieval (上下文检索),实现方式,对于每段chunk都调用一次LLM,让他结合全文给一个独属于这个chunk的背景说明,比如他在文章中属于哪个章节,主要的内容是什么(context),然后在chunk 向量化之前,将chunk+context,然后一起向量化(embedding+BM25),这样就可以解决语义切割导致的无关联性,同一个背景下的chunk关联性就很大,但是如果切割不当,再加上没有上下文说明,chunk之间的差异性就很大,提高了召回率。
- 用 Prompt Caching 大幅度降低LLM 调用费用,按照上面的描述,每个chunk都需要调用一次LLM,但是每次LLM的prompt中某些前缀字段是一致的,比如
SystemPrompt: "请阅读以下文档,并为接下来的文本块撰写背景..." Document: [整篇文档内容] (这是不变的!) Current Chunk: [第 1 个块的内容] (这是变的)- 那么就可以采缓存的方式将前缀复用,这里的复用不是单纯的把最终的答案存在缓存中然后相同直接调用,而是将读取完之后创建的关联模型利用缓存技术(KV cache)存储,下次就不需要再次阅读这部分的内容并建立关联了,这样首次会消耗额外的缓存费用,但是后续的消耗就会大大下降。
在 RAG 中 Embedding 究竟是什么?如何选择和评估一个 Embedding 模型?
什么是 embedding 模型
将自然语言映射成一个固定长度的浮点数向量,向量构成的空间成为语义空间,每个向量都存在一个方向,一个方向就代表一种语义,基于这个原理,如果两个向量方向越接近,说明语义越接近,而不是单纯的方向越近,越相近,所以这就是为什么选择余弦判断而不是直线距离。
这跟直接查询关键词有什么区别
直接查询属于文本匹配,并非语义理解,对于同一事物会有不同的表达,最明显的就是中英文两种表达,对于固定的关键词,单靠查询是不能完全搜索出来的,虽然我也不知道embedding模型用了什么方式达成了语义理解,但是就是实现了对语义的理解,可以找到表达内容相同相近的语句。
Embedding 模型选择
- 中英文比例,根据知识库的语言选择适合的模型,不同模型对于语言的理解能力不同
- 数据合规要求,有些数据好像不允许存储在国外数据库,优先考虑国内
- 向量维度对存储和检索速度的影响,维度越高精度越好,但是空间和检索时间会增加,百万级别知识库1024维度比较合适,如果规模小1536维可以,好像维度越小越好,毕竟成本降低
如何评估Embedding模型
结合业务场景选择合适的模型,具体评估策略是准备多条问题+答案chunk,然后测验不同模型对问题检索情况,有个指标叫Hit@K,Hit@5=0.8 表示80%的问题都可以在前五个召回档案中找到答案,一般占比70%以下就需要考虑换模型
Embedding 有哪几种算法你了解过吗?
这个有点难了,看的有点迷糊,先【挖坑】吧
什么是向量数据库?有没有做过向量数据库的对比选型?
我认为一个比较经典的问题就是为什么 不选择在mysql添加向量字段,而是选择特别的向量数据库?
回答这个问题明显是从检索方面回答,mysql加向量字段只能说明可以存储向量,但是检索效率呢,mysql的B-tree 索引无法实现对高维向量的排序,mysql的索引创建基于一些可以量化,维度低的数据,然后做的还是精确匹配,对于高维向量,索引结构根本创建不起来,最后只能无脑全遍历,时间直接爆炸。
之所以选择向量数据库,原因在于他的检索算法 ANN 近似最近搜索
具体的实现方式有两种
- HNSW :我不做过多介绍,大体说一下,其实即使一个分层图,每个向量都有一个父节点所属,父节点所属祖父节点,这时候我就想,这不跟btree一样吗,咋就不行了,原因很简单,他不是有序的,mysql的索引逻辑是根据内容进行分层,一步一步的精确到内容维护位置,类似于线段树,找到合适的区间,一步一步的缩短,我问了千问,虽然二者在结构上相同,但是mysql只能创建有序索引,也就是说树的结构必须是有序的,不能做类划分,这就是区别,
- IVF ,设计很多个桶,每个桶代表一类向量数据,然后遍历桶
还有几个辅佐的功能
- medatda过滤,因为chunk由于一个metadata存储着元数据和某些条件,可以用这一点进行初步的过滤,不符合条件的直接过滤掉,然后再检索
- 实时更新,向量数据库支持增删改查
有没有做过向量数据库的对比选型
- Qdrant 利用小规模团队,部署简单 数据规模在万级别
- Chroma 便于上手,适合跑实验和Demo,没有分布式能力,不适合生产,数据规模在百万级别
- Milvus 大厂在用,功能最全,扩展性最强的开源选项,支持分布式部署,读写节点分离,能横向扩容,适合数据在百万到十亿级别,并发查询量大的生产环境,亿级别
讲讲你用的向量数据库?数据量级是多大?性能如何?遇到过性能瓶颈吗?
我们的生产环境是Milvus,数据量级在百万条向量左右,每条是1024维度,应用HNSW索引,单次查询的延迟在20到50毫秒,选 Milvus 主要原因是因为他支持分布式部署和读写分离,适合数据量大,高并发的场景
第一个是内存压力,百万级的 1024 维向量光原始数据就要好几个 GB,后来我们开启了标量量化 SQ8,把 float32 压成 int8,内存直接降到原来的四分之一
第二个是大批量写入的时候会触发后台 Segment 合并,影响查询延迟,我们的解法是把批量写入改到业务低峰期,分批小批次写入
上面是小林的回答,很明显,我看不懂hhhhh
Milvus(miu e s) 使用的原因是单机支撑不住数据量和知识库需要每天有增量更新,需要读写分离不影响查询服务的稳定性
Milvus 的核心概念
Collection 集合:类似关系数据库中的表,存储一类向量数据,一个数据库就是一个Collection
Segment 段:是Milvus内部管理数据的单位,一开始数据先写进增量段,当增量段累积到一定量触发合并,形成封存段,封存段在建立好索引,合并的过程会消耗CPU和磁盘IO,会影响查询效率
Index 索引:加速检索的结构,常用HNSW,分层图算法,将向量组织成分层图,从稀疏的顶层开始,定位到大致区域,然后逐层细化,
标量量化 SQ8:用于将存储向量的float32 4字节变成int8 1字节,就是把小数点后7位的数字保留到小数点后2位,但部分语义信息实际在高位,截掉低位精度损失很小,并且还能将空间压缩到1/4,召回率基本无损失。
实际遇到的问题
- 内存不足,存储150万级别的数据需要6GB,在8G运行十分吃力,到时查询时,操作系统频繁swap,从内存数据换到磁盘,时间从20ms到2S——SQ8量化压缩空间
- 批量写入触发段合并,影响查询效率,异步写入,每次值写入1000条,或者错峰写入,只在晚上查询频率小的时候写入文件
检索优化篇
RAG 检索优化策略有哪些?
这算是对前面的知识做一个总结了,其实回答起来非常简单,只需要回忆一下,哪些部分我们讲到了多种方法实现,就是一个优化地方。
文档切割部分
在该部分,我们讲到了,固定大小切割,语义边界切割,父子切割,命题切割(摘要切割),句子窗口切割,上下文切割。本质就在解决语义切割导致意思不当的问题,这些方法都可以作为优化策略
问题预处理部分
在这一部分,有简单更改,HyDE 文体更改,回退更改,多版本扩展,也是为了提高问题的提示词质量,提高召回率,这些也是优化策略
检索部分
向量检索 ANN 和关键词检索 BM25,又称多路召回,再加上RRF 倒数排名,提高粗排质量
重排序部分
Rerank 的必要性,普通的RAG中其实可以没必要有精排,因为没有他也可以直接问大模型,坏处就是上下文窗口占据大,所以重排序也是一种优化策略。
了解哪些更复杂的 RAG 范式
- Naive RAG 普通检索
- Advanced RAG 加了问题预处理和精排
- Modular RAG 每个流程形成一个模块,根据具体问题判断使用不同的方法,比如多路召回有 向量检索和关键词检索,有时候可以多路召回,有时候可以选择单独使用向量检索。
更高级的几个范式
- Self RAG 让ai自动判断是否需要检索,对于一些简单的问题无需检索,实现方式竟然需要微调,成本高
- CRAG,在精排后面添加一个决策器,根据精排的打分判断是否相关,不相关就采取兜底策略网络搜索。
- Graph RAG,通过知识图谱增强检索,实现多步跳跃,通过知识库推断出多个实体之间的关联
- Agentic RAG,判断是否在进行一次,是否需要更改关键词,答案是否足够等,全部都由agent判断。
生产落地篇
如何规避 RAG 系统中大模型的幻觉?
- Prompt 强约束:在系统提示词中写明【不允许凭空捏造】【不允许在召回相关性不高的情况下超常发挥】【严格按照资料回答】
- 检索质量把控:Rerank分数设置阈值,不达标的直接舍弃
- 生成答案后核查,用LLM再次核查答案是否相关
- 结构化输出,方便溯源,json格式,结论标注来源编号
小林在文章说,幻觉的来源方向分两种:凭空捏造和低质量超常发挥。
怎么量化你的 RAG 效果?
在线指标和离线指标,在线指标就是通过用户反馈汲取和转人工率,离线指标就是在检索层和生成层做好评估,保证检索成功率
检索层评估
Hit@K——Hit@5=90% 代表90%的问题都可以在前五条返回数据中找到答案,检验的是召回数据中是否存在答案
MRR——平均倒数排名,1代表第一名,0.5代表第二名,0.33代表第三名,如果是0.33就代表虽然返回的数据中有正确答案,但是基本都在第三名或者第四名,排名靠后,检验答案是否排名靠前
生成层评估
RAGAs 本质是 [LLM-as-a-Judge]
Faithfulness 忠诚度——将生成的答案每一句都问LLM,是否存在chunk来源,越没有依据,就越低,这个用来判断幻觉程度
Answer Relevancy 答案相关性——用LLM检验问题和答案是否相关
Context Recall 上下文召回率——检索到的内容是否覆盖了标准答案中的所有必须事实,有提前给出标准答案作为参照,不单单是Top-k,指的是正确文章的数量,说明多路召回不足
Context Precision 上下文精确率,空间所内容里噪音多不多,Rerank没过滤掉无关内容
成本缩减
因为本质每次都是问LLM,降低成本的方法有两种,一种就是抽取核心数据集然后测试,降低测试范围,第二个就是使用推理能力弱,成本低的LLM
RAG 知识库如何实现动态与持续更新?
偷个懒,就不详细写了
- 更新策略——先删后添,有一个更加保险的手段,并行写入,有版本区别,检查没问题再删除,因为容易直接将旧版本删除,无法回滚。
- 维护策略——每个chunk有hash值,文档ID和chunkID命名格式高度绑定,要么 chunk ID 前缀直接包含文档ID,要么 metadata数据中包含文档ID,每次检查文档是否更改就直接检查hash值,算法MD5加密算法,hash值不一样就说明更改过了
- 感知方式
- 定时****轮询,按照固定的时间间隔,扫描所有的文档对比hash值是否一致,把所有变动文档重新处理,注意,在生产环境中,一般不会直接对RAG系统的向量数据库发生更新,这种操作一般是实践驱动,也就是直接上传文件,但是这在生产级别的RAG系统中是不允许用户上传文件,这样会恶意攻击向量数据库,所以一般是更新文档所在的本地数据库,比如mysql,这时候就需要RAG系统主动轮询数据库比较hash值大小以此来判断是否需要更新向量数据库。
- 事件驱动,这个跟轮询是反着来的,轮询是RAG主动询问数据库有没有更新数据,而事件驱动属于更新方,比如数据库发生刚更新,就会在消息队列发送消息,然后异步发送到RAG系统执行更新程序,或者更新后自动发送http请求实现更新。
在实际落地中,你觉得 RAG 最难的地方是哪里?
最后一个了,直接润!我抄!
我觉得 RAG 最难的不是把它跑起来,一个基础的 Demo 一两天就能搭起来,难的是把它调好。工程上最让我头疼的有三块。
第一是文档预处理,原始数据的格式五花八门,PDF 里面的表格、图片、嵌套的格式,处理不好就是一堆乱码进了知识库,进去的是垃圾出来的也是垃圾。
第二是检索质量的调优,向量召回不准是整个系统效果的天花板,但问题来源很多,Chunking、Embedding、Query 改写,任何一个环节出问题都会影响结果,排查起来很费劲。
第三是效果评估,答案对不对很难系统性地衡量,不知道是哪个环节出了问题,优化就变成了瞎猜。

博主个人学习RAG的学习笔记
浙公网安备 33010602011771号