从 Karpathy 方法论到工程落地:LLM Wiki 的两步思维链摄入管线解析
本文基于开源项目 LLM Wiki(v0.3.1)的源码,对比 Andrej Karpathy 的原始方法论文档,分析该项目在摄入管线、增量缓存、队列调度和检索管线四个方面的工程实现与设计取舍。
一、Karpathy 的原始方法论
2026 年初,Andrej Karpathy 发布了一份名为 llm-wiki.md 的设计模式文档,提出了一种用 LLM 构建个人知识库的思路。其核心观点是:与传统 RAG 在每次查询时从头检索不同,LLM 应该增量构建并维护一个持久化的 Wiki,知识只编译一次并持续更新。
该文档定义了三层架构和三个核心操作:
三层架构:
- Raw sources(原始资料):不可变的源文档集合
- Wiki(知识库):LLM 生成和维护的 Markdown 文件集合
- Schema(规则):告诉 LLM 如何组织 Wiki 的配置文档
三个操作:
- Ingest(摄入):导入新文档,LLM 阅读后更新 Wiki
- Query(查询):基于 Wiki 内容回答问题
- Lint(检查):定期健康检查,发现矛盾、孤立页面、知识空白
但 Karpathy 在文档末尾明确指出:“This document is intentionally abstract. It describes the idea, not a specific implementation.”——这是一个设计模式,不是工程方案。
LLM Wiki 项目将这个抽象模式实现为一个完整的跨平台桌面应用(Tauri v2 + React 19),在此过程中做了大量工程决策。本文聚焦其中最核心的部分:摄入管线。
二、两步思维链摄入
2.1 原始方法论的描述
Karpathy 对摄入的描述是一个概括性的流程:“the LLM reads the source, discusses key takeaways with you, writes a summary page in the wiki, updates the index, updates relevant entity and concept pages across the wiki, and appends an entry to the log.”——LLM 阅读、讨论、写入,一气呵成。
2.2 工程实现:拆分为分析与生成
LLM Wiki 将这个单步流程拆分为两次独立的 LLM 调用,核心代码在 ingest.ts 的 autoIngest() 函数中:
Step 1(分析):LLM 阅读原始文档 → 输出结构化分析报告
Step 2(生成):LLM 基于分析报告 + 原始文档 → 生成 Wiki 文件
第一步:分析
系统向 LLM 发送原始文档内容,要求输出一份结构化分析,涵盖六个维度:关键实体、关键概念、主要论点与发现、与现有 Wiki 的关联、矛盾与张力、建议。
分析阶段的 system prompt 中包含两个关键上下文:Wiki 的 purpose.md(目标定义)和 index.md(现有内容目录)。这使得 LLM 在分析时能够判断哪些内容是新增的、哪些与现有知识重叠或矛盾。
const [sourceContent, schema, purpose, index, overview] = await Promise.all([
tryReadFile(sp),
tryReadFile(`${pp}/schema.md`),
tryReadFile(`${pp}/purpose.md`),
tryReadFile(`${pp}/wiki/index.md`),
tryReadFile(`${pp}/wiki/overview.md`),
])
五个文件并行读取,减少 I/O 等待。
第二步:生成
LLM 接收第一步的分析报告和原始文档内容,按照严格的格式规范生成 Wiki 文件。输出格式使用自定义的文件块标记:
---FILE: wiki/sources/filename.md---
(完整文件内容,含 YAML frontmatter)
---END FILE---
生成阶段的 prompt 对输出格式有详细约束:每个页面必须包含 YAML frontmatter(type、title、created、updated、tags、related、sources),必须使用 [[wikilink]] 语法进行交叉引用,文件名使用 kebab-case。
2.3 拆分的工程价值
两步拆分带来三个具体收益:
质量提升:分析步骤迫使 LLM 先"思考"再"动手"。在单步模式下,LLM 容易在阅读文档的同时就开始生成页面,导致遗漏后文中的重要信息或忽略与现有 Wiki 的矛盾。两步模式下,第一步的分析报告作为第二步的输入,确保生成阶段拥有完整的分析上下文。
可观测性:分析报告是一个可读的中间产物。如果生成结果不理想,开发者可以检查分析报告来定位问题——是分析阶段遗漏了关键信息,还是生成阶段没有正确执行分析建议。
容错性:两步之间是独立的 LLM 调用。如果生成步骤失败(如 API 超时),分析结果不会丢失,可以基于已有分析重试生成。
2.4 审核项的生成
生成阶段还会输出审核项(Review Items),使用另一种标记格式:
---REVIEW: type | Title---
描述内容
OPTIONS: Create Page | Skip
PAGES: wiki/page1.md, wiki/page2.md
SEARCH: search query 1 | search query 2
---END REVIEW---
审核类型被限制为四种:contradiction(矛盾)、duplicate(重复)、missing-page(缺页)、suggestion(建议)。操作选项也被限制为 Create Page 和 Skip 两种。这种约束设计避免了 LLM 生成任意操作带来的不可控性。
每个审核项还包含预生成的搜索查询(SEARCH 字段),用于后续的深度研究功能。这些查询在摄入时由 LLM 生成,针对搜索引擎优化,而非通用的自然语言描述。
2.5 兜底机制
代码中包含一个兜底逻辑:如果 LLM 的生成输出中没有包含资料摘要页面(wiki/sources/ 目录下的文件),系统会自动创建一个,内容取自第一步的分析报告:
if (!hasSourceSummary) {
const fallbackContent = [
"---",
`type: source`,
`title: "Source: ${fileName}"`,
// ... frontmatter
"---",
"",
analysis ? analysis.slice(0, 3000) : "(Analysis not available)",
].join("\n")
await writeFile(sourceSummaryFullPath, fallbackContent)
}
这确保了每份原始文档至少有一个对应的 Wiki 摘要页面,即使 LLM 在生成阶段遗漏了它。
三、SHA256 增量缓存
3.1 原始方法论中的空白
Karpathy 的文档没有讨论重复摄入的问题。在实际使用中,用户可能重新导入已有文档(如应用重启后恢复队列),如果每次都执行完整的两步 LLM 调用,会浪费 token 和时间。
3.2 工程实现
ingest-cache.ts 实现了基于 SHA256 的增量缓存,核心逻辑简洁:
export async function checkIngestCache(
projectPath: string,
sourceFileName: string,
sourceContent: string,
): Promise<string[] | null> {
const cache = await loadCache(projectPath)
const entry = cache.entries[sourceFileName]
if (!entry) return null
const currentHash = await sha256(sourceContent)
if (entry.hash === currentHash) {
return entry.filesWritten // 命中缓存,返回上次生成的文件列表
}
return null // 内容已变更,需要重新摄入
}
缓存数据结构:
interface CacheEntry {
hash: string // 源文件内容的 SHA256 哈希
timestamp: number // 摄入时间戳
filesWritten: string[] // 上次摄入生成的文件路径列表
}
缓存以 JSON 文件形式持久化在 .llm-wiki/ingest-cache.json 中。哈希计算使用 Web Crypto API(crypto.subtle.digest),在 Tauri 的 WebView 环境中原生可用。
缓存的粒度是源文件级别,以文件名为键。当源文件被删除时,对应的缓存条目也会被清除(removeFromIngestCache)。
3.3 设计取舍
该缓存方案选择了简单性而非精确性:它只检查源文件内容是否变更,不检查 Wiki 的当前状态。如果用户手动编辑了 Wiki 页面(如修正了一个实体页面的描述),然后重新导入同一份源文件,缓存会命中并跳过摄入,用户的手动修改不会被覆盖。这在大多数场景下是合理的行为,但在某些边缘情况下(如 Wiki schema 发生了重大变更,需要基于新规则重新生成所有页面)可能需要手动清除缓存。
四、持久化摄入队列
4.1 原始方法论的描述
Karpathy 提到可以"batch-ingest many sources at once with less supervision",但没有讨论并发控制、崩溃恢复等工程问题。
4.2 串行处理与崩溃恢复
ingest-queue.ts 实现了一个持久化的任务队列,核心设计决策是严格串行处理:
async function processNext(projectPath: string): Promise<void> {
if (processing) return // 同一时刻只有一个任务在执行
const next = queue.find((t) => t.status === "pending")
if (!next) return
processing = true
next.status = "processing"
await saveQueue(projectPath)
// ... 执行摄入 ...
processing = false
processNext(projectPath) // 递归处理下一个
}
串行处理的原因是:每次摄入都会读取和更新 index.md、overview.md 等共享文件,并发执行会导致写冲突。
队列状态持久化到 .llm-wiki/ingest-queue.json。应用启动时,restoreQueue() 函数加载磁盘上的队列,将所有 processing 状态的任务重置为 pending(因为它们是被应用关闭中断的),然后恢复处理:
export async function restoreQueue(projectPath: string): Promise<void> {
const saved = await loadQueue(pp)
// 将中断的任务重置为待处理
for (const task of saved) {
if (task.status === "processing") {
task.status = "pending"
restored++
}
}
queue = saved
await saveQueue(pp)
processNext(pp) // 恢复处理
}
4.3 失败重试与取消
失败的任务会自动重试,最多 3 次:
const MAX_RETRIES = 3
// 在 catch 块中:
next.retryCount++
if (next.retryCount >= MAX_RETRIES) {
next.status = "failed"
} else {
next.status = "pending" // 重新排队
}
取消正在执行的任务时,系统通过 AbortController 中断 LLM 调用,并清理已写入的文件:
export async function cancelTask(projectPath: string, taskId: string): Promise<void> {
if (task.status === "processing") {
if (currentAbortController) {
currentAbortController.abort()
}
// 清理已写入的文件
for (const filePath of lastWrittenFiles) {
await deleteFile(fullPath)
}
}
queue = queue.filter((t) => t.id !== taskId)
}
lastWrittenFiles 数组跟踪当前摄入任务已写入的文件路径。取消时逐一删除,避免留下不完整的 Wiki 页面。
4.4 文件夹上下文
批量导入时,文件的目录路径作为 folderContext 传递给 LLM:
export interface IngestTask {
sourcePath: string // "raw/sources/papers/energy/solar-panel-review.pdf"
folderContext: string // "papers > energy"
// ...
}
分析阶段的 prompt 中包含指引:“If a folder context is provided, use it as a hint for categorization — the folder structure often reflects the user’s organizational intent.”
这是一个低成本但有效的设计:用户的文件夹组织方式本身就包含分类信息,将其传递给 LLM 可以提高实体和概念分类的准确性,而不需要额外的分类模型。
五、多阶段检索管线
5.1 原始方法论的描述
Karpathy 描述的查询流程是:"The LLM searches for relevant pages, reads them, and synthesizes an answer with citations."他建议在小规模时使用 index.md 作为导航入口,规模增长后引入搜索引擎。
5.2 工程实现:四阶段管线
LLM Wiki 实现了一个多阶段检索管线,代码分布在 search.ts 和 graph-relevance.ts 中:
阶段 1:分词搜索
对查询文本进行分词,支持中英文混合:
export function tokenizeQuery(query: string): string[] {
// 英文:按空格和标点分词 + 停用词过滤
// 中文:CJK 字符检测 → 二元组分词
// "默会知识" → ["默会", "会知", "知识", "默", "会", "知", "识"]
}
中文分词采用了二元组(bigram)策略而非分词库,这是一个务实的选择——避免了引入 jieba 等分词库的依赖,在大多数场景下二元组的召回率足够。标题匹配额外加 10 分。
搜索范围覆盖 wiki/ 和 raw/sources/ 两个目录。
阶段 2:向量语义搜索(可选)
如果用户在设置中启用了向量搜索,系统会通过 LanceDB(嵌入式向量数据库,Rust 实现)进行语义检索:
if (embCfg.enabled && embCfg.model) {
const vectorResults = await searchByEmbedding(pp, query, embCfg, 10)
// 合并策略:已有结果加分,新结果追加
for (const vr of vectorResults) {
const existing = results.find(/* 匹配已有结果 */)
if (existing) {
existing.score += vr.score * 5 // 增强已有匹配
} else {
results.push(/* 添加新发现 */) // 补充新页面
}
}
}
向量搜索的结果与分词搜索的结果通过分数叠加进行融合,而非替换。这确保了即使向量搜索返回了误匹配,分词搜索的精确匹配仍然排在前面。
阶段 3:图谱扩展
在查询阶段,系统使用四信号关联度模型(详见本系列第一篇文章)对搜索命中的页面进行图谱扩展,发现语义相关但未被关键词或向量搜索命中的页面。
阶段 4:上下文预算控制
用户可配置上下文窗口大小(4K 到 1M tokens),系统按固定比例分配预算:
- 60% 给 Wiki 页面内容
- 20% 给聊天历史
- 5% 给 index.md
- 15% 给系统提示
页面按搜索分数 + 图谱关联度的综合分数排序,在预算内尽可能多地纳入高相关度页面。
5.3 与 Karpathy 方案的对比
| 维度 | Karpathy 方法论 | LLM Wiki 实现 |
|---|---|---|
| 小规模检索 | index.md 导航 | 分词搜索 + index.md |
| 大规模检索 | 建议引入搜索引擎 | 内置分词搜索 + 可选向量搜索 |
| 关联发现 | wikilink 手动浏览 | 四信号图谱自动扩展 |
| 上下文控制 | 未讨论 | 可配置预算 + 比例分配 |
| 中文支持 | 未讨论 | CJK 二元组分词 |
六、Purpose.md:方法论中缺失的一环
Karpathy 的方法论定义了 Schema(Wiki 如何运作),但没有定义 为什么 这个 Wiki 存在。LLM Wiki 新增了 purpose.md,包含目标、关键问题、研究范围等信息。
这个文件在摄入和查询的每次 LLM 调用中都作为上下文注入。其工程价值在于:它为 LLM 提供了一个稳定的"方向锚点",使得不同时间点的摄入结果保持一致的关注方向,而不是每次都由 LLM 自行判断什么是重要的。
七、方法论到工程的差距总结
| 方法论描述 | 工程问题 | LLM Wiki 的解决方案 |
|---|---|---|
| “LLM reads and writes” | 单步生成质量不稳定 | 两步思维链(分析→生成) |
| “batch-ingest many sources” | 并发写冲突 | 串行队列 + 持久化 |
| 未讨论 | 重复摄入浪费 token | SHA256 增量缓存 |
| 未讨论 | 应用崩溃后队列丢失 | 磁盘持久化 + 启动恢复 |
| 未讨论 | 摄入中断留下残留文件 | AbortController + 文件清理 |
| 未讨论 | LLM 遗漏必要页面 | 资料摘要兜底机制 |
| “index.md is enough at moderate scale” | 中文分词、语义检索 | 多阶段检索管线 |
| 未讨论 | Wiki 的目标方向漂移 | purpose.md 方向锚点 |
| “humans in the loop reviewing” | 阻塞式审核影响效率 | 异步审核队列 + 预定义操作 |
Karpathy 的方法论提供了正确的抽象——三层架构、三个操作、持久化 Wiki 优于即时 RAG。LLM Wiki 的贡献在于将这些抽象落地为可运行的工程系统,过程中解决了大量方法论未涉及的实际问题。这种从设计模式到工程实现的转化过程,对于任何基于 LLM 构建知识管理系统的团队都有参考价值。
本文分析基于 LLM Wiki v0.3.1 源码,项目地址:https://github.com/nashsu/llm_wiki ,采用 GPL-3.0 许可证。Karpathy 原始方法论文档:https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)