ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG落地实战:分块策略、混合召回与质量评估全解析

RAG落地实战:分块策略、混合召回与质量评估全解析 先交代一下背景。我大概从去年初开始集中做RAG落地从最早拿LangChain默认配置跑POC到后面把整套流程拆开重做、量化评估、持续调优踩了不少坑也攒下了一套相对系统的打法。这篇就围绕三个最核心的环节展开分块策略、混合召回、质量评估。如果你正准备搭企业知识库问答或者已经跑通了一个Demo但发现效果不稳定——召回不准、回答幻觉、调参靠蒙——那这篇应该对你有用。我会把每个环节的取舍逻辑、实操参数、常见坑都写清楚尽量让你能直接拿去用。1. 搭建RAG前先想清楚这三件看不见的事很多人一上来就急着选向量数据库、调Embedding模型其实在动任何代码之前有三个问题值得先想清楚。它们不会写进架构图里但几乎决定了你整个RAG系统的效果天花板。第一件事你的源数据到底是什么形态这听起来像废话但实际操作中影响巨大。我在一个项目里遇到过客户给的知识库实际上是几千份扫描版PDF全是图片OCR都没跑过。这种情况下你无论用什么分块策略、多先进的召回模型都没用因为信息根本没有被提取出来。源数据的形态直接决定了你的预处理管线纯文本/Markdown/Word最好处理的形态直接进入分块流程即可PDF需要先判断是文本型还是扫描型扫描型要先过OCR而且中文PDF的OCR识别率和排版还原度跟英文比还是有差距HTML/XML需要先做正文提取去掉导航、页脚、脚本标签否则会污染语义Excel/CSV结构化表格数据处理逻辑完全不同经常要按行列转成描述性文本再入库音视频/会议纪要需要先转写再处理转写错误会直接带进知识库第二件事你的问题到底是什么样的目标问题形态决定了检索策略的复杂度。如果只是根据文档回答问题那标准向量检索就够。但真实场景往往是混合的关键词明确的问题比如服务器报错500怎么处理这种问题里500是强信号纯向量检索往往不如关键词/稀疏检索BM25表现好语义相似但字面不匹配的问题比如系统宕机了怎么办和文档中的服务不可用恢复流程这种就需要向量语义召回多跳推理问题比如A部门的预算审批过审时间跟B部门相比哪个更长这种问题甚至需要拆解成多轮检索单纯召回几个chunk还不够第三件事你的约束条件是什么这里的约束包括数据隐私能不能用云端大模型API、延迟要求是允许10秒还是必须2秒内返回、成本上限Embedding和LLM的token成本、以及团队的技术栈Java团队和Python团队选型会很不一样。这些约束不提前想清楚后面架构设计很容易推翻重来。我把这三件事跟团队复盘过很多次结论是RAG系统80%的问题其实在数据准备阶段就已经注定了。检索和生成环节是在有限的条件下尽量补救。所以建议你在动手前先花一到两天把这三件事写成文档想得越清楚后面的工就越顺。2. 分块策略为什么说chunk size是个薛定谔的参数分块是整个RAG链条里最容易被低估的一环。很多人直接让LangChain的TextSplitter默认参数跑一遍就完事结果检索效果差也不知道是哪个环节出了问题。2.1 chunk size对召回效果的影响机制先说结论chunk size没有绝对最优它是跟你的文档类型、检索策略、LLM上下文窗口强耦合的。但搞清楚它怎么影响效果你就能自己判断该调大还是调小。我用一个简单的例子说明。假设文档里有这么一段话服务器在运行过程中如果检测到CPU温度超过85摄氏度会自动触发降频保护机制。该机制会逐步降低CPU工作频率并记录告警日志通知运维人员进行处理。如果chunk size设得很大比如1000字以上这一段会跟很多其他内容合在一起。当你问CPU过热会有什么保护机制时向量相似度会被整段的其他信息稀释召回的chunk可能包含了答案但不够聚焦喂给LLM后回答容易发散。如果chunk size设得很小比如50字这一段会被切成几部分语义完整性被破坏。当问题涉及降频保护机制时可能只召回自动触发降频保护机制这一个碎片缺少降频保护机制的具体行为上下文LLM生成时会猜测甚至编造。所以chunk size相关的是一个语义完整性 vs 检索精准度的平衡问题。块越大语义越完整但检索精度越低块越小检索越精准但上下文越容易碎片化。2.2 各种分块方法的对比与选型在实际工程里我用过四类分块方法各有适用场景分块方法实现方式优点缺点适用场景固定长度切分按字符数/词数硬切实现简单可控性强语义割裂严重仅作为baseline递归字符切分按段落标题→换行→句号→词逐级回退保持基本语义单元对结构复杂的文档支持有限通用型文本LangChain默认方案结构感知切分按Markdown标题/HTML标签/代码函数边界切语义边界清晰上下文完整强依赖文档结构没有结构就失效技术文档、Markdown、代码库语义分块基于Embedding相似度变化点切分语义边界自然适应性强计算成本高边界判断有误差主题跳跃较大的文档、公告类内容我个人的推荐是优先用结构感知切分没有结构再用递归字符切分RecursiveCharacterTextSplitter不要用固定长度硬切。理由很简单——语义边界这种东西文档结构就是最廉价的信号。Markdown里的##、###天然就是主题边界HTML里的h1、h2也是。抛弃这些直接用长度硬切等于把免费信息扔掉了。2.3 具体的分块参数与实战配置如果你用LangChain一个相对可用的起步配置是这样from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size512, # 按字符数计算的块大小 chunk_overlap64, # 相邻块之间的重叠区间 separators[\n\n, \n, 。, , , , , , ], length_functionlen, )几个参数的取舍逻辑chunk_size512中文场景下约等于300到500字的段落。这个大小对于多数问答场景是比较安全的起点。如果文档偏技术规范、逻辑链条长可以往768到1024调如果是FAQ、条款类短文本256左右可能更好。chunk_overlap64保证切分边界上的语义不断裂。比如一个知识点刚好跨在两个块的边界有overlap就能在两侧都保留一部分上下文。separators的顺序很重要它代表切分时的优先级先试段落、再试换行、再试句号。这个顺序保证了能用段落边界就不用句号能用句号就不用逗号尽量减少对语义的破坏。我在实际项目里一般会把chunk_size和overlap配成8:1到10:1的关系overlap太大会引入大量冗余token增加存储和检索成本太小又起不到连接作用。2.4 一个经常被忽略的分块思路父子分块如果你做过一段时间的RAG可能会遇到这样的情况chunk里包含了答案但缺少足够的背景信息导致LLM回答得很片面。这时候可以考虑父子分块Parent-Child Chunking方案。思路很简单检索用小块child喂给LLM用大块parent。建库时文档按大块比如1024字切分再在每个大块内部细分为小块比如256字建立小块到大块的映射关系检索时用小块去做向量检索召回精准拿到命中的小块后回溯对应的完整大块把大块内容作为上下文喂给LLM这样既保证了检索的精准度又保证上下文完整。缺点是实现复杂度稍高且喂给LLM的token量会增加成本稍微高一点。LangChain的ParentDocumentRetriever支持这个模式用起来很方便from langchain.retrievers import ParentDocumentRetriever from langchain.storage import InMemoryStore from langchain.vectorstores import Chroma # 小chunk的切分器 child_splitter RecursiveCharacterTextSplitter(chunk_size256, chunk_overlap32) # 大chunk的切分器 parent_splitter RecursiveCharacterTextSplitter(chunk_size1024, chunk_overlap64) retriever ParentDocumentRetriever( vectorstorevectorstore, docstoreInMemoryStore(), child_splitterchild_splitter, parent_splitterparent_splitter, ) # 入库时先按parent_splitter切分并存储到docstore # 再将每个父块细分为子块用子块更新向量索引2.5 元数据设计让检索结果更容易被过滤很多人把chunk切完了就往向量库里塞忽略了一个重要的事情——元数据。元数据是RAG检索的后门可以在向量检索之前或之后做硬性过滤能解决掉很多纯靠向量相似度解决不了的问题。常见的有用的元数据字段来源文档ID / 文件名定位和溯源章节路径比如第3章/3.2节支持按章节范围检索文档类型FAQ / 规范 / 工单 / 变更记录更新时间做时间衰减或按版本过滤业务线/部门知识库场景下做权限隔离作者/维护人方便追溯和反馈举个实际例子企业知识库里可能有《XX系统操作手册2023版》和《XX系统操作手册2024版》新旧版本内容互相冲突。如果你的检索不区分版本用户问XX系统怎么配置召回的结果可能同时包含两版内容LLM就会给出自相矛盾的答案。这时候在元数据里加一个version字段检索时按条件过滤掉旧版本问题直接消失。3. 混合召回让稀疏检索、向量检索和Rerank各司其职3.1 为什么纯向量检索不够用这几年Embedding模型发展很快语义检索的效果越来越好。但如果你做的是真实业务系统会发现纯向量检索有几个先天短板专有名词、型号、工单编号不敏感RTX4090的性能参数和RTX3080的性能参数如果两段文本在同一个上下文里出现Embedding可能会把它们混在一起精确匹配失效用户的P95延迟和文档里的Percentile 95延迟在语义上是同一件事但如果模型训练数据里没覆盖到这种等价关系向量相似度可能很低低频术语遗忘Embedding模型对生僻词、缩写词、代码变量名的表示通常很差因为这些词在预训练语料里出现频率太低了这就是为什么需要混合召回用稀疏检索BM25解决精确匹配和关键词命中用稠密向量检索Embedding解决语义泛化两者互补。3.2 混合检索的工程实现常见的混合检索方案就是把BM25命中和向量命中的结果合并起来再做去重和重排。Elasticsearch同时支持BM25和向量检索kNNWeaviate、Milvus、Qdrant这些向量数据库也都陆续支持了混合检索能力。如果你用Python栈可以这样组合from langchain.retrievers import BM25Retriever, EnsembleRetriever from langchain.vectorstores import Milvus # 向量检索器 vector_retriever Milvus(embedding_functionembeddings, collection_namedocs).as_retriever( search_kwargs{k: 10} ) # BM25检索器 bm25_retriever BM25Retriever.from_texts(docs, k10) # 混合检索器加权融合 hybrid_retriever EnsembleRetriever( retrievers[bm25_retriever, vector_retriever], weights[0.3, 0.7], )这里有两个关键点值得展开说。第一个是要不要做加权以及权重怎么设。我现在更倾向于不用固定权重而是用RRFReciprocal Rank Fusion来做结果融合。RRF的思路很简单每个召回结果按排名得到一个倒数分数最后按分数汇总。它不依赖你预先调权重对两个检索器的分数尺度差异也不敏感鲁棒性好很多。公式是score(d) Σ 1 / (k rank_i(d))其中k是一个常数通常取60。rank_i(d)表示文档d在第i个检索器里的排名。如果某个结果同时在两个检索器里都排得靠前它的RRF分数会很高。第二个是混合检索不是简单的11。两个检索器召回的内容可能高度重叠如果不做去重最后喂给LLM的上下文会有一堆重复信息浪费token还干扰判断。所以融合之后需要按score排序并去重这一步千万别省。3.3 Rerank让结果排序更贴合你的真实需求混合召回解决的是找得到Rerank解决的是排得对。向量检索和BM25返回的排序是基于相关性估计的但这个相关性跟你最终问的问题之间是有差距的。我举个例子你问公司年假制度是什么向量召回的结果可能有几条一条是《员工手册》里关于年假的计算规则另一条是某个员工发的离职帖子提到了年假折算还有一条是HR在公告里通知年假申报时间。后两条在语义上跟年假制度都有关系但真正的答案只来自第一条。Rerank模型的作用就是输入查询和候选文档输出每篇文档精细化的相关性分数。它通常比Embedding模型的交互式匹配要精细得多因为它把query和document做深度交互编码而不是分别编码再算相似度。开源方案里BGE-Reranker和Cohere Rerank是两种常用选择。BGE系列对中文支持不错跑在本地也方便。使用时要考虑推理时间——如果引入Rerank后整个链路的延迟多了好几秒用户的体验很难接受。我一般先召回10到20个候选项然后用Rerank挑出top 3到5个这样一个流程加几十到几百毫秒用户可以接受。from FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-base, use_fp16True) pairs [[query, doc_text] for doc_text in candidate_docs] scores reranker.compute_score(pairs, normalizeTrue) # 按rerank分数重新排序取top_k sorted_results sorted(zip(candidate_docs, scores), keylambda x: x[1], reverseTrue)[:top_k]3.4 混合召回链路中的延迟控制与缓存工程落地的时候召回链路里加了BM25、向量检索、Rerank三个环节每个环节都有耗时加起来可能就慢了。需要做几件事并行召回BM25和向量检索互相独立应该并发执行而不是串行等待。Python里用asyncio.gather或者多线程就能做到。缓存热点查询高频问题比如密码重置流程如何提交报销的召回结果可以直接缓存设置合理的TTL比如按天能大幅降低重复查询的压力。分层召回第一轮用快速检索把候选集控制在100条以内第二轮用更重的语义模型精排。这个思路在数据量大的时候尤其有效能避免全局计算带来的性能瓶颈。从我自己项目的压测数据看加了缓存之后P95延迟从差不多4.5秒降到了1.8秒其中很大一部分是因为热点问题不再走完整检索链路。4. 质量评估用数据而不是感觉来判断RAG好不好分块、混合召回这些做完之后一个绕不开的问题是怎么知道系统到底好不好很多人靠手工测试几个问题感觉还行就上线了结果用户一用全是问题。RAG效果必须用一套可量化的评估体系来衡量。4.1 评估维度的拆解RAG的质量评估可以拆成两个大环节检索质量和生成质量。每个环节再往下拆检索质量召回率Recallk正确答案在不在召回的前k条里精确率Precisionk召回的前k条里有多少是真正相关的MRRMean Reciprocal Rank第一个正确答案排在第几位生成质量Faithfulness忠实度回答内容是否严格基于检索到的上下文有没有编造或用模型自己的知识补充Answer Relevancy答案相关性回答是否真的回应了用户的问题有没有答非所问Context Relevance上下文相关性检索到的上下文是否跟问题高度相关幻觉率回答中是否有原文没有支撑的信息这些指标里Faithfulness是RAG系统最核心的指标因为它直接衡量了RAG最想解决的问题——幻觉。4.2 评估集的构建没有评估集一切指标都是空谈。实际上建立一个小而精的评估集比搭一个复杂的评估平台更优先。我建议从这几类问题构建评估集核心高频问题从用户真实日志里抽如果还没有日志就让业务方提供50到100个最常被问到的问题并标注标准答案边界和否定问题公司年假按什么标准计算和公司年假按什么标准不计算这种考察检索对否定语义的区分能力跨文档问题答案需要综合两篇或多篇文档的内容考察系统的多跳检索能力近似语义问题同样一个意思用不同的措辞提问看系统能不能稳定召回对抗性问题故意问知识库里没有的内容比如公司根本没有的业务看系统会不会一本正经地编造评估集的标注不需要一蹴而就每轮迭代加一部分就行。关键是先有50个高质量问题让评估这件事跑起来后续再慢慢扩充覆盖度。4.3 自动化评估的实现方案人工评估100条case要老半天自动化评估是必须的。目前常用的做法有两个方向。方向一用LLM做裁判LLM-as-a-judge。即让一个大模型来评估系统回答的忠实度、相关度等指标。RAGAS框架就是这样的思路它用LangChain或LlamaIndex把评估流程封装好了能算出一组指标分数。from ragas import evaluate from ragas.metrics import ( faithfulness, answer_relevancy, context_precision, context_recall, ) result evaluate( dataseteval_dataset, # 包含question, answer, contexts, ground_truth的验证集 metrics[faithfulness, answer_relevancy, context_precision, context_recall], )用LLM做裁判的好处是普适性强、不需要人工标注坏处是成本高、且有裁判模型自身偏见的风险。所以建议关键指标比如Faithfulness定期抽取部分case做人工复核确保自动评估没有漂移。方向二基于标注集的规则指标。对检索质量这块你用前面构建的包含标准答案的评估集直接算Recallk、Precisionk、MRR等。这些指标不依赖LLM计算快、稳定适合在CI里跑回归。我的实践是两套并行用标注集算检索指标每轮迭代跑保证召回能力不退步用LLM裁判算生成指标小批量跑监控整体质量趋势。4.4 用指标反推系统问题评估的价值不只是打分而是能帮你定位问题出在哪一环。我这里整理一个简单的排查路径现象可能原因排查方向检索召回率低、正确答案不在候选集里分块过大或过小、Embedding模型与领域不匹配、元数据过滤过严调整chunk size换领域Embedding模型检查过滤条件检索精确率低、召回了一堆不相关的内容chunk过小导致碎片化、Rerank没做或权重不对加大chunk或做父子分块引入Rerank召回了相关内容但回答不忠实、编造事实上下文信息不足、提示词约束不够、LLM被无关上下文干扰切换父块上下文强化提示词约束清理无关chunk回答相关但流于表面、没有深入解答只给了片断上下文缺少段落级背景用父子分块方案检索chunk但生成用parent chunk不同问法效果波动大评估集覆盖不够、混合召回权重不合适扩充评估集调研BM25与向量的权重分配在项目里我见过不少明明是生成环节问题却跑去调检索参数的情况。有了这层映射关系可以少走不少弯路。5. 一些现场踩过的坑和个人收尾经验最后分享几个我实际经历过的有代表性的坑很多是文档上不会明写的细节。坑一Embedding模型的max sequence length。有一个项目我们把chunk_size调到了1500个字符然后发现向量化时某些文本报错一查才发现Embedding模型的最大输入长度是512个token。中文字符约等于0.6到1个token不等1500字符很可能超限。超限后有些库直接截断有些库直接报错而截断会导致向量表达偏差。这个必须在选Embedding模型前就确认清楚然后反推chunk_size的限制范围。坑二PDF解析的质量参差不齐。不同PDF解析工具对双栏论文、表格、页眉页脚的处理差异很大。我用过PyPDF2、pdfplumber、unstructured、PyMuPDF效果最好的方案往往是结合使用先用unstructured做基础的layout分析表格部分用专门的表格抽取模块再人工检查样例输出。如果解析质量不过关后面所有环节的效果都会被带偏。坑三Rerank的耗时是真真实实的延迟来源。第一次接BGE-Reranker时没做并发、没做缓存一个查询多了2秒多。后来做了召回结果缓存和异步重排延迟才降到可接受。任何新环节上线前建议先测一下它对整个链路端点延迟的影响不然功能是有了用户在等的时候可不会管你内部逻辑有多精密。坑四评估集要跟着系统一起演进。我曾经花了一整天标注了100个问题测完一轮调整后就丢在那不管了。等到系统迭代了几个版本再用老的评估集一测发现很多指标变差了。其实不是系统变差了而是文档更新了、问题场景变了老的评估集已经不能反映真实情况。评估集需要随知识库内容变化定期增量更新。回到开头说的RAG工程实践本质上没有多少高深的算法创新更多是把分块、召回、生成、评估这些环节里的细节做扎实让每个环节都稳定可控。我在这篇文章里给出的参数和方法算是从我自己的实践中沉淀下来的基线值不一定直接适用于你的场景但如果你想找个起点这些是可以放心用的。后续结合你自己的数据集多测几轮很快就能摸到那套属于你的最优配置。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进