obsidian:~/vault$ search --hybrid obsidian

Example vault location

#

words: 2547 read_time: 41m updated: 2026-03-02 07:55
$ retriever search --hybrid obsidian

重點摘要

重點在於上下文工程,而非筆記記錄。 Obsidian 知識庫對 AI 的價值不在於筆記本身,而在於讓筆記可被查詢的檢索層。一個擁有 16,000 個檔案但沒有檢索功能的知識庫,只是一個只寫不讀的資料庫。一個僅有 200 個檔案但具備混合搜尋與 MCP 整合的知識庫,才是真正的 AI 知識庫。檢索基礎設施才是產品,筆記只是原料。

混合檢索優於純關鍵字或純語意搜尋。 BM25 能精確捕捉識別符與函式名稱。向量搜尋能捕捉同義詞及跨不同術語的概念匹配。Reciprocal Rank Fusion(RRF)將兩者合併,無需進行分數校準。任何單一方法都無法涵蓋兩種失敗模式。MS MARCO 段落排序的研究證實了這個模式:混合檢索的效能持續優於單獨使用任一方法。1 混合檢索器深度解析涵蓋了 RRF 數學原理、使用真實數據的範例、失敗模式分析,以及互動式融合計算器。

MCP 讓 AI 工具直接存取知識庫。 Model Context Protocol(MCP)伺服器將檢索器公開為工具,使 Claude Code、Codex CLI、Cursor 及其他 AI 工具能直接呼叫。代理查詢知識庫,接收帶有來源歸屬的排序結果,並在不載入完整檔案的情況下使用上下文。MCP 伺服器是檢索引擎之上的薄封裝層。

本機優先意味著零 API 成本與完整隱私。 整個技術堆疊在單一機器上運行:SQLite 負責儲存、Model2Vec 負責嵌入向量、FTS5 負責關鍵字搜尋、sqlite-vec 負責向量 KNN。無需雲端服務、無需 API 呼叫、無網路依賴。個人筆記永遠不會離開您的機器。完整重新嵌入 49,746 個區塊以 OpenAI API 價格計算約需 $0.30,但真正的成本在於延遲、隱私暴露,以及對一個應該能離線運作的系統產生網路依賴。2

增量索引讓系統在 10 秒內保持最新。 透過檔案修改時間比對來偵測變更。只有修改過的檔案才會重新分塊與重新嵌入。在 Apple M 系列硬體上,完整重新索引約需四分鐘。一天中的典型編輯量進行增量更新只需不到十秒。系統無需手動介入即可保持最新。

此架構可從 200 篇筆記擴展至 20,000 篇以上。 相同的三層設計(擷取、檢索、整合)適用於任何知識庫規模。從小型知識庫的 BM25 純關鍵字搜尋開始。當關鍵字碰撞成為問題時,加入向量搜尋。當您同時需要精確匹配與語意匹配時,加入 RRF 融合。每一層都可獨立使用,也可獨立移除。


如何使用本指南

本指南涵蓋完整系統。您的起點取決於您目前的狀態:

您是… 從這裡開始 然後探索
Obsidian + AI 新手 為什麼選擇 Obsidian 作為 AI 基礎設施快速開始 知識庫架構MCP 伺服器架構
已有知識庫,想接入 AI MCP 伺服器架構Claude Code 整合 嵌入模型全文搜尋
正在建構檢索系統 完整檢索管線Reciprocal Rank Fusion 效能調校疑難排解
團隊或企業情境 決策框架知識圖譜模式 開發者工作流程食譜遷移指南

標記為合約的章節包含實作細節、設定區塊與失敗模式。標記為敘事的章節聚焦於概念、架構決策,以及設計選擇背後的推理。標記為食譜的章節提供逐步工作流程。


為什麼選擇 Obsidian 作為 AI 基礎設施

本指南的核心論點:Obsidian 知識庫是個人 AI 知識庫的最佳基底,因為它是本機優先、純文字、圖結構的,且使用者可控制技術堆疊的每一層。

Obsidian 提供了其他替代方案所沒有的 AI 優勢

純文字 Markdown 檔案。 每篇筆記都是檔案系統上的 .md 檔案。沒有專有格式、不需要資料庫匯出、不需要 API 就能讀取內容。任何能讀取檔案的工具都能讀取您的知識庫。grepripgrep、Python 的 pathlib、SQLite FTS5——它們都能直接處理原始檔案。當您建構檢索系統時,您索引的是檔案,而非 API 回應。索引始終與原始檔案保持一致,因為來源就是檔案系統。

本機優先架構。 知識庫存在於您的機器上。沒有伺服器、不依賴雲端同步、沒有 API 速率限制、沒有管控您如何處理自己內容的服務條款。您可以在不使用任何外部服務的情況下對筆記進行嵌入、索引、分塊與搜尋。這對 AI 基礎設施至關重要,因為檢索管線的速度取決於您的磁碟效能,而非 API 端點的回應速度。這對隱私同樣重要:包含憑證、健康資料、財務資訊及個人反思的筆記永遠不會離開您的機器。

透過 wiki-link 建立的圖結構。 Obsidian 的 [[wiki-link]] 語法在筆記之間建立有向圖。一篇關於 OAuth 實作的筆記會連結到關於權杖輪換、工作階段管理與 API 安全性的筆記。圖結構編碼了人為策劃的概念間關係。向量嵌入捕捉語意相似性,但 wiki-link 捕捉的是作者在思考主題時建立的有意識連結。這是嵌入向量無法複製的訊號。

外掛生態系統。 Obsidian 擁有超過 1,800 個社群外掛。Dataview 讓您像查詢資料庫一樣查詢知識庫。Templater 使用 JavaScript 邏輯從範本生成筆記。Git 整合將知識庫同步至儲存庫。Linter 確保格式一致性。這些外掛為知識庫增添結構,而不改變底層的純文字格式。檢索系統索引的是這些外掛的輸出,而非外掛本身。

超過 500 萬使用者。 Obsidian 擁有龐大的活躍社群,產出範本、工作流程、外掛與文件。當您遇到知識庫組織或外掛設定的問題時,很可能已有人記錄了解決方案。社群也產出 Obsidian 周邊工具:MCP 伺服器、索引腳本、發布管線與 API 封裝器。

單純的檔案系統無法提供的功能

一個 Markdown 檔案的目錄具備純文字優勢,但缺少 Obsidian 增添的三項功能:

  1. 雙向連結。 Obsidian 自動追蹤反向連結。當您從筆記 A 連結到筆記 B 時,筆記 B 會顯示筆記 A 引用了它。圖面板以視覺化方式呈現連結叢集。這種雙向感知是純檔案系統無法提供的後設資料。

  2. 帶有外掛渲染的即時預覽。 Dataview 查詢、Mermaid 圖表和標註區塊即時渲染。書寫體驗比文字編輯器更豐富,而儲存格式仍然是純文字。您在豐富的環境中書寫與組織;檢索系統索引的是原始 Markdown。

  3. 社群基礎設施。 外掛探索、主題市集、同步服務(選用)、發布服務(選用),以及文件生態系統。您可以使用獨立工具複製任何單一功能,但 Obsidian 將它們整合為一套連貫的工作流程。

Obsidian 不做的事(以及您需要建構的部分)

Obsidian 不包含檢索基礎設施。它具備基本搜尋功能(全文、檔案名稱、標籤),但沒有嵌入管線、沒有向量搜尋、沒有融合排序、沒有 MCP 伺服器、沒有憑證過濾、沒有分塊策略,也沒有與外部 AI 工具的整合鉤子。本指南涵蓋您在 Obsidian 之上建構的基礎設施。 知識庫是基底。檢索管線、MCP 伺服器與整合鉤子才是基礎設施。

此處描述的架構是以 Markdown 為優先,而非 Obsidian 獨佔。 如果您使用 Logseq、Foam、Dendron 或純 Markdown 檔案目錄,檢索管線的運作方式完全相同。分塊器讀取 .md 檔案。嵌入器處理文字字串。索引器寫入 SQLite。這些元件都不依賴 Obsidian 特定功能。Obsidian 的貢獻在於提供書寫與組織環境,產出檢索器所索引的 Markdown 檔案。


快速開始:第一個 AI 連接的知識庫

本節將在五分鐘內將知識庫連接到 AI 工具。您將安裝 Obsidian、建立知識庫、安裝 MCP 伺服器,並執行第一次查詢。快速開始使用社群 MCP 伺服器以獲得即時結果。後續章節將涵蓋建構用於生產環境的自訂檢索管線。

先決條件

  • macOS、Linux 或 Windows
  • Node.js 18+(用於 MCP 伺服器)
  • 已安裝 Claude Code、Codex CLI 或 Cursor

步驟 1:建立知識庫

obsidian.md 下載 Obsidian 並建立新的知識庫。選擇一個您能記住的位置——MCP 伺服器需要絕對路徑。

# Example vault location
~/Documents/knowledge-base/

新增幾則筆記,讓檢索器有內容可以處理。即使只有 10-20 則筆記也足以看到結果。每則筆記應為 .md 檔案,具有有意義的標題和至少一段內容。

步驟 2:安裝 MCP 伺服器

obsidian-mcp 社群伺服器提供即時的知識庫存取。安裝方式:

npm install -g obsidian-mcp-server

步驟 3:設定您的 AI 工具

Claude Code — 新增至 ~/.claude/settings.json

{
  "mcpServers": {
    "obsidian": {
      "command": "obsidian-mcp-server",
      "args": ["--vault", "/absolute/path/to/your/vault"]
    }
  }
}

Codex CLI — 新增至 .codex/config.toml

[mcp_servers.obsidian]
command = "obsidian-mcp-server"
args = ["--vault", "/absolute/path/to/your/vault"]

Cursor — 新增至 .cursor/mcp.json

{
  "mcpServers": {
    "obsidian": {
      "command": "obsidian-mcp-server",
      "args": ["--vault", "/absolute/path/to/your/vault"]
    }
  }
}

步驟 4:執行第一次查詢

開啟您的 AI 工具,詢問一個您的知識庫筆記能夠回答的問題:

Search my Obsidian vault for notes about [topic you wrote about]

AI 工具會呼叫 MCP 伺服器,搜尋您的知識庫並回傳匹配的內容。您應該會看到包含檔案路徑和相關摘錄的結果。

您剛才建構了什麼

您透過標準協定將本機知識庫連接到了 AI 工具。MCP 伺服器讀取您的知識庫檔案、執行基本搜尋並回傳結果。這是最小可行版本。

此快速開始未提供的功能: - Hybrid 檢索(BM25 + 向量搜尋 + RRF 融合) - 基於 Embedding 的語意搜尋 - 憑證過濾 - 增量索引 - 基於 Hook 的自動上下文注入

本指南的其餘部分將涵蓋建構上述每項功能。快速開始驗證了概念。完整管線提供生產品質的檢索。


決策框架:Obsidian 與替代方案比較

並非每個使用場景都需要 Obsidian。本節說明何時 Obsidian 是正確的基礎架構、何時大材小用,以及何時其他工具更為合適。

決策樹

START: What is your primary content type?

├─ Structured data (tables, records, schemas)
   Use a database. SQLite, PostgreSQL, or a spreadsheet.
   Obsidian is for prose, not tabular data.

├─ Ephemeral context (current project, temporary notes)
   Use CLAUDE.md / AGENTS.md in the project repo.
   These travel with the code and reset per project.

├─ Team wiki (shared documentation, onboarding)
   Evaluate Notion, Confluence, or a shared git repo.
   Obsidian vaults are personal-first. Team sync is possible
    but not native.

└─ Growing personal knowledge corpus
   
   ├─ < 50 notes
      A folder of markdown files + grep is sufficient.
      Obsidian adds value mainly through the link graph,
       which needs density to be useful.
   
   ├─ 50 - 500 notes
      Obsidian adds value. Wiki-links create a navigable graph.
      BM25-only search (FTS5) is sufficient at this scale.
      Skip vector search and RRF until keyword collisions appear.
   
   ├─ 500 - 5,000 notes
      Full hybrid retrieval becomes valuable. Keyword collisions
       increase. Semantic search catches queries that BM25 misses.
      Add vector search + RRF fusion at this scale.
   
   └─ 5,000+ notes
       Full pipeline is essential. BM25-only returns too much noise.
       Credential filtering becomes critical (more notes = more
        accidentally pasted secrets).
       Incremental indexing matters (full reindex takes minutes).
       MCP integration pays dividends on every AI interaction.

比較矩陣

標準 Obsidian Notion Apple Notes 純檔案系統 CLAUDE.md
本機優先 否(雲端) 部分(iCloud)
純文字 是(markdown) 否(區塊) 否(專有格式)
圖譜結構 是(wiki-links) 部分(提及)
AI 可索引 直接存取檔案 需要 API 需要匯出 直接存取檔案 已在上下文中
外掛生態系統 1,800+ 外掛 整合功能 不適用 不適用
離線使用 完整支援 僅快取唯讀 部分 完整支援 完整支援
擴展至 10K+ 筆記 是(需 API) 效能下降 否(單一檔案)
費用 免費(核心) $10/月起 免費 免費 免費

何時 Obsidian 大材小用

  • 單一專案上下文。 如果 AI 僅需要當前程式碼庫的上下文,請將其放在 CLAUDE.mdAGENTS.md 或專案層級文件中。這些檔案隨程式碼庫移動,並自動載入。
  • 結構化資料。 如果內容是表格、記錄或綱要,請使用資料庫。Obsidian 筆記以文章為主。Dataview 可以查詢 frontmatter 欄位,但真正的資料庫更能處理結構化查詢。
  • 臨時研究。 如果筆記在專案結束後將被捨棄,使用包含 markdown 檔案的暫存目錄更為簡單。不要為臨時性內容建構檢索基礎設施。

何時 Obsidian 是正確選擇

  • 長期累積知識。 隨著語料庫成長,價值會不斷累積。一個 200 則筆記的知識庫每天查詢持續六個月,比一個 5,000 則筆記的知識庫僅查詢一次提供更多價值。
  • 單一語料庫涵蓋多個領域。 一個包含程式設計、架構、安全、設計和個人專案筆記的知識庫,受益於跨領域檢索——這是專案特定的 CLAUDE.md 無法提供的。
  • 隱私敏感內容。 本機優先意味著檢索管線永遠不會將內容傳送至外部服務。知識庫包含您放入的任何內容,包括您不願上傳至雲端服務的內容。

心智模型:三層架構

系統有三個獨立運作但結合後效果倍增的層級。每個層級有不同的關注點和不同的失敗模式。

┌─────────────────────────────────────────────────────┐
                 INTEGRATION LAYER                     
  MCP servers, hooks, skills, context injection        
  Concern: delivering context to AI tools              
  Failure: wrong context, too much context, stale      
└──────────────────────┬──────────────────────────────┘
                        query + ranked results
┌──────────────────────┴──────────────────────────────┐
                  RETRIEVAL LAYER                      
  BM25, vector KNN, RRF fusion, token budget           
  Concern: finding the right content for any query     
  Failure: wrong ranking, missed results, slow queries 
└──────────────────────┬──────────────────────────────┘
                        chunked, embedded, indexed
┌──────────────────────┴──────────────────────────────┐
                   INTAKE LAYER                        
  Note creation, signal triage, vault organization     
  Concern: what enters the vault and how it's stored   │
  Failure: noise, duplicates, missing structure        
└─────────────────────────────────────────────────────┘

收錄層(Intake) 決定什麼進入知識庫。若缺乏篩選,知識庫會累積雜訊:推文截圖、無註解的複製貼上文章、缺少上下文的半成品想法。收錄層負責在進入點進行品質控管。評分管線、標籤慣例或人工審核流程——任何確保知識庫包含值得檢索內容的機制。

檢索層(Retrieval) 使知識庫可查詢。這是引擎:將筆記分塊為搜尋單位、將分塊嵌入向量空間、建立關鍵字和語意搜尋索引、使用 RRF 融合結果。檢索層將一個檔案目錄轉變為可查詢的知識庫。缺少此層級,知識庫只能透過手動瀏覽和基本搜尋導覽,而無法被 AI 工具程式化存取。

整合層(Integration) 將檢索層連接到 AI 工具。MCP 伺服器將檢索公開為可呼叫的工具。Hook 自動注入上下文。Skill 將新知識擷取回知識庫。整合層是知識庫與使用它的 AI 代理之間的介面。

各層級在設計上是解耦的。收錄評分管線不知道 Embedding 的存在。檢索器不知道信號路由規則。MCP 伺服器不知道筆記是如何建立的。這種解耦意味著您可以獨立改進任何層級。替換 Embedding 模型而不需更改收錄管線。新增 MCP 功能而不需修改檢索器。更改信號評分啟發式規則而不需觸及索引。


適用於 AI 檢索的 Vault 架構

針對 AI 檢索最佳化的 vault 與針對個人瀏覽最佳化的 vault 遵循不同的慣例。本節涵蓋資料夾結構、筆記綱要、frontmatter 慣例,以及能提升檢索品質的特定模式。

資料夾結構

使用數字前綴為頂層資料夾建立可預測的組織層級。數字並不代表優先順序——它們將相關領域分組,並使結構易於瀏覽。

vault/
├── 00-inbox/              # Unsorted captures, pending triage
├── 01-projects/           # Active project notes
├── 02-areas/              # Ongoing areas of responsibility
├── 03-resources/          # Reference material by topic
   ├── programming/
   ├── security/
   ├── ai-engineering/
   ├── design/
   └── devops/
├── 04-archive/            # Completed projects, old references
├── 05-signals/            # Scored signal intake
   ├── ai-tooling/
   ├── security/
   ├── systems/
   └── ...12 domain folders
├── 06-daily/              # Daily notes (if used)
├── 07-templates/          # Note templates (excluded from index)
├── 08-attachments/        # Images, PDFs (excluded from index)
├── .obsidian/             # Obsidian config (excluded from index)
└── .indexignore            # Paths to exclude from retrieval index

應建立索引的資料夾:所有包含 markdown 文章的資料夾——專案、領域、資源、信號、每日筆記。

應排除在索引之外的資料夾:範本(包含佔位變數而非內容)、附件(二進位檔案)、Obsidian 設定檔,以及任何包含您不希望出現在檢索索引中的敏感內容的資料夾。

.indexignore 檔案

在 vault 根目錄建立 .indexignore 檔案,明確排除不需要納入檢索索引的路徑。語法與 .gitignore 相同:

# Obsidian internal
.obsidian/

# Templates contain placeholders, not content
07-templates/

# Binary attachments
08-attachments/

# Personal health/medical notes
02-areas/health/

# Financial records
02-areas/finance/personal/

# Career documents (resumes, salary data)
02-areas/career/private/

索引器在掃描前會讀取此檔案,並完全跳過匹配的路徑。被排除路徑中的檔案永遠不會被分塊、不會被嵌入,也不會出現在搜尋結果中。

筆記綱要

每篇筆記都應包含 YAML frontmatter。檢索器使用 frontmatter 欄位進行篩選和上下文豐富化:

---
title: "OAuth Token Rotation Patterns"
type: note           # note | signal | project | moc | daily
domain: security     # primary domain for routing
tags:
  - authentication
  - oauth
  - token-management
created: 2026-01-15
updated: 2026-02-28
source: ""           # URL if captured from external source
status: active       # active | archived | draft
---

檢索所需的必要欄位:

  • title — 用於搜尋結果顯示以及 BM25 的標題上下文
  • type — 啟用類型篩選查詢(「只顯示 MOC」或「只顯示信號」)
  • tags — 以 0.3 的權重在 FTS5 標題上下文中建立索引,即使正文使用不同術語也能提供關鍵字匹配

非必要但有價值的欄位:

  • domain — 啟用領域範圍查詢(「僅搜尋安全相關筆記」)
  • source — 擷取內容的來源標註;檢索器可在結果中包含來源 URL
  • status — 允許從活躍搜尋中排除已歸檔或草稿狀態的筆記

分塊慣例

檢索器以 H2(##)標題為邊界進行分塊(chunking)。這代表您的筆記結構會直接影響檢索的精細度:

有利於檢索的寫法:

## Token Rotation Strategy

The rotation interval depends on the threat model...

## Implementation with refresh_token

The OAuth 2.0 refresh token flow requires...

## Error Handling: Expired Tokens

When a token expires mid-request...

三個 H2 區段會產生三個可獨立搜尋的分塊。每個分塊包含足夠的上下文,讓 embedding 能捕捉其含義。關於「過期權杖處理」的查詢會精確匹配到第三個分塊。

不利於檢索的寫法:

# OAuth Notes

Token rotation depends on threat model. The OAuth 2.0 refresh
token flow requires storing the refresh token securely. When a
token expires mid-request, the client should retry after refresh.
The rotation interval is typically 15-30 minutes for access tokens
and 7-30 days for refresh tokens...

一個沒有 H2 標題的長區段只會產生一個大型分塊。embedding 會對該區段中所有主題進行平均化。對任何子主題的查詢都會同等程度地匹配整篇筆記。

經驗法則:如果某個區段涵蓋不止一個概念,請將其拆分為 H2 子區段。分塊器會處理其餘工作。

筆記中不應包含的內容

會降低檢索品質的內容:

  • 未加註解的完整文章原文複製貼上。檢索器會索引原始文章的關鍵字,以您未撰寫的內容稀釋您的 vault。請改為新增摘要、提取重點,或連結至來源 URL。
  • 沒有文字描述的螢幕截圖。檢索器索引的是 markdown 文字。沒有替代文字或周圍描述的圖片,對 BM25 和向量搜尋而言都是不可見的。
  • 憑證字串。API 金鑰、權杖、密碼、連線字串。即使有憑證篩選機制,最安全的做法仍是永遠不要在筆記中貼上機密資訊。請改用名稱參照(例如「~/.env 中的 Cloudflare API 權杖」)。
  • 未經整理的自動產生內容。如果某個工具產生了筆記(會議逐字稿、Readwise 標註、RSS 匯入),請在其進入永久 vault 之前先審閱並加上註解。未經整理的自動匯入只增加數量,不增加可檢索的價值。

AI 工作流程的外掛生態系

能提升 vault 品質以利 AI 檢索的 Obsidian 外掛分為三類:結構性(強制一致性)、查詢性(暴露詮釋資料)和同步性(保持 vault 內容即時更新)。

必備外掛

Dataview。使用 frontmatter 欄位像資料庫一樣查詢您的 vault。建立動態索引:「過去30天內更新的所有標記為 security 的筆記」或「所有狀態為 active 的專案筆記」。Dataview 並不直接幫助檢索,但能協助您識別 vault 涵蓋範圍的缺口,並找出需要更新的筆記。

TABLE type, domain, updated
FROM "03-resources"
WHERE status = "active"
SORT updated DESC
LIMIT 20

Templater。使用含動態欄位的範本建立筆記。透過使用預先填入 createdtypedomain 欄位的範本,確保每篇新筆記都以正確的 frontmatter 開頭。一致的 frontmatter 能改善檢索篩選效果。

<%* /* New Resource Note Template */ %>
---
title: "<% tp.file.cursor() %>"
type: note
domain: <% tp.system.suggester(["programming", "security", "ai-engineering", "design", "devops"], ["programming", "security", "ai-engineering", "design", "devops"]) %>
tags: []
created: <% tp.date.now("YYYY-MM-DD") %>
updated: <% tp.date.now("YYYY-MM-DD") %>
source: ""
status: active
---

## Key Points

## Details

## 參考資源

Linter。在整個vault中強制執行格式規則。一致的標題層級結構(H1用於標題、H2用於章節、H3用於子章節)確保chunker產生可預測的結果。對檢索有影響的Linter規則:

  • 標題遞增:強制使用連續的標題層級(不可從H1直接跳到H3)
  • YAML title:與檔案名稱一致
  • 尾隨空格:移除(避免FTS5分詞產生雜訊)
  • 連續空行:限制為1行(產生更乾淨的chunk)

Git整合。為您的vault提供版本控制。追蹤歷史變更、在不同機器間同步,以及從意外刪除中復原。Git也提供mtime資料,索引器會利用此資料進行增量變更偵測。

有助於索引的外掛

Smart Connections。一個Obsidian外掛,在Obsidian內部提供AI驅動的語意搜尋。它會建立自己的embedding索引。雖然本指南中的檢索系統是Obsidian外部的(以Python管線運行),但Smart Connections在撰寫時探索語意關聯非常實用。這兩個系統索引相同的內容,但服務於不同的使用情境:Smart Connections用於編輯器內的探索發現,外部檢索器用於AI工具整合。

Metadata Menu。提供結構化的frontmatter編輯功能,支援欄位值的自動完成。減少typedomaintags欄位中的拼寫錯誤。一致的metadata可提升檢索篩選的準確性。

不利於索引的外掛

Excalidraw。將繪圖儲存為嵌入在markdown檔案中的JSON。該JSON在語法上是有效的markdown,但經過chunking和embedding後會產生無意義的內容。請透過.indexignore或依副檔名篩選,將Excalidraw檔案排除在索引之外。

Kanban。將看板狀態儲存為特殊格式的markdown。該格式是為Kanban呈現而設計,不適合散文檢索。chunker會產生卡片標題和metadata的片段,這些片段無法產生良好的embedding。請將Kanban看板排除在索引之外。

Calendar。建立內容極少的每日筆記(通常僅有一個日期標題)。空白或接近空白的筆記會產生低品質的chunk。如果您使用每日筆記,請在其中撰寫有實質內容的文字,或將每日筆記資料夾排除在索引之外。

重要的外掛設定

檔案復原→啟用。防止意外刪除筆記。雖然與檢索沒有直接關係,但對於您所依賴的知識庫至關重要。

嚴格換行→停用。Markdown標準換行(雙換行產生段落)比Obsidian的嚴格模式(單換行產生<br>)能產生更乾淨的chunk。

新檔案預設位置→指定資料夾。將新檔案導向00-inbox/,使未分類的筆記不會污染領域資料夾。收件匣是一個暫存區;檔案在整理後移至領域資料夾。

Wiki-link格式→盡可能使用最短路徑。較短的連結目標讓檢索器在索引連結結構時更容易解析。


Embedding 模型:選擇與設定

Embedding 模型將文字區塊轉換為數值向量,用於語義搜尋。模型的選擇決定了檢索品質、索引大小、embedding 速度及執行時期依賴項。本節說明為何 Model2Vec 的 potion-base-8M 是預設選擇,以及何時應考慮替代方案。

為何選擇 Model2Vec potion-base-8M

模型: minishlab/potion-base-8M 參數量: 760 萬 維度: 256 大小: 約 30 MB 依賴項: model2vec(僅需 numpy,無需 PyTorch) 推論: 僅限 CPU,靜態詞嵌入(無注意力層)

Model2Vec 將句子轉換器的知識蒸餾為靜態 token embeddings。與 BERT、MiniLM 及其他 transformer 模型對輸入執行注意力層不同,Model2Vec 透過對預先計算的 token embeddings 進行加權平均來產生向量。3 實際效果是:embedding 速度比基於 transformer 的模型快 50 至 500 倍,因為不需要序列化運算。

在 MTEB 基準測試套件上,potion-base-8M 達到 all-MiniLM-L6-v2 效能的 89%(平均分 50.03 對 56.09)。4 這 11% 的品質差距是速度與簡潔性優勢的取捨。對於短篇 Markdown 區塊(典型知識庫中平均 200-400 字),品質差異不如長篇文件明顯,因為兩種模型在處理短而集中的文字時會收斂到相似的表示。

設定

# embedder.py
DEFAULT_MODEL = "minishlab/potion-base-8M"
EMBEDDING_DIM = 256

class Model2VecEmbedder:
    def __init__(self, model_name=DEFAULT_MODEL):
        self._model_name = model_name
        self._model = None

    def _ensure_model(self):
        if self._model is not None:
            return
        _activate_venv()  # Add isolated venv to sys.path
        from model2vec import StaticModel
        self._model = StaticModel.from_pretrained(self._model_name)

    def embed_batch(self, texts):
        self._ensure_model()
        vecs = self._model.encode(texts)
        return [v.tolist() for v in vecs]

延遲載入。 模型在首次使用時載入,而非在匯入時載入。當檢索器以僅 BM25 備用模式運作時(例如未安裝 embedding 虛擬環境),匯入 embedder 模組不會產生任何開銷。

隔離的虛擬環境。 模型在專用的虛擬環境中執行(例如 ~/.claude/venvs/memory/),以避免與工具鏈的其他部分產生依賴衝突。_activate_venv() 函式在執行時期將虛擬環境的 site-packages 加入 sys.path

# Create isolated venv
python3 -m venv ~/.claude/venvs/memory
~/.claude/venvs/memory/bin/pip install model2vec

批次處理。 Embedder 以 64 個為一批處理文字,以攤銷 Model2Vec 的額外開銷。索引器將區塊傳送至 embed_batch() 而非逐一進行 embedding。

何時選擇替代方案

模型 維度 大小 速度 品質(MTEB) 最適用途
potion-base-8M 256 30 MB 500x 50.03 預設:本地、快速、無需 GPU
potion-base-32M 256 120 MB 400x 52.46 更高品質,仍為靜態
potion-retrieval-32M 256 120 MB 400x 36.35(檢索) 針對檢索最佳化的靜態模型
all-MiniLM-L6-v2 384 80 MB 1x 56.09 更高品質,仍為本地
nomic-embed-text-v1.5 768 270 MB 0.5x 62.28 最佳本地品質
text-embedding-3-small 1536 API N/A 62.30 基於 API,最高品質

選擇 potion-base-32M,當您希望獲得比 potion-base-8M 更高的品質,又不想離開靜態 embedding 系列時。該模型於 2025 年 1 月發布,使用從 baai/bge-base-en-v1.5 蒸餾的更大詞彙表,MTEB 平均分達到 52.46(比 potion-base-8M 提升 5%),同時維持相同的 256 維輸出及僅需 numpy 的依賴。18 4 倍大的模型檔案會增加記憶體使用量,但 embedding 速度仍比 transformer 模型快數個數量級。

選擇 potion-retrieval-32M,當您的主要用途為檢索時(知識庫搜尋正是如此)。此變體基於 potion-base-32M 針對檢索任務進行微調,在 MTEB 檢索基準測試中得分 36.35,而基礎模型為 33.52。18 整體 MTEB 平均分降至 49.73,因為微調以犧牲通用效能換取檢索特定的增益。

選擇 all-MiniLM-L6-v2,當檢索品質比速度更重要且您已安裝 PyTorch 時。384 維向量使 SQLite 資料庫大小比 256 維向量增加約 50%。在 M 系列硬體上對 15,000 個檔案進行完整重新索引時,embedding 速度從不到 1 分鐘降至約 10 分鐘。

選擇 nomic-embed-text-v1.5,當您需要最佳的本地檢索品質,且可接受較慢的索引速度時。768 維向量大約使資料庫大小增為三倍。需要 PyTorch 及現代 CPU 或 GPU。

選擇 text-embedding-3-small,當網路延遲和隱私為可接受的取捨時。API 產生最高品質的 embeddings,但會引入雲端依賴、按 token 計費的成本(每百萬 token 0.02 美元),並將您的內容傳送至 OpenAI 的伺服器。

在其他所有情況下維持使用 potion-base-8M。 速度優勢對於迭代式索引至關重要(開發期間重新索引),僅需 numpy 的依賴避免了 PyTorch 安裝的複雜性,而 256 維向量保持資料庫的精簡。

量化與降維

Model2Vec v0.5.0+ 支援以降低精度和維度載入模型。18 這對於在受限硬體上部署或在不更換模型的情況下縮小資料庫大小非常有用:

from model2vec import StaticModel

# Load with int8 quantization (25% of original size)
model = StaticModel.from_pretrained("minishlab/potion-base-8M", quantize=True)

# Load with reduced dimensions (e.g., 128 instead of 256)
model = StaticModel.from_pretrained("minishlab/potion-base-8M", dimensionality=128)

量化模型以極小的記憶體佔用保留近乎相同的檢索品質。降維遵循 Matryoshka 風格的截斷——前 N 個維度承載最多資訊。從 256 維降至 128 維可將向量儲存減半,對短文字檢索的品質損失極小。

模型雜湊追蹤

索引器儲存一個由模型名稱和詞彙大小衍生的雜湊值。如果您更換 embedding 模型,索引器會在下次增量執行時偵測到不匹配,並自動觸發完整重新索引。

def _compute_model_hash(self):
    """Hash model name + vocab size for compatibility tracking."""
    key = f"{self._model_name}:{self._model.vocab_size}"
    return hashlib.sha256(key.encode()).hexdigest()[:16]

這可防止在同一資料庫中混合來自不同模型的向量,否則會產生無意義的 cosine similarity 分數。

失敗模式

模型下載失敗。 首次執行時會從 Hugging Face 下載模型。若下載失敗(網路問題、企業防火牆),檢索器將退回至僅 BM25 模式。模型在首次下載後會快取於本地。

維度不匹配。 若您在未清除資料庫的情況下切換模型,已儲存的向量與新 embeddings 的維度不同。索引器透過模型雜湊偵測此情況並觸發完整重新索引。若雜湊檢查失敗(未具備正確雜湊的自訂模型),sqlite-vec 將在維度不匹配的 KNN 查詢中回報錯誤。

大型知識庫的記憶體壓力。 在單一批次中 embedding 50,000 個以上的區塊可能會消耗大量記憶體。索引器以 64 個為一批處理以限制尖峰記憶體使用量。若記憶體仍有問題,請降低批次大小。


使用FTS5進行全文搜尋

SQLite的FTS5擴充功能提供具備BM25排名的全文搜尋。FTS5是混合檢索(hybrid retrieval)管線中的關鍵字搜尋元件。本節涵蓋FTS5的設定、BM25的優勢場景,以及其特定的失敗模式。

FTS5虛擬資料表

CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text,
    section,
    heading_context,
    content=chunks,
    content_rowid=id
);

內容同步模式。content=chunks參數指示FTS5直接參照chunks資料表,而非儲存文字的副本。這可將儲存需求減半,但代表當chunk被新增、更新或刪除時,必須手動同步FTS5。

欄位。索引包含三個欄位: - chunk_text — 每個chunk的主要內容(BM25權重:1.0) - section — H2標題文字(BM25權重:0.5) - heading_context — 筆記標題、標籤和中繼資料(BM25權重:0.3)

BM25排名

BM25根據詞頻、逆文件頻率和文件長度正規化來對文件進行排名。FTS5中的bm25()輔助函式接受各欄位的權重:

SELECT
    c.id, c.file_path, c.section, c.chunk_text,
    bm25(chunks_fts, 1.0, 0.5, 0.3) AS score
FROM chunks_fts
JOIN chunks c ON chunks_fts.rowid = c.id
WHERE chunks_fts MATCH ?
ORDER BY score
LIMIT 30;

欄位權重(1.0、0.5、0.3)的意義為: - 在chunk_text中的關鍵字匹配對分數貢獻最大 - 在section(標題)中的匹配貢獻為一半 - 在heading_context(標題、標籤)中的匹配貢獻為30%

這些權重可以調整。如果您的知識庫擁有能準確預測內容品質的描述性標題,請提高section的權重。如果您的標籤完整且準確,請提高heading_context的權重。

BM25的優勢場景

BM25在包含精確識別符的查詢中表現出色:

  • 函式名稱:_rrf_fuseembed_batchget_stale_files
  • CLI旗標:--incremental--vault--model
  • 設定鍵值:bm25_weightmax_tokensbatch_size
  • 錯誤訊息:SQLITE_LOCKEDConnectionRefusedError
  • 特定專有術語:PostToolUsePreToolUseAGENTS.md

對於這些查詢,BM25能立即找到精確匹配。向量搜尋會回傳語意相關的內容,但可能將精確匹配的排名排在概念性討論之後。

BM25的失敗場景

BM25在查詢使用與儲存內容不同的術語時會失敗:

  • 查詢:「how to handle authentication failures」→知識庫中包含關於「login error recovery」和「session expiration handling」的筆記。BM25無法匹配,因為關鍵字不同。
  • 查詢:「what is the best way to manage state」→知識庫中包含關於「Redux store patterns」和「context providers」的筆記。BM25未能命中,因為「state management」是透過特定技術名稱來表達的。

BM25在大規模情境下也會遭遇關鍵字碰撞問題。在一個擁有15,000個檔案的知識庫中,搜尋「configuration」會匹配數百個筆記,因為幾乎每個專案筆記都提到了configuration。結果在技術上是正確的,但實際上毫無用處——排名機制無法判斷哪個「configuration」筆記與當前查詢相關。

FTS5分詞器

FTS5預設使用unicode61分詞器,可處理ASCII和Unicode文字。對於包含大量CJK(中文、日文、韓文)內容的知識庫,可考慮使用trigram分詞器:

-- For CJK-heavy vaults
CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text, section, heading_context,
    content=chunks, content_rowid=id,
    tokenize='trigram'
);

預設的unicode61分詞器按詞邊界分割,這對於詞與詞之間沒有空格的語言效果不佳。trigram分詞器每三個字元進行分割,可實現子字串匹配,但索引大小會增加(約為3倍)。

維護

當底層的chunks資料表發生變更時,FTS5需要明確進行同步:

# After inserting chunks
cursor.execute("""
    INSERT INTO chunks_fts(chunks_fts)
    VALUES('rebuild')
""")

rebuild命令會從內容資料表重建FTS5索引。在批次新增(完整重建索引)後執行此命令,但不要在單筆增量更新後執行——對於單筆更新,請使用INSERT INTO chunks_fts(rowid, chunk_text, section, heading_context)來同步個別列。


使用sqlite-vec進行向量搜尋

sqlite-vec擴充功能將向量KNN(K-Nearest Neighbors)搜尋引入SQLite。本節涵蓋sqlite-vec的設定、從筆記到可搜尋向量的嵌入(embedding)管線,以及特定的查詢模式。

sqlite-vec虛擬資料表

CREATE VIRTUAL TABLE chunk_vecs USING vec0(
    id INTEGER PRIMARY KEY,
    embedding float[256]
);

vec0模組將256維浮點向量儲存為打包的二進位資料。id欄位與chunks資料表呈1:1對應,可在向量結果與chunk中繼資料之間進行聯結查詢。

嵌入管線

管線從筆記流向可搜尋的向量:

Note (.md file)
   Chunker: split at H2 boundaries
     Chunks (30-2000 chars each)
       Credential filter: scrub secrets
         Embedder: Model2Vec encode
           Vectors (256-dim float arrays)
             sqlite-vec: store as packed binary
               Ready for KNN queries

向量序列化

Python的struct模組將浮點向量序列化以供sqlite-vec儲存:

import struct

def _serialize_vector(vec):
    """Pack float list into binary for sqlite-vec."""
    return struct.pack(f"{len(vec)}f", *vec)

def _deserialize_vector(blob, dim=256):
    """Unpack binary blob to float list."""
    return list(struct.unpack(f"{dim}f", blob))

KNN查詢

向量搜尋查詢先將輸入查詢進行嵌入,然後按餘弦距離(cosine distance)找出最近的K個chunk:

def _vector_search(self, query_text, limit=30):
    query_vec = self.embedder.embed_batch([query_text])[0]
    packed = _serialize_vector(query_vec)

    results = self.db.execute("""
        SELECT
            cv.id,
            cv.distance,
            c.file_path,
            c.section,
            c.chunk_text
        FROM chunk_vecs cv
        JOIN chunks c ON cv.id = c.id
        WHERE embedding MATCH ?
            AND k = ?
        ORDER BY distance
    """, [packed, limit]).fetchall()

    return results

sqlite-vec中的MATCH運算子執行近似最近鄰搜尋。k參數控制回傳結果的數量。distance欄位包含餘弦距離(0=完全相同,2=完全相反)。

向量搜尋的優勢場景

向量搜尋在概念比特定用詞更重要的查詢中表現出色:

  • 查詢:「how to handle authentication failures」→找到關於「login error recovery」的筆記(相同語意空間,不同關鍵字)
  • 查詢:「what patterns exist for caching」→找到關於「memoization」、「Redis TTL strategies」和「HTTP cache headers」的筆記(相關概念,多樣化的術語)
  • 查詢:「approaches to testing asynchronous code」→找到關於「pytest-asyncio fixtures」、「mock event loops」和「async test patterns」的筆記(相同概念透過實作細節表達)

向量搜尋的失敗場景

向量搜尋在處理精確識別符時表現不佳:

  • 查詢:_rrf_fuse→回傳關於「fusion algorithms」和「rank merging」的筆記,但可能將實際的函式定義排在概念性討論之後
  • 查詢:PostToolUse→回傳關於「tool lifecycle hooks」和「post-execution handlers」的筆記,而非特定的hook名稱

向量搜尋在處理結構化資料時也表現不佳。JSON設定檔、YAML區塊和程式碼片段產生的嵌入向量捕捉的是結構模式而非語意意義。一個包含"review": true的JSON檔案,其嵌入結果與一段關於程式碼審查的文字描述截然不同。

優雅降級

如果sqlite-vec載入失敗(缺少擴充功能、平台不相容、函式庫損毀),檢索器會退回到僅使用BM25的搜尋模式:

class VectorIndex:
    def __init__(self, db_path):
        self.db = sqlite3.connect(db_path)
        self._vec_available = False
        try:
            self.db.enable_load_extension(True)
            self.db.load_extension("vec0")
            self._vec_available = True
        except Exception:
            pass  # BM25-only mode

    @property
    def vec_available(self):
        return self._vec_available

檢索器在嘗試向量查詢前會檢查vec_available。當此功能停用時,所有搜尋僅使用BM25,並跳過RRF融合步驟。


Reciprocal Rank Fusion(RRF)

RRF 將兩個排序清單合併,無需進行分數校準。本節涵蓋演算法原理、一個完整的查詢追蹤範例、k 參數調校,以及為何選擇 RRF 而非其他替代方案。如需互動式計算機(可編輯排名、情境預設及視覺化架構探索),請參閱 hybrid retriever 深入解析

演算法原理

RRF 僅根據文件在各清單中的排名位置來賦予分數:

score(d) = Σ (weight_i / (k + rank_i))

其中: - k 是平滑常數(60,依據 Cormack 等人的研究1) - rank_i 是文件在結果清單 i 中以 1 為起始的排名 - weight_i 是各清單的選用權重乘數(預設為 1.0)

在多個清單中排名良好的文件會獲得更高的融合分數。僅出現在單一清單中的文件則只會取得該單一來源的分數。

為何選擇 RRF 而非其他替代方案

加權線性組合需要校準 BM25 分數與 cosine similarity 距離。BM25 分數無上界且隨語料庫大小而變動。餘弦距離的範圍限定在 [0, 2]。將兩者結合需要正規化,而正規化參數取決於資料集。RRF 僅使用排名位置,無論評分方法為何,排名始終是從 1 開始的整數。

學習式融合模型需要標註過的訓練資料——查詢與文件的相關性配對。對於個人知識庫而言,這類訓練資料並不存在。您需要手動判斷數百組查詢與文件的配對才能訓練出實用的模型。RRF 無需任何訓練資料即可運作。

Condorcet 投票法(Borda 計數、Schulze 方法)在理論上很精巧,但實作與調校更為複雜。原始 RRF 論文已證明,RRF 在 TREC 評估資料上的表現優於 Condorcet 方法。1

融合實務範例

查詢:”how does the review aggregator handle disagreements”

BM25 將 review-aggregator.py 排在第 3 位(精確關鍵字匹配 “review”、”aggregator”、”disagreements”),但將兩個設定檔排在更前面(它們對 “review” 的匹配度更高)。向量搜尋將同一區塊排在第 1 位(語義匹配衝突解決相關概念)。經過 RRF 融合後:

區塊 BM25 Vec 融合分數
review-aggregator.py “Disagreement Resolution” #3 #1 0.0323
code-review-patterns.md “Multi-Reviewer” #4 #2 0.0317
deliberation-config.json “Review Weights” #1 0.0164

兩個清單中排名良好的區塊會浮上頂端。僅出現在單一清單中的區塊只會取得單一來源分數,因而排在雙重排名結果之後。實際的分歧解決邏輯勝出,因為兩種方法都找到了它——BM25 透過關鍵字,向量搜尋透過語義。

如需完整的逐步追蹤與各排名的 RRF 數學計算,請在互動式 RRF 計算機中嘗試不同的 k 值。

實作

RRF_K = 60

def _rrf_fuse(self, bm25_results, vec_results,
              bm25_weight=1.0, vec_weight=1.0):
    """Fuse BM25 and vector results using Reciprocal Rank Fusion."""
    scores = {}

    for rank, r in enumerate(bm25_results, start=1):
        cid = r["id"]
        if cid not in scores:
            scores[cid] = {
                "rrf_score": 0.0,
                "file_path": r["file_path"],
                "section": r["section"],
                "chunk_text": r["chunk_text"],
                "bm25_rank": None,
                "vec_rank": None,
            }
        scores[cid]["rrf_score"] += bm25_weight / (self._rrf_k + rank)
        scores[cid]["bm25_rank"] = rank

    for rank, r in enumerate(vec_results, start=1):
        cid = r["id"]
        if cid not in scores:
            scores[cid] = {
                "rrf_score": 0.0,
                "file_path": r["file_path"],
                "section": r["section"],
                "chunk_text": r["chunk_text"],
                "bm25_rank": None,
                "vec_rank": None,
            }
        scores[cid]["rrf_score"] += vec_weight / (self._rrf_k + rank)
        scores[cid]["vec_rank"] = rank

    fused = sorted(
        scores.values(),
        key=lambda x: x["rrf_score"],
        reverse=True,
    )
    return fused

調校 k 值

k 常數控制頂端排名結果相對於較低排名結果所獲得的權重比例:

  • 較低的 k(例如 10):頂端排名結果佔主導地位。排名 1 的分數為 1/11 = 0.091,排名 10 的分數為 1/20 = 0.050(相差 1.8 倍)。適用於您信任各排序器能正確找出最佳結果的情境。
  • 預設 k(60):均衡。排名 1 的分數為 1/61 = 0.0164,排名 10 的分數為 1/70 = 0.0143(相差 1.15 倍)。排名差異被壓縮,使得同時出現在多個清單中的權重增加。
  • 較高的 k(例如 200):同時出現在兩個清單中的重要性遠大於排名位置。排名 1 的分數為 1/201,排名 10 的分數為 1/210——幾乎相同。適用於各排序器產生的排名較不穩定、但跨清單一致性可靠的情境。

建議從 k=60 開始。原始 RRF 論文發現此值在多樣化的 TREC 資料集上表現穩健。僅在針對您自己的查詢分佈衡量失敗案例後,再進行調校。

同分處理

當兩個區塊具有相同的 RRF 分數時(罕見但可能發生,例如在某一清單中排名相同且未出現在另一清單中),依以下規則處理同分:

  1. 優先選擇同時出現在兩個清單中的區塊,而非僅出現在單一清單中的區塊
  2. 在同時出現於兩個清單的區塊中,優先選擇合併排名較低(即排名較前)的區塊
  3. 在僅出現於單一清單的區塊中,優先選擇在該清單中排名較前的區塊

完整檢索管線

本節追蹤一個查詢從輸入到輸出的完整管線流程:BM25 搜尋、向量搜尋、RRF 融合、token 預算截斷,以及上下文組裝。

端對端流程

User query: "PostToolUse hook for context compression"
  │
  ├─ BM25 Search (FTS5)
  │    → MATCH "PostToolUse hook context compression"
  │    → Top 30 results ranked by BM25 score
  │    → 12ms
  │
  ├─ Vector Search (sqlite-vec)
  │    → Embed query with Model2Vec
  │    → KNN k=30 on chunk_vecs
  │    → Top 30 results ranked by cosine distance
  │    → 8ms
  │
  └─ RRF Fusion
       → Merge 60 candidates (may overlap)
       → Score by rank position
       → Top 10 results
       → 3ms
       │
       └─ Token Budget
            → Truncate to max_tokens (default 4000)
            → Estimate at 4 chars per token
            → Return results with metadata
            → <1ms

總延遲:約23毫秒,測試環境為搭載 Apple M3 Pro 硬體的49,746個區塊資料庫。

搜尋API

class HybridRetriever:
    def search(self, query, limit=10, max_tokens=4000,
               bm25_weight=1.0, vec_weight=1.0):
        """
        Search the vault using hybrid BM25 + vector retrieval.

        Args:
            query: Search query text
            limit: Maximum results to return
            max_tokens: Token budget for total result text
            bm25_weight: Weight for BM25 results in RRF
            vec_weight: Weight for vector results in RRF

        Returns:
            List of SearchResult with file_path, section,
            chunk_text, rrf_score, bm25_rank, vec_rank
        """
        # BM25 search
        bm25_results = self._bm25_search(query, limit=30)

        # Vector search (if available)
        if self.index.vec_available:
            vec_results = self._vector_search(query, limit=30)
            fused = self._rrf_fuse(
                bm25_results, vec_results,
                bm25_weight, vec_weight,
            )
        else:
            fused = bm25_results  # BM25-only fallback

        # Token budget truncation
        results = []
        token_count = 0
        for r in fused[:limit]:
            chunk_tokens = len(r["chunk_text"]) // 4
            if token_count + chunk_tokens > max_tokens:
                break
            results.append(r)
            token_count += chunk_tokens

        return results

Token 預算截斷

max_tokens 參數可防止檢索器回傳超過 AI 工具所能使用的上下文量。估算方式採用每 token 4 個字元(對英文散文而言是合理的近似值)。結果以貪婪策略截斷:按排名順序依次加入結果,直到預算耗盡為止。

這是一種保守策略。更精密的做法會考量每筆結果的品質分數,優先選擇較短但品質較高的結果,而非較長但品質較低的結果。貪婪策略較為簡單,且在實務中表現良好,因為 RRF 排名已經依照相關性排序了結果。

資料庫結構描述(完整版)

-- Chunk content and metadata
CREATE TABLE chunks (
    id INTEGER PRIMARY KEY,
    file_path TEXT NOT NULL,
    section TEXT NOT NULL,
    chunk_text TEXT NOT NULL,
    heading_context TEXT DEFAULT '',
    mtime_ns INTEGER NOT NULL,
    embedded_at REAL NOT NULL
);

CREATE INDEX idx_chunks_file ON chunks(file_path);
CREATE INDEX idx_chunks_mtime ON chunks(mtime_ns);

-- FTS5 for BM25 search (content-synced to chunks table)
CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text, section, heading_context,
    content=chunks, content_rowid=id
);

-- sqlite-vec for vector KNN search
CREATE VIRTUAL TABLE chunk_vecs USING vec0(
    id INTEGER PRIMARY KEY,
    embedding float[256]
);

-- Model metadata for compatibility tracking
CREATE TABLE model_meta (
    key TEXT PRIMARY KEY,
    value TEXT
);

優雅降級路徑

Full pipeline:     BM25 + Vector + RRF    Best results
No sqlite-vec:     BM25 only              Good results (no semantic)
No model download:  BM25 only              Good results (no semantic)
No FTS5:           Vector only             Decent results (no keyword)
No database:       Error                   Prompt user to run indexer

檢索器在初始化時檢查可用功能,並據此調整查詢策略。缺少某個元件會降低品質,但不會導致錯誤。唯一的硬性失敗條件是資料庫檔案遺失。

生產環境統計數據

測試環境為包含16,894個檔案、49,746個區塊、83 MB SQLite 資料庫的知識庫,搭載 Apple M3 Pro:

指標 數值
檔案總數 16,894
區塊總數 49,746
資料庫大小 83 MB
BM25 查詢延遲(p50) 12ms
向量查詢延遲(p50) 8ms
RRF 融合延遲 3ms
端對端搜尋延遲(p50) 23ms
完整重建索引時間 約4分鐘
增量重建索引時間 <10秒
Embedding 模型 potion-base-8M(256維)
BM25 候選池 30
向量候選池 30
預設結果上限 10
預設 token 預算 4,000 tokens

內容雜湊與變更偵測

索引器需要知道自上次索引執行以來哪些檔案發生了變更。本節介紹變更偵測機制與雜湊策略。

檔案修改時間比對

索引器在 chunks 表中為每個區塊儲存 mtime_ns(檔案修改時間,以奈秒為單位)。在增量執行時,索引器會:

  1. 掃描知識庫中允許資料夾內的所有 .md 檔案
  2. 從檔案系統讀取每個檔案的 mtime_ns
  3. 與資料庫中儲存的 mtime_ns 進行比對
  4. 識別三種類別:
  5. 新檔案: 路徑存在於檔案系統但不存在於資料庫
  6. 已變更檔案: 路徑同時存在於兩者但 mtime_ns 不同
  7. 已刪除檔案: 路徑存在於資料庫但不存在於檔案系統
def get_stale_files(self, vault_mtimes):
    """Find files whose mtime changed or are new."""
    stored = dict(self.db.execute(
        "SELECT DISTINCT file_path, mtime_ns FROM chunks"
    ).fetchall())

    stale = []
    for path, mtime in vault_mtimes.items():
        if path not in stored or stored[path] != mtime:
            stale.append(path)
    return stale

def get_deleted_files(self, vault_paths):
    """Find files in database that no longer exist in vault."""
    stored_paths = set(r[0] for r in self.db.execute(
        "SELECT DISTINCT file_path FROM chunks"
    ).fetchall())
    return stored_paths - set(vault_paths)

為何選擇 mtime 而非內容雜湊

內容雜湊(對檔案內容計算 SHA-256)比 mtime 比對更為可靠——它能偵測到檔案被觸碰但內容未變更的情況(例如 git checkout 還原了原始的 mtime)。然而,雜湊需要在每次增量執行時讀取每個檔案的內容。對於16,894個檔案,讀取檔案內容需要2至3秒。從檔案系統讀取 mtime 則只需不到100毫秒。

這是一種權衡:mtime 比對偶爾會觸發對未變更檔案的不必要重新索引(誤報),但絕不會遺漏實際的變更。誤報僅會在每次執行時多產生幾次額外的 embedding 呼叫。速度差異(100毫秒 vs 3秒)使得 mtime 成為在每次 AI 互動時執行的系統的務實選擇。

處理刪除

當檔案從知識庫中刪除時,索引器會從資料庫中移除該檔案的所有區塊:

def remove_file(self, file_path):
    """Remove all chunks and vectors for a file."""
    chunk_ids = [r[0] for r in self.db.execute(
        "SELECT id FROM chunks WHERE file_path = ?",
        [file_path],
    ).fetchall()]

    for cid in chunk_ids:
        self.db.execute(
            "DELETE FROM chunk_vecs WHERE id = ?", [cid]
        )
    self.db.execute(
        "DELETE FROM chunks WHERE file_path = ?",
        [file_path],
    )

FTS5 內容同步表需要透過 INSERT INTO chunks_fts(chunks_fts, rowid, ...) VALUES('delete', ?, ...) 針對每筆移除的資料列進行明確刪除。索引器會在檔案移除流程中處理此操作。


增量與完整重新索引

索引器支援兩種模式:增量(快速,日常使用)和完整(較慢,偶爾使用)。本節涵蓋各模式的使用時機、冪等性保證,以及損壞復原方式。

增量重新索引

使用時機: 編輯筆記後的日常索引。這是預設模式。

運作方式: 1. 掃描知識庫的檔案變更(mtime 比對) 2. 刪除已刪除檔案的區塊 3. 重新分塊並重新產生已變更檔案的embeddings 4. 為新檔案插入新的區塊 5. 同步FTS5索引

一般所需時間: 在16,000個檔案的知識庫中,一天的編輯量不到10秒。

python index_vault.py --incremental

完整重新索引

使用時機: - 更換embedding模型後(偵測到模型雜湊值不符) - 結構描述遷移後(新欄位、變更的索引) - 資料庫損壞後(完整性檢查失敗) - 增量索引產生非預期結果時

運作方式: 1. 刪除所有現有資料(區塊、向量、FTS5項目) 2. 掃描整個知識庫 3. 對所有檔案進行分塊 4. 對所有區塊產生embeddings 5. 從頭建立FTS5索引

一般所需時間: 在Apple M3 Pro上處理16,894個檔案約需4分鐘。

python index_vault.py --full

冪等性

兩種模式都具有冪等性:執行相同命令兩次會產生相同的結果。索引器會在插入新區塊之前,先刪除該檔案的現有區塊,因此對已經是最新狀態的資料庫重複執行增量索引不會產生任何變更。重複執行完整索引則會產生完全相同的資料庫。

損壞復原

如果SQLite資料庫發生損壞(寫入時斷電、磁碟錯誤、交易進行中程序被終止):

# Check integrity
sqlite3 vectors.db "PRAGMA integrity_check;"

# If corruption detected, full reindex rebuilds from source files
python index_vault.py --full

真實來源永遠是知識庫檔案,而非資料庫。資料庫是衍生產物,可以隨時重建。這是一個關鍵的設計特性:您永遠不需要備份資料庫。

--incremental 旗標

當索引器以--incremental執行時:

  1. 模型雜湊值檢查。 比對儲存的模型雜湊值與目前的模型。如果不同,自動切換為完整重新索引模式並警告使用者。
  2. 檔案掃描。 遍歷允許的資料夾,收集檔案路徑與mtime。
  3. 變更偵測。 與儲存的資料進行比對。
  4. 批次處理。 以每批64個的方式重新分塊並重新產生已變更檔案的embeddings。
  5. 進度回報。 印出已處理檔案數量與經過時間。
  6. 優雅關閉。 處理SIGINT訊號時,完成目前檔案後再停止。

憑證過濾與資料邊界

個人筆記中包含機密資訊:API金鑰、bearer權杖、資料庫連線字串、除錯過程中貼上的私鑰。憑證過濾器可防止這些資訊進入檢索索引。

問題所在

一篇關於除錯OAuth整合的筆記可能包含:

The token was: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
I used this curl command:
  curl -H "Authorization: Bearer sk-ant-api03-abc123..."

如果沒有過濾機制,JWT和API金鑰都會被分塊、產生embeddings並儲存在資料庫中。搜尋「authentication」會回傳包含真實機密資訊的區塊。更糟的是,如果檢索器透過MCP將結果傳送給AI工具,這些機密資訊會出現在AI的上下文視窗中,甚至可能出現在工具的日誌中。

基於模式的過濾

憑證過濾器在每個區塊儲存前執行,比對25種供應商特定模式以及通用模式:

供應商特定模式:

模式 範例 正規表示式
OpenAI API金鑰 sk-... sk-[a-zA-Z0-9_-]{20,}
Anthropic API金鑰 sk-ant-api03-... sk-ant-api\d{2}-[a-zA-Z0-9_-]{20,}
GitHub PAT ghp_... gh[ps]_[a-zA-Z0-9]{36,}
AWS Access Key AKIA... AKIA[0-9A-Z]{16}
Stripe金鑰 sk_live_... [sr]k_(live\|test)_[a-zA-Z0-9]{24,}
Cloudflare權杖 ... 多種模式

通用模式:

模式 偵測方式
JWT權杖 eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+
Bearer權杖 Bearer\s+[a-zA-Z0-9_\-\.]+
私鑰 -----BEGIN (RSA\|EC\|OPENSSH) PRIVATE KEY-----
高熵值base64 每字元熵值>4.5位元、40+字元的字串
密碼指派 password\s*[:=]\s*["'][^"']+["']

過濾器實作

def clean_content(text):
    """Scrub credentials from text before indexing."""
    result = ScanResult(is_clean=True, match_count=0, patterns=[])

    for pattern in CREDENTIAL_PATTERNS:
        matches = pattern.regex.findall(text)
        if matches:
            text = pattern.regex.sub(
                f"[REDACTED:{pattern.name}]", text
            )
            result.is_clean = False
            result.match_count += len(matches)
            result.patterns.append(pattern.name)

    return text, result

關鍵設計決策:

  1. 在產生embeddings之前過濾。 清理後的文字才是用來產生embeddings的內容。向量表示絕不會編碼憑證模式。搜尋「API金鑰」會回傳討論API金鑰管理的筆記,而非包含實際金鑰的筆記。

  2. 替換而非移除。 [REDACTED:pattern-name]標記保留了周圍文字的語意上下文。embeddings會捕捉到「此處曾有類似憑證的內容」,但不會編碼憑證本身。

  3. 記錄模式而非數值。 過濾器記錄哪些模式被比對到(例如「從oauth-debug.md清除了2個憑證 [jwt, bearer-token]」),但絕不記錄憑證的實際值。

基於路徑的排除

.indexignore檔案提供基於路徑的粗粒度排除。憑證過濾器則在已索引的檔案內提供細粒度的清洗。兩者都是必要的:

  • .indexignore用於排除您確知包含敏感內容的整個資料夾(健康筆記、財務紀錄、職涯文件)
  • 憑證過濾器用於處理意外嵌入在可索引內容中的機密資訊

資料分級

對於包含多樣化內容的知識庫,建議依敏感度對筆記進行分級:

層級 範例 是否索引? 是否過濾?
公開 部落格草稿、技術筆記
內部 專案計畫、架構決策
敏感 薪資資料、健康紀錄 否(.indexignore) 不適用
受限 憑證、私鑰 否(.indexignore) 不適用

MCP 伺服器架構

Model Context Protocol(MCP)伺服器將檢索器公開為 AI 代理可呼叫的工具。本節涵蓋伺服器設計、功能範圍及權限邊界。

協定選擇:STDIO 與 HTTP

MCP 支援兩種傳輸模式:

STDIO — AI 工具將 MCP 伺服器作為子程序啟動,並透過 stdin/stdout 進行通訊。這是本機工具的標準模式。Claude Code、Codex CLI 和 Cursor 都支援 STDIO MCP 伺服器。

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/path/to/vault",
        "DB_PATH": "/path/to/vectors.db"
      }
    }
  }
}

HTTP — MCP 伺服器作為獨立的 HTTP 服務運行。適用於遠端存取、多客戶端設定,或知識庫位於共享伺服器的團隊配置。

{
  "mcpServers": {
    "obsidian": {
      "url": "http://localhost:3333/mcp"
    }
  }
}

建議:個人知識庫請使用 STDIO。它更簡單、更安全(無網路暴露),且伺服器生命週期由 AI 工具管理。僅在多個工具或多台機器需要同時存取同一知識庫時才使用 HTTP。

MCP 規範演進。2025年6月的 MCP 規範新增了基於 OAuth 的授權、結構化工具輸出(具型別的回傳結構描述)及引導式互動(伺服器發起的使用者提示)。16 下一版規範(暫定2026年6月)提議加入長時間任務的非同步操作、以無狀態請求處理作為預設傳輸模式,以及透過 .well-known URL 進行伺服器探索。16 對於個人知識庫伺服器,STDIO 仍是最簡單的路徑。規範變更主要影響具有多租戶路由和負載平衡的企業 HTTP 部署。請關注 MCP 發展藍圖 以瞭解影響您傳輸選擇的更新。

功能設計

MCP 伺服器應公開最少量的工具集:

search — 主要工具。執行混合檢索並回傳排序結果。

{
  "name": "obsidian_search",
  "description": "Search the Obsidian vault using hybrid BM25 + vector retrieval",
  "parameters": {
    "query": { "type": "string", "description": "Search query" },
    "limit": { "type": "integer", "default": 5 },
    "max_tokens": { "type": "integer", "default": 2000 }
  }
}

read_note — 依路徑讀取特定筆記的完整內容。當代理需要查看搜尋結果的完整上下文時使用。

{
  "name": "obsidian_read_note",
  "description": "Read the full content of a note by file path",
  "parameters": {
    "file_path": { "type": "string", "description": "Relative path within vault" }
  }
}

list_notes — 列出符合篩選條件的筆記(依資料夾、標籤、類型或日期範圍)。當代理沒有特定查詢需要進行探索時使用。

{
  "name": "obsidian_list_notes",
  "description": "List notes matching filters",
  "parameters": {
    "folder": { "type": "string", "description": "Folder path within vault" },
    "tag": { "type": "string", "description": "Tag to filter by" },
    "limit": { "type": "integer", "default": 20 }
  }
}

get_context — 便利工具,執行搜尋並將結果格式化為適合注入對話的上下文區塊。

{
  "name": "obsidian_get_context",
  "description": "Get formatted context from vault for a topic",
  "parameters": {
    "topic": { "type": "string", "description": "Topic to get context for" },
    "max_tokens": { "type": "integer", "default": 2000 }
  }
}

權限邊界

MCP 伺服器應執行嚴格的邊界控制:

  1. 唯讀。伺服器讀取知識庫和索引資料庫,不建立、修改或刪除筆記。寫入操作(擷取新筆記)由獨立的 hooks 或 skills 處理,而非 MCP 伺服器。

  2. 限定知識庫範圍。伺服器僅讀取配置的知識庫路徑內的檔案。路徑穿越嘗試(../../etc/passwd)必須被拒絕。

  3. 憑證過濾輸出。即使資料庫包含預先過濾的內容,仍應對輸出套用憑證過濾作為深度防禦措施。

  4. Token 限制回應。對所有工具回應強制執行 max_tokens 限制,以防止 AI 工具接收過大的上下文區塊。

錯誤處理

MCP 工具應回傳結構化的錯誤訊息,協助 AI 工具進行復原:

def search(self, query, limit=5, max_tokens=2000):
    if not self.db_path.exists():
        return {
            "error": "Index database not found. Run the indexer first.",
            "suggestion": "python index_vault.py --full"
        }

    results = self.retriever.search(query, limit, max_tokens)

    if not results:
        return {
            "results": [],
            "message": f"No results found for '{query}'. Try broader terms."
        }

    return {
        "results": [
            {
                "file_path": r["file_path"],
                "section": r["section"],
                "text": r["chunk_text"],
                "score": round(r["rrf_score"], 4),
            }
            for r in results
        ],
        "count": len(results),
        "query": query,
    }

Claude Code 整合

Claude Code 是 Obsidian 檢索系統的主要使用者。本節涵蓋 MCP 配置、hook 整合及 obsidian_bridge.py 模式。

MCP 配置

將 Obsidian MCP 伺服器新增至 ~/.claude/settings.json

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/absolute/path/to/vault",
        "DB_PATH": "/absolute/path/to/vectors.db"
      }
    }
  }
}

新增配置後,重新啟動 Claude Code。MCP 伺服器將作為子程序啟動。驗證其是否正在運行:

> What tools do you have from the obsidian MCP server?

Claude Code 應列出可用的工具(obsidian_searchobsidian_read_note 等)。

Hook 整合

Hooks 在定義的生命週期節點擴展 Claude Code 的行為。有兩個 hooks 與 Obsidian 整合相關:

PreToolUse hook — 在代理處理工具呼叫之前查詢知識庫,自動注入相關上下文。

#!/bin/bash
# ~/.claude/hooks/pre-tool-use/obsidian-context.sh
# Automatically inject vault context before tool execution

TOOL_NAME="$1"
PROMPT="$2"

# Only inject context for code-related tools
case "$TOOL_NAME" in
    Edit|Write|Bash)
        # Query the vault
        CONTEXT=$(python /path/to/retriever.py search "$PROMPT" --limit 3 --max-tokens 1500)
        if [ -n "$CONTEXT" ]; then
            echo "---"
            echo "Relevant vault context:"
            echo "$CONTEXT"
            echo "---"
        fi
        ;;
esac

PostToolUse hook — 將重要的工具輸出擷取回知識庫,供未來檢索使用。

#!/bin/bash
# ~/.claude/hooks/post-tool-use/capture-insight.sh
# Capture significant outputs to vault (selective)

TOOL_NAME="$1"
OUTPUT="$2"

# Only capture substantial outputs
if [ ${#OUTPUT} -gt 500 ]; then
    python /path/to/capture.py --text "$OUTPUT" --source "claude-code-$TOOL_NAME"
fi

obsidian_bridge.py 模式

橋接模組提供 Python API,供 hooks 和 skills 呼叫:

# obsidian_bridge.py
from retriever import HybridRetriever

_retriever = None

def get_retriever():
    global _retriever
    if _retriever is None:
        _retriever = HybridRetriever(
            db_path="/path/to/vectors.db",
            vault_path="/path/to/vault",
        )
    return _retriever

def search_vault(query, limit=5, max_tokens=2000):
    """Search vault and return formatted context."""
    retriever = get_retriever()
    results = retriever.search(query, limit, max_tokens)

    if not results:
        return ""

    lines = ["## Vault Context\n"]
    for r in results:
        lines.append(f"**{r['file_path']}** — {r['section']}")
        lines.append(f"> {r['chunk_text'][:500]}")
        lines.append("")

    return "\n".join(lines)

/capture Skill

用於將洞見擷取回知識庫的 Claude Code skill:

/capture "OAuth token rotation requires both access and refresh token invalidation"
  --domain security
  --tags oauth,tokens

此 skill 會在 00-inbox/ 中建立一則含有正確 frontmatter 的新筆記,並觸發增量重新索引,使新筆記可立即被搜尋。

上下文視窗管理

整合時應注意 Claude Code 的上下文視窗:

  • 每次查詢注入的上下文限制在1,500至2,000個 tokens。超過此數量會與代理的工作記憶競爭。
  • 包含來源標註。始終包含檔案路徑和章節標題,以便代理可以引用來源。
  • 截斷區塊文字。較長的區塊應以 ... 截斷而非完全省略。前300至500個字元通常包含關鍵資訊。
  • 不要在每次工具呼叫時都注入。PreToolUse hook 應根據所呼叫的工具選擇性地注入上下文。讀取操作不需要知識庫上下文,寫入和編輯操作則受益於此。

Codex CLI 整合

Codex CLI 透過 config.toml 連接 MCP 伺服器。其整合模式在配置語法和指令傳遞方面與 Claude Code 不同。

MCP 配置

新增至 .codex/config.toml~/.codex/config.toml

[mcp_servers.obsidian]
command = "python"
args = ["/path/to/obsidian_mcp.py"]

[mcp_servers.obsidian.env]
VAULT_PATH = "/absolute/path/to/vault"
DB_PATH = "/absolute/path/to/vectors.db"

AGENTS.md 模式

Codex CLI 讀取 AGENTS.md 以取得專案層級的指令。請包含知識庫搜尋指引:

## 可用工具

### Obsidian 知識庫(MCP:obsidian)
使用 `obsidian_search` 工具從知識庫中尋找相關上下文。
在您需要以下資訊時搜尋知識庫:
- 概念或模式的背景資料
- 先前的決策或理由
- 實作所需的參考資料

查詢範例:
- "authentication patterns in FastAPI"
- "how does the review aggregator work"
- "sqlite-vec configuration"

與 Claude Code 的差異

功能 Claude Code Codex CLI
MCP 設定 settings.json config.toml
Hooks ~/.claude/hooks/ 不支援
Skills ~/.claude/skills/ 不支援
指令檔案 CLAUDE.md AGENTS.md
核准模式 --dangerously-skip-permissions suggest / auto-edit / full-auto

關鍵差異: Codex CLI 不支援 hooks。自動上下文注入模式(PreToolUse hook)無法使用。取而代之的是,在 AGENTS.md 中加入明確指令,告訴代理在開始工作前搜尋知識庫。


Cursor 與其他工具

Cursor 及其他支援 MCP 的 AI 工具可以連接到同一個 Obsidian MCP 伺服器。本節涵蓋常見工具的設定方式。

Cursor

在專案根目錄的 .cursor/mcp.json 中加入:

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/absolute/path/to/vault",
        "DB_PATH": "/absolute/path/to/vectors.db"
      }
    }
  }
}

Cursor 的 .cursorrules 檔案可以包含使用知識庫的指令:

When working on implementation tasks, search the Obsidian vault
for relevant context before writing code. Use the obsidian_search
tool with descriptive queries about the concept you're implementing.

相容性矩陣

工具 MCP 支援 傳輸方式 設定位置
Claude Code 完整 STDIO ~/.claude/settings.json
Codex CLI 完整 STDIO .codex/config.toml
Cursor 完整 STDIO .cursor/mcp.json
Windsurf 完整 STDIO .windsurf/mcp.json
Continue.dev 部分 HTTP ~/.continue/config.json
Zed 開發中 STDIO 設定介面

非 MCP 工具的替代方案

對於不支援 MCP 的工具,檢索器可以封裝為 CLI:

# Search from command line
python retriever_cli.py search "query text" --limit 5

# Output formatted for copy-paste into any tool
python retriever_cli.py context "query text" --format markdown

此 CLI 會輸出結構化文字,可手動貼入任何 AI 工具的輸入欄位。雖然不如 MCP 整合那樣優雅,但可通用於所有工具。


從結構化筆記實現提示快取

知識庫中的結構化筆記可作為可重複使用的上下文區塊,減少 AI 互動中的 token 消耗。本節涵蓋快取鍵設計與 token 預算管理。

模式說明

與其在每次互動時搜尋上下文,不如從結構良好的知識庫筆記中預先建構上下文區塊並加以快取:

# cache_keys.py
CONTEXT_BLOCKS = {
    "auth-patterns": {
        "vault_query": "authentication patterns implementation",
        "max_tokens": 1500,
        "ttl_hours": 24,  # Rebuild daily
    },
    "api-conventions": {
        "vault_query": "API design conventions REST patterns",
        "max_tokens": 1000,
        "ttl_hours": 168,  # Rebuild weekly
    },
    "project-architecture": {
        "vault_query": "current project architecture decisions",
        "max_tokens": 2000,
        "ttl_hours": 12,  # Rebuild twice daily
    },
}

快取失效

快取失效基於兩個訊號:

  1. TTL 到期。 每個上下文區塊都有存活時間(TTL)。當 TTL 到期時,系統會重新查詢知識庫來重建該區塊。
  2. 知識庫變更偵測。 當索引器偵測到對快取上下文區塊有貢獻的檔案發生變更時,該區塊會立即失效。

Token 預算管理

一個工作階段以總上下文預算開始。快取區塊會消耗該預算的一部分:

Total context budget:    8,000 tokens
├─ System prompt:        1,500 tokens
├─ Cached blocks:        3,000 tokens (pre-loaded)
├─ Dynamic search:       2,000 tokens (on-demand)
└─ Conversation:         1,500 tokens (remaining)

快取區塊在工作階段開始時載入。動態搜尋結果則按每次查詢填充剩餘預算。這種混合方法讓代理擁有常用上下文的基準,同時為特定查詢保留預算空間。

使用前後的 Token 消耗比較

未使用快取: 每個相關查詢都會觸發知識庫搜尋,回傳 1,500-2,000 個 token 的上下文。在一個工作階段中進行 10 次查詢,代理會消耗 15,000-20,000 個 token 的知識庫上下文。

使用快取: 三個預先建構的上下文區塊總共消耗 4,500 個 token。額外搜尋每次獨特查詢增加 1,500-2,000 個 token。在 10 次查詢中若有 6 次被快取區塊涵蓋,代理僅消耗 4,500 +(4 × 1,500)= 10,500 個 token——大約是未快取用量的一半。


PostToolUse Hooks 實現上下文壓縮

工具輸出可能非常冗長:堆疊追蹤、檔案列表、測試結果。PostToolUse hook 可以在這些輸出消耗上下文視窗空間之前進行壓縮。

問題所在

一個執行測試的 Bash 工具呼叫可能回傳:

PASSED tests/test_auth.py::test_login_success
PASSED tests/test_auth.py::test_login_failure
PASSED tests/test_auth.py::test_token_refresh
PASSED tests/test_auth.py::test_session_expiry
... (200 more lines)
FAILED tests/test_api.py::test_rate_limit_exceeded

完整輸出為 5,000 個 token,但真正有用的資訊只在 2 行:200 個通過、1 個失敗。

Hook 實作

#!/bin/bash
# ~/.claude/hooks/post-tool-use/compress-output.sh
# Compress verbose tool outputs to preserve context window

TOOL_NAME="$1"
OUTPUT="$2"
OUTPUT_LEN=${#OUTPUT}

# Only compress large outputs
if [ "$OUTPUT_LEN" -lt 2000 ]; then
    exit 0  # Pass through unchanged
fi

case "$TOOL_NAME" in
    Bash)
        # Compress test output
        if echo "$OUTPUT" | grep -q "PASSED\|FAILED"; then
            PASSED=$(echo "$OUTPUT" | grep -c "PASSED")
            FAILED=$(echo "$OUTPUT" | grep -c "FAILED")
            FAILURES=$(echo "$OUTPUT" | grep "FAILED")
            echo "Tests: $PASSED passed, $FAILED failed"
            if [ "$FAILED" -gt 0 ]; then
                echo "Failures:"
                echo "$FAILURES"
            fi
        fi
        ;;
esac

防止遞迴觸發

壓縮 hook 若產生輸出,可能在未加防護的情況下觸發自身:

# Guard against recursive invocation
if [ -n "$COMPRESS_HOOK_ACTIVE" ]; then
    exit 0
fi
export COMPRESS_HOOK_ACTIVE=1

壓縮策略

輸出類型 偵測方式 壓縮策略
測試結果 PASSED / FAILED 關鍵字 計算通過/失敗數量,僅顯示失敗項目
檔案列表 命令中包含 lsfind 截取前 20 筆並附上總數
堆疊追蹤 Traceback 關鍵字 保留首尾框架與錯誤訊息
Git 狀態 modified: / new file: 按狀態彙整數量
建構輸出 warning: / error: 移除資訊行,保留警告與錯誤

訊號收集與分類管道

收集層決定了哪些內容進入知識庫。若缺乏篩選,知識庫將累積大量雜訊。本節介紹將訊號路由至領域資料夾的評分管道。

來源

訊號來自多個管道:

  • RSS 訂閱:技術部落格、安全公告、版本發布說明
  • 書籤:透過 Obsidian Web Clipper 或書籤工具儲存的瀏覽器書籤
  • 電子報:電子郵件通訊中的重點摘錄
  • 手動擷取:閱讀、對話或研究過程中撰寫的筆記
  • 工具輸出:透過 hooks 擷取的重要 AI 工具輸出

評分維度

每個訊號依據四個維度進行評分(各維度 0.0 至 1.0):

維度 問題 低分(0.0-0.3) 高分(0.7-1.0)
相關性 這是否與我的活躍領域相關? 邊緣性質,超出範圍 與目前工作直接相關
可行性 我能否運用這項資訊? 純理論,無法應用 可直接應用的具體技術或模式
深度 內容的實質性如何? 標題、淺層摘要 附帶範例的深入分析
權威性 來源的可信度如何? 匿名部落格、未經驗證 一手來源、同行評審、公認專家

綜合分數與路由

composite = (relevance * 0.35) + (actionability * 0.25) +
            (depth * 0.25) + (authority * 0.15)
分數範圍 動作
0.55+ 自動路由至領域資料夾
0.40 - 0.55 排入人工審查佇列
< 0.40 丟棄(不儲存)

領域路由

評分超過 0.55 的訊號會根據關鍵字比對與主題分類,路由至 12 個領域資料夾之一:

05-signals/
├── ai-tooling/        # Claude, LLMs, AI development tools
├── security/          # Vulnerabilities, auth, cryptography
├── systems/           # Architecture, distributed systems
├── programming/       # Languages, patterns, algorithms
├── web/               # Frontend, backends, APIs
├── data/              # Databases, data engineering
├── devops/            # CI/CD, containers, infrastructure
├── design/            # UI/UX, product design
├── mobile/            # iOS, Android, cross-platform
├── career/            # Industry trends, hiring, growth
├── research/          # Academic papers, whitepapers
└── other/             # Signals that don't fit a domain

生產環境統計

經過 14 個月的運作:

指標 數值
處理的訊號總數 7,771
自動路由(>0.55) 4,832(62%)
排入審查佇列(0.40-0.55) 1,543(20%)
丟棄(<0.40) 1,396(18%)
活躍領域資料夾 12
每日平均訊號數 ~18

知識圖譜模式

Obsidian 的 wiki-link 圖譜編碼了筆記之間的關係。本節涵蓋連結語意、用於擴展上下文的圖譜遍歷,以及降低圖譜品質的反模式。

每個 wiki-link 在圖譜中建立一條有向邊。Obsidian 同時追蹤正向連結與 backlink:

  • 正向連結:筆記 A 包含 [[筆記 B]] → A 連結至 B
  • Backlink:筆記 B 顯示筆記 A 引用了它

圖譜根據上下文編碼不同類型的關係:

連結模式 語意 範例
行內連結 「與…相關」 “See [[OAuth Token Rotation]] for details”
標題連結 「包含子主題」 ”## Related\n- [[Token Rotation]]\n- [[Session Management]]”
標籤式連結 「被分類為」 ”[[type/reference]]”
MOC 連結 「屬於」 Maps of Content 筆記列出相關筆記

Maps of Content(MOC)

MOC 是索引筆記,將相關筆記組織成可導航的結構:

---
title: "Authentication & Security MOC"
type: moc
domain: security
---

## Core Concepts
- [[OAuth 2.0 Overview]]
- [[JWT Token Anatomy]]
- [[Session Management Patterns]]

## Implementation Patterns
- [[OAuth Token Rotation]]
- [[Refresh Token Security]]
- [[PKCE Flow Implementation]]

## Failure Modes
- [[Token Expiry Handling]]
- [[Session Fixation Prevention]]
- [[CSRF Defense Strategies]]

MOC 從兩方面提升檢索效果:

  1. 直接匹配。搜尋「authentication overview」時會匹配到 MOC 本身,為代理提供一份精心策劃的相關筆記列表。
  2. 上下文擴展。找到特定筆記後,檢索器可以檢查該筆記是否出現在任何 MOC 中,並將 MOC 的結構納入結果,為代理提供更廣泛主題的脈絡圖。

圖譜遍歷擴展上下文

檢索器的未來增強功能:在找到最佳結果後,透過追蹤連結來擴展上下文:

def expand_context(results, depth=1):
    """Follow wiki-links from top results to find related context."""
    expanded = set()
    for result in results:
        # Parse wiki-links from chunk text
        links = extract_wiki_links(result["chunk_text"])
        for link_target in links:
            # Resolve link to file path
            target_path = resolve_wiki_link(link_target)
            if target_path and target_path not in expanded:
                expanded.add(target_path)
                # Include target's most relevant chunk
                target_chunks = get_chunks_for_file(target_path)
                # ... rank and include best chunk
    return results + list(expanded_results)

此功能尚未在目前的檢索器中實作,但它是圖譜結構的自然延伸。

反模式

孤立叢集。一組筆記相互連結,但與知識庫的其餘部分沒有任何連接。Obsidian 的圖譜面板會將這些顯示為斷開的孤島。孤立叢集表明缺少 MOC 或缺少跨領域連結。

標籤蔓延。不一致地使用標籤或建立過多細粒度標籤。一個擁有 5,000 篇筆記卻有 500 個不同標籤的知識庫,平均每 10 個標籤僅對應 1 篇筆記——這些標籤無法有效用於篩選。應整合為 20-50 個與您的領域資料夾對應的高階標籤。

連結多、內容少的筆記。完全由 wiki-link 組成而沒有任何文字敘述的筆記。這類筆記的索引效果很差,因為分塊器沒有文字可供 embedding。請至少添加一段說明這些連結筆記之間關聯性的文字。

對所有內容都使用雙向連結。並非每個引用都需要成為 wiki-link。在行文中順帶提及「OAuth」並不需要使用 [[OAuth 2.0 Overview]]。請將 wiki-link 保留給有意圖的、可導航的關係——點擊連結確實能提供有用的上下文時才使用。


開發者工作流程配方

將知識庫檢索與日常開發任務結合的實用工作流程。

每日晨間上下文載入

開始一天的工作時載入相關上下文:

Search my vault for notes about [current project] updated in the last week

檢索器會回傳與您活躍專案相關的近期筆記,讓您快速回顧上次中斷的進度。比重新閱讀昨天的 commit 訊息更有效率。

編碼過程中的研究擷取

在實作功能時,無需離開編輯器即可擷取洞察:

/capture "FastAPI dependency injection with async generators requires yield,
not return. The generator is the dependency lifecycle."
  --domain programming
  --tags fastapi,dependency-injection

擷取的洞察會立即被索引,並可供未來檢索使用。數月累積下來,這些微型擷取會構建出一套針對具體實作的知識語料庫。

專案啟動

開始新專案或新功能時:

  1. 搜尋知識庫:「我對 [技術/模式] 有哪些了解?」
  2. 審查前 5 項結果,回顧先前的決策與注意事項
  3. 檢查該領域是否已有 MOC;若無,則建立一個
  4. 搜尋失敗模式:「[技術] 的常見問題」

利用知識庫搜尋進行除錯

遇到錯誤或非預期行為時:

Search my vault for [error message or symptom]

先前的除錯筆記通常包含根本原因與修復方法。這對於跨專案的重複問題特別有價值——知識庫記住了您所遺忘的。

程式碼審查準備

在審查 PR 之前:

Search my vault for patterns and conventions about [module being changed]

知識庫會回傳與正在審查的程式碼相關的先前決策、架構約束與編碼規範。審查將基於機構知識而非僅憑差異對比。

效能調校

本節涵蓋針對不同知識庫規模和使用模式的最佳化策略。

索引大小管理

知識庫規模 區塊數 資料庫大小 完整重建索引 增量更新
500 篇筆記 ~1,500 3 MB 15 秒 <1 秒
2,000 篇筆記 ~6,000 12 MB 45 秒 2 秒
5,000 篇筆記 ~15,000 30 MB 2 分鐘 4 秒
15,000 篇筆記 ~50,000 83 MB 4 分鐘 <10 秒
50,000 篇筆記 ~150,000 250 MB 15 分鐘 30 秒

當筆記數達到50,000以上時,建議: - 將批次大小從64增加到128以加速embedding處理 - 使用WAL模式(預設)以支援並行存取 - 在離峰時段執行完整重建索引

查詢最佳化

WAL模式。 SQLite的Write-Ahead Logging模式可在索引器寫入時允許並行讀取:

db.execute("PRAGMA journal_mode=WAL")

當MCP伺服器在索引器執行增量更新時處理查詢,這點至關重要。

連線池。 MCP伺服器應重複使用資料庫連線,而非每次查詢都開啟新連線。單一長效連線搭配WAL模式即可支援並行讀取。

# MCP server initialization
db = sqlite3.connect(DB_PATH, check_same_thread=False)
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA mmap_size=268435456")  # 256 MB mmap

記憶體映射I/O。 mmap_size pragma指示SQLite對資料庫檔案使用記憶體映射I/O。對於83 MB的資料庫,將整個檔案映射到記憶體中可消除大部分磁碟讀取。

FTS5最佳化。 完整重建索引後,請執行:

INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');

這會合併FTS5內部的b-tree區段,降低後續搜尋的查詢延遲。

擴展效能基準

測試環境:Apple M3 Pro、36 GB RAM、NVMe SSD:

操作 500 篇筆記 5K 篇筆記 15K 篇筆記 50K 篇筆記
BM25查詢 2ms 5ms 12ms 25ms
向量查詢 1ms 3ms 8ms 20ms
RRF融合 <1ms <1ms 3ms 5ms
完整搜尋 3ms 8ms 23ms 50ms

所有基準測試皆包含資料庫存取、查詢執行及結果格式化。MCP STDIO通訊的網路延遲會額外增加1-2ms。


疑難排解

索引偏移

症狀: 搜尋回傳過時的結果,或遺漏最近新增的筆記。

原因: 新增筆記後增量索引器未執行,或檔案的mtime未更新(例如從其他機器同步時保留了原始時間戳記)。

修復方式: 執行完整重建索引:python index_vault.py --full

Embedding模型更換

症狀: 更換embedding模型後,向量搜尋回傳無意義的結果。

原因: 舊向量(來自先前的模型)正在與新的查詢向量進行比較。維度或向量空間語義不相容。

修復方式: 索引器應偵測模型雜湊值不匹配並自動觸發完整重建索引。若未自動執行,請手動清除資料庫並重建索引:

rm vectors.db
python index_vault.py --full

FTS5維護

症狀: 經過多次增量更新後,FTS5查詢回傳不正確或不完整的結果。

原因: 經過多次小型更新後,FTS5內部區段可能變得碎片化。

修復方式: 重建並最佳化:

INSERT INTO chunks_fts(chunks_fts) VALUES('rebuild');
INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');

MCP逾時

症狀: AI工具回報MCP伺服器逾時。

原因: 第一次查詢會觸發模型載入(延遲初始化),需要2-5秒。AI工具的預設MCP逾時時間可能較短。

修復方式: 在伺服器啟動時預先暖機模型:

# In MCP server initialization
retriever = HybridRetriever(db_path, vault_path)
retriever.search("warmup", limit=1)  # Trigger model load

SQLite檔案鎖定

症狀: 出現SQLITE_BUSYSQLITE_LOCKED錯誤。

原因: 多個程序同時寫入資料庫。WAL模式允許並行讀取,但僅允許一個寫入器。

修復方式: 確保只有一個程序(索引器)寫入資料庫。MCP伺服器和hooks應僅進行讀取。若您需要並行寫入,請使用WAL模式並設定忙碌逾時:

db.execute("PRAGMA busy_timeout=5000")  # Wait up to 5 seconds

sqlite-vec無法載入

症狀: 向量搜尋已停用;檢索器以僅BM25模式運行。

原因: sqlite-vec擴充功能未安裝、在函式庫路徑中找不到,或與SQLite版本不相容。

修復方式:

# Install via pip
pip install sqlite-vec

# Or compile from source
git clone https://github.com/asg017/sqlite-vec
cd sqlite-vec && make

驗證擴充功能是否正常載入:

import sqlite3
db = sqlite3.connect(":memory:")
db.enable_load_extension(True)
db.load_extension("vec0")
print("sqlite-vec loaded successfully")

大型知識庫記憶體問題

症狀: 對大型知識庫(50,000篇以上筆記)進行完整重建索引時出現記憶體不足錯誤。

原因: Embedding批次大小過大,或所有檔案內容同時載入記憶體。

修復方式: 減少批次大小並以增量方式處理檔案:

BATCH_SIZE = 32  # Reduce from 64

同時確保索引器逐一處理檔案(讀取、分塊、並對每個檔案進行embedding後再處理下一個),而非將所有檔案一次載入記憶體。


遷移指南

從Apple Notes遷移

  1. 透過「全部匯出」選項(macOS)匯出Apple Notes,或使用遷移工具如apple-notes-liberator
  2. 使用markdownifypandoc將HTML匯出檔轉換為markdown
  3. 將轉換後的檔案移至知識庫的00-inbox/資料夾
  4. 檢閱並為每篇筆記新增frontmatter
  5. 將筆記移至適當的領域資料夾

從Notion遷移

  1. 從Notion匯出:Settings → Export → Markdown & CSV
  2. 將匯出的壓縮檔解壓至知識庫的00-inbox/資料夾
  3. 修復Notion特有的markdown格式問題:
  4. Notion使用- [ ]表示核取清單——這是標準markdown語法
  5. Notion將屬性表格以HTML呈現——請轉換為YAML frontmatter
  6. Notion以相對路徑嵌入圖片——請將圖片複製到您的附件資料夾
  7. 新增標準frontmatter(typedomaintags
  8. 以Obsidian wiki-links取代Notion頁面連結

從Google Docs遷移

  1. 使用Google Takeout匯出所有文件
  2. .docx檔案轉換為markdown:pandoc -f docx -t markdown input.docx -o output.md
  3. 批次轉換:for f in *.docx; do pandoc -f docx -t markdown "$f" -o "${f%.docx}.md"; done
  4. 移至知識庫,新增frontmatter,整理至資料夾

從純Markdown(非Obsidian)遷移

若您已有一個markdown檔案目錄:

  1. 以Obsidian知識庫方式開啟該目錄(Obsidian → Open Vault → Open folder)
  2. 若該目錄有版本控制,請將.obsidian/加入.gitignore
  3. 建立frontmatter範本並套用至現有檔案
  4. 在閱讀和整理過程中開始使用[[wiki-links]]連結筆記
  5. 立即執行索引器——檢索系統從第一天就能運作

從其他檢索系統遷移

若您正從不同的embedding/搜尋系統遷移:

  1. 不要嘗試遷移向量。 不同模型產生不相容的向量空間。請使用新模型執行完整重建索引。
  2. 遷移內容,而非索引。 知識庫檔案是唯一的真實來源。索引是衍生產物。
  3. 遷移後進行驗證。 執行10-20個您已知答案的查詢,驗證結果是否符合預期。

更新日誌

日期 變更內容
2026-03-02 新增potion-base-32M和potion-retrieval-32M至模型比較。新增量化/降維章節。新增MCP規格演進說明。
2026-03-01 初始發布

參考文獻


  1. Cormack, G.V., Clarke, C.L.A., and Buettcher, S. Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods. SIGIR, 2009. 提出以k=60作為無參數方法的RRF,用於合併排序列表。 

  2. OpenAI Embeddings Pricing. text-embedding-3-small:每百萬token $0.02。估計每次完整重新索引知識庫的成本:約$0.30。 

  3. van Dongen, T. et al. Model2Vec: Turn any Sentence Transformer into a Small Fast Model. arXiv, 2025. 描述從句子轉換器產生靜態embeddings的蒸餾方法。 

  4. MTEB: Massive Text Embedding Benchmark. potion-base-8M平均分數50.03,相比all-MiniLM-L6-v2的56.09(保留89%效能)。 

  5. SQLite FTS5 Extension. FTS5提供具備BM25排序及可設定欄位權重的全文檢索功能。 

  6. sqlite-vec: A vector search SQLite extension. 提供vec0虛擬表格,用於在SQLite中進行KNN向量搜尋。 

  7. Robertson, S. and Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 2009. 

  8. Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. EMNLP, 2020. 密集表示在開放領域問答上超越BM25達9-19%。 

  9. Reimers, N. and Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. EMNLP, 2019. 密集語意相似度的基礎研究。 

  10. Luan, Y. et al. Sparse, Dense, and Attentional Representations for Text Retrieval. TACL, 2021. Hybrid retrieval在MS MARCO上持續優於單一模態方法。 

  11. SQLite Write-Ahead Logging. WAL模式支援並行讀取與單一寫入。 

  12. Gao, Y. et al. Retrieval-Augmented Generation for Large Language Models: A Survey. arXiv, 2024. RAG架構與分塊策略的綜合調查。 

  13. Thakur, N. et al. BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS, 2021. 

  14. Model2Vec: Distill a Small Fast Model from any Sentence Transformer. Minish Lab, 2024. 

  15. Obsidian Documentation. Obsidian官方文件。 

  16. Model Context Protocol Specification. 連接AI工具與資料來源的MCP標準。 

  17. 作者的生產環境資料。16,894個檔案、49,746個區塊、83.56 MB SQLite資料庫,14個月內處理7,771個訊號。查詢延遲透過time.perf_counter()測量。 

  18. Model2Vec Potion Models. Minish Lab, 2025. Potion-base-32M(MTEB 52.46)、potion-retrieval-32M(MTEB retrieval 36.35),以及v0.5.0+的量化/降維功能。 

VAULT obsidian.md INDEXED