今天要解的問題
前兩天都在講 llm-wiki 的儲存層:從 iCloud 搬到 git 的決定,以及那之前五個月被同步碟咬出來的三類症狀。今天講它本身——上面那些是它存在哪裡,這篇是它長什麼樣子。
起點是一個很普通的困境。我在 Notion 寫過讀書心得,在卡片式筆記工具裡建過白板,在 Obsidian 開過「以後要整理」的資料夾。幾個月後想找某個概念,找不到;或者找到了,但已經不記得當初為什麼覺得它重要。
LLM 流行之後,這個問題的「正確解法」看起來很明顯:把所有筆記餵進向量資料庫,用語意搜尋查。也就是 RAG。
我沒有走那條路。Andrej Karpathy 有一句話講中了原因:
“The wiki is a persistent, compounding artifact.”
差別在於整理發生在什麼時候。RAG 是查詢時才整理,每次查都要從一堆雜訊裡重新推導;wiki 是寫入時整理一次,之後反覆複用。同一份筆記,前者永遠是原料,後者會變成資產。
這個 vault 現在是 756 個 markdown 進版控,跑了大半年。以下是它長成現在這樣的五個取捨。
想法與取捨

一、為什麼不用 RAG
RAG 這個方案假設你的原始資料已經夠好了。
實際上不是。原始筆記裡有倉促抄下的引文、講到一半的思路、沒有脈絡的截圖。每次查詢都從這裡面推導,成本高、品質不穩定,而且它不會變好——你查一百次,那一百次都在對同一堆雜訊做同樣的工。
wiki 方法把那份工前置。代價是寫入變重(要整理、要分類、要連結),換到的是查詢變輕,而且整理出來的東西會累積。
有一個附帶的好處:沒有向量資料庫要維護,查詢的 context 通常少於 10 個檔,成本可以忽略不計。
二、為什麼是域優先,不是 tag 或資料夾分類法
Karpathy 的原始架構是三層分工——Raw(原始素材,人放入、LLM 只讀)、Wiki(整理後的概念頁,LLM 維護)、Schema(一份規則檔定義結構,人制定)。
我加的那一層是:頂層資料夾用生活域切,不是用主題或專案狀態切。
llm-wiki/
books/ finance/ coffee/ engineer/
art/ ideas/ skateboard/ projects/
others/ hubs/ Inbox/ log/
理由是查詢路徑。找咖啡相關的東西,先看 coffee/index.md,再往下看 coffee/wiki/——兩跳就到,不需要掃全庫。LLM 每次讀的檔案數從幾百個降到 2 到 4 個。
tag 做不到這件事。tag 是多對多的,「從哪裡開始找」這個問題它回答不了;資料夾是樹狀的,回答得了。所以 tag 留著當跨域探勘用(後面 hub 會講),但進入點一律是域。
每個域按追蹤型態長成兩種之一,而且互斥:
records.md:單檔表格追蹤,適合一列一筆的域(coffee、art)logs/YYYY-MM-DD.md:每日一檔的時序 journal,適合記當日體感的練習域(skateboard)
不讓一個域同時有這兩個,是因為它們對「一次記錄」的定義不同,混在一起之後誰都不知道該往哪寫。
三、為什麼 raw 不到五個檔就不准開 wiki
這條看起來像潔癖,但它擋掉的是一個很具體的失敗模式:先把資料夾結構建好,然後那些資料夾永遠是空的。
規則是:raw/ 累積到 5 個檔才開;wiki/ 和 index.md 跟 raw/ 同時建立。域本身也一樣——不確定歸屬的東西先丟 others/raw/,同主題聚到 5 個檔以上才升成子域。
others/ 在這裡的定位是合法的停車格,不是垃圾桶。它存在的價值是讓我在捕捉的當下可以誠實地說「我還不知道這是什麼」,而不是被迫硬塞進一個不對的域。
選擇預設放 others/ 而不是預設建新域,理由是不對稱:往上搬比往下藏的心理成本低。 把一個長大的子域升成頂層域,是一件令人愉快的事;把一個建了三個月還是只有兩個檔的頂層域降級,會拖很久,因為那等於承認當初判斷錯了。
四、為什麼規範要用 hook 擋,不是寫在文件裡
這條是前兩天的主題,但它的根在架構這一層。
寫進 skill、以及那份每個 session 都會自動載入的規則檔(後面會講)的規則,是軟約束,靠 LLM 自律遵守。跑久了會發現,覆蓋率很高(所有動作都吃得到)但強制力是零,而且失敗是隨機的——同一條規則,今天守了明天沒守。
所以最常被違反的那幾條改成硬約束,用 PreToolUse hook 在寫入前物理擋下:根目錄寫一次性腳本、寫進依賴夾、產生衝突副本檔名、wiki 頁缺必要 frontmatter。
意義不是「多一道保險」,而是把規則的性質整個換掉:從「希望 LLM 記得」變成「做錯了根本存不進去」。
但護欄是長出來的,不是設計出來的。這四條每一條背後都有一次真的發生過的違規,不是一開始就想好的。一開始就把想得到的規則全部寫成 hook,只會得到一堆從來沒觸發過的檢查,外加一個沒人敢改的 dispatcher。
五、skill 從原子指令收斂成管線
操作介面是一組 Claude Code skill,放在 .claude/skills/,目前 15 個。
剛開始是 9 個原子指令,一個動作一個。跑了半年之後最大的變化是合併:原本的 book-add / book-review / book-ingest / book-update 四個指令收成一個 /book-review 一條龍;wiki-lint 併進 /vault-maintain;inbox-enrich 升級成 /note-enrich。
驅動合併的是兩條設計法則:
- 單一寫入點:每個管線只有一個入口。讀完一本書就是
/book-review,它自己判斷現在該走登記、寫心得、還是提概念,不需要我記得順序。 - pull-not-push:skill 收尾時主動提議下一步,而不是散成一堆要自己記得串接的小指令。
第二條解掉的是一個很真實的問題:原子指令的管線斷點全部落在人腦記憶上。/inbox-triage 分類完,接下來該對哪幾個檔跑 enrich?如果沒人記得,那些檔就永遠停在 raw/。現在 triage 一律把需要後續處理的項目寫進根目錄 TODO.md 的固定區段——管線的接力棒要有實體,不能只是對話裡提過一句。
實作

Schema 就是一份 markdown
整個架構的「設定檔」是根目錄的 CLAUDE.md,每個 session 自動載入。它定義域結構、路由規則、寫作格式、禁止事項。
wiki 頁的 frontmatter 規格是其中最常被用到的一段:
title: string # required, 頁面標題
summary: string # required, 一行,供 index 用
tags: [string] # required, 中文主題 tag 為主
date: YYYY-MM-DD # required
aliases: [string] # optional, 檔名與常用稱呼語言不同時必加(中英對照)
sources: "[[...]]" # optional, 外提概念頁指向 vault 內來源節點
summary 是 required 而不是 optional,因為域的 index.md 直接吃它:
[[page-path]] — 一行摘要
這一行是整個查詢加速的核心。LLM 掃 index 時只讀索引行、不讀全文,所以 summary 寫得好不好,直接決定「這頁會不會在該被找到的時候被找到」。
aliases 看起來可有可無,但它補的是查重最大的盲區:同一個概念用中文建過一次、用英文又建一次。 名稱語言不同的時候 grep 兩邊都不會命中,只能靠 aliases 接起來。
規則怎麼變成程式
這四個必填欄位在寫入前的判斷式裡長這樣(一支唯讀腳本,從 stdin 收到這次要寫的檔名與內容):
# Rule 4: wiki frontmatter gate (Write only -- Edit lacks the full content)
if tool == "Write" and re.search(r"(?i)/wiki/", norm) and ext == ".md":
content = tool_input.get("content", "") or ""
fm = ""
m = re.match(r"(?s)\A?---\s*\r?\n(.*?)\r?\n---", content)
if m:
fm = m.group(1)
missing = []
for k in ("title", "summary", "tags", "date"):
if not re.search(rf"(?im)^[ \t]*{k}[ \t]*:[ \t]*\S", fm):
missing.append(k)
if missing:
deny(
f"wiki page missing required frontmatter: {', '.join(missing)}. "
f"Every {{domain}}/wiki/*.md needs title / summary / tags / date. File: {leaf}"
)
幾個刻意的選擇:只檢查 Write 不檢查 Edit(Edit 拿不到完整內容,檢查會誤判);正則是 [ \t]*{k}[ \t]*:[ \t]*\S,要求欄位後面真的有非空白字元,擋掉 summary: 空著的情況;開頭的 \A?--- 容許 BOM。
還有一個貫穿全檔的原則——解析失敗永不阻斷:
except SystemExit:
raise
except Exception:
sys.exit(0) # never block on unexpected failure
護欄的目的是擋掉已知的壞動作,不是把不確定的情況全部鎖死。這跟前一天那個 fail-open 的判斷是同一條線。
跨域連結:讓知識真的複利
前面講的都是「單一主題怎麼長」。但複利不是自動發生的,它靠兩個動作反覆把新頁織進舊網:
寫入當下——所有產出 wiki 頁的 skill 收尾時跑同一個 pass:拿本頁的 summary 和 tags 去掃其他域的 index.md(只讀索引行),挑最多 3 個候選頁,每個附一句關聯理由(互相支持或互相打架都算),列給我確認才寫入,並補上反向連結。沒有合適候選就明說「本次無跨域候選」,不硬湊。
巡檢時——/vault-maintain 的語意掃描會回頭補網:找出同一概念被建了兩份的情況,以及「跨兩域以上、五頁以上、還沒有 hub」的 tag 叢集。
第二個產出的是 hub 策展頁,放在頂層 hubs/,目前有 8 個:注意力、情緒、學習方法、溝通、藝術創作、興趣經營、ai-協作工程、ai-與人。
hub 和 wiki-build 的分工很明確:wiki-build 把單域的 raw 合成成內容頁;hub 只聚合、不合成,它把各域既有的 wiki 頁連過來,而且每條連結都必附一句「為什麼在這個展間」——純連結清單是被禁止的。
為什麼要禁?因為沒有理由的連結清單,三個月後我自己也看不懂當初為什麼把這兩頁放在一起。那一句理由才是 hub 的內容,連結只是骨架。
目前的樣子
ideas 是唯一 wiki(89)多過 raw(26)的域,因為它裝的是從書頁、藝評頁外提出來的概念——沉澱留在來源側,用 frontmatter 的 sources 溯源。books 則相反,224 個 raw 對 45 個 wiki:筆記記了兩百多本,寫成心得頁的四十幾本。
這個比例落差本身就是資訊。它告訴我 books 域的瓶頸不在收集,在消化。
踩到的坑
這三個都是判斷錯誤,不是程式錯誤,而且都只在事後對帳時才會現形。
一、別讓模型數數
/vault-maintain 早期版本讓 LLM 去統計 vault 狀態,結果它把 43 本書數成 41 本。
不是 hallucination 那種明顯的錯,就是單純數錯了。而且這種錯特別危險——數字看起來合理,你不會想到要去驗。
所以現在的分工是硬規則:所有可機械判定的檢查(欄位缺漏、連結格式、index 同步、壞連結、孤兒 raw、計數)一律寫成唯讀腳本,快、準、零 token,輸出結構化 JSON,主 agent 直接信任,不再讓模型複審機械項。
LLM 只負責語意層——判斷兩頁講的是不是同一個概念、這頁路由得合不合理、這叢 tag 是不是一個真的主題。
二、最輕的模型只能搬運,不能判斷
掃描層用了模型分層:腳本跑機械項,中階模型跑語意項,主 agent 做最終裁決。中間試過把最輕的模型也拉進來做分類,結果它把一篇繪畫心理學的筆記判成滑板。
現在最輕的那一層只做「讀檔、回摘要」的搬運,不做任何內容判斷。
這件事的教訓不是「輕模型不好用」,是要先分清楚一個任務需要的是吞吐還是判斷。搬運可以壓成本,判斷不行,而它們混在同一個 prompt 裡的時候看不出來。
三、同名檔讓 wikilink 變成歧義
books/raw/{書名}.md 是讀書筆記的沉澱,books/wiki/{書名}.md 是心得頁。兩個檔同 basename,而且都是有效的連結目標——所以裸名 [[某本書]] 在 Obsidian 裡無法判斷指向哪一個。
處置是給 books 開一條明文例外:任何指向書的連結一律用路徑限定。
sources: "[[books/wiki/{書名}]]" # 連心得頁
sources: "[[books/raw/{書名}]]" # 連沉澱
這條之所以要寫成例外而不是通則,是因為其他域沒有這個問題:{域}/raw/{概念}.md 是「被 wiki 頁取代的沉澱」、不是獨立的連結目標,所以裸名照慣例解析成 wiki 概念頁就好,維持最短路徑寫法。
只有 books 因為 raw 和 wiki 兩邊都是有效目標,才需要強制消歧。 把例外的適用邊界寫清楚,跟寫例外本身一樣重要——不然下次整理文件的時候,它很容易被「統一」成通則。
小結
llm-wiki 的骨架講完了:Raw / Wiki / Schema 三層分工加一層域優先結構,Inbox 當唯一入口,15 個 skill 收斂成幾條有單一入口的管線,規範分成 CLAUDE.md 的軟約束和 hook 的硬約束兩層,跨域靠寫入時的連結 pass 加巡檢時的 hub 探勘織網。
如果要從頭開始,我會照這個順序:先立三層加一個域加一份 CLAUDE.md(最小可跑骨架),再裝 inbox-triage(每天的入口,投報率最高),跑一陣子之後把常一起用的指令收成管線,等到某類錯誤反覆發生再把那一條變成 hook。
明天要還的是搬家時欠的一筆帳。Day8 結尾承認過一件事:擋寫入的那幾條規則有兩份各自獨立的實作,一份給 macOS、一份給 Windows,而 Windows 那份至今一次都沒有真的執行過——因為改它的那台機器上沒有 PowerShell 可以驗。這篇講的所有規範,有一半的強制力就押在那份沒驗過的程式碼上。明天去 Windows 端跑它。
本文同步發表於 kiwi-walk.com:https://kiwi-walk.com/blogs/engineer/ironman-2026-day10-what-is-llm-wiki/
