1. 从“存知识”到“编译知识”:LLM Wiki 到底在解决什么问题
第一次听到“LLM Wiki”这个词,很多人会下意识把它理解成“又一个知识库工具”。我一开始也这么想,直到把 Karpathy 那套思路完整读了一遍,才发现它跟传统 RAG 知识库压根不是一回事。传统知识库的核心动作是“存”和“取”:你把文档切块、向量化、塞进数据库,用户提问时做相似度检索,把最像的几段拼进上下文喂给模型。而 LLM Wiki 的核心动作是“编译”——把散落的原始素材,经过 LLM 的加工,变成一份结构化、可追溯、可增量维护的知识产物。
打个比方。传统 RAG 像是一个巨大的仓库,你问它“螺丝刀在哪”,它靠标签和位置索引帮你翻出来几把长得像的。而 LLM Wiki 更像是一个老师傅,他先把仓库里的零件全部拆解、归类、标注用途,写成一本《工具手册》,你问“我要拧十字螺丝该用哪个”,他直接翻到手册第 3 章第 2 节告诉你答案,还附上“为什么不能用一字螺丝刀”的解释。前者是检索,后者是编译。
这个区别带来的连锁反应非常大。检索式 RAG 的瓶颈大家都清楚:切块粒度难调、召回率上不去、多跳问题答不了、上下文塞太多导致模型注意力涣散。而知识编译的思路是把这些难题前移到“离线加工”阶段——用 LLM 在入库前就把知识嚼碎、重组、建立关联,查询时只需要读一份已经整理好的“wiki 页面”,而不是临时拼凑碎片。这就是为什么热词里同时出现了rag瓶颈、agentic rag、ontology rag、graphrag llm wiki 本体rag这些词——大家都在找 RAG 的下一站,而 LLM Wiki 是其中一条很务实的分支。
这篇文章适合谁看?如果你正在用 Dify、Obsidian、Ollama 搭个人或团队知识库,如果你被 RAG 的召回率和幻觉折磨过,如果你想搞清楚“知识编译”和“知识检索”的工程差异,那这篇就是写给你的。我会从设计思路、核心机制、实操落地、踩坑排查四个层面,把 LLM Wiki 这套东西拆到能直接抄作业的程度。不吹概念,只讲我实际跑下来觉得有用的部分。
2. 核心设计思路:为什么是“编译”而不是“检索”
2.1 传统 RAG 的三个结构性瓶颈
要理解 LLM Wiki 的价值,得先把传统 RAG 的痛点说透。我做过好几个基于向量库的知识问答项目,踩过的坑基本集中在三个地方。
第一个是切块悖论。切得太碎,单块信息不完整,模型拿到半句话没法回答;切得太粗,一块里混了好几个主题,检索时噪声大。我试过 256、512、1024 三种 chunk size 在同一份技术文档上跑,命中率能差出 20 个百分点,而且没有一个尺寸对所有文档都最优。这个参数本质上是在“信息完整性”和“检索精度”之间做妥协,而妥协就意味着两头都不讨好。
第二个是语义漂移。用户问“这个接口超时怎么调”,文档里写的是“请求响应时间阈值配置”,字面相似度不高,但语义是一回事。纯向量检索靠的是 embedding 空间的距离,遇到这种同义不同词的情况,召回经常掉链子。你加 BM25 做混合检索能缓解,但又引入了新的权重调参问题。
第三个是多跳断裂。用户问“A 模块的配置改了之后,会影响 B 模块的哪些行为”,答案分散在三四个文档里,需要模型自己串联。传统 RAG 一次性召回 top-k,很难保证这几块恰好都被捞出来,更别说让模型正确建立它们之间的因果链。
这三个瓶颈的根源是同一个:检索发生在查询时,而查询时你只有一次机会。LLM Wiki 的思路是把这次机会前移,在入库阶段就用 LLM 把知识之间的关系理清楚,查询时读的是已经编译好的成品。
2.2 知识编译的核心机制:Token 三元组视角
热词里有一条特别有意思:llm的token三个点key我是谁、query我在找什么、value我能提供什么。这其实点出了 LLM Wiki 的底层逻辑——把每个知识单元看成一组 key-query-value 三元组。
- key(我是谁):这个知识块的主题标识,比如“Ollama 本地部署”“Dify 流水线配置”。
- query(我在找什么):什么样的提问应该命中这个块,比如“怎么在本地跑模型”“知识库怎么接工作流”。
- value(我能提供什么):这个块实际承载的答案内容。
传统 RAG 只做了 key 和 value 的匹配,query 是用户临时给的,匹配靠向量距离。而 LLM Wiki 在编译阶段就让 LLM 为每个知识块生成显式的 query 描述和关联链接,相当于提前把“用户可能会怎么问”这件事想了一遍。查询时不是做模糊的向量匹配,而是做更接近“意图对齐”的匹配。
这个机制的好处是,你可以用相对小的模型来完成编译。热词里有人问卡帕西的知识库可以用小模型做吗,我的实测答案是:编译阶段用 7B 到 14B 的模型做结构化抽取和摘要,效果已经够用;真正需要大模型的是最终的问答生成环节。因为编译是“整理”任务,不是“推理”任务,对模型的要求没那么高。
2.3 与 GraphRAG、Ontology RAG 的关系
很多人会把 LLM Wiki 和 GraphRAG 混为一谈。两者确实有交集,但侧重点不同。GraphRAG 强调的是实体和关系的图谱化,适合“谁和谁有关系”这类问题;LLM Wiki 强调的是知识的页面化编译,适合“这个东西是什么、怎么用”这类问题。热词里的ontology rag、rag graphrag llm wiki 本体rag说明大家已经在尝试把两者结合——用本体(ontology)定义知识结构,用图谱建立关系,用 wiki 页面承载最终内容。
我的实践建议是:如果你的知识领域实体关系复杂(比如医疗、法律、供应链),上 GraphRAG 或本体 RAG 更合适;如果是文档型、教程型、经验型知识(比如技术文档、产品手册、个人笔记),LLM Wiki 的编译思路性价比更高。两者不是替代关系,而是可以叠加——wiki 页面里嵌入图谱链接,图谱节点指向 wiki 详情。
3. 核心细节解析:LLM Wiki 的四个关键环节
3.1 素材预处理:别急着喂给模型
编译质量的上限由素材质量决定。我见过太多人把 PDF 直接丢进流水线,结果编译出来的 wiki 页面全是乱码和断句。预处理这一步不能省,而且要分三层做。
第一层是格式归一化。把 PDF、Word、Markdown、网页剪藏统一转成干净的 Markdown。PDF 转换我推荐用marker或pymupdf4llm,比传统的 pdfplumber 保留结构的能力强很多,标题层级、表格、代码块都能识别。网页内容用trafilatura抽取正文,能自动去掉导航栏和广告。
第二层是语义分块。不要用固定字符数切,用基于标题层级和段落语义的切分。我的做法是先按 Markdown 的#、##、###切,如果某个块超过 1500 字再按段落二次切分。这样每个块天然带有一个主题,编译时 LLM 更容易判断“这块在讲什么”。
第三层是元数据标注。每个块至少带上来源文件、章节路径、更新时间三个字段。来源文件用于追溯,章节路径用于建立层级关系,更新时间用于增量编译时判断哪些块需要重新处理。这一步看起来琐碎,但后面做增量更新和溯源时,没有这些元数据会非常痛苦。
注意:预处理阶段不要做摘要。摘要留给编译阶段做,因为编译时的 LLM 能看到更多上下文,摘要质量更高。提前摘要等于把信息压缩了两次,损失很大。
3.2 编译流水线:LLM 到底在做什么
这是 LLM Wiki 的核心。编译流水线一般跑三个 LLM 任务,我按重要性排序。
任务一:主题归并。把语义相近的块合并成一个 wiki 主题。比如“Ollama 安装”“Ollama 模型拉取”“Ollama 端口配置”三个块,应该归并到“Ollama 本地部署”这一个主题下。做法是先用 embedding 做粗聚类,再让 LLM 判断每个簇是否应该合并、合并后的主题名是什么。这一步能大幅减少知识碎片,是提升查询命中率的关键。
任务二:页面生成。对每个主题,让 LLM 基于所有相关块生成一份结构化 wiki 页面。页面结构我固定用四段:概述、关键步骤、注意事项、关联主题。概述用两三句话讲清楚这个主题是什么;关键步骤用有序列表列出操作流程;注意事项放踩坑经验;关联主题列出相关的其他 wiki 页面链接。这个结构是我试了七八种模板后觉得最好用的,既完整又不会让 LLM 自由发挥跑偏。
任务三:关系抽取。让 LLM 识别页面之间的依赖、前置、对比关系,生成一张有向图。比如“Dify 知识库配置”依赖“Dify 基础部署”,“Ollama 和 vLLM 对比”是并列关系。这张图在查询时用于多跳扩展——用户问 A,系统可以顺着图把相关的 B、C 也带出来。
这三个任务的 prompt 我建议都加上 few-shot 示例,尤其是页面生成任务。不给示例的话,LLM 生成的页面结构每次都不一样,后期维护很头疼。
3.3 存储与索引:向量库不是唯一选择
编译产物的存储我推荐双轨制:Markdown 文件 + 向量索引。
Markdown 文件是主存储,每个 wiki 页面一个.md文件,放在 Git 仓库里管理。这样做的好处是版本可控、人可读可编辑、迁移成本低。Obsidian 直接打开这个文件夹就是一个知识库,热词里obsidian知识库搭建说的就是这个用法。我甚至建议你定期人工审阅这些页面,LLM 编译难免有错,人工修正后质量会明显提升。
向量索引是辅助检索,把每个 wiki 页面的概述段和关键步骤段做 embedding 存进去。注意这里索引的是编译后的页面,不是原始块,所以索引量小很多,检索精度也高很多。我用bge-m3做 embedding,配合milvus或qdrant都行,个人用chroma也够。
关系图我存在一个简单的 JSON 文件里,节点是页面 ID,边是关系类型。查询时先向量检索命中入口页面,再顺着图扩展一跳或两跳。这个图不需要图数据库,JSON 加内存索引完全够用,除非你的知识规模上万页。
3.4 查询链路:从提问到答案的完整路径
查询链路的设计直接决定用户体验。我的实现分四步。
第一步,意图解析。用小模型或规则把用户问题拆成“核心意图 + 限定条件”。比如“Ollama 在 Windows 上怎么配 GPU”拆成核心意图“Ollama GPU 配置”和限定条件“Windows”。
第二步,页面召回。用核心意图做向量检索,命中 top-3 的 wiki 页面。如果命中页面的关联图里有高权重邻居,一并带出来。
第三步,上下文组装。把命中的页面按“概述 + 关键步骤 + 注意事项”的顺序拼进 prompt,关联页面只放概述。控制总 token 在 4000 以内,留足空间给模型生成。
第四步,生成与溯源。让模型基于组装好的上下文回答,并要求它在答案里标注引用了哪个 wiki 页面。这样用户能点进去看原文,幻觉问题也能被用户自己发现。
这套链路跑下来,我实测在多跳问题上的准确率比传统 RAG 高了大概 30%,而且答案的可读性明显更好,因为模型读的是整理过的页面,不是碎片。
4. 实操落地:从零搭一套 LLM Wiki 流水线
4.1 环境与工具选型
先把工具链定下来。我的推荐组合是:
| 环节 | 工具 | 理由 |
|---|---|---|
| 文档转换 | marker / pymupdf4llm | 保留结构能力强 |
| 网页抽取 | trafilatura | 正文识别准确 |
| 分块 | 自写脚本 + markdown 解析 | 灵活可控 |
| 编译 LLM | Qwen2.5-14B / Llama-3.1-8B | 本地可跑,结构化输出稳 |
| Embedding | bge-m3 | 中英混合效果好 |
| 向量库 | qdrant / chroma | 轻量,部署简单 |
| 编排 | 自写 Python 脚本 | 比框架更透明 |
| 存储 | Git + Markdown | 版本可控,人可读 |
为什么不直接用 Dify 或 LangChain 的现成流水线?热词里dify知识库流水线、langchain4j easy rag都有人问。我的看法是:Dify 适合快速验证,但它的知识库是检索式的,不是编译式的,你要做 LLM Wiki 得自己写编译节点。LangChain 的抽象层太厚,调试编译 prompt 时很不方便。自写脚本虽然前期麻烦,但每个环节都透明,出问题好定位。
模型方面,如果你有 GPU,Qwen2.5-14B 是编译任务的最佳性价比选择。没有 GPU 就用 API,豆包、通义、DeepSeek 都行,热词里用豆包搭建知识库文件说的就是这个路子。编译任务对模型要求不高,API 成本可控。
4.2 编译脚本的核心实现
下面是我实际用的编译脚本骨架,Python 写的,跑在本地。
import json from pathlib import Path from openai import OpenAI client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama") def compile_topic(topic_name, chunks): """把一个主题下的所有块编译成 wiki 页面""" context = "\n\n---\n\n".join([c["content"] for c in chunks]) prompt = f"""你是一个知识编译助手。请基于以下素材,为主题「{topic_name}」生成一份结构化 wiki 页面。 素材: {context} 请严格按以下格式输出: ## 概述 (两三句话说明这个主题是什么) ## 关键步骤 1. ... 2. ... ## 注意事项 - ... ## 关联主题 - [[相关主题名]] """ resp = client.chat.completions.create( model="qwen2.5:14b", messages=[{"role": "user", "content": prompt}], temperature=0.3 ) return resp.choices[0].message.content def main(): chunks = json.loads(Path("chunks.json").read_text()) topics = json.loads(Path("topics.json").read_text()) for topic in topics: related = [c for c in chunks if c["id"] in topic["chunk_ids"]] page = compile_topic(topic["name"], related) out = Path(f"wiki/{topic['name']}.md") out.parent.mkdir(exist_ok=True) out.write_text(page, encoding="utf-8") print(f"编译完成: {topic['name']}") if __name__ == "__main__": main()这段代码的关键点有三个。temperature=0.3是为了让输出稳定,太高了每次格式都不一样。prompt 里强制指定输出格式,并且用[[主题名]]这种双链语法,方便 Obsidian 直接识别成关联链接。素材之间用---分隔,让模型能区分不同来源。
主题归并那一步我单独写了个脚本,先用bge-m3算块之间的相似度,用 0.75 作为阈值做聚类,再把每个簇的块标题喂给 LLM 让它起一个统一主题名。阈值 0.75 是我试出来的,太低会把不相关的合并,太高又合并不了。
4.3 增量编译:别每次全量重跑
知识库是要持续更新的,全量重编译既慢又费钱。我的做法是基于元数据做增量。
每个块带一个content_hash字段,内容变了 hash 就变。每次更新时,先扫描所有块,找出 hash 变化的块和新增的块,标记为“脏块”。然后只对包含脏块的主题重新编译,其他主题不动。如果脏块导致主题归并结果变化(比如新增块形成了一个新主题),再单独处理。
这套机制跑下来,日常更新只需要重编译 5% 到 10% 的页面,速度从全量的十几分钟降到一两分钟。Git 提交时能看到具体哪些页面变了,审阅也方便。
实操心得:增量编译一定要保留旧版本。我用 Git 的 branch 机制,每次编译前先 commit 当前状态,编译后对比 diff。LLM 偶尔会把之前正确的内容改错,有 diff 就能及时发现并回滚。
4.4 查询服务的搭建
查询服务我用 FastAPI 包了一层,对外提供/ask接口。
from fastapi import FastAPI from pydantic import BaseModel import json from pathlib import Path app = FastAPI() class Query(BaseModel): question: str @app.post("/ask") def ask(q: Query): # 1. 意图解析(简化版,实际用模型) intent = q.question # 2. 向量检索命中页面 hits = vector_search(intent, top_k=3) # 3. 组装上下文 context_parts = [] for hit in hits: page = Path(f"wiki/{hit['name']}.md").read_text() context_parts.append(f"# {hit['name']}\n{page}") context = "\n\n".join(context_parts) # 4. 生成答案 prompt = f"""基于以下 wiki 内容回答问题,并在答案中标注引用的页面名。 {context} 问题:{q.question} """ answer = call_llm(prompt) return {"answer": answer, "sources": [h["name"] for h in hits]}这个服务跑起来后,前端可以用任何东西接,Obsidian 插件、网页、命令行都行。热词里cursor连接dify知识库的思路类似,只不过我们连的是自己的 wiki 服务。
5. 常见问题与排查技巧实录
5.1 编译质量问题的排查
问题一:wiki 页面内容空洞,全是套话。
这是最常见的。原因通常是素材块本身信息量不足,或者 prompt 没约束好。排查方法:先看素材块,如果块里只有标题没有正文,那 LLM 也变不出内容。解决方法是回到预处理阶段,检查分块是不是切太碎了。如果素材没问题,那就是 prompt 需要加 few-shot 示例,给模型看一个“好页面”长什么样。
问题二:同一主题被拆成多个页面。
主题归并的阈值设高了。把相似度阈值从 0.75 降到 0.65 试试,或者让 LLM 做二次归并——把生成的页面标题列表再喂给 LLM,让它判断哪些应该合并。
问题三:关联主题链接指向不存在的页面。
关系抽取时 LLM 会幻觉出不存在的主题名。解决方法是生成链接后做一次校验,只保留在页面列表里真实存在的链接。这个校验用简单的字符串匹配就行。
5.2 查询效果问题的排查
问题四:用户问的问题命不中任何页面。
先看向量检索的 top-10 里有没有相关页面。如果有但没进 top-3,说明 embedding 模型对这个问题领域不敏感,考虑换模型或加关键词检索做混合。如果 top-10 里都没有,说明知识库里确实没有这个知识,或者编译时这个主题被漏掉了。前者是正常情况,后者要检查主题归并的覆盖率。
问题五:答案里引用了页面但内容对不上。
这是上下文组装的问题。检查拼进 prompt 的页面内容是不是被截断了。我遇到过页面太长被 token 限制截掉后半段,导致模型只看到概述没看到步骤。解决方法是组装时按段落优先级截断,优先保留概述和关键步骤,注意事项和关联主题可以砍。
问题六:多跳问题还是答不好。
检查关系图有没有正确建立。我早期版本关系抽取做得很粗糙,只抽了“相关”一种关系,导致多跳扩展时带出一堆无关页面。后来改成区分“依赖”“前置”“对比”“包含”四种关系,并且给每种关系设了不同的扩展权重,多跳准确率明显提升。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查动作 | 解决方向 |
|---|---|---|---|
| 页面空洞 | 素材碎/prompt 弱 | 看素材块完整性 | 调分块/加 few-shot |
| 主题重复 | 归并阈值高 | 看相似度分布 | 降阈值/二次归并 |
| 链接失效 | LLM 幻觉 | 校验链接存在性 | 加后置校验 |
| 命中率低 | embedding 不匹配 | 看 top-10 召回 | 换模型/混合检索 |
| 答案截断 | token 超限 | 看组装后长度 | 按优先级截断 |
| 多跳不准 | 关系图粗糙 | 看关系类型 | 细分关系+权重 |
| 编译太慢 | 全量重跑 | 看脏块比例 | 上增量编译 |
| 成本太高 | 大模型编译 | 看 token 消耗 | 换小模型编译 |
5.4 几条踩坑心得
第一条,别追求一次编译完美。LLM 编译出来的页面,第一版能有 70 分就不错了。我的做法是先跑通全流程,然后人工审阅 top-20 高频页面,手动修正,再让 LLM 学习修正后的风格重新编译其他页面。迭代两三轮,质量就上来了。
第二条,prompt 里的格式约束要狠。我试过温和地说“请尽量按以下格式”,结果模型经常自由发挥。后来改成“必须严格按以下格式,不得添加任何额外章节”,输出稳定性大幅提升。对编译任务,约束越硬越好。
第三条,保留原始块和编译页面的映射关系。每个 wiki 页面记录它由哪些块编译而来,这样用户质疑答案时,你能一路追溯到原始文档。这个映射在调试时也很有用,页面内容不对,顺着映射就能找到是哪个块的问题。
第四条,小模型编译 + 大模型问答是性价比最优解。热词里卡帕西的知识库可以用小模型做吗这个问题,我的答案是可以,而且应该。编译用 7B 到 14B,问答用 32B 或 API 大模型,整体成本能降一半以上,效果损失很小。
6. 知识编译的边界与我的实际体会
LLM Wiki 不是银弹,它有明确的适用边界。我踩过的最大一个坑,是试图用它处理一份高度结构化的 API 文档。结果 LLM 把原本清晰的参数表编译成了散文,反而丢失了精度。后来我改成对这类文档不做编译,直接保留原始结构做检索,效果更好。所以判断标准很简单:知识是叙述性的、经验性的、需要串联的,适合编译;知识是结构化的、精确的、查表式的,适合直接检索。
另一个体会是,知识编译的价值会随着知识规模增长而放大。几百个块的时候,传统 RAG 和 LLM Wiki 差别不大;到了几千个块,传统 RAG 的召回噪声明显上升,而 LLM Wiki 因为提前做了归并和关联,查询质量依然稳定。所以如果你只是搭个小笔记库,不用折腾这套;但如果你要维护一个持续增长的知识体系,编译思路值得投入。
最后分享一个我最近在试的扩展方向:把编译产物反过来喂给 Agent 做工具。每个 wiki 页面不只是一个知识单元,还可以挂上对应的操作脚本。比如“Ollama 部署”页面挂一个部署脚本,“数据清洗”页面挂一个清洗函数。Agent 遇到任务时先检索 wiki 页面,再调用页面挂载的工具执行。这样知识库就从“问答”进化到了“执行”,热词里llm powered autonomous agents、agentic rag说的就是这个方向。我目前跑通了原型,效果比纯问答有意思得多,等稳定了再单独写一篇。