最近在公司做内部知识库的时候,一个憋了很久的问题终于被我堵上了:Wiki里攒了快两百篇文档,但大家遇到问题第一反应还是在群里喊,没人去翻文档。我不怪同事,因为连我自己都记不住目录结构。文档写得再用心,找不到等于白写。后来我把RAG(检索增强生成)接进来,让Wiki从一个“只读仓库”变成能直接对话的问答系统,局面才算真正闭环。这篇文章就围绕“RAG找答案,Wiki长知识”这个思路,讲讲我从零搭起本地RAG知识库的过程,以及评估命中率、调优检索效果、整理Wiki内容时踩过的坑。如果你也正在纠结要不要上RAG,或者上了RAG又觉得回答不靠谱,这篇应该能给你一些可直接复用的经验。
1. 先想清楚:Wiki和RAG到底谁管哪一段
1.1 “知道”和“记得”是两件事
我见过很多团队把知识管理做成“囤积游戏”:Wiki页面越来越多,文档越来越长,但真到用的时候,根本搜不出来。原因很简单——人的记忆是模糊的,目录结构只能解决“你想起去哪里查”的问题,解决不了“你想不起来自己需要什么上下文”的问题。
RAG解决的就是“记得”这件事。它先把你写好的Wiki文档切分成小块,用向量模型转成数字表示,存进向量库。用户提问的时候,同样把问题转成向量,去库里找最相似的几块文本,最后把这些文本塞给大模型,让大模型基于这些原文来组织答案。这样一来,Wiki负责“长知识”——沉淀结构化的、可信的内容;RAG负责“找答案”——把内容变成随时可调用的即时答案。
这两个角色不冲突,也不重复。Wiki的重点是组织和管理知识,让人类能维护、能审阅、能有版本概念;RAG的重点是缩短从“知道有这回事”到“拿到可用答案”的距离。如果你只是想要一个问答机器人,没有Wiki这件事也能做,但你会发现答案质量的波动很大,因为缺少了一个人类可读、可校验的知识基座。反过来,如果你只有Wiki而没有RAG,知识就会烂在库里,固定资产变成沉没成本。
1.2 RAG不是搜索引擎的平替
有人会问:Wiki自带的搜索功能不也能搜吗?用Elasticsearch不也能搜吗?为什么非要RAG?
区别在输出形态。搜索引擎给你一串链接,你还是得一个一个点开,自己读,自己拼装答案。RAG给你的是一段完整的、基于原文生成的答案,后面还会附上它引用了哪几个文档片段。对内部知识库来说,这个差别是决定性的。同事在工位上问“新服务器的防火墙端口怎么放通”,传统搜索返回三个页面,他得先分辨哪个是旧的、哪个是过时的;RAG直接说“按2024版标准,需要放通443、8443和一个用于监控的9100端口,依据是运维手册第3.2节”,同时给出原文引用。
这不是说RAG一定比搜索引擎更聪明,而是RAG把“检索”和“阅读理解”绑定在了一起。搜索引擎把阅读理解的工作留给人,RAG把它交给了大模型。所以在“找答案”这个场景下,RAG更接近一个同事或者一个助手,而不是一个链接列表。
我说的这个“找答案”,指的是有明确信息需求、需要引用依据、答案有对错之分的场景。它不适合取代Wiki首页、项目导航这类“漫游浏览”场景。这也是为什么标题把两件事分开:RAG负责解决“我要找到某段具体的知识”,Wiki负责解决“知识怎么长得更完整、更有序”。
1.3 什么样的Wiki才值得喂给RAG
不是所有Wiki都适合直接接RAG。我见过一个团队把十几篇“配置说明”文档塞进去,每篇都叫这个名字,内容还互相矛盾,结果RAG回答出来的东西像“缝合怪”——一会儿用旧版的参数,一会儿用新版的格式。问题不在RAG,而在Wiki本身缺乏单一事实来源。
喂给RAG的Wiki至少要满足三个条件:
- 标题唯一且语义明确。不要出现同一层目录下三篇都叫“README”的文档,检索器分不清。
- 段落之间有上下文连贯性。一句话一行、全靠截图的文档,切分之后基本没有承载能力。
- 有可区分的标识。最好有分类标签、文档编号、更新日期或负责人字段,这样能在检索后做元数据过滤,也能在引用时追溯到人。
如果Wiki当前不满足这些,不要急着搭RAG。先花两周把目录结构理清,给核心文档补上标题和摘要。这些工作在RAG上线之后一定会加倍返还给你——好的Wiki结构,直接决定了检索器的上限。我自己试过在同一个向量库上对比“整理前”和“整理后”的命中率,差距可以拉到30%以上。
2. 用Ollama和LangChain在本地搭一套可问答的Wiki知识库
2.1 硬件和软件准备:一台普通电脑够吗?
先说结论:如果文档量在几百篇以内,一台16GB内存、没有独立显卡的笔记本也能跑,只是生成答案慢一点;有8GB显存的GPU会更舒服。这套方案的思路是本地化,理由有两个:一是公司内部Wiki往往涉及敏感信息,不能随便扔给公网API;二是本地跑可以反复调参,不用烧钱。
我选用的组合是:
- Ollama:用来在本地跑大模型。它把模型封装成了类似OpenAI的接口,调起来非常方便。
- LangChain:用来做文档加载、文本切分、向量检索的编排。热词里提到的LangChain4j是Java版本,原理一样,对Java团队更友好,但本文以Python为例。
- 向量库Chroma:一个本地文件型的向量存储,零部署,适合轻量场景。
模型方面,我用的是两个模型:嵌入模型用nomic-embed-text,负责把文本转成向量;生成模型用qwen2.5:7b-instruct,负责基于检索结果生成答案。这两者分工明确,嵌入模型不需要聊天能力,但需要语义理解稳定;生成模型需要指令遵循能力强,能严格按上下文作答。
安装很简单(此命令基于Ollama官方CLI):
ollama pull qwen2.5:7b-instruct ollama pull nomic-embed-text如果没有Ollama,先去官网下载对应系统的安装包。安装完之后,在命令行执行上面两条命令,模型会被拉取到本地。选模型的具体参数,我建议按下面这个表来:
| 硬件条件 | 生成模型 | 嵌入模型 | 体验 |
|---|---|---|---|
| 16G内存无GPU | qwen2.5:7b | nomic-embed-text | 能跑,回答偏慢 |
| 32G内存或8G显存 | qwen2.5:14b / qwen2.5:7b | bge-m3 | 流畅,语义更好 |
| 24G显存 | qwen2.5:32b / llama3.1:8b | bge-m3 | 质量明显提升 |
这里的重点是别把生成模型和嵌入模型搞混。很多人首次搭RAG,只关心大模型强不强,忽略了嵌入模型。实际上,检索质量的一半以上取决于嵌入模型能不能把“问题”和“文档”映射到同一个语义空间。
2.2 把Wiki文档变成可检索的索引
Wiki一般都能导出成Markdown或HTML。我用的是Confluence导出的Markdown目录,格式是每个页面一个文件夹加一个index.md。如果你的Wiki在飞书或者Notion里,也可以导出为Markdown。准备数据这一步不写代码,但决定了后续一切。
索引过程的核心代码大概是这样的(使用LangChain 0.3及对应社区包):
from langchain_community.document_loaders import DirectoryLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载全部Markdown文档 loader = DirectoryLoader("./wiki_export/", glob="**/*.md", show_progress=True) docs = loader.load() # 2. 递归切分:按标题、段落、句子逐级切到目标长度 splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n## ", "\n### ", "\n", "。", " "] ) chunks = splitter.split_documents(docs) # 3. 用本地嵌入模型向量化并存入Chroma embeddings = OllamaEmbeddings(model="nomic-embed-text") vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./wiki_vector_db" )这套流程看起来简单,但有几个值得细说的点。
首先是chunk_size的选择。我一开始用200,结果很多技术文档被切得七零八落,一个问题涉及三个段落时,检索出来的块都“答非所问”。后来改成500,并把chunk_overlap设成50,效果明显好了。原因在于,切得太碎会丢失上下文,切得太大又会把不相关的信息混在一起。500字符在中文场景下大致对应4到6个自然段,既保留了上下文,又不会污染向量。
其次是分割符的顺序。RecursiveCharacterTextSplitter会按我给的顺序优先用\n##这种标题分隔符切分,这样可以尽量保证一个chunk不会横跨两个章节。如果你直接用默认的\n\n,很可能会把两个不同主题的段落拼在一起,检索时出来一个不伦不类的内容块。
跑完这段代码,你会得到一个wiki_vector_db目录,这就是你的Wiki知识库的向量索引。这一步是最占时间的,几百篇文档大概需要几分钟。之后每次问答,都不用重新索引,除非Wiki内容变了。
2.3 第一次问答:效果不如预期是正常的
索引建好之后,我用一个最小化的问答验证效果:
from langchain_community.chat_models import ChatOllama from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) prompt = ChatPromptTemplate.from_template(""" 你是内部知识库助手。请基于下面的上下文回答问题。如果上下文里没有答案,直接说“知识库中未找到”,不要编造。 上下文: {context} 问题: {question} """) model = ChatOllama(model="qwen2.5:7b-instruct") chain = ( {"context": retriever, "question": RunnablePassthrough()} | prompt | model | StrOutputParser() ) print(chain.invoke("新服务器的防火墙端口怎么放通?"))第一次测试,我提了一个和文档措辞高度一致的问题,回答还不错。但当我换成一个口语化的问法,比如“帮我看下最近哪个服务器密码过期了”,结果就有点飘。原因是嵌入模型对“口语表达”和“文档书面表达”之间的语义映射不够敏感,加上我当时的文档标题信息不足。这是RAG项目的常态:第一个版本只是证明“链路通了”,离“好用”还有很长的路要走。
这一版也就是一个Baseline。接下来别急着去调大模型,先去看检索结果本身对不对。把retriever.invoke("问题")打印出来,看返回的4个块里有没有一个真正相关的内容。如果没有,问题出在检索环节;如果有,那说明生成环节的提示词或模型指令遵循能力需要优化。这个排查思路,能省下你很多瞎调的时间。
3. 从“单轮查文档”到“会拐弯的智能体”:RAG演进路线
3.1 普通RAG的三个明显瓶颈
第一版RAG跑通后,我很快遇到了真正的难点。普通RAG本质上是一锤子买卖:拿你的问题去向量库搜一次,然后把结果丢给大模型。这种方式的瓶颈非常清晰:
- 多跳问题搜不到。比如“A项目的负责人同时负责哪几台服务器”,这个答案可能分散在两篇文档里,一次检索只能命中其中一篇。
- 全局问题搜不全。比如“总结一下本月所有运维变更”,需要聚合十几篇文档,向量检索返回的Top-K个块根本装不下。
- 语义鸿沟难跨越。口语问“机器挂了”,文档里写“节点不可用”,向量相似度再高,匹配也经常偏。
这不是我一个人的体验。热词里老出现“RAG瓶颈”,本质就是这三个问题。搞清瓶颈在哪,再去选对应的升级方案,比无脑换大模型有意义得多。
3.2 Agentic RAG:把检索权交给大模型
Agentic RAG的思路是:不再由用户的问题直接触发一次检索,而是让大模型像一个会查资料的人一样,自己决定“要不要查、查什么、查几次、查完要不要再查”。典型的技术包括Self-RAG、CRAG(Corrective RAG)和基于LangGraph搭的检索Agent。
我用LangGraph搭过一个简单的反射式检索流程:第一步,大模型判断自身知识能否回答,不能则触发检索;第二步,拿到Top结果后,先让一个轻量模型给结果打分,如果分数低,就重写用户的查询,换一个更贴近文档术语的问法再搜一次;第三步,把两次检索的结果合并去重,再交给生成模型。跑完之后,我测试了10个原本“搜不到”的多跳问题,有7个能给出让人满意的答案。
这个提升其实很好理解。原来的流程是一次检索,命中不了就拉倒;Agentic RAG至少给了两次机会,同时允许大模型在第一次结果里发现“缺哪部分信息”,然后有针对性地补查。代价是延迟变长,可能从2秒变成8秒。如果在线上服务里用,可以做取舍,比如先走普通RAG,分数低再fallback到Agentic。
这里我没有贴图,单说一个判断标准:如果你的问题场景中超过一半需要引用两个不同的文档区域,普通RAG基本扛不住,值得考虑Agentic。否则,先不要上,复杂度真不是白给的。
3.3 GraphRAG和本体RAG:用Wiki的结构对抗碎片
热词里有两组让人很在意的词:graphrag和ontology rag。它们解决的是比“多跳”更深一层的问题——实体之间的关系。
GraphRAG是微软开源的一种方案。它从文档中抽取实体、关系和事件,构建成知识图谱,然后对图结构做社区检测和摘要。问“哪些模块依赖了那个已经废弃的SDK”,普通RAG要翻好几个页面,GraphRAG能直接从关系图里找到路径。代价是索引成本高,对计算资源要求也高,而且文档一改,图谱更新很麻烦。
本体RAG(Ontology RAG)则是先定义一个领域本体——比如“服务器有属性、归属团队、运行服务、有依赖关系”——然后约束RAG按这个schema去抽取和检索。它的好处是精准,适合专业领域知识库;坏处是维护本体本身要花不少专业功夫。
对比一下,我自己用的策略是分阶段做:
| 方案 | 解决什么 | 成本 | 建议 |
|---|---|---|---|
| 普通RAG | 单一知识点检索 | 低 | 默认起点 |
| Agentic RAG | 多轮检索、查询重写 | 低到中 | 多跳问题多时上 |
| GraphRAG | 实体关系、全局归纳 | 高 | 文档量很大再考虑 |
| 本体RAG | 领域约束、精准检索 | 中到高 | 有明确Schema时上 |
我个人不太建议一上来就直接上GraphRAG。内部Wiki初期最值钱的是“能被准确找到”,而不是“能发现关系的网络”。先把普通RAG的检索命中率和召回率调到位,再渐进式升级,每一步都有可量化的收益,团队也更容易接受。
4. 命中率上不去?用这套方法评估和调优RAG
4.1 先量化“找得到”:Hit Rate与MRR
RAG项目最怕没有指标。没有指标,你会陷入“感觉好用一点了”“感觉又变差了”的循环。所以我做的第一件事,是建立了评估集。
所谓Hit Rate,指的是检索结果中能命中的比例。比如你准备了30个真实问题,每个问题都人工标记了它对应的正确文档片段。系统检索Top-K个结果,如果正确片段在其中,就算一次命中。计算方式:
Hit Rate = 命中次数 / 总问题数比如30个问题,系统返回Top-5,命中了21个,那么Hit Rate@5就是70%。这个数直观地告诉你,检索器有没有把“正确答案”捞出来。它不保证最终生成答案正确,但它决定了生成环节有没有机会正确。
MRR全称是Mean Reciprocal Rank,它不只关心有没有命中,还关心正确答案排在第几位。第一个答案就对,得1分;排第二,得0.5分;排第三,得0.33分,然后取平均。这个指标对RAG特别有意义,因为大模型通常只取前几块做上下文,如果正确答案排在第五位以后,它基本没机会被看见。
实际操作时,我从Wiki里选了几个最常被问到的模块,手写了20个问答对,每对标注一个“golden snippet”的ID。写代码跑一遍检索,就能算出Hit Rate和MRR。这一步看起来笨,但效果极好,它能帮你定位到具体是哪一类Query在拖后腿。
4.2 调优顺序:分块、嵌入、重排、提示词
拿到基线指标后,不要同时调好几个参数。我建议按顺序调:
首先是分块大小。如果Hit Rate低,先试增大或减小chunk_size。经验是:文档性技术说明,500字符合适;FAQ类短条目,150到250更合适。如果你发现“检索结果里有相关内容,但上下文缺了点火候”,那就是分块太小,试着切到800并增加50到100的重叠。
然后是嵌入模型。nomic-embed-text是通用型,适合零基础跑通。如果你需要更好的中文语义,可以换bge-m3或bge-large-zh。切换嵌入模型后,必须重建整个向量库,否则新旧向量混在一个空间里,检索质量会崩。重建很快,几百篇文档几分钟内搞定。
第三步是重排Rerank。当Top-K取到10或20时,检索器给出的排序并不总是最优。轻量做法是用交叉编码器,比如bge-reranker-base,把问题逐一和候选块做精细匹配,重排后再取前4块喂给大模型。这一步能显著提升最终回答质量,代价是要跑一次模型推理,多花几百毫秒。
最后才是提示词。常见的提示词坑是:没告诉模型“找不到就直说”,导致模型在原文档没有答案时强行圆场。我在系统提示里加了一句“如果上下文中没有任何相关信息,请明确回答知识库未找到,并建议用户咨询文档维护人”,幻觉率肉眼可见地下降了。
给一个调优顺序表,方便对照执行:
| 顺序 | 调什么 | 怎么验证 | 预期效果 |
|---|---|---|---|
| 1 | 分块大小和重叠 | 跑Hit Rate | 解决上下文不完整 |
| 2 | 嵌入模型 | 对比多模型Hit Rate | 解决语义映射偏 |
| 3 | 重排器 | 对比MRR | 提升排序质量 |
| 4 | 提示词 | 人工抽查答案 | 减少幻觉和瞎编 |
4.3 一张表讲清楚常见问题和修法
在跑RAG的过程中,我汇总了一批高频症状,直接给排查建议:
| 现象 | 可能根因 | 修法 |
|---|---|---|
| 答案和文档内容对不上 | 检索到的块里混入了无关信息 | 减小chunk_size,做重排 |
| 明明有文档却搜不到 | 问题用词和文档用词差异太大 | 换更强的嵌入模型,或加同义词改写 |
| 多个页面互相矛盾 | Wiki本身没有唯一事实源 | 先整理Wiki,给文档标注版本和状态 |
| 答案引用了错误章节 | 切分时多主题混在一个块里 | 用标题分隔符优先切分,或增加元数据过滤 |
| 回答总是“知识库未找到” | 检索的K太小或向量库为空 | 检查索引流程,调大top_k |
| 回答慢到没法用 | 生成模型太大或没启动GPU | 换小模型,或用重排替代更长上下文 |
当你的Hit Rate能稳定在80%以上时,说明“找答案”这条链路基本通了。接下来,真正的长期竞争力反而回到了Wiki内容本身——这也就是标题里“长知识”那半边。
5. 让Wiki真正“长知识”:内容组织、多模态与SOP
5.1 从Obsidian到团队Wiki:双链笔记如何降低RAG难度
我在自己玩的知识管理工具里,Obsidian用了很久。后来我发现Obsidian里养成的写作习惯,对RAG特别友好。原因在于双链笔记天然就是结构化的知识组织:每篇笔记有唯一标题,开头有front matter,笔记之间有链接。
把这个思路搬到团队Wiki,具体落地可以参考“Obsidian搭建知识体系SOP”的理念:每篇文档维护一套元数据,包括创建时间、更新日期、负责人、文档状态、标签。RAG检索时,我可以在元数据维度上做硬性过滤,比如只检索“状态=有效”的文档,或者优先检索“负责人=某个人”的文档。更进一步,还可以把文档链接当作上下文线索,检索到一篇文档后,把它的双链邻居也拉进来,一起喂给大模型。
这里要澄清一个常见误区:双链和RAG不是竞争关系。双链帮人发现知识,RAG帮机器检索知识。Wiki里的链接结构,对RAG来说是一个天然的“路标”,它告诉检索器哪些内容在语义上是相邻的。
一个很值得养成的小习惯是:在每篇Wiki文档顶部写一段“摘要”,一到三句话概括全文核心。这段摘要不要被当作正文的替代,它是给检索器额外锚点。当我用RAG检索时,如果把摘要字段单独建索引,能明显提升命中率,因为摘要往往是问题与文档之间最直接的语义桥梁。
5.2 图片、表格和代码块:多模态知识库的取舍
很多人问“RAG知识库能存储图片嘛”。直接回答:能,但默认情况下,普通的文本RAG是不会理解图片的。向量化的是文字,图片只会作为文件对象存放在库里,检索不到里面的内容。
我在知识库里处理图片,通常有三种方案:
- 把图片中的文字用OCR工具提取出来,转为文本块,随原文档一起向量化。这个对流程架构图、表格截图特别有效。
- 用多模态模型生成图片描述,再把描述文本存入向量库。比如“这是新服务器上线的网络拓扑,包含防火墙端口和DMZ区”,生成模型在回答时能引用描述来辅助判断。
- 如果图片本来就是附件性质,不参与语义检索,那就让它继续挂在Wiki页面上,RAG只负责定位到图片所在的页面,再由人去查看图片。
表格是另一个大坑。我一开始把整个Markdown表格当成一个chunk,结果切分时表格被拦腰截断,检索到的只是一半的列名,大模型根本拼不出完整答案。后面我用了一个土办法:把表格拆成“逐行”的文本块,并在每行前加上表头说明,比如“服务器列表:编号123,IP 10.0.0.8,用途日志采集”。这个方法虽然数据冗余,但检索效果稳定。
代码块也一样,最好单独作为一个chunk,并保留代码块的注释信息。不要让代码与其上下文混在一起,否则引用位置会莫名其妙。多年维数据库实践下来,多模态部分的核心原则是:任何检索器能理解的内容,最终都必须变成文本,且这个文本要有上下文。
5.3 把SOP写进Wiki,让RAG变成“自动档”
如果说上面都在谈技术调优,那这一节讲的是让RAG真正被团队用起来的业务习惯。我观察到一个规律:写文档最勤快的团队,RAG效果普遍更好。原因是RAG的本质是“从已有文档里找答案”,而一份文档如果没有结构、没有步骤、没有可验证的标准,那即使被检索到,也无法转化成高质量回答。
所以我把团队Wiki模板改成了偏SOP风格。每个运维操作页面都包含四段:
- 背景和前置条件。
- 操作步骤,每一步写清楚命令或动作。
- 验证方法,怎么确认操作成功。
- 常见错误清单,以及对应的处理方式。
这样改之后,RAG输出的答案天然具备“可执行性”。问“怎么扩容磁盘”,它会把背景、步骤、验证、排错都从对应章节抓出来,形成一段有前后逻辑的操作流程。团队里面哪怕是个新人,照着这个回答,也能独立完成任务。
每周我还会抽时间,把群里被反复提问的会话整理成新的FAQ条目,补进Wiki对应章节,然后重新跑一遍索引。这个过程我称为“让知识库长新知识”,它比追求更复杂的算法更有价值,因为内容的增量和更新频率,决定了RAG答案的时效性。
大家常说的“知识割裂”,根源往往不是没有知识,而是知识散落在聊天记录、邮件、wiki和PPT里,互相之间没有索引关系。RAG可以打通这些渠道,但前提是把Wiki当成“主干知识库”,其他来源的内容经过沉淀之后,同步回主干。这样RAG检索到的永远是最新、最完整的上下文,而Wiki也真正变成了一个越用越厚的活知识库。
我自己的体会是,RAG项目最大的失败模式不是技术选型错,而是你搭好了一个流水线,却喂给它一堆烂数据。如今回头再看这个项目,最有价值的反而不是那套Ollama和LangChain的代码,而是我们重新梳理出来的Wiki结构和维护习惯。只要Wiki一直在长,RAG就能一直帮团队把答案准确翻出来。最后分享一个小技巧:在CI流程里加一步,检测到Wiki有commit时就自动重建向量索引,让知识库始终跟文档保持同步,你就再也不用担心“文档更新了但机器人还在答旧内容”了。