首页 / 资讯中心 / 文章详情

RAG落地实战:从文档分块到向量检索的完整指南

RAG落地实战:从文档分块到向量检索的完整指南 ★ FEATURED ARTICLE
这几年做AI应用如果没碰过RAG那大概率还没真正做过落地项目。RAG全称是Retrieval-Augmented Generation检索增强生成名字听着高级思路其实特别朴素大模型再强也不能让它凭空回答不知道的事干脆把知识放在外部知识库里来问题了先检索再让模型基于检索结果回答。这一套组合拳能同时解决三件糟心事——模型一本正经编答案的幻觉、训练数据过时带来的知识滞后、以及私有文档完全不在模型视野里的问题。这篇文章我会从实际落地角度把RAG拆成几个环节逐个过一遍文档清洗与分块、Embedding与向量库、检索调优、生成编排再到工业级的上线评估。每一部分都会给可直接用的代码片段和真实踩过的坑。适合两类人看一类是已经跑通了RAG Demo但发现效果忽好忽坏想搞清楚症结的朋友另一类是正在做知识库问答选型想知道这些组件之间到底怎么配合的同学。1. RAG到底解决什么问题从原理看架构选型1.1 大模型的三块短板与RAG的定位先说个大前提。很多团队的误区是拿到一个模型就要它万能让它解释产品功能、让它回答售后政策、还让它基于内部技术文档写方案。模型本身确实强但它有三个绕不开的短板。第一是幻觉。模型不知道答案的时候会很自然地编一个听起来合理的答案出来。这不是它不诚实而是它的底层机制就是预测下一个词当训练语料里没有相关知识时生成路径只能靠“脑补”。第二是知识时效。训练截止之后发生的事情模型一概不知。比如你公司昨天刚发的新版报销制度今天问它它只能按三个月前的老制度回复。第三是私域数据隔离。公司内部文档、客户案例、渠道政策这些内容不会出现在任何公开训练语料里模型不经过针对性调优根本接触不到。RAG的思路就是绕开“让模型记住所有知识”这条路。它把知识从参数里搬到一个可以随时查询的索引库里模型回答问题前先检索相关资料再把这些资料作为参考上下文交给模型组织答案。这样做的直接收益是知识能随时更新加一篇文档进去系统马上就能回答相关内容回答还有依据可以支持引用溯源幻觉也会明显减少因为模型不再需要凭空发挥。微调和RAG经常被拿出来对比但实际项目里它们不是二选一往往是互补——微调负责调语气、调格式、教模型怎么写RAG负责给模型提供事实素材。1.2 理解RAG的完整链路离线索引与在线问答RAG整体分两条链路。离线索引链路负责把文档库变成可检索的向量库解析原始文件清洗出干净文本按一定规则切成小块chunk每一块用Embedding模型转成向量最后写入向量数据库。在线问答链路负责在用户提问后完成“检索-生成”把用户问题同样转成向量在向量库里做相似度检索拿到最相关的若干个片段然后连同问题一起组装成Prompt交给大模型生成最终答案。这个架构里很多人以为核心是最后的生成模型其实真正的胜负手在前面。生成环节模型只做一件事把检索到的内容组织成通顺回答。如果检索回来的片段质量很差模型写得再漂亮也是错。这也是为什么我把这篇文章的重点安排在检索链路的两端——没有高质量的索引就没有高质量的召回没有高质量的召回谈Prompt工程都是白搭。你可以把RAG理解成一个“先查书再答题”的过程书查没查对决定了答案的下限。2. 文档准备与分块策略决定RAG上限的第一关2.1 文档解析源文档不清洗一切白搭第一个坑往往不是技术选型而是文档本身。真实世界的文档是乱七八糟的PDF里有页眉页脚、水印、表格、图片里的文字Word里有多级目录和批注还有些是扫描件连文字层都没有。这些内容如果不处理会被embedding模型当成正文语义后果非常直接——检索时召回了“第3页页脚的公司电话”而不是正文里的产品参数。我的习惯是先用两步快速摸底第一步看文档类型分布哪些是PDF、哪些是Word、哪些是Markdown第二步抽样打开几份看有没有扫描版、表格占比和高频噪声。摸底之后再决定解析方案。PDF优先用PyMuPDF这类库抽取文本代码量小对数字型文档的版式还原也算稳import fitz # PyMuPDF def parse_pdf_to_pages(path): doc fitz.open(path) pages [] for page_num in range(doc.page_count): page doc.load_page(page_num) text page.get_text(text) if text and text.strip(): pages.append({ page: page_num 1, text: text.strip() }) return pages解析出来之后还要做清洗。常见的清洗规则包括把全角字符统一成半角、去掉页眉页脚和水印关键词、把连续空行压缩成单行、去掉URL和邮件地址如果这些对业务语义无价值、修正乱码和异常换行。表格是个特别烦的东西纯文本方式抽取出来的表格会变成一行行碎片。我一般用规则检测“横向分隔线”或者“多个制表符连续出现”的特征把它们归为一个“疑似表格块”在分块阶段作为一个整体保留避免被切成残片。很多工具号称能一步到位解析PDF但实际项目里还是要做二次清洗这一步省不了。2.2 分块策略怎么切文本直接决定召回质量分块是RAG所有环节里“一分钱一分货”的地方。太粗的块比如整页作为一块会导致块内主题过多向量被稀释检索时不伦不类太细的块比如一两句话一块语义不完整模型拿到的上下文信息量太小生成质量会很差。更麻烦的是不少文档本身是连续的长文段落之间没有结构这时候切分规则稍微设计不好就会把“问题和答案”切到两块里去。我调过一个线上项目FAQ文档里每条都是“问题答案”的格式一开始用固定长度切分结果很多条FAQ被拦腰切开问题部分和答案部分分隔在两个chunk里。用户问“发票遗失怎么处理”召回回来的片段只有答案没有问题关键词相似度低得离谱。后来改成按“问题行”作为边界强制合并为一条chunk问题立刻消失。固定窗口切分是最基本的回溯方案适合无结构的纯文本。下面的实现里overlap参数很关键它保证跨边界的语义不被生生切断def split_fixed(text, chunk_size400, overlap80): chunks [] start 0 while start len(text): end min(start chunk_size, len(text)) chunks.append(text[start:end]) if end len(text): break start max(0, end - overlap) return chunkschunk_size和overlap怎么取值先看embedding模型的max_seq_length超过这个长度的文本会被截断向量自然失真。像常用的bge系列中文模型建议把chunk_size压在250到450之间。overlap我习惯取chunk_size的五分之一到四分之一太少起不到缓冲作用太多会让同一个内容反复出现在多个chunk里检索时资源浪费还容易造成重复回答。对于有结构的Markdown文档我强烈建议按标题切分。标题是天然的语义边界用#号级别作为切分线把每个二级/三级标题下的内容作为独立chunk语义内聚度远高于固定长度。生产里我是写一个简单解析器把标题行作为父级正文作为子块最终组装成带层级关系的chunk。这样检索到任何一个块都能知道它在文档中的位置后续引用来源也好处理。还有一点容易被忽略chunk不是越大越好。有的团队觉得上下文窗口大干脆把整章塞进去。窗口确实装得下但向量化之后这一章可能包含多个不相关主题相似度被平均值拉平关键信息反而被淹没。原则只有一个每个chunk最好能回答一个具体问题。太长了就再细化边界。3. Embedding模型与向量库选型别在最容易出错的环节省事3.1 Embedding模型怎么选维度、语言与微调Embedding模型负责把文本变成向量也就是把“语义”变成“坐标”。这里最容易犯的错是图省事用通用英文模型来处理中文文档。中文和英文的语义空间差异很大拿英文模型做中文检索结果看着能出几条实际相关度一塌糊涂。我见过一个项目用英文向量模型搭中文知识库召回率不到60%换成中文优化的模型后直接拉到80%以上。模型本身不贵但选错代价极高。选择时看三样东西语言覆盖、最大长度、向量维度。语言覆盖决定中文语义表达是否贴合最大长度决定你能喂多长的chunk而不被截断向量维度影响存储和检索成本维度不是越高越好得看模型训练目标与你的业务场景是否匹配。目前中文场景比较稳的路径是用开源的通用中文Embedding模型比如bge系列配合归一化操作让cosine相似度计算更稳定。加载和编码本身不复杂from sentence_transformers import SentenceTransformer embed_model SentenceTransformer(bge-large-zh-v1.5, devicecuda) embeddings embed_model.encode( chunk_text_list, batch_size32, normalize_embeddingsTrue, show_progress_barTrue )这里有一个细节normalize_embeddingsTrue一定要开。向量归一化之后余弦相似度直接等价于点积后续阈值和距离计算才统一。如果不开不同长度的文本向量模长差异很大排序结果会失真。batch_size也别贪大显存有限时调小一点保证不OOM。如果你用的是某个商业API同样要考虑它模型的max tokens限制超长文本一样会被截断。3.2 向量库选型数据量决定架构复杂度向量数据库做的事情本身不复杂存向量、算相似度、返回TopK。难点在规模。几百条数据用内存里的列表算也行几万条数据选个轻量级嵌入式向量库就够了几百万条甚至上亿条就要认真考虑分布式方案和索引类型了。起步阶段我推荐先用嵌入式向量库比如Chroma这类方案本地一个文件夹就能持久化配好collection之后直接读写适合验证思路和跑通Demoimport chromadb client chromadb.PersistentClient(path./kb_store) collection client.get_or_create_collection( namekb_docs, metadata{hnsw:space: cosine} ) collection.add( idschunk_ids, documentschunk_text_list, metadataschunk_meta_list, embeddingsembeddings.tolist() )向量库里有一部分隐藏配置直接决定查询性能比如HNSW索引的M和efConstruction。M控制每个节点的连接数值越大图越稠密召回率略高但内存占用涨efConstruction控制在构建索引时的搜索范围值越大构建越慢但质量越好。起步阶段用默认值没问题数据量上来后再根据“构建耗时、查询延迟、召回率”三个指标联合调。不要一上来就追求最优索引维护成本远超出你的想象。这里还要认真谈谈混合检索。纯向量检索有个天生弱点对关键词匹配不敏感。用户问“iPhone 15价格”向量检索能召回语义相关的内容但如果你文档里写的是“iPhone 15售价”价格和售价两个词在向量空间里可能不算最近邻。所以当知识库规模到十几万条以上我基本都会加一层关键词检索BM25把向量召回和关键词召回的结果做合并再用重排模型精排。纯向量的方案在文档噪声大、表述多样时召回稳定性很难保证。向量库不负责解决所有检索问题它负责快速过滤出“可能相关”的候选集真正的精排靠后面的环节。4. 检索环节的细节调优召回率与精准度的平衡4.1 检索的解耦召回、重排与阈值很多新手做检索只会一步向量TopK取前5条塞给模型。这在几百条的Demo知识库里看不出问题量一上来马上出事。用户问“怎么申请补发发票”知识库里有一篇《发票补发与红冲操作手册》还有一篇《费用报销制度修改通知》前者和问题强相关后者只是在某一段提到“发票”两个字。向量相似度排序会把两者都召回直接塞给模型模型很可能把不相关的费用报销内容也揉进回答里。所以我习惯把检索拆成两阶段召回尽量多、重排尽量精。召回阶段取Top20甚至Top50目标是“别漏”重排阶段再用更精细的模型把候选重新排序目标是“别错”。重排常用的方案是交叉编码器CrossEncoder它把问题和文档一起输入模型输出相关度打分比向量相似度更准。代码示意如下query_embedding embed_model.encode([question], normalize_embeddingsTrue) hits collection.query( query_embeddingsquery_embedding, n_results20, include[documents, metadatas, distances] ) candidates hits[documents][0] # 用交叉编码器重排 from sentence_transformers import CrossEncoder reranker CrossEncoder(rerank-model-name) # 换成你实际使用的重排模型 pairs [[question, doc] for doc in candidates] scores reranker.predict(pairs) final [candidates[i] for i in np.argsort(scores)[::-1][:5]]重排模型确实会增加延迟但换来的是回答质量的显著提升。如果对延迟敏感可以只对前20个候选做重排而非全量通常能兼顾质量和性能。另外相似度阈值也是经常被忽略的点。很多系统不设阈值结果用户问了个知识库里完全不存在的冷门问题系统还是硬找几条“最相近”但实际无关的内容模型就开始编。设一个最低相似度阈值低于阈值直接回复“知识库中没有相关内容”效果远好过硬答。4.2 查询改写与检索扩展用户提问和文档表述不一致是检索失效的头号原因。文档里写的是“差旅费报销流程”用户问的是“出差花了钱怎么走账”关键词对不上向量相似度不够结果就是召回失败。这个问题靠调向量模型很难根治更实用的办法是查询改写。最常见的做法是让大模型先把用户问题改写和扩展成几个可用于检索的句子再分别去检索。改写Prompt大概长这样请根据用户问题生成3种不同表述的搜索查询要求覆盖同义词、行业术语、口语化表达。 原始问题出差花了钱怎么走账 输出格式每行一个查询不要多余解释。这样可能生成“差旅费报销流程”、“出差费用如何账务处理”、“差旅报销需要哪些凭证”三个查询分别检索召回的概率一下子高很多。这一步的开销是一次额外的大模型调用但收益明显尤其适合C端用户口语化提问的场景。更激进的做法是HyDE让模型先对着问题生成一段“假设答案”再用这段答案去做相似度检索。它的逻辑是假设答案和真实文档在语义空间上更接近尤其是当问题本身很短、信息量不足时。我试过几条场景发现HyDE不是万能药在某些领域效果显著在另一些领域反而引入噪声需要实际测试后才好判断。不管用哪种方式检索扩展的核心原则是一致的不要让一个表达方式的差异把正确答案挡在召回范围之外。5. 生成环节与Prompt编排让大模型真正读懂检索结果5.1 把检索结果组织成模型能看懂的上下文检索完成之后很多人直接把Top5的chunk文本往Prompt里一拼就交给模型。这样做的结果是模型分不清哪段信息是主体、哪段是补充回答时东拉西扯。生成阶段的第一要务是把检索结果组织成有结构、有优先级的上下文。我给每条检索结果都加上编号和来源信息按相关度从高到低排列并且在Prompt里明确告诉模型“参考资料按相关度排序优先参考前面的内容”。同时要注意控制总上下文长度尤其是还要给历史对话留位置时需要按token预算做截断。一般我会预留系统词、历史对话、问题本身的token剩下的全部给检索片段片段太长就按需截断到指定长度。引用溯源也是生成阶段的重要功能。做法不复杂在每条片段前标一个来源IDPrompt里要求模型“当回答引用到某条资料时在句末标注对应编号”这样最终回答天然带引用。用户能自己点开原始文档确认信任感完全不是一个层级。我特别建议在chunk元数据里存好“文档名、页码、标题、原文位置”这些信息生成引用时直接从元数据里取。5.2 Prompt模板与幻觉控制Prompt模板的细节直接决定模型最后输出是“规范客服口吻”还是“学术报告风”。我常用的模板骨架是这样的你是一名企业知识库问答助手请根据参考资料回答用户问题。 规则 1. 只能使用参考资料中的信息作答不得编造。 2. 如果参考资料中没有明确答案直接回复资料中没有找到相关内容。 3. 回答时在相关句子后用[1][2]标注引用来源。 4. 回答语言简洁分点说明。 参考资料 [1] 来源产品手册-第三章.pdf 第7页 | 标题质量保证范围 内容chunk_text [2] 来源售后政策-2025版.docx 第2页 | 标题退换货流程 内容chunk_text 用户问题question关键不在措辞多华丽而在于把约束讲清楚。尤其“如果参考资料中没有明确答案直接说没有”这一条比任何长篇大论都有效。模型只要在不确定时倾向于承认不知道幻觉概率就大幅下降。不同模型对Prompt的服从度差别很大。有的模型会把“规则1”忽略照样自由发挥有的模型对“分点说明”服从很好但对“引用标注”不敏感。我的经验是先在两张小测试集上验证Prompt的稳定效果再决定是否要在规则部分加重复强调。还有一个细节历史对话不要一股脑全塞进去只保留最近2到3轮就够否则旧信息会稀释检索上下文的相对权重影响模型对当前问题的注意力。6. 工业级落地避坑手册从Demo到生产环境的差距6.1 索引更新与增量同步Demo阶段数据是静态的一次性全量导入向量库就完事。生产环境里知识库时刻在变新政策发布、旧文档下线、某篇手册更新了几个章节。如果索引不跟着更新用户问到的就是过期内容。最基础的方案是全量重建每天凌晨重新解析全部文档、重新生成向量、重新写入库。简单可靠但数据量大时费时费力而且白天更新的文档要等到第二天才能生效业务等不了。更好的方案是增量更新每篇文档维护一个版本标识定期扫描文档库的变更记录只对新增和修改的文档重新解析和向量化删除的文档对应chunk也一并清理。向量库操作上关键是维护好chunk与文档的映射关系。我习惯给每条chunk的metadata里写入“document_id”和“updated_at”更新时按这两个字段定位删除和重建。一定要保证chunk_id生成规则稳定不要每次重新生成否则重复写入会导致同一文档的旧向量和新向量同时存在检索结果混乱。6.2 质量评估不要只凭一两个例子优化这是大部分团队最缺失的环节。很多人看两三个例子觉得效果不错就以为系统没问题。真正上线之后遇到用户千奇百怪的问法才发现召回率和准确性根本不过关。我的建议是搭建一个最小规模的测试集至少50到100条真实用户问题每条标注好对应的正确答案或正确chunk来源。之后的每一次优化——换embedding模型、调分块参数、加查询改写——都跑一遍测试集量化对比召回率和准确率的变化而不是靠记忆里的零散感觉。评估指标上可以关注三个层面。检索层面看RecallK考察正确答案是否在召回范围内生成层面看Answer Correctness由人工或大模型当裁判判断回答是否正确且完整如果上了重排和引用溯源还可以看引用准确率即模型引用的来源是否真的支撑了对应陈述。开源评估库里有现成的RAG评估框架它的好处是把指标标准化不用自己写一堆评估逻辑。我在一个客服知识库项目里就是靠这套测试集定位到问题源于分块不合理——把FAQ问题与答案拆开了。因为测试集里这类问题的召回率明显低于其他类型顺着数据一查就看到了。没有量化评估很难在几十个环节变量里找到真正的元凶。6.3 常见问题速查表实际项目中反复出现的坑按照“现象—原因—处理”整理成一张速查表比自己翻聊天记录找答案高效得多。常见现象可能原因处理建议相似问题回答忽好忽坏索引未同步更新或chunk切分不稳定建立文档版本标识更新时定位到chunk级别重建换一种问法就召不回答案查询与文档表述不一致引入查询改写或同义词扩展叠加混合检索知识库有答案但模型说没有相似度阈值设置过高或召回不足降低阈值或增加重排环节重新选取TopK回答中引用来源对不上引用溯源元数据缺失在chunk metadata中写入文档名、页码、标题向量检索结果明显不相关文档含页眉页脚/表格碎片/噪声加强文档清洗表格单独处理后再切分同一问题多次回答内容重复片段overlap设置过大或chunk_id不稳定调小overlap保证chunk_id全局唯一系统延迟高召回数量过大或重排环节太重召回TopN从50降到20重排只处理候选集这份表格里我要特别点名chunk_id这个问题。用Chroma这类库时如果id重复后写入的文档会覆盖旧记录。我遇到过元数据里的document_id重复导致整批chunk被覆盖成最后一条排查了大半天才发现是id生成逻辑写错了。生产环境里id生成一定加上文档唯一键加chunk序号保证全局稳定唯一。最后说一点我自己的体会。RAG这个工程最忌讳一口吃成胖子。第一步先跑通最小闭环别急着上混合检索、重排、查询改写这些高级组件第二步开始攒测试集把每个环节的效果量化出来第三步再逐步加复杂度。我见过不少项目上来就想一步到位结果系统复杂到出错都不知道该查哪。先把地基打稳再搭上层这句话在RAG领域一样适用。
阅读完成 · 觉得有帮助?
咨询建站