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

AI Workflow · part 12

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

cat --toc

TL;DR

我拿 ~600 篇 markdown 筆記當 AI 的長期記憶。qmd 搜尋讓筆記找得到,但 keyword 有天花板:同一件事、用詞不同的兩篇筆記永遠湊不到一起。加上 musubi 知識圖譜才補起來——連結靠『共享概念』,不是靠用詞。這張圖一個下午重寫三次才不是垃圾:v1 用 embedding 相似度連邊,程式碼檔洗版成一坨 hairball、零訊號;v2 改用共享概念,卻冒出 102 座孤島(35%);v3 把概念字典擴三倍才落地——零孤島、約 7000 條 edge、五個真社群。兩個教訓:層是疊加的(檔案=真相源,搜尋和圖是可重建快取,箭頭只往上),圖也要治理——我寫來描述系統的索引檔,後來變成圖裡最吵的 node。

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

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

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

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

知識圖譜要補的就是這個。你可以把它想成一張自動畫出來的地圖:哪幾個檔在講相關的事,就用線連起來。重點是它連的方式——不是比對用詞,是比對『概念』。兩篇只要談到同一個概念,就牽一條線,就算它們一個字都沒重複。有了這張地圖,我才問得出搜尋框答不了的問題:這篇筆記,還連到哪些我早就忘掉的東西?

這篇文章就是加這張地圖的過程。老實說不太順:我先做壞了兩個版本,一版連成一團亂麻、什麼都黏在一起,另一版反過來,三分之一的筆記變成一座座孤島、誰也不認識誰。第三版才總算能用。而且它上線之後還絆到自己一次——我為了幫 AI 帶路、親手畫的那張索引,後來反而把真正的答案蓋掉了。這些一個一個講。


前言

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

這個系列的 Part 6,我介紹過怎麼用搜尋引擎加圖這套工具『幫你省 token』。這篇往下拆一層:這兩塊東西,在我自己的環境裡到底是怎麼做出來的。每一層都不是我事先規劃好的,是下面那層先撞了牆,才逼出來的。

所以這篇往前講,連我做壞的版本一起講。

第一道牆:大約 600 篇筆記,我的 AI 一直忘記自己寫過

agent 有了記憶之後,還是常常把早就解過的事再解一次。你換一個 session、context 乾乾淨淨,它又把三個禮拜前自己寫下的那個 fix,原封不動再推導一遍。理由很單純:一個資料夾的 markdown 不是檢索,它就只是一個資料夾。

qmd 是一個在本地跑的 markdown 搜尋引擎——keyword 大約 30ms、vector 大約 2 秒。它怎麼運作我在 Part 6 拆過,這裡不重講(三種搜尋模式、加一筆 token 帳,都在那)。這裡只講一件事:我把 qmd 接成 Claude Code 的 first-class tool 之後,『開工前先查一下記憶』這個動作就自動發生了。平常某一天我回頭數,一天記到 64 次查詢——沒特別想,它就這樣發生了。這堆筆記,從一個資料夾變成了記憶。

然後,這堆筆記唬了我一把:它讓我以為整套記憶系統跑得好得很。

那份說我 82% 搜尋都在失敗、其實在說謊的稽核

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

錯了。拿一句長長的自然語言去跑 keyword 計分,本來就『必然』撈不太到東西:你問的是一整句話,它比對的只是 token。同一批被判低分的 query,我丟去 vector search,分數打到 0.7 上下。真的一篇一篇去查,只有 2 篇是真的缺。那 82% 不是知識缺口,是一個假象——我量到的其實是『我在用哪一種檢索方法』,不是『我的知識庫缺不缺』。

修法很小,現在是一條硬規則:keyword 空手、或者 query 超過大約四個詞,就先自動用 vector 重試,再做別的。

但重點不在這個修法。vector 解掉的只是『同一個 query 換句話講』;更大的問題還在——它沒辦法告訴我,我正在讀的這篇筆記,『還』連到了哪些東西。問『找那篇講 X 的筆記』,意思比對有效;問『有什麼東西連到 X、是我忘了的』,根本沒有東西可以拿去比對。那不是搜尋能解的問題。那是第二道牆。

第二道牆:搜尋找得到筆記,卻說不出筆記之間怎麼連

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

把它編碼進去的,是『圖』。筆記當 node,兩篇只要講到同一個概念就牽一條 edge,算一次、之後就能一直 traverse。這樣一來,『這篇的鄰居是誰?』變成一個真的問得出口的 query;『哪些筆記是孤立的、哪些太熱門、哪些正在過時?』也是——這些全是搜尋框答不了的問題。

這個想法我不是憑空冒出來的。Graphify 那套 extract→graph→cluster 的流程、LightRAG,還有一篇叫 〈Stop Calling It Memory〉 的文章——它主張 flat-file 的記憶沒辦法 filter、也沒辦法 traverse,所以那還不算真的記憶。

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

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

這張圖的重點只有一個:圖沒有取代搜尋,搜尋也沒有取代檔案。每一層都是疊在下一層上面的,箭頭只往上——快取壞了可以從檔案重建,檔案壞了不可能從快取長回來。

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

你以為難的地方是『把圖建出來』。其實真正難的是能不能『快點把它建壞』——你得先多踩幾次雷,才知道一條有用的 edge 該長什麼樣。我一個下午做了三版。

v1 用 embedding 相似度來連 edge,結果是一坨 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。

那個下午,就是我之前那篇比較乾淨的文章裡從來沒寫進去的部分。工具現在能用了——但走到能用這一步,靠的是三張錯的圖,加一個被我丟掉的 stopword。說穿了,建知識圖譜沒有一步登天這種事:你得先把它練壞幾次,那條好 edge 的手感才回得來。

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

最讓我火大的是這一段。圖做完了——我給它取名叫 musubi,日文『繫起來』的意思。碰到『這跟什麼相關』這種問題,它明顯比純搜尋好用。然後我發現,我在 Claude Code 裡還是一直伸手去拿那個舊的搜尋框,新做的這個幾乎碰都沒碰。

原因跟品質一點關係都沒有。qmd 對 agent 來說是一個 first-class tool——它列在 tool schema 裡,看得到。musubi 呢,『只是一個 CLI』。agent 會伸手去拿的,是它 schema 裡列出來的那些工具,不是我的 shell history——介面沒告訴它有這條命令,它就當這條命令不存在。工具更有用,卻更不可見;而每一次,agent 伸手拿的都是『看得見』的那個。

修法大概 150 行:我把 musubi 的 search、neighbors、cold 包成一個 MCP server,讓 agent 直接看到它們是真的工具。包完之後,分工自己就定下來了——『找過去的經驗、給我相關的』走圖,『給我那個特定的檔案』走搜尋。

通則是這個:替一個 agent 做東西,最後一步不是把它做好,是把它做到『看得見』。模型看不到的工具,就是不會被用的工具。

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

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

那個具體的失敗,講出來幾乎有點好笑。我寫過一份 orientation 檔——一份手動維護的索引,用來幫 agent 在知識庫裡認路。這份檔變成了整張圖最大的 hub,degree 破 1100,因為它引用了所有東西。於是我搜某一篇特定筆記時,回傳結果開始把『那份索引』排在筆記本身前面:graph boost 硬是把這份 meta-doc 抬了上去,壓在它本來該指向的那個答案上面。我寫來『描述這個系統』的東西,變成了系統裡最吵的那個 node。

修法就是把排序規則收緊:直接命中的排在圖鄰居前面、meta-doc 沒被指名就降權、鄰居的 boost 設上限、鏡像重複的合併成單一 canonical(1272 篇 raw 併成 993 篇)。

這次踩的坑,如果你讀過 Part 11 會很眼熟——系統對自己的描述,慢慢跟它實際在做的事對不上——只是這次發生在更底層。Part 11 講的是 doctrine 跟 runtime 漂開;這裡是一張蓋在知識上面的圖,跟知識本身一樣需要治理。

收穫

最花時間的地方

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

可以搬走的診斷

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

通則

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

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

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

架子、伸手就拿得到的工具、還有那張只有你知道哪支鑽頭配哪個治具的地圖——三樣到齊,這間工作坊才真的是你的。qmd 跟 musubi 都是開源、在本地跑,任何一個 markdown 資料夾都能套上去。

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

常見問題

AI 記憶只靠 keyword 搜尋,為什麼不夠?
因為 keyword 只找『用同樣字詞』的筆記。同一件事,一篇寫『模型佔著記憶體不放』、另一篇寫『服務一開就崩』,搜其中一篇,另一篇永遠不會浮出來——明明在講同一回事,只因為用詞不一樣就湊不到。而且拿一整句自然語言去搜,keyword 幾乎一定撈不到東西——那不是知識庫有缺,是你衡量的方法不對。
知識圖譜用白話講是什麼?
把每篇筆記當成一個點,兩篇只要講到同一個概念就牽一條線,連的是『概念』不是『用詞』。連完之後你會得到一張地圖:哪些筆記在講相關的事、哪些是孤立的、哪些太熱門。這讓你問得出搜尋框答不了的問題——這篇筆記,還連到哪些我早忘掉的東西?
要做這件事,是不是得把筆記全搬進 Obsidian?
不用,我到現在還慶幸沒搬。markdown 檔留在原地當唯一的真相源,圖是疊在上面、永遠不去改那些檔的一層。檔案優先、圖只是一個隨時可以重建的衍生物——就是這一刀切得乾淨,後面每一步才都保持可逆。
儀表板說我 82% 的搜尋都失敗,代表知識庫破了一堆洞嗎?
多半不是。keyword 計分對長的自然語言問句本來就會給低分,那量到的是『我在用哪一種檢索方法』,不是『我的知識庫缺不缺』。同一批低分 query 丟去 vector search 分數就打到 0.7 上下,一篇一篇去查,真的缺的只有 2 篇。看到像知識缺口的數字,先確認它不是方法造成的假象,再決定要不要動手。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。