1. 为什么“RAG 结果”需要变成“知识资产”1.1 从“能查到”到“能维护”的鸿沟做过 RAG 项目的人大概都有这种体验搭一个能跑通的原型一两天就够了。向量库选一个、文档切一切、embedding 一算、检索一拼、丢给大模型生成Demo 效果看着还挺唬人。但真正上线跑一段时间问题就全冒出来了——同一个问题今天答得对、明天答得离谱知识库里塞了一堆过期文档没人清理用户反馈“答案不对”的时候你根本不知道是切块切坏了、检索没召回、还是模型自己编的。这就是 RAG 最尴尬的地方它天生是个“黑盒流水线”中间产物全是临时的、不可见的、不可维护的。检索出来的 chunk 用完就扔生成的答案没有溯源知识更新了也没人知道哪些缓存失效了。你手里握着的是一堆“查询结果”而不是一份“知识资产”。LLM Wiki 这个项目想解决的恰恰就是这个断层。它的核心思路很朴素但很关键把 RAG 每次检索、生成、验证的中间结果沉淀成一份人类可读、可编辑、可版本管理的 Markdown 知识库。换句话说它不满足于让模型“答对一次”而是要让这次答对的过程变成一份可以持续迭代的资产。1.2 LLM Wiki 到底是个什么东西先把定位说清楚。LLM Wiki 是一个基于 TypeScript 构建的开源工具它把 RAG 的检索结果、LLM 的生成内容、以及人工的修订意见统一收敛到一个 Markdown 驱动的 Wiki 结构里。你可以把它理解成“RAG 的输出层 知识管理层”的结合体。它和传统 RAG 的区别我用一个表格说清楚维度传统 RAGLLM Wiki中间产物临时 chunk用完即弃持久化 Markdown 页面知识更新重新灌库全量重算增量编辑diff 可追溯人工介入几乎无法介入直接编辑 Markdown溯源能力弱靠 metadata 拼强页面即证据协作方式工程师改代码任何人改文档与 LLM 交互单向查询双向可回写这里有个关键点值得展开为什么选 Markdown 作为知识的载体。这不是随便选的。Markdown 有几个天然优势——纯文本所以能进 Git 做版本管理结构简单所以人和模型都能轻松解析渲染友好所以非技术同学也能看懂而且它本身就是大模型训练语料里最常见的格式之一模型对 Markdown 的理解和生成质量明显高于其他结构化格式。你让模型输出 JSON它偶尔会漏括号你让它输出 Markdown它稳得多。1.3 谁适合上手这个项目我不太喜欢那种“人人都该学”的套话。LLM Wiki 有明确的适用人群正在做 RAG 落地、被“答案不稳定”折磨的工程师你需要一个能沉淀、能排查、能迭代的中间层。需要多人协作维护知识库的团队产品、运营、客服都能直接编辑 Markdown不用碰代码。想把个人知识库做成长期资产的人你喂给模型的资料不该是一次性消耗品。对 MCP 协议感兴趣、想找个真实项目练手的开发者LLM Wiki 天然适合包装成 MCP Server。反过来说如果你只是想快速搭个问答 Demo 玩一玩那这个项目可能有点“重”——它解决的是长期维护问题不是快速出效果问题。2. 核心设计思路拆解为什么是 Markdown MCP TypeScript2.1 知识资产化的三层结构LLM Wiki 的设计里我观察到它其实把知识分成了三层这个分层是理解整个项目的钥匙第一层是原始素材层。就是你喂进去的 PDF、网页、文档、代码注释等等。这一层是只读的、不可变的相当于“事实来源”。第二层是 Wiki 页面层。这是项目的核心创新点。RAG 检索出来的内容不是直接丢给模型就完事而是先被组织成一个结构化的 Markdown 页面——包含标题、摘要、正文、引用来源、相关链接、最后更新时间等字段。这个页面是人类可读、可编辑的。第三层是查询与回写层。当用户提问时系统检索的不只是原始素材还包括已经沉淀下来的 Wiki 页面。生成答案后如果产生了新的、有价值的知识可以回写成新的 Wiki 页面或更新已有页面。这个三层结构解决了一个根本问题知识不再是“检索时临时拼凑的”而是“持续积累的”。每一次查询都在为知识库做贡献而不是白白消耗算力。2.2 为什么用 TypeScript 而不是 Python这是个很有意思的选型问题。RAG 领域 Python 是绝对主流LangChain、LlamaIndex 全是 Python 生态。LLM Wiki 偏偏选了 TypeScript我琢磨了一下理由大概有这么几条第一前端集成天然顺畅。Wiki 这种东西最终是要给人看的要渲染、要编辑、要预览。TypeScript 直接和 React、Vue 这些前端框架无缝衔接不用搞 Python 后端 JS 前端的两套语言割裂。你在一个仓库里就能把“知识处理”和“知识展示”全搞定。第二MCP 生态对 TypeScript 友好。MCPModel Context Protocol是让模型和外部工具、数据源交互的协议。它的官方 SDK 对 TypeScript 支持很好写一个 MCP Server 用 TS 是相当自然的选择。LLM Wiki 把自己包装成 MCP Server 之后任何支持 MCP 的客户端都能直接调用它的知识库能力。第三类型系统对知识结构建模有帮助。知识页面有固定的字段结构用 TypeScript 的 interface 和 type 来描述编译期就能发现字段缺失、类型错误。这在维护一个长期演进的知识库时价值很大。当然选 TS 也有代价——Python 那边成熟的 embedding、向量检索库TS 这边要么用 API 调用要么用 WASM 版本性能和生态都稍逊。所以 LLM Wiki 的定位很聪明它不重复造向量检索的轮子而是专注做“知识管理层”把检索交给专门的向量库或 API。2.3 MCP 在这里扮演什么角色很多人搞不清 RAG 和 MCP 的区别我借这个项目说清楚。RAG 是一种“知识注入”的技术它解决的是“模型不知道某件事我临时把相关资料塞给它”的问题。核心动作是检索 拼接上下文。MCP 是一种“能力暴露”的协议它解决的是“模型想用某个工具或数据源怎么标准化地调用”的问题。核心动作是定义工具接口 标准化调用。两者不是替代关系而是互补。LLM Wiki 的巧妙之处在于它把 RAG 的能力通过 MCP 协议暴露出去。也就是说任何支持 MCP 的 AI 客户端比如各种 AI 编程助手、桌面 AI 应用都能把 LLM Wiki 当成一个“知识库工具”来调用。你问它问题它内部走 RAG 流程但对外表现成一个标准的 MCP 工具。这样一来知识库就不再绑定某个特定的应用了。今天你在 A 工具里用明天换 B 工具只要都支持 MCP知识库照样能用。这是“资产化”的另一个维度——资产要能跨平台复用才叫资产。2.4 增量更新与版本管理传统 RAG 最让人头疼的是更新。文档改了一个字你得重新切块、重新 embedding、重新灌库全量重算。LLM Wiki 因为把知识存成了 Markdown 文件更新就变成了“改文件”这么简单。具体来说它依赖 Git 做版本管理。每次知识更新就是一次 commit谁改的、改了什么、为什么改全在 commit message 里。想回滚git revert 就行。想对比两个版本的知识差异git diff 一目了然。这个设计还有个隐藏好处知识库的演进历史本身就是一份宝贵的训练数据。你能看到知识是怎么被修正的、哪些地方容易出错、人工修订的模式是什么。这些信息对优化 RAG 流程极有价值。3. 核心细节解析与实操要点3.1 Wiki 页面的数据结构设计一个设计良好的 Wiki 页面字段不能太多也不能太少。太多了维护成本高太少了溯源能力弱。根据常见实践LLM Wiki 的页面结构大概长这样--- title: 什么是向量检索 tags: [RAG, 检索, 基础概念] source: [doc-001.pdf, web-2024-03-15] created: 2024-03-15 updated: 2024-04-02 confidence: 0.85 related: [什么是embedding, 切块策略] --- ## 摘要 向量检索是把文本转成高维向量通过计算向量相似度来找到语义相近内容的技术。 ## 正文 这里是主体内容由 LLM 生成 人工修订 ## 引用来源 - doc-001.pdf 第 12 页 - web-2024-03-15 第三节 ## 修订记录 - 2024-03-15 初始生成 - 2024-04-02 修正了相似度计算的描述这里有几个字段值得说道confidence 字段这是模型对自己生成内容的置信度。低于某个阈值比如 0.7的页面会被标记出来提示人工复核。这个机制很实用相当于给知识库加了个“质量看板”。source 字段记录这条知识是从哪些原始素材来的。这是溯源的关键。当有人质疑某条知识时你能立刻定位到源头。related 字段页面之间的关联。这让知识库不是一堆孤立的页面而是一张网。检索时可以通过关联扩展召回生成时也能提供更丰富的上下文。注意frontmatter 的字段设计要克制。我见过有人往里塞几十个字段结果没人填、没人看反而成了负担。核心字段控制在 6-8 个以内够用就行。3.2 从 RAG 结果到 Wiki 页面的转换逻辑这是整个项目最核心的一环。RAG 检索出来的是一堆 chunk怎么把它们变成结构化的 Wiki 页面流程大致是这样的检索阶段用户提问系统从向量库召回 top-k 相关 chunk。聚合阶段把召回的 chunk 按来源文档分组同一文档的 chunk 合并。生成阶段把聚合后的内容 用户问题一起丢给 LLM让它生成一个结构化的 Wiki 页面草稿。校验阶段检查生成的页面是否包含幻觉与源 chunk 不符的内容标记可疑部分。落盘阶段把页面写入 Markdown 文件更新索引。这里第三步的 prompt 设计很关键。你不能简单地说“根据这些内容生成一个页面”那样模型会自由发挥。好的 prompt 会明确要求只使用提供的 chunk 中的信息不要引入外部知识每个关键陈述后面标注来源 chunk 的编号如果 chunk 之间有矛盾明确指出并保留两种说法输出严格的 Markdown 格式包含指定的 frontmatter 字段第四步的校验常见做法是用另一个 LLM 调用做“事实核查”——把生成的页面和源 chunk 一起给它让它逐句判断是否有依据。这一步会增加成本但对知识质量至关重要。我的经验是校验这一步不能省尤其是知识库要长期维护的时候。宁可慢一点也不要让错误知识沉淀进去因为错误知识一旦被后续检索引用会污染整个知识网络。3.3 MCP Server 的接口设计把 LLM Wiki 包装成 MCP Server需要暴露哪些工具根据 MCP 协议的常见实践至少要有这几个工具名功能输入输出search_knowledge检索知识库query, top_k相关页面列表get_page获取指定页面page_id页面完整内容create_page创建新页面title, content, source页面 IDupdate_page更新页面page_id, content更新结果list_pages列出所有页面tag?, limit?页面摘要列表这几个工具覆盖了知识库的增删改查。设计时要注意几点search_knowledge 的返回要精简。不要返回整个页面内容只返回标题、摘要、相关度分数。让调用方决定要不要拉取完整内容。这样能省 token也更快。create_page 和 update_page 要有权限控制。不是所有调用方都该有写权限。MCP 协议支持在工具定义里声明权限要求要利用起来。错误处理要清晰。MCP 工具调用失败时返回的错误信息要能让调用方通常是 LLM理解并做出正确反应。比如“页面不存在”和“权限不足”要区分开。3.4 切块策略对知识质量的影响虽然 LLM Wiki 专注知识管理但它的输入质量取决于上游的切块。切块切得不好后面再怎么管理都是垃圾进垃圾出。这里分享几个实操中总结的切块经验按语义切不要按字数切。固定 512 token 切一块是最省事的做法但也是最容易切断语义的。更好的做法是用语义分割——先按段落、标题切如果某段太长再按句子切。Markdown 文档天然有标题层级按标题切是最自然的。保留上下文重叠。相邻 chunk 之间保留 10%-20% 的重叠能避免关键信息正好卡在边界上被切断。代价是存储和计算量增加但值得。给 chunk 加上位置元数据。记录每个 chunk 来自哪个文档、第几节、前后是什么内容。这些元数据在聚合阶段能帮你把 chunk 还原成完整的上下文。表格和代码块要特殊处理。表格被切断基本就废了代码块被切断也没法用。遇到这类结构要么整块保留要么用专门的解析器处理。4. 实操过程与核心环节实现4.1 环境准备与项目初始化假设你要从零搭一个 LLM Wiki 实例环境准备大概是这样# 初始化项目 mkdir llm-wiki cd llm-wiki npm init -y # 安装核心依赖 npm install typescript ts-node types/node npm install modelcontextprotocol/sdk npm install gray-matter # 解析 Markdown frontmatter npm install simple-git # Git 操作 # 初始化 TypeScript 配置 npx tsc --inittsconfig.json里要注意几个配置。现在 TypeScript 5.x 已经弃用了moduleResolution: node10和baseUrl这类老选项TypeScript 7.0 会彻底移除。新项目直接用{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, strict: true, esModuleInterop: true, skipLibCheck: true, outDir: ./dist } }提示如果你在维护老项目看到baseUrl弃用警告别慌。用paths配合moduleResolution: bundler或NodeNext就能替代。baseUrl的移除主要是因为它和现代模块解析方式冲突。4.2 知识页面的读写实现先定义页面的类型interface WikiPage { id: string; title: string; tags: string[]; source: string[]; created: string; updated: string; confidence: number; related: string[]; content: string; }读取页面用gray-matter解析 frontmatterimport matter from gray-matter; import fs from fs/promises; async function readPage(filePath: string): PromiseWikiPage { const raw await fs.readFile(filePath, utf-8); const { data, content } matter(raw); return { id: filePath, title: data.title, tags: data.tags || [], source: data.source || [], created: data.created, updated: data.updated, confidence: data.confidence ?? 0.5, related: data.related || [], content: content.trim(), }; }写入页面时要保证 frontmatter 格式稳定这样 Git diff 才干净async function writePage(page: WikiPage, filePath: string) { const frontmatter { title: page.title, tags: page.tags, source: page.source, created: page.created, updated: new Date().toISOString().split(T)[0], confidence: page.confidence, related: page.related, }; const output matter.stringify(page.content, frontmatter); await fs.writeFile(filePath, output, utf-8); }这里有个细节updated字段每次写入都自动更新但created保持不变。这样你能一眼看出哪些页面是新的、哪些是最近改过的。4.3 检索与聚合的实现检索部分LLM Wiki 通常不自己实现向量检索而是调用外部服务。假设你用一个向量库 APIasync function retrieveChunks(query: string, topK: number 10) { const response await fetch(YOUR_VECTOR_DB_ENDPOINT/query, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ query, top_k: topK }), }); const data await response.json(); return data.matches; // [{ id, score, text, metadata }] }聚合阶段把同一来源的 chunk 合并function aggregateBySource(chunks: any[]) { const grouped new Mapstring, any[](); for (const chunk of chunks) { const source chunk.metadata.source; if (!grouped.has(source)) grouped.set(source, []); grouped.get(source)!.push(chunk); } // 每个来源内部按位置排序 for (const [source, items] of grouped) { items.sort((a, b) a.metadata.position - b.metadata.position); } return grouped; }聚合之后把每个来源的内容拼成一段附上来源标记再交给 LLM 生成页面。4.4 生成与校验的 Prompt 设计生成页面的 prompt 我建议这样写你是一个知识整理助手。请根据以下资料生成一个结构化的知识页面。 要求 1. 只使用资料中的信息不要引入任何外部知识 2. 每个关键陈述后用 [来源N] 标注依据 3. 如果资料之间有矛盾明确指出并保留两种说法 4. 输出格式为 Markdown包含以下部分摘要、正文、引用来源 5. 正文要条理清晰适合作为长期参考 资料 [来源1] ... [来源2] ... 用户问题{query}校验的 prompt请逐句检查以下知识页面判断每个陈述是否有资料支撑。 对于每个陈述输出 - 陈述内容 - 是否有支撑是/否/部分 - 依据的来源编号 最后给出整体置信度0-1。 知识页面 {page_content} 资料 {source_chunks}校验结果里如果发现无支撑的陈述要么删掉要么标记为“待核实”。置信度低于 0.7 的页面在 frontmatter 里标出来提醒人工复核。4.5 MCP Server 的启动与注册MCP Server 的入口大概长这样import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; const server new Server( { name: llm-wiki, version: 1.0.0 }, { capabilities: { tools: {} } } ); server.setRequestHandler(tools/list, async () ({ tools: [ { name: search_knowledge, description: 检索知识库返回相关页面, inputSchema: { type: object, properties: { query: { type: string }, top_k: { type: number, default: 5 }, }, required: [query], }, }, // ... 其他工具 ], })); server.setRequestHandler(tools/call, async (request) { const { name, arguments: args } request.params; switch (name) { case search_knowledge: return { content: [{ type: text, text: await searchKnowledge(args) }] }; // ... 其他工具处理 } }); const transport new StdioServerTransport(); await server.connect(transport);启动后在支持 MCP 的客户端里配置这个 Server 的启动命令就能用了。配置通常是一个 JSON{ mcpServers: { llm-wiki: { command: node, args: [/path/to/llm-wiki/dist/index.js], env: { WIKI_ROOT: /path/to/wiki, VECTOR_DB_ENDPOINT: http://localhost:8000 } } } }5. 常见问题与排查技巧实录5.1 生成内容与源资料不符怎么办这是最常见的问题也是知识库最大的风险。排查思路先看检索质量。如果召回的 chunk 本身就不相关那生成的内容肯定跑偏。检查 embedding 模型是否适合你的领域检查切块是否合理检查 top_k 是否太小。再看 prompt。如果 prompt 里没有明确“只用资料中的信息”模型很容易自由发挥。加上明确的约束并且要求标注来源。最后看校验环节。如果校验没发现问题但人工复核发现了说明校验的 prompt 不够严格。可以让校验模型更“挑剔”一些宁可误报也不要漏报。我踩过的一个坑早期为了省成本跳过了校验环节结果错误知识沉淀进库后来被检索引用生成了更多错误内容形成了“错误放大”。清理起来非常麻烦。校验这一步真的不能省。5.2 知识库越来越大检索变慢怎么办知识库增长到几千个页面后全量检索会明显变慢。几个优化方向分层索引。把页面按标签、主题分成多个子索引检索时先定位子索引再细查。这能大幅减少检索范围。缓存高频查询。统计哪些查询最常出现把结果缓存起来。知识库更新时只失效相关的缓存。定期归档冷页面。长期没被访问、没被更新的页面移到归档区不参与主检索。需要时再手动拉出来。用更高效的向量索引。如果用的是暴力检索换成 HNSW 或 IVF 这类近似索引速度能提升一个数量级代价是召回率略降。5.3 多人协作时的冲突处理多人同时编辑知识库冲突是难免的。Git 能处理文本冲突但知识冲突两个人对同一件事有不同说法需要人工判断。我的做法是在页面里保留“争议”区块。当检测到冲突时不自动合并而是把两种说法都保留标记为“待裁决”。然后通知相关人复核。这样既不丢失信息又能推动问题解决。另外给页面加“负责人”字段很有用。每个页面有明确的负责人冲突时直接找负责人裁决避免扯皮。5.4 常见问题速查表问题现象可能原因排查方向解决思路答案与资料不符检索不准 / prompt 不严检查召回 chunk 相关性优化切块、加严 prompt、启用校验检索变慢索引过大 / 无缓存看查询耗时分布分层索引、加缓存、归档冷页面页面格式混乱frontmatter 不规范检查写入逻辑统一写入函数、加格式校验MCP 调用失败权限 / 路径错误看错误信息检查配置、加权限控制知识更新不生效缓存未失效检查缓存策略更新时主动失效相关缓存置信度普遍偏低校验过严 / 资料质量差抽样人工复核调整校验阈值、清洗源资料5.5 几个实操心得心得一先小范围跑通再扩大规模。别一上来就灌几万篇文档。先用几十篇跑通全流程把切块、生成、校验、检索都调顺了再逐步扩大。规模一大问题会被放大排查成本剧增。心得二把 prompt 当代码管理。prompt 的每次修改都要记录、要测试。我见过太多团队 prompt 改来改去最后没人知道当前用的是哪版。用 Git 管理 prompt每次改动写清楚原因和效果。心得三定期做“知识体检”。每隔一段时间抽样检查知识库的质量——随机抽页面人工判断准确性统计置信度分布看是否有异常检查孤儿页面没有任何关联的页面看是否需要清理或补充关联。心得四别追求 100% 自动化。知识管理这件事人的判断永远不可替代。LLM Wiki 的价值是“降低人工介入的成本”而不是“消除人工介入”。把人工精力集中在高价值环节——比如争议裁决、质量抽检——而不是逐字校对。心得五知识库的元数据比内容更值钱。内容会过时但“这条知识从哪来、谁维护、什么时候更新、置信度多少”这些元数据是长期资产。设计时多花点心思在元数据上回报很高。6. 从 LLM Wiki 延伸出去的几个方向6.1 与 Agentic RAG 的结合Agentic RAG 是最近很热的方向——让 Agent 自主决定检索什么、检索几次、怎么用检索结果。LLM Wiki 天然适合做 Agentic RAG 的“记忆层”。Agent 每次检索、生成、验证的结果都沉淀成 Wiki 页面下次遇到类似问题直接复用不用重新走一遍流程。这能大幅降低 Agent 的推理成本同时让它的“经验”可积累、可审查。6.2 知识图谱化的可能现在的 Wiki 页面之间靠related字段关联是扁平的。如果引入知识图谱把页面里的实体、关系抽出来就能做更智能的检索和推理。比如“A 依赖 BB 被 C 替代”这种关系用图谱表达比用文本表达更清晰。当然这会增加复杂度适合知识规模较大、关系较复杂的场景。6.3 多模态知识的纳入目前 LLM Wiki 主要处理文本。但实际知识库里图片、表格、流程图占比不小。把这些多模态内容也纳入 Wiki 页面用 Markdown 的图片语法、表格语法承载配合多模态模型做理解和生成是很有价值的扩展方向。Markdown 本身对图片和表格的支持就很好扩展起来不算难。6.4 与本地知识库方案的对比市面上有不少本地知识库方案各有侧重。LLM Wiki 的差异化在于“可维护性”和“协议标准化”。它不追求开箱即用的极致体验而是提供一个可定制、可扩展、可长期演进的框架。如果你需要的是一个“能自己掌控、能持续迭代”的知识管理方案它比那些黑盒方案更合适。我在实际使用中最大的体会是知识管理的难点从来不是技术而是习惯。工具再好如果没人愿意维护、没人愿意修订、没人愿意复核知识库很快就会腐烂。LLM Wiki 这类项目的价值是把维护的门槛降到足够低——低到改一个 Markdown 文件、提一个 commit 就能完成——让“维护知识”这件事变得像“写笔记”一样自然。技术能做的就是让正确的行为变得容易让错误的行为变得困难。这一点上LLM Wiki 的方向是对的。
