今天要解的問題
llm-wiki 是我用 Obsidian 加 Claude Code 維護的個人知識庫。素材分兩層:{域}/raw/ 放擷取進來、還沒整理的筆記,{域}/wiki/ 放整理好的概念頁。
- raw -> wiki: 期待的消化
- raw 沒人用: 堆積
同一個域堆到 5 篇,Claude Code 裡的 vault 儀表板 navi(做法見 Day26)就會在狀態列亮出「N 域 raw 未消化」。

上圖是 2026-10-14 的狀態,照 navi 當時的計算重繪:七個域裡四個堆積,engineer 一域就有 31 篇。
這個提醒的用意是推我去跑 /wiki-build 把素材整理掉。問題是 engineer 域有一批 raw,我已經不打算整理了:openspec、ruflo、openclaw、hermes 這幾個 AI 工具的研究筆記。原因很單純:
AI 技術變化太快,所以這些用不到。
它們不會被消化,所以只要它們還在,engineer 的堆積數就降不下來,狀態列也一直亮著。
raw 堆著不管也不行。raw 不像 wiki 有 index.md 索引,查詢時我和 AI 都是從索引進去,raw 基本上不會被翻到:
我怕 raw 堆積一堆我根本沒看的,對我的第二大腦就是冗餘,而且 raw 沒有列在 index,所以基本上就很容易被我與 ai 忽略。
所以要的是一條「用不到的 raw 怎麼離開」的路:讓它不再算堆積,但也不是一標了之、從此沒人管。

想法與取捨
堆積數有一部分是誤報
同一天稍早,堆積數本身先被查了一輪,發現有一部分是誤報,原因有兩個:
- coffee 域的 wiki 在來源表裡寫的是縮寫檔名的純文字,不是完整的
[[raw 檔名]]。navi 判斷「有沒有被引用」是拿 raw 檔名去 wiki 全文找字串,縮寫對不上,就算沒引用。 - navi 只看同一個域的 wiki。raw 被別的域的 wiki 引用,照樣算堆積。
把 coffee 的引用改成完整 wikilink、判斷改成看全 vault 的 wiki,再替 8 頁「內容已經併進去、但沒寫引用」的 wiki 補上連結。堆積數變成這樣:
| 域 | 修正前 | 修正後 |
|---|---|---|
| coffee | 21 | 0 |
| engineer | 31 | 23 |
| ideas | 19 | 12 |
| others | 14 | 13 |
剩下的才是真的沒消化,openspec 那一批就在 engineer 的 23 篇裡。誤報修掉之後,它們才是真正要處理的對象。
標記,不刪也不搬
讓 raw 退場有三種做法:直接刪、搬到封存資料夾、原地加標記。
刪掉最乾淨,但我猶豫過,最後沒刪:
我的確有猶豫是否保留,還是直接刪除,但我覺得還是有些用途,等一年後再看看。
搬檔也不行。vault 的寫作規範本來就有一條「不對現有檔案 rename」:搬家或改名會打斷別頁的 ![[...]] 圖片引用和 frontmatter 的 sources 溯源。
所以選了最輕的一種:在 frontmatter 加 status: retired,檔案留在原地,只是 navi 和 vault 健檢腳本都不再把它算成堆積。要復活就把這行刪掉。
第一批標了 9 篇。除了上面那四個工具,還有一篇 multica 的筆記,原文自己就寫著「目前我用不太到」,問過我之後一起標了。
標完當天,9 篇全被撤回
退場標完的同一天,另一個 session 正在跑 /wiki-build,把 engineer 的 raw 整理成 openclaw、hermes-agent、multi-agent-orchestration 等 wiki 頁。這 9 篇全部被引為來源。它們有 wiki 連入,就不是堆積了,退場標記反而多餘,所以又撤掉了。
我對這件事的看法是,先萃取成 wiki 本來就比較對:
我覺得先萃取成 wiki,先解決看不到堆疊的 raw,因為目前的 navi panel 都是比較針對 wiki 去查看,所以會比較容易發現過時的。
navi 面板主要是看 wiki 的,「這個技術過時了」放在 wiki 層比較容易被看見;raw 層要避免的是變成看不到的死角。退場機制沒有因此拿掉,之後真的不打算萃取的 raw 一樣用得到。
退場太多會怎樣
機制做好之後,我覺得要同時想好退場太多時怎麼辦。列出來有三個問題:標 status: retired 不用花任何成本,容易變成讓堆積數好看的開關;退場檔還留在 raw/ 裡,退得越多雜訊越多;而且只有進入退場的路,沒有出口。
考慮過另開一個 _retired/ 資料夾集中放,同樣因為搬檔會打斷連結而放棄。最後補了三道關卡,由輕到重:
- 退場要寫理由:
retired(退場日)和retired_reason從選填改成必填,缺了就報出來。寫不出理由,代表它其實還該消化。 - 滿期出口:退場滿 180 天、又沒有任何頁連到它,就列成「建議刪除」,由我確認後才刪。刪掉之後用 git 歷史找得回來,不需要另外的封存區。
- 比例提醒:某個域退場的 raw 超過 20%,或整個 vault 本月新退場超過 10 篇,狀態列提醒一句,不擋任何操作。
第 2 條只列不刪,也不把 180 天寫死。我對刪除的判斷本身就可能變:
目前有設定 vault-maintain 針對半年的做清掃,但難保我一個月後決定刪除,而不保留。
所以天數做成參數,刪除一律留給人決定。第 3 條的 20% 和 10 篇是先抓的起始值,當時 engineer 是 9/84 篇退場,大約 11%,沒有更多依據,寫成常數方便之後調。
實作
退場的 frontmatter
規則寫在 vault 的寫作規範 .claude/rules/wiki-format.md,三行缺一不可:
status: retired
retired: YYYY-MM-DD # 退場日,I8 從這天起算
retired_reason: 一句話 # 為什麼用不到;寫不出理由就是還該消化,不要退場
原本沒有 frontmatter 的 raw,就補一個 --- 區塊。撤回時,原本沒有 frontmatter 的 7 篇直接還原成退場前的版本,有 frontmatter 的 2 篇只刪這三行。
navi:退場不算堆積
navi 用 regex 只看 frontmatter 區塊裡的 status: 行,順便取出退場日:
// 沒退場回 undefined;退場回 frontmatter `retired:` 日期(沒填回 '')
async function retiredOn($: $, path: string): Promise<string | undefined> {
try {
const fm = (await $.fs.read(path)).match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]
if (fm === undefined || !/^status:\s*retired\s*$/m.test(fm)) return undefined
return fm.match(/^retired:\s*["']?(\d{4}-\d{2}-\d{2})/m)?.[1] ?? ''
} catch {
return undefined
}
}
只比對 frontmatter 區塊,正文裡出現同樣一行字不算。
堆積的判斷是先把退場檔挑掉,剩下的才拿去全 vault 的 wiki 全文找有沒有被引用:
function countUndigested(domain: string, wiki: string[], raw: string[], corpus: string): number {
if (domain === 'books') return 0
return raw.filter(n => !wiki.includes(n) && !corpus.includes(n)).length
}
corpus 是全部域的 wiki 內容串在一起,這就是「被別域引用也算消化」的修正。books 的 raw 是讀書筆記,本來就不算堆積。
健檢腳本:W6 與 I8
vault 的健檢 /vault-maintain 底下有一支唯讀的 lint.py,規則分 E(錯誤)、W(警告)、I(提示)。raw 孤兒原本是 W5,退場檔在這裡分流出兩條新規則:
fm = v.parsed(p)[1]
if str(fm.get("status", "")).strip() == "retired":
retired.append(v.rel(p)) # status: retired = raw 退場, kept in place, not an orphan
# W6: retiring must say when and why (otherwise it is just a way to silence W5)
missing = [k for k in ("retired", "retired_reason") if not str(fm.get(k) or "").strip()]
if missing:
out.append(finding("W6", v.rel(p), f"retired raw missing: {', '.join(missing)}"))
# I8: retired long enough and nothing links to it -> suggest deletion (git history is the archive).
# Whole-vault only: a single-domain run does not see links from other domains.
m = re.match(r"\d{4}-\d{2}-\d{2}$", str(fm.get("retired") or "").strip())
if domain is None and m and p not in linked:
days = (dt.date.today() - dt.date.fromisoformat(m.group(0))).days
if days >= retire_days:
out.append(finding("I8", v.rel(p), f"retired {days}d ago, no inbound links: suggest delete"))
continue
linked是前面掃連結時收集的「有被某頁 wikilink 指到的檔」。只要還有頁連到它,就算退場滿期也不會建議刪除,刪了就是斷鏈。- I8 只在全 vault 模式判斷。單域模式只掃得到同域的連結,別域連進來的會被漏掉,進而誤報「沒人連」。
retire_days預設 180,用--retire-days調。I8 不在自動修復的清單裡,這支腳本永遠只報告。
navi 狀態列的比例提醒
function statusLine(inbox: number, stats: DomainStat[], audit?: string): string {
const parts: string[] = []
if (inbox > 0) parts.push(`Inbox ${inbox} 待分類`)
const piled = stats.filter(s => s.rawOrphans >= PILE).length
if (piled > 0) parts.push(`${piled} 域 raw 未消化`)
const heavy = stats.filter(s => s.raw > 0 && (s.rawRetired ?? 0) / s.raw > RETIRE_RATIO)
if (heavy.length > 0) parts.push(`${heavy.map(s => s.domain).join('、')} 退場 > ${RETIRE_RATIO * 100}%`)
const month = stats.reduce((n, s) => n + (s.rawRetiredMonth ?? 0), 0)
if (month > RETIRE_MONTH) parts.push(`本月退場 ${month} 篇`)
if (audit) parts.push(audit)
return parts.length > 0 ? parts.join(' · ') : '一切正常'
}
RETIRE_RATIO = 0.2、RETIRE_MONTH = 10 定義在檔案開頭。「本月」看的是 retired 日期,用台灣時間的 YYYY-MM 比對。s.raw > 0 擋掉沒有 raw 的域,免得除以零。把這個函式單獨抽出來跑過邊界:剛好 20%、本月剛好 10 篇都不提示,超過才提示。
踩到的坑
改了文件,規則卻沒生效
退場機制做到一半,main 上的 /vault-maintain 被重構了:原本每次在暫存區臨時寫掃描腳本,改成一支常駐的 lint.py;vault 的寫作格式也從 CLAUDE.md 搬到 .claude/rules/wiki-format.md。
我原本的改法是在 vault-maintain 的說明文件裡寫「W5 排除 status: retired」。併入新版時,說明文件有衝突,但衝突只告訴你兩邊的文字不同;真正判斷孤兒的已經換成 lint.py,它根本不知道退場這件事。是解衝突時看到 scripts/ 底下多了 lint.py、W5 改成看最後一次 commit 的時間,才發現只改文件不會生效。最後把排除邏輯寫進 lint.py,規則文字則改寫到新的 rules 檔。
navi 也類似:同一天它的堆積判斷從「同域」改成「全 vault」,函式從 async 改成同步、參數也換了。我原本的寫法不能直接留,得在新結構上重新套一次退場排除。
錯誤數變少,其實是更糟
navi 沒有自己的 tsconfig,我照 Claude Code mod 的官方設定在暫存區建一份,拿改動前後的版本各跑一次 tsc,比較錯誤數有沒有增加。
加狀態列提示那次,結果是這樣:
tsc new: 2
tsc old: 49
錯誤從 49 降到 2,看起來像順手修好了一堆東西。實際上新的 2 個錯誤都是:
hooks/register.tsx: error TS1382: Unexpected token. Did you mean `{'>'}` or `>`?
我在說明分頁的 JSX 文字裡直接寫了「退場 raw > 20%」,> 在 JSX 文字裡是語法錯誤。語法錯誤讓 tsc 停在解析階段,後面的型別檢查根本沒跑,所以錯誤「變少」了。把 > 改成「超過」之後,回到跟改動前一模一樣的 49 個。
這次會發現,是因為比對時除了數字,還把兩邊的錯誤訊息去掉行號後 diff:數字看起來是好消息,diff 卻顯示兩邊的錯誤完全不同。只比數字就會漏掉。
測 I8 時,它一筆都不報
I8 寫好後,用 --retire-days 0 在真的 vault 上跑,預期那 9 篇退場檔全部被列出來,結果一筆都沒有。
先用 lint.py 自己的連結解析,把「誰連到這 9 篇」印出來,才發現每一篇都有 wiki 連入,例如:
openclaw.md -> ['engineer/wiki/multi-agent-orchestration.md ←[[openclaw]]', 'engineer/wiki/openclaw.md ←[[engineer/raw/openclaw]]']
multica.md -> ['engineer/wiki/multi-agent-orchestration.md ←[[multica]]']
這就是上面「當天被撤回」的由來:同一天的 wiki-build 已經把它們引為來源。I8 不報是對的,程式沒錯。邏輯本身另外在暫存區建了一個小 vault 驗證:缺欄位的報 W6,滿期又沒連入的報 I8;有連入、未滿期、單域模式都不報。
小結
raw 現在有一條完整的退場路線:
- 用不到的 raw 標
status: retired,必須寫日期和理由,原地保留,不再算堆積。 - 退場滿 180 天又沒有任何頁連到它,健檢列出「建議刪除」,由人確認後才刪,刪了用 git 找得回來。
- 退場太多時,狀態列會提醒。
第一批 9 篇剛標完退場,當天就被 wiki 收走,這反而是比較好的結局:先萃取成 wiki,過時與否在 wiki 層比較看得見。退場是給真的不打算萃取的 raw 用的;因為 raw 不在索引裡,很容易被人和 AI 一起忽略,所以不能讓它一直堆著。而刪不刪的判斷可能隨時改變,所以天數是參數,刪除永遠留給人確認。
