news 2026/10/20 5:21:07

RAG完整链路实战:文档切分、向量检索与生成全流程拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RAG完整链路实战:文档切分、向量检索与生成全流程拆解

做AI应用开发的朋友应该都有这种感觉:模型本身的生成能力再强,一问到私有文档、企业资料、实时信息,立刻就露怯。你问它最新的产品说明,它一本正经地编;你让它基于某份内部报告回答,它完全不知道那玩意儿存在。RAG(检索增强生成)就是为了解决这个问题出现的。它不修改模型参数,而是在模型回答之前,先从外部知识库里检索出相关片段,连问题一起塞进Prompt,让模型“开卷考试”。这篇文章我把RAG的完整链路从零捋一遍:文档怎么加载、文本怎么切、向量怎么算、索引怎么建、检索怎么查、答案怎么生成,配套的代码逐行拆解,最后再说说我在实际项目里踩过的坑。

适合看这篇的人很明确:准备做知识库问答、企业文档助手、客服智能应答,或者单纯想把私域数据接到大模型上的开发者。我不会绕弯子,也不堆概念,直接按链路往下走。

1. RAG链路全貌:从架构到数据流

1.1 为什么必须要用RAG,而不是硬背或无限加长

讨论RAG之前,得先理解它要替代什么。过去大家最常用的手段有两个:一是微调,二是把文档全塞进上下文。

微调的问题很明显:成本高,而且知识更新慢。今天改了一版产品手册,微调模型要重新准备数据、重新训练、重新评测,一整套流程下来以天甚至周为单位。对于内容频繁变动的场景,微调完全跟不上节奏。另外,微调本质上是“改变模型的记忆”,它会带来模型能力的偏移——学业务知识的同时忘了通用能力,这是很常见的事情。

把文档全塞进上下文是另一个思路。现在的模型上下文窗口越来越大,好像没问题?但长上下文的成本是指数级上升的,而且模型对超长上下文的注意力衰减很严重。你给了它10万字的资料,它很可能漏掉最关键的细节,回答得比没给资料还差。还有一个现实问题:业务知识库动辄几千份文档,全部塞进去在工程上根本不现实。

RAG的思路则完全不同:知识不走参数,走检索。系统先把文档切成小块、向量化、建索引;用户提问时,把问题也向量化,在索引里召回最相关的几段,拼进Prompt。这样每次调用大模型只带“开卷考试需要的几页纸”,成本低、更新快、可解释性强。架构上理解,RAG就是把“记忆”从黑盒参数里抽出来,放到了你能控制的外部存储里。

1.2 RAG完整链路拆解:离线建库与在线查询

整个RAG链路可以清晰分成两个阶段。

离线阶段的产物是知识索引,俗称“入库”。流程是:原始文档 → 加载解析 → 清洗去噪 → 文本切分 → Embedding向量化 → 写入向量索引。这个阶段通常跑在任务队列或脚本里,文档更新时增量重跑。

在线阶段的产物是最终答案。流程是:用户输入问题 → 问题向量化 → 检索Top-K相似片段 → 可选的Rerank重排序 → 拼接Prompt → 调LLM生成 → 返回带引用的答案。这个阶段要求低延迟,通常在100ms到2s内完成检索和生成。

这里有几个容易被忽略的底层逻辑。离线阶段决定“知识能不能被找到”,也就是召回率的天花板;在线阶段决定“找到的内容能不能被正确使用”,也就是最终的答案质量。很多项目上了RAG发现效果差,80%的问题出在离线阶段——文档没切好、向量没算对、索引脏数据太多。在线阶段反而相对好调。

1.3 链路各环节的可替换性

RAG的每一环都不是只有一种实现方式。文档解析可以用正则硬抠,也可以用专业的解析库;切分可以是固定长度,也可以按语义递归切;向量化有开源的Embedding模型,也有云端API;索引可以用简易的Numpy矩阵,也可以用专门的向量数据库;重排序可以用,也可以不用。

这种可替换性恰恰是RAG最大的工程优势。你可以先用最简单的方式把链路跑通,哪个环节拖后腿再针对性换掉。我下面的实战就遵循这个原则:先用纯Python加Numpy实现一个朴素版RAG,跑通之后再告诉你每个环节在真实项目中该往哪个方向升级。

2. 数据准备与索引构建:决定RAG质量的上游工程

2.1 文档加载与清洗:格式解析看着简单,实际最耗时间

文档加载是RAG链条里最不起眼、也最容易翻车的一步。你拿到的原始数据永远没有想象中干净:PDF里提取出来的文字顺序是乱的,表格被拆成碎片,扫描件是图片根本没有文本层,Word文档里还带着批注和修订记录。

我处理过一批真实项目里的产品手册,PDF看起来排版很整齐,但用解析库提取后,标题和正文字号信息混在一起,段落顺序完全错乱。后来单独写了清洗逻辑:识别标题级别、过滤页眉页脚、把换行符统一成段落标记、去掉目录页的重复导航。这一套下来,文本质量明显提升,后续的检索效果也跟着上去了。

清洗的基本原则是:保留文档的结构化信息(标题、层级),去掉与正文无关的噪音(页码、水印、重复的目录项)。因为后续切分如果能把标题保留在片段里,向量化时上下文信息会丰富很多。很多新手直接跳过清洗直接切分,结果向量库里的垃圾片段成片出现,检索出来的东西读都读不通。

2.2 文本切分策略与参数设定:不是字越多越好

文本切分是整个RAG链路里最讲究“手艺”的环节。切得太粗,一个片段里包含多个主题,向量化后语义被稀释,检索时匹配不精确;切得太细,单个片段语义不完整,经常出现上句不接下句,召回了也没法用。

最常见的基础策略是固定长度切分,加一个重叠窗口。伪代码逻辑很简单:

chunk_size = 500 # 每个切片的字符数 chunk_overlap = 50 # 相邻切片重叠的字符数 start = 0 while start < len(text): end = start + chunk_size chunk = text[start:end] start = end - chunk_overlap

重叠窗口的作用是防止语义断层。比如一段话跨越了切分的边界,前半部分说“根据上述分析”,后半部分才是真正的结论。如果切片之间没有重叠,这个“结论”片段丢失了“根据上述分析”的指代信息,语义不完整。实际项目中,重叠量一般取chunk_size的10%~20%,也就是500字切片配50~100字重叠。

但固定长度切分有明显的硬伤:它在任意位置硬切,很可能把一句话、一个词组从中间劈开。中文尤其明显——一个成语被切成两半,或者一个专有名词被拦腰切断,向量化时语义完全变味。

更稳妥的方案是递归切分:先按段落标记(换行)切,段落太长再按句子结束符(句号、问号、感叹号)切,句子还是太长再按逗号、分号、空格切。这样每一层都在语义边界处断开,保留完整句子。我把这个思路放到代码里做验证,同样一个文档,递归切分出来的片段可读性和检索命中率都比固定切分明显高出一截。

2.3 Embedding向量化:模型怎么选、向量怎么用

文本切好之后,下一步是把每一段文字变成一串数字向量。Embedding模型做的事情是理解文本语义,然后把语义映射到高维空间的一个点上。语义接近的文本,它们在向量空间里的距离也接近。

选Embedding模型有三个核心标准。第一,领域匹配度。医疗、法律、代码、金融这类垂直领域需要专门的模型或专门的微调版本,通用模型在这些领域表现往往一般。第二,语言支持。中文场景必须选中文理解能力强的模型,很多英文模型对中文的支持很鸡肋。第三,维度与性能。维度决定存储成本和计算速度,768维和1024维的模型效果有差距,但也不是越高越好,要结合你的语料规模权衡。

向量使用上的细节更不能忽视。检索时通常用余弦相似度衡量两个向量的语义距离,范围在-1到1之间,越接近1越相似。但在实际编码中,很多Embedding模型输出的向量本身带有不同的模长,直接用点积会受模长干扰。稳妥做法是:入库前先把所有向量归一化到单位长度,查询向量也归一化,这样点积就等于余弦相似度,省一次计算。

def normalize(vectors): norms = np.linalg.norm(vectors, axis=1, keepdims=True) return vectors / norms

这一小步很多人忽略。如果你的向量库距离计算的结果异常,先检查是不是忘了归一化。

2.4 索引构建:先拿Numpy搭一个极简向量库

不要一上来就上企业级向量数据库。链路还没跑通,复杂基础设施只会掩盖问题。我先用一个Numpy矩阵做简易向量索引,数据量不超过几十万条时性能完全够用。

索引的构建逻辑:把所有切分片段的向量拼成一个二维矩阵,每一行是一个片段的向量。查询时,把问题向量和矩阵做一次批量点积,取Top-K。

class SimpleVectorIndex: def __init__(self): self.vectors = [] self.texts = [] def add(self, text, vector): self.texts.append(text) self.vectors.append(vector) def search(self, query_vector, top_k=3): matrix = np.array(self.vectors) scores = matrix @ query_vector top_idx = np.argsort(scores)[-top_k:][::-1] return [(self.texts[i], scores[i]) for i in top_idx]

这个索引结构虽然简单,但已经把核心思想表达清楚了。真实项目里,这段逻辑被封装进向量数据库产品,底层换成HNSW这类近似最近邻算法,能支撑百万级以上规模。但原理是同一个:把向量矩阵化,批量算相似度,取最接近的若干条。

3. 检索与生成:把知识正确送到模型嘴边

3.1 Top-K挑选:不是越多越好,要控制Prompt质量

检索返回的Top-K结果会被拼进Prompt,而Prompt最终是喂给大模型的内容。这个K值的选择直接影响答案质量,而且影响方向并不总是“越多越好”。

K值太小,比如1,召回的信息可能覆盖不全,模型缺依据。K值太大,比如10,一些低相关度的片段混进来,反而干扰模型判断。我在实战中的经验是:默认K=3起步,通过评测集调优,大部分场景落在3~5之间。关键指标是“最后一名的相关度”——如果第5名已经开始明显跑偏,说明K=5太激进了。

另外,检索结果需要按照相似度从高到低排列后再拼进Prompt。模型对顺序很敏感,把最相关的内容放在最前面,回答质量会明显更稳。有人在代码里忘了排序,把召回的片段按原文顺序或随机顺序塞进Prompt,结果模型被后面的低质量片段带偏。

3.2 重排序:召回靠粗排,排序靠精排

用Embedding向量做相似度检索本质上是“粗排”。它在召回效率上很有优势,但语义匹配精度有限。比如用户问“责任免除条款有哪些”,某段文档确实提到了“责任免除”,但只是顺带一提,核心内容是理赔流程,向量相似度也会给出比较高的分。

这时候引出一个环节叫Rerank重排序。思路是:先用向量检索粗召回Top-50,再用一个专门的重排序模型对50条结果逐对精排,选出真正Top-3。重排序模型通常比Embedding模型更精细,能捕捉更深层的语义关系。代价是计算量更大,所以不能对所有文档做重排,只对粗召回结果做。

引入重排序是我在项目里收益最大的优化。它不改变召回路径,只改变排序结果,但最终答案质量能提升一个档次。代价是多一次模型调用,延迟增加100ms左右,对于质量优先的项目非常值得。

3.3 Prompt模板设计:让模型学会“照着念”

Prompt设计在RAG链路里经常被低估。检索做得再好,Prompt写得稀烂,模型依然会自由发挥。

我的Prompt模板固定包含四块内容:角色设定、参考资料、任务指令、问题本身。角色设定让模型切换到“业务专家”模式;参考资料直接放检索结果;任务指令明确“只能依据参考资料回答,资料中没有就明确说不知道”;最后才是用户问题。

prompt = f""" 你是一名严谨的客服助理。请严格依据下面的参考资料回答问题。 如果参考资料中没有相关信息,请直接回答"资料中未找到相关内容",不要编造。 参考资料: {context} 问题:{query} """

这里最关键的指令是“禁止编造”。大模型的默认行为是尽量回答,哪怕不知道也会猜。你必须用明确的指令压住它的这种倾向。加了这一句之后,幻觉问题会显著下降,代价是回答可能变得保守,但保守在RAG场景里是优点。

3.4 生成参数与引用溯源

生成环节还有几个参数值得注意。temperature设置为0或接近0,保证输出确定性,这是RAG问答场景的标配。top_p也相应调低,避免采样引入随机性。模型回复时,建议在输出结果里附带上来源片段。这不算模型生成能力,而是工程上在Prompt里要求模型输出形式,或在后处理时把命中的文档ID关联进去。带引用的答案不仅是更好的用户体验,更是调试RAG时定位问题的关键——哪一句话是模型编的,哪一句话是引用里有的,一目了然。

4. 完整代码逐行拆解:一个可运行的朴素RAG

4.1 环境准备与整体结构

代码使用Python实现,只依赖Numpy来处理向量计算。Embedding模型向量化、LLM生成两个环节我用函数占位,你接入自己手头可用的模型服务即可。整体按五个模块组织:文档加载、文本切分、向量化、索引构建、查询生成。每一步都能独立调试,出了问题也容易定位。

pip install numpy

4.2 文档加载与切分实现

def load_document(filepath): with open(filepath, "r", encoding="utf-8") as f: return f.read()

加载逻辑很简单,但不代表不重要。真实项目中这里会有各种格式解析逻辑,这里先用纯文本示例。

def split_text(text, chunk_size=500, chunk_overlap=50): """按固定长度切分文本,保留重叠窗口。""" chunks = [] start = 0 while start < len(text): end = start + chunk_size chunks.append(text[start:end]) start = end - chunk_overlap return chunks

这只作为最基础的切分实现。为了更贴近实用,我在批量跑数据时会把固定切分换成递归切分:

def recursive_split(text, chunk_size=400, chunk_overlap=80): separators = ["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] # 先按段落分,段落还超长再按句子分,逐层降级 ...

递归切分不是万能,但它在绝大多数场景下的表现都优于直接硬切。选400~500作为chunk_size,是因为这个长度足够覆盖一个完整的知识点,又不会让向量化语义过于分散。

4.3 向量化与索引构建实现

import numpy as np def embed_texts(texts): # 调用Embedding模型接口,返回向量列表 # 示例:vectors = model.encode(texts) vectors = your_embedding_model(texts) return np.array(vectors) def normalize(vectors): norms = np.linalg.norm(vectors, axis=1, keepdims=True) return vectors / norms

注意embed_texts里的注释:这里需要替换成你自己的Embedding模型调用方式。常见做法是加载一个开源模型,一次性批量编码所有片段;如果你的语料规模很大,分批编码,避免内存爆掉。

索引构建就是把编码并归一化后的向量逐条塞进SimpleVectorIndex:

class SimpleVectorIndex: def __init__(self): self.vectors = [] self.texts = [] def add(self, text, vector): self.texts.append(text) self.vectors.append(vector) def search(self, query_vector, top_k=3): matrix = np.array(self.vectors) scores = matrix @ query_vector top_idx = np.argsort(scores)[-top_k:][::-1] return [(self.texts[i], scores[i]) for i in top_idx]

matrix @ query_vector是Numpy的矩阵乘法,等价于批量计算点积。因为前面已经归一化,点积结果就是余弦相似度。

4.4 检索与生成流程实现

def answer_question(query, index, top_k=3): # 1. 问题向量化并归一化 query_vector = normalize(embed_texts([query]))[0] # 2. 检索Top-K相关片段 results = index.search(query_vector, top_k=top_k) context = "\n\n".join([text for text, _ in results]) # 3. 组装Prompt prompt = f""" 你是一名严谨的客服助理。请严格依据下面的参考资料回答问题。 如果参考资料中没有相关信息,请直接回答"资料中未找到相关内容",不要编造。 参考资料: {context} 问题:{query} """ # 4. 调用LLM生成最终答案 answer = call_llm(prompt) return answer

这个流程里值得留意的细节是:检索结果按相似度降序排列后拼进context,模型最先读到的就是最相关的信息。call_llm同样是占位函数,实际接入大模型服务时,用流式输出或普通模式都行,但temperature务必设为0。

4.5 主流程串联与运行验证

现在把整个流程串起来:

if __name__ == "__main__": # 1. 加载原始文档 text = load_document("manual.txt") # 2. 切分文本 chunks = split_text(text, chunk_size=500, chunk_overlap=50) # 3. 向量化并归一化 vectors = normalize(embed_texts(chunks)) # 4. 构建索引 index = SimpleVectorIndex() for chunk, vector in zip(chunks, vectors): index.add(chunk, vector) # 5. 测试提问 query = "如何申请退货?" answer = answer_question(query, index) print(answer)

用一段3000字左右的说明文档做测试语料,跑一遍下来,正常情况能回答出与文档内容一致的结果。如果答非所问,不要先怀疑模型,去检查召回的Top-3内容到底是不是相关的。把中间结果打印出来看,是我调试RAG时最常用的手段:

for text, score in index.search(query_vector, top_k=3): print(score, text[:100])

这一步能直接定位问题到底出在检索还是生成上。

5. 常见问题排查与进阶优化实录

5.1 召回不到正确内容时,按这个顺序排查

检索结果不对是最常见的RAG翻车现场。我的排查顺序是固定的一套。

第一步,检查切分。是不是文档标题和正文被切开了?是不是片段太短导致语义缺失?把切分结果打印出来,肉眼扫一遍。第二步,检查Embedding模型。当前的模型是不是能理解你的领域词汇?某次做法律合同问答,用通用Embedding模型,检索出来的片段跟问题风马牛不相及,换成一个在法律语料上训练过的模型后,效果立刻正常了。第三步,检查查询方式。问题本身的表述和文档正文差异太大时,向量检索很难命中,可以试试对问题进行改写,让它更接近文档的语言风格。

这三步逐个排查,90%的召回问题都能定位。千万不要一上来就换向量数据库,那解决不了根本问题。

5.2 切分粒度不对的典型症状

切分粒度过大时,一个片段包含多个主题,向量被拉成“四不像”,检索时匹配不到精准内容。症状是:回答内容泛泛而谈,或者把两个无关的知识点硬拼到一起。

切分粒度过小时,症状更隐蔽:召回很准,每一段都对得上关键词,但上下文信息不足,模型回答起来“接不上话”。比如片段里只有“退货政策如下:”,完整的政策内容在下一个片段里,模型就只能基于一句废话猜测答案。

调试方法是:把一个高频问题对应的召回结果打印出来,看Top-3片段是否都围绕同一主题、是否能独立构成完整语义。如果不行,就是切分设置有问题。

5.3 相似度阈值怎么定

很多RAG系统会设置一个相似度阈值,低于阈值的检索结果不进入Prompt。这样能防止“硬凑”答案。

但阈值设多少没有通用答案。不同Embedding模型产出的相似度分布完全不同,有的模型相关片段相似度在0.7以上,有的模型0.5就已经很相关了。正确做法是:跑一批标准问题,把检索相似度分布打出来,找到“相关与不相关”的分界线,再取一个安全余量作为阈值。

我见过最不靠谱的做法是把阈值设成0.9,结果一半以上的问题都召回不到结果,因为该模型相关片段只有0.75。所以阈值一定要基于自己的数据实测。

5.4 幻觉依旧存在时的补救策略

如果你已经加了“资料中没有就回答不知道”的指令,幻觉依然冒头,问题大概率出在Prompt里混入的上下文太多。Top-3片段里可能有1段是无关的,模型被这段干扰,开始放飞自我。

补救手段有两个。第一,加重排序环节,把高置信片段提到最前面,低质量片段直接剔除。第二,在Prompt里给每个片段标注来源,并要求模型回答时引用对应编号。比如“参考片段1:... 参考片段2:... 请回答并标注基于哪个片段”。这一招能让模型更克制,回答也会更严谨。

5.5 进阶优化方向与扩展

链路跑通之后,可以沿着几个方向逐步加强。

  • 混合检索:在向量检索基础上叠加关键词检索,解决专有名词和缩写词匹配不到的问题。BM25加向量检索是目前实践中最稳定的组合之一。
  • 查询改写:把一次提问拆成多个子查询,分别召回再合并,解决复杂问题信息分散的问题。
  • 增量索引:文档更新时,只对新文档做完整链路,旧文档不用重建,节省大量算力。
  • 评估体系:建立一个含标准答案的测试集,每次改动跑一遍,用召回率、命中率、答案忠实度量化评估,而不是靠感觉调参。

这些方向里,混合检索和评估体系是我最推荐优先做的。前者解决真实语料里词不匹配的痛点,后者让你每一次调整都有数据支撑,不再瞎调。

最后分享一个我在真实项目里的体会:别一上来就上重排序、混合检索、GraphRAG这些花活。先把最朴素的链路跑通,把文档清洗和切分调好,效果往往已经够用了。我见过太多项目,80%的时间花在调模型参数上,结果最后发现文档根本没被搜出来。RAG这条链路,每一环都是短板,但数据是最长的板,也最容易被人忽略。跑数据的时候耐心一点,把中间结果都打印出来看一遍,你很快就能摸清楚问题究竟出在哪一环。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 5:21:07

LLM替换传统推荐引擎成趋势?从协同过滤到语义生成的技术演进

最近圈子里讨论最热烈的一个话题&#xff0c;就是流媒体平台的推荐引擎要翻天了。起因是某头部流媒体公司公开放出了内部技术方向&#xff1a;他们正在用LLM&#xff08;大语言模型&#xff09;逐步替换掉传统推荐引擎&#xff0c;把过去那套以协同过滤、矩阵分解、深度排序为核…

作者头像 李华
网站建设 2026/10/20 5:20:42

知识蒸馏实战指南:从原理到部署的关键细节与常见坑

知识蒸馏这个话题&#xff0c;我在不同场合聊过很多次。每次讲完总有人问&#xff1a;这玩意儿到底解决什么问题&#xff1f; Bert 都上亿参数了&#xff0c;谁能直接部署&#xff1f; 知识蒸馏本质上就是回答这个问题的——它解决的是模型能力向小模型迁移的工程问题&#xff…

作者头像 李华
网站建设 2026/10/20 5:20:40

AI漏洞报告进入加密安全页:如何阅读、判断与使用

1. 从一条漏洞报告说起&#xff1a;AI 生成的加密安全情报到底长什么样2026 年 10 月 10 日&#xff0c;我在整理每日安全参考的时候&#xff0c;注意到一个挺有意思的变化&#xff1a;某加密资产相关的安全公告页面&#xff0c;开始出现由 AI 辅助生成的漏洞报告条目。不是那种…

作者头像 李华
网站建设 2026/10/20 5:20:03

AI编程模型迭代加速:从Argon到Carbon,什么是真正的代码生成“手感”?

最近圈子里有个消息挺有意思&#xff1a;某头部科技公司还没把内部代号Argon的编程模型推到全员使用&#xff0c;下一代Carbon就已经开始小范围内测了。参与内测的人给出的评价很耐人寻味——他们说Carbon的编程“手感”已经接近内部基准模型Opus 5.5。这话听起来像一句随口的评…

作者头像 李华
网站建设 2026/10/20 5:19:10

pytest 钩子 pytest_report_teststatus 与启动参数 -n 踩坑记录

在自动化测试项目中&#xff0c;我们通过 pytest_report_teststatus 钩子收集用例结果并上报服务端。本地调试一切正常&#xff0c;可一旦给启动命令加上 -n 参数&#xff08;无论是 -n 1 还是 -n 5&#xff09;&#xff0c;同一个用例就会被上报两次——用例明明只执行了一次&…

作者头像 李华
网站建设 2026/10/20 5:18:44

AI Agent市场深度拆解:五类玩家靠谱度实测与选型指南

1. 从一堆Demo到真实生产力&#xff1a;AI Agent市场到底走到哪一步了过去大半年&#xff0c;我几乎每周都会花几个小时泡在各种AI Agent的发布页、开源仓库和产品体验群里。原因很简单&#xff1a;作为一个长期做自动化和工具链整合的人&#xff0c;我太想知道这东西到底是&qu…

作者头像 李华