最近在做一个智能硬件的客服知识库项目。 200 多篇文档,涵盖产品规格、FAQ、植物百科、用户博客。 最初用 Agentic RAG 方案来实现的系统,即给AI几个异构的数据源访问入口,然后让AI自己根据问题决定应该访问什么数据源获取什么数据。 几轮测试验证后发现召回率不及预期。用户问一个稍微复杂的问题,可能涉及到多类文档,LLM 需要同时理解产品参数、种植经验、故障排查三个维度的信息,但 RAG 每次只能捞回几个相关片段,拼不出完整答案。召回是问题,重排也是问题。
本质上看,不是模型不够聪明,是它每次都在从零开始,从零散的知识碎片中拼凑出它们的理解。 这种方式召回经常不稳定,一旦召回做得不好答案就天差地别,甚至可能瞎说。
最近看到了 Karpathy 发的那篇 LLM Wiki,一开始还没怎么注意,但是过了几天再回过头看,有些通透了,似乎事情本该这样做。 人类的知识海洋也是这么经过一代一代人不停地“编译”创造出来的,是靠知识树的构建和剪枝,而不是靠不关联的知识碎片的无脑堆叠。
增量式编译Wiki
4月初,Karpathy 在 GitHub 上发了一个 Gist,标题很朴素:LLM Wiki。抛开争议不说,他的核心洞察非常清晰:
RAG 的问题不是检索不准,而是知识没有积累。每次查询都在重新发现,没有东西被"建设"起来。
他提出的替代方案是:让 LLM 增量构建和维护一个持久化的 Wiki。不是查询时临时拼凑,而是在摄入新源材料时就把知识整合好,更新实体页、修订主题总结、标注矛盾之处。 Wiki 是一个持续复利的产物。而LLM是图书的编辑作者与图书馆的管理员。他必须确保它编译出来的百科是完整的、有目录的、能索引的、能被渐进式发现的。
架构分三层:
- Raw Sources: 是原始材料,不可修改,是真相来源
- Wiki: 是LLM生成并维护的 Markdown 文件集,是检索的入口
- Schema: 是告诉 LLM 该怎么组织 Wiki 的规则文档
人负责选材料、提问题、做判断。 LLM 负责整理、交叉引用、记账。 换句话说,新的知识应该被“编译”一次进入到百科中(交叉查找验证应该放置的章节位置,并且创建好索引目录),而不是每次遇到问题再去拼凑答案并现场执行。
从理念到实践
受这个理念启发,我重构了那个知识库项目,想要验证一下效果是否更好。 架构和 Karpathy 描述的几乎一致,但做了一些适配:
-
Layer 0 是原始数据。从后台 API 拉下来的 JSON,每个条目一个文件,脚本自动抓取,不允许手动修改。这是 single source of truth。
-
Layer 1 是源文件。脚本把 JSON 自动转写成 Markdown,保留结构化元数据(frontmatter),方便后续索引。FAQ、植物百科、博客文章各一个目录,产品规格页因为没有 API,手动维护。
-
Layer 2 是 Wiki。这一层完全由 LLM 生成。它读完 Layer 1 的所有源文件后,综合出主题页、设备页、指南页、对比页。比如一篇“新手入门指南”,背后综合了 5 篇植物百科、4 条 FAQ、3 篇博客文章的信息。
这里有一个重要的设计原则:Agent 是图书馆员,不是作者。Wiki 的每一句话都应该能溯源到 Layer 1 的原始材料。LLM 不创造新知识,它只是把散落的知识整理到正确的书架上。
效果是立竿见影的。以前用户问"度假时植物怎么办",RAG 可能捞到一条关于自动浇水的 FAQ 和一篇不太相关的博客。现在 Wiki 里有一篇专门的 vacation-care 指南,已经把自动浇水、水箱容量、不同设备的续航能力、出发前的准备清单全部综合好了。LLM 直接读这一页就能给出完整回答。
index.md 与人工微调
整个系统的检索入口是一个 index.md 文件。 每个文档一行摘要,带链接。 LLM 先读 index,定位相关页面,再深入阅读。在 200 多个文档的规模下,这种方式比向量检索更可控。
但自动生成的摘要不够好。LLM 生成的索引条目往往太泛,缺少用户真正会搜索的关键词。这个时候从错误的案例中吸取经验是很有帮助的。
这不是什么高深技术,就是最朴素的标注(交给AI来做)。效果显著,几十条覆盖就让检索准确率提升了一大截。
AI 驱动的索引优化脚本,核心逻辑如下:
1. 运行检索测试 → 读取 test-results.json
2. 找出 fileRecall < 100% 的失败 case
3. 对每个 missed file:
a. 读取文件实际内容
b. 读取 index.md 中当前摘要
c. 收集所有涉及该文件的失败问题
d. 调 LLM:根据文件内容 + 失败问题 → 生成增强摘要
4. 直接更新 index.md 中对应行
5. 重跑测试验证
6. 输出优化报告(改了什么、效果如何)Wiki 长大后搜索成为瓶颈
Karpathy 自己也承认,index.md 只在 Wiki 规模较小时够用。当文档超过几百页,你需要真正的搜索引擎。他推荐了一个工具:QMD。
我花了一些时间研究 QMD 的源码。这是 Shopify CEO Tobias 的开源项目,4 个月拿到超过两万 stars。几个关键算法值得展开讲讲。
先探测,再调模型
QMD 的混合搜索管线有 8 个步骤,但第 2 步就藏着一个精妙的优化:Strong-Signal Short-Circuit。
在调用任何 LLM 之前,它先做一次廉价的 BM25 全文搜索。如果最高分结果的得分超过 0.85,并且和第二名的差距超过 0.15,直接跳过 LLM 扩展,省掉模型加载和推理延迟。
直觉很简单:用户搜"JWT auth middleware"这种精确查询时,BM25 已经完美命中了,没必要浪费 LLM 去"语义理解"。但搜"怎么登录"时,BM25 得分低且模糊,这时候才值得调 LLM。
这个模式可以迁移到任何多阶段 AI 系统:在调用重量级模型之前,先用轻量级信号判断是否有必要。这是成本控制的第一原则。
原始查询 + 扩展变体
QMD 的 Query Expansion 是整个系统的灵魂。它用一个自研微调的 1.7B 模型,把用户的自然语言查询扩展成多个结构化搜索指令,然后连同原始查询一起并行检索。原始查询会被赋予 2x 权重,确保用户原意不被扩展稀释。每个扩展指令有不同的类型,对应不同的搜索后端:
lex (关键词变体): 给 BM25 用。把"auth config"扩展成"authentication configuration settings jwt"。
vec (语义重写): 给向量搜索用。把"auth config"重写成"how to configure user authentication settings in the application",更接近文档的实际表述。
hyde (假文档生成): 最巧妙的一路。LLM 先生成一段"假答案",比如"Authentication is configured via the config.yml file. Set the auth_provider field to...",然后用这段假答案去做向量搜索。因为假答案的文风和真文档更接近,cosine similarity 更高,能找到那些和用户查询措辞完全不同但语义相关的文档。
HyDE(Hypothetical Document Embedding)是学术界已有的技巧(Gao et al. 2022),但 QMD 把它和 lex、vec 做成了并行的多路管线,并用 Reciprocal Rank Fusion 融合结果。这个设计让它在各种查询风格下都保持稳定的召回率。
补充几句这些算法的直觉解释,方便不熟悉检索领域的读者理解。
BM25 本质上是 TF-IDF 的升级版。核心逻辑是:一个词在某篇文档中出现得越多(词频 TF),同时在所有文档中出现得越少(逆文档频率 IDF),那这个词对这篇文档就越“特征化”。它还做了词频饱和(出现 10 次和 100 次差别不大)和文档长度归一化(长文档不占便宜)。简化版公式:
其中 k₁ 控制词频饱和速度,b 控制文档长度的惩罚力度。BM25 的优点是快(纯索引查找,无需模型推理),缺点是只看词面匹配,用户搜“怎么登录”找不到写着“authentication flow”的文档。
HyDE 的思路则完全不同。传统向量搜索的问题是,用户查询(短句)和文档(长段落)在向量空间里的文体差异很大,导致 cosine similarity 偏低。HyDE 让 LLM 先生成一段“假答案”,内容可能不准确,但文体和真文档接近。用假答案的向量去搜,就能找到那些用户查询原文“搜不到但其实相关”的文档。
保护精确匹配的融合策略
融合是 RAG 系统最容易出问题的环节。不同搜索引擎的打分不在同一量纲(BM25 分数可能是 0-25,向量搜索是 0-1),没法直接相加。QMD 在这里做了三层防护。
第一层是 RRF(Reciprocal Rank Fusion)。它的解法很优雅:不看分数,只看排名。公式是:
其中 k=60 是平滑常数,rank 是文档在某路搜索结果中的排名,weight 是该路查询的权重(原始查询给 2x,扩展查询给 1x)。所有搜索列表的分数累加,在多路中都排名靠前的文档自然胜出。举个例子:某文档在 BM25 排第 1,在向量搜索排第 5,融合分 = 1/(60+1) + 1/(60+5) = 0.032;另一个文档在 BM25 排第 10,向量排第 2,融合分 = 1/(60+10) + 1/(60+2) = 0.030。第一个胜出,因为它在两路中都排名靠前。
第二层是 Top-Rank Bonus。如果某个文档在任何一个搜索列表中排名第一,额外加 0.05 分;排名第二或第三,加 0.02 分。这防止了“精确匹配被语义噪音稀释”的问题。
第三层是 Position-Aware Blending。Reranker(交叉编码器)虽然更准确,但它可能推翻高置信度的检索结果。QMD 的做法是按排名位置动态调整 RRF 和 Reranker 的混合比例,分三个梯度:
- 排名前 1-3:75% 信任 RRF,25% 信任 Reranker,保护精确命中
- 排名 4-10:60% 信任 RRF,40% 信任 Reranker,逐步过渡
- 排名 11+:40% 信任 RRF,60% 信任 Reranker,让 Reranker 多做判断
其中 α 根据 RRF 排名动态变化(0.75 → 0.60 → 0.40)。
这个设计的洞察是:检索引擎在“高置信区”比 Reranker 更可靠,Reranker 在“低置信区”比检索引擎更有判断力。三级梯度比全局统一权重好得多。
两个理念的交汇
回头看,Karpathy 的 LLM Wiki 和 QMD 解决的是同一个问题的两个面。
Karpathy 解决的是知识的组织方式。把散落的信息编译成结构化的 Wiki,减少 LLM 每次查询的认知负担。这是离线阶段的优化。
QMD 解决的是知识的检索方式。当编译产物足够多,如何精准找到需要的那一页。这是在线阶段的优化。
两者互补。Wiki 降低了检索的难度,因为知识已经被预综合,搜索引擎不需要跨 5 个文档去拼凑答案。QMD 提升了检索的精度,当 Wiki 页面达到几百上千时,混合搜索比 grep 索引文件好几个数量级。
有趣的是,QMD 的 Context System (给路径打层级描述标签) 和 Karpathy 说的 Schema(告诉 LLM 如何理解 Wiki 结构)本质上是同一件事:用少量元数据为 AI 系统提供导航信号。
社区的争议
这套理念不是没有争议。Karpathy 的 Gist 评论区有激烈的反对意见。批评主要集中在三点:Wiki 由 LLM 生成,存在幻觉风险;规模增长后本质上还是需要 RAG;"Wiki"这个词用得不准确,真正的 Wiki 应该是多用户协作的。
这些批评有道理,但我认为不影响核心价值。LLM Wiki 从来就不是要替代 RAG,它是 RAG 的上游。Wiki 是编译产物,RAG(或 QMD 这类混合搜索)是运行时。两者不矛盾。
至于幻觉,Agent 是图书馆员而非作者这个设计原则,加上每条 Wiki 内容可溯源到原始材料,已经把风险控制在可接受范围内。完美吗?不完美。但比每次查询都让 LLM 从零拼凑要可靠得多。
我的思考
做了这个知识库项目之后,我有几个比较明确的判断。
-
"编译 vs 检索"不是二选一,而是互补关系。小规模时 Wiki + index.md 就够用,规模增长后叠加 QMD 这样的搜索层。不需要一步到位。
-
人工微调的 ROI 极高。用少量人力投入换取不成比例的质量提升。这是 AI 系统中最被低估的优化手段。观测失败的用例,看看结果与预期的差距(大概率是检索问题),然后让AI反思和检查索引是否有代表性。这是形成完美百科闭环的其中一步。
-
QMD 的几个工程模式有很强的通用性。Strong-Signal Short-Circuit(轻量信号先行判断)、三路 Query Expansion(一个意图拆成多种搜索形态)、Position-Aware Blending(按置信度分段混合)。这些不只是搜索技术,是构建任何多阶段 AI 管线的通用思路。
-
对创业者来说,Karpathy 描述的 LLM Wiki 模式在中小规模(几百个文档以内)已经非常实用。不需要复杂的基础设施,不需要向量数据库,一个目录结构加一个 index 文件就能跑起来。等真正需要扩展时,工具已经准备好了。
知识管理这件事,过去靠人的毅力维护,所以大多数知识库最终沦为信息坟场。LLM 改变的不是知识本身,是维护知识的成本。当维护成本趋近于零,知识才有可能真正复利。