新闻详情

新闻详情

首页 / 资讯中心 / 详情

RAG 实战全链路:从文档切块到检索生成的避坑指南

发布时间:2026/9/24 23:07:46来源:尧图网络
RAG 实战全链路:从文档切块到检索生成的避坑指南
RAG 这个词从 2023 年火到现在几乎成了 Agent 开发的必修课。但我在带团队和看社区提问时发现一个很普遍的现象很多人能把 LangChain 的RetrievalQA链跑通Demo 里问一句答一句看着挺像回事可一旦换成自己的业务文档检索出来的东西就开始驴唇不对马嘴——要么召回一堆无关段落要么明明文档里有答案却死活搜不到要么生成的内容开始一本正经地胡说八道。问题往往不在模型而在从建库到检索再到生成这条链路上每一步都藏着需要拿经验去填的坑。这篇笔记我打算把 RAG 的完整链路拆开讲透从文档切块、Embedding 选型、向量库落地到检索策略、重排、再到最终喂给 LLM 的生成环节。适合已经跑通过最简 Demo、但想让系统真正能用的开发者也适合刚开始接触 Agent 开发、想搞清楚 RAG 到底怎么回事的朋友。我会尽量把每个环节为什么这么做讲清楚而不是只丢一段能跑的代码——因为 RAG 这东西参数调错一个效果能差出一大截。1. 先把 RAG 的链路想明白再动手1.1 RAG 到底在解决什么问题大模型有两个天生的短板一是知识有截止日期训练完之后新发生的事它不知道二是它不知道你私有的东西你公司内部的文档、产品手册、客服记录它一概没概念。你当然可以把文档内容直接塞进 prompt 里让它读但上下文窗口再大也是有限的而且塞太多无关内容反而会稀释模型的注意力让它抓不住重点。RAGRetrieval-Augmented Generation检索增强生成的思路很朴素既然模型不知道那我在它回答之前先去我的知识库里把相关的内容找出来连同问题一起喂给它让它看着材料答题。这就像开卷考试——模型是考生向量库是那本可以快速翻到对应页码的参考书检索器就是索引。理解这一点很关键因为它决定了你优化的方向。RAG 系统的质量上限取决于你找出来的材料质量如何。材料找错了再强的模型也救不回来。所以整条链路里检索环节的权重其实比很多人想象的要高得多。1.2 一条完整的 RAG 链路长什么样我把链路拆成两个阶段来看这样更清晰。离线建库阶段一次性或定期执行加载原始文档PDF、Word、Markdown、网页、数据库记录等文本切块Chunking把长文档切成一段段适合检索的小块对每个块做 Embedding转成向量把向量和原始文本、元数据一起存进向量数据库在线检索生成阶段每次用户提问时执行把用户问题也做 Embedding转成向量在向量库里做相似度检索召回 Top-K 个最相关的块可选但强烈建议对召回结果做重排Rerank把问题和召回内容拼成 prompt交给 LLM 生成答案这两个阶段里建库阶段决定了库里有什么、以什么粒度存检索阶段决定了能不能精准捞出来生成阶段决定了捞出来之后怎么用好。三个环节环环相扣任何一个掉链子整体体验都会崩。1.3 为什么很多人第一步就走偏了我见过太多人一上来就纠结用哪个向量库、哪个 Embedding 模型却忽略了最基础也最影响效果的一环——切块。文档切得不好后面全是白搭。一个 2000 字的长段落被硬切成两半语义被拦腰截断检索时两个半块都变得不完整谁都匹配不上。反过来块切得太大一个块里混了好几个主题Embedding 出来的向量就成了四不像检索精度直线下降。所以我的建议是先把切块策略想清楚再谈模型和库的选型。这是整条链路的地基。2. 文档切块决定检索质量的地基2.1 切块粒度怎么定切块大小没有万能答案但有一个经验区间中文场景下单块 300 到 800 字是比较舒服的范围。太小了语义不完整太大了噪声多。英文的话大概对应 200 到 500 个 token。但比大小更重要的是怎么切。最粗暴的做法是按固定字符数硬切比如每 500 字一刀。这种做法的问题是它完全不管语义边界经常把一句话、一个表格、一段代码从中间劈开。稍微好一点的做法是按段落、按标题层级切让每个块尽量是一个语义完整的单元。LangChain 里提供了几种切分器我常用的组合是这样的from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , , , ], length_functionlen, ) chunks splitter.split_text(long_document)这里有几个点值得说。separators的顺序很重要它会优先按靠前的分隔符切切出来的块如果还超过chunk_size就换下一个分隔符继续切。中文场景一定要把中文标点加进去否则它只会按空格和换行切效果很差。chunk_overlap是块与块之间的重叠部分我一般设成 chunk_size 的 15% 到 20%。为什么要重叠因为硬边界处容易丢上下文重叠一段能让被切断的语义在相邻块里补回来检索时更容易命中。2.2 结构化文档要特殊对待如果你的文档是 Markdown、HTML 或者带标题层级的千万别用纯文本切分器一把梭。这类文档的结构本身就是重要的语义信息。Markdown 可以用MarkdownHeaderTextSplitter它会按标题层级切并且把标题路径作为元数据保留下来。from langchain.text_splitter import MarkdownHeaderTextSplitter headers_to_split_on [ (#, h1), (##, h2), (###, h3), ] md_splitter MarkdownHeaderTextSplitter(headers_to_split_onheaders_to_split_on) md_chunks md_splitter.split_text(markdown_content)这样切出来的每个块都带着它属于哪个章节的信息检索命中后你还能知道这段内容出自文档的哪个部分对生成答案时的引用和溯源特别有用。对于表格我的经验是尽量整表保留不要切开。表格一旦被切断行列对应关系就乱了模型读到的是一堆没有意义的数字。如果表格实在太大可以考虑把表格转成字段: 值的描述性文本再切。2.3 元数据是容易被忽略的宝藏很多人切完块就直接 Embedding 入库了元数据一概不留。这是个损失。元数据能在检索阶段帮你做过滤大幅提升精度。我一般会给每个块打上这些元数据source来源文件名或 URLsection所属章节标题page页码PDF 场景doc_type文档类型手册、FAQ、政策等updated_at更新时间有了这些检索时就能做只在某类文档里搜只搜最近更新的内容这类过滤。比如用户问的是产品价格你就可以限定doc_typeprice_list再检索召回质量立刻不一样。提示元数据的字段设计要在建库前就想好因为一旦入库后期补元数据需要重新处理整个库成本很高。3. Embedding 模型选型别只看排行榜3.1 中文场景下的选型思路Embedding 模型负责把文本转成向量它的质量直接决定检索的准确度。网上各种排行榜很多但排行榜上的分数是在通用数据集上跑的未必贴合你的业务。我的建议是先圈定几个候选然后拿你自己的真实问题去测谁召回得准用谁。中文场景下我实际用过并且觉得靠谱的有几个方向。一是 BGE 系列如 bge-large-zh、bge-m3在中文语义匹配上表现稳定社区支持好本地部署方便。二是 M3E 系列轻量、速度快适合对延迟敏感的场景。三是一些商业 API 提供的 Embedding 服务省去部署麻烦但要注意数据出境的合规问题内部敏感文档慎用。选型时除了准确度还要看这几个维度维度说明影响向量维度常见 768、1024、1536维度越高存储和计算成本越大最大输入长度一般 512 token超过会被截断影响长文本效果推理速度本地模型看硬件影响建库耗时和检索延迟多语言支持是否支持中英混合混合文档场景很关键部署方式本地 / API涉及成本和数据合规3.2 一个常被忽视的坑查询和文档要用同一个模型这是新手最容易犯的错误之一。建库时用模型 A 把文档转成向量检索时用模型 B 把问题转成向量然后发现检索结果一塌糊涂。原因很简单不同模型生成的向量空间是不兼容的A 空间里的向量和 B 空间里的向量做相似度计算结果毫无意义。所以记住一条铁律建库和检索必须用同一个 Embedding 模型连版本都要一致。如果你中途换了模型整个库都得重新 Embedding 一遍没有捷径。3.3 要不要做向量归一化大部分现代 Embedding 模型输出的向量已经做过归一化用余弦相似度检索时不需要额外处理。但如果你用的是自定义模型或者不确定建议在建库前统一做一次 L2 归一化这样用内积dot product就能等价于余弦相似度检索时计算更快。import numpy as np def normalize(vec): norm np.linalg.norm(vec) return vec / norm if norm 0 else vec这个细节看起来小但在大规模检索时对性能有实际影响。4. 向量数据库落地Milvus、Chroma、Qdrant 怎么选4.1 三个库的定位差异向量数据库这块社区里讨论最多的就是 Milvus、Chroma 和 Qdrant。它们不是互相替代的关系而是面向不同场景的。Chroma主打轻量和易用几行代码就能跑起来适合原型验证、小规模本地知识库、个人项目。它可以直接以嵌入式模式运行不需要单独起服务数据存在本地。缺点是规模上来之后性能和功能都吃紧不适合生产环境的大规模场景。Qdrant是 Rust 写的性能和资源效率都不错支持丰富的过滤条件和 payload 索引API 设计也比较清爽。它适合中小规模的生产场景部署简单单机就能扛不少量。过滤检索是它的强项如果你的 RAG 需要大量基于元数据的过滤Qdrant 会很顺手。Milvus是冲着大规模去的支持分布式部署、多种索引类型IVF、HNSW、DiskANN 等、存算分离。它适合数据量上千万甚至上亿、对吞吐和扩展性有要求的场景。代价是部署和运维复杂度高小项目用它属于杀鸡用牛刀。我一般这么选个人学习、Demo、几千到几万条数据Chroma中小型生产、需要复杂过滤、几十万到几百万条Qdrant大规模、高并发、需要水平扩展Milvus4.2 用 Chroma 快速搭一个本地库先看 Chroma 的用法因为它最简单适合把链路先跑通。import chromadb from chromadb.utils import embedding_functions client chromadb.PersistentClient(path./my_rag_db) emb_fn embedding_functions.SentenceTransformerEmbeddingFunction( model_nameBAAI/bge-large-zh-v1.5 ) collection client.get_or_create_collection( nameknowledge_base, embedding_functionemb_fn, metadata{hnsw:space: cosine} ) collection.add( documents[文档块1的文本, 文档块2的文本], metadatas[{source: a.pdf, section: intro}, {source: b.pdf, section: faq}], ids[chunk_001, chunk_002] )PersistentClient会把数据持久化到磁盘重启不丢。hnsw:space设成 cosine 表示用余弦距离。注意 Chroma 可以自己管理 Embedding传embedding_function也可以你算好向量直接传embeddings参数后者更灵活适合你已经用别的模型算好向量的情况。4.3 Qdrant 的过滤检索实战Qdrant 的过滤能力是我最欣赏的地方。假设你只想在产品手册类文档里检索可以这样写from qdrant_client import QdrantClient from qdrant_client.models import Filter, FieldCondition, MatchValue client QdrantClient(path./qdrant_db) hits client.search( collection_nameknowledge_base, query_vectorquery_embedding, query_filterFilter( must[ FieldCondition( keydoc_type, matchMatchValue(valueproduct_manual) ) ] ), limit5 )这种先过滤再检索的能力在真实业务里非常有用。因为用户的问题往往自带范围比如售后政策里关于退货的规定你完全可以把doc_type限定在售后类文档把无关的产品介绍、技术文档全部排除召回精度能提升一大截。4.4 索引类型的选择向量库的检索性能很大程度上取决于索引。以 HNSW 为例它是目前最常用的近似最近邻索引通过构建多层图结构来加速检索。关键参数有两个M每个节点的连接数和ef_construction建索引时的候选集大小。M 越大、ef_construction 越大索引质量越高但建索引越慢、内存占用越大。我的经验值是M 取 16 到 32ef_construction 取 100 到 200对大多数场景够用。检索时的ef_search参数控制精度和速度的权衡调大召回更全但更慢一般从 64 起步往上试。注意索引参数没有最优解只有最适合你数据分布和查询模式的解。上线前一定要用真实查询压测别照搬别人的配置。5. 检索策略从能搜到到搜得准5.1 纯向量检索的局限向量检索擅长语义匹配用户问怎么退款它能召回退货流程说明即使字面没有退款两个字。这是它的优势。但它也有短板对精确的关键词、专有名词、编号、代码这类内容不敏感。用户问错误码 E1024 什么意思向量检索可能召回一堆泛泛的错误处理说明却漏掉那条精确提到 E1024 的记录。所以纯向量检索在真实场景里往往不够用需要配合其他手段。5.2 混合检索向量 关键词混合检索的思路是把向量检索和关键词检索如 BM25的结果融合起来兼顾语义和精确匹配。常见做法是两路各召回一批然后用 RRFReciprocal Rank Fusion之类的算法合并排序。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) return sorted(scores.items(), keylambda x: x[1], reverseTrue)RRF 的好处是不需要归一化两路分数直接按排名融合简单又稳。实测下来混合检索在包含大量专有名词的技术文档场景里召回率比纯向量检索有明显提升。5.3 重排把最相关的顶上来检索召回 Top-K 之后还有一个提升精度的利器——重排Rerank。向量检索用的是双塔结构查询和文档分别编码再算相似度速度快但精度有限。重排模型用的是交叉编码器Cross-Encoder把查询和文档拼在一起过一遍模型精度高得多代价是慢。所以典型做法是先用向量检索快速召回一批比如 50 条再用重排模型精排取前 5 条喂给 LLM。这样兼顾了速度和精度。from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-large) pairs [(query, doc) for doc in candidate_docs] scores reranker.predict(pairs) ranked sorted(zip(candidate_docs, scores), keylambda x: x[1], reverseTrue) top_docs [doc for doc, _ in ranked[:5]]重排这一步是我认为性价比最高的优化之一。很多 RAG 系统效果不好加个重排就能明显改善而且改动量很小。5.4 查询改写让问题更好搜用户的问题往往口语化、含糊直接拿去检索效果不好。查询改写Query Rewriting就是先把问题加工一下再检索。常见手法有几种一是扩展把问题里的关键词做同义词扩展比如退款扩展成退款、退货、退费。二是分解把复杂问题拆成几个子问题分别检索比如对比 A 和 B 的价格和售后可以拆成A 的价格B 的价格A 的售后B 的售后。三是改写用 LLM 把口语化问题改写成更适合检索的形式。rewrite_prompt 把下面的用户问题改写成适合知识库检索的查询语句 保留关键实体和意图去掉口语化表达。只输出改写后的查询不要解释。 用户问题{question} 改写查询这一步用 LLM 来做成本不高但效果明显尤其是面对复杂问题时。6. 生成环节把检索结果用好6.1 Prompt 怎么拼检索到相关块之后要把它们和问题一起拼成 prompt 交给 LLM。这里有几个要点。第一明确告诉模型只根据给定材料回答。不这么约束模型很容易用自己的先验知识去补甚至编造。第二给材料编号要求模型在回答时标注引用来源方便溯源。第三如果材料里没有答案要允许模型说不知道而不是硬编。system_prompt 你是一个严谨的知识库助手。请严格根据下面提供的参考资料回答问题。 规则 1. 只使用参考资料中的信息不要使用你自己的知识 2. 如果参考资料中没有相关信息直接回答根据现有资料无法回答该问题 3. 回答时用 [1][2] 标注引用的资料编号 4. 保持回答简洁准确不要编造 参考资料 {context} user_prompt 问题{question}6.2 上下文塞多少合适召回的内容不是越多越好。塞太多一是超出上下文窗口二是引入噪声三是增加成本。我的经验是精排后取 3 到 5 个块比较合适具体看块的大小。如果块比较小300 字左右可以取 5 到 8 个块比较大800 字以上取 3 个就够。还有一个技巧是给每个块加上来源标注比如【来源产品手册 第3章】这样模型在生成时能更好地组织答案用户也能看到依据。6.3 处理检索不到的情况真实系统里用户问的问题有相当一部分是知识库里没有的。这时候如果硬让模型回答它就会开始编。所以要在检索阶段设一个相似度阈值低于阈值的召回直接丢弃。如果过滤后一个块都不剩就直接返回未找到相关内容不要交给 LLM。SCORE_THRESHOLD 0.5 valid_docs [doc for doc, score in retrieved if score SCORE_THRESHOLD] if not valid_docs: return 抱歉知识库中没有找到与您问题相关的内容。这个阈值需要根据你的 Embedding 模型和相似度度量方式来调建议拿一批真实问题测一下看看正样本和负样本的分数分布取一个能分开两者的值。6.4 流式输出与引用展示体验层面生成环节建议用流式输出让用户看到答案一个字一个字蹦出来感知延迟低很多。同时把引用的来源块展示在答案下方用户点开能看到原文既增加可信度也方便核对。for chunk in llm.stream(prompt): yield chunk.content引用展示这块可以在召回时就把每个块的source和section记下来生成完答案后一并返回给前端。7. 几个我踩过的坑和对应的解法7.1 检索结果总是差一口气有段时间我发现明明文档里有答案检索就是搜不出来。排查下来问题出在切块上——那个关键信息正好被切在了两个块的交界处两个块各拿一半谁都不完整。解法是加大chunk_overlap让交界处的信息在相邻块里都完整出现一次。这个坑很隐蔽因为你看单个块都觉得挺正常但就是搜不到。7.2 换了 Embedding 模型后全乱了前面提过建库和检索必须用同一个模型。我有个同事图省事建库用了一个模型后来觉得另一个模型排行榜更高检索时偷偷换了结果召回质量断崖式下跌。查了半天才想起来是模型不一致。所以换模型一定要重新建库别偷懒。7.3 元数据过滤把结果全滤没了用 Qdrant 做过滤检索时我一度把doc_type设得太细结果用户的问题跨了好几个类型过滤后一个都不剩。后来改成优先过滤过滤后为空则回退到全库检索的策略兼顾了精度和召回。filtered search_with_filter(query, doc_typemanual) if not filtered: filtered search_without_filter(query)7.4 重排模型拖慢了响应重排虽然准但它是串行计算的候选多了会很慢。我的做法是控制送入重排的候选数量一般 30 到 50 条足够再多边际收益就低了。另外重排模型可以量化或者用更小的版本速度能快不少精度损失可接受。7.5 中文标点导致的切块异常用RecursiveCharacterTextSplitter时如果separators里没加中文标点它就只能按空格和换行切中文文档里空格少结果就是一大坨一大坨地切块大得离谱。加上中文标点后立刻正常。这个坑新手特别容易踩因为英文教程里默认的 separators 是不带中文标点的。8. 从 Demo 到能用还需要补的几块8.1 增量更新与去重真实知识库是活的文档会新增、修改、删除。你的建库流程要支持增量更新而不是每次全量重建。做法是给每个块一个稳定的 ID比如source section 内容hash更新时先按 ID 删除旧块再插入新块。去重也很重要同一份文档被重复导入会产生大量冗余块检索时互相挤占名额。8.2 效果评估怎么做RAG 系统不能靠感觉调得有评估。最基础的是准备一批问题-标准答案-相关文档的测试集然后看两个指标检索的召回率相关文档有没有被召回和生成的准确率答案对不对。检索和生成要分开评估这样才能定位问题出在哪一环。没有标注数据的话可以用 LLM 来做自动评估让它判断生成的答案是否被召回内容支持、是否回答了问题。虽然不如人工准但胜在能快速迭代。8.3 和 Agent 的结合RAG 本身是个检索工具把它封装成 Agent 的一个 toolAgent 就能在需要的时候自主调用。比如一个客服 Agent遇到产品问题调 RAG 查手册遇到订单问题调另一个 tool 查数据库。这种Agent 编排多个工具的模式比单纯的 RAG 问答灵活得多也是现在 Agent 开发的主流方向。from langchain.tools import Tool rag_tool Tool( nameknowledge_search, funcrag_pipeline.query, description当需要查询产品手册、政策文档等内部知识时使用此工具 )把 RAG 做成 tool 之后Agent 会根据问题类型决定要不要检索、检索什么而不是无脑每次都检索。这在多轮对话和复杂任务里优势明显。8.4 缓存与性能高频问题可以加一层缓存相同或相似的问题直接返回缓存结果省去检索和生成的开销。缓存 key 可以用问题的 Embedding 做近似匹配也可以用问题文本的 hash 做精确匹配。另外 Embedding 计算和向量检索都可以批量化建库时批量 Embedding 比逐条快得多。我在实际项目里最深的一个体会是RAG 的效果提升往往不是靠换更强的模型而是靠把切块、检索、重排这些脏活累活做扎实。模型是现成的但你的数据怎么切、怎么存、怎么捞这些才是真正拉开差距的地方。一个切块策略调好的小模型 RAG效果能吊打一个切块稀烂的大模型 RAG。所以别急着追新模型先把链路上每个环节的细节抠到位收益远比想象中大。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

粒子群算法(PSO)原理与MATLAB实战:从Rastrigin寻优到TSP求解 2026/9/24 23:51:33

粒子群算法(PSO)原理与MATLAB实战:从Rastrigin寻优到TSP求解

粒子群算法(Particle Swarm Optimization,PSO)是少数几个我用了五年多还觉得"每次都有新感觉"的启发式算法。最早接触它是在研究生阶段的智能计算课程上,当时我已经被遗传算法的编码、选择、交叉、变异搞得头晕&#xf…

阅读更多 →
MCP构建工具实战:在Grix中打造高可靠AI服务中枢 2026/9/24 23:51:33

MCP构建工具实战:在Grix中打造高可靠AI服务中枢

1. 为什么要在Grix里孵化MCP构建工具先把我的理解放在前面。Model Context Protocol(模型上下文协议,简称MCP)解决的是大模型与外部世界之间的连接问题。过去我们做一个AI应用,接入数据库、调用API、读取文件,每一步都…

阅读更多 →
基于Django与协同过滤的新疆特产推荐系统设计与可视化实践 2026/9/24 23:51:33

基于Django与协同过滤的新疆特产推荐系统设计与可视化实践

每年到了毕业设计季,总能在各种群里看到“求推荐系统毕设”的消息。其实推荐系统这个方向本身很适合做本科或硕士的毕设课题:它既有算法层面的东西可以写、能讲出深度,又有数据和界面层面的东西可以展示、能给出直观的演示效果,再…

阅读更多 →
CCKS 2019中文电子病历数据集实战:从解压到实体识别全流程 2026/9/24 23:51:33

CCKS 2019中文电子病历数据集实战:从解压到实体识别全流程

简介:这是一份面向自然语言处理与医学信息学研究人员的中文电子病历数据集,源自CCKS 2019中文电子病历命名实体识别评测任务,包含1379例真实病历样本,每份样本提供原始文本与实体标注,实体涵盖手术、解剖部位、药物、疾…

阅读更多 →
Django Web开发实战:从环境搭建到博客系统完整教程 2026/9/24 23:51:33

Django Web开发实战:从环境搭建到博客系统完整教程

1. 为什么是Django:Python Web开发的第一选择1.1 从零开始认识Django框架先说个真实的感受。我做了这么多年的Python开发,带过不少新人,也见过很多人在Web框架的选择上纠结半天。有人觉得Flask轻量灵活,有人觉得FastAPI性能好&…

阅读更多 →
水波纹:点一下水面就漾开,压力感瞬间被揉散 2026/9/24 23:51:20

水波纹:点一下水面就漾开,压力感瞬间被揉散

水波纹最解压的点在于“动一下立刻有回应”。这个网页把整块画布当成一片可触摸的水面:点一下会漾出一圈圈涟漪,按住滑动会连成一条水痕,开启雨滴模式后还有雨点随机落下、各自打出小水花。波纹强度、扩散速度和涟漪颜色都能调,右…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞