~/blog/from-markdown-search-to-knowledge-graph

AI Workflow · part 12

[Dev Workflow] 搜尋找得到筆記、卻連不起來:幫 AI 記憶加一張知識圖譜

cat --toc

TL;DR

我的 AI 長期記憶,是 ~600 篇純 markdown 檔,分成三層。檔案本身是唯一的真相源;一個本地搜尋引擎(qmd)讓它們找得到;一張知識圖譜(musubi)疊在上面,用『共享概念』把筆記連起來,讓我問得出搜尋答不了的問題——『這篇還連到什麼?』。撐住這套設計的一條規則:檔案是正本,搜尋和圖都是疊在上面、可以隨時重建的快取,箭頭只往上。這篇先把這個設計、每一層為什麼存在講清楚,最後才另外講我建這張圖時走錯的路(它一個下午變垃圾兩次才能用)。

首圖:一個年輕開發者在發光的筆電前,一張張 markdown 筆記卡從桌面浮起,連成一張由發光節點與邊構成的星座——幾張筆記被 cyan 與 green 的光線串起,背景是黃昏的藍紫光暈。AI-MUNINN。

白話版:把學到的東西寫成純文字檔,再教 AI 看出它們怎麼連

我把學到的東西寫成一個一個純文字檔(markdown),一個檔講一件事——踩過的坑、修好的 bug、哪台機器該怎麼設定。這樣的檔累積了大概六百個。

只是把檔堆在那,AI 用不到。你得先給它一個搜尋框,它才找得到。但搜尋框有個先天的毛病:它只找『用同樣字詞』的檔。同一件事,我一篇寫『模型一直佔著記憶體不放』、另一篇寫『那個服務一開就崩』,其實是同一個原因——記憶體被開得太滿了。可是搜其中一篇,另一篇永遠不會浮出來,只因為兩篇用的字不一樣。

知識圖譜要補的就是這個。你可以把它想成一張自動畫出來的地圖:哪幾個檔在講相關的事,就用線連起來,連的是『概念』不是『用詞』。這篇文章就是這套三層記憶的設計:每一層是什麼、為什麼要有它、換來什麼好處。至於比較亂的部分——我怎麼先把它建錯幾次的——收在最後、標得清清楚楚,不讓它擋住主線。


前言

想像一間工作坊。你先要架子,東西才不會落地——那是儲存。接著你要能一伸手就找到某一支工具——那是搜尋。但真正讓一間工作坊變成『你的』工作坊的,是另一件事:你知道三號架上那個治具,跟九號抽屜那支鑽頭,是同一件工作會一起用到的——就算架上沒有任何標籤這樣寫。那不是儲存,也不是搜尋,是一張關係的地圖,你花好幾年在腦子裡慢慢建起來的。

這個系列的 Part 6,我把工具(搜尋引擎加圖)當成一個『幫你省 token』的方法介紹給你。這篇再往下拆一層:這套記憶到底是怎麼設計的、每一塊為什麼存在。我先把整體架構講清楚,最後才另外講我走到這裡踩過的坑。

這套系統長什麼樣:三層,只有一層是真相源

先給你一張全景。我的長期記憶,底層是一個裝滿 markdown 檔的資料夾。疊在它上面的,是兩個讓這些檔『用得動』的東西——一個搜尋 index、一張圖——但這兩個都不是記憶本身,檔案才是。上面那兩層都是衍生出來的,我隨時可以砍掉重建。

三層檢索堆疊:最底層是 markdown 檔案,是唯一的真相源;中間是 qmd 的 keyword 加 vector 搜尋,是可重建的快取;最上層是 musubi 的概念圖,也是可重建的快取。側邊一個往上的箭頭寫著:快取從檔案重建,檔案不會從快取重建。

  • 檔案——一個檔講一件事,大概幾百個。這是唯一的真相源。
  • qmd——一個在本地跑、蓋在這些檔上的搜尋引擎:keyword(比對關鍵字,大約 30ms)加 vector(比對語意、不只看字面,大約 2 秒)。
  • musubi——疊在上面的一張圖,用『共享概念』把筆記連起來,讓你問得出『這跟什麼相關』,而不只是『什麼字對得上』。

接下來整篇,就是在講上面這兩層各自為什麼非有不可。它們誰都沒有取代下面那層;每一層都是在回答下一層答不了的問題。

一個資料夾為什麼不算記憶——搜尋補上的那塊

agent 一有記憶,就會撞上一個問題:它會一直重新發現自己早就知道的事。換一個 session、context 乾乾淨淨,它又把三個禮拜前自己寫下的那個 fix,原封不動再推導一遍。理由很單純:一個資料夾的 markdown 不是檢索,它就只是一個資料夾。

這就是 qmd 做的事。它讓這一堆檔『找得到』——keyword 找精確的字,vector 在你講不出確切字眼時用語意去找。等我把它接成一個 first-class tool(agent 一眼就看得到、會主動去用的工具),agent 開工前就會自己先查記憶。一天跑上幾十次很平常,我根本不會特別注意。這堆檔,從一個資料夾變成了記憶。

但搜尋也有極限,而這正是下一層存在的理由。

光有搜尋為什麼不夠——圖補上的那塊

先把兩件事分清楚。搜尋幫你做的是『找到某一篇你想找的筆記』;它答不出的是另一種問題——『我正在看的這篇,還跟哪些筆記有關?』。「找得到」跟「連得起來」是兩回事,而搜尋只做得到前者。

只要有兩篇筆記明明有關、卻沒有共同用詞,你馬上就會卡在這裡。我有一篇筆記寫『Ollama 會把模型常駐在記憶體裡』,另一篇寫『vLLM 一啟動就崩』。這兩篇其實在講同一個問題——同一個 128GB 的記憶體池被開得太滿——但它們幾乎沒有半個共同的用詞。你搜其中一篇,另一篇永遠不會冒出來。這個關聯是真的,只是它活在概念這一層,而搜尋的 index 根本沒把它記下來。

圖就記得下來。做法其實很土:把每篇筆記當成一個點(node),兩篇只要講到同一個概念,就在它們之間連一條線(edge);整張圖算一次,之後就能『順著線一路走過去』(traverse)。這樣一來,『這篇的鄰居是誰?』變成一個真的問得出口的 query;『哪些筆記是孤立的、哪些正在過時?』也是——這些全是搜尋框根本答不了的問題。

有一個設計決定,讓上面這一切加起來是安全的:我沒把任何東西搬進 Obsidian、或任何一個 app。markdown 檔留在原地當唯一的真相源;圖是疊在上面、永遠不去改動那些檔的一層。檔案優先、圖只是一個隨時可以重建的衍生物——就是這一刀切得乾淨,後面每一步才都保持可逆。

這樣分三層,換來什麼

三層疊起來,有四個我在任何記憶系統裡都會想留下的性質:

  • 層是疊加的,不是彼此取代。 圖沒有取代搜尋,搜尋也沒有取代檔案。每一層疊在下一層上面,回答它答不了的問題:檔案負責『存』、搜尋負責『找』、圖負責『連』。
  • 只有檔案是正本。 搜尋 index 跟圖都只是快取。箭頭只往一個方向——快取從檔案重建,檔案不會從快取長回來——所以我隨時可以把任一個快取砍掉重建,不會掉任何東西。
  • 不被綁死。 它就是資料夾裡的純 markdown。換機器、換工具,只要幾行指令就能從檔案把 index 跟圖重建回來。
  • 每一層都得先證明自己非有不可。 我不是一開始就規劃三層。我先有一個資料夾,撞牆了才加搜尋;又撞牆了才加圖。一層如果沒在回答下面那層答不了的問題,它就不該存在。

架構到這裡就講完了。只想直接套用的,文末那份五行 checklist 就是全部;想看我怎麼踩坑的,再往下讀。

我實際上怎麼走到這裡——那些走錯的路

底下是我走到這套設計之前試過的失敗版本,以及每次失敗教會我的事。這些都不是「用這套系統」的必要知識——只想要設計的,直接跳到收穫。

這張圖一個下午變成垃圾兩次,才終於能用

你以為難的地方是『把圖建出來』。其實真正難的是能不能『快點把它建壞』——你得先多踩幾次雷,才知道一條有用的 edge 該長什麼樣。我一個下午做了三版,而每一版的差別其實只有一件事:我到底『憑什麼說這兩篇筆記該連一條線』。

v1 我用 embedding 相似度來連 edge——embedding 就是把一段文字壓成一串數字,兩串數字越接近,兩篇就越像。結果是一坨 hairball(所有點全糊在一起、看不出結構)。我那些 .py.swift.tsx 檔把整張圖洗版了——兩個 Python 檔當純文字看天生就像,但這不代表它們在知識上相關。__init__.py 這種檔直接變成一個 hub。我得把相似度 threshold 一路拉到 0.90,才勉強看得到一點結構——光這一點就在說:embedding 距離,是一個很爛的 edge 指標。602 個 node、142K 條 edge、零訊號。

v2 我只留 markdown、edge 改用共享概念來連:乾淨多了,但這下換 102 篇筆記(35%)變成孤島,因為我的概念字典太窄,接不住它們在講什麼。

v3 我把概念字典擴成三倍、再加上從檔案路徑推出來的概念,並且讓 embedding 只在概念完全接不到時當 fallback:這才總算做對——零孤島、大約 7000 條 edge、五個真的社群。而且這五個社群告訴我一件我的資料夾從來沒告訴我的事:我的 collection 分類,跟知識實際的結構對不上。一批我自己歸在一起的交易筆記,散在三個不同的社群裡。還有一個概念 api,在當時那 291 篇裡就佔了 172 篇——它泛到變成一個 god concept(幾乎每篇都有,多到根本分不出誰跟誰),把所有東西連到所有東西,跟 NLP 裡的 stopword(像『的、是、在』這種到處都有、可以直接略過的字)一模一樣。你得先把它丟掉,剩下的連結才有意義。

同一批筆記的三種畫法。左格 v1 用 embedding:糾成一團的 hairball,幾乎全是灰色的程式碼檔,說明『程式碼檔洗版、什麼都黏在一起』。中格 v2 用窄概念:幾團連著的筆記、外圍散落一堆孤點,說明『102 座孤島,佔 35%』。右格 v3 用加權概念:五個乾淨的彩色社群,說明『0 孤島、5 個社群』。

修好這張圖的,不是一個更聰明的演算法,是一個更好的定義:什麼才算一條 edge。

稽核說我 82% 搜尋都在失敗,其實在說謊

一開始,我打開自己記憶系統的儀表板,看到一個數字:82% 的查詢『低分』。直覺——至少我的直覺——馬上跳到一個結論:知識庫破了一堆洞,我該去把缺的筆記補上。

錯了,而且錯的方式值得記下來。拿一句長長的自然語言去跑 keyword 計分,本來就『必然』撈不太到東西:你問的是一整句話,它比對的只是 token。同一批被判低分的 query,丟去 vector search 分數就打到 0.7 上下。真的一篇一篇去查,只有 2 篇是真的缺。那 82% 不是知識缺口,是我量到『自己在用哪一種檢索方法』的假象。(修法現在是一條硬規則:keyword 空手、或 query 超過大約四個詞,就先自動用 vector 重試。)看到一個檢索指標之前,先確認它量的是方法本身、還是資料本身。

我做了一個更好的工具,結果自己都不用它

這是最讓我火大的一段。圖做完之後,碰到『這跟什麼相關』這種問題,它明顯比純搜尋好用。然後我發現,我在 Claude Code 裡還是一直伸手去拿那個舊的搜尋框,新做的這個幾乎碰都沒碰。

原因跟品質一點關係都沒有。qmd 對 agent 來說是一個 first-class tool——它列在 tool schema(agent 每次開場就會讀的那份『我有哪些工具可用』清單)裡,看得到。musubi 呢,『只是一個 CLI』。agent 會伸手去拿的,是它 schema 裡列出來的那些工具,不是我的 shell history——介面沒告訴它有這條命令,它就當這條命令不存在。工具更有用,卻更不可見;而每一次,agent 伸手拿的都是『看得見』的那個。修法大概 150 行:把 musubi 掛進去,包成一個 MCP server(把外部工具掛進 agent 工具清單的標準介面),這樣 agent 就直接看到它是真的工具。通則是:替一個 agent 做東西,最後一步不是把它做好,是把它做到『看得見』。

圖長成一坨 hairball,我畫的那張索引反而把答案洗版了

每天寫筆記寫了幾個月之後,這張圖涵蓋了我所有 collection,規模衝到大約 1272 篇、二十萬條以上的 edge、平均 degree 好幾百。又是一坨 hairball——但這次是反過來的:連結太多,不是太少。

那個具體的失敗,講出來幾乎有點好笑。我寫過一份 orientation 檔——一份手動維護的索引,用來幫 agent 在知識庫裡認路。這份檔變成了整張圖最大的 hub,degree 破 1100,因為它引用了所有東西。於是我搜某一篇特定筆記時,回傳結果開始把『那份索引』排在筆記本身前面:graph boost 會幫命中筆記旁邊的鄰居加分,結果它硬是把這份 meta-doc(描述知識庫本身的檔,不是知識本身)抬了上去,壓在它本來該指向的那個答案上面。修法就是把排序規則收緊:直接命中的排在圖鄰居前面、meta-doc 沒被指名就降權、鄰居的 boost 設上限、鏡像重複的合併成單一 canonical(1272 篇 raw 併成 993 篇)。這次踩的坑如果你讀過 Part 11 會很眼熟——系統對自己的描述,慢慢跟它實際在做的事對不上——只是這次發生在更底層。一張蓋在知識上面的圖,跟知識本身一樣需要治理。

收穫

最花時間的地方

不是把圖建出來,是把它建錯兩次。整個第一個下午,說穿了只換來一個發現:embedding 相似度,對一個程式碼跟散文混在一起的 corpus 來說,是一個很爛的 edge 指標——因為程式碼不管在講什麼,當純文字看都自我相似。真正有效的訊號,是概念的共現,再把太常見的概念降權。那個東西其實就是 IDF(越稀有的概念,鑑別力越強),只是我用硬撞的方式撞出來的。

可以搬走的診斷

當一個指標看起來像知識缺口(82% 低分),先確認它是不是方法本身造成的假象,再決定要不要動手。把你的 god concept 當 stopword 處理——任何一個出現在過半筆記裡的概念,是連結的噪音,不是訊號。記得包成 MCP server:agent 在 schema 裡看不到的工具,再好也不會被用。盯著你的 meta-doc:一份『蓋在圖上面的索引』,會悄悄長成圖裡最吵的 hub,把真正的答案蓋掉。

通則

這些層是疊加的,不是彼此取代。檔案是真相源;搜尋和圖是疊在上面、可以隨時重建的快取,箭頭只往上。而且每一層,都是被下面那層撞牆逼出來的:先要能存、再要能找、最後要能連。還有第三道牆在等著:就算檢索做到再好,它撈回來的還是『原始材料』,你每一次都得重新推它到底在講什麼——這件事本身就有成本。不過那是下一篇的事了。

如果你要給 AI agent 一個 markdown 記憶,這是我試出來的順序

  1. 檔案優先。 一個檔講一件事、純文字。這是唯一『是真相源』的那一層,其他全部從它重建。
  2. 加搜尋,而且知道它的天花板在哪。 keyword 找精確的字、vector 找意思。query 是一整句時,自動 fallback 到 vector——而且不要去信一個用 keyword 算出來的『覆蓋率』數字。
  3. 需要 traverse、不只是 lookup 的時候,才加圖。 edge 用共享概念、不要用 embedding 相似度,然後把那些出現在所有東西裡的概念 stopword 掉。
  4. 讓它看得見。 如果你的使用者是一個 agent,工具沒變成 MCP tool 就還沒算做完。
  5. 治理那張圖。 直接命中排在鄰居前面、你自己畫的那些地圖降權、鏡像去重——不然你自己的索引會把答案淹掉。

在自己的筆記上試試

負責知識圖譜的就是 musubi——開源、MIT、在本地跑,建圖不需要任何 AI 服務(全是 deterministic 的概念比對,不用 LLM)。裝起來、指到一個 markdown 資料夾就行:

# 裝一次,之後就能對自己的筆記跑
uv tool install git+https://github.com/coolthor/musubi
musubi init                 # 互動式——可以先玩內建 demo,或直接指到你自己的筆記
musubi build --source ~/your-notes
musubi neighbors "docker"   # 換成你筆記裡的某個關鍵字或標題

它還有 musubi benchmark,省多少 token 你可以在自己的 corpus 上量,不用信我的數字;以及 musubi mcp——一個 stdio server,註冊進 Claude Code 之後,agent 就會把 musubi 當 first-class tool(這正好補上前面 discoverability 那個洞)。搜尋那一半是 qmd。repo 在這:github.com/coolthor/musubi(覺得有用就給顆星)。

下一篇:為什麼『搜尋得到你的筆記』還是不夠——以及我疊在這兩者之上的那一個蒸餾層。

常見問題

知識圖譜用白話講是什麼?
把每篇筆記當成一個點,兩篇只要講到同一個概念就牽一條線,連的是『概念』不是『用詞』。連完之後你會得到一張地圖:哪些筆記在講相關的事、哪些是孤立的、哪些太熱門。這讓你問得出搜尋框答不了的問題——這篇筆記,還連到哪些我早忘掉的東西?
AI 記憶只靠 keyword 搜尋,為什麼不夠?
因為 keyword 只找『用同樣字詞』的筆記。同一件事,一篇寫『模型佔著記憶體不放』、另一篇寫『服務一開就崩』,搜其中一篇,另一篇永遠不會浮出來——明明在講同一回事,只因為用詞不一樣就湊不到。這是『連得起來』的問題,不是『找得到』的問題,搜尋解不了,要靠圖。
要做這件事,是不是得把筆記全搬進 Obsidian?
不用,我到現在還慶幸沒搬。markdown 檔留在原地當唯一的真相源,圖是疊在上面、永遠不去改那些檔的一層。檔案優先、圖只是一個隨時可以重建的衍生物——就是這一刀切得乾淨,後面每一步才都保持可逆。
把檔案、搜尋、圖分成三層,而不是做成一個系統,到底有什麼好處?
每一層回答下一層答不了的問題:檔案負責存、搜尋負責找、圖負責連。分開的好處是檔案永遠是唯一的真相源,搜尋 index 跟圖都只是可重建的快取——你可以把任一個丟掉再重建,而且沒有任何東西被綁死在某個工具或某台機器上。

接著讀

  • 2026-07-15
    [Dev Workflow] 搜尋找得到,卻要每個 session 重推一遍:幫 AI 記憶加一層蒸餾層

    我拿一批 markdown 檔當 AI 的長期記憶,在檢索(搜尋加知識圖譜)之上再疊一層蒸餾層。檢索撈回的是原始材料,每個 session、每個 agent 都得重讀重推一遍;蒸餾層把定案結論冶煉一次、存成一檔一個 claim,之後直接取用。這篇講清楚這層是什麼、為什麼它的目的是『少解釋一次』而不是『逼大家講一樣』、以及我怎麼學會真的去信它。

  • 2026-07-17
    [Dev Workflow] 幫 AI agent 的 skill 減肥:193 個塞爆 2% 預算,砍到剩 7 個

    AI Workflow 系列第 15 篇。某天早上我讀 codex 紀錄,滑到一行:skill 描述被剪短,才塞得進 2% 的 context 預算。我共用的 root 累到 193 個 skill,全載進去撐爆上限,每條描述都被截斷——我天天在用的,被剪掉是為了替我從不碰的一百多個騰位子。這篇講怎麼用 per-agent allowlist(不是刪)砍到 7 個、薄殼配深引擎壓低每個 skill 的載入成本,再用一個閘門加稽核加可逆退場區守住,不讓它肥回去。

  • 2026-07-16
    [Dev Workflow] 一群 AI 交接工作卻不用重講:原來是同一套系統的兩條軸

    這是 AI Workflow 系列裡『兩軸合流』這一段的收尾。回頭看整段路,前面六篇其實在蓋同一套系統的兩條軸:持久知識(我知道什麼,長效、要用再查)和即時狀態(我現在做到哪,易逝、隨時寫)。這篇講清楚兩條軸為什麼互不代替、怎麼匯流到『多 AI 交接』、又受同一套哲學管——以及那條讓一群不完全信任的模型還能安全協作的中心律:沒有收據就不信。

  • 2026-04-13
    Claude Code Token 燒太快?8 招讓你的 Session 撐 10 倍久

    剛開始用 Claude Code,context window 一直滿。這篇解釋 token 花去哪、哪些浪費了、怎麼讓 Claude 更精準地找資料而不是暴力讀檔。

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。