
1. 为什么还要回头聊“经典 RAG”我经常被问到一个问题现在 GraphRAG、LightRAG、RAG 智能体这些概念都满天飞了为什么还要像考古一样去拆经典 RAG我的答案是因为经典 RAG 是所有“花活”的参照系。你只要跟人聊 RAG、RAG 教程、RAG 实战绕不开的还是那条最朴素的三段式链路——索引、检索、生成。搞懂这一条链路你就握住了 RAG 世界的坐标原点。几乎所有主流的 RAG 框架从 LangChain、LlamaIndex 到 Spring AI、LangChain4J 这种 Java 侧的框架核心 API 仍然是同一个套路加载文档 → 切分成块 → 向量化 → 存入向量库 → 检索 Top-K → 组装 Prompt → 交给大模型生成。新出的变体无非是把这个流程里“某一步”改得更复杂或者在外面套了一层控制循环。如果你完全没理解经典 RAG 的每一步在干什么你就看不懂这些变体到底改了什么、解决什么问题、又牺牲了什么。所以这篇内容面向三类人一是想零基础搭建本地 RAG 知识库但不知道从哪下手的同学二是已经在用某些 RAG 框架但总觉得“回答质量不稳”、想搞明白哪里出问题的朋友三是想评估 RAG 瓶颈到底在哪里、自己该不该换 GraphRAG 或 ontology RAG 的决策者。我尽量少说发布会上的漂亮话多讲能直接落地的东西包括参数怎么算、踩过的坑、还有那些框架文档里不会告诉你的细节。2. 经典 RAG 的完整流水线从文档到答案既然要把它当参照系我们就先把这条三段式管线拆到最细看看每一站到底发生了什么。2.1 索引链路分块、向量化、入库经典 RAG 的上游是知识库的准备也就是“把一份文档变成向量库里可以被搜索的单元”。这一部分可以拆成四件事文档解析、文本拆解、向量化、写入存储。文档解析这步很容易被低估。很多人拿 PDF 直接喂给切分器结果切出来的 chunk 全是乱码或乱序后面怎么做都是白费力气。一个很务实的建议是解析阶段要区分文本型 PDF 和扫描件。文本型 PDF 用本地工具直接抽取扫描件要过 OCR。工具层面本地 RAG 文本拆解工具我实测下来比较稳的是 unstructured、markitdown以及专门处理扫描件的 PaddleOCR。这里有个经验以前我图省事用一个在线接口做 PDF 解析虽然方便但公司内部文档的安全性始终是个隐患后来全部换成本地工具后反而更顺手。遇到扫描版图书或带复杂表格的文档先用 OCR 识别成带坐标的文本再做版面还原再进入切分流程。别嫌麻烦这一步决定了知识库的上限。然后是文本拆解也就是 chunking。为什么非要把文档切碎因为嵌入模型embedding model有固定的最大上下文长度比如很多常见模型是 512 到 2048 个 token。你不切一篇 5000 字的文档根本塞不进向量化接口就算硬塞向量里语义会被平均掉检索时谁也查不到。分块的核心矛盾是块太大语义颗粒度粗检索精度下降块太小单个块信息量不足生成阶段拿到也没用。后面我会给一套比较能落地的参数计算方式这里先记住这个矛盾。向量化就是把“文本”变成“高维坐标”。每个 chunk 经过 embedding 模型后变成一个几十到几千维的浮点数组。这个数组有两个特点一是同义文本距离近二是计算相似度快。这里必须强调一个容易误导新人的点向量相似度高不等于“语义完全一样”它只是在模型的语义空间里“长得像”。不同 embedding 模型的空间不一样就像不同国家的地图投影方式不同同一个地点在不同模型里坐标数值完全不同。所以换了 embedding 模型就必须全部重新向量化不能混着用。最后写入向量库。所谓向量库核心就是一个“支持向量索引 相似度检索”的存储系统。这里不仅是存向量还得把原始文本、文档来源、页码、章节标题这些 metadata 一起存。为什么 metadata 这么重要因为万一查出来一个 chunk你不知道它来自哪份文件RAG 就无法给你“带出处的答案”也就没法做引用溯源。常见本地存储选择有 Chroma、FAISS、LanceDB、Qdrant 等。对零基础的人来说我用得最多的是 Chroma因为 API 简单默认就是本地持久化不用额外起服务端。后面实操部分会展开。2.2 检索链路召回和重排检索这步在经典 RAG 里最大的误区是很多人把它当成“数据库查询”。查询是一条 SQL 给一个准确结果检索却是“给你若干个最像的候选”。候选质量的好坏直接决定下文生成的质量。圈内有一句话流传很广检索决定了 RAG 回答的上限生成只是把结果往这个上限去逼。我见过很多项目Prompt 写得天花乱坠结果召回的内容压根不对大模型也只能一本正经地胡说八道。经典的检索主流是两种向量相似度检索和关键词检索。向量检索擅长“语义相关”比如用户问“公司年假怎么申请”文档里写的是“休假制度”两者字面完全不同但语义相近向量方式能命中。但它不擅长“精确匹配”比如查一段代码、一个编号或一串专有名词向量检索经常翻车。关键词检索恰好相反BM25 这类算法对精确词命中很拿手。所以很多较成熟的 RAG 项目都会做“混合检索”向量召回 关键词召回然后合并去重再用 RRFReciprocal Rank Fusion合并排名。经典 RAG 严格来说可以是只用向量检索的初始形态但你要把它用到生产里我劝你在第一步就考虑混合检索这几乎免费提升效果。如果你用的是 Ollama 本地 RAG还可以先只跑向量检索验证流程之后再接 BM25不必一开始就堆复杂度。检索出来通常不止一条。知识库里有 500 个块每块 300 字查一次可能召回 20 条但大模型上下文只有 4K token放不下这么多原文。所以中间经常还要加一步重排rerank。重排模型的工作是拿用户问题去和召回的候选逐条比对给出更精准的相关性打分然后只把得分最高的前 3-5 块塞进 Prompt。重排模型比 embedding 模型更“重”但候选集小、只排序不建库所以速度可控。这一环我建议有条件的都加上没有条件的用“先粗召回 8 条再按向量得分截断到 3 条”的暴力截断法也能临时顶一顶只是效果粗糙些。2.3 生成链路Prompt 组装与答案合成检索完成之后就要把“用户问题 检索到的知识块 系统指令”拼成 Prompt。这步看似简单其实有个经常被忽略的纪律必须把“知识块”和“模型自身知识”以可区分的方式放进 Prompt。也就是说Prompt 里要明确告诉模型“以下是知识库材料你需要基于这些材料回答如果材料里没有答案就直接说你不知道不要编”。这一句话能给减少幻觉帮大忙。组装 Prompt 时还要处理三件事。第一是“引用溯源”让模型在回答末尾以编号形式标出依据如 [1][2]对应传入的知识块来源这对企业场景尤其必要因为领导看到回答的第一反应通常不是“答得对不对”而是“这个结论是哪里来的”。第二是“上下文外监督”有时召回的块里存在互相矛盾的信息比如不同年度的政策文件。你在 Prompt 里可以加一条“如果知识库材料中存在矛盾指出矛盾并分别标注出处”而不是让模型强行选边站。第三是“控制生成边界”尤其当问题超出知识库范围时明确让它拒绝回答。这一步做过跟没做过体验差异很大。还有一个细节容易被忽略回答生成时把“问题改写”和“检索”串起来的思路。经典 RAG 里的一个潜在弱点是单轮检索直接拿用户原始问题去查。用户口语化说“那个去年报过税的表怎么弄”文档里写的是“2023年度个人所得税申报表”向量检索未必能命中。进阶做法是在检索之前先让大模型把用户问题改写成更适合检索的形式比如“2023年度个人所得税申报表填写流程”。这个思路本质上是后来 RAG 智能体、查询改写这类新玩法的基础。但你完全可以在经典管道里先做这一步收益非常直接。3. 经典 RAG 的边界瓶颈与适用场景没有哪个方案是全能的。经典 RAG 之所以被当成参照系恰恰因为它的“缺陷”被研究得最透彻新的方案几乎都是在补它的洞。3.1 RAG 瓶颈单段上下文与语义遮蔽经典 RAG 最大的瓶颈之一单次检索只能拿到若干个小块。假设你问“我们公司所有项目里哪些用到了 Redis”如果 Redis 这个信息散落在 50 个项目的文档里而每个 chunk 都只属于某个单独项目检索接口只返回 3 条那么模型看到的信息就是残缺的。这不是模型能力问题而是经典 RAG 的设计限制它以“局部块”为检索单元缺少全局视野。另一个瓶题是“语义遮蔽”又被戏称为“检索迷惑”。当知识库里的话题密度很高时比如 1000 个 chunk 都在讲数据库优化用户问一个宽泛的数据库问题召回的前 3 条可能全是“长得像但答非所问”的内容。向量检索只看语义相似度不考虑文档结构、概念层级。这也是为什么最近 ontology RAG、GraphRAG 这些变体开始受捧——它们做的事情本质上是引入知识体系和结构关系而不是单纯靠向量距离找局部片段。但话说回来对绝大多数中小规模知识库语义遮蔽问题完全可以用“metadata 过滤 重排 高质量分块”去压制不必动不动上图谱。3.2 知识库能存图片吗多模态问题怎么归类有不少朋友问RAG 知识库能存图片吗直接回答经典 RAG 的文本链路存不了“纯图片”但你可以变通。什么叫“纯图片”就是一张 JPG、PNG 里画了个架构图、拍了个产品照片没有附着文字。经典 embedding 模型是文本模型只接受文字输入图片进来没法直接转成可检索的文本向量。可行的变通方案有三种。第一种是“OCR 文本化”把图片里的文字抽出来作为文本 chunk 的附件或独立 chunk 入库这种最常用成本最低。第二种是“图片描述化”用视觉大模型把图片内容写成一段文字描述再把描述入库检索时查到的还是文本但回答时可以把原图路径作为附件带出来。第三种是“多模态向量化”用像 CLIP 这类真正支持图像与文本对齐的向量模型把图片和文本编码到同一个向量空间这是真正意义上的多模态 RAG。三者适用场景不同中小团队先用前两种即可第三种工程复杂性会高一些后续我也专门写一篇细说。3.3 ontology、GraphRAG 这些变体为什么不算“经典”先一句话说清 ontology RAG它是在知识接收阶段先按照领域本体来组织实体、属性、关系再基于这些结构化约束来做检索。GraphRAG 则是把你的文档提炼成实体关系图回答问题时结合全局图谱和多跳遍历。从结构上看它们都跳出了经典 RAG 的“文档块”单元是对“索引”这一环的根本改造。所以当你看到类似“ontonology RAG 比经典 RAG 强”的说法时不要懵其实它们已经不在同一个维度上了。但注意这些变体不是银弹。GraphRAG 的优势在于跨文档、跨块的多跳推理比如“A 项目的负责人有没有在 B 项目里出现过”经典 RAG 因为只取局部块很难把两个项目的片段连起来。但 GraphRAG 的劣势也很明显建图要额外调用大模型抽取实体与关系处理大量文档时成本高、耗时长而且对数据质量要求很高文档写得一塌糊涂抽出来的图也是废的。对一个 200 页的本地 wiki 知识库用经典 RAG 就够了对一个 2 万份文档的知识中台GraphRAG 才可能值得投入。这里还要提一个高频误区wiki 和 RAG 的关系。很多人以为 Wiki 天然适合做 RAG 知识库其实要看你的问题类型。Wiki 的每个条目都是独立、结构清晰的段落确实对经典 RAG 友好但 Wiki 页面里大量的信息框、分类、条目链接这些结构性信息经典 RAG 也会丢失因为切分之后它们和正文混在一起变成了普通文本。所以如果你要做一个 Wiki 问答机器人与其盲目用经典 RAG 硬啃不如先把 Wiki 的结构化字段标题层级、信息框、链接单独抽出来作为 metadata再给每条 chunk 挂上这些标签。4. 零基础可复制的本地 RAG 实操讲完理论直接上能复现的东西。这一节我把一条“Ollama 本地文本拆解工具 Chroma”的简易本地 RAG 知识库完整走一遍。给小白看也能直接照抄给有基础的人看里面的参数取舍逻辑值得参考。4.1 环境准备Ollama、向量模型与文本拆解工具先说为什么用 Ollama。它是一个本地大模型运行工具能把 Llama、Qwen 等开源模型跑在你自己的机器上不需要把数据发给第三方接口。对知识库这类场景数据隐私是很多人第一诉求。实测下来Ollama 的安装非常简单Windows 下装一个安装包macOS 和 Linux 用一条命令即可。装好后拉一个会话模型比如通义千问系列或 Llama 3.1再拉一个嵌入模型比如 nomic-embed-text 或 bge-m3。这些模型体积从几百 MB 到几 GB 不等建议先从小模型开始跑通流程。文本拆解工具用本地方案。我推荐用 markitdown它能把 PDF、Word、PowerPoint、Excel 统一转成 Markdown 文本对命令行使用者非常友好。遇到扫描版 PDF再装 PaddleOCR。实测组合是“markitdown 转文本 → 按 Markdown 标题结构切分 → 向量化入库”这条链路对大多数 PDF 文档问题不大。如果你的文档库以网页为主也可以把 markitdown 换成 trafilatura。核心原则只有一个一定要拿到干净、保留结构的文本不要拿到一堆空白字符和乱码。不同工具的侧重点用一张表说明工具适用场景主要特点注意点markitdown通用文档PDF/Word/PPT/Excel统一转 Markdown附带结构扫描件需配合 OCRunstructured复杂排版文档支持版面貌似还原、多格式解析安装依赖较多稍重PaddleOCR扫描件、图片文字抽取中文识别效果好支持表格需要 Python 环境trafilatura网页正文抽取快速去广告和导航保留正文不适合离线 PDF4.2 分块参数怎么算一个可以直接套的计算方法切分参数并非拍脑袋。我有一套比较稳妥的打法先量 embedding 模型的最大输入长度再结合字体大小和文档结构往下推三步。第一步确认上下文上限。比如 nomic-embed-text 最大输入是 2048 token但推荐不要用满留 20% 冗余给特殊字符和代码所以我把它约等于 1600 token。第二步看语言类型。中文一个汉字大约相当于 1 到 1.5 个 token英文一个单词大约 1.3 个 token。对一份中文文档1600 token 大约是 1000-1300 字。不要直接拿“字符数”等于“token 数”那样会超出模型窗口导致向量化时被截断丢失尾部信息。第三步结合语义段落切。推荐按“Markdown 标题 自然段落”来做而不是硬按字符数切。我常用的一组参数是chunk_size 设为 512 字符chunk_overlap 设为 64 字符。为什么是 overlap因为语义会跨块上一段末尾可能是下一段开头的主语如果不重叠检索时经常出现“只有一半信息”的尴尬。overlap 设为 chunk_size 的 10%-20%通常够用。你也可以先跑 256/32观察检索效果再微调。记住这些参数不是固定的它们依赖你的文档风格、embedding 模型、甚至问题类型。做一版之后拿真实问题跑一遍你会对这套参数心里有数。4.3 搭建一条最简本地 RAG 链路我直接给一段可跑的 Python 示例用的是 Chroma 和 Ollama 的 OpenAI 兼容接口。这里的代码逻辑足够简单零基础也能看懂有基础的人可以复制改造。import chromadb from chromadb.utils import embedding_functions # 1. 准备 chroma 客户端指定本地持久化目录 client chromadb.PersistentClient(path./my_rag_db) collection client.get_or_create_collection( namedoc_qa, embedding_functionembedding_functions.OllamaEmbeddingFunction( urlhttp://localhost:11434/api/embeddings, model_namenomic-embed-text, ), ) # 2. 把之前切好的 chunk 逐条入库 chunks [ {id: doc1_chunk1, text: 公司年假政策入职满一年后每年享有5天年假……}, {id: doc1_chunk2, text: 年假申请需提前三个工作日提交审批……}, ] # metadata 一定带来源和标题方便引用 collection.add( ids[c[id] for c in chunks], documents[c[text] for c in chunks], metadatas[ {source: 员工手册.pdf, page: 12, title: 年假制度} for c in chunks ], ) # 3. 查询 query 入职一年有多少天年假 results collection.query(query_texts[query], n_results3) # 4. 把检索结果和问题组装进 Prompt 再调用大模型 from openai import OpenAI ollama_client OpenAI(base_urlhttp://localhost:11434/v1, api_keyollama) prompt f请基于下面提供的知识库片段回答问题。 如果片段中没有答案请明确说“知识库中未找到相关信息”。 问题{query} 片段 {chr(10).join(results[documents][0])} resp ollama_client.chat.completions.create( modelqwen2.5:7b, messages[{role: user, content: prompt}], ) print(resp.choices[0].message.content)这个流程真正的骨架只有三句话建 collection、存 chunk、查 chunk。其余的东西全是围绕它做增强。先把这一版跑通再去加混合检索、重排、引用溯源会更从容。我当初第一次跑通时内心其实挺震撼的因为一个能回答自己文档问题的系统从零写出来也就几十行代码当然真正让它回答得“好”后面全是调优的功夫。5. 调优实战从“答非所问”到“差不多能回答”网上很多 RAG 教程到“跑通”就结束了但实际用到业务里你会发现“能跑”和“能用”之间隔着一整片海。这一节我挑几个高频问题附上排查思路和绕过坑的经验。5.1 高频问题与排查方向现象可能原因排查方向问什么都答“不知道”检索结果为空或相似度阈值过严看检索日志实际召回了多少条切分是否太碎答非所问东拼西凑分块太大或语义遮蔽缩小 chunk_size增加 overlap尝试重排回答内容正确但完全没用到知识库Prompt 没有强制模型基于片段在系统指令里加“只基于材料回答”同一个问题换一种问法结果完全不同用户问题太口语化加查询改写环节把口语改成检索友好的表达答案没有出处metadata 没传或 Prompt 未要求引用在 Prompt 中强制输出编号引用知识库更新后仍答旧内容旧 chunk 未删除按 id 或来源删除旧数据并重新入库排bug有个笨办法但很有效把检索到的 chunks 直接打印出来人工看一遍。如果连你自己都觉得这几段没法回答问题那问题在检索或切分不在大模型。别一上来就怀疑 Prompt 写得不够炫先看输入数据。5.2 调优绕坑技巧经验之谈第一不要无脑增大 n_results 或 top_k。我见过同学为了“更全面”把 top_k 从 3 改到 20结果 Prompt 塞满无关文本大模型反而被带偏。合理的做法是先粗召回 8-10 条重排后截取 3-5 条进入 Prompt。候选数和最终喂给模型的数量要分开管理。第二注意“知识的时效性”。如果你的知识库包含不同年份的制度、政策或新闻RAG 系统不会自然给出“按时间看最新”的结果反而可能同时返回旧版和新版生成时无所适从。解决办法切片时把日期放入 metadata检索前按时间过滤或者 Prompt 里强制“优先使用最新来源并在回答中注明时间”。第三警惕“问一个宽泛问题”时向量检索被高频词带偏。比如你问“数据库性能优化”文档里任何提到“性能”的 chunk 都可能是候选它们并不都是回答这个问题的好素材。这种情况靠纯向量检索很难解决掺杂词权重调整或关键词过滤能改善一些。这也是为什么我一直强调 metadata 和结构化切分的重要性。第四关于“本地 RAG 文本拆解工具”我之前踩过一个具体的坑用默认参数切一个 PDF里面表格特别多结果表格被切成两半检索出来的内容只有表头没有数据。后来我把表格识别单独提出来表格整体作为一个 chunk正文再按段落切分效果立刻好了很多。这条经验送给所有文档里表格多的朋友。第五关于 RAG 智能体的关系说一句。经典 RAG 是单轮“检索-生成”RAG 智能体是在外面加一个循环控制它可以自己判断当前检索结果是否足够不够就改写问题再查一次或者调用工具查另一个知识库。这个思路能解决一部分问题但也带来新的问题循环次数越多延迟和成本越高且多了“跑偏”的风险。先老老实实把经典 RAG 调好再考虑上智能体控制循环这是更稳健的路线。6. 结尾经典 RAG 作为参照系最后说点我个人的体会。我见过很多项目一上来就追求“高级”GraphRAG、ontology 层次结构、多 Agent 编排搞了半个月发现维护成本极高反而连最基础的“文档能不能准确检索出来”都没做好。究其原因是缺少一个清晰的参照系。你可以把经典 RAG 当成一把尺子把任何新方案放回“索引 → 检索 → 生成”三段式坐标里看它改动了哪一段带来了什么收益又增加了什么成本。改索引的可能建图慢改检索的可能多跳延迟高改生成的可能 Prompt 难以稳定。这些问题只有你在经典 RAG 上亲手调试过才会有感觉。这个内容后续还可以这样扩展尝试把本地知识库从 PDF 扩展到 wiki 页面、邮件归档、聊天记录尝试把单轮检索扩展成“问题改写 多轮检索”尝试给答案加引用溯源。每前进一步都回来跟经典 RAG 对比一下效果与成本你会发现自己对 RAG 的理解越来越扎实。RAG 领域还会不断出新名词但经典 RAG 这条基线不会过时它就是那个值得反复回望的参照系。