ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RAG 检索增强生成实战:从文档切分到混合检索的本地知识库搭建指南

RAG 检索增强生成实战:从文档切分到混合检索的本地知识库搭建指南 1. RAG 到底是什么为什么现在人人都在聊RAG全称 Retrieval-Augmented Generation中文叫检索增强生成。拆开看就三件事检索、增强、生成。检索是从你自己的资料库里找到跟问题相关的内容增强是把这些内容塞进大模型的上下文里生成是让模型基于这些内容给出回答。说白了就是给大模型配了一个“开卷考试”的小抄让它不用全靠脑子里记的东西硬答。我最早接触 RAG 是因为一个很实际的问题公司内部有几千份产品文档、会议纪要、客服记录想让大模型帮忙回答员工的问题但直接问模型它要么胡编要么说“我不知道”。微调成本太高每次文档更新还得重新训练。RAG 就不一样了文档更新只需要重新索引模型本身不用动。这个特性让 RAG 在过去两年里成了企业落地大模型最主流的方案之一。RAG 适合谁如果你是开发者想给自己的应用加一个“懂业务”的问答能力如果你是产品经理在规划知识库类产品甚至你只是个普通用户想用本地电脑搭一个能查自己笔记的助手RAG 都是目前门槛最低、效果最可控的路径。它不需要你懂模型训练不需要 GPU 集群一台普通笔记本就能跑起来。但 RAG 也不是银弹。我见过太多人兴冲冲搭了一个 demo结果回答质量惨不忍睹。问题往往不在模型而在检索环节。检索没找对内容后面生成再强也是白搭。所以这篇文章我会把重点放在“怎么让检索真正管用”上而不是只给你一个能跑通的代码就完事。2. RAG 的核心链路拆解与方案选型2.1 一条完整的 RAG 链路包含哪些环节很多人以为 RAG 就是“向量数据库 大模型”其实中间有七八个环节每个环节都会影响最终效果。我把它拆成下面这条链路文档加载把 PDF、Word、Markdown、网页、数据库记录等各种格式的原始资料读进来。文本切分把长文档切成小块因为模型上下文有限而且检索粒度太粗会引入噪音。向量化用嵌入模型把每个文本块转成向量存进向量数据库。索引构建除了向量索引通常还会建关键词索引、元数据索引方便混合检索。查询理解对用户的问题做改写、扩展、意图识别提升检索命中率。检索召回从索引里找出最相关的若干文本块。重排序用更精细的模型对召回结果重新打分把最相关的排到前面。生成回答把问题和筛选后的上下文一起送给大模型让它组织答案。这条链路里切分策略和检索策略是决定成败的两个关键点。切分不好检索再强也找不到完整信息检索不好生成模型只能靠猜。2.2 为什么我推荐从本地轻量方案起步网上有很多 RAG 教程一上来就让你买云服务、调 API、配向量数据库集群。我的建议是先用本地方案跑通全流程再考虑上云。原因有三个。第一本地方案让你能看清每个环节的数据流转。用 Ollama 跑本地模型用 Chroma 或 FAISS 做向量库整个链路在你自己的机器上出问题容易排查。第二成本几乎为零。你不需要为每次调试付 API 费用可以反复试错。第三数据安全可控。很多企业的文档涉及内部信息本地跑不用担心数据外流。当然本地方案有性能上限。如果你要处理百万级文档、支持高并发查询最终还是得走服务化架构。但那是第二步的事。第一步永远是用最小成本验证你的切分和检索策略是否有效。2.3 嵌入模型和生成模型怎么选嵌入模型负责把文本转成向量它的质量直接决定检索准不准。生成模型负责根据上下文写答案它的质量决定回答读起来顺不顺。我的经验是嵌入模型比生成模型更值得花时间挑选。原因很简单生成模型现在普遍不差7B 参数的中文模型已经能写出通顺的回答但嵌入模型如果选错了检索出来的内容跟问题八竿子打不着生成模型再强也救不回来。选嵌入模型时重点看三个指标中文支持、向量维度、推理速度。中文支持不用多说很多英文嵌入模型在中文上表现很差。向量维度影响存储和检索速度768 维和 1024 维是常见选择。推理速度决定你建索引要等多久本地跑的话建议选参数量小一点的。生成模型方面本地跑推荐 7B 到 14B 参数区间的指令微调模型。太小了回答质量差太大了消费级显卡跑不动。如果你有 24G 显存的卡14B 量化版本是比较舒服的选择。3. 文档切分最容易被忽视但最影响效果的一步3.1 为什么固定长度切分往往不好用大部分入门教程会告诉你按 500 字一段切重叠 50 字。这个策略简单但实际效果经常很差。问题在于固定长度切分会把完整的语义单元切碎。比如一个操作步骤写到一半被切断了检索到前半段模型看到的是不完整的信息回答自然缺胳膊少腿。我踩过最典型的一个坑一份产品故障排查文档每个故障现象和解决方案是一一对应的。按固定长度切分后某个故障现象的描述和它的解决方案被分到了两个块里。用户问“XX 故障怎么处理”检索只召回了现象描述那块模型看到现象但看不到解决方案只能编一个。后来改成按标题层级切分每个故障作为一个独立块问题立刻解决了。所以切分的第一原则是优先按文档的自然结构切而不是按字数切。Markdown 按标题切HTML 按标签切代码按函数切对话按轮次切。只有在文档没有明显结构时才退而求其次用固定长度。3.2 切分粒度怎么定才合理切分粒度太粗一个块里包含多个主题检索时容易引入无关信息切分粒度太细一个完整意思被拆散模型拼不起来。我的经验值是每个块 200 到 500 字或者 3 到 8 句话。这个范围能容纳一个相对完整的语义单元又不至于太泛。但这不是死规定。技术文档可以细一点因为概念密集叙事类内容可以粗一点因为需要上下文连贯。关键是做实验拿十几个典型问题看检索出来的块是否包含回答所需的全部信息。如果经常缺信息就调大粒度如果经常混入无关内容就调小粒度。还有一个技巧是保留父子关系。切分时记录每个块属于哪个父文档、哪个章节。检索时先召回小块然后根据父文档 ID 把相邻的块也拉进来这样既保证了检索精度又保证了上下文完整。这个策略在 LangChain 和 LlamaIndex 里都有现成实现。3.3 元数据是提升检索质量的隐藏武器很多人切分完只存文本和向量把元数据丢了。这是个巨大的浪费。元数据包括文档标题、章节路径、创建时间、作者、文档类型、标签等等。这些信息在检索时能发挥大作用。举个例子用户问“最新的报销政策是什么”。如果你的块里存了“生效日期”这个元数据检索时就可以优先召回日期最新的块而不是靠语义相似度碰运气。再比如用户问“技术方案里怎么说的”你可以用“文档类型技术方案”做过滤把行政通知类的块直接排除。我的做法是切分时尽可能多地保留元数据检索时先用元数据做粗筛再用向量做精排。这个组合策略比纯向量检索的准确率高出一大截。4. 检索策略从“能找到”到“找得准”4.1 纯向量检索的局限性在哪里向量检索擅长语义匹配。用户问“怎么退款”文档里写“如何申请退货返还货款”向量检索能匹配上因为它理解语义。这是它的优势。但向量检索有三个明显短板。第一对精确匹配不敏感。用户问“错误码 E1024 怎么解决”向量检索可能召回一堆讲错误处理的块但就是漏掉那个专门讲 E1024 的块因为数字和代码在向量空间里区分度不高。第二对否定和条件不敏感。用户问“哪些情况不能退款”向量检索可能召回一堆讲退款流程的块因为语义上很接近。第三对长尾问题召回率低。训练数据里少见的表达方式向量化后可能偏离主流语义空间。所以纯向量检索只适合做初筛不能作为唯一手段。4.2 混合检索向量加关键词才是正解我的标准配置是向量检索 BM25 关键词检索两路召回后合并去重。向量负责语义匹配BM25 负责精确匹配。两路各取前 20 个结果合并后用重排序模型统一打分。BM25 是一个经典的关键词检索算法它考虑词频和逆文档频率对精确术语、代码、数字特别有效。上面那个 E1024 的例子BM25 能精准命中包含“E1024”的块弥补向量检索的不足。合并策略有两种一种是简单加权向量得分乘 0.7BM25 得分乘 0.3相加排序另一种是 Reciprocal Rank Fusion按排名倒数融合不依赖分数绝对值。我一般用 RRF因为它对不同检索器的分数尺度不敏感更省心。4.3 重排序模型把最相关的推到最前面召回阶段追求的是“不漏”所以会取比较多结果。但送给生成模型的上下文有限通常只能放 3 到 5 个块。这就需要重排序模型来精挑细选。重排序模型和嵌入模型不同。嵌入模型是双塔结构问题和文档分别编码速度快但精度有限。重排序模型是交叉编码问题和文档拼在一起过模型精度高但速度慢。所以典型流程是嵌入模型召回 50 个重排序模型精选 5 个。本地跑重排序模型推荐用轻量级的参数量在 1B 以下推理速度可以接受。如果不想额外部署模型也可以用大模型本身来做重排序让它给每个块的相关性打分。这个方式慢一些但省了一个模型。4.4 查询改写让用户的问题更容易被检索到用户的问题往往很短、很口语化直接拿去检索效果不好。查询改写就是把用户问题扩展成更适合检索的形式。常见手法有几种。同义词扩展把“退款”扩展成“退款 退货 返还货款”。假设文档生成让大模型先根据问题编一个假想的答案然后用这个答案去检索因为答案的表述风格更接近文档。子问题拆解把“A 和 B 有什么区别”拆成“A 是什么”和“B 是什么”分别检索。我实测下来假设文档生成对提升召回率效果最明显尤其适合文档表述和用户提问风格差异大的场景。但它会增加一次大模型调用延迟会上升。如果对延迟敏感可以只用同义词扩展成本低见效快。5. 从零搭建一个本地 RAG 知识库的完整实操5.1 环境准备与工具选型先列一下我这次实操用的工具栈都是本地可跑的组件选型理由生成模型Ollama 跑 7B 中文指令模型本地推理无需 API嵌入模型中文优化的轻量嵌入模型中文语义匹配好速度快向量库Chroma零配置适合本地开发关键词检索rank_bm25 库纯 Python无需额外服务重排序轻量交叉编码模型本地可跑精度提升明显编排框架LangChain生态成熟组件丰富安装依赖的命令大致如下pip install langchain langchain-community chromadb rank_bm25 pip install sentence-transformers pypdf markdownOllama 需要单独安装装好后拉取模型ollama pull qwen2.5:7b提示模型名称根据你实际可用的中文模型调整重点是选指令微调版本基座模型不擅长遵循指令。5.2 文档加载与切分的具体实现假设你有一批 Markdown 格式的产品文档放在docs/目录下。加载和切分的代码如下from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import MarkdownHeaderTextSplitter # 加载文档 loader DirectoryLoader(docs/, glob**/*.md, loader_clsTextLoader) documents loader.load() # 按标题层级切分 headers_to_split_on [ (#, h1), (##, h2), (###, h3), ] splitter MarkdownHeaderTextSplitter(headers_to_split_onheaders_to_split_on) chunks [] for doc in documents: splits splitter.split_text(doc.page_content) for s in splits: # 把标题路径写进元数据 s.metadata[source] doc.metadata[source] chunks.append(s)这段代码的关键点是用 MarkdownHeaderTextSplitter 而不是 RecursiveCharacterTextSplitter。前者按标题切每个块自带标题路径元数据后者按字数切会破坏语义完整性。切完后检查一下块的大小分布。如果有些块超过 800 字可以再对超长块做二次切分但尽量在段落边界切。5.3 向量化与索引构建接下来把块向量化并存入 Chromafrom langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma embedding HuggingFaceEmbeddings( model_nameyour-chinese-embedding-model, model_kwargs{device: cpu}, ) vectorstore Chroma.from_documents( documentschunks, embeddingembedding, persist_directory./chroma_db, ) vectorstore.persist()同时构建 BM25 索引from rank_bm25 import BM25Okapi import jieba # 中文需要分词 tokenized [list(jieba.cut(chunk.page_content)) for chunk in chunks] bm25 BM25Okapi(tokenized)注意中文 BM25 必须分词直接用空格切分效果很差。jieba 是最省事的选择如果对分词精度要求高可以换其他分词器。5.4 混合检索与重排序的代码实现检索阶段把向量和 BM25 的结果合并def hybrid_search(query, top_k5): # 向量检索 vector_results vectorstore.similarity_search_with_score(query, k20) # BM25 检索 tokenized_query list(jieba.cut(query)) bm25_scores bm25.get_scores(tokenized_query) bm25_top sorted(range(len(bm25_scores)), keylambda i: bm25_scores[i], reverseTrue)[:20] # RRF 融合 rrf_scores {} for rank, (doc, _) in enumerate(vector_results): key doc.page_content rrf_scores[key] rrf_scores.get(key, 0) 1 / (60 rank) for rank, idx in enumerate(bm25_top): key chunks[idx].page_content rrf_scores[key] rrf_scores.get(key, 0) 1 / (60 rank) # 排序取前 top_k sorted_items sorted(rrf_scores.items(), keylambda x: x[1], reverseTrue) return [item[0] for item in sorted_items[:top_k]]这里的 60 是 RRF 的标准平滑参数不用改。融合后取前 5 个块送给生成模型。5.5 生成回答的提示词设计提示词直接决定回答质量。我的模板是这样的PROMPT 你是一个知识库助手。请严格根据以下参考资料回答问题。 参考资料 {context} 用户问题{question} 要求 1. 只使用参考资料中的信息不要编造。 2. 如果参考资料中没有答案直接说“资料中没有相关信息”。 3. 回答要简洁分点说明时用数字编号。 4. 引用具体内容时注明来自哪个文档。 回答这个模板有三个关键约束只用参考资料、没有就说没有、注明来源。第一条防止模型胡编第二条防止它硬答第三条方便你追溯和验证。把检索到的块拼成 context调用 Ollama 生成import requests def generate(question, context): prompt PROMPT.format(contextcontext, questionquestion) resp requests.post(http://localhost:11434/api/generate, json{ model: qwen2.5:7b, prompt: prompt, stream: False, }) return resp.json()[response]整个链路跑通后拿十几个典型问题测一遍看回答是否准确、是否引用了正确来源。如果发现检索不准回到切分和检索策略调整如果检索准但回答不好调整提示词或换生成模型。6. 实战中踩过的坑与排查技巧6.1 检索召回不准的常见原因问题一问题里的关键词在文档里表述不同。用户说“怎么退钱”文档写“退款流程”。向量检索可能匹配上但 BM25 匹配不上。解决办法是加同义词扩展或者依赖向量检索兜底。问题二文档里有多个相似主题检索分不清。比如文档里同时有“个人退款”和“企业退款”用户问“退款”两边的块都被召回。解决办法是在元数据里加分类标签检索时先过滤。问题三块太大一个块里混了多个主题。检索召回了块但块里只有一小部分相关。解决办法是调小切分粒度或者用句子级检索再合并。问题四嵌入模型对领域术语不敏感。通用嵌入模型没见过你行业的专有名词向量化后区分度低。解决办法是用领域数据微调嵌入模型或者补充关键词检索。6.2 生成回答胡编乱造的应对方法模型胡编通常是因为上下文里没有答案但它又不想说“不知道”。除了在提示词里明确要求“没有就说没有”还可以做两件事。第一加相关性阈值。检索结果的重排序分数低于某个阈值时直接返回“未找到相关信息”不调用生成模型。这个阈值需要根据你的数据调一般从 0.5 开始试。第二要求模型引用原文。让模型在回答里标注每句话来自哪个块的哪部分。如果它引用的内容在上下文里找不到说明它在编。这个约束会显著降低胡编率。6.3 性能优化的几个实用技巧本地跑 RAG 最容易卡在三个地方嵌入慢、检索慢、生成慢。嵌入慢的解决办法是批量处理不要一个块一个块地调嵌入模型攒够 32 或 64 个块一起编码。另外嵌入可以离线做一次之后增量更新即可。检索慢通常是向量库没建好索引。Chroma 默认用暴力搜索数据量大了会慢。超过一万个块建议换 FAISS 或 Milvus它们支持近似最近邻搜索速度快很多。生成慢主要是模型太大或硬件不够。7B 模型在 CPU 上跑每秒可能只有几个 token。如果有 GPU用 GPU 推理会快十倍以上。没有 GPU 的话可以考虑用量化版本牺牲一点质量换速度。6.4 常见问题速查表现象可能原因排查方向回答总是“不知道”检索没召回相关块检查切分粒度、嵌入模型、检索策略回答内容张冠李戴召回了错误主题的块加元数据过滤、调小切分粒度回答不完整块被切断信息缺失调大切分粒度、加父子块关联精确术语检索不到纯向量检索的短板加 BM25 关键词检索回答胡编上下文无答案但模型硬答加提示词约束、加相关性阈值检索结果重复多路召回未去重合并时按内容去重建索引太慢嵌入模型太大或未批量换轻量模型、批量编码查询延迟高重排序模型太重换轻量重排序或减少召回数量7. 进阶方向从能用到好用7.1 Agentic RAG让模型自己决定怎么查基础 RAG 是“一次检索一次生成”。Agentic RAG 是让模型自己判断这个问题需要查吗查哪个库查几次查到的够不够不够再查什么举个例子用户问“对比 A 产品和 B 产品的退款政策”。基础 RAG 可能只召回一个产品的政策。Agentic RAG 会先把问题拆成两个子问题分别检索再合并对比。这个模式适合复杂查询但实现复杂度高延迟也大。我的建议是先把基础 RAG 做扎实再考虑上 Agent。7.2 知识图谱增强解决多跳推理问题有些问题需要跨多个文档推理才能回答。比如“张三负责的项目里哪些用了李四的技术方案”。这需要先查张三负责哪些项目再查这些项目用了谁的技术方案。纯向量检索很难处理这种多跳关系。知识图谱增强的思路是把文档里的实体和关系抽出来建成图结构。检索时先在图上做多跳查询把相关实体和关系拉出来再送给生成模型。这个方案适合实体关系密集的领域比如法律、医疗、金融。但建图成本高维护也复杂不是所有场景都值得上。7.3 服务化与多租户从个人工具到团队产品个人用的 RAG 脚本和团队用的 RAG 服务是两回事。服务化要考虑多用户并发、权限隔离、文档版本管理、检索日志、效果监控。权限隔离是重点。不同部门的人只能查自己部门的文档这需要在元数据里加权限标签检索时强制过滤。文档版本管理也很关键政策文件更新后旧版本要能追溯但不能被检索到。这些工程问题比算法问题更磨人但决定了 RAG 能不能真正在团队里用起来。我个人的体会是RAG 的效果 20% 靠模型80% 靠数据和检索策略。花时间整理文档结构、设计切分方案、调检索参数比换更大的模型收益高得多。很多团队一上来就追求最新最强的模型结果文档一团糟检索一塌糊涂再强的模型也救不回来。先把数据治理做好再谈模型选型这个顺序不能反。
返回列表