昨天說,前 20 天講我日常在用的工具。今天第一個遇到的,就是**「陌生專案」**。
而今天只回答一件事:陌生專案該從哪三個檔開始看。
情境很具體:你 clone 了一個從沒碰過的 repo,444 個 Swift 檔攤在那裡。你要在動手改之前先知道它長什麼樣子。
一般的做法是打開 README、翻 Sources/、找啟動點或是隨便點幾個檔進去看。問題是,這個做法沒有終點。你不知道自己到底理解了多少,也不知道漏掉的,是不是最重要的那個檔案。
知識圖解決的,不是「把知識存起來」,而是規劃閱讀路徑:先算出哪幾個節點連得最多、程式被分成幾群、群跟群之間靠誰接起來,然後先讀那幾個檔。
今天要走完這條路:從 clone 一個沒看過的 repo,到最後說得出「先讀這三個」。中間會經過一次撞牆 —— 因為第一個直覺的做法,會把你送進別人的測試套件裡。
今天的實驗對象是 apple/container —— Apple 那套 macOS 容器工具的 CLI 端,commit d6de569。
⚠️ 注意,它會更動你哪些檔案
graphify claude install會寫進當前目錄的CLAUDE.md,並在.claude/settings.json註冊 PreToolUse hook。在自己的專案跑之前,先確認 git 是乾淨的 —— 跑完git diff一眼就看得出來它加了什麼。
在一個乾淨的目錄跑:
git clone https://github.com/apple/container.git
cd container
git checkout d6de5694 # 釘住版本,不然你跟我的數字對不上
uv tool install graphifyy # PyPI 套件名是 graphifyy,指令是 graphify
graphify claude install # 把 skill 裝進 Claude Code
然後在 Claude Code 裡下 /graphify .。
這裡有個坑值得先講:graphify 這支 CLI 本身不負責掃描。graphify --help 列出來的是 install / query / path / explain / diagnose —— 掃描是 skill 在 agent 裡跑的多段流程,不是一句 shell 指令。我第一次寫這段時,順手寫成 graphify .。貼回終端機才發現那個指令不存在。
實際跑的時候,我拆成三段來量時間:
| 階段 | 做什麼 | 耗時 |
|---|---|---|
| 偵測 | 走一遍檔案樹,分類出 code / 文件 / 圖片 | 0.43 秒 |
| 結構抽取 | 457 個 code 檔跑 AST,14 個 worker 平行 | 2.41 秒 |
| 建圖 + 分群 | Leiden 分群、算內聚度、找 god node | 1.39 秒 |
| 總和 | 4.23 秒 |

四秒出頭,你就有一張 5,992 個節點、13,881 條邊、330 個群集左右的圖。
「左右」兩個字是認真的,等一下會回來講為什麼。
這四秒沒有花任何 token。
這件事值得單獨講,因為它跟大部分「用 AI 讀 codebase」的做法不一樣。AST 解析是純程式:tree-sitter 走一遍語法樹,誰呼叫誰、誰繼承誰,都由程式碼的語法結構決定,不需要問任何模型。
「0 token」不是推論的,而是結果:
$ cat graphify-out/.graphify_semantic.json
{"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 0, "output_tokens": 0}
節點零、邊零、token 零 —— 5,992 個節點,一個都不是模型生出來的。
對照一下規模:這張圖之後查一次約 29k tokens,而把等量原始碼直接塞給模型約 403k(這兩個數字是 graphify 自己估的,出自另一輪掃 HEAD 9a8917ca 的收尾輸出;兩版規模差 1%,量級不變)。入場費是零,使用費可以降一個量級 —— 這才是為什麼值得先花四秒建圖,而不是每次都把整個 repo 貼給它。
順帶一句:AST 抽出來的是 15,223 條邊,建圖時併掉了 1,342 條相同端點的重複引用(BuildCommand → String 出現 17 次算一條),所以 13,881 是「不同的關係」的數量,不是「引用次數」。明天會回頭用到這一點。
第三行的 checkout 不是裝飾。 二十天後我拿新 clone 的 HEAD 重跑:502 個檔變成 512、節點從 5,992 變成 6,052。沒釘 commit 的重現步驟,不叫重現步驟。
**第五行寫出來的 CLAUDE.md,下次重掃會被算成多一個 document 檔。**開頭的警告講的是它會動你的檔;這裡多一層 —— 工具把自己的產物,又餵回自己的輸入,我就是這樣多出第 43 個 md 檔的。
超過 500 檔,它會停下來問範圍,而預設答案會讓你拿到另一種圖。選項一是「Whole repo(Recommended)」,但畫面上同時寫著 42 份文件 + 3 張圖會送進 LLM。我要的是純 AST:讀碼路徑要的是程式的結構,不是 README 講了什麼;而且 AST 是決定性的,跑一百次結果一樣,LLM 不是。**結果會變的東西,不能拿來當對照的基準。**所以選「Type something」,打:
Whole repo, but structural (AST) extraction only — skip semantic extraction.
跑完它會回一句 0 tokens spent (AST-only, as you asked — no LLM call was made)。這行就是收據。 沒有它,你不知道自己拿到的是哪一種圖。
之前我掃過它的姊妹庫 apple/containerization(那是底層 library,這次是 CLI)。對照組是同一支指令、同一版工具跑出來的:

407 個檔、4.49 秒,同樣是零 token。兩份數字擺在一起,第一眼是「怎麼這麼像」:
container |
containerization |
|
|---|---|---|
| commit | d6de569 |
74ace14 |
| 節點 | 5,992 | 6,182 |
| 邊 | 13,881 | 15,855 |
| 群集 | 329–335 | 315–316 |
可對回 Package.swift 的 target |
79.9% | 77.2% |
| target 數 | 35 | 29 |
規模、密度、分群數,三項都在同一個量級。其實不意外。同一個團隊、同一套 Swift 慣例,圖的形狀自然會有相似之處。
但有一格差很多:
container |
containerization |
|
|---|---|---|
| 群集加權平均純度 | 83.7–83.9% | 87.0–87.3% |
| 邊留在同一個 target 內 | 61.3% | 71.7% |
| 跨 target 的邊 | 38.7% | 28.3% |
同樣是 Apple 出品,CLI 那份有 38.7% 的邊是跨模組的,底層 library 只有 28.3%。
這個數字要怎麼讀?它不是「CLI 寫得比較差」。 CLI 本來就是黏合層。它的工作就是把底層的東西接起來給人用,所以跨模組是它的本分。不過把跨模組的邊攤到檔案上,前兩名是兩個測試檔(548、456 條)—— 測試本來就要 import 被測的東西;扣掉測試,真正的跨模組樞紐是 FileManager+AllocatedSize.swift(240 條)和 Parser.swift(186 條)。所以 61.3% 對 71.7% 的差距,有一部分是測試組織方式的差別,不全是產品程式碼的耦合。但這正是圖有用的地方:**你還沒讀任何一行 code,就已經知道這個 repo 的耦合長在哪一層。**接下來要讀的是那 38.7% 的邊穿過哪幾個節點,不是隨便點 Sources/ 裡的檔案。
上個月那次我挖到一項度量:看連接度會誤判,要看 method 邊的佔比。
同樣叫「連接度高」,病因可以完全相反:
method 佔比低 = 很多人用我。這是 API 入口該有的樣子,健康。method 佔比高 = 我自己扛太多。那才是 god node。這次換一個 repo,同一套方法又立刻抓到東西:
| repo | 節點 | 總邊 | 其中 method |
佔比 |
|---|---|---|---|---|
containerization |
Sendable |
500 | 0 | 0.0% |
container |
ContainerizationError |
447 | 0 | 0.0% |
container |
Foundation |
314 | 0 | 0.0% |
container |
ParserTest |
137 | 136 | 99.3% |
前面三個都是 0% ——很多人用它們,正常。
ParserTest 是唯一一個破 90% 的:**137 條邊裡 136 條是它自己的方法。**一個檔案裡塞了一百多個測試方法,而它在圖上長得跟 god node 一模一樣。但它是測試,不是正式程式碼。
先前在 containerization 上,唯一破百的自帶方法數也出現在測試。兩個 repo,同一種形狀 —— Apple 的正式程式碼守得很好,反而是測試比較鬆。
到這裡,一張 5,992 個節點、13,881 條邊、330 個群集左右的圖就在硬碟上了。它還附了一份互動網頁,graphify-out/graph.html,打開長這樣:

這是知識圖最常見的死法 —— 圖產出來很漂亮,截圖發個限動,然後就沒有然後了。圖如果不能收斂成「接下來打開哪個檔」,它就只是一張很貴的桌布。
而這張桌布還有一個更具體的問題:放大之後,每一個節點都叫「Community N」。

Community 135、Community 7、Community 306…332 個,沒有一個檔名、沒有一個型別名。點下去,右邊那塊 NODE INFO 也只會告訴你那是第幾號群集。
⚠️ 這不是工具的缺陷,是它的預設保護。 超過 5,000 個節點時它會自動聚合成群集視圖 —— 5,992 個點畫在同一張畫布上,只會糊成一團,那才是真的沒用。想看符號層級要下
graphify export html --obsidian,代價是一個節點一個檔,5,992 個 md。今天用不到。
所以圖不是拿來看的,是拿來算的。今天只做一件事:從那張圖走到一份具體的閱讀清單,而且要能驗收。
最直覺的做法是把節點的度數加總到檔案層級,誰的總度數高就先讀誰。跑出來是這樣:
| 排名 | 總度數 | 節點數 | 檔案 |
|---|---|---|---|
| 1 | 751 | 99 | Tests/TerminalProgressTests/ProgressBarTests.swift |
| 2 | 506 | 77 | Sources/Services/RuntimeLinux/Server/RuntimeService.swift |
| 3 | 498 | 20 | Tests/ContainerResourceTests/PublishSocketTests.swift |
| 4 | 459 | 138 | Tests/ContainerAPIClientTests/ParserTest.swift |
| 5 | 454 | 69 | Tests/ContainerPersistenceTests/ConfigSnapshotDecoderTests.swift |
| 6 | 398 | 96 | Sources/ContainerBuild/Builder.pb.swift |
前五名有四個是測試檔。
⚠️ 先解釋一個看起來矛盾的地方:前面那個
ParserTest是 137 條邊,這裡卻是 459。兩個都對 —— 只是它們量的不是同一個東西。前面量的是ParserTest這一個節點;這張表是整個檔案,把裡面 138 個節點的度數全部加起來。同一個檔,兩種尺度,而排行榜要用哪一種,本身就是一個決定。
如果我照這張表讀下去,一個下午會花在讀別人的測試 —— 測試當然有價值,但那不是「搞懂這個專案在做什麼」該先看的東西。
而且第六名 Builder.pb.swift 是另一種陷阱:.pb.swift 是 protobuf 產生出來的。它度數高是因為它機械地宣告了一大堆型別,讀它,等於讀一份自動產生的樣板。
圖沒有錯,是我問錯問題了。 度數量的是「連得多」,不是「值得讀」。
規則很簡單,兩條:
Tests/ 底下的——測試反映的是驗證方式,不是設計本身.pb.swift、+Generated、Resources/ 之類)篩完剩下這三個:
| 總度數 | 節點數 | 檔案 | 它是什麼 |
|---|---|---|---|
| 506 | 77 | Sources/Services/RuntimeLinux/Server/RuntimeService.swift |
Linux runtime 的服務端 |
| 398 | 41 | Sources/Services/ContainerAPIService/Client/Parser.swift |
API 回應的解析 |
| 357 | 44 | .../ContainerAPIService/Server/Containers/ContainersService.swift |
容器操作的服務端 |
**這就是今天的答案:先讀這三個檔。**一個 runtime 服務端、一個 API 服務端、一個把兩邊接起來的 parser —— 三個檔看完,這個專案的骨幹,大概就在腦子裡了。
今天從頭到尾沒有讓 AI 介入:graphify 純靠程式走一遍語法樹,四秒、0 token,一個從沒看過的 repo 就變成一張 5,992 個節點的圖。但圖本身不會告訴你該讀哪裡 —— 第一次按連接度排,前六名有五個是測試檔和產生碼;加上兩道過濾器之後,答案才收斂成三個檔:RuntimeService.swift / Parser.swift / ContainersService.swift。
這是我現在碰到陌生專案的第一步。它取代的不是 README —— 專案有 README 還是先讀完,那是作者親口說的東西;它取代的是「隨便點幾個檔進去看」那種沒有終點的讀法。
ParserTest:137 邊、99.3% 是自己的方法,唯一一個真的 god 形狀,而它是測試RuntimeService.swift / Parser.swift / ContainersService.swift
這一篇留下的心法:
**圖給你候選,不給你答案。**看連接度會誤判,要看
method邊的佔比 —— 低佔比是「很多人用我」,高佔比才是「我自己扛太多」。而就算換了這個指標,度數量的仍然是「連得多」,不是「值得讀」—— 測試檔和產生碼永遠會排在前面,因為它們機械地連了一堆東西。一張沒有過濾器的排行榜,會很有自信地把你送進別人的測試套件裡。
明天:換 AI 來讀同一個 repo,不給圖、也不讓它讀 Package.swift,讓它寫一份架構文件。我逐句對賬它有沒有編,再看它挑的入口檔,跟今天這三個對不對得上。
apple/container 的 d6de5694(2026-08-20),不是撰稿當下的 HEAD;對照組 containerization 是 74ace148(2026-07-27)。兩份都用 graphifyy 0.9.22 重新建圖後才拿來比 —— 版本不同的工具產的兩張圖不能擺在一起。apple/container,commit d6de5694200468d99a61662bfb9bb3aba763e3e5:github.com/apple/container
apple/containerization(對照組),commit 74ace148ded72f7bb3c878b142e4962ae668adf4:github.com/apple/containerization
graphifyy,MIT),本文所有數字都出自 0.9.22:github.com/Graphify-Labs/graphify 安裝與呼叫:uv tool install graphifyy → graphify claude install → 在 agent 裡下 /graphify <path>
graph-metrics.py,輸入是 graphify-out/graph.json:github.com/n913239/Medium/tree/main/tools/graph-metrics README 寫明了歸屬規則(節點的 source_file 路徑中若出現 Sources/ 或 Tests/,取其後一段當 target,其餘視為無法歸屬)與純度的分母(群集內可歸屬的節點數),你可以拿去打自己的 repocontainerization 的完整記錄;對照組那欄的原始數字出自這一篇