用LLM构建本地Wiki知识库:从语义检索到RAG实践
发布时间:2026/9/14 4:43:54 锦皓数字建站

在本地用 LLM 给自己造一个 Wiki 知识库这事我琢磨了挺久最近终于把 llm_wiki 这个项目跑通了整体效果超出预期。说白了llm_wiki 就是把大语言模型和 Wiki 这套知识管理方式结合起来让你能把散落在各处的工作笔记、技术文档、会议记录、研究资料全部汇总到一个私人知识库里然后用自然语言去提问、检索、甚至让模型帮你归纳总结。这项目非常适合那些日常信息量巨大、又不想把所有资料传到云端给第三方平台的技术人、研究者、产品经理和写作爱好者尤其适合对数据隐私有要求的人。我最初做这个项目是因为发现自己本地攒了几千个 Markdown 文件散落在不同的仓库、文件夹和笔记本里想找一个之前写过的技术方案光是回想存在哪就花半天。后来尝试了几款在线知识库工具总觉得不太对味数据上传到别人的服务器总归不踏实而且通用搜索工具对文档内容的理解太浅搜出来一堆不相关的片段。所以我就琢磨能不能用现在已经很成熟的大语言模型自己在本地搭一套能读懂文档内容的智能知识库。这就是 llm_wiki 的由来。下面我完整拆解一下这个项目的设计思路、核心实现以及我在实操中踩过的坑。1. 整体设计为什么是本地 Wiki而不是直接用在线工具在动手写代码之前我先花了不少时间想清楚整个项目要解决什么问题。这步挺关键因为如果后面发现方向错了返工成本会很大。首先我明确了这个项目的第一需求是私有化。我的工作笔记里有不少客户信息、市场分析和内部技术架构讨论这些东西直接丢到网络上我是真不放心。就算很多在线知识库工具提供加密传输和存储但从安全边界来讲数据离开本机的那一刻你的控制权就已经让渡了一部分。所以 llm_wiki 的核心定位从一开始就定成所有数据、索引、模型调用全部在本地完成。第二个需求是语义检索。传统的关键词搜索比如 grep 或者大多数笔记软件的搜索功能本质是字符串匹配。当我搜图像分割方案的准确率优化时如果文档里写的全是mIoU 提升segmentation head 调参这类表述关键词匹配基本搜不到。而 LLM 能理解语义我可以把文档切成块用嵌入模型转成向量存进本地向量数据库查询时把问题也转成向量做相似度检索。这是从字面匹配到语义理解的思维转变。第三个需求是交互友好。我不想每次查点东西都开终端敲 SQL 或者跑 Python 脚本我想要一个能自然对话的界面。我可能问它上个月我和老王讨论的那个模型蒸馏方案关键结论是啥它应当能给出准确的段落和摘要甚至告诉我这篇笔记在哪个文件里。基于这三点llm_wiki 的整体架构就清晰了它由四个部分组成文档导入与解析层把各种格式的本地文档Markdown、TXT、PDF、Word 等解析成纯文本。文本切分与向量化层把长文本按语义边界切成小段然后用嵌入模型把每个段转成向量。存储与索引层向量库负责存向量和原文的映射元数据库记录文件来源、更新时间等信息。问答与展示层本地 Web 界面用户输入问题系统调 LLM 生成回答同时展示参考来源。这个四层架构每一项都有现成工具能选但选型思路各有门道下面详细说说。2. 核心技术选型从模型到向量库每一层都值得较真2.1 嵌入模型与本地推理引擎怎么选llm_wiki 的嵌入Embedding模型选择是第一步也是比较影响效果的一步。嵌入模型的作用是把一段文字变成一个高维向量向量和向量之间的距离代表语义的相近程度这是向量检索的基础。我测试过三种路线。第一种是直接调用在线 Embedding API比如 OpenAI 的 text-embedding-ada-002。效果确实不错中文和英文混合场景下表现稳定但问题是它需要联网数据要发到第三方服务器这违背了私有化初衷。第二种是本地用小模型跑嵌入比如 BAAI/bge-small-zh-v1.5参数量不大纯 CPU 也能跑但实际测下来对长文档的语义捕捉不够细腻。第三种也是我最后选定的方案是用更通用的 bge-m3 模型。这个模型支持中文和英文混合输入并且输出向量维度是 1024 维语义区分度比小模型明显强。在本地单张老款 1080Ti 显卡上跑嵌入 1000 个文档块大约需要几分钟属于可接受范围。选嵌入模型时要特别注意一个指标叫 MTEB这是业界常用的嵌入模型评测基准。但别只看总分要看它在你所用语言和场景下的表现。例如我工作中中文文档居多就优先看中文任务分数。实测下来 bge 系列的中文效果在开源模型里是第一梯队的。2.2 生成模型选哪种决定问答质量的上限嵌入模型负责找得准生成模型负责答得好。llm_wiki 里的问答环节我用一个本地部署的 ChatGLM3-6B 来做生成。有些人可能会问为什么不用更大的模型比如 13B 甚至 70B 参数的模型原因很简单我的工作机只有 24G 显存一张 3090跑 6B 模型做推理比较流畅。如果强行上 13B虽然也能跑但并发处理能力下降明显体验会卡顿。这里有一个关键点值得注意生成模型的大小和嵌入模型的大小并不是同一个概念。生成模型决定的是回答语言组织的质量和推理深度嵌入模型决定的是检索内容的准确度。在实际使用中我发现 6B 级别的本地模型做知识库问答完全够用因为它不需要它无中生有只需要它根据检索到的上下文片段做总结归纳这个任务复杂度并不高。所以如果你显存不算大不必追求超大模型。2.3 向量数据库Chroma 足够轻量但也有限制向量数据库的选择也比较关键。市面上主流的有 Milvus、Qdrant、Weaviate 和 Chroma。我最后选了 Chroma原因是它的轻量模式太方便了。Chroma 可以以嵌入式模式运行不需要单独起一个数据库服务。在你的 Python 进程里直接调用 Chroma 的 API数据存在本地目录下一个文件夹搞定所有。对于个人知识库这种使用场景这简直是最优解。如果项目规模到了一定程度需要多人协作或者更复杂的权限管理那时再迁移到 Milvus 这类重量级方案也不迟。但轻量也意味着它有些限制。比如 Chroma 的并发性能比较一般多人同时访问时可能会成为瓶颈。不过在单用户私有用例下我目前没有遇到过明显的性能问题。还有一个要注意的地方Chroma 的数据结构相对简单如果你需要复杂过滤逻辑比如按特定标签、日期范围过滤后再检索就要注意你的元数据设计是否合理。我在实际项目里就给每个文档块加了不少元数据包括文件路径、一级标题、更新时间、文档类型等等这些字段在后续过滤查询中非常有用。2.4 交互界面Gradio 是快速搭建的首选界面这一层我选择了 Gradio。原因很简单它能让我用最少的代码搭建一个可用的 Web 聊天界面且支持 Markdown 渲染对我这种以 Markdown 笔记为主的项目特别友好。为什么不选 StreamlitStreamlit 更适合做数据展示类的应用交互流畅度上Gradio 在聊天对话场景下体验更自然。而且 Gradio 对多轮对话上下文的支持也更加原生我只需维护一个消息列表传给后端就行。界面虽然看起来简单但细节需要打磨。比如我在界面上展示了参考来源这个板块这非常重要。当你问一个问题系统除了给你回答还应该告诉你它依据的是哪个文件哪一段。这样做有两个好处一是你能去核对信息的真实性二是能帮助你逐步信任这个系统。这个功能在 Gradio 里实现起来不算复杂只需在返回回答时同时把命中的文档块和源文件路径传回前端即可。3. 实操搭建过程从零到可用的完整步骤这一部分我尽量写得细致一些几乎每一步都会说明为什么这么做方便你直接照着做也能理解其中的原理。整个过程分为环境准备、数据导入、索引构建、问答实现四个环节。3.1 环境准备Python 虚拟环境与依赖安装我建议使用 conda 或者 venv 创建一个干净的虚拟环境避免和系统 Python 环境打架。我的环境是基于 Python 3.10 的这是目前兼容性最好的版本。然后安装以下依赖pip install chromadb0.4.0 pip install sentence-transformers2.2.0 pip install gradio3.40.0 pip install pypdf pip install python-docx pip install transformers accelerate关于 transformers 和 accelerate如果你的显卡驱动和 CUDA 环境配置正常transformers 会自动调用 GPU 进行推理。如果没 GPUCPU 模式也能跑但速度会明显慢建议量力而行。3.2 文档导入与解析Markdown、PDF、Word 全覆盖我平时文档以 Markdown 为主但同事发来的资料经常是 PDF 或者 Word所以文档解析层必须覆盖这三种格式。这里有一个细节解析 PDF 时如果用 pypdf 直接提取中文文档很容易出现乱码或者文字顺序错乱的问题。我实测下来解决方法是优先提取 PDF 中文本层的文字如果发现提取结果几乎不可读就改用 OCR 方式。由于 llm_wiki 是本地项目我直接用 PaddleOCR 做兜底。这一步的逻辑大概是def extract_text(file_path): if file_path.endswith(.md): with open(file_path, r, encodingutf-8) as f: return f.read() elif file_path.endswith(.pdf): return extract_pdf_text(file_path) # 内部先尝试文本提取失败则走 OCR elif file_path.endswith(.docx): doc Document(file_path) return \n.join([p.text for p in doc.paragraphs])这个解析层是整个系统的基础文档都解析不对后面的所有环节都白搭。我在调试中发现很多 PDF 文件的元信息特别多比如页眉页脚、页码等这些内容如果不清理后续切分出的文本块就会很脏向量检索时也会引入噪声。因此我在解析后补了一个预处理步骤用规则表达式去掉页眉页脚和重复的空行。3.3 文本切分策略按段落切而不是按固定字数切切分文本是知识库构造中一个容易被忽视但影响巨大的环节。刚开始用 LangChain 的默认切分器按固定 token 数切后来发现效果很差。比如一个技术文档里讲模型架构的章节中间可能有一张表的说明但这部分和上下文关联极强按固定 token 切很容易把完整语义破坏掉。我最终的方案是优先按 Markdown 标题结构切分遇到没有标题的文档就按段落切每个块最大控制在 500 个 token 左右。这个逻辑用 Python 实现也比较直接def split_text_by_headers(text): lines text.split(\n) chunks [] current_title untitled current_content [] for line in lines: if line.startswith(#): if current_content: chunks.append({title: current_title, content: \n.join(current_content)}) current_title line.lstrip(#).strip() current_content [] else: current_content.append(line) if current_content: chunks.append({title: current_title, content: \n.join(current_content)}) return chunks注意每个 chunk 里还保留了标题字段这是很有用的。因为后续插入向量库时我会把标题和正文拼接起来一起做嵌入这样检索时如果命中该块模型不仅能看到正文还能理解它属于哪个主题章节。3.4 向量化与入库一条龙流程切分好了接下来把每块文本送入嵌入模型生成向量然后把向量、原文、元数据一起存入 Chroma。这一步需要注意的是每插入一条数据时要检查一下是否已经存在同内容的向量防止重复入库。我用的策略是为每个文档生成一个 MD5 哈希作为唯一 ID这样如果同一篇文档被重复导入会被自动跳过。import chromadb from chromadb.utils import embedding_functions client chromadb.PersistentClient(path./llm_wiki_db) collection client.get_or_create_collection( namewiki_docs, embedding_functionembedding_functions.SentenceTransformerEmbeddingFunction( model_nameBAAI/bge-m3 ) ) for chunk in chunks: doc_id hashlib.md5((file_path chunk[title] chunk[content]).encode()).hexdigest() existing collection.get(ids[doc_id]) if existing[ids]: continue collection.add( ids[doc_id], documents[chunk[title] \n chunk[content]], metadatas[{ source: file_path, title: chunk[title], updated_at: timestamp }] )这里有个关于 Chroma 版本的小坑不同版本的 API 有一些差异。新版 Chroma 把 embedding_function 的加载方式做了调整建议直接用官方文档的写法并且固定版本号避免后续升级引发不兼容问题。3.5 问答链路检索增强生成RAG的完整实现出门在外核心功能还是要能回答用户问题。问答链路我采用标准 RAG 模式也就是先检索再让 LLM 基于检索结果作答。这个流程可以拆成四步第一步把用户的问题也用嵌入模型转成向量。第二步用这个向量去 Chroma 里做相似度检索取 top_k 个最相关的文档块k 我一般设为 5。第三步把这 5 个块拼接成一段参考资料加上用户的原始问题一起构造 Prompt。第四步把完整的 Prompt 送入本地大模型 ChatGLM3-6B生成回答。Prompt 模板我用的是这样一个结构请基于以下参考资料回答问题。如果参考资料中没有相关信息请直接回答知识库中未找到相关内容不要编造。 参考资料 [1] (文件路径: xxx.md, 标题: 模型压缩方案) (文档内容...) [2] (文件路径: xxx.md, 标题: 蒸馏实验记录) (文档内容...) 问题模型蒸馏时温度参数一般设为多少比较合适 回答这个模板看起来简单但很有效。不要编造这两个字非常重要。大模型有个天然毛病叫幻觉它可能会编造一些看似合理但实际不存在的内容。通过指令约束加参考资料限制可以大大降低幻觉率。实测下来在 llm_wiki 上问基于知识库的问题回答准确率在 85% 以上。关于 k 值的设置我建议不要过大。很多初学者以为参考资料越多越好其实并非如此。k 值过大大量不相关的文本块会被强行拼进 Prompt不仅会稀释有效信息的浓度还会增长模型的推理时间。k 值太小则可能漏掉关键内容。我测试过 3、5、8 三个档位5 在准确率和速度上最均衡。4. 常见问题与排查技巧我在实际部署中踩过的坑4.1 向量检索结果不准问题可能出在切分上如果你发现检索出来的内容总是差不多但差点意思大概率不是嵌入模型的问题而是文本切分太粗或者太细。切得太粗比如把整个章节作为一个向量那么这段文字的语义重心会被大量篇幅淹没检索精度就会下降。切得太细比如一句话一个块那检索出来的上下文不够完整大模型无法理解背景。我建议的原则是每个块尽量表达一个完整的小主题既能独立看懂又保留了上下文关联。另外一个容易踩的坑是嵌入时需要把标题和正文放一起。如果你的库只嵌入正文检索时命中的块可能不知道该内容属于哪个主题回答起来缺少归属感准确率自然下降。4.2 模型回答有幻觉教你三招压制幻觉问题在 llm_wiki 里我也遇到过尤其是问一些模糊的问题时。例如我明明没有在笔记里写过具体某个实验的结论模型却给出了一个看似合理的结果。我总结了三招压制幻觉的方法。第一招是在 Prompt 里强调只能依据参考资料这招最管用。第二招是在返回界面上显著展示参考来源让用户有信息核实路径。第三招是设置一个相似度阈值如果检索出的最相关文本块得分低于某个阈值就默认拒绝回答告知用户未找到相关内容。阈值需要你根据自己的文档情况动态调整我这边设置的是 0.32可以参考。4.3 导入大量文档时内存爆掉分批处理是关键第一次导入一万多个文档块时程序直接吃掉了我 32G 内存然后被系统 OOM 杀掉。原因是嵌入模型批量处理时一次性把太多文本放进了显存和内存。解决办法是分批处理每批只处理 32 个文本块处理完立即释放变量。这个改动看起来不起眼但极大降低了内存峰值。batch_size 32 for i in range(0, len(chunks), batch_size): batch chunks[i:ibatch_size] collection.add(documentsbatch) time.sleep(0.5)加入一个极短的 sleep 是为了避免 CPU 和 GPU 之间数据搬运过频导致 IO 阻塞实测下来整体吞吐量没有下降多少但稳定性提升明显。4.4 界面回答太慢量化模型与流式输出双管齐下本地模型回答速度是一个绕不开的话题。刚开始我把完整回答生成完之后才一次性显示在界面上遇到长问题的推理时间有十几秒非常煎熬。后来我做了两个改动一是用 GPTQ 量化版本的 ChatGLM3-6B推理速度提升约 30%显存占用也从 14G 降到 10G 左右二是在 Gradio 里开启流式输出模型每生成一个 token 就实时显示这样的体验提升是巨大的。用户在界面上看到回答一行一行出来等待感完全不一样。4.5 更新文档后索引不同步建立版本校验机制这个坑是我后来才发现的。当我在本地修改了一篇笔记重新导入时由于 MD5 是文件路径加标题加正文生成的正文一变ID 就变旧的那份数据不会被自动清理库就出现了重复内容。解决方式是文档导入时先按 source 字段删除所有旧记录再重新插入新内容。删除用 Chroma 的 collection.delete(where{source: file_path}) 即可实现。4.6 中文路径导致的兼容性问题最后提醒一个非常实际的问题尽量别让你的知识库文件路径包含中文和空格。这不是说系统不支持中文路径而是在实际开发中很多库对中文路径的处理存在潜在问题特别是 PDF 解析和一些背后的编码处理。万一遇到莫名其妙的读取失败可以先检查一下路径是不是存在特殊字符。5. 扩展方向与进阶玩法llm_wiki 目前的版本已经解决了我个人知识管理的大部分痛点但它的潜力远不止于此。我在实际使用中已经在规划几个扩展方向。第一个方向是多模态支持。现在的知识库还是纯文本但我的资料里有大量图片和截图比如架构图、实验结果图、白板照片。下一步我打算接入本地视觉模型对图片进行描述并生成文本摘要再和图片一起入库。这样用户就能通过文字搜索到图片内容了。第二个方向是知识图谱融合。纯向量检索有个弱点就是它无法理解实体之间的逻辑关系。比如A 方案比 B 方案好这种判断性结论在向量空间里能被检索到但如果你问目前有哪些方案的对比结论系统无法主动跨文档把分散的对比信息汇总成图。如果引入知识图谱用 LLM 自动抽取实体和关系就能实现更智能的多跳问答。第三个方向是自动化定期更新。我设想让 llm_wiki 变成一个常驻后台服务监视我指定的几个文件夹一旦发现文件变更就自动增量更新索引不需要手动触发。这个本质上可以做成一个文件系统监听器加定时任务的组合。实际开发起来也不难只需要把导入流程封装成函数用 watchdog 库监控目录变化即可。第四个方向是共享协作。既然是 Wiki理论上支持多人协作是自然延伸。后续可以考虑加入用户权限体系和共享折叠让团队内部也能用起来。不过这需要把向量库切换到更重量级的方案同时引入 Web 服务层做鉴权工程量会大不少适合当作 v2.0 的规划。写在最后的一点个人体会llm_wiki 这个项目做下来我最深的一点体会是大模型时代真正值钱的不只是模型本身怎么组织和利用好你手头已有的信息是一件更需要花心思的事。很多人以为搭一个本地知识库就是把文档喂给模型就完事了但实际做下来你会发现文本切分、索引更新、检索阈值、Prompt 设计……每一个细节都会影响最终的使用效果。这些经验不是看几篇教程就能全了解的必须自己动手踩坑才能形成感觉。如果你也想搭一个自己的 llm_wiki我建议不要一开始就追求大而全的功能。先从最核心的链路开始哪怕只是把你平时的 Markdown 笔记导入进去跑通本地文档 — 向量化 — 检索问答这条线你就能切实感受到它和传统搜索工具的巨大区别。之后再逐步加入 PDF、Word、图片再慢慢调优效果。一步一步来最后你收获的不只是一个工具还有整套关于语义检索和 RAG 架构的实战理解。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。