ARTICLE DETAIL

资讯详情

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

用《天龙八部》练手RAG:从切块、混合检索到Graph RAG的完整实践

用《天龙八部》练手RAG:从切块、混合检索到Graph RAG的完整实践 最近在折腾 RAGRetrieval-Augmented Generation检索增强生成有朋友甩了个问题给我能不能拿《天龙八部》当语料搭一个知识库问答系统我一开始觉得这就是玩票真上手了才发现武侠小说做 RAG 演示语料简直是绝配。人物关系够复杂、武功门派够多、事件线够密而且不涉及版权风险拿来验证各种检索策略比那些标准的说明书文档有意思多了。这篇博文就是基于这个项目总结的。我会从 RAG 的原理讲起然后完整走一遍数据清洗、文本切块、向量化、混合检索、重排序、生成问答的流程最后再聊聊 Graph RAG、Agentic RAG、权限卡控这些进阶方向。无论你是刚接触 RAG 的新手还是已经在业务里踩过一些坑的开发者这篇文章应该都能帮你在动手之前把“为什么这么设计”想清楚。我会结合具体的问题比如“段誉有哪些师父”“乔峰的武功是谁教的”手把手把整个链路拆开给你看。1. 先聊清楚RAG 到底解决什么问题很多朋友上来就问“RAG 和微调有什么区别”“RAG 是不是换个说法把文档拼进去”这些都属于对核心问题还没吃透。要理解 RAG先得理解大模型的幻觉问题。1.1 从“背课文考试”到“开卷考试”大模型本质上是一个“背了大量课文的学生”。你问它问题它调动的是训练时学到的“记忆”不是现场去查资料。问题在于训练数据有截止日期新知识它不知道。训练数据里的内容可能相互冲突它没法判断哪个对。对于训练数据里没出现过的细节它会用“最可能的表达”来编一个答案——这就是幻觉的来源。RAG 的思路特别朴素既然模型会编那我们就给它一份“参考资料”再让它回答。相当于从闭卷考试变成开卷考试模型的记忆力不再重要重要的是能不能从你给的资料里找到正确答案。这个思路听上去简单但带来的实际收益非常大。一是知识可以实时更新你不需要重新训练模型二是答案可以溯源每一步都有出处三是成本可控不需要为了一个问题去微调一个模型。1.2 RAG 的核心管线拆开看就三步RAG 的标准流程可以分为索引、检索、生成三个阶段。索引阶段把文档加载进来做清洗、切块再用 embedding 模型把每段文本转成向量存进向量数据库。检索阶段把用户问题也转成向量在向量库里找最相似的几个文本块。生成阶段把检索到的文本块和用户问题拼到一起组成 prompt交给大模型生成回答。很多教程把这三步包装得很玄乎尤其是第三步“生成”看似只是调用一次 API其实 prompt 怎么设计、检索到的内容是否干净、是否需要重排序都直接影响最终答案的质量。我在后面实战部分会一个一个讲。1.3 为什么用 RAG 而不是微调这是一个几乎每次分享都会被问到的问题。微调最大的问题是“训练成本和维护成本太高”。业务知识天天在变微调一次动辄几小时起步而且你很难控制模型到底记住了什么。RAG 则像是给模型配了个实时更新的工具箱文档改一版重新过一遍索引就能用。另一个关键点是可控性。RAG 模式下模型只能基于你检索到的内容作答答不出来可以直接说“不知道”。微调模式下模型很容易把训练数据和通用知识混在一起一旦给出个自信满满的错误答案排查起来相当痛苦。当然RAG 不是万能药。如果业务需要模型改变说话风格、学会某种特定的输出格式比如把一段话改写成文言文那确实得靠微调。我的习惯是需要领域事实用 RAG需要行为风格用微调。两者不冲突甚至可以叠加。2. 项目设计为什么拿《天龙八部》当语料方案怎么定动手写代码之前先把方案想清楚。这个项目的核心目标不是“做一个小说问答”而是通过一部大家都很熟悉的小说把 RAG 每个环节的设计取舍讲明白。选《天龙八部》有几点很实在的考虑。2.1 语料选型武侠小说怎么就成了好用例第一版权干净。金庸先生去世后版权有归属但网上流传的简体版文本来源复杂个人学习自己用问题不大别拿去商用就行。第二内容密度高。《天龙八部》里面人物上百门派十几家武功几十种关系网盘根错节非常适合测试“多跳推理”场景——比如“段誉的师父是不是也是虚竹的师父”这种问题可能需要跨好几个章节才能拼出答案。第三中文质量高。作为中文文本句子结构完整、叙事感染力强切块之后比那些扫描版 PDF 的 OCR 乱码好处理得多。要是你用英文文档测试 RAG很多中文生态里的坑比如分词、编码、全角半角都碰不到做出来看似顺利一换到真实业务文档就翻车。用《天龙八部》这种自然中文文本折腾一遍你基本能把中文 RAG 的常见问题都踩一遍这才是它的价值所在。2.2 技术选型LangChain bge-m3 Chroma 的组合技术栈我选得比较保守都是社区里验证过的方案语言框架Python生态最全跑实验最快。编排框架LangChain。虽然很多老哥吐槽它抽象层次多、升级不兼容但作为演示项目它封装的切块器、向量存储、检索链还是能省不少事。Embedding 模型BAAI/bge-m3。中文效果稳支持 8192 个 token 输入还自带稀疏检索能力一个模型兼顾稠密和稀疏很适合当默认选择。向量数据库Chroma。本地跑、零配置、API 简单适合学习和验证。生产环境我会换 Milvus 或者 Qdrant但这不是本项目的重点。大模型这里我建议按自己的资源选。可以用 OpenAI 的接口也可以用 Ollama 跑一个本地量化模型。为了验证 RAG 效果本身用 Qwen2.5 或者 GLM 系列都可以关键是把输出格式控制好。重排序模型BAAI/bge-reranker-base。这个后面细说。这个组合的优点是每个组件都简单、可替换。你觉得哪一步效果不好单独换掉就完事。2.3 功能边界与设计取舍做项目最怕伪需求自己给自己挖坑。这个知识库问答系统我明确了几条边界只做封闭域问答。只回答能从《天龙八部》原文里检索到答案的问题其他问题一律拒绝或回答“不知道”。不追求全文情节总结。比如“用 500 字概括天龙八部全书的剧情”这种问题需要把全书上下文都放进去超出单次检索的能力范围不属于这个项目的目标。答案必须溯源。每个回答后面带引用来源哪怕只是章节名都尽量让用户能回去翻原文验证。这个边界想清楚之后后面所有评估标准就都围绕“能不能从原文里找到答案”来定了。3. 数据准备与切块成败往往在进向量库之前RAG 项目里有个很反直觉的规律向量模型和大模型用的哪家不是最重要的数据切得好不好才是决定效果上限的关键。我见过太多人一上来就 pip install 两个库然后 text.split(“\n”) 直接塞进向量库最后检索效果一塌糊涂。3.1 拿到《天龙八部》文本后先做清洗和结构化我在本地拿到的是 TXT 版本的《天龙八部》大概一百多万字。这种来源的文本一般有以下问题开头结尾有广告、网站签名、空白行、乱码。全角空格、半角空格混用还有各种不可见字符。章节标题不统一有的写“第一章”有的写“第 1 章”有的干脆没有回目。段落间可能有重复文本比如翻页残留。我处理的顺序是先去头去尾用正则把常见广告签名行删掉然后统一换行符把全角空格替换为半角删除控制字符接着按“第X章”或“第X回”这种模式切出每个章节的标题和正文内容最后按章节存成一个结构化数据结构方便后面处理。这里有个小技巧保留章节元数据特别重要。每个章节名比如“第三十七章燕云十八骑奔腾如虎风烟举”本身就是很好的文档标签可以在后面做过滤和引用溯源。3.2 切块策略为什么不能一把梭很多教程会告诉你“chunk_size 设 500overlap 设 100”这是最省事的做法但你需要理解背后的逻辑。切块太大了一个块里塞了太多不相关内容向量化之后语义容易被稀释检索精度下降切块太小了块与块之间上下文断裂找回的块可能只有半句话大模型根本没法用。以《天龙八部》这种叙事文本为例我试了几个组合切块方式chunk_sizeoverlap效果按行切纯字符数30050召回乱经常断在对话中间按段切保留段落500100相对稳定但跨段问题仍需多块拼接按段切段内再分40080结合段首信息效果最佳所谓“按段切”就是先把文本按“\n\n”拆成自然段再把相邻的多个自然段组合成一个 chunk而不是不看格式死板地按字符硬切。这样至少能保证每个 chunk 内部的语句是完整的。对于对话密集的小说我会额外注意 chunk 的边界不要落在引号中间否则检索出来的内容经常是半截对话谁说的都看不出来。3.3 父子分块解决“检索到的一句话不够用”切块之后我发现了一个典型问题用户问“乔峰的降龙十八掌是谁教的”向量检索往往能找回一句关键原文但这句话的上下文前因后果不在同一个块里。直接拿这句话去生成答案大模型只能发挥想象力补全细节。这个问题的解法是“父子分块”。父块是较大的文本单元比如整个章节或几个自然段的组合子块是从父块里切出来的较小文本单元。检索时用子块去匹配用户问题找到相关子块后不是把这个子块直接塞给大模型而是把它对应的父块找出来一起送给大模型。这样既保证了检索的精度又让大模型有了完整的上下文背景。我用 LangChain 的 ParentDocumentRetriever 实现了这个逻辑。子块大小设 300 字左右父块直接按章节取。有次我测“游坦之怎么练成易筋经的”子块模式只找回一句“摩伽陀国传来的一本经书”父块模式则能找回游坦之在破庙里拿到经书并误打误撞练成的完整段落后续生成效果完全不一样。3.4 元数据设计给每个块贴好标签向量数据库里存的不是裸文本而是“文本 元数据 向量”。元数据就像是快递单上的地址标签也是后面做权限控制、过滤检索的基础。在这个项目里我为每个 chunk 设计了以下元数据source来源文件名比如“天龙八部.txt”。chunk_id块 ID方便回溯。chapter所属章节名。chapter_index章节序号方便排序。characters这一块里出现的主要人物。这个字段可以之后做实体识别自动提取也可以人工标注关键块。有了这些标签我可以在检索时直接过滤。比如用户说“只查虚竹相关的内容”就可以在查询向量库之前先按 characters 包含“虚竹”做一次过滤既减小了检索范围又提高了精度。这也是权限卡控的一个雏形。4. 索引与检索把 embedding、向量库和多路召回跑通准备工作做完才开始进入真正“看起来像 AI”的环节。这个环节最容易犯的错误是只依赖向量相似度以为 embedding 一算就万事大吉。4.1 Embedding 模型选型中文场景多试再定我把文档切块后先在几个 embedding 模型上做了个快速对比。当时候选有 OpenAI text-embedding-3-small、text2vec、bge-large-zh-v1.5 和 bge-m3。结论是在完全本地、中文文本、没有调优的前提下bge-m3 各方面表现最均衡。这里解释一句为什么不能用 OpenAI 的 embedding 直接跑中文小说。不是说效果不行而是 openai 的 embedding 输出维度是 1536 维对资源消耗更大而且对于中文古文、武侠术语这种偏门语料openai embedding 的语义空间未必比国产模型更擅长。另一个现实问题是 API 调用成本一百多万字的语料全部过一遍 API费用虽然不高但重复实验几次就不划算了。bge-m3 还有一个特性是支持“稠密 稀疏 多向量”三种检索方式同一个模型可以同时产出稠密向量和稀疏向量后面做混合检索就很方便。4.2 向量数据库演示用 Chroma生产再换重武器Chroma 在这个项目里够用了pip 装上就能跑数据存在本地目录。它的 API 也简单一个 collection 就能搞定增删改查。需要考虑的是真实项目里数据量一大还是得换专业向量库。我把 Milvus 和 Qdrant 都试用过说实话对于十万级以下的向量Chroma 的性能也完全能扛住。换库的主要原因是生产环境需要的过滤能力、分布式能力、权限隔离能力不一样这是后话。4.3 检索从“单路向量召回”到“混合检索”这个项目最让我意外的是单独用向量检索效果真的不够。比如用户问“乔峰会哪些武功”向量检索能搜到“降龙十八掌”“打狗棒法”但也会漏掉一些字面上不相似、语义上相关的句子比如“他顺手使出了擒龙功”这种。这就是单路召回的局限向量相似度对语义的理解是统计性的不是逻辑性的。关键词完全一致但语义不重要的句子可能排在前面关键词不一致但语义重要的句子可能排在后面。组合方案是“混合检索”一路走向量召回一路走 BM25 关键词召回然后把两路结果用 RRFReciprocal Rank Fusion融合起来。RRF 的思想很简单如果某个文档在向量路里排第 2在关键词路里排第 5那么它的融合分数就是 1/(602) 1/(605)。两个列表都排得靠前的文档融合分必然高。我在 LangChain 里用 EnsembleRetriever 把 bge-m3 的向量检索和 BM25Retriever 组合起来权重各占一半。实测下来之前漏掉的“擒龙功”就从关键词路找回来了整体召回率提升明显。4.4 重排序检索和生成之间的“质检员”混合检索完会拿到一批候选块比如 top 20。但如果把这 20 块全塞给大模型答案质量反而下降因为无关信息太多会干扰生成。正确做法是加一个 rerank 环节先用便宜的方式召回足够多的候选再用更精确的模型把最相关的几块排到最前面。我用的 bge-reranker-base 会同时接收“问题 文档块”作为输入输出一个相关度分数比纯向量相似度判断更准确。它比较慢所以只对前 20 个候选重排取 top 5 进生成环节。这个“先粗召回、再精排序”的思路在搜索领域很成熟做 RAG 也是一样的道理。直接向量检索 top 5 就生成是很多 RAG 项目效果差的常见原因。4.5 检索效果验证肉眼看过才知道行不行代码跑通之后我强烈建议你手动做一次“检索效果抽查”。具体方法是自己先想好 20 个问题一个个拿去检索打印出 top 5 候选块肉眼看一看这些块到底有没有包含答案的关键信息。比如我拿“虚竹的师父是谁”去检索如果 top 5 里全是玄慈方丈的段落而没出现天山童姥、无崖子这些真正教过虚竹武功的人那说明检索链路有问题。这个过程不需要任何自动化评估脚本但能快速暴露问题。做几轮下来你对“chunk_size 设 400 还是 500”“要不要父子分块”这些参数心里就完全有数了。5. 生成与问答让大模型用检索到的内容开口说话检索做好了相当于把书翻到了正确答案那一页。但大模型会不会照着答案说会不会自己发挥这就要看生成环节的设计了。5.1 提示词模板设计把“不许编”写进去生成环节最关键的是 prompt 设计。我用的模板大致是你是一个严格基于给定资料回答问题的助手。 请只根据下面的资料片段来回答用户问题。 如果资料中没有足够的信息请明确回答“资料中没有提到”。 不要使用自己的常识或外部知识来补充。 回答时尽量引用资料中的原文。 资料片段 {context} 用户问题 {question}很多人会觉得“我的模型很强不至于乱编”但实测下来如果不加“资料中没有提到就直说”的约束模型非常喜欢靠自己的知识补全答案。尤其是在《天龙八部》这种模型训练数据里肯定包含的小说上模型几乎什么都“知道”一旦开始依赖自己的“记忆”你的 RAG 后端就白搭了。5.2 完整问答流程从问题到答案在实际代码里完整流程是这样一个函数def ask(question: str): # 1. 向量召回 dense_docs vector_retriever.invoke(question) # 2. 关键词召回 sparese_docs bm25_retriever.invoke(question) # 3. RRF融合 fused fuse_results(dense_docs, sparese_docs) # 4. Rerank取top5 top_docs reranker.rerank(question, fused)[:5] # 5. 组装prompt并调用LLM context \n\n.join([d.page_content for d in top_docs]) answer llm.invoke(build_prompt(context, question)) return answer这个流程每一步都有明确的目的不是把代码堆在一起就算了。注意第 4 步的 rerank 结果我一般还会把相似度分数低于某个阈值的块直接过滤掉宁可让模型说“不知道”也不要硬找一段无关内容来回答。5.3 实测效果几个问答案例我拿整理后的链路跑了一些问题结果如下问题检索到的主要内容最终回答是否可用段誉的师父是谁段誉学六脉神剑、凌波微步、北冥神功的相关段落可用能列出多位师父乔峰的降龙十八掌是跟谁学的汪剑通、玄苦、丐帮传承相关段落可用能答出汪剑通阿紫的眼睛是怎么瞎的游坦之献眼睛给阿紫的相关段落可用且能带出游坦之天山童姥返老还童是怎么回事八荒六合唯我独尊功的设定段落可用能说清返老还童周期虚竹最后当了皇帝吗检索到的内容偏向“西夏驸马”部分可用需结合原文判断这个表格想说明一个问题检索质量直接决定了回答质量。凡是能准确召回关键段落的回答质量都不错凡是召回阶段就丢三落四的生成阶段再努力也白搭。这也是我为什么反复强调要把精力放在数据、切块、检索这些前置环节上。6. 进阶玩法Graph RAG、Agentic RAG 与权限卡控如果只做单轮“问题 - 检索 - 回答”上面这些内容基本够用了。但真实业务场景里用户的问题不会那么老实知识库里的文档也没那么干净。这里再聊几个进阶方向。6.1 Graph RAG用知识图谱补足多跳推理传统的向量 RAG 擅长回答“某段文本里直接能找到答案”的问题但不擅长跨多个文档片段进行推理。比如“段誉的师父、虚竹的师父、乔峰的师父之间有什么共同点”这种问题要联合多个实体关系才能答出来向量检索很难一次性召回所有相关文本。Graph RAG 的思路是在索引阶段就把文本里的实体和关系抽取出来构建成知识图谱。我用了 LightRAG 的思路做了个简化版把《天龙八部》里的人物、门派、武功、师承关系抽出来存成图结构。检索时先通过图遍历找到相关实体再回溯到文本片段把图信息和文本信息一起送给大模型。实测这种模式在处理“谁是谁的师叔”“哪个门派和哪个门派有仇”这类关系型问题上比纯向量检索强很多。代价是抽取实体和关系的步骤增加了不少处理时间而且对抽取模型的准确度要求高抽错了会影响下游推理。6.2 Agentic RAG让智能体自己决定怎么查普通的 RAG 是“一问一查一答”。但用户问题经常没那么简单比如“帮我对比一下鸠摩智的火焰刀和段誉的六脉神剑哪家强”这个问题需要先检索火焰刀的信息再检索六脉神剑的信息最后综合比较。一条检索路径搞不定。Agentic RAG 解法是把“检索”变成一个 Agent 手里的工具。模型先理解问题如果觉得信息不够就调用一次检索工具不够就再调用一次甚至可以在多次检索后自我总结再决定下一步动作。这种模式好处是能处理复杂任务坏处是调试难度和成本直线上升模型一旦调用工具上瘾可能会来回查很多次并带来 token 费用增加。如果打算在生产环境搞 Agentic RAG建议给检索工具加一个“搜索预算”上限最多允许调用 3 次并在 prompt 里明确“能一次查清就不要分多次”。6.3 查询优化与权限卡控真实落地的两道难题真实知识库项目里“用户问得不好”是常态。用户不会像测试用例一样问得那么规范所以需要在检索前加一个“查询优化”模块先让大模型把用户口语化的问题改写成适合检索的关键词序列。比如用户问“那个在少林寺偷学武功的和尚后来又怎么样了”可以先改写为“鸠摩智 少林寺 偷学武功 后续”。权限卡控则是另一个容易在“看起来做完了”之后才发现的坑。企业知识库往往有部门隔离A 部门的人不应该检索到 B 部门的文档。解决方案是在元数据里给每个 chunk 打上可见范围标签检索时强制加上过滤条件。向量数据库层面可以通过 filter 实现但更规范的做法是在应用层做统一的访问控制向量库只是一个存储层。这一点如果项目一开始没想清楚后面加权限会非常痛苦。7. 常见问题与排查把坑填平再谈效果做 RAG 项目大概率会遇到下面这些问题。我整理了一份自己的排查清单每次效果不好就按这个顺序逐项查。7.1 检索为空或召回结果明显不对如果检索结果为空先别怀疑向量库坏了。检查顺序是输入问题本身是否包含生僻词/拼写错误。切块后的文本是否真的写入了向量库很多情况是写入前清洗出了问题。embedding 模型是否和查询时用的一致版本不同也会导致向量空间对不上。相似度阈值是否设得太高导致所有结果都被过滤掉了。如果检索结果有内容但明显不对那就更常见了一是切块太碎导致语义不完整二是单路向量召回漏召回需要上混合检索三是没有做 rerank导致有用的块排到了后面。针对“有但不对”我的习惯是手动打印 top 10 的原文块用肉眼判断问题出在召回还是排序。这一步能排除掉一半的玄学问题。7.2 大模型还在幻觉怎么压都压不住提示词里写了“不许编”模型还是编这种情况多半是检索回来的上下文本身就不干净。比如你想问“段誉有几个妹妹”检索回来的块大部分在讲段正淳的感情史上下文里有大量干扰信息大模型就会根据这些信息“推断”出一个答案然后表现得非常自信。这时候重要的不是继续加压提示词而是去优化检索。把 top 5 改成 top 3减少干扰或者对候选块做相关性过滤低于设定分数的一律不要再或者用 rerank 把真正相关的块顶到最前面。记住一个原则大模型生成的质量上限由检索回来的上下文决定。上下文是垃圾输出就是垃圾。7.3 效率与成本从原型到生产要多想几步本地实验跑通之后如果要把这套东西接到真实业务里效率和成本就得算账了。有几个优化点很实用embedding 批量处理不要一条一条调用 API按批跑能省一半时间。结果缓存常见问题固定答案可以缓存不用每次重新检索和生成。模型蒸馏如果业务场景相对固定可以把大模型换成一个更小的微调模型效果接近但速度快很多。向量库做主从部署或者加一层缓存避免高并发时数据库被打满。针对“缓存”多说一句我见过好几个团队把 RAG 系统做上线之后发现 60% 的问题是重复的与其让大模型一遍一遍重算不如在 RAG 链路最前面加一层语义缓存把问过的问题直接映射到之前的答案。这个优化成本极低但对体验的提升非常明显。7.4 关于 RAG面试里翻来覆去就这些题因为 RAG 这两年太火面试题也跟着卷起来了。我整理了几个高频问题基本覆盖了核心知识点面试题参考思路RAG 和微调有什么区别分别适合什么场景事实更新/溯源选 RAG行为风格/输出格式选微调可叠加如何选择 chunk size无标准答案需要按文档结构、检索任务、大模型上下文长度综合测试混合检索怎么实现向量召回 BM25 关键词召回用 RRF 融合得分重排序是干什么的对粗召回结果做更精细的相关性排序压缩输入给大模型的上下文如何评估 RAG 系统检索侧看召回率/命中率生成侧看答案正确性和忠实度无参考答案可用 LLM 打分Graph RAG 适合什么场景实体关系密集、多跳推理多、需要全局结构化信息的问题权限卡控怎么设计文档/块级打标签应用层做访问控制向量检索层加强制过滤如何减少幻觉优化检索质量、提示词明确“不知道就直说”、强制引用来源、设置拒绝回答策略这张表里每一条背后都能展开聊很久。我建议看这篇博文的同学做完项目之后再回来看这张表你会有完全不同的理解。最后再分享一个我做这个项目过程中最深的体会RAG 不是“模型不够强拿检索来凑”的妥协方案而是一套完整的信息系统设计。它的难点不在于某个模型有多强而在于你怎么把文档处理好、把检索链路搭对、把用户需求理解透。拿《天龙八部》当语料折腾这一圈我最大的收获不是会调 LangChain 了而是学会了不迷信模型、不迷信某个参数而是老老实实看数据、看中间结果、看最终输出。如果你也想动手练手我建议先别急着上生产框架就用一部小说、一个小向量库、一个本地模型把整条链路跑通之后再去碰那些更复杂的框架和概念。这条路走起来慢但走得稳。
返回列表