ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LLM Wiki:用Markdown沉淀RAG知识资产,MCP协议实战指南

LLM Wiki:用Markdown沉淀RAG知识资产,MCP协议实战指南 1. 从检索完就丢到沉淀成资产LLM Wiki 要解决的真问题做过 RAG 项目的人大概都有过这种体验向量库搭好了切块策略调了又调召回率看着还行但用着用着就发现一个尴尬的事实——每次问答产生的那些高质量结论全都随着对话窗口一关就消失了。下次遇到类似问题模型还得从头检索、重新拼凑、再生成一遍。这就像你雇了个记忆力只有七秒的助理每次都得把背景重新讲一遍。LLM Wiki 这个开源项目瞄准的就是这个痛点。它的核心思路非常朴素但有效把 RAG 的检索结果和生成内容自动沉淀成结构化的 Markdown 知识页面让一次性的问答变成可复用、可维护、可版本控制的知识资产。关键词里的 LLM Wiki、RAG、Markdown、TypeScript、MCP 五个词基本勾勒出了它的技术轮廓——用 TypeScript 写的、以 Markdown 为存储格式、通过 MCP 协议对外提供能力、服务于 RAG 场景的知识库工具。它适合谁我认为有三类人值得认真看看。第一类是正在做 RAG 应用但苦于知识无法沉淀的开发者尤其是那些用 Dify、FastGPT 之类平台搭了原型、却发现知识管理一团乱麻的人。第二类是习惯用 Markdown 管理个人知识库的技术人比如 Obsidian、Logseq 的重度用户他们天然理解知识要能被自己掌控这件事的价值。第三类是想了解 MCP 协议实际落地场景的工程师因为 LLM Wiki 本身就是一个很好的 MCP Server 实践样本。需要先说明的是这个项目目前还处于比较早期的阶段社区讨论里提到的 karpathy llm wiki、卡帕西 llm wiki 这些热词更多是概念层面的呼应——Karpathy 一直倡导用纯文本管理知识的理念而 LLM Wiki 可以看作是这个理念在 RAG 时代的一种工程化尝试。下面我会从设计动机、核心机制、实操落地、踩坑经验几个维度把这个项目拆开讲透。2. 为什么是 Markdown 而不是向量库存储格式背后的取舍逻辑2.1 向量库的黑盒困境与 Markdown 的白盒优势大多数 RAG 项目的默认存储是向量数据库Chroma、Milvus、Qdrant 这些。向量库的好处是检索快、语义匹配强但它有个致命问题你没法直接阅读和编辑里面的内容。当模型召回了一段错误信息你想修正它只能找到原始文档重新切块重新嵌入整个链路又长又重。LLM Wiki 选择 Markdown 作为知识载体本质上是把知识的所有权还给了人。每个知识页面就是一个.md文件你可以用任何编辑器打开、修改、diff、git commit。这带来的好处是连锁的可审计模型生成了什么、基于哪些来源全部落在文件里一目了然可维护发现错误直接改文本不需要重新嵌入可迁移不绑定任何向量库厂商文件在本地随时能搬走可协作Markdown 天然适合 git 工作流多人协作有成熟的冲突解决机制我自己的经验是凡是需要长期维护的知识库存储格式的可读性比检索性能更重要。检索性能可以靠索引优化补但格式选错了后期迁移成本极高。2.2 Markdown 语法在知识库场景的几个关键细节既然选了 Markdown就得把它的语法用到位。这里有几个在知识库场景下特别容易踩的坑我结合实际使用说一下。换行问题。Markdown 里单个换行默认不生效必须行尾加两个空格或者空一行。这在写知识条目时很烦因为知识条目往往是短句列表。我的做法是统一用空行分隔段落列表项之间不空行靠-符号本身区分。如果你的渲染器支持 GFMGitHub Flavored Markdown那直接换行也能生效但为了兼容性还是建议显式处理。表格转换。知识库里经常需要把 Markdown 表格导出成 Excel 做进一步分析。这里推荐用pandoc做转换命令很简单pandoc knowledge.md -o knowledge.xlsx但要注意pandoc 对复杂表格合并单元格、嵌套列表支持有限如果表格结构复杂建议先用脚本把 Markdown 表格解析成 CSV再导入 Excel。Python 的markdown库配合pandas可以做到import pandas as pd from markdown import markdown import re def md_table_to_df(md_text): # 提取第一个表格 lines md_text.split(\n) table_lines [l for l in lines if l.strip().startswith(|)] # 去掉分隔行 table_lines [l for l in table_lines if not re.match(r^\|[\s\-:|]\|$, l.strip())] rows [[c.strip() for c in l.strip().strip(|).split(|)] for l in table_lines] return pd.DataFrame(rows[1:], columnsrows[0])图片路径。知识库里的图片建议用相对路径并且统一放在assets/目录下。绝对路径在迁移时会全部失效而相对路径只要保持目录结构就能正常工作。如果知识库要发布到网页可以用baseurl配置项来统一处理路径前缀——不过要注意TypeScript 7.0 里baseurl选项已经标记为弃用新的项目建议用paths配置替代。2.3 知识页面的结构约定LLM Wiki 生成的每个知识页面我建议遵循一个固定的结构模板这样后期检索和维护都方便。我的模板是这样的# 知识标题 ## 元信息 - 来源: [原始文档链接或标识] - 生成时间: 2024-XX-XX - 置信度: 高/中/低 - 标签: #tag1 #tag2 ## 核心结论 一段话概括 ## 详细内容 展开论述 ## 相关链接 - [[关联知识页面1]] - [[关联知识页面2]]这个结构的好处是元信息部分可以被程序解析用来做过滤和排序核心结论部分适合快速浏览详细内容部分保留完整上下文。[[...]]这种双链语法是 Obsidian 的风格如果你不用 Obsidian可以换成普通的 Markdown 链接。3. TypeScript 技术栈的选型考量与版本兼容陷阱3.1 为什么用 TypeScript 而不是 PythonRAG 领域 Python 是绝对主流LangChain、LlamaIndex 都是 Python 生态。LLM Wiki 选 TypeScript乍看有点反直觉但细想有它的道理。第一MCP 协议的原生支持。MCPModel Context Protocol是 Anthropic 推出的协议官方 SDK 对 TypeScript 的支持非常完善。如果你的工具要作为 MCP Server 被 Claude Desktop、Cursor 这类客户端调用用 TypeScript 写是最顺的路径。第二前端集成方便。知识库最终往往要有个可视化界面TypeScript 全栈可以复用类型定义前后端联调成本低。热词里提到的 React TypeScript、Vue 类型工具都是这个生态的一部分。第三类型安全对知识结构化有帮助。知识页面的元信息、标签、关联关系用 TypeScript 的 interface 定义出来编译期就能发现结构错误比 Python 的运行时校验更早暴露问题。当然代价是生态不如 Python 丰富。如果你要做复杂的文本嵌入、向量计算可能还是得调 Python 服务。LLM Wiki 的定位是知识管理层把重计算的部分交给外部服务自己专注在知识的组织、存储、检索接口上这个分工是合理的。3.2 TypeScript 7.0 弃用选项的应对热词里反复出现baseurl已弃用、moduleresolutionnode10已弃用这些提示说明不少人在升级 TypeScript 版本时遇到了兼容问题。这里我把常见的几个坑和解决方案整理一下。弃用选项替代方案影响范围baseUrlpaths配合rootDir路径别名解析moduleResolution: node10moduleResolution: bundler或node16模块解析策略importsNotUsedAsValuesverbatimModuleSyntax类型导入处理preserveValueImportsverbatimModuleSyntax值导入保留以baseUrl为例老项目里常见这样的配置{ compilerOptions: { baseUrl: ./src, paths: { /*: [*] } } }新版本要改成{ compilerOptions: { rootDir: ./src, paths: { /*: [./src/*] } } }注意paths里的路径要相对于tsconfig.json所在目录而不是相对于rootDir。这个改动看起来小但如果你项目里大量用了/别名改错了会导致全项目报错。还有一个坑是vue-tsc和 TypeScript 版本的匹配。热词里提到vue-tsc: ^1.8.27配typescript: ^5.3.3这个组合是能正常工作的。但如果你把 TypeScript 升到 5.5 以上vue-tsc也得跟着升到 2.x否则会出现类型工具不兼容的报错。我的建议是Vue 项目里锁定 TypeScript 版本不要盲目追新等vue-tsc官方声明支持了再升。3.3 Electron 打包时的类型检查陷阱如果 LLM Wiki 要做成桌面应用Electron 打包是常见需求有个坑必须提前知道Electron 的主进程和渲染进程用的是不同的 TypeScript 配置。主进程跑在 Node 环境渲染进程跑在浏览器环境lib和types配置不一样。常见错误是把两个进程的代码放在同一个tsconfig.json下编译结果要么主进程报document is not defined要么渲染进程报require is not defined。正确做法是拆成三个配置tsconfig.json # 基础配置被下面两个继承 tsconfig.main.json # 主进程lib: [ES2022], types: [node] tsconfig.renderer.json # 渲染进程lib: [ES2022, DOM], types: []打包时用electron-builder的话还要注意files字段的配置确保编译产物和资源文件都被正确包含。我踩过的坑是assets/目录没加进去结果打包后知识库的图片全部 404。4. MCP 协议接入让知识库变成模型的外挂大脑4.1 MCP 到底是什么和 RAG 什么关系热词里 mcp、mcp协议、mcp是什么、rag和mcp区别这几个词出现频率很高说明很多人对这两个概念的关系还不太清楚。我用一句话概括RAG 解决的是模型不知道什么的问题MCP 解决的是模型能做什么的问题。RAG 是检索增强生成模型在回答前先去知识库捞相关内容然后基于捞到的内容生成答案。MCP 是模型上下文协议它定义了一套标准接口让模型能调用外部工具、读取外部资源。两者不是替代关系而是互补关系。LLM Wiki 同时涉及两者它本身是一个知识库RAG 的存储层同时通过 MCP Server 的形式对外暴露能力让模型能主动查询、写入知识。这种设计的好处是模型不再只是被动地被检索而是可以主动地去查、去记。4.2 实现一个最小可用的 MCP ServerMCP Server 的核心是定义工具tools和资源resources。工具是模型可以调用的函数资源是模型可以读取的数据。下面是一个最小示例展示如何把知识库的查询和写入暴露成 MCP 工具import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; const server new Server( { name: llm-wiki, version: 0.1.0 }, { capabilities: { tools: {}, resources: {} } } ); // 定义工具列表 server.setRequestHandler(tools/list, async () ({ tools: [ { name: search_knowledge, description: 在知识库中搜索相关内容, inputSchema: { type: object, properties: { query: { type: string, description: 搜索关键词 }, limit: { type: number, description: 返回条数, default: 5 } }, required: [query] } }, { name: write_knowledge, description: 向知识库写入一条新知识, inputSchema: { type: object, properties: { title: { type: string }, content: { type: string }, tags: { type: array, items: { type: string } } }, required: [title, content] } } ] })); // 处理工具调用 server.setRequestHandler(tools/call, async (request) { const { name, arguments: args } request.params; if (name search_knowledge) { const results await searchKnowledge(args.query, args.limit); return { content: [{ type: text, text: JSON.stringify(results) }] }; } if (name write_knowledge) { await writeKnowledge(args.title, args.content, args.tags); return { content: [{ type: text, text: 写入成功 }] }; } throw new Error(Unknown tool: ${name}); }); const transport new StdioServerTransport(); await server.connect(transport);这段代码的关键点在于inputSchema的定义它用的是 JSON Schema 格式模型会根据这个 schema 来构造调用参数。schema 写得越清晰模型调用越准确。我见过很多 MCP Server 的 schema 写得很模糊结果模型老是传错参数调试半天才发现是 schema 描述不清楚。4.3 在 Claude Desktop 和 Cursor 里配置 MCP Server写好了 Server接下来要在客户端里配置。Claude Desktop 的配置文件在~/Library/Application Support/Claude/claude_desktop_config.jsonmacOS或%APPDATA%\Claude\claude_desktop_config.jsonWindows{ mcpServers: { llm-wiki: { command: node, args: [/path/to/llm-wiki/dist/index.js], env: { WIKI_PATH: /path/to/your/wiki } } } }Cursor 的配置类似在设置里找到 MCP 相关选项填入相同的配置。配置完重启客户端如果一切正常你就能在对话里让模型直接查询和写入知识库了。这里有个实操经验先用npx modelcontextprotocol/inspector调试 Server确认工具能被正确列出和调用再去配置客户端。直接配客户端的话出错了很难定位是 Server 的问题还是配置的问题。Inspector 会给你一个可视化的界面能看到所有工具、资源、以及调用日志。5. RAG 切块策略与知识沉淀的实操细节5.1 切块粒度对知识质量的影响RAG 切块chunking是个老生常谈的话题但在知识沉淀这个场景下切块策略需要重新考虑。传统 RAG 的切块是为了检索块可以小、可以碎只要召回时能拼起来就行。但 LLM Wiki 的切块是为了生成可独立阅读的知识页面块太小会导致页面内容不完整块太大又会导致页面主题不聚焦。我的经验是按一个完整知识点来切而不是按固定 token 数来切。具体判断标准是如果这段内容单独拿出来能不能回答一个明确的问题。能就是一个合格的块不能就说明切错了。实际操作中我会先用固定长度切一版然后人工过一遍把明显被切断的知识点合并回去。这个过程很费时间但一次投入长期受益。热词里提到的 rag切块、rag详解讲的都是这个层面的东西但大多数教程只讲技术不讲判断标准这是不够的。5.2 从检索结果到知识页面的转换逻辑LLM Wiki 的核心动作是检索 → 生成 → 沉淀。这个转换过程有几个关键决策点。第一什么时候触发沉淀。不是每次问答都值得沉淀否则知识库会被低质量内容淹没。我的做法是设置一个沉淀阈值只有当模型生成的答案满足以下条件之一时才沉淀——用户显式要求保存、答案被用户采纳比如点了赞、或者答案引用了多个来源且逻辑自洽。第二沉淀时保留多少上下文。我的建议是保留问题 答案 来源三要素。问题帮助理解知识的适用场景答案是要沉淀的核心内容来源保证可追溯。不要保留完整的对话历史那会让页面变得冗长且难以维护。第三如何处理重复知识。同一个知识点可能被多次检索和生成如果每次都新建页面知识库会迅速膨胀。我的做法是写入前先做一次相似度检查如果已有页面相似度超过阈值比如 0.85就更新已有页面而不是新建。更新时保留新旧两个版本的内容用## 历史版本小节记录这样知识的演进过程也能被追溯。5.3 知识库的目录组织与标签体系知识页面多了之后目录组织就成了问题。我试过几种方案最后稳定下来的结构是这样的wiki/ ├── index.md # 总索引自动生成 ├── assets/ # 图片等资源 ├── by-topic/ # 按主题分类 │ ├── rag/ │ ├── mcp/ │ └── typescript/ ├── by-date/ # 按日期归档 │ └── 2024-12/ └── inbox/ # 待整理by-topic是主要检索入口by-date用于回顾和审计inbox是临时存放区新生成的知识先放这里人工确认后再移到正式目录。这个收件箱机制很重要它让自动生成和人工审核解耦不会因为自动写入而污染主知识库。标签体系我建议控制在两级以内比如#rag/chunking、#mcp/server。层级太深会导致标签爆炸检索时反而找不到。标签的命名要统一不要一会儿用中文一会儿用英文一会儿单数一会儿复数。6. 实际部署中踩过的坑与排查链路6.1 MCP Server 启动失败从日志到根因的完整排查我第一次部署 LLM Wiki 的 MCP Server 时客户端一直显示连接失败没有任何有用信息。排查过程记录如下希望能帮你少走弯路。第一步确认 Server 能独立运行。直接在终端执行node dist/index.js如果报错说明是代码问题如果卡住不动说明在等 stdin 输入这是正常的MCP 用 stdio 通信。这一步能排除掉大部分低级错误。第二步用 Inspector 连接。执行npx modelcontextprotocol/inspector node dist/index.js如果 Inspector 能列出工具说明 Server 本身没问题问题在客户端配置。如果 Inspector 也连不上那就是 Server 的 transport 配置有问题。第三步检查客户端配置的路径。这是最常见的坑配置文件里写的路径是相对路径但客户端的工作目录不是你以为的那个。一律用绝对路径能省掉 90% 的路径问题。第四步检查环境变量。如果 Server 依赖WIKI_PATH这类环境变量确认客户端配置的env字段正确传递了。有些客户端对环境变量的处理有 bug可以改成在代码里读配置文件来绕过。第五步看客户端日志。Claude Desktop 的日志在~/Library/Logs/Claude/下Cursor 的日志在开发者工具的控制台里。日志里通常会有 Server 的 stderr 输出这是定位问题的关键。我最后发现的问题是Server 代码里用了console.log输出调试信息而 MCP 的 stdio 通信要求 stdout 只能用于协议消息console.log会污染协议流导致解析失败。所有调试输出必须用console.error这个坑很隐蔽因为单独运行 Server 时看起来一切正常。6.2 知识页面写入乱码与编码问题另一个坑是文件编码。Windows 环境下默认编码可能是 GBK而知识库统一用 UTF-8结果写入的中文全是乱码。解决方案是在读写文件时显式指定编码import { readFile, writeFile } from fs/promises; // 读取时指定编码 const content await readFile(filePath, utf-8); // 写入时指定编码 await writeFile(filePath, content, utf-8);如果已经产生了乱码文件可以用iconv批量转换# 把 GBK 编码的文件转成 UTF-8 iconv -f GBK -t UTF-8 broken.md fixed.md批量处理的话写个脚本遍历目录find wiki/ -name *.md -exec sh -c iconv -f GBK -t UTF-8 $1 $1.tmp mv $1.tmp $1 _ {} \;6.3 检索质量下降的隐性原因用了一段时间后我发现检索质量在下降但代码没改过。排查后发现是知识库膨胀导致的早期只有几十个页面时简单的关键词匹配就能召回准确页面涨到几百个后关键词匹配的噪声就大了。解决方案是引入两阶段检索先用关键词粗筛出候选集再用嵌入模型做精排。嵌入模型可以用本地的比如xenova/transformers跑 ONNX 模型也可以用 API。本地的好处是免费且隐私安全代价是首次加载模型慢一点。import { pipeline } from xenova/transformers; const embedder await pipeline(feature-extraction, Xenova/all-MiniLM-L6-v2); async function getEmbedding(text: string): Promisenumber[] { const output await embedder(text, { pooling: mean, normalize: true }); return Array.from(output.data); }这个模型只有几十 MB跑在本地很快对中文的支持也还行。如果你的知识库以中文为主可以考虑换成Xenova/paraphrase-multilingual-MiniLM-L12-v2多语言支持更好。7. 知识资产化的长期维护思路7.1 定期审计与知识保鲜知识库最大的敌人不是技术问题是内容腐化。技术方案会过时API 会变更今天正确的结论明天可能就错了。我给自己定的规矩是每季度做一次知识审计重点检查三类页面标注了置信度低的、超过半年没更新的、被引用次数最多的。审计的方式很简单随机抽 20 个页面逐个验证内容是否还准确。不准确的要么更新要么标记为已过时并保留历史记录也有价值。这个过程很枯燥但不做的话知识库会慢慢变成看起来很大但不敢信的状态。7.2 用 Git 管理知识演进知识库用 Git 管理好处不只是版本控制更重要的是变更可追溯。每次知识更新都对应一个 commitcommit message 写清楚为什么改半年后回头看能快速理解当时的判断依据。我的 commit 规范是这样的knowledge: 更新 RAG 切块策略的结论 - 原结论基于 2024-06 的测试已过时 - 新结论基于 2024-12 的复测切块粒度从 512 调整为 256 - 来源: [测试报告链接]配合git log --follow可以追踪单个知识页面的完整演进历史这比任何文档工具都可靠。7.3 知识库的对外输出沉淀下来的知识最终要能输出才有价值。除了通过 MCP 被模型调用我还会定期把知识库导出成静态站点用mkdocs或vitepress生成可浏览的文档。这样非技术同事也能查阅知识库从个人工具变成团队资产。导出时要注意 Markdown 语法的兼容性。比如[[双链]]语法在标准 Markdown 渲染器里不生效需要预处理成普通链接。表格的复杂格式在不同渲染器里表现也不一样导出前最好在目标渲染器里预览一遍。热词里提到的 markdown preview mermaid support、vscode markdown 插件都是提升编辑体验的工具。我的 VSCode 配置里装了 Markdown All in One 和 Mermaid Preview写知识页面时能实时预览效果效率提升明显。但要注意Mermaid 图表在导出静态站点时可能需要额外的渲染插件不是所有平台都原生支持。7.4 关于知识资产的一点个人体会用了大半年 LLM Wiki 这套思路我最大的感受是知识的价值不在于存了多少而在于能不能被信任和复用。一个存了十万条但没人敢用的知识库不如一个存了一千条但每条都经过验证的知识库。自动生成的知识必须经过人工确认才能进入主库这个人工确认的环节不能省。它看起来降低了效率实际上是在为知识的可信度做投资。我现在的流程是模型生成的知识先进入inbox/我每周花半小时过一遍确认的移到正式目录不确定的留在 inbox 继续观察明显错误的直接删掉。这个节奏不累但能保证主库的质量。另外不要追求知识库的大而全。我见过有人想把所有文档都塞进去结果检索时噪声太大反而不好用。知识库应该聚焦在高频使用、需要长期记忆的内容上一次性的、临时的信息用完就丢没必要沉淀。判断标准很简单这个问题我半年后还会再问吗会就沉淀不会就算了。
返回列表