多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

RAG检索增强生成实战:从文档切分到检索评估的完整指南

RAG检索增强生成实战:从文档切分到检索评估的完整指南 1. 为什么RAG值得你花时间搞明白RAG这个词这两年在大模型圈子里出现的频率高得离谱。不管你是做AI应用的开发者还是刚入门大模型的小白只要涉及到“让模型回答得更准”“让模型知道它原本不知道的东西”“让模型别瞎编”绕来绕去最终都会落到RAG上。RAG的全称是Retrieval-Augmented Generation翻译过来叫检索增强生成。名字听着挺学术但核心逻辑其实特别朴素——模型自己记不住或者不知道的东西你帮它去外面查资料查到了再让它组织语言回答。我刚开始接触RAG的时候也觉得这东西应该不难不就是把文档切一切、向量化、存进数据库、查询的时候做相似度匹配、把匹配到的内容塞进提示词里让模型生成答案吗但真正动手做起来才发现坑远比想象的多。切分粒度怎么定向量模型选哪个检索回来一堆不相关的内容怎么办检索评估怎么做这些问题不解决搭出来的RAG系统就是个花架子看着能用实际一问就露馅。这篇内容适合谁看如果你是零基础想入门RAG我会从最基础的概念讲起把每个环节的原理和实操都拆开揉碎如果你已经搭过简单的RAG流程但效果不理想我会重点讲检索评估和优化策略这些是区分“能用”和“好用”的关键。整篇内容会围绕RAG的功能原理、系统构建、检索评估三条主线展开每一步都配上可复现的操作思路和参数选择逻辑。注意RAG不是万能药。它解决的是“知识时效性”和“领域知识注入”的问题但解决不了模型本身推理能力不足的问题。搞清楚它的能力边界比盲目上手更重要。2. RAG到底在解决什么问题2.1 大模型的三道硬伤要理解RAG的价值得先搞清楚大模型本身有哪些绕不过去的局限。我总结下来主要是三道硬伤。第一道是知识截止。任何大模型的训练数据都有时间边界比如某个模型训练数据截止到2024年初那你问它2025年发生的事它要么说不知道要么就开始编。这不是模型笨是它确实没见过。第二道是私有知识盲区。你公司内部的文档、产品手册、客户资料这些东西不可能出现在公开训练数据里。你直接问模型“我们公司产品的退货政策是什么”它只能瞎猜。第三道是幻觉问题。大模型本质上是概率模型它在生成每一个token的时候是在预测“下一个最可能出现的词”。当它没有足够信息支撑的时候它会用看起来合理但实际错误的内容来填充答案。而且它说得特别自信你如果不了解情况根本分辨不出来。这三道硬伤靠重新训练模型或者微调成本高、周期长、效果还不一定好。RAG提供了一条更轻量的路径不改模型参数通过外挂知识库的方式让模型在生成回答之前先“查资料”。2.2 RAG的核心工作流拆解RAG的完整流程可以拆成两个阶段索引阶段和查询阶段。索引阶段是离线的你先把所有知识文档处理好存进向量数据库。具体步骤包括文档加载、文本切分、向量化、存入向量数据库。这个阶段相当于你给模型建了一个“图书馆”把所有的书都编好目、上好书架。查询阶段是在线的用户提问之后系统先把问题向量化然后去向量数据库里找最相似的文本片段把这些片段和原始问题一起塞进提示词最后交给大模型生成答案。这个阶段相当于用户来图书馆问问题你先去书架上找到相关的几本书翻到相关页码然后基于这些内容来回答。听起来很简单对吧但每个环节都有大量细节需要做决策。比如文本切分你切得太碎语义不完整切得太粗检索精度下降。比如向量模型你选中文效果差的模型检索出来的东西驴唇不对马嘴。比如检索策略你只用向量相似度可能漏掉关键词精确匹配的情况。2.3 RAG和微调怎么选经常有人问RAG和微调到底用哪个我的经验是它们解决的不是同一类问题。微调适合改变模型的行为模式比如让模型的输出风格更正式、更简洁或者让模型学会某种特定的输出格式。微调是在教模型“怎么说话”。RAG适合给模型补充知识内容比如让模型知道最新的政策法规、公司内部流程、特定领域的专业知识。RAG是在告诉模型“说什么”。实际项目中两者经常配合使用。先用RAG解决知识注入的问题如果发现模型在特定任务上的输出格式或推理方式不理想再考虑用微调来调整行为。但绝大多数场景下RAG的投入产出比远高于微调因为微调需要标注数据、需要GPU资源、需要反复实验而RAG的迭代周期短得多。实操心得如果你刚开始做RAG不要一上来就追求完美。先用最简单的方案跑通全流程哪怕切分策略很粗糙、向量模型用的是默认的先看到效果再逐步优化每个环节。我见过太多人卡在“选哪个向量模型”这一步纠结好几天结果全流程都没跑通。3. 动手搭建RAG系统从文档到向量库3.1 文档加载与预处理搭建RAG的第一步是把你的知识文档加载进来。文档格式可能五花八门PDF、Word、Markdown、HTML、Excel、数据库导出等等。不同格式需要不同的加载器。PDF是最麻烦的格式之一。有些PDF是扫描件需要OCR有些PDF有复杂的表格和图表直接提取文本会丢失结构信息有些PDF是多栏排版提取出来的文本顺序是乱的。我的建议是如果PDF质量太差宁可手动整理成Markdown或纯文本也不要硬用自动提取因为垃圾进垃圾出后面检索效果一定好不了。Word文档相对好处理用python-docx之类的库可以提取段落和表格。Markdown和纯文本最简单直接读取就行。HTML需要去掉标签保留正文内容。预处理阶段还需要做几件事去掉页眉页脚、去掉重复内容、统一编码格式、处理特殊字符。这些看起来是小事但如果不做后面切分出来的文本块会包含大量噪音影响检索质量。# 以PDF加载为例的伪代码思路 from langchain.document_loaders import PyPDFLoader loader PyPDFLoader(knowledge_base.pdf) pages loader.load() # 检查每页内容质量 for i, page in enumerate(pages): text page.page_content.strip() if len(text) 50: print(f第{i}页内容过短可能是扫描件或空白页需要单独处理)3.2 文本切分粒度决定成败文本切分是RAG系统中最容易被低估的环节。很多人随便设个chunk_size1000、chunk_overlap200就完事了结果检索效果一塌糊涂。切分的核心矛盾在于块太小语义不完整块太大噪音太多。你想想如果一个文本块只有一句话“该政策自2024年1月1日起施行”检索出来之后模型根本不知道这是什么政策。但如果一个文本块有3000字里面涵盖了五个不同的主题你检索“退货政策”的时候可能匹配到的是这个块里关于“换货政策”的那部分但模型看到的是整个3000字容易被无关信息干扰。我的经验是按语义边界切分比按固定字数切分效果好得多。具体来说对于结构化文档如Markdown、HTML按标题层级切分每个小节作为一个块如果小节太长再按段落细分。对于非结构化文档如纯文本、对话记录按段落切分如果段落太长再按句子切分。对于代码文档按函数或类切分。对于表格把表头和每一行组合成一个完整的描述块。chunk_size的设置没有标准答案但有一个经验范围中文文本建议在300-800字之间英文文本建议在200-500词之间。chunk_overlap建议设为chunk_size的10%-20%目的是让相邻块之间有上下文衔接避免在边界处丢失信息。# 按标题层级切分的思路 from langchain.text_splitter import MarkdownHeaderTextSplitter headers_to_split_on [ (#, 一级标题), (##, 二级标题), (###, 三级标题), ] splitter MarkdownHeaderTextSplitter(headers_to_split_onheaders_to_split_on) chunks splitter.split_text(markdown_content) # 如果某个块超过800字再用递归切分器细分 from langchain.text_splitter import RecursiveCharacterTextSplitter recursive_splitter RecursiveCharacterTextSplitter( chunk_size600, chunk_overlap100, separators[\n\n, \n, 。, , , , , ] )注意事项切分的时候一定要保留元数据。比如每个块属于哪个文档、哪个章节、哪个页码。这些元数据在检索阶段可以用来做过滤在生成阶段可以用来做引用标注。我见过有人切完只保留文本内容后面想加过滤功能的时候发现元数据全丢了只能重新处理一遍。3.3 向量化选对模型比选贵的更重要向量化就是把文本转成一串数字向量这串数字代表了文本的语义信息。语义相近的文本向量距离就近语义无关的文本向量距离就远。向量模型的选择直接决定了检索质量的上限。如果向量模型本身对中文语义理解不好后面再怎么优化检索策略都是白搭。选向量模型的时候我主要看几个维度语言支持是否支持中文中文效果如何。有些模型英文很强但中文拉胯。维度向量维度越高表达能力越强但存储和计算成本也越高。常见的有384维、768维、1024维、1536维。最大输入长度模型能处理多长的文本。如果你的chunk_size是800字模型最大输入只有512个token那超出的部分会被截断。推理速度如果你有大量文档需要向量化推理速度直接影响索引构建时间。是否开源开源模型可以本地部署数据不出内网闭源API模型通常效果更好但需要联网调用。目前中文场景下常用的向量模型有几类一类是专门针对中文优化的开源模型一类是多语言通用模型还有一类是商业API。我的建议是如果你的数据敏感度不高可以先用商业API快速验证效果如果数据不能出内网就选开源模型本地部署。# 向量化示例以开源模型为例 from sentence_transformers import SentenceTransformer model SentenceTransformer(your-chinese-embedding-model) texts [文本块1的内容, 文本块2的内容] embeddings model.encode(texts, normalize_embeddingsTrue) # normalize_embeddingsTrue 很重要 # 归一化之后余弦相似度计算可以用点积代替速度更快实操心得不要盲目追求高维度。我实测下来768维和1536维在大多数中文检索场景下的效果差异并不明显但存储成本差了一倍。先用768维跑起来如果发现检索效果确实不够再考虑升级。3.4 向量数据库选型与入库向量数据库是专门用来存储和检索向量的。选型的时候主要考虑几个因素数据量级、查询延迟要求、是否需要持久化、是否需要分布式、运维成本。小规模场景几万到几十万条向量用FAISS就够了。FAISS是Facebook开源的向量检索库轻量、快、不需要额外部署服务直接嵌在Python代码里就能用。缺点是它本质上是个索引文件不支持增删改查的实时操作每次更新都需要重建索引。中等规模场景百万到千万级可以考虑Milvus、Qdrant、Weaviate这类专门的向量数据库。它们支持实时增删改查、支持分布式部署、有完善的API和监控。缺点是部署和运维复杂度上来了。如果已经有PostgreSQL可以用pgvector扩展直接在关系型数据库里存向量。好处是不用额外维护一套数据库坏处是性能和功能不如专门的向量数据库。大规模场景亿级以上那就需要考虑分布式向量数据库了比如Milvus集群版。这个量级一般公司也碰不到这里不展开。入库的时候除了向量本身还要存原始文本和元数据。原始文本用于后续塞进提示词元数据用于过滤和引用。# 以FAISS为例的入库思路 import faiss import numpy as np dimension 768 # 向量维度 index faiss.IndexFlatIP(dimension) # IP Inner Product内积 # 假设embeddings是numpy数组shape为(n, 768) embeddings np.array(embeddings).astype(float32) index.add(embeddings) # 保存索引 faiss.write_index(index, vector_index.faiss) # 同时保存文本和元数据 import json with open(chunks.json, w, encodingutf-8) as f: json.dump(chunks, f, ensure_asciiFalse)4. 检索策略与效果评估4.1 向量检索的局限与补充方案向量检索的核心是语义相似度它擅长处理“意思相近但用词不同”的情况。比如用户问“怎么退钱”文档里写的是“退款流程”向量检索能匹配上。但向量检索也有明显的短板。第一它对精确匹配不敏感。比如用户问“产品型号X200的保修期”如果文档里写的是“X200型号保修期为两年”向量检索可能匹配到其他型号的保修信息因为语义上都是“保修期”。第二它对否定语义处理不好。比如用户问“哪些情况不支持退货”向量检索可能匹配到“支持退货的情况”因为语义相似度很高。第三它对数字和专有名词不敏感。比如“2024年政策”和“2023年政策”向量可能很接近但实际内容完全不同。解决这些问题的常见方案是混合检索向量检索 关键词检索如BM25然后把两路结果融合。融合策略有几种加权求和、RRFReciprocal Rank Fusion、先向量后关键词过滤等。# RRF融合策略的伪代码 def rrf_fusion(vector_results, keyword_results, k60): scores {} for rank, doc_id in enumerate(vector_results): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) for rank, doc_id in enumerate(keyword_results): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) # 按分数降序排列 sorted_docs sorted(scores.items(), keylambda x: x[1], reverseTrue) return [doc_id for doc_id, _ in sorted_docs]RRF的好处是不需要调权重对两路检索的分数尺度不敏感。我实测下来RRF在大多数场景下比加权求和更稳定。4.2 重排序让最相关的结果排到最前面检索回来Top-K个结果之后还有一个优化空间重排序。向量检索用的是双塔模型查询和文档分别编码然后算相似度。这种方式速度快但精度有限。重排序用的是交叉编码器把查询和文档拼在一起输入模型模型直接输出相关性分数。这种方式精度高但速度慢所以只适合对少量候选结果做精排。典型流程是向量检索召回Top-50然后用重排序模型对50个结果精排取Top-5塞进提示词。这样既保证了召回率又保证了精度。重排序模型的选择和向量模型类似也要看中文支持、推理速度、是否开源。有些向量模型本身就提供了配套的重排序模型搭配使用效果更好。注意事项重排序不是必须的。如果你的检索结果本身已经很好Top-5都是相关的加不加重排序差别不大。但如果你的检索结果噪音比较多重排序能显著提升效果。建议先做检索评估看看当前的问题出在召回阶段还是排序阶段再决定要不要加重排序。4.3 检索评估怎么知道你的RAG好不好这是很多人忽略的环节。搭完RAG系统随便问几个问题觉得“好像还行”就上线了。结果用户一问稍微偏一点的问题就露馅了。检索评估需要一套系统的指标和方法。核心指标包括召回率相关文档有多少被检索出来了。召回率低说明你的检索策略漏掉了重要信息。精确率检索出来的文档有多少是相关的。精确率低说明噪音太多会干扰模型生成。MRR第一个相关文档排在第几位。MRR高说明最相关的内容排在最前面。NDCG考虑排序位置的相关性指标越相关的内容排得越靠前分数越高。做评估需要标注数据。你需要准备一批查询每个查询标注哪些文档是相关的。标注数据不用很多50-100个查询就能看出问题。关键是覆盖不同类型的查询事实型、对比型、否定型、多跳推理型等。# 简单的检索评估示例 def evaluate_retrieval(queries, ground_truth, retriever, k5): recall_scores [] precision_scores [] mrr_scores [] for query, relevant_ids in zip(queries, ground_truth): retrieved retriever.search(query, top_kk) retrieved_ids [doc.id for doc in retrieved] # 召回率 hit len(set(retrieved_ids) set(relevant_ids)) recall hit / len(relevant_ids) if relevant_ids else 0 recall_scores.append(recall) # 精确率 precision hit / k precision_scores.append(precision) # MRR for rank, doc_id in enumerate(retrieved_ids): if doc_id in relevant_ids: mrr_scores.append(1 / (rank 1)) break else: mrr_scores.append(0) return { recallk: sum(recall_scores) / len(recall_scores), precisionk: sum(precision_scores) / len(precision_scores), mrr: sum(mrr_scores) / len(mrr_scores) }评估结果怎么用如果召回率低说明你的切分粒度、向量模型、检索策略有问题需要调整。如果精确率低但召回率高说明检索回来的东西太多太杂需要加重排序或者调整Top-K。如果MRR低说明排序有问题最相关的内容没有排到前面。4.4 生成阶段的优化技巧检索做完了最后一步是把检索结果和用户问题一起塞给大模型生成答案。这一步也有不少优化空间。提示词设计是关键。你需要明确告诉模型只根据提供的参考资料回答如果参考资料里没有相关信息就说不知道不要自己编。这个约束能大幅降低幻觉。上下文长度控制也很重要。检索回来的内容不是越多越好。塞太多内容进去一是可能超出模型的上下文窗口二是无关信息会干扰模型。我的经验是Top-3到Top-5通常就够了具体看chunk_size和模型上下文窗口。引用标注能提升可信度。在提示词里要求模型在回答中标注信息来源比如“根据文档A第3节的内容...”。这样用户能验证答案的可靠性也方便排查问题。# 提示词模板示例 PROMPT_TEMPLATE 你是一个知识助手。请根据以下参考资料回答用户问题。 参考资料 {context} 用户问题{question} 回答要求 1. 只根据参考资料回答不要使用你自己的知识 2. 如果参考资料中没有相关信息直接说根据现有资料无法回答该问题 3. 回答时标注信息来源格式为[来源文档名] 4. 回答要简洁准确不要展开无关内容 回答5. 常见问题与排查技巧实录5.1 检索效果差的排查思路检索效果差是最常见的问题。排查的时候我一般按这个顺序来第一步检查切分质量。随便抽几个chunk出来看看是不是语义完整有没有被切断的句子有没有包含大量噪音如果切分质量差后面怎么优化都是白搭。第二步检查向量模型。拿几个查询和对应的相关文档手动算一下向量相似度。如果相关文档的相似度还不如无关文档高说明向量模型不适合你的场景。第三步检查检索策略。是不是只用了向量检索有没有考虑混合检索Top-K设的是多少K太小可能漏掉相关结果K太大噪音太多。第四步检查评估方法。你的评估指标合理吗标注数据准确吗有时候不是系统效果差是评估方法有问题。5.2 模型回答不准确的原因分析检索回来的内容是对的但模型回答还是不对这种情况通常有几个原因提示词约束不够模型没有严格遵循“只根据参考资料回答”的指令掺杂了自己的知识。上下文太长塞了太多内容模型注意力被分散关键信息被淹没。参考资料格式混乱检索回来的文本没有清晰的边界模型分不清哪些是参考资料、哪些是问题。模型本身能力不足有些小模型在长上下文场景下表现确实差换更大的模型可能就解决了。5.3 性能优化的几个方向RAG系统的性能优化主要从两个维度考虑延迟和成本。延迟优化向量检索本身很快瓶颈通常在向量化和重排序。如果向量模型推理慢可以考虑用更小的模型或者做量化。如果重排序慢可以减少候选数量或者用更快的重排序模型。成本优化如果用的是商业API向量化和生成都是按量计费的。优化方向包括减少不必要的向量化比如增量更新而不是全量重建、压缩上下文长度、缓存常见查询的结果。实操心得我踩过最大的坑是忽略了增量更新。一开始每次文档有更新就全量重建索引几万条数据要跑好几个小时。后来改成只对新增和修改的文档做向量化删除的文档从索引里移除更新时间缩短到几分钟。如果你的知识库更新频繁一定要设计好增量更新机制。5.4 常见问题速查表问题现象可能原因排查方向解决思路检索结果完全不相关向量模型不匹配检查向量模型的中文效果换用中文优化的向量模型检索结果漏掉关键信息切分粒度过粗或过细检查chunk_size和切分边界调整切分策略按语义边界切分相似问题检索结果不稳定向量模型对语义变化敏感测试不同表述的相似度增加查询改写或混合检索模型回答包含幻觉提示词约束不够检查提示词模板强化“只根据参考资料回答”的约束系统响应慢向量化或重排序耗时分阶段计时优化模型选择或减少候选数量新增文档检索不到索引未更新检查索引更新机制实现增量更新流程6. 从能用走向好用持续迭代的思路RAG系统不是搭完就完事了它需要持续迭代。迭代的依据来自两个方面用户反馈和评估数据。用户反馈是最直接的信号。用户点了“答案不准”的按钮或者追问“你确定吗”这些都是优化线索。把这些问题收集起来分析是检索问题还是生成问题然后针对性优化。评估数据是更系统的依据。定期跑一遍评估集看各项指标的变化趋势。如果召回率在下降可能是知识库内容老化了如果精确率在下降可能是文档噪音增加了。迭代的优先级建议是先解决检索问题再解决生成问题。因为检索是根基检索不准生成再优化也没用。检索问题里先解决切分和向量模型的问题再考虑混合检索和重排序。还有一个容易被忽略的点知识库的维护。文档会过期、会更新、会删除。你需要一套机制来保证知识库和实际业务保持一致。我见过一些RAG系统刚上线效果很好过了半年就越来越差因为知识库没人维护里面全是过时信息。最后分享一个我在实际项目中总结的小技巧给每个chunk打上时间戳和版本号。检索的时候可以优先返回最新版本的内容。如果同一个问题有多个版本的答案模型可以基于时间戳来判断哪个是最新的。这个小小的元数据字段在知识频繁更新的场景下能省很多事。
返回列表