搞了大半年本地知识库问答,折腾过各种RAG(检索增强生成)方案之后,我最想说的其实是这个标题里的两件套:RAG负责帮你在已有内容里快速找答案,Wiki负责把零散内容沉淀成能持续生长的知识体系。它们根本不是一个层面的东西,却总被混在一起用。结果就是——RAG搭了一堆,文档也写了一大堆,真正上线问答的时候,要么答非所问,要么搜出来的东西驴唇不对马嘴。
这篇文章把这件事讲透:RAG到底解决什么、Wiki在这个体系里管哪一段、为什么它们必须配合,以及怎么从零到一搭出一套能用、能长期维护的知识库问答系统。适合正在搭团队知识库、想搞本地RAG问答,或者已经搭完但效果一直很差的朋友。
1. “找答案”和“长知识”不是同一件事:为什么你的RAG配不上你的Wiki
先讲一下我自己踩过的坑。去年帮团队搭一个内部知识库问答,当时的思路很简单:把散落在各个文档里的Wiki页面、微信群里的FAQ、项目复盘导出成markdown,一股脑丢给RAG系统。Demo阶段用三个常问问题跑通,感觉稳了。结果一上真实场景,同事问了一个需要跨三篇文档综合判断的问题,模型给出来的答案完全跑偏。
排查了几天,最终问题不在模型,也不在检索参数,而在于源头的知识管理太差:同一件事在三份文档里有三种说法,其中一份已经过了时;有的页面一段里塞了四五个主题,切分之后每个块都信息混乱;还有大量重复内容和断掉的链接,根本没有做知识治理的人。这时候我才意识到,RAG和Wiki虽然都跟“知识库”有关,但它们在干完全不同的活。
1.1 用“图书馆+咨询员”来拆开理解
你可以把Wiki想象成一座图书馆,RAG是图书馆里新招的咨询员。咨询员再厉害,如果图书馆的书乱摆、标签乱贴、目录不全,他也说不出个所以然;反过来,图书馆整理得再规矩,没有咨询员的话,读者还是得自己查目录、找书架、翻半天。所以这两者不是替代关系,是搭档关系。
在技术上,RAG做的事情是“检索+生成”:从你给定的知识库片段里检索出可能的答案证据,再让大模型基于这些证据组织回答。它每次回答完就结束,不会沉淀任何记忆。而Wiki承担的是知识的“沉淀与组织”:谁写的、什么时候更新的、和哪些页面有关联、哪里已经过时了。这些信息需要人去维护,靠模型搞不定。
1.2 RAG只读,Wiki负责“养数据”
很多人把RAG效果差归咎于“大模型不够聪明”,其实大多数情况下是知识侧出了问题——文档过时、重复、碎片化,甚至相互冲突。RAG是个只读系统,它只能忠实反映你喂进去的内容质量;文档烂,检索结果一定烂,生成的答案再流畅也是错的。要让系统变好,必须回到Wiki侧去改数据。
这就是为什么我觉得RAG与Wiki必须组成闭环:RAG每回答一个用户问题,都应该反馈到“这条知识存在什么问题”——是不是没搜到?是不是搜到了过时信息?是不是某一页根本没有写清楚?然后有人在Wiki里补上缺失、标记过时、合并重复。下次再有同样问题,检索结果就会自然变好。这个从“找答案”到“长知识”的循环,才是这套体系的真正价值,也是解决知识割裂问题的最朴素解法。
2. 命中率上不去:把烂文档喂给再好的检索器,RAG也救不回来
聊到RAG的硬指标,很多人第一反应就是hit rate。简单解释一下:hit rate衡量的是“检索结果里有多少次真的包含了能回答用户问题的内容”。比如用户问“支付网关超时应该找谁”,系统检索top-5文档,如果这5个文档里没有任何一个提到“支付网关超时处理流程”,那这次调用就算miss。我见过不少团队把生成模型从7B换到72B,hit rate纹丝不动,原因就在这里:检索侧没做好,生成模型再强也只能对着空气输出。
2.1 先分清是检索坏了还是生成坏了
遇到回答不对的情况,第一件事不是调prompt,而是做一次最基础的分离测试:把检索到的top-3文档原文打出来,自己看一眼。如果文档里确实有答案,但模型没答对,那是生成环节的问题,去换prompt、换模型;如果文档里根本没有答案,那是检索环节的问题,这时候再怎么调生成层都没用。
我自己会维护一个小规模的自测集,20个左右真实问题,每个问题记录期望答案出处对应的文档路径。每次改了切分策略或embedding模型,就跑一遍这个集合,统计命中率。这个流程简单得有点土,但相当有效。用同样的问题集跑不同参数,你就能直观看到改动带来的收益。
2.2 检索瓶颈到底卡在哪
排查下来,高频的检索层瓶颈主要有这么几类:
- 切分策略不对。固定按512字符切,很容易把一句话拦腰斩断,或者把一张表格拆得稀碎。模型拿到的上下文本身是残缺的,答案自然残缺。
- 嵌入模型和内容语言不匹配。中文内容配一个主要基于英文语料训练的向量模型,语义对齐能力很弱。
- 缺少元数据过滤。文档没有日期、类型、状态这些标记,搜索时无法排除过期内容,也无法按标签缩小范围。
- 只用了纯向量检索。没有配合BM25这类基于关键词的方案,同义词、缩写、精确匹配很容易漏掉。
但其实这四条里,至少一半的根因在知识侧。举个我实测过的例子:同样30篇产品FAQ,用固定512字符切分,命中率只有51%;改成按Markdown标题层级加一问一答结构切分之后,命中率直接到86%。过程中没换任何模型和框架,只改了文档切分方式。因为FAQ原来是按“问句+答句”组织好的,一个块就是一问一答,检索时天然容易命中。这也侧面说明:Wiki侧把结构写清楚,比RAG侧调一万个参数都重要。
2.3 元数据是Wiki给RAG的“索引卡”
图书馆里每本书都有索书号,Wiki里的页面也应该有等价物。Obsidian知识库里的frontmatter(标题、标签、创建时间、状态)、MediaWiki的分类系统、通用文档里的“Doc Type”字段,这些都是元数据。它们对RAG的意义在于:让检索可以先在“索引卡”里筛掉明显不相关的内容,再进行向量相似度比较。
比如团队Wiki里同时有2023版和2024版的技术规范,如果文档都带“version: 2024”和“status: active”这类字段,检索时就可以直接排除旧版本。没有这套元数据,新旧文档语义相似度极高,模型几乎必然会混着引用。这个问题的解法不在RAG框架里,而在Wiki侧有没有坚持维护元数据的习惯。
3. 零基础能直接抄的本地方案:Ollama + 简易RAG接入Wiki导出的文档
如果你不想把团队文档或笔记传给云端API,本地部署是更稳妥的选择。下面这套方案我实测下来很稳,对一个几千页文档规模的知识库足够用了,而且全部跑在本地机器上,数据不出域。环境假设是你有一台能跑Ollama的电脑,最好有16G以上内存,显卡不是必需(有更好)。
3.1 我的“最小可用”选型表
| 模块 | 选型 | 为什么选它 |
|---|---|---|
| 本地大模型 | Ollama + qwen2.5:7b | 一条命令拉起,API兼容OpenAI格式,中文效果好,7B在本地推理速度可接受 |
| 向量化模型 | Ollama + bge-m3 | 对中文语义理解明显优于通用英文模型,支持多语言,本地可跑 |
| 向量数据库 | Chroma | 嵌入式,不需要单独起服务,demo和中小规模项目最省心 |
| 切分组件 | LangChain 的 MarkdownHeaderTextSplitter | 按Wiki本身的标题层级切分,而不是傻傻按字符硬切 |
| 编排框架 | LangChain/LangChain4j | 文档全、社区多,问题好搜;Java团队可留意LangChain4j的Easy Rag封装,快速串起加载、切分、检索链路 |
这套组合的思路就是“能跑就行,但每一环都可靠”。有些朋友一上来就上重型分布式向量库或复杂Agent框架,链路没跑通前,引入的变量太多,出了问题很难定位。先把链路从数据到答案完整走一遍,再逐步替换薄弱环节,是我比较建议的推进方式。
3.2 四步“搬书”流程:导出、清洗、切分、索引
第一步是导出。Obsidian、MediaWiki、语雀、飞书Wiki都能导出markdown或HTML。导出的内容往往带着导航目录、HTML标签、链接残留,需要做一次清洗。用Pandoc把HTML转成干净的markdown,再写一个几十行的脚本去除多余标签和无关导航段,保留正文结构即可。
第二步是切分。这是整个链路里最容易出效果也最容易被轻视的一步。我自己用的是LangChain的MarkdownHeaderTextSplitter,按H1/H2/H3标题层级切分,同时保留标题作为段落上下文。代码大致长这样:
from langchain_text_splitters import MarkdownHeaderTextSplitter splitter = MarkdownHeaderTextSplitter( headers_to_split_on=[ ("#", "H1"), ("##", "H2"), ("###", "H3"), ] ) chunks = splitter.split_text(markdown_document)这样每个chunk自带所在章节的标题信息,检索时模型能知道这段内容的领域归属。文档没有清晰标题层级的话,Wiki侧需要先补标题,这一步不能偷懒。
第三步是向量化并写入本地库。用Ollama跑embedding模型,把切好的chunk逐个向量化,存进Chroma的持久化目录。
from langchain_chroma import Chroma from langchain_community.embeddings import OllamaEmbeddings embeddings = OllamaEmbeddings(model="bge-m3") vectorstore = Chroma(persist_directory="./wiki_rag_db", embedding_function=embeddings) for chunk in chunks: vectorstore.add_texts( texts=[chunk.page_content], metadatas=[{"source": chunk.metadata.get("H1", ""), "title": chunk.metadata.get("H2", "")}] )第四步是检索问答。我习惯的做法是:向量检索top_k设为5,再把BM25关键词检索的结果混进来,去重后拼装成prompt。然后在prompt里明确写“只能基于检索内容回答,如果检索内容没有相关信息,直接说明不知道”,并要求带上引用来源。这一步能在相当程度上抑制模型编造,尤其是本地7B模型,不强制约束的话很容易发散。
3.3 跑通之后,改哪个参数最划算
很多朋友建完库就开始刷模型版本,我的建议是优先级反着来,从成本低、收益高的部分往上升:
- 第一优先:切分方式。固定字符切改成标题层级切或按语义单元切,收益最大且几乎零成本。
- 第二优先:元数据过滤。给文档加上“更新日期、文档类型、状态”等字段,并在检索时做前置过滤,能解决大量过期内容问题。
- 第三优先:混合检索和top_k调节。向量检索加BM25评分融合,扩大候选池后再重排,通常能把漏召回率降低一截。
- 第四优先:embedding模型。中文知识库场景选对模型比选大模型更重要,bge-m3这类多语言模型是稳妥起点。
- 最后才是换更大的生成模型。模型再大,也救不了切碎的信息和缺失的召回。
4. 从向量检索往上升级:Ontology RAG、GraphRAG与Agentic RAG的取舍
基础RAG版本跑通之后,还是会撞墙。撞墙的场景通常很一致:多跳问题、跨文档聚合问题、同义实体关联问题。比如“项目A依赖的组件在项目B的文档里叫什么”“过去一年所有线上故障的平均处理时长是多少”“支付网关在这篇文档叫payment gateway,在另一篇文档又简称PGW”。这些问题单靠向量检索一次性检索某个片段根本答不了,因为这些答案本来就不存在于任何一个独立的文本chunk里,而是散落在多个文档、多条关系中。
这种情况下就该考虑往RAG的进阶形态走了。
4.1 多跳问题为什么卡死纯向量检索
纯向量检索的本质是“相似文本匹配”。它能回答“支付网关超时怎么办”,因为它能抓住“支付网关超时”这几个词的语义;但它回答不了“支付网关超时惹怒的客户,他们主要来自哪个产品线”,因为这个问题的答案链条涉及支付网关故障文档、客户反馈记录、产品线归属说明三处不同来源。embedding模型算的是局部语义相似度,它对“文档之间的关系结构”没有感知,也没有能力做跨文档路径推理。
这正是Wiki侧“双链”价值所在。Obsidian这类工具里,页面之间用双链互相引用,天然形成一张关系网络。检索时沿着双链往外扩一圈,把相邻页面也拉进候选池,往往就能把分散在多个页面里的信息拼起来。这个做法不需要上复杂框架,但效果非常直接,本质上是“让Wiki的图结构参与检索”。
4.2 GraphRAG真正值钱的地方与代价
GraphRAG是另一条被讨论很多的路线。它的思路是:用大模型从文档中抽取实体、关系、属性,构建出知识图谱,再生成社区摘要和层次化的索引结构。回答问题时,先在图谱层面做全局导航,再回到原始文本段落获取证据。对“这个文档集主要在讲什么”“某个主题波及到哪些模块”这类全局性问题,GraphRAG的效果是纯向量检索比不了的。
但它有明显的代价。构建阶段需要消耗大量token,文档更新后图谱需要增量维护,索引层的复杂度也比向量库高一大截。我的建议是:几百篇文档以内的个人或小团队知识库,先把Wiki结构和双链做好,GraphRAG的价值体现不出来;文档量上了万、且大量问题需要跨文档全局回答时,再认真考虑它。
4.3 Agentic RAG适合作为“编排层”而非“检索层”
Agentic RAG这段时间讨论很多,它的核心变化是:从“检索一次然后生成”变成“让智能体自主决定检索动作”——要不要改写query、要不要多路检索、要不要迭代追问、要不要综合多个来源交叉验证。
听起来很强,但我在实际项目里的观察是:解药在架构分层。Agent层作为“调度中心”很合适,它可以把一个复杂问题拆成几个子查询分别检索,再汇总答案;但底层的“检索网络”必须本身是健康的。如果检索层命中率本来就低,Agent再怎么调度,也只是在快速且系统地拿回错误信息。用快递做个类比,正常检索是单件配送,Agent是调度中心,可以做拆单、中转、合并,但底层的快递网络得先通畅。所以Agentic RAG适合在基础检索能力扎实之后,再叠加为入口编排层,而不是一上来就用Agent掩盖检索层的缺陷。
关于Ontology RAG也一样。它要求你先把Wiki侧的知识组织成显式本体——定义实体类型、属性和关系,例如“项目依赖模块”“缺陷复现于版本”“模块由团队A维护”。有了这层显式结构,检索可以沿关系路径去找候选文档,而不是全靠向量相似度蒙。对知识割裂严重的团队,这几乎是可落地的解药,但前期维护本体确实费人费事,建议结合现有Wiki分类体系和双链关系逐步演化,别想着一步到位。
5. 文本拆解与知识维护:最不起眼却决定长期体验的“脏活”
如果团队里有人问“有没有本地的RAG文本拆解工具”,说明已经意识到问题出在数据侧了。文本拆解(chunking)确实是RAG项目里最脏最累、回报又最大的一块。拆得好,检索命中率肉眼可见上涨;拆得糙,后面所有环节都在帮烂数据“锦上添花”。
先列几个我常用的本地工具:
- Pandoc:格式转换神器,HTML转markdown、docx转markdown都很稳。
- Unstructured:支持PDF、docx、pptx多种格式解析,能输出带标题层级的结构化文本,社区版可以本地跑。
- 简单正则脚本:针对markdown人工清理导航、链接、重复空行,成本低且可控。
- 大模型辅助拆解:让LLM先识别文档结构再按语义单元切分,效果最好但耗时耗token,适合对重点文档做精细化处理。
5.1 不同文档类型要用不同的“拆法”
这里没有银弹,但有一个基本经验:拆法必须跟着文档形态走。
| 文档类型 | 推荐拆法 | 原因 |
|---|---|---|
| FAQ类 | 一问一答一个块,问题作为标题 | 检索时命中问题文本,答案完整返回,命中率极高 |
| 操作手册 | 按章节标题切,步骤与配图说明放同一块 | 每个步骤是一个完整操作单元,拆碎会导致上下文丢失 |
| 会议纪要 | 按讨论主题拆,结论与行动项单独标注 | 主题之间逻辑独立,便于按议题查找 |
| 技术规范 | 按节标题切,版本号与生效日期写入元数据 | 方便按版本过滤,避免新旧混用 |
| 复盘文档 | 按“背景—原因—措施—结果”段落切 | 语义单元清晰,模型更容易取到完整结论 |
我自己实测过的案例里,FAQ按一问一答切分后命中率从51%提到86%;操作手册如果不按步骤切,而是整章丢进一个块,又会出现文本超长被截断、细节丢失的问题。所以“按文档结构切”永远优先于“按固定字数切”,Wiki标题层级清晰与否,在这里直接决定了RAG的上下限。
5.2 一个“足够用”的回归测试脚本
知识库是活的,文档每隔一段时间就会更新。为了让RAG效果不悄悄滑坡,我强烈建议维护一份小型测试集:把20到50个真实问题存成JSON,每个问题记录预期命中的文档。每周跑一次脚本,统计命中率变化。脚本逻辑很简单:
def hit_rate(eval_set, retriever): hits = 0 for item in eval_set: docs = retriever.invoke(item["question"]) sources = {doc.metadata["source"] for doc in docs} if item["expect_source"] in sources: hits += 1 return hits / len(eval_set)用这套测试集做回归,每次调整切分策略、换embedding模型、改检索参数,都能立刻看到数字反馈,而不是凭着“感觉好像变好了”来决策。这个测试集合本身就是团队Wiki的“质检清单”,也是让RAG长期可用最被低估的一环。
文本拆解相关的心得再补一句:Wiki页面顶部那一段“一句话概述”非常宝贵。如果每次写新页面时,都顺手写一段一两句话的摘要放在页面最上方,并把它单独抽出来作为元数据交给RAG,检索效果会再涨一截。道理很简单——模型面对一整页信息时,先看到一句话摘要,比翻遍全文找重点要可靠得多。这比换任何高级检索算法都便宜,也是我强烈建议写进团队知识库规范里的一条约定。
最后的一点体会
做完这个项目之后,我最大的认知转变是:RAG不是把模型变大就能解决问题的,它解决的只是“快速找到答案”,而答案长在哪、长得是否健康、有没有人持续维护,是Wiki该管的事。现在不管去哪个团队,我第一件事永远是翻他们的知识库,看结构乱不乱、有没有人维护、页面标题是否清晰。结构乱的信息资产,喂给再先进的检索增强方案也白搭。
如果你正想动手搭这么一套,我的建议是:先别急着下载最新模型,先把Wiki里的页面标题理一理,给每个页面写上一段摘要,标好更新日期和文档状态。数据养好了,RAG自然找得到答案。反过来说,RAG每次答错,也是Wiki下一次迭代最好的线索。把这层循环转起来,知识库才会从“文档堆”真正变成“会生长的知识体系”。