10. 你使用 RAG 给大模型一个输入,系统是怎样的工作流程?
当你把一个问题输入给 RAG 系统,它不会直接丢给大模型,而是先经历一套「检索 -> 整理 -> 生成」的流水线。
具体来说:系统先对问题做预处理(改写成更适合检索的形式),然后把问题向量化,去向量库里找最相关的文档片段,再经过精排筛掉噪音,最后把筛选出来的片段和问题一起拼成 Prompt 交给大模型,大模型基于这些「参考资料」生成最终答案。
整个流程的核心目标只有一个:让大模型在回答时有真实的知识作为依据,而不是凭空发挥。
为什么不能直接把问题扔给大模型
先说为什么需要这套流程。
大模型的上下文窗口是有限的,不可能把整个知识库都塞进去;而且没有外部知识依据时,大模型只靠参数里的记忆回答,容易出现幻觉。所以 RAG 的在线流程本质是一个「精准取件」的过程:从海量知识里找到和这个问题最相关的那几段,然后再让大模型在这个「小范围」里作答。
你可能会想,为什么不把用户问题直接拿去向量库里搜,搜完就完了?因为在实际业务中,用户的提问往往是口语化的、带指代的、甚至有歧义的,直接拿去检索效果极差。所以整个在线流程按顺序分为以下几步,每一步都在为下一步准备更好的输入。
第一步:Query 预处理(Query Rewrite)
为什么要把 Query 预处理放在第一步?因为用户原始提问的质量决定了后续所有环节的天花板。如果输入就差,后面再怎么精排、再怎么拼 Prompt 都是白搭。
用户的提问往往是口语化的,甚至带着指代,比如「上次那个方案怎么样」,这种问题离开对话上下文完全没法检索。另外,就算问题表达清楚,用词和知识库里文档的用词可能完全不一样,直接拿去检索命中率会很低。
这一步通常让一个小模型对原始问题做改写。常见技术有几种。
一是简单改写,把口语化问题改写成更正式、独立完整的检索句。
二是 HyDE(Hypothetical Document Embeddings),让 LLM 先「假设」一个可能的答案,用这个假设答案的向量去检索。你可能会觉得奇怪,不用问题去搜反而用假设答案去搜?原因很简单:问题和答案的用词往往差异很大,但假设答案和真实答案的用词更接近,它们的语义空间天然更匹配,命中率往往更高。
三是多角度扩写,把同一个问题扩展成 3-5 种不同表述,分别检索后合并结果,覆盖面更广。
第二步:Query Embedding(问题向量化)
Query 改写好了,接下来要把它转换成向量才能去向量库里搜索。把处理后的问题用 Embedding 模型转成向量,这一步本身很简单,但有一个容易忽略的细节。
很多人以为随便选一个 Embedding 模型就行了,其实不然,必须用和离线建库时完全相同的 Embedding 模型。
为什么?因为不同 Embedding 模型在训练时见过的数据、目标函数、输出维度、向量空间的"形状"都不一样,模型 A 让「苹果手机」和「iPhone」的向量落在空间里的某个方向,模型 B 完全有可能让它们落在另一个方向,甚至连维度数都对不上(A 是 1024 维,B 是 1536 维,根本没法算距离)。用 A 模型建的库,如果用 B 模型来检索,两边的向量就像在不同坐标系里,距离计算完全没有意义,检索结果会一塌糊涂。
这就像你用经纬度定位,但一个系统用的是北京坐标系,另一个用的是 WGS84 坐标系,数值看起来差不多但实际位置差了十万八千里。这也是为什么一旦换 Embedding 模型,整个知识库必须重建——老向量和新查询不在同一套坐标系里,没法兼容。
第三步:向量检索(ANN 搜索)+ 多路召回
拿着问题向量,去向量数据库里做近似最近邻搜索(ANN),找出余弦相似度最高的 Top-K 个文档片段。这一步速度非常快,即使百万量级的向量库,通常几十毫秒就能返回结果。
但工程实践中,只用向量检索这一路往往不够。所以这一步通常同时进行多路召回:向量检索负责捕获语义相似性,BM25/全文检索负责捕获关键词精确匹配,两路各有所长。
很多人以为向量检索已经够用了,为什么还要 BM25?因为向量检索对精确词语(比如产品型号、专有名词、错误拼写)的识别能力比较弱,而 BM25 对这些精确匹配反而更在行。
比如用户问「LSTM 和 Transformer 的区别」,向量检索能找到「序列模型对比」相关的语义内容,BM25 能精准命中包含「LSTM」「Transformer」这两个词的文档。多路结果通过 RRF(互倒排名融合)算法合并,最终召回的结果比单路覆盖面更广、质量更高。
第四步:Rerank 精排
理解了第三步的粗排召回,你会发现一个自然的问题:Top-20 的结果里不可能条条都相关,肯定混了一些干扰项进去。Rerank 就是为了解决这个问题的。
向量检索是「粗排」,召回的 Top-K(比如 20 条)里可能混入相关度不高的干扰片段。Rerank 模型(Cross-Encoder 结构)会把用户问题和每个候选片段拼在一起输入,深度理解它们之间的语义匹配程度,重新打分排序。最终只保留 Top-3 到 Top-5 的高质量片段,把噪音过滤掉。
你可能会问,为什么不直接用 Rerank 模型来检索,还要先粗排再精排?因为 Rerank 是 Cross-Encoder 结构,需要把查询和每个候选拼在一起过模型,计算量比向量检索大得多。如果拿它对百万条数据逐一算分,延迟完全不可接受。所以工程上采用「粗排筛到几十条,精排再从几十条里挑最好的几条」这种两阶段策略,兼顾速度和质量。Rerank 整体耗时通常在几百毫秒以内,对用户体感影响不大,但检索质量的提升非常明显。
第五步:Prompt 拼装
精排后的高质量片段拿到了,接下来要把它们和用户的原始问题组装成 Prompt 交给大模型。典型模板大概是这样:
prompt = f""" 你是一个专业助手,请根据以下参考资料回答用户的问题。 如果参考资料中没有相关信息,请回答「根据现有资料无法回答」,不要自行猜测。 参考资料: [1] {chunk_1} [2] {chunk_2} [3] {chunk_3} 用户问题:{user_query} """你可能会觉得这个 Prompt 挺简单的,没什么好讲的。但这里面每一条指令都有明确的工程意图。很多人以为只要把检索结果丢给大模型就行了,其实 Prompt 拼得不好,大模型一样会乱回答。
「只根据资料回答」是为了抑制大模型凭记忆发挥的倾向,大模型天生喜欢「帮忙」,你问什么它都想着要回答,哪怕资料里没有相关信息它也会自己编一个出来。
「资料没有就说不知道」是防止它在信息不足时强行补全出幻觉内容。参考资料带编号是为了方便后续做引用溯源,让用户知道每句话的依据是什么。
第六步:大模型生成 + 溯源
大模型拿到 Prompt 之后,基于参考资料生成答案。到这一步,整个 RAG 在线流程的核心工作就完成了。
工程实践中通常还会要求大模型在答案里标注每句话来自哪个片段(比如「根据资料[1]...」),这样用户可以追溯到原始文档,验证答案是否准确,可信度大幅提升。很多人以为 RAG 生成的答案就是最终答案,不需要再验证了,其实溯源这一步非常关键。大模型即使在有参考资料的情况下,依然可能过度发挥或者误读资料,溯源让用户有能力判断哪些内容是可靠的、哪些需要再确认。
整个链路串起来,在线阶段的耗时分布大概是:Query 改写几十毫秒,向量检索几十毫秒,Rerank 几百毫秒,Prompt 组装可忽略不计,大模型生成根据输出长度在 1-10 秒不等。工程上常见的优化手段是缓存高频 Query 的检索结果,以及把向量检索和 BM25 检索并行执行,把两路召回的延迟从串行叠加变成取最大值。
正确回答要按顺序把六个步骤讲清楚。
第一步 Query 预处理(改写、HyDE、多角度扩写),把口语化的问题转成适合检索的形式;
第二步 Query Embedding,注意必须用和建库时相同的模型;
第三步向量检索 + 多路召回,向量检索和 BM25 各有所长,通过 RRF 融合结果。
第四步 Rerank 精排,用 Cross-Encoder 深度理解语义,把粗排的噪音过滤掉;
第五步 Prompt 拼装,明确约束 LLM 只根据资料回答、不知道就说不知道;
第六步生成 + 溯源,标注引用来源提升可信度。
11. 请你介绍一下向量检索和关键词检索的区别?
关键词检索(BM25 这类)靠的是词频统计,看查询词在文档里出现了多少次,擅长精确命中;向量检索靠的是语义空间里的距离,能理解「换了种表达方式的同一个意思」,擅长模糊语义匹配。两者各有盲区:BM25 遇到同义词就没辙,向量检索遇到专有名词、产品型号这类精确词就容易漏。所以 RAG 系统里通常两路都跑,向量检索捕获语义相关的内容,BM25 精准命中关键词,再用 RRF 算法把两路结果合并,这样覆盖面比单路宽很多。
检索要解决的核心问题
检索的本质是:给你一段查询,从海量文档里找出最相关的那几条。
这个「相关」可以有两种理解方式,一种是字面意义上的词汇重叠,一种是语义层面的意思接近。很多人以为检索就是「搜关键词」,其实那只是其中一种理解方式。关键词检索和向量检索分别对应这两种理解,各有各的擅长和盲区。理解了这一点,后面的内容就顺理成章了。
关键词检索:字面匹配,靠统计
先从最直观的方式说起。关键词检索的代表是 BM25(Best Match 25),这是 Elasticsearch、Lucene 等传统搜索引擎的核心算法。
它的基本思路可以用一个类比来理解:想象一个巨大的图书管理员,他给每本书建了一张「词汇卡片」,记录这本书里每个词出现了几次。用户来查「手机 截图」,管理员就把包含「手机」的书挑出来,再把包含「截图」的书挑出来,然后看哪些书同时被挑中、被挑中的词出现频率高,这些书就排前面。这个「词汇卡片」系统就是倒排索引,它记录的不是「每篇文档里有哪些词」,而是反过来,记录「每个词出现在哪些文档里」。
打分的时候核心看两个因素。
第一个是词频(TF):这个词在这篇文档里出现了几次,出现越多说明越相关。
第二个是稀缺度(IDF):这个词在所有文档里有多罕见。
你可能会想,直接数词频不就行了,为什么还要看稀缺度?
原因很简单:「的」「是」「在」这些词在哪篇文章里都高频出现,如果只看词频,每篇文档都会因为包含这些常用词而拿到高分,根本区分不出哪篇真正相关。所以 IDF 的作用就是给常见词降权、给罕见词加权,让真正有区分力的词决定排序结果。
BM25 在 TF-IDF 基础上又加了饱和度限制,防止某个词重复出现太多次后权重无限叠加。
# 用 rank_bm25 库做关键词检索的例子 from rank_bm25 import BM25Okapi # 文档库(实际使用时是分词后的 token 列表) corpus = [ ["苹果", "手机", "截图", "方法"], ["iPhone", "截屏", "教程"], ["安卓", "手机", "拍照"], ] bm25 = BM25Okapi(corpus) # 查询也要分词 query = ["苹果", "手机", "截图"] scores = bm25.get_scores(query) # 每个文档的 BM25 分数BM25 的优势是对精确词汇命中率极高,产品型号「iPhone 15 Pro Max」、专有名词「LSTM」、缩写「RAG」,只要文档里有这个词,BM25 就能精准找到。
但它的劣势同样明显,而且刚好是向量检索的优势所在:遇到同义词就束手无策。
用户查「手机截图」,文档里写的是「iPhone 截屏教程」,BM25 看到没有任何词汇重叠,分数为零,直接忽略这篇文档,哪怕它正好是最相关的答案。这个致命的盲区,直接催生了向量检索的出现。
向量检索:语义匹配,靠 Embedding
BM25 搞不定同义词,那能不能让检索系统「理解意思」而不是「匹配字面」?这就是向量检索要解决的问题。
它的思路是这样的:先用一个 Embedding 模型把每段文本转成一个高维数字向量(比如 1024 维的浮点数列表),这个向量可以理解成这段文本在「语义空间」里的坐标。语义相近的文本,坐标就靠近;语义不相关的,坐标就离得远。好比给每段文字在地图上钉了个钉子,意思越相近的钉子挨得越近。用户查询来了,也转成向量,然后去找最近的几个钉子。
这样就好理解了:「苹果手机怎么截图」和「iPhone 如何截屏」这两句话,虽然一个字都不一样,但经过 Embedding 之后,向量的余弦相似度可以高达 0.95。向量检索通过近似最近邻算法(ANN)在向量库里找和查询向量最近的 Top-K 条,速度极快,百万量级的向量库通常几十毫秒就能返回结果。
那向量检索是不是万能的?很多人以为向量检索比关键词检索「高级」,所以所有场景都该用向量检索,其实不是。
向量检索的优势是语义理解能力强,能跨越同义词、近义词、不同表达方式的障碍;但它的劣势是对精确词汇不敏感,产品型号「Pro Max 256GB」、人名、版本号这类词,在 Embedding 空间里可能彼此距离并不近,向量检索容易把它们混淆在一起,反而不如 BM25 直接命中准确。
这就是为什么面试官会拿「M4 Pro 芯片参数」这个问题来追问:向量检索对这类精确专有名词天然不擅长,而 BM25 却能秒找到。两种方式的盲区恰好互补,谁也不能替代谁。
混合检索:两路都跑,合并取长补短
既然两种方式各有盲区,而且刚好互补,那最自然的想法就是两路都跑。好比考试既考选择题(精确匹配)又考问答题(语义理解),两科综合评分比只考一科更全面。工程实践中的标准做法叫混合检索(Hybrid Search):同时跑向量检索和 BM25,各自召回一批候选,然后用 RRF(Reciprocal Rank Fusion,互倒排名融合)算法把两路结果合并排序。
RRF 的思路很巧妙:它不看各路的原始分数,只看排名。你可能会问,为什么不直接把两路的分数加权平均呢?原因是向量相似度(0 到 1 的余弦值)和 BM25 分数(可能是任意正数)的量纲完全不同,就像拿摄氏度和华氏度直接加在一起没有意义。RRF 用排名的倒数来打分,排名越靠前倒数越大,最终按总分降序排列。这样,两路都认为相关的文档会排在最前面,只有一路认为相关的文档也不会被完全丢掉。
def reciprocal_rank_fusion(results_list, k=60): """ results_list: 多路检索结果,每路是一个 [doc_id, ...] 的有序列表 k: 平滑参数,防止排名第 1 的文档权重过大,通常取 60 """ scores = {} for results in results_list: for rank, doc_id in enumerate(results): if doc_id not in scores: scores[doc_id] = 0 # 排名越靠前(rank 越小),倒数越大 scores[doc_id] += 1 / (rank + k) # 按总分降序排列,取 Top-K return sorted(scores.items(), key=lambda x: x[1], reverse=True) # 使用示例 vector_results = ["doc_a", "doc_b", "doc_c"] # 向量检索的排序结果 bm25_results = ["doc_b", "doc_d", "doc_a"] # BM25 的排序结果 merged = reciprocal_rank_fusion([vector_results, bm25_results]) # doc_b 两路都高排,doc_a 也两路命中,会排在前面这个方案还有一个工程上的好处:两路检索可以并行执行,总延迟取两路中的最大值而不是两者相加,几乎没有额外的时间代价,但召回质量比单路明显更好。这也是为什么生产环境的 RAG 系统很少只用纯向量检索,混合检索已经成为行业默认做法。