「我記得上次解過這個問題」:當記憶記不下踩坑過程,我怎麼讓 AI 學會自己翻舊帳

「這問題我們上次是不是解過?」 「我記得那時候試了某個解法會把設定搞壞,後來放棄了,原因是什麼?」

在日常寫程式的對話裡,這類問句幾乎每天都會出現。但如果你的工作環境跟我一樣,天天重度依賴 Claude Code、Codex CLI 與 Pi 這些 coding agent,幾個月下來累積的對話紀錄不知不覺長到 44GB、5.3 萬個 .jsonl 檔案時,上面這句話丟出去,結果往往只有兩種:要嘛 AI 開始胡思亂想憑印象瞎掰,要嘛它試著在硬碟裡四處搜尋,最後把整個回合徹底卡死。

整理過的記憶檔記不下每一次除錯的踩坑過程,而直接搜尋原始對話檔又搜不動。我需要的是讓 AI 學會自己翻舊帳。

整理好的記憶檔,記不下踩坑的歷史現場

我之前在《MEMORY.md 只有 25KB,要當目錄用、別當倉庫塞》聊過,給 AI 讀的長期記憶檔必須維持極高密度的策展,裡面記的是拍板定案的規則、架構約束與經過驗證的決策。而在《我把自己裝的向量記憶砍掉了——928 次寫入換來 3 次搜尋》裡我也提過,把記憶檔當成無底洞什麼都塞,或是期待外掛一套複雜的向量資料庫來幫忙回憶,最後往往只會讓 AI 的注意力被稀釋,連基本的專案規範都遵守不好。

但真實的軟體開發現場,多數時間都在踩坑與試錯。

某個第三方套件在特定環境下為什麼會壞、當時試過哪幾種修法發現是死路、為了避開某個底層問題最後妥協換成哪套做法,這些充滿細節的除錯過程與失敗分支,本來就不可能、也不應該全塞進精簡的記憶檔裡。更麻煩的是,這些工作過程往往散落在三套 agent 裡:有的是 Claude Code 跑的一般開發對話,有的是交給 Codex 執行的任務,還有的是在 Pi 裡頭跑的子工作紀錄。

當某一天類似的錯誤再次冒出來,你想知道「上次到底怎麼搞定這件事」的時候,手邊能依靠的線索只有硬碟裡那批散落各處的紀錄。

如果讓 AI 直接在命令列用 grep 去掃這批龐大的 .jsonl 對話檔,龐大的資料量會立刻讓指令逾時,直接中斷當前的工作流。而市面上常見的外部方案,要嘛預設只支援英文分詞,遇到中文查詢就直接失效;要嘛動輒需要架設額外的背景服務與複雜依賴。我只想要一個極其輕量、免多養常駐服務,而且能直接對繁體中文子字串做全文檢索的本機解法。

用單檔 Python 與 SQLite,搭一個能搜中文的輕量索引

我的解法很簡單:用 Python 標準庫寫一個單檔搜尋工具 GGGODLIN/cc-session-index(以下簡稱 ccsi),底層直接掛在 SQLite 內建的 FTS5 全文搜尋引擎上,做到零第三方套件依賴。

這套設計有兩個關鍵點。首先,分詞器選用 SQLite 內建的 trigram。它不需要任何外部依賴或額外的語言模型套件,而是直接把文字切成連續 3 個字元的片段來建索引,原生支援繁體中文與英文的子字串檢索。其次,它用單一命令列介面同時掃描與收錄 Claude Code、Codex 與 Pi 三套 agent 的對話紀錄。索引內容只收錄使用者與 AI 的對話文字,以及每次工具呼叫的參數摘要,排除動輒幾萬行的 tool_result 工具原始輸出,並透過系統內建的定時排程(如 macOS launchd 或 cron)定時跑一下腳本做增量更新,平時完全不需要多養常駐背景服務。

在日常使用中,ccsi 提供了兩個子命令:

主力翻舊帳的是 ccsi search。當日常需要找回某個歷史問題的解法、當初放棄某個做法的具體原因,或是確認某項決策是在哪一個 session 拍板時,AI 可以直接用關鍵字找出當時的對話片段與 session ID。

另一個順手提供的是 ccsi events。它把三套 agent 散落的事件流正規化成統一的格式輸出,方便統計各工具與 hook 的呼叫紀錄,後文提到的兩千多筆實戰對帳也是靠它撈出來的。

架構立住了,但真正上線之後,我立刻迎來了第一個現實問題。

裝了工具不等於會用:從單月 15 次到兩個月 2,569 次

工具寫完並放進系統路徑的第一個月,我興致勃勃地等著 AI 主動使用它。但在還沒把使用規則寫進設定前,只有我偶爾在對話裡指名要它跑、或是少數個別腳本寫死呼叫時才會觸發,整個月下來只有 15 次。

這完全印證了我在《codebase 工具數據很強,到我 agent 手上剩不到一成》寫過的教訓:把一個 CLI 工具放進環境裡,AI 根本不會通靈。它的注意力永遠只停留在當下的 prompt、context 以及預設會碰的幾個檔案裡。如果你沒有明確告訴它什麼時候該用、怎麼用,這項工具在它眼裡就等於不存在。

為了解決這個問題,我在個人公開的環境配置 GGGODLIN/gggodlin-cc-harness 裡做了兩層接線:

第一層是寫入常駐的搜尋路由規則。我在全域規則檔案裡明確補上一條指示:只要涉及「過去是怎麼做的、上次為什麼放棄某做法、某個決策發生在哪個 session」的歷史檢索,統一使用 ccsi search,明定檢索時機與標準的 ccsi search 指令格式。

第二層是掛上 prompt 送出時的提醒 hook。光有靜態規則還不夠,因為當使用者口語化地拋出「我記得上次不是解過嗎」的時候,AI 很容易順著慣性先回話。因此我在 prompt 送出的關卡寫了一支攔截 hook,專門偵測使用者輸入是否包含「我記得、之前、上次、翻一下、查一下」這類指涉過去工作的詞彙。一旦命中,hook 就會在背景自動注入一段提醒:「使用者正在指涉過去的工作:回答前先翻紀錄,禁止憑印象回答。」

規則定義路線,hook 攔截慣性。這套雙層機制接上去之後,AI 的呼叫習慣徹底翻盤:在接下來的兩個月內,作為我日常主力入口的 Claude Code 主動跨三套 agent 歷史紀錄呼叫 ccsi 的次數從單月 15 次暴增到 2,569 次(連同首月共 2,584 次呼叫,跨越 429 個獨立 session)。它終於不再是躺在硬碟裡的玩具,而是真正變成了 AI 遇到歷史問題時的第一反射動作。

翻看 2,610 筆真實紀錄:AI 搜完之後都在幹嘛

呼叫量衝上去了,但 AI 到底有沒有查到東西?它搜完之後又去做了什麼?

把這 2,584 次呼叫(因部分單次指令串接了多筆查詢)拆開配對,共有 2,610 筆真實執行結果。在其中 2,360 筆可判讀輸出的 ccsi search 查詢裡,直接命中率達到了 68.2%(共 1,610 次,其餘約三成包含查無結果、早期標點語法報錯與管線截斷)。而在這 1,610 次成功命中的案例中,AI 拿到結果後的下一步動作可以清楚分成四種路徑:

有 45.9%(739 次)的場合,AI 看完搜尋工具吐回來的文字摘要,就已經掌握足夠的 context,能直接回答問題或結束查找。這代表接近半數的查詢在搜尋當下就完成了任務。

有 25.0%(403 次)的場合,AI 順著搜尋結果裡挖到的檔案線索,轉去呼叫其他工具讀取專案記憶檔或程式碼,把過去的解法接續套用到眼前的任務裡。

有 19.4%(313 次)的案例,AI 在命中之後,立刻又回頭去開原始的 .jsonl 對話檔。

最後還有 9.6%(155 次),是 AI 發現結果不夠精準,換個關鍵字再搜一次。

這裡需要持平地看那 19.4% 回頭開原檔的操作:有的是為了查閱未納入全文索引的 tool_result 工具原始輸出,有的是在同一個對話視窗內順手讀取專案裡其他 .jsonl 格式的紀錄檔,所以把文字摘要拉長並不能百分之百消滅回讀原檔。但這個數字依然揭露了一個重要訊號:現有的搜尋摘要給得不夠好用,確實造成不少「害 AI 多開一次大檔」的摩擦。

體檢索引與 278 筆零命中:修掉三個影響 AI 體感的搜尋盲區

除了命中後的動作,我也回頭體檢了資料庫內容,並拆解 278 筆單次查詢回傳查無結果的零命中案例。順著真實的使用現場,我抓出了三個嚴重影響 AI 搜尋體感的盲區,並一一動手修正:

第一個盲區是漏記工具的讀寫檔名。資料庫除了索引使用者與 AI 的對話文字,也會把每次工具呼叫存成一列,並標上 [Read]、[Edit]、[Skill] 這類工具標籤。但舊版在標籤後面只抓取了終端指令與 prompt 欄位,漏抽了讀寫工具傳入的具體檔案路徑與 skill 名稱,結果在資料庫裡生出高達 26.65%(多達 426,834 列)只有工具標籤卻沒有檔名的空殼紀錄;同時還誤把 skill 載入時展開的整份說明書收進索引,嚴重污染搜尋結果。修復時我一邊補抽檔案路徑與 skill 名稱並跳過無參數呼叫,讓空殼列從 426,834 列直接降到了 8 列,一邊把展開的 skill 說明書濾掉,排除大量雜訊。

第二個盲區是兩字中文詞的檢索死角。SQLite FTS5 的 trigram 分詞器是把文字切成連續 3 個字元來建索引。這意味著在 278 筆零命中裡佔了 25.2%(70 筆)的兩字中文詞查詢(像是「驗收」、「白話」這類日常詞),單獨丟進去搜尋必然回傳零筆;而如果把長詞與兩字詞混在一起搜尋,兩字詞更會被系統靜默吃掉,導致 AI 拿到一堆假陽性結果。修復時我針對單搜兩字短詞改為主動回傳明確錯誤,並在錯誤訊息與規則裡提示 AI 改用 3 個字以上的詞組、或把兩個兩字詞用雙引號包成片語再搜,別傻傻當成過去沒紀錄;長詞與兩字短詞混搜時,則讓 3 個字元以上的長詞先走 FTS5 索引縮小範圍,外層再自動補上 SQL LIKE 子字串比對把兩字短詞篩出來。

第三個盲區是回傳摘要太短與搜到當前對話。SQLite FTS5 擷取摘要的 snippet() 長度單位是 token 數:舊版設為 24 是按 24 個英文單字估算,但在 trigram 分詞下每滑動 1 個字元就算 1 個 token,換算下來每次只印出約 26 個中文字。AI 常常只瞄到半句話,根本抓不到 context 重點,只好被迫額外花費步驟去翻原始檔案。修正後我把長度調升至上限 64(約 66 個字),並新增了前後文展開參數 -C。同時,因為同一個對話只要聊超過 15 分鐘,前半段內容就會被定時排程收進索引,我也補上了 --exclude-session 選項來排除當前 session,避免 AI 搜到幾分鐘前才在同一個視窗聊過的話。

零命中裡也有一部分事後回推,是剛聊完還沒等到排程更新。把定時排程縮短到每 15 分鐘跑一次(日常增量更新一次約 0.6 至 6.0 秒,其中 0.6 秒是剛重建完的暖快取表現)能大幅縮小這個空窗,當然事後回推不代表縮短排程就能消滅所有這類未命中。

給 AI 用的工具,要走完三步閉環才算做完

這兩個月的實測走下來,給我的最大啟發不是寫出了一個搜尋器,而是看清了 AI 協作中「記憶」這件事的真實形狀。

記憶從來就不只有一種維度。經過深思熟慮、整理好的架構定案與不可碰觸的底線,適合寫進精簡的 MEMORY.md 當作日常的行為約束;而除錯過程中那些雜亂的嘗試、失敗原因與跨工具的踩坑紀錄,則應該完整封存在歷史對話裡,交給 ccsi 這類輕量的全文搜尋器在需要時隨時翻找。

而要讓一個工具真正為 AI 所用,通常必須走完三步閉環:

第一步是做出符合自身場景的極簡工具,不依賴龐雜架構,把最痛的點先解決掉; 第二步是在常駐規則裡寫明路由,並在關鍵觸發點掛上 hook 攔截習慣,逼 AI 把它當成預設反射動作; 第三步是回頭看真實的呼叫紀錄,從 AI 失敗或卡關的地方找出體感盲區,把工具打磨到多數時候不必再去大海撈針翻原始大檔。

這套做法目前只在我的工作流裡驗證跑通。但如果你也常常看著終端機發呆,心裡納悶「上次那個問題明明解過,AI 怎麼又在胡說八道」,那也許是時候幫它建一本隨時能翻的歷史舊帳了。