Skip to content
AI Ferryman Alex
Go back

代碼倉庫轉爲圖譜,floorplan好用麼?

那天我把 pytest 自己的倉庫交給幾個 agent 會話輪流去改。因爲我想看一看,像 floorplan 這種給倉庫留記錄的工具,到了晚上收工的時候,到底還能剩下多少賬可以對。四件事做完之後,它完整記住的只有一件。

一個人同時能盯住的 agent 本來就只有 6 到 7 個,因爲每個 agent 在改什麼和改到了哪一步,還有哪一個該停下來,全都得記在自己的腦子裏。所以等它們都改完之後,我常常已經說不清哪一處改動對應的是哪一個需求了。

也正是因爲這個,我纔想把記賬這件事交給工具去做,好讓自己晚上對賬的時候,能有一份現成的記錄可以直接查。

floorplan 能做的事

floorplan 是一個給代碼倉庫畫地圖的插件。這個插件裝在 Claude Code 或者 Codex 裏面工作。它能做的事情有這麼幾件。

  1. 它會給倉庫裏的每個文件都配一張卡片,並且在每張卡片上都寫明瞭這個文件的大白話名字和一句話說明,而且卡片之間還連着引用關係,所以順着卡片就能看出來哪些文件在互相調用。

  2. 它有一塊最近工作板,並且在這塊工作板上記着剛剛結束的那個會話改動了哪些卡片,而且每張被改動的卡片旁邊都寫明瞭爲什麼改,所以回頭看的時候就能知道這次改動是衝着哪個需求去的。

  3. 它還會給倉庫做體檢,也就是把代碼從頭掃一遍,專門找硬編碼寫在代碼裏的密鑰這一類問題。同時它也會把體積過大的文件先挑出來,再把它們列在一起提醒你注意。

floorplan 建出的地圖打開就是這個樣子:左邊是 pytest 倉庫的模塊圖譜,右邊是點中一個模塊之後的詳情面板,頂上還掛着體檢和最近工作的計數

廠方在 README 裏承諾過,說每次 Claude 或者 Codex 改完代碼之後,你都能確切地看到它動了哪些地方,而且還能看到它爲什麼要動那些地方。

我之所以拿 pytest 自己的倉庫來測它,就是因爲 pytest 是一個真的多子系統項目,其中配置、運行器和緩存這些部分都是實打實在用的代碼。所以我要是想知道一個工具到底能不能把這種倉庫的賬記清楚,就得先在 pytest 這樣的真項目上測過一遍,因爲只有測出來的結果纔算數。

測試當天的安排

那天的安排是這樣的:我一共開了 4 個會話,而且這幾個會話是一個接一個在同一個倉庫上工作的。也就是說,前一個會話結束之後,後一個會話纔開始。

因爲後面的會話事先並不知道前面的會話做過什麼,所以它們想了解前面發生了什麼,就只能去看倉庫本身的狀態,再加上 floorplan 留下來的那些記錄。幾個會話領到的任務各不相同,具體是下面這幾件。

  1. 第一個會話先建出了整張地圖,然後又給 pytest 加了一個新選項 —reverse-order。這個選項打開之後,收集到的測試會按跟原來相反的順序運行,所以藏在執行順序裏的問題就能被翻出來。選項的註冊和順序翻轉寫在 src/_pytest/main.py 裏,而證明順序確實反過來了的測試,則寫在 testing/test_collection.py 裏。

  2. 第二個會話接到的是一份故障報告,說 —lf 找不到先前的失敗記錄,害得每次都把全套重跑一遍。它先在地圖裏搜了 lastfailed,然後就直接落到了 src/_pytest/cacheprovider.py 上,接着順着失敗的測試往回查,最後查出來毛病只出在一個詞上。

這個詞是 cacheprovider.py 裏管 —lf 的那個類 LFPlugin 在處理測試報告時要看的第一個分支條件裏的詞,而且它原本是 passed,後來被改成了 failed。這個條件看的是測試報告裏記下來的結果,也就是這個測試到底是通過了還是失敗了。它決定一個測試跑完之後,是清掉它舊的失敗記錄,還是把它的新失敗寫進 lastfailed。

第一個分支負責清掉舊記錄,而它要等條件成立了纔會走。原來的詞是 passed,所以報告說這個測試通過了,條件就成立,它就走這個分支。如果報告說的是失敗,條件不成立,就不走清記錄的分支,所以失敗就會被寫進 lastfailed。

詞被改成 failed 之後,這個條件看的就成了失敗:報告說這個測試失敗了,條件反而成立。所以一個測試真的失敗時,它會先走清掉舊記錄的那個分支。因爲它先走了清記錄的分支,所以這次失敗就沒有被寫進去,所以 lastfailed 裏的記錄只會減少,不會增加,所以 —lf 後來就一直找不到先前的失敗。

第二個會話只改了一行,就把 failed 改回了 passed,所以失敗又能被正常記錄了,而且 —lf 也跟着恢復了正常。

  1. 第三個會話乾的是一次重構,具體要動的是 src/_pytest/terminal.py,因爲這個文件裏有一段循環,負責把測試結果彙總成一行統計。它把那段循環抽出來,再放進一個新的私有方法裏,而原來的彙總過程則改成調用這個新方法。因爲這次只搬了代碼的位置,並沒有改變任何行爲,所以它在改之前和改之後各跑了一遍同一批測試,而兩次結果完全一樣,這才確認原來的行爲沒有變。

  2. 第四個會話給斷言截斷模塊補測試,因爲 src/_pytest/assertion/truncate.py 裏有些邊界以前沒被測到,所以它新建了 testing/test_assertion_truncate.py 來測它們。它還發現,truncate.py 有個毛病:如果拿空列表去調用裏面的一個私有截斷函數,它就會當場報錯。因爲正常路徑走不到這裏,所以那個會話只記下這個毛病,沒有順手改掉它。

就這樣,4 件事一件接着一件都做完了。而且每一件做完之後,會話都按 floorplan 的流程更新了地圖,並且把自己這一輪的記錄寫進了前面說的那塊最近工作板。

收工之後的對賬

等到收工的時候,我另外開了一個全新的會話來當審計員,專門負責對賬。因爲這個審計員是我新開的一個 agent,而且白天的任何一個會話它都沒有參加過,所以它對白天發生過什麼一無所知。也正因爲這樣,我只讓它看 floorplan 留下來的記錄,同時明確不許它去翻代碼的歷史,也不許它去看別的東西。

因爲它手裏只有工具的記錄,所以它能不能把白天的賬對清楚,就正好能說明這份記錄本身到底夠不夠用。結果它逐件去還原,最後能完整找回來的只有 1 件,也就是白天最後做的那件補測試的事。

對賬時打開最近工作板,板上只剩下一張卡片,就是白天最後那個會話補的截斷測試,前面三個會話的任務一張都不在

加新功能的那件事留下來的記錄只剩下了半份。這半份就是測試文件那張卡片上的一句提到反序運行的話,再加上符號表裏 TestReverseOrder 這個名字。符號表是 floorplan 給整個倉庫列的清單,專門記各文件裏定義了哪些類。因爲實現這個功能的那個文件,也就是 src/_pytest/main.py,它的卡片上從頭到尾就沒有提過這個新選項,而且卡片上的說明還是原來那一套講會話怎麼跑的內容。

src/_pytest/main.py 的卡片從頭到尾沒有提過那天新加的 --reverse-order 選項,卡片上的說明還是原來那套講會話怎麼跑的內容

與此同時,測試文件的那張卡片上倒是留下了一句提到反序運行的話,所以審計員只能從這一句話看出來,好像有人加過這麼一個功能。可是它既看不到當時的任務標題,也看不到我當時提出的需求原話,更看不出這件事到底是那天才做的,還是早就存在於倉庫裏的。

testing/test_collection.py 的卡片上倒是留着一句提到反序運行的話,反序這件事的記錄落在了測試文件的卡片上,而不是實現它的文件上

修故障和做重構的那兩件事則沒有留下任何痕跡,因爲它們既沒有留下任務記錄,也沒有任何記錄能讓人把它們找回來。之所以會這樣,是因爲最近工作板在每次更新的時候,會把上一個會話寫下的那份記錄整份換掉,所以新記錄一落盤,舊記錄也就跟着消失了。

floorplan 自帶的技能說明裏就明明白白地寫着,每次更新最近工作板的時候,都會把上一個會話留下的那份記錄替換掉。也就是說,它從設計上就是隻記最近的一次。

廠方承諾的那種回看,放在一個剛剛結束的會話身上是成立的,因爲那個會話動過哪裏又爲什麼動,全都寫在最近工作板上。可是放到一整天身上就不成立了,因爲前面的記錄已經被後面的替換掉了。

所以到了晚上,當我想弄清楚哪一處改動對應的是哪一個需求的時候,我手裏能用的就只剩下最後那個會話的記錄了。白天前面做過的那些事,我還是得靠自己的腦子去回想,或者另外去翻別的記錄。可惜,按照 floorplan 的設計,它無法完成我的這個需求。

它做得好的地方

  1. 地圖這一塊的覆蓋是全的,因爲那天它一共覆蓋了 284 個文件,而且一個文件都沒有漏掉,所以後面幾個會話想找東西的時候,至少不用擔心地圖本身缺了一角。

  2. 定位這件事它也確實幫上了忙,因爲修故障的那個會話在地圖裏搜了 lastfailed 之後,就直接落到了正確的文件上,而且它中間也沒有在別的文件上浪費時間,所以後面的排查才接得上。

  3. 它給單個會話寫的那份記錄本身寫得很細,因爲裏面有這次任務的標題,也有我當時提出需求的原話。而且它每改動一張卡片,旁邊都有一條說明,而說明裏講清楚了這次爲什麼要改這張卡片。所以只看這一份記錄,就能把剛結束的這個會話交代明白。

  4. 體檢報出來的那條高危問題也是真的,因爲 codecov.yml 第 5 行裏確實躺着一串 token,而且這串 token 本身就是一個完整的 UUID,所以它不像是隨手敲上去的佔位內容。

  5. 體檢同時報出來的超大文件也沒有冤枉誰,因爲它一共點名了 15 個文件,而我後來逐個覈對下來發現,這些文件確實都很大,所以這一類提醒同樣是可以直接拿來用的。

體檢面板一共報了兩件事:codecov.yml 裏寫着一串像密鑰的 token,還有 15 個文件體積過大,這兩條都逐個覈對過是真的

實測裏對不上的地方

除了記錄會被替換之外,4 個會話先後在同一個倉庫上工作時,前後一共觸發了 4 次地圖更新。而這 4 次更新報出來的變化,也全都是文件和類這一級的變化,因爲它記錄的粒度只到文件和類這一級。至於方法這一級改了什麼,它根本就不往記錄裏寫。

所以重構那次明明只動了 1 個文件,可是新抽出來的那個方法的名字,在它全部的記錄裏一共出現了 0 次,等於這次改動在記錄這一側根本就不存在。

地圖裏還憑空多出來了 3 條路由,可是 pytest 壓根就沒有服務器,所以它本來就不可能有什麼路由。這幾條假路由之所以會出現,是因爲那個負責把倉庫裏的文件逐個讀過去再建出地圖的掃描器,把寫在字符串裏的示例代碼中的 mock.patch 裝飾器當真了。

這種裝飾器在示例代碼裏寫出來是這個樣子:@mock.patch(“os.path.abspath”)。其中 patch 是這個裝飾器名字的一部分,括號裏那串帶點的字符,是它要替換的那個函數的名字寫成的字符串,也就是 os.path.abspath 這個函數名。

掃描器把這一行當成了一條路由聲明:它把 patch 讀成了 HTTP 方法 PATCH,又把括號裏那串字符讀成了網址的路徑,方法和路徑湊齊,一行就拼成了一條路由。地圖上那幾條路由,條條都是它這樣拼出來的,所以從頭到尾都是誤讀出來的。

類似的誤讀還出現在依賴關係上,因爲地圖把 warnings 和 pathlib 標成了倉庫內部的依賴,可是它們兩個其實都是 Python 自帶的標準庫,也和 pytest 自己寫的模塊沒有關係。所以順着地圖去理解這個倉庫的依賴關係時,在這種地方就得自己多留一個心眼。

行數這一塊它也數得和真實情況對不上,因爲它只數那些非空的行,所以倉庫裏行數最多的那個文件真實有 6326 行,可是在地圖上卻只寫了 5457 行。如果有人拿着地圖上的行數去判斷一個文件到底有多大,那麼他得出來的印象,就會比真實情況小上一截。

體檢這一塊也有它結構上就看不見的東西,因爲它只做文本匹配,也就是拿代碼裏的文字去和一組事先定好的匹配樣式逐字比對,其中就有專門找密鑰字符串的樣式,而且它從來不真的去運行代碼,所以邏輯上寫錯了的地方,它是發現不了的。

那個把 —lf 弄壞的錯詞,從這次實測開始之前就一直待在 cacheprovider.py 裏,直到那天才被修好。而在它待着的這段時間裏,體檢對這個文件始終沒有給出過任何提示。再加上後來修好它的那次改動也沒有在記錄裏留下痕跡,所以這一壞一修兩件事,在 floorplan 那邊就等於從來沒有發生過。

它適合誰用

所以如果你平時一次只跑 1 個 agent,那麼你就在每個會話一結束的時候,馬上回頭去檢查它剛剛改過的東西。在這種用法下,floorplan 的地圖加上那份剛寫好的記錄,對你來說就已經夠用了,因爲你要對的賬正好就是它還記着的那一份。

可是如果你和我一樣,也是一次放出好幾個 agent 去做事,而且要等到晚上才坐下來統一對賬,那麼 floorplan 就回答不了你最想問的那個問題。而那個問題就是,哪一處改動到底對應着哪一個需求。

因爲它的記錄只留最後一次,所以你還是得另外準備一份能把賬記全的東西。比如用 git 來記。每一處改動都單獨提交一次,並且把改動的原因順手寫進提交說明裏,這樣到了晚上,你順着提交記錄就能把賬一筆一筆對回來。

要是你不想把這件事全交給 git,那麼你也可以自己隨手記一份流水,也就是把每個需求和它引起的改動對應着寫下來,這樣至少不會到了晚上全靠回憶。

你平時讓幾個 agent 一起工作之後,到了晚上都是怎麼對賬的呢?是靠工具留下的記錄,還是和我一樣,最後還是得回到 git 的提交記錄裏一筆一筆地翻?

如果這篇實測對你有幫助,歡迎訂閱這個博客,或者先收藏起來。我後面還會繼續分享 AI、安全和開源方面的實測和乾貨,到時候咱們一起把這些工具先測明白了再用。

來源:floorplan 項目倉庫,https://github.com/haydenschmitty/floorplan


在 X 上討論→

評論都在 X 上——回覆、引用和討論都在那邊進行。