RAG实战全流程:文档切块、向量检索到答案生成

发布时间:2026/10/20 7:19:33
RAG实战全流程:文档切块、向量检索到答案生成 先说结论RAGRetrieval-Augmented Generation检索增强生成是目前把大模型接入业务系统时性价比最高、落地速度最快的路径。我见过太多团队一上来就微调模型折腾几周后用私有数据一问模型依然一本正经地编答案最后绕了一大圈还是回到 RAG。这篇实战教程会用一套完整的流程把文档解析、文本切块、向量化、检索入库、重排序到答案生成全部串起来并附带可直接参考的代码和参数。不管你是刚接触大模型应用开发还是已经踩过几个坑但效果一直不理想这篇内容都能给你一条明确的路。这篇内容基于我一个实际做过的模拟项目 X 的经验整理而来场景是给某个部门搭建内部知识库问答助手资料类型包含 PDF 政策文件、Word 制度文档和少量表格用户输入的问题偏口语化对答案的准确性要求比较高。整个过程经历过多次重写最后沉淀下来的流程我认为比较通用适合绝大多数以“私有文档问答”为目标的中小型 RAG 项目。1. RAG项目到底要解决什么问题1.1 为什么大模型需要外挂知识库先讲一个很常见的现象。你直接问大模型“咱们公司的报销流程是什么”它能给你一个非常流畅的答案但中间至少有三个地方是你公司独有的细节它根本不可能知道。这不是模型笨而是它的训练数据里压根没有这些内容。就算你花大力气把公司文档塞进权重里模型也会面临两个问题一是训练成本极高二是知识更新极不灵活。RAG 的解法简单粗暴把回答问题的过程拆成两步。第一步用检索系统在预设的文档库里找到相关的片段第二步把这些片段连同一段指令交给大模型让它基于这些材料组织答案。这么做最大的好处是知识不藏在模型的参数里而是藏在可随时增删的文档库里。今天更新一条制度明天删掉一项流程只需要重新切块入库不需要重训模型。这也是我当初在技术选型时坚持用 RAG 而不是微调的原因。从用户感知看RAG 的效果天花板取决于检索质量而检索质量是可以被量化评估和持续改进的从维护角度看文档库的变化比模型权重的变化好管理得多。简单说RAG 把“让模型变聪明”这个伪命题转化成了“让检索更准”这个可拆解的真问题。1.2 RAG的整体链路从文档到回答的必经之路一个完整的 RAG 系统从用户输入到输出回答核心链路可以概括成五个环节文档接入 - 解析清洗 - 切块 - 向量化入库 - 检索重排 - 生成答案前四个环节属于离线部分负责把原始文档构建成一个可检索的知识索引后两个环节属于在线部分负责实时处理用户的问题并返回答案。大多数项目把 70% 的时间花在前四个环节上这是完全正常的因为检索的上限决定了生成的上限。在进入实战之前我需要先把你对这条链路的预期建立起来。很多新人以为 RAG 的难点在调提示词实际上提示词在整套流程里是最简单的部分。真正决定系统好坏的是文档怎么切、向量怎么存、检索结果怎么排序。后面每一节我都会围绕这条链路展开并且每到一个环节都会说明“为什么要这么做”而不是只告诉你“怎么做”。如果你想直接看代码可以直接跳到第 3 节但建议还是先把第 2 节读完因为它能帮你避掉不少后面必然会踩的坑。2. 方案选型哪些环节最影响效果2.1 文档解析与文本切块的底层逻辑很多人把 RAG 的起点当作“文本加载”但我更愿意把起点当作“文本清洗”。我在做模拟项目 X 的时候第一版跑出来的效果非常差检索到的片段经常是和用户问题毫无关系的表格碎块。排查到最后问题不在向量模型而在解析环节PDF 里那些页眉、页脚、水印、目录全被当成了正文内容塞进了向量库。解析这一步看起来简单实际上是整个项目里坑最多的环节。我在实践中总结的几条原则优先按段落结构提取而不是按页提取。按页切会让一个完整主题被截断后续检索时召回率大幅下降。对解析出的文本做基础清洗比如去掉多余换行、合并断行、统一字符编码。PDF 解析出来的文本经常每行就是一个独立换行直接切块会导致切出来的每块都是半句话。表格内容要单独处理。大多数现有工具把表格转成 Markdown 或 HTML 的原始字符串这个做法在检索阶段通常效果很差建议针对结构化表格单独做字段级的索引或者至少把表格中每一行变成一个独立条目。接着是切块。这是人人都知道重要但很少有人愿意认真调的一个环节。切块的核心目标是让每个块尽可能完整地表达一个主题同时块与块之间不要有大量的信息冗余。块大小、重叠长度和分隔符的选择会直接影响后续检索的精度。我在模拟项目 X 里最终的配置是“按句子切分 块大小 400 ~ 600 字 重叠 80 ~ 100 字”这个配置对中文政策类文档和制度类文档的表现都比较好。但注意这个数字不是银弹。如果你的文档是连续的技术说明型文本800 字一块甚至 1000 字一块可能效果更好如果是客服问答对可能 200 字就足够了。关键是你要明白调整这些参数背后的目的块太小语义信息不完整检索不到真正相关的内容块太大块内噪音过多向量表示会被无关信息稀释相关的那部分语义反而排不上去。注意初次使用切块工具时先不要急着追求最优参数。先用默认参数跑通全流程再根据检索效果回头调切块参数这样可以避免同时引入太多变量出了问题反而不知道是哪一个环节导致的。2.2 向量化模型、向量库、重排序方案的对比取舍向量化模型是三要素里最容易让人纠结的部分。现阶段我的建议是开源中文向量模型已经能够覆盖绝大多数国内业务场景你可以先选一个下载量高、社区反馈好的中文向量模型作为基线而不是一开始就上收费的商用 API。这里给出我实测的一组对比思路你可以参考对比维度基线开源模型商用API方案中文语义理解良好对常见业务名词够用更好尤其适合口语化问题部署成本免费需自备GPU或CPU运行按调用量付费隐私可控性数据不出内网需要把数据发到外部服务调优空间可通过微调进一步适配一般不可调适合场景数据敏感、量大的内部项目快速验证、外部公开数据我的选择逻辑是项目初期先把隐私和成本放在第一位用开源模型跑通基线再根据评估结果决定要不要换更强的模型。换向量模型的成本其实不高因为索引是可以随时重建的你完全可以在同一个切块结果上生成多套向量横向对比哪套向量在检索阶段效果最好。向量库方面如果你只是构建一个千万级以下向量规模的内部系统完全没有必要自己写向量检索逻辑直接用成熟的向量数据库就行。国内用得比较多的开源方案包括 Chroma、FAISS、Milvus、Qdrant 等其中 FAISS 属于库而非数据库适合单机环境Chroma 和 Qdrant 更适合作为服务使用。选型的判断标准也很简单你的并发量高不高向量规模会不会快速增长团队有没有能力运维一套独立的数据库服务。大多数人一开始选 Chroma 这类轻量方案就足够了等服务化需求明确后再迁移到 Milvus 这类重方案。重排序Rerank是我在整个选型里最建议你认真对待的一项。向量检索本质上是在“语义相似”的层面上找候选但它有一个致命弱点对关键性细节不够敏感。比如“报销需要什么材料”和“报销流程是什么”这两句话语义上算相近但用户的实际诉求完全不同。如果只靠向量检索你可能会把关于“流程”的段落排到最前面尽管它与“材料”高度相关却答非所问。重排序模型的作用就是对向量检索返回的候选进行更精细的交互式打分把真正需要的知识点重新顶到前面。实践上加不加重排序对最终回答质量的提升通常比换一个更强的向量模型更明显。2.3 三类典型的业务场景拆解RAG 项目并不是千篇一律的我习惯把它们的需求和方案分成三类然后针对性地调整链路第一类是“制度问答型”。比如企业内部的员工手册、报销制度、考勤规则。这类场景的特点是问题重复度比较高对答案的权威性要求也很高。需要特别注意的是答案里必须能追溯来源最好在生成时附上原文引用。检索环节建议打开基于关键词的检索叫 BM25 召回与向量检索的混合模式因为制度里大量的条款是强词汇匹配比如“年假”“加班工资”靠纯语义向量容易绕弯。第二类是“知识问答型”。比如技术文档、产品手册、研发Wiki 的问答。这类场景的问题比较多样知识本身更新频率也比较高对检索准确率不苛刻甚至允许“没找到就老老实实说不知道”。在这种场景切块策略可以更激进比如适当加大块大小以保留完整上下文同时要把答案生成阶段的提示词设计得保守一些尽量避免模型在材料不足时自行发挥。第三类是“多文档综合分析型”。比如给一份长报告做要点总结或者跨多个文档抽取关联信息。这类场景已经不仅仅是 RAG还涉及到多条链路的组合通常的做法是先用检索找出多个相关的片段再做一轮合并去重和摘要。由于需要跨文档对比重排序的作用会变得更加重要同时你也可能需要对最终生成的答案做一套质量校验比如让模型自检“回答是否完全基于给定的文档片段”。明确类型后后面的每个技术决策都变得有依据了。比如同样一个切块参数在制度问答型和知识问答型上的表现可能截然不同。做项目前先花半小时把场景定义清楚比埋头调三天参数有效得多。3. 实战从零搭建一个可运行的RAG流程3.1 环境准备与依赖安装考虑到大部分读者的技术栈是 Python我会把整个项目建立在 Python 3.9 的基础上。先说我的运行环境一台 8C16G 的 Linux 服务器显卡是普通单卡没有独立 GPU 也能跑完整个流程只是向量化阶段会慢一些。如果你只是在本机 Windows 上做验证步骤也完全一致。基础依赖包括三块文档解析类、向量化和检索类、服务封装类。我用的都是当前比较常见、社区维护活跃的库你可以直接在虚拟环境里安装pip install pypdf4.2.0 pip install python-docx1.1.2 pip install chromadb0.5.3 pip install sentence-transformers3.0.1 pip install fastapi0.115.2 pip install uvicorn0.30.6 pip install openai1.51.0这里要特别提醒两个容易踩坑的点。第一pypdf与PyPDF2是两套不同的项目API 不一样不要混用第二sentence-transformers会自动拉取一个底层的 torch 依赖体积比较大安装时建议给它多一点时间。如果你后面在 huggingface 上下载模型出现超时可以把环境变量HF_ENDPOINT指向国内镜像源这是纯网络避坑操作对速度提升极其明显。在代码目录结构上我建议从第一天就按照职责分好目录否则项目跑到后期会变成一锅粥rag_project/ ├── app.py # FastAPI 服务入口 ├── ingest.py # 离线入库脚本 ├── config.py # 全局配置 ├── docs/ # 原始文档 ├── output/ # 切块后的中间产物 ├── db/ # 向量库持久化目录 └── evaluate.py # 检索效果评估脚本这是一套我沿用至今的简易分层。把入库和在线检索拆成两个脚本也是刻意为之。离线入库是低频操作在线检索是高并发操作两者对日志、异常处理的要求完全不一样混在一起只会互相干扰。3.2 文档解析、切块与入库实操现在开始第一部分的重头戏把各种形态的文档变成可检索的向量索引。这个过程虽然不复杂但它直接决定了你后面收到的每一份问答质量。我会以 PDF 和 Word 两种最常见的格式为例演示具体代码。PDF 解析我使用pypdf但实际项目里如果遇到扫描版 PDF纯图片pypdf提取出来的会是空白这时候你就需要配合 OCR 组件使用。下面的代码聚焦在文本型 PDFfrom pypdf import PdfReader def extract_from_pdf(path): reader PdfReader(path) texts [] for page in reader.pages: txt page.extract_text() # 去掉页眉页脚里常见的无效内容 lines [l.strip() for l in txt.splitlines() if len(l.strip()) 1] texts.append(\n.join(lines)) return \n.join(texts)首席博主视角的补充是如果你发现extract_text()返回的文本存在大量错序或缺字先检查 PDF 是不是扫描件或者是不是由扫描件经 OCR 软件生成的“假文本”这类 PDF 的文本顺序是乱的在后面切块时会产生很多语义断裂。遇到这种情况我的建议是放弃pypdf换 OCR 管线把整页转成图像再抽取文本。Word 文档的解析我用python-docx。它有一个特性段落之间不自动加换行所以手动拼接时要在段落之间补一个\nfrom docx import Document def extract_from_word(path): doc Document(path) # 注意 docx 里的表格和正文段落是分开的 parts [p.text for p in doc.paragraphs if p.text.strip()] for table in doc.tables: for row in table.rows: cells [cell.text.strip() for cell in row.cells] parts.append( | .join(cells)) return \n.join(parts)切块器用递归字符切分器这是我比较推荐的中文通用方案因为它的基本思路是“优先按段落、句子等自然边界切而不是无脑按固定字符数硬切”from langchain_text_splitters import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , , ], keep_separatorFalse, ) chunks splitter.split_text(full_text)这里的separators顺序是有讲究的代码会从前往后尝试按每一个分隔符切如果切出来的块还超过chunk_size就继续下一个更小的分隔符。这就是 Recursive 名字的来由。中文场景下把句号、感叹号、问号、逗号都放进去能明显减少把一个完整句子拦腰截断的情况。设置chunk_overlap的目的则是让处于两块交界处的语义信息不要完全丢失。切块完成后要做一步容易被忽略的工作给每一块加上元数据。元数据至少应该包括文档来源、页码、标题层级这些信息在最后展示答案来源时是必须的。在入库之前还要过滤掉那些明显没有检索价值的块——比如纯表格数字块、只有一两个词的无意义块。这一步我当初是靠正则加上人工抽查完成初筛的。向量化和入库我选的是sentence-transformers加 Chroma代码如下from sentence_transformers import SentenceTransformer import chromadb model SentenceTransformer(BAAI/bge-large-zh-v1.5) client chromadb.PersistentClient(path./db) collection client.get_or_create_collection( nameknowledge_base, metadata{hnsw:space: cosine} ) def ingest(chunks, metadatas): embeddings model.encode(chunks, normalize_embeddingsTrue).tolist() ids [fchunk_{i} for i in range(len(chunks))] collection.upsert( idsids, documentschunks, embeddingsembeddings, metadatasmetadatas, )normalize_embeddingsTrue的理由也顺带说明向量归一化后向量之间的点积就直接等于余弦相似度这一步很多人在实现时漏掉导致同一个向量模型在后续检索时表现不一致。Chroma 里设置hnsw:space: cosine也是同理让底层的 ANN 索引空间与你的相似度度量保持一致。3.3 检索、重排序与答案生成完整实现离线部分做完后系统已经拥有了一个知识索引。现在进入在线部分需要实现三个能力召回候选、重排序、生成答案。召回我用的是混合检索我认为这是中文知识库场景下最稳的方案。向量检索负责找语义相近的段落BM25 负责找关键词强匹配的段落两者结果合并后去重再进入重排序。Chroma 原生不带 BM25所以我用一个轻量级的本地检索器来接这一环。为了控制篇幅这里直接给出合并逻辑的关键代码def retrieve(query, top_k20): query_vec model.encode(query, normalize_embeddingsTrue).tolist() # 1. 向量检索召回 top_k vec_res collection.query(query_embeddings[query_vec], n_resultstop_k) # 2. BM25 检索召回已在 ingest 阶段构建好 doc 频率 bm25_res bm25_index.search(query, top_k) # 3. 按 doc_id 合并去重保留双端命中的片段加权加分 merged merge_results(vec_res, bm25_res) return merged你可能注意到我在代码里强调“双端命中的片段加权重”这是一个非常实用的小技巧。如果一个文档片段既被向量检索认为是语义相似的又恰好命中了用户问题里的关键词那它几乎可以肯定是高价值片段。在合并排序时给它一个加权系数能有效提升最终召回质量。重排序环节我使用FlagEmbedding提供的 reranker 模型。这个模型会接收一个查询和一组候选文档输出交互式相关分数效果通常远好于纯向量打分from FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-v2-m3, use_fp16True) def rerank(query, candidates, top_n5): pairs [[query, doc] for doc in candidates] scores reranker.compute_score(pairs, normalizeTrue) ranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return ranked[:top_n]重排之后最终保留下来的片段数量可以控制到 3 ~ 5 个。别贪多片段越多大模型也就越容易迷失在冗余信息里。答案生成阶段我用大模型的对话接口来封装。这里有两个关键点。第一把检索到的片段严格放进系统提示词里并用分隔符包起来第二在提示词里明确要求“如果用户问题在给定文档中找不到明确答案只能回答不知道不得自行扩展”。这第二点是我认为所有 RAG 类应用都必须写进提示词的底线约束否则系统就会失去“基于文档回答”的意义。def generate_answer(query, contexts): context_text \n\n---\n\n.join( [f来源{i1}:\n{doc} for i, doc in enumerate(contexts)] ) system_prompt f你是一个内部知识库问答助手。请严格基于下面的文档片段回答用户问题。 【文档片段开始】 {context_text} 【文档片段结束】 要求 1. 如果片段中没有足够信息回答直接说“根据当前文档无法回答”。 2. 回答时要说明依据可以引用片段中的原句。 3. 不要让用户意识到你使用了上下文提示词用自然的口吻组织语言。 messages [ {role: system, content: system_prompt}, {role: user, content: query}, ] # 调用大模型接口得到response return get_llm_response(messages)保险起见我还会在生成前做一次硬校验如果重排序之后得分最高的片段其分数低于某个阈值那就直接返回“未检索到相关资料”不进入生成阶段。这个逻辑尤其适用于私有知识不足时避免模型强行输出幻觉内容。3.4 带评测数据的快速回测RAG 项目最怕一种状态就是你感觉改了什么但说不清楚改完是变好了还是变坏了。没有评测体系的优化说难听点就是在凭感觉调参。我建议在项目第一天就准备一组评测数据集规模不用大30 ~ 50 条就够。每一条评测数据包含三个字段{ question: 员工年假在什么条件下可以累积到下一年, reference_source: 员工休假管理制度.pdf }评测时用你的检索管线为每个问题返回 top_k 结果检查这些结果中是否包含与reference_source对应的文档块。包含即为命中。统计命中率作为召回指标以此判断检索链路改动是否有效。再进一步把评测数据里的人工标准答案也补上然后用大模型自己来打分判断生成答案与标准答案的语义一致性。这属于 LLM-as-Judge 的范畴虽然不完美但作为日常回归手段已经够用。我在模拟项目 X 里的做法是每改一次切块参数或重排序策略先跑一遍离线评测确认命中率没有下降再上生产环境。4. 效果调优与工程化避坑4.1 检索准不准先看召回还是先看排序调优 RAG 项目时我见过不少人的顺序是反的一上来就调提示词或者换更强的向量模型结果效果依然很差。我来给你一个比较标准的排查路径。先判断“召回”是否足够。召回的意思是和用户问题相关的片段有没有进入候选集合。如果没有进入后续一切排序和生成都是无米之炊。怎么判断召回够不够用上面说的评测数据把候选集合的 top_k 调大一些比如 top_k50看相关文档块在不在里面。如果不在问题出在切块策略、向量模型、解析质量上如果在那检索链路基本没毛病问题出在重排序和候选集合过大导致的噪音。第二步再判断“排序”是否合理。把重排后的前 5 个和后 20 个对比一下看看好片段有没有被顶到前面。如果高相关片段明明出现在候选里但排名很靠后就要优化重排序环节或者调整混合检索里向量与 BM25 的权重。第三步才是生成质量的调优。回答不好但检索没问题那大概率是提示词里对答案的约束不足或者模型本身理解能力不够。记住这个顺序排查效率会高很多。4.2 常见问题与排查实录下面把这大半年我在模拟项目 X 及周边实践里遇到的典型问题整理成一个速查表每一条都是我实际踩过并验证过解决思路的。现象根因解决方案检索结果经常是无关的表格碎片表格被当普通文本切块入库表格单独成一类块或按行拆成独立条目用户问“报销材料”返回的都是“报销流程”纯向量检索对关键词不敏感引入 BM25 混合检索关键词匹配结果加权回答内容高度雷同且引用错误来源重排序没生效候选顺序混乱在重排序环节输出分数检查排序逻辑确认使用了交互式 reranker 而非纯向量相似度文档更新后问答结果没有变化向量库未做增量更新入库脚本要支持 upsert 和基于元数据删除旧 doc_id切 500 字一块后经常出现上下文断裂切块时没有以自然段落/句子为边界使用递归切分器调整分隔符优先级大模型经常“自由发挥”超出文档范围提示词约束不足明确要求“无法回答时直接说不知道”并做阈值拦截第一次查询特别慢之后变快向量模型加载和索引加载耗时服务启动时预热提前加载模型和索引到内存特别注意增量更新这个坑最隐蔽。初期你没有更新需求纯入库一次性完成看不出来问题但一旦文档变成每周更新忘记处理旧版本用户就会看到时效性错误答案。我后来给入库脚本加了基于元数据的去重建策略每次更新时按 doc_id 范围先删后插保证了索引与源文档的一致性。4.3 我调试 RAG 的几个独家技巧第一把向量相似度分数和重排序分数同时打印到日志里。这样做的好处是一旦用户投诉答案不对你可以快速区分是哪一步出现了问题而不是把整个链路从头到尾翻一遍。第二对切块结果做可视化的抽查。把切出来的块按顺序打印一眼就能看出有没有断句、东拼西凑的问题。这个动作成本极低却几乎每次都能发现解析环节留下的低级错误。第三建立“用户在问什么”的问题聚类分析。运行一段时间后把用户真实的问题收集起来按主题聚类看哪些问题总是检索不到答案。这个反馈循环比任何指标都能更快地帮助你发现知识库覆盖率的缺口。第四在大模型调用前加缓存。对于重复度较高的制度类问题做一个基于问题哈希的缓存服务相同问题直接命中缓存能显著降低在线链路的时延和成本。4.4 从原型走向服务的几个工程建议如果你打算把 RAG 从“能跑”推向“稳定服务”我建议尽早考虑这几件看似外围、实则关键的事。第一接口层要做限流和超时控制。大模型生成的耗时波动很大差的时候可能到十几秒。接口层如果不设置超时和重试策略上游服务很容易被拖死。我在一个项目中就遇到过一个耗时的问答接口把整个 Web 服务的连接池占满影响了所有其他的正常接口。后来通过给问答接口单独拆服务、设置合理的超时和限流才把风险控制住。第二用户反馈通道一定要留。不用做得多复杂一个“答案是否有帮助”的按钮即可。这些反馈数据是你后续做评测集扩充和模型调优最重要的原料。没有反馈你就只能靠肉眼抽查永远无法发现长尾问题。第三隐私与权限在架构上要提前设计。如果不同类型用户只能查看不同密级的文档那你的向量库不能只是一个公共集合至少需要按密级拆分集合或者在元数据上做过滤条件。这是最容易在后期引发改造成本的环节宁可前期多做一层设计。我原本在这个项目中也设计过更细的权限体系后来因为人员和成本原因做了简化但主体思路没有变技术选型上预留了多集合和元数据过滤的能力后面加权限时就平滑很多。最后再分享一个小技巧也是我一路走下来觉得最管用的构建一个“评测壳子”比构建一个“优雅的检索实现”更值得优先投入。因为没有评测壳子你的所有优化都是无效的你根本不知道同一个改动到底是让系统变好了还是变差了。我后来每次在这类项目上都是先花一小时写评测脚本再开始调链路事实证明这个时间花得非常值。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询