AI 工具 21 min read

個人知識庫怎麼讓 AI 真的用上?Obsidian + Claude Code hook 讀寫兩條路實作

我的個人知識庫是一個 258 張卡片的 Obsidian vault,由 Claude Code 的 Stop hook 排隊、每晚用 claude -p 整理成卡片,再由 UserPromptSubmit hook 用 BM25 在每次提問時撈出相關卡片注入 context。六週 51 封日報共寫入 142 張卡,檢索每次約 0.5 到 0.9 秒——字面比對撈得準,換個說法就撈不到,是目前最大的缺口。

個人知識庫怎麼讓 AI 真的用上?Obsidian + Claude Code hook 讀寫兩條路實作
本文目錄 · 9

我的個人知識庫是一個 Obsidian vault,裡面放了 258 張 markdown 卡片,寫的人是 Claude Code。它有兩條路:寫入由 session 結束時的 hook 排隊,每晚交給 claude -p 整理成卡片;讀出由每次提問時的 hook 用 BM25 比對卡片,把最相關的幾張路徑塞進 Claude 的 context。兩條路都是 Claude Code 官方的 hook 機制加上兩支 bash 腳本,不需要另外架伺服器。

這篇先整理個人知識庫有哪幾種公開的做法,再講我這套怎麼蓋、數字長什麼樣、還有哪些地方沒做好。

個人知識庫有哪幾種做法?

我參考的是四份公開的來源,它們各自回答的問題不一樣,可以疊著用:

做法來源核心概念誰負責整理
Building a Second BrainTiago Forte(Forte Labs)CODE 四步驟:Capture、Organize、Distill、Express;用 PARA 分類:Projects、Areas、Resources、Archive人
LLM WikiAndrej Karpathy 的 gist(2026-04-04)三層:raw sources(不可改的原始資料)、wiki(LLM 寫、LLM 維護的 markdown)、schema(CLAUDE.md 這類規則檔);三個操作:ingest、query、lintLLM
RAG向量資料庫類工具把文件切塊、轉成向量,提問時用語意相似度撈回片段程式(不整理,只索引)
CLAUDE.md + auto memoryClaude Code 官方文件CLAUDE.md 由人寫規則;auto memory 由 Claude 自己記,MEMORY.md 每次開場載入前 200 行或 25KB人+Claude
Karpathy 在 gist 裡對 wiki 的描述是「a persistent, compounding artifact」,也就是知識會累積,不用每次重新從原始檔推一遍。RAG 則是每次提問都回原始資料撈片段。這兩種做法我在 LLM Wiki 跟 RAG 差在哪 那篇有實測比較過。

我的做法偏向 LLM Wiki:原始資料是每場 Claude Code 對話,wiki 是 Claude 整理出的卡片,schema 是寫在 CLAUDE.md 裡的寫卡規則。另外多補了一條「讀出」的自動化,下面會講為什麼需要它。

我的知識庫長什麼樣?

Stop hook
obsidian-sync.sh

每晚排程
vault-nightly.sh

UserPromptSubmit hook
vault-recall.sh

注入 context

Claude Code 對話

佇列 + 濃縮文字
.queue / .inbox

claude -p 判斷
新增 / 補充 / SKIP

Obsidian vault
258 張卡片

lint + 日報信

BM25 比對
取前 5 張

下一次提問

vault 的位置是 ~/.claude/memory/,用 Obsidian 開就是一個普通的筆記庫。目前的分布:

資料夾卡片數放什麼
feedback/130我給 Claude 的行為規範與糾正
reference/52外部系統、部署流程與工具設定
projects/47各專案的進行狀態與決策
debugging/21踩過的坑與排查步驟
其他(moc、teaching、user、meta)8索引頁與少量分類
根目錄0只放索引 MEMORY.md
每張卡片開頭有 frontmatter,name 和 description 是給檢索用的摘要,正文才是細節。

整套怎麼運作?條列總覽

先用條列把整套講完,後面的段落再逐一拆細節。

存放

  • 本體是 ~/.claude/memory/,一個 Obsidian vault,一張卡一個 .md 檔,目前 258 張。
  • 卡片依類型放進子資料夾:projects/、feedback/、reference/、debugging/、user/、teaching/,moc/ 放總覽頁(知識總覽、專案總覽、時間軸),meta/ 放這個 vault 自己的說明。
  • 根目錄只放索引 MEMORY.md,每張卡一行。
  • 每張卡有 frontmatter(name、description、type、tags),結尾有「## 相關」區塊用 [[卡名]] 連到別張卡,卡片之間是網狀的。
  • 同一件事只有一張主卡。衍生出來的卡標成 type: subproject 加 parent:,或標成已併入,卡上放一條連回主卡的提示,不直接刪掉。
讀出:每次提問時
  • UserPromptSubmit hook vault-recall.sh 用 BM25 比對所有卡片,把最相關幾張的路徑和摘要注入 Claude 的 context。
  • 不會整個 vault 載入,只帶相關的卡,卡片變多也不會撐爆 context。
  • Claude 看到相關卡片後,會先 Read 全文再回答。
寫入:對話結束後
  • Stop hook obsidian-sync.sh 判斷這場對話有沒有值得記的內容,有就排進佇列 .queue/pending.jsonl,濃縮文字放在 .inbox/。
  • 每晚 vault-nightly.sh 用 claude -p 消化佇列:能擴充舊卡就不開新卡,新卡一律放進正確的子資料夾、補上 tags 和「## 相關」。
  • 還沒消化的佇列,下次開 session 時會由 hook 提醒。對話中也可以直接叫 Claude 寫卡。
跨機同步:兩台電腦共用一份
  • sync.sh 把 ~/.claude/ 同步到兩個私有 GitHub repo:一個放 skills、scripts、rules 這類設定,一個放 memory 和 settings。
  • 預設是雙向:本機變更先推上 repo,用 git 合併其他機器的變更,再寫回本機。
  • 一次要刪超過 30 個檔就中止,必須加 --force 才會執行,避免誤刪。
  • 根目錄若出現跟子資料夾同名的舊卡,同步時直接略過,不推上 repo。
品質把關
  • vault-lint.sh 檢查缺 frontmatter、缺 tags、檔名重複、孤島卡(沒有任何連結),這四項一律要修。
  • 指向還沒建立的卡的連結是預期狀態,代表「這件事值得之後寫一張」;章節太長只是建議拆分。

寫入:對話結束時排隊,每晚整理成卡片

Stop hook 決定這場對話要不要排隊

Claude Code 每回合結束會觸發 Stop hook。我的 obsidian-sync.sh 讀這場對話的 transcript,條件只有兩個:

  • 我實際提問至少 2 輪
  • Claude 的文字回覆累積至少 60 個字元
符合就寫進 ~/.claude/memory/.queue/pending.jsonl,同時把整場對話濃縮成純文字存到 .inbox/<session_id>.txt。存濃縮檔的原因是我有兩台電腦共用同一個 vault,佇列記的 transcript 路徑在另一台機器上不存在,濃縮文字放在 .inbox/ 會跟著 vault 一起同步,負責整理的那台機器就能直接讀。

這個 hook 不擋 Claude 收尾,也不在當下寫卡。session 結束的那一刻通常是最趕的時候,我讓它只做排隊,寫卡留給晚上。

每晚排程用 claude -p 判斷要不要寫卡

vault-nightly.sh 由排程器每晚執行,分四個階段:

  1. 消化佇列:每一筆濃縮文字交給 claude -p,依寫卡規則判斷。vault 裡已經有同主題的卡就補充進去,沒有才開新卡,判斷不出值得長期保存就 SKIP。
  2. 大整理:跑 vault-lint.sh 檢查格式,重算索引頁的統計數字。
  3. 快照:有 git repo 的機器做一次 commit;沒有的機器跳過,跨機同步另外走 rsync。
  4. 寄日報:把當天寫了哪幾張卡、佇列剩多少寄到我的 Gmail。
claude -p 是 Claude Code 的非互動模式,用的是訂閱額度。額度用完時腳本會換第二個帳號重試同一筆,帳號清單寫在腳本開頭。

從 2026-09-07 到 10-19 的日報信一共 51 封,合計寫入 142 張卡。每晚寫入數量不平均:

期間每晚寫入說明
09/07–09/090–4 張,多數是 0排程器給的 PATH 找不到 timeout、python3,每筆都失敗
09/1617 張PATH 修好後一次消化累積的佇列
09/22–10/18多數 1–7 張穩定期,10/16 有 11 張
10/190 張4 筆都回 You've hit your weekly limit,之後加了換帳號重試
51 封裡有 18 封是「寫入 0 張」。除了上表的兩段故障,其餘多半是當天的對話被判斷成 SKIP,日誌裡常見的理由是「內容已由某張卡完整涵蓋」。SKIP 本身是正常結果,代表它在查重。

排程器的 PATH 這件事值得多講一句:macOS 的 launchd 和 Windows 工作排程器給的 PATH 都比互動 shell 少很多,手動跑一切正常、排程跑就整輪失敗。Windows 還有一個同名陷阱,C:\Windows\System32\timeout.exe 是「等待 N 秒」,跟 GNU coreutils 的 timeout(限時執行指令)語意完全不同。我現在讓腳本一啟動就檢查 timeout --help 有沒有 --kill-after,沒有就直接停。

讀出:每次提問用 BM25 撈相關卡片

為什麼需要另外一條「讀出」

寫進去的卡片,Claude 不一定會讀。Claude Code 官方文件對 auto memory 的說明是:

The first 200 lines of MEMORY.md, or the first 25KB, whichever comes first, are loaded at the start of every conversation.

我的 vault 索引 MEMORY.md 有 465 行,而且這個 vault 不是 Claude Code 預設的 auto memory 目錄,本來就不會自動載入。卡片本體則是需要時才讀。所以如果沒有東西提醒 Claude「這題 vault 裡有一張相關的卡」,它多半不會想到去翻。

UserPromptSubmit hook 把相關卡片放進 context

Claude Code 的 hooks 文件 寫明,UserPromptSubmit 是少數會把 stdout 當成 context 的事件:

The exceptions are UserPromptSubmit, UserPromptExpansion, SessionStart, and PostModelSwitch, where Claude Code adds plain-text stdout as context that Claude can see and act on.

所以 vault-recall.sh 只要把找到的卡片印到 stdout,Claude 在回答這一題之前就會看到。在 ~/.claude/settings.json 註冊的方式是:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$HOME/.claude/scripts/hooks/vault-recall.sh\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

比對方式:BM25 加上中文 bigram

我選 BM25 是因為它不需要 embedding 模型、不需要常駐服務,Python 標準函式庫就寫得出來。幾個關鍵設計:

  • 斷詞:英數字整個字當一個詞;中文切成相鄰兩字一組(bigram),例如「向量索引」切成「向量」「量索」「索引」。不用斷詞套件。
  • 兩個欄位:正文用 BM25 算詞頻分數;frontmatter 的 name、description、tags 加上檔名和標題算標題欄,提問詞只要出現在標題欄就加 3 分(不看次數),再和正文分數一起乘上該詞的 IDF。正文單一詞的 BM25 分數最高約 2.2,所以命中標題欄比正文出現很多次更有份量。
  • 門檻:分數低於 15 不注入;只保留分數至少是第一名一半的卡片;至少要命中 2 個不同的提問詞;最多 5 張。
  • 只給路徑和摘要:不把卡片全文塞進 context,Claude 判斷相關才自己去讀。
def tokens(text):
    text = text.lower()
    out = [w for w in re.findall(r"[a-z0-9][a-z0-9_\-\.]*[a-z0-9]", text)
           if w not in ASCII_STOP]
    for run in re.findall(f"[{CJK}]+", text):
        out.extend(run[i:i + 2] for i in range(len(run) - 1))
    return out

實際跑起來的樣子

下面是今天直接餵三個問題給這支 hook 的輸出,路徑開頭的使用者目錄我換成 ~:

== mempalace 向量索引壞了怎麼修
【vault 相關卡片】依本次提問比對 Obsidian vault(~/.claude/memory)找到下列卡片。與本題相關的先 Read 全文再判斷;不相關就忽略:
- ~\.claude\memory\feedback\feedback_mempalace_benign_warnings.md — mempalace 用非零 exit 表達 benign 警告(如 HNSW flush-lag),需 wrapper 過濾,debug 前先看訊息內容判斷
- ~\.claude\memory\debugging\debugging_mempalace_hnsw_crash.md — mempalace tool call 立刻 Connection closed、stderr 無 Python traceback 時,是 HNSW native segment crash + 殭屍 process,照此 SOP 修
- ~\.claude\memory\meta\_reflection_2026-05-21.md — 反思 memory 為什麼長成花朵狀 / 內容重疊 / 缺 cross-link。分析根因 + 改進方案,作為後續整理 memory 跟改 CLAUDE.md 的依據
(608 ms)
== vite 部署到自訂網域 base 路徑要怎麼設
【vault 相關卡片】...
- ~\.claude\memory\reference\reference_firebase_custom_domain_cloudflare.md — Firebase Hosting 接自訂網域(DNS 在 Cloudflare)的最短路徑 SOP ...
- ~\.claude\memory\feedback\feedback_vite_base_custom_domain.md — Vite + GitHub Pages 換 custom domain 要同步改 base + 寫死絕對 URL ...
(856 ms)
== 好的繼續
(811 ms)
終端機執行 vault-recall.sh 的輸出:三個提問中兩個撈到相關卡片,「好的繼續」沒有輸出

前兩題撈到的都是對的卡,第一題的第三張反思卡算是雜訊。「好的繼續」這種沒有內容的回覆不會注入任何東西,這是門檻設計想要的結果。每次執行約 0.5 到 0.9 秒,卡片每次都重新讀檔計算(實測時是 372 張),沒有做索引快取。

這套跟 LLM Wiki、RAG 的對應關係

LLM Wiki 的概念我的實作
raw sourcesClaude Code 的對話 transcript,以及 .inbox/ 的濃縮文字
wiki~/.claude/memory/ 裡的卡片
schemaCLAUDE.md 與寫卡規則
ingestvault-nightly.sh 階段一
lintvault-lint.sh
queryvault-recall.sh,以及 Claude 自己 Read 卡片
RAG 那一層我另外裝了 mempalace(底層是 ChromaDB),把 ~/.claude/ 底下的檔案索引成 9,790 筆。但寫這篇時查它的狀態,向量檢索是停用的:HNSW 索引只有 9,000 筆,跟 SQLite 裡的 9,790 筆對不上,工具自己判定要先跑 mempalace repair 重建才能用。所以目前每天真正在運作的是 BM25 這條。

還沒解決的問題

  • BM25 只認字面:提問用的詞跟卡片寫的詞不一樣時就撈不到。vault 裡明明有排程器 PATH 的紀錄,但我問「為什麼排程跑不起來」時沒有任何卡片過門檻;換成「排程的時候指令找不到」,撈回來的只有一張不相關的反思卡。我評估的下一步是用本機的 embedding 模型(例如 Ollama 跑 bge-m3)做混合檢索,還沒實作。
  • 整理靠訂閱額度:claude -p 吃的是 Claude 訂閱額度,週額度用完那晚就整理不了。換帳號是止血,不是根治。
  • 索引超過 200 行:MEMORY.md 465 行,如果改用官方 auto memory 目錄,超過的部分不會載入。這也是我把檢索交給 hook,不依賴索引整份被讀的原因。

想自己做一套,可以從哪裡開始?

依我自己的經驗,順序可以這樣排:

  1. 先用官方的 CLAUDE.md 和 auto memory。卡片少的時候,200 行索引放得下,不需要任何 hook。
  2. 卡片多到索引放不下時,加 UserPromptSubmit hook 做檢索。BM25 一個 Python 檔就夠,先確認每次注入的卡片是對的,再考慮換向量。
  3. 不想每次手動叫 Claude 記筆記時,加 Stop hook 排隊。排隊和寫卡分開,Stop hook 本身只做輕量的事。
  4. 最後才加夜間整理。排程器的 PATH、執行逾時、額度用完,每一個都要在腳本開頭先檢查,不然會連續幾晚空轉才發現。
整理卡片本身的品質是另一個題目,我在 Memory 治理 EP1 寫過怎麼清理寫壞的卡,在 EP2 寫過為什麼有些規則寫成卡片 AI 還是不會照做。

常見問題

一定要用 Obsidian 嗎?

不用。卡片就是一般的 markdown 檔,hook 只讀檔案系統。Obsidian 的用處是讓我自己翻卡片、看連結關係比較方便。

為什麼不直接用向量資料庫?

向量檢索需要 embedding 模型和索引維護。我裝的 mempalace 就遇過索引跟資料筆數對不上、需要重建的狀況。BM25 每次直接讀 markdown 計算,沒有額外的狀態要維護,卡片數在幾百張的規模時速度也夠。

UserPromptSubmit hook 會不會拖慢每次提問?

我的實測是每次 0.5 到 0.9 秒,當時 372 張卡。hook 有設 10 秒逾時。卡片數再多一個數量級時,就需要把斷詞結果快取起來,不要每次重算。

夜間整理會不會把錯的東西寫進卡片?

會有這個風險,因為判斷寫什麼的是 LLM。我的做法是每晚寄日報列出寫了哪幾張卡,自己看標題抽查;寫卡規則要求先找同主題的卡補充,不要一直開新卡。

author
陳
陳彥彤

AI 工程師 · AI 顧問。Java 後端 8 年、AI 工程師 2 年。AI 內訓 · AI 導入顧問 · 前後端與雲端培訓。

support

覺得文章有用可以到 GitHub 給個 star,或是透過信箱聊聊 AI 內訓、AI 導入顧問或前後端 / 雲端培訓。

related

相關文章

[AI 教學] · 17min
LLM Wiki 實作:Claude Code + Obsidian 把 12 份筆記編成 75 頁知識庫,附 CLAUDE.md 與三個指令
Karpathy 的 LLM Wiki 起手包其實只有兩個資料夾、一份 CLAUDE.md 和 ingest/query/lint 三個指令,Obsidian 只負責畫關聯圖。我在 Windows 上用 claude -p 跑 12 份研究報告,632 秒長出 73 頁,lint 再補成 75 頁、207 條連結——前 4 份只建新頁,第 5 份起才開始回頭改舊頁,連結就是這樣變密的。
[AI 工具] · 31min
LLM Wiki 跟 RAG 差在哪?Karpathy 那套我跑兩週實測 token 降 87%
2026 年 4 月 Karpathy 在 gist 丟了一份 200 行 markdown,一週後全網炸開、12 個社群 implementation。一句話講:讓 LLM 把你丟的所有資料「編譯」成一個結構化的 markdown 知識庫,以後問問題不查原始檔、查這個被整理過的 wiki。本文拆解 RAG vs LLM Wiki 的編譯式/解釋式之差、三層架構、30 分鐘上手路徑、我把 7 場教學逐字稿 + 200 則 Discord QA 丟進去跑兩週的實測(token 降 87%)、5 個踩坑、什麼情境裝了反而是負擔。
[AI 教學] · 12min
個人知識庫怎麼建、怎麼用?CODE 四步、PARA 與卡片盒,加上 196 筆 LINE 交辦整理成 7 類的實測
個人知識庫是寫給未來自己的筆記系統,照 Tiago Forte 的 CODE 四步建,整理時用 PARA 按行動性分四格,或用卡片盒一張卡一個想法。我把 2026-10-22 到 11-08 的 196 筆 LINE 交辦串成 92 條任務鏈、分成固定 7 類,關鍵字規則跑完仍有 25 條鏈要人工改判——規則適合當第一輪,不能直接當結果。