<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Claude Code on Tarragon</title><link>https://tarrragon.github.io/blog/tags/claude-code/</link><description>Recent content in Claude Code on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 25 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/claude-code/index.xml" rel="self" type="application/rss+xml"/><item><title>codebase-memory-mcp：155 語言 tree-sitter 知識圖譜 MCP 的能力與邊界</title><link>https://tarrragon.github.io/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/</guid><description>&lt;h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題&lt;/h2>
&lt;p>codebase-memory-mcp（下稱 cbm）的核心定位是「&lt;strong>把整個 codebase 預先解析成可被 LLM 廉價查詢的知識圖譜&lt;/strong>」。它要替代的是 agent 在不熟悉的 codebase 上「拿 grep / glob / read 連環翻檔」的探索 pattern——人類用 IDE 編輯、agent 用 cbm 探索、兩者服務不同的工作流。&lt;/p>
&lt;p>設計上跟其他「graph + LLM」工具的關鍵分野，在於它&lt;strong>不內嵌任何 LLM 做自然語言 → 查詢轉換&lt;/strong>：&lt;/p>
&lt;blockquote>
&lt;p>Other code graph tools embed an LLM for natural language → graph query translation. This means extra API keys, extra cost, and another model to configure. With MCP, the agent you&amp;rsquo;re already talking to &lt;em>is&lt;/em> the query translator.&lt;/p>&lt;/blockquote>
&lt;p>所以 cbm 自己只是個提供高品質 graph 查詢 API 的 server，「翻譯自然語言」這件事直接讓呼叫端的 agent 做。這個取捨對 Claude Code 這類 host 是理想的，因為 host 端已經有一顆夠強的模型在跑。&lt;/p>
&lt;h2 id="部署形態決定它的甜蜜點">部署形態決定它的甜蜜點&lt;/h2>
&lt;p>cbm 是&lt;strong>單一靜態 binary&lt;/strong>，所有依賴（155 種 tree-sitter grammar、SQLite、tokenizer）都在 binary 內，安裝後沒有外部 runtime 依賴。&lt;/p>
&lt;p>這個取捨直接影響它的甜蜜點：&lt;/p>
&lt;ul>
&lt;li>跨平台分發成本低，CI 上跑也方便&lt;/li>
&lt;li>不需要為個別語言裝 toolchain（不像 LSP 路線要對應 language server）&lt;/li>
&lt;li>但代價是「能力上限」被 binary 內附的 grammar 跟自寫的 type resolution 算法綁住，無法靠 IDE 生態的成熟度借力&lt;/li>
&lt;/ul>
&lt;p>知道這個取捨之後，後面所有能力差異都解釋得通：能做的事多半是「靜態可推導」的，需要 query 外部 toolchain（如 IDE language server）的場景多半要靠別的工具補。&lt;/p>
&lt;h2 id="索引架構多-pass--ram-first">索引架構：多 pass + RAM-first&lt;/h2>
&lt;p>cbm 的索引流程是 &lt;strong>RAM-first 的多 pass pipeline&lt;/strong>，pass 之間有明確的責任分工：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Pass&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>抽出的 edge / node（為主）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>structure&lt;/td>
 &lt;td>tree-sitter 解 AST，建初始 node&lt;/td>
 &lt;td>Project / Package / Folder / File / Module&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>definitions&lt;/td>
 &lt;td>抽函式 / 類別 / 介面 / 型別定義&lt;/td>
 &lt;td>Class / Function / Method / Interface / Enum / Type&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>calls&lt;/td>
 &lt;td>解析 function call、結合 import 與型別&lt;/td>
 &lt;td>CALLS / ASYNC_CALLS / USAGE / USES_TYPE / IMPLEMENTS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HTTP links&lt;/td>
 &lt;td>偵測 REST / gRPC / GraphQL route&lt;/td>
 &lt;td>Route、HTTP_CALLS、HANDLES&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>configuration&lt;/td>
 &lt;td>掃 Docker / Kubernetes / Kustomize&lt;/td>
 &lt;td>Resource、CONFIGURES、WRITES&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>tests&lt;/td>
 &lt;td>偵測測試函式與被測對象關係&lt;/td>
 &lt;td>TESTS、FILE_CHANGES_WITH&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>執行期用 LZ4 壓縮的記憶體 SQLite 加速，所有 pass 跑完一次性 dump 成持久化 DB（路徑 &lt;code>~/.cache/codebase-memory-mcp/&lt;/code>，WAL mode）。team 共享情境下可加跑 zstd 壓縮（best tier 用 &lt;code>zstd -9&lt;/code> + index strip、fast tier 用 &lt;code>zstd -3&lt;/code> 走 watcher 增量），匯出成 &lt;code>.codebase-memory/graph.db.zst&lt;/code> artifact 給 CI / 隊友共用。&lt;/p></description><content:encoded><![CDATA[<h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題</h2>
<p>codebase-memory-mcp（下稱 cbm）的核心定位是「<strong>把整個 codebase 預先解析成可被 LLM 廉價查詢的知識圖譜</strong>」。它要替代的是 agent 在不熟悉的 codebase 上「拿 grep / glob / read 連環翻檔」的探索 pattern——人類用 IDE 編輯、agent 用 cbm 探索、兩者服務不同的工作流。</p>
<p>設計上跟其他「graph + LLM」工具的關鍵分野，在於它<strong>不內嵌任何 LLM 做自然語言 → 查詢轉換</strong>：</p>
<blockquote>
<p>Other code graph tools embed an LLM for natural language → graph query translation. This means extra API keys, extra cost, and another model to configure. With MCP, the agent you&rsquo;re already talking to <em>is</em> the query translator.</p></blockquote>
<p>所以 cbm 自己只是個提供高品質 graph 查詢 API 的 server，「翻譯自然語言」這件事直接讓呼叫端的 agent 做。這個取捨對 Claude Code 這類 host 是理想的，因為 host 端已經有一顆夠強的模型在跑。</p>
<h2 id="部署形態決定它的甜蜜點">部署形態決定它的甜蜜點</h2>
<p>cbm 是<strong>單一靜態 binary</strong>，所有依賴（155 種 tree-sitter grammar、SQLite、tokenizer）都在 binary 內，安裝後沒有外部 runtime 依賴。</p>
<p>這個取捨直接影響它的甜蜜點：</p>
<ul>
<li>跨平台分發成本低，CI 上跑也方便</li>
<li>不需要為個別語言裝 toolchain（不像 LSP 路線要對應 language server）</li>
<li>但代價是「能力上限」被 binary 內附的 grammar 跟自寫的 type resolution 算法綁住，無法靠 IDE 生態的成熟度借力</li>
</ul>
<p>知道這個取捨之後，後面所有能力差異都解釋得通：能做的事多半是「靜態可推導」的，需要 query 外部 toolchain（如 IDE language server）的場景多半要靠別的工具補。</p>
<h2 id="索引架構多-pass--ram-first">索引架構：多 pass + RAM-first</h2>
<p>cbm 的索引流程是 <strong>RAM-first 的多 pass pipeline</strong>，pass 之間有明確的責任分工：</p>
<table>
  <thead>
      <tr>
          <th>Pass</th>
          <th>責任</th>
          <th>抽出的 edge / node（為主）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>structure</td>
          <td>tree-sitter 解 AST，建初始 node</td>
          <td>Project / Package / Folder / File / Module</td>
      </tr>
      <tr>
          <td>definitions</td>
          <td>抽函式 / 類別 / 介面 / 型別定義</td>
          <td>Class / Function / Method / Interface / Enum / Type</td>
      </tr>
      <tr>
          <td>calls</td>
          <td>解析 function call、結合 import 與型別</td>
          <td>CALLS / ASYNC_CALLS / USAGE / USES_TYPE / IMPLEMENTS</td>
      </tr>
      <tr>
          <td>HTTP links</td>
          <td>偵測 REST / gRPC / GraphQL route</td>
          <td>Route、HTTP_CALLS、HANDLES</td>
      </tr>
      <tr>
          <td>configuration</td>
          <td>掃 Docker / Kubernetes / Kustomize</td>
          <td>Resource、CONFIGURES、WRITES</td>
      </tr>
      <tr>
          <td>tests</td>
          <td>偵測測試函式與被測對象關係</td>
          <td>TESTS、FILE_CHANGES_WITH</td>
      </tr>
  </tbody>
</table>
<p>執行期用 LZ4 壓縮的記憶體 SQLite 加速，所有 pass 跑完一次性 dump 成持久化 DB（路徑 <code>~/.cache/codebase-memory-mcp/</code>，WAL mode）。team 共享情境下可加跑 zstd 壓縮（best tier 用 <code>zstd -9</code> + index strip、fast tier 用 <code>zstd -3</code> 走 watcher 增量），匯出成 <code>.codebase-memory/graph.db.zst</code> artifact 給 CI / 隊友共用。</p>
<p>Pass 排序遵循明確的依賴關係：calls 一定在 definitions 之後（因為 call edge 要連到已被建出來的 function / method node）、HTTP links 一定在 calls 之後（需要先有 call edge 才能比對 route 跟 handler）、configuration / tests 是 cross-cutting 的最終層（前面的結構與 call graph 都齊備、它們才能掛上 CONFIGURES / TESTS edge）。實務影響：HTTP links pass 在「單 service repo」上等於 no-op、configuration pass 在「缺 IaC manifest」的 repo 上也是 no-op、這兩個 pass 的價值高度依賴 repo 結構。</p>
<p>這個架構的副作用是：<strong>單次完整 index 速度快</strong>（README 聲稱 Linux kernel 3 分鐘），但<strong>增量更新採背景 git polling</strong>（IDE-style file watcher 是即時觸發、cbm 是定期掃描），對「邊改邊查」的工作流會有秒級延遲。</p>
<h2 id="11-signal-語意搜尋cbm-最強的差異化">11-signal 語意搜尋：cbm 最強的差異化</h2>
<p>如果只看 README 寫的「BM25 全文搜尋」，會嚴重低估 cbm 的搜尋能力。實際上 <code>search_graph</code> 的 ranking 是 <strong>11 個 signal 的加權組合</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>TF-IDF</td>
          <td>詞頻 / 逆文檔頻率，傳統文字相關性</td>
      </tr>
      <tr>
          <td>RRI</td>
          <td>Reverse rank importance，符號在 graph 中的重要性</td>
      </tr>
      <tr>
          <td>API / Type / Decorator signature</td>
          <td>函式簽章、型別標註、decorator 是高權重訊號</td>
      </tr>
      <tr>
          <td>AST profile</td>
          <td>AST 結構相似性</td>
      </tr>
      <tr>
          <td>Data flow</td>
          <td>變數與參數依賴鏈</td>
      </tr>
      <tr>
          <td>Halstead-lite</td>
          <td>簡化的程式複雜度指標</td>
      </tr>
      <tr>
          <td>MinHash</td>
          <td>近重複偵測（找變體 / 複製貼上）</td>
      </tr>
      <tr>
          <td>Module proximity</td>
          <td>符號在依賴 graph 上的距離</td>
      </tr>
      <tr>
          <td>Graph diffusion</td>
          <td>在 graph 上做 spreading activation</td>
      </tr>
  </tbody>
</table>
<p>表格列了 9 個明確 signal、README 另說有 11 個（剩 2 個是 implementation detail 沒公開細節）。實務上 11-signal 的價值在於<strong>幾個高權重 signal 各自負責不同 query 類型</strong>——權重分配有明顯的高低差：</p>
<ul>
<li><strong>RRI</strong> 是 cbm 對「重要符號優先」的 graph 結構 prior。一個被大量檔案 import 的核心 class、即使在 query 字串裡只有間接匹配、RRI 也會把它往上推。這層對「找這個 codebase 的入口 / 主要抽象」類 query 特別重要。</li>
<li><strong>Data flow</strong> 是 cbm 對「概念上接近、但符號名沒共字」的 query 的關鍵 signal。例如查「金額顯示」、<code>formatAmount</code> 跟 <code>_buildPriceDisplay</code> 在符號名上沒共字、但 data flow 能捕捉「<code>formatAmount</code> 的回傳值流入了 <code>_buildPriceDisplay</code> 的 widget」這層連結。</li>
<li><strong>Graph diffusion</strong> 是 cbm 對「擴散式相關性」的最終 boost——已經被前面 signal 推到高分的符號，會把分數擴散到 graph 上鄰近的符號。實務影響：monorepo 上效果最強（跨 module 鄰近性有意義）、單一檔案的小專案上幾乎沒效果。</li>
</ul>
<p>加上一層 <strong><code>cbm_camel_split</code> tokenizer</strong>：對 <code>getMoneyField</code> 這類 identifier 做 camelCase / snake_case 切詞，所以查 <code>money field display</code> 能命中 <code>getMoneyField</code>、<code>MoneyFieldRenderer</code> 之類符號。</p>
<p>這套組合的判讀價值在於：<strong>對「我不知道精確符號名」的概念性查詢，cbm 是少數能給出合理 top-N 的工具</strong>。例如查「金額顯示相關」、結果裡會出現 <code>formatAmount</code> 實作 + <code>_buildPriceDisplay</code> + <code>getBalanceDisplay</code>，這些都跟「金額顯示」業務概念相關、不會被 <code>displayName</code> / <code>displayTags</code> 這種只共享 <code>display</code> 子字串的雜訊淹沒。</p>
<p>下一步路由：要看實測案例，見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="hybrid-type-resolution只給五個語言的特殊待遇">Hybrid type resolution：只給五個語言的特殊待遇</h2>
<p>cbm 對 <strong>Go / C / C++ / TypeScript / JavaScript</strong>（JS 含 JSX、TS 含 TSX）額外跑一層 type resolution，README 描述是：</p>
<blockquote>
<p>Clean-room reimplementation of tsserver / typescript-go&rsquo;s type resolution algorithms — parameter binding, return-type inference, generic substitution, JSX component dispatch, JSDoc inference for plain JS files.</p></blockquote>
<p>換言之，這幾個語言的 <code>CALLS</code> edge 在 syntactic match 之上多了一層 type-aware dispatch resolution，效果接近 LSP。其他 149 個語言只跑純 tree-sitter pass，能力會降到「<strong>結構抽得到、call edge 抽得很有限</strong>」。</p>
<p>實測對照（在某 Dart 商業專案上）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">cbm 索引完成統計：3,038 nodes、6,355 edges
</span></span><span class="line"><span class="ln">2</span><span class="cl">其中 CALLS edge 總數：2（整個專案僅 2 條）</span></span></code></pre></div><p>這個數字反映 cbm 的設計選擇——<strong>hybrid resolution 名單只涵蓋 5 個語言、Dart 在名單外</strong>——所以 <code>trace_call_path</code> 對 Dart symbol 一律回 0 caller，這個 0 屬於 by design 行為。對 Go / TS 主力專案，這個能力上限會完全不一樣。</p>
<p>判讀訊號：開發前先確認自己的主力語言在不在那五個語言內。在的話 cbm 是準 LSP；不在的話它只是個「結構 + 全文搜尋」工具，呼叫鏈相關問題要靠別的 MCP 補。</p>
<h2 id="跨-service-鏈接first-class-http_calls-edge">跨 service 鏈接：first-class HTTP_CALLS edge</h2>
<p>cbm 的另一個差異化能力是把 <strong>REST / gRPC / GraphQL / tRPC route 當 first-class node</strong>，建立跨 service 的 <code>HTTP_CALLS</code> edge：</p>
<ul>
<li>Route 偵測：對應主流 web framework（Express / NestJS / FastAPI / Gin / Rails 等）的 route 定義語法</li>
<li>Call site 比對：以 route pattern 比對 client 端的 URL 字面值或變數，附 confidence score</li>
<li>額外的 channel edge：Socket.IO / EventEmitter / 各種 pub-sub 的 <code>EMITS</code> / <code>LISTENS_ON</code></li>
</ul>
<p>這層能力對單一 monorepo 內的多 service 架構（microservice repo / BFF / API gateway pattern）特別有價值——可以查「這個前端 API call 對應哪個後端 handler」這種跨 service 問題。對單一 service 的單體 repo，這層能力派不上用場。</p>
<p>實際使用前提：要 index 的 repo 必須<strong>同時包含 client 跟 server 端</strong>，分散在多 repo 的話 cbm 不會自動跨 repo 連邊。</p>
<h2 id="cypher-子集支援的查詢與邊界">Cypher 子集：支援的查詢與邊界</h2>
<p>cbm 提供的 <code>query_graph</code> 是 Cypher 的<strong>真子集</strong>——覆蓋大部分 read-only query 語法、省略 mutation 與部分 aggregation 語法：</p>
<p><strong>支援</strong>：</p>
<ul>
<li><code>MATCH</code> 含 label / relationship type / 變長路徑</li>
<li><code>WHERE</code> 含比較 / regex / <code>CONTAINS</code></li>
<li><code>RETURN</code> 含 property access、<code>COUNT</code>、<code>DISTINCT</code></li>
<li><code>ORDER BY</code>、<code>LIMIT</code></li>
</ul>
<p><strong>不支援</strong>：</p>
<ul>
<li><code>WITH</code>（不能多階段 pipeline）</li>
<li><code>COLLECT</code>（不能 aggregate 成 list）</li>
<li><code>OPTIONAL MATCH</code>（不能 left-join）</li>
<li><code>labels(n)</code> / <code>type(r)</code> 等函數呼叫</li>
<li><code>AS</code> 別名</li>
<li>任何 mutation（純讀）</li>
</ul>
<p>幾個限制各自踩到的事故型態：</p>
<ul>
<li><strong><code>WITH</code> 缺席</strong>：所有需要「先 match 一組、再 filter / aggregate」的二階段 query 都寫不出來。例如「列出每個 module 內最常被呼叫的 function」這種 Top-K per group 的 query、在 Cypher 是 <code>MATCH ... WITH module, COUNT(*) AS c ORDER BY c LIMIT 1</code>、在 cbm 要拆成「先 list modules、再對每個 module 跑一次 callers query、外層排序」。</li>
<li><strong><code>OPTIONAL MATCH</code> 缺席</strong>：left-join 場景做不到。例如「列出所有 class、附上它的 supertype（若有）」這種「主結果不該因為某個關係缺失就丟掉」的 query 寫不出來。cbm 上的做法是先抓全部 class、再對每個 class 跑一次 supertype query、在 client 端合併。</li>
<li><strong><code>labels(n)</code> 缺席</strong>：拿不到 graph 內所有 node label 種類的清單。想做「我的 graph 裡有哪幾類 node」這種 schema 探索類 query、得退回 <code>get_graph_schema</code> 拿固定的 schema 介紹、看不到 instance 層真實分布。</li>
<li><strong><code>AS</code> 別名缺席</strong>：query 結果直接是 node / relationship object、沒法 rename 欄位給 downstream consumer。</li>
</ul>
<p>這些限制的共通實際影響：<strong>想做 group-by-count 類的 graph stats 查詢得退回 <code>search_graph</code> 逐 label 抽</strong>。例如「列出每個 file 有幾個 method」這種一行 Cypher 在標準 Neo4j 能寫的、在 cbm 上要拆成多次 query 在外層彙整。</p>
<p>判讀訊號：若 query 需要 <code>WITH ... COLLECT(...) AS xs</code> 這類二階段語法，先別硬寫 Cypher，改用 <code>search_graph</code> 加 client 端聚合。</p>
<h2 id="安裝行為與兩個要注意的小坑">安裝行為與兩個要注意的小坑</h2>
<p>cbm 的 <code>install.sh</code> 對 <code>~/.claude/settings.json</code> 動的範圍比 README 寫得多。實際安裝會：</p>
<ul>
<li>下載對應平台 binary、剝 macOS quarantine、ad-hoc sign</li>
<li>自動偵測 11 種 coding agent，逐一注入 MCP server config</li>
<li>對 Claude Code 寫入 <code>.claude/.mcp.json</code>、4 個 Skill、PreToolUse hook</li>
<li>Hook 名稱：<code>cbm-code-discovery-gate</code>，攔截 Grep / Glob 注入結構化 context</li>
</ul>
<p>兩個實際踩過的小坑：</p>
<p><strong>Hook matcher 與 README 不一致</strong>。README 描述「intercepts Grep/Glob — never Read」，實際安裝版本 matcher 是 <code>&quot;Grep|Glob|Read|Search&quot;</code>，連 Read 也被擋。修法：手動把 matcher 改成 <code>&quot;Grep|Glob|Search&quot;</code>。注意 <code>codebase-memory-mcp update</code> 可能會把這行改回原樣，每次升級要重新確認。</p>
<p><strong>uninstall 不清 hook</strong>。卸載 binary 不會主動把 <code>~/.claude/settings.json</code> 裡的 hook 條目移除。決定不再用 cbm 時要手動清掉 <code>PreToolUse</code> 下的 <code>cbm-code-discovery-gate</code> 條目，否則之後安裝其他工具或除錯時會看到神祕的 BLOCKED 訊息。</p>
<h2 id="14-個-mcp-tool-的分類">14 個 MCP tool 的分類</h2>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>Tool</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引</td>
          <td><code>index_repository</code>、<code>list_projects</code>、<code>delete_project</code>、<code>index_status</code></td>
      </tr>
      <tr>
          <td>查詢</td>
          <td><code>search_graph</code>、<code>trace_call_path</code>、<code>detect_changes</code>、<code>query_graph</code>、<code>get_graph_schema</code>、<code>get_code_snippet</code>、<code>get_architecture</code>、<code>search_code</code></td>
      </tr>
      <tr>
          <td>管理</td>
          <td><code>manage_adr</code>（架構決策紀錄 CRUD）、<code>ingest_traces</code>（runtime trace 驗證 HTTP_CALLS）</td>
      </tr>
  </tbody>
</table>
<p>特別值得提的兩個：</p>
<ul>
<li><code>manage_adr</code>：把 Architecture Decision Records 當持久化資源管理。對長期專案有累積架構決策需求的場景有用，但若團隊已用 ADR-tools 或 Notion 管 ADR，這層會重複。</li>
<li><code>ingest_traces</code>：餵 runtime trace 進來驗證 <code>HTTP_CALLS</code> edge 是否反映實際的 runtime 調用。可以把靜態推測的 cross-service edge 與真實 runtime 行為對齊。實務上要先有 distributed tracing 基礎建設才開得了，門檻偏高。</li>
</ul>
<h2 id="適用--不適用情境的判讀">適用 / 不適用情境的判讀</h2>
<p><strong>適用情境</strong>：</p>
<ul>
<li><strong>主力語言在 Go / C / C++ / TS / JS 名單內</strong> → 享受 hybrid type resolution。判讀方法：對 5 個熱門 class 跑 <code>trace_call_path</code>、若 caller 數跟 IDE「Find Usages」結果對得上、表示 hybrid 正常工作。</li>
<li><strong>概念性 / 自然語言搜尋需求高</strong> → 11-signal scoring 是少數能勝任的 MCP。判讀方法：對「我只記得功能類別、不記得名字」的 query 跑 cbm 跟其他工具的 search、若 cbm top-10 命中率明顯高、值得當主要入口。</li>
<li><strong>跨 service 的 monorepo</strong> → first-class HTTP_CALLS edge 抽得到 cross-service 鏈。判讀方法：repo 內若有多個 service 用 HTTP / gRPC / GraphQL 互相呼叫、又分散在同一個 git tree 內、cbm 能跨 service 連邊；若只是單 service repo 這條沒效。</li>
<li><strong>偏好單 binary 部署</strong> → 不想為個別語言裝 toolchain、cbm 是少數零外部依賴的選項。</li>
</ul>
<p><strong>不適用情境</strong>：</p>
<ul>
<li><strong>主力語言不在 hybrid resolution 名單</strong>（如 Dart / Swift / Kotlin）且核心需求是 caller / blast radius 追蹤。判讀方法：在自己 repo 跑 cbm <code>trace_call_path</code> 對 5 個熱門 class、若 caller 數明顯偏低或 0、表示 cbm 在這語言只剩結構抽取、要靠 LSP 工具補。</li>
<li><strong>要 symbol-level 編輯</strong>（rename / replace_symbol_body）— cbm 純讀、沒這層。判讀方法：要做「rename method 並更新所有 reference」這類 atomic refactor 時、cbm 完全幫不上忙、要走 LSP 工具。</li>
<li><strong>要編譯 diagnostic 整合</strong> — cbm 不接 LSP、沒法把 type error / unused import 拋給 agent。</li>
</ul>
<p><strong>搭配建議</strong>：在不在 hybrid resolution 名單的語言上，cbm 通常需要配合一個 LSP-based MCP（如 <a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a>）做 caller / impact 補位，加上一個 tree-sitter call graph 工具（如 <a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a>）做日常結構查詢。三者怎麼分工見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="結論">結論</h2>
<p>cbm 的核心價值在三件事：<strong>單 binary 部署</strong>、<strong>11-signal 語意搜尋</strong>、<strong>跨 service HTTP/RPC 鏈接</strong>。前兩件對任何語言都成立，第三件對微服務 monorepo 特別有意義。</p>
<p>它的能力上限被 hybrid type resolution 的語言名單卡死——名單內等於準 LSP，名單外只是個結構抽取器。評估時第一個要問的問題是：「我的主力語言在不在那五個（Go / C / C++ / TS / JS）？」答案決定 cbm 是主刀還是輔刀。</p>
]]></content:encoded></item><item><title>codegraph：用 tree-sitter per-language query 撐起 19+ 語言 call graph 的 MCP</title><link>https://tarrragon.github.io/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/</guid><description>&lt;h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題&lt;/h2>
&lt;p>codegraph 的設計動機很具體：&lt;strong>Claude Code 探索 codebase 時 spawn 的 Explore agent 會用 grep / glob / read 連續刷檔，每個 tool call 都吃 token&lt;/strong>。codegraph 把這層探索預先做好，agent 直接查預建好的 graph。&lt;/p>
&lt;blockquote>
&lt;p>When Claude Code explores a codebase, it spawns Explore agents that scan files with grep, glob, and Read — consuming tokens on every tool call. CodeGraph gives those agents a pre-indexed knowledge graph — symbol relationships, call graphs, and code structure.&lt;/p>&lt;/blockquote>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm&lt;/a> 比，codegraph 的 scope 更窄、更專注：不做跨 service 鏈接、不做 ADR / runtime trace 管理、不做 11-signal 語意搜尋，&lt;strong>只把 call graph 跟 symbol relationship 做好&lt;/strong>。這個取捨讓它的 MCP tool 只有 10 個、每個責任都很單一。&lt;/p>
&lt;h2 id="技術架構tree-sitter--per-language-query--fts5">技術架構：tree-sitter + per-language query + FTS5&lt;/h2>
&lt;p>codegraph 的核心 pipeline：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">tree-sitter parse → per-language query 抽 nodes/edges
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> → 解析 reference（import / extends / implements / calls）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> → 寫進 SQLite + FTS5&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵設計：&lt;strong>對每個語言寫專屬的 tree-sitter query&lt;/strong>——比起通用 AST visitor 路線、這個設計能對特定語言的 dispatch pattern 抽到更精確的 node 跟 edge。&lt;/p>
&lt;blockquote>
&lt;p>Language-specific queries extract nodes (functions, classes, methods) and edges (calls, imports, extends, implements).&lt;/p>&lt;/blockquote>
&lt;p>這個設計選擇直接決定了 codegraph 對非主流語言（如 Dart / Svelte / Liquid）的支援深度——因為每個語言都有專屬 query，所以 19+ 語言裡的 Dart 真的有 working call graph，不像純 tree-sitter wrapper 那樣只能抽結構。&lt;/p>
&lt;p>實際支援的 19+ 語言：&lt;/p>
&lt;p>TypeScript、JavaScript、Python、Go、Rust、Java、C#、PHP、Ruby、C、C++、Swift、Kotlin、Scala、Dart、Svelte、Vue、Liquid、Lua、Luau、Pascal/Delphi。&lt;/p>
&lt;p>過濾規則：「&lt;strong>Files larger than 1 MB are skipped&lt;/strong>」（generated bundle / minified JS / vendored blob 自動忽略）。&lt;/p>
&lt;h2 id="auto-syncnative-os-file-watcher--2s-debounce">Auto-sync：native OS file watcher + 2s debounce&lt;/h2>
&lt;p>codegraph 預設啟用 file watcher、用 native OS 事件（macOS FSEvents / Linux inotify / Windows ReadDirectoryChanges）：&lt;/p></description><content:encoded><![CDATA[<h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題</h2>
<p>codegraph 的設計動機很具體：<strong>Claude Code 探索 codebase 時 spawn 的 Explore agent 會用 grep / glob / read 連續刷檔，每個 tool call 都吃 token</strong>。codegraph 把這層探索預先做好，agent 直接查預建好的 graph。</p>
<blockquote>
<p>When Claude Code explores a codebase, it spawns Explore agents that scan files with grep, glob, and Read — consuming tokens on every tool call. CodeGraph gives those agents a pre-indexed knowledge graph — symbol relationships, call graphs, and code structure.</p></blockquote>
<p>跟 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 比，codegraph 的 scope 更窄、更專注：不做跨 service 鏈接、不做 ADR / runtime trace 管理、不做 11-signal 語意搜尋，<strong>只把 call graph 跟 symbol relationship 做好</strong>。這個取捨讓它的 MCP tool 只有 10 個、每個責任都很單一。</p>
<h2 id="技術架構tree-sitter--per-language-query--fts5">技術架構：tree-sitter + per-language query + FTS5</h2>
<p>codegraph 的核心 pipeline：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">tree-sitter parse → per-language query 抽 nodes/edges
</span></span><span class="line"><span class="ln">2</span><span class="cl">                  → 解析 reference（import / extends / implements / calls）
</span></span><span class="line"><span class="ln">3</span><span class="cl">                  → 寫進 SQLite + FTS5</span></span></code></pre></div><p>關鍵設計：<strong>對每個語言寫專屬的 tree-sitter query</strong>——比起通用 AST visitor 路線、這個設計能對特定語言的 dispatch pattern 抽到更精確的 node 跟 edge。</p>
<blockquote>
<p>Language-specific queries extract nodes (functions, classes, methods) and edges (calls, imports, extends, implements).</p></blockquote>
<p>這個設計選擇直接決定了 codegraph 對非主流語言（如 Dart / Svelte / Liquid）的支援深度——因為每個語言都有專屬 query，所以 19+ 語言裡的 Dart 真的有 working call graph，不像純 tree-sitter wrapper 那樣只能抽結構。</p>
<p>實際支援的 19+ 語言：</p>
<p>TypeScript、JavaScript、Python、Go、Rust、Java、C#、PHP、Ruby、C、C++、Swift、Kotlin、Scala、Dart、Svelte、Vue、Liquid、Lua、Luau、Pascal/Delphi。</p>
<p>過濾規則：「<strong>Files larger than 1 MB are skipped</strong>」（generated bundle / minified JS / vendored blob 自動忽略）。</p>
<h2 id="auto-syncnative-os-file-watcher--2s-debounce">Auto-sync：native OS file watcher + 2s debounce</h2>
<p>codegraph 預設啟用 file watcher、用 native OS 事件（macOS FSEvents / Linux inotify / Windows ReadDirectoryChanges）：</p>
<ul>
<li>Debounce window：2 秒（避免快速連續存檔重複觸發）</li>
<li>過濾範圍：只看 source 檔案（按副檔名）</li>
<li>行為描述：「<strong>Incremental sync. The graph stays current as you code — no configuration needed</strong>」</li>
</ul>
<p>這層比 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 的「背景 git polling」更貼近 IDE — 改完檔案 2 秒內 graph 就同步好，「邊改邊問」工作流更順。</p>
<p>判讀訊號：剛存完檔立刻問 caller 還是漏，等 3 秒再試一次；持續漏的話跑 <code>codegraph status</code> 看 indexed 數字對不對得上預期。</p>
<h2 id="call-graph-抽取的能力與聲稱">Call graph 抽取的能力與聲稱</h2>
<p>codegraph 對 caller / callee / impact / trace 這四個查詢的覆蓋是它的主賣點。README 對 <code>codegraph_trace</code> 的聲稱是：</p>
<blockquote>
<p>Follow dynamic-dispatch hops (callbacks, React re-render, interface→impl) that grep can&rsquo;t.</p></blockquote>
<p>實際機制 README 沒詳細寫，從 source 推測是「<strong>對某些常見動態 dispatch pattern 寫了專屬 query</strong>」——比如 React component 的 JSX → component definition 解析、interface method → implementation 對應這類。</p>
<p>這個 claim 在實測上<strong>有但有限</strong>——對 type-inferred receiver 仍會漏。例如 Dart 上（<code>Money</code> 在該專案是 extension type）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">final</span> <span class="n">Money</span> <span class="n">samplePrice</span> <span class="o">=</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">samplePrice</span><span class="p">.</span><span class="n">multiplyByRate</span><span class="p">(</span><span class="n">rate</span><span class="p">);</span>   <span class="o">//</span> <span class="err">←</span> <span class="n">codegraph</span> <span class="err">抽不到這條</span> <span class="n">edge</span></span></span></code></pre></div><p><code>samplePrice</code> 是 local variable，要做型別推斷才知道 receiver 是 <code>Money</code>。tree-sitter 看到的只是 <code>&lt;identifier&gt;.multiplyByRate(...)</code>、不知道 <code>samplePrice</code> 的型別、無法 dispatch 到 <code>Money.multiplyByRate</code>。</p>
<p>判讀訊號：<strong>對「靠型別解析才能找到的 callsite」會漏</strong>。如果專案大量使用 generics、type aliasing、factory pattern 隱藏型別、duck typing，codegraph 的 caller 數字會系統性偏低。重要 refactor 別只看它的數字決策。</p>
<p>下一步路由：實測對照數字見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="caller-跟-callsite-的計數單位差異">Caller 跟 callsite 的計數單位差異</h2>
<p>codegraph 的 <code>codegraph_callers</code> 採用的計數單位是「<strong>caller symbol 數</strong>」（同一個 method 內呼叫目標兩次仍然只算 1 個 caller）——跟「callsite 數」屬於兩種不同的統計方式。</p>
<p>這個設計的影響：跟 LSP-based 工具（如 <a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a>）對比時，數字會看起來「少」，但這是計數規則的差異、跟精度差距屬於兩個不同議題。寫實測 baseline 時要把這個單位寫死，避免「codegraph 回 3、serena 回 9」被誤判成「codegraph 漏 6 個」。</p>
<p>實際上這 3 vs 9 的差距要分兩段看：codegraph 抓到的 3 個 caller symbol 對應 6 個 callsite（同一個 method 內有多處呼叫、被計數規則合併成 1 caller）、剩下的 3 個 callsite 在第 4 個檔案（<code>product.dart</code>）、是真的漏（type-inferred dispatch）。算術：6 callsite（codegraph 算 3 caller）+ 3 callsite（真的漏）= serena 的 9。要拆開看才知道哪部分是計數差異、哪部分是能力差距。</p>
<h2 id="14-web-framework-的-route-識別">14 web framework 的 route 識別</h2>
<p>codegraph 內建對 web framework 的 route 識別：</p>
<p>Django、Flask、FastAPI、Express、NestJS、Laravel、Drupal、Rails、Spring、Gin / chi / gorilla / mux、Axum / actix / Rocket、ASP.NET、Vapor、React Router、SvelteKit。</p>
<p>README 標稱「14 個」、實際展開後是 15 條（Gin / chi / gorilla / mux 跟 Axum / actix / Rocket 各算一組路由生態）。這個小落差源自分組計數方式、不影響功能。</p>
<p>這層的角色是讓 <code>codegraph_search</code> 能用 URL pattern 找到對應 handler，不必去猜 handler 函式名。但跟 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 的 first-class HTTP_CALLS edge 不一樣，codegraph 沒做「client URL 字面值 → server route 比對」，所以<strong>單一 service 內找 handler 可以、跨 service 鏈接做不到</strong>。</p>
<p>判讀訊號：純前端 / 純後端 repo 上這層夠用；要跨 service 追 cross-service call 仍要靠 cbm 或別的工具。</p>
<h2 id="10-個-mcp-tool-的責任分工">10 個 MCP tool 的責任分工</h2>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>codegraph_search</code></td>
          <td>用名稱 / pattern 找 symbol</td>
      </tr>
      <tr>
          <td><code>codegraph_context</code></td>
          <td>給定 task，組出相關 code context</td>
      </tr>
      <tr>
          <td><code>codegraph_trace</code></td>
          <td>兩個 symbol 之間的 call path、每跳含 body</td>
      </tr>
      <tr>
          <td><code>codegraph_callers</code></td>
          <td>找誰呼叫了 X（一跳）</td>
      </tr>
      <tr>
          <td><code>codegraph_callees</code></td>
          <td>找 X 呼叫了誰（一跳）</td>
      </tr>
      <tr>
          <td><code>codegraph_impact</code></td>
          <td>改 X 會影響什麼（blast radius）</td>
      </tr>
      <tr>
          <td><code>codegraph_node</code></td>
          <td>取 symbol 詳情 + 原始碼</td>
      </tr>
      <tr>
          <td><code>codegraph_explore</code></td>
          <td>一次回多個相關 symbol 的原始碼</td>
      </tr>
      <tr>
          <td><code>codegraph_files</code></td>
          <td>已索引的檔案結構</td>
      </tr>
      <tr>
          <td><code>codegraph_status</code></td>
          <td>索引健康度跟統計</td>
      </tr>
  </tbody>
</table>
<p>設計上有四個值得單獨展開的 tool：</p>
<p><strong><code>codegraph_explore</code></strong> 是為了<strong>省 tool call</strong> — 不用對 N 個 symbol 各呼叫一次 <code>codegraph_node</code>、一次拿到所有 source。這直接呼應 codegraph 整體「省 token / 省 tool call」的設計目標。</p>
<p><strong><code>codegraph_trace</code></strong> <strong>單一 call 涵蓋整個路徑</strong>、每一跳的 function body 直接 inline 在結果裡。對「X 怎麼影響到 Y」這種多跳問題，傳統做法要 N 次 <code>codegraph_callers</code> + N 次 <code>codegraph_node</code>，trace 把這壓成 1 次。代價是若兩個 symbol 之間沒有 static-resolvable 路徑（如 type-inferred dispatch 中斷），會直接回「No direct path」、不會主動找替代解釋。</p>
<p><strong><code>codegraph_context</code></strong> 跟 <code>codegraph_explore</code> 的責任差別常被搞混。<code>codegraph_explore</code> 是「我已經知道要看哪幾個 symbol」、一次拿原始碼；<code>codegraph_context</code> 是「我有個 task description、不知道相關 symbol 是哪些」、由它從 task 內容拉出可能相關的 graph 鄰域。前者是「精確檢索」、後者是「概念性彙整」。實務上 task agent 開新任務時用 <code>codegraph_context</code>、debug 細節時用 <code>codegraph_explore</code>。</p>
<p><strong><code>codegraph_impact</code></strong> 是 blast radius 工具、但<strong>它的精度被 tree-sitter syntactic 限制卡住</strong>——跟 caller / callee 同源、type-inferred dispatch 的影響範圍會漏。實務影響：對「rename method 會影響什麼」這類重要 refactor 不能單看它的數字、要走 LSP 工具 cross-check。判讀訊號：<code>codegraph_impact X</code> 回的 affected symbol 數明顯少於預期、且 X 是被廣泛使用的 type / method 時、blast radius 多半有漏、要補 LSP 驗證。</p>
<h2 id="token-efficiency-benchmark方法論與限制">Token efficiency benchmark：方法論與限制</h2>
<p>README 聲稱「<strong>~35% cheaper · ~70% fewer tool calls · 100% local</strong>」、median 跨 7 codebase：</p>
<ul>
<li>Cost: 35% reduction</li>
<li>Tokens: 57% fewer</li>
<li>Time: 46% faster</li>
<li>Tool calls: 71% fewer</li>
</ul>
<p>方法論：</p>
<blockquote>
<p>Claude Opus 4.7 run headlessly. WITH = CodeGraph&rsquo;s MCP server enabled, WITHOUT = empty MCP config. Same question per repo, 4 runs per arm, median reported.</p></blockquote>
<p>7 個 benchmark codebase：</p>
<table>
  <thead>
      <tr>
          <th>Repo</th>
          <th>語言</th>
          <th>規模</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>VS Code</td>
          <td>TypeScript</td>
          <td>~10k 檔</td>
      </tr>
      <tr>
          <td>Excalidraw</td>
          <td>TypeScript</td>
          <td>~640 檔</td>
      </tr>
      <tr>
          <td>Django</td>
          <td>Python</td>
          <td>~3k 檔</td>
      </tr>
      <tr>
          <td>Tokio</td>
          <td>Rust</td>
          <td>~790 檔</td>
      </tr>
      <tr>
          <td>OkHttp</td>
          <td>Java</td>
          <td>~645 檔</td>
      </tr>
      <tr>
          <td>Gin</td>
          <td>Go</td>
          <td>~110 檔</td>
      </tr>
      <tr>
          <td>Alamofire</td>
          <td>Swift</td>
          <td>~110 檔</td>
      </tr>
  </tbody>
</table>
<p>幾個要注意的解讀偏差：</p>
<p><strong>Benchmark 集中在 codegraph 強項語言</strong>。VS Code / Django / Tokio 都是 codegraph 的核心支援語言、且 LSP 生態成熟。Dart / Svelte / Liquid 這類 long-tail 語言沒列在 benchmark 內，token 節省效果在那些語言上是否成立不知道。</p>
<p><strong>Empty MCP config 的對照組不一定貼近實務</strong>。沒裝任何 MCP 時 agent 的 baseline 探索行為跟「裝了其他 MCP」不同。實務 stack 通常多個 MCP 並用，這個 35% 對「加裝 codegraph 進已有 MCP stack」的邊際效益會打折。</p>
<p>判讀訊號：benchmark 數字當「值得試」的參考、不當「裝了就省 35%」的硬保證。實際省多少要在自己的 stack 上跑同樣 question set 才知。</p>
<h2 id="安裝行為">安裝行為</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">npm i -g @colbymchenry/codegraph
</span></span><span class="line"><span class="ln">2</span><span class="cl">codegraph install --target claude --location global -y
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">cd</span> your-project <span class="o">&amp;&amp;</span> codegraph init -i</span></span></code></pre></div><p><code>codegraph install</code> 會把 MCP server 條目寫進 <code>~/.claude.json</code> 的 <code>mcpServers</code>、<code>codegraph init -i</code> 在當前 repo 建 <code>.codegraph/codegraph.db</code>、啟動 watcher。</p>
<p>跟 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 不一樣：codegraph <strong>不寫 PreToolUse hook</strong>、不攔截 Grep/Glob。它純粹當 MCP server 提供 tool、決策權留給 agent，對既有工作流的干擾較小。</p>
<p>CLI mode 是另一個方便點：所有 MCP tool 在 CLI 都有對應指令（<code>codegraph callers X</code> / <code>codegraph trace X Y</code>），不必等 Claude Code 重啟載入 MCP 就能先在 terminal 驗證效果。</p>
<h2 id="適用--不適用情境的判讀">適用 / 不適用情境的判讀</h2>
<p><strong>適用情境</strong>：</p>
<ul>
<li>主力語言在 19+ 支援列表內，且需要可靠的 caller / impact / trace 查詢</li>
<li>「邊改邊問」工作流（auto-sync 2s debounce 比較貼近 IDE）</li>
<li>希望 MCP 保持原生 grep / glob 行為、把決策權留給 agent 自主判斷（避開 hook 層強制介入）</li>
<li>要 CLI 跟 MCP 雙管道使用（CLI 可先試、MCP 給 agent 用）</li>
</ul>
<p><strong>不適用情境</strong>：</p>
<ul>
<li>語言不在支援列表（codegraph 不像 cbm 一次 vendor 155 個 grammar）</li>
<li>需要跨 service 的 client URL → server route 鏈接（codegraph 只認 route definition）</li>
<li>需要 symbol-level atomic edit（codegraph 純讀、沒 rename / replace_symbol_body）</li>
<li>重要 refactor 要保證不漏 callsite（tree-sitter syntactic 上限會漏 type-inferred dispatch）</li>
</ul>
<p><strong>搭配建議</strong>：對 type-inferred dispatch 漏的部分，可以靠 LSP-based 工具（如 <a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a>）補位。對概念性自然語言搜尋，<a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 的 11-signal scoring 比 codegraph 的 symbol pattern match 更強。三者怎麼分工見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="結論">結論</h2>
<p>codegraph 的核心價值是<strong>用 per-language tree-sitter query 把 call graph 做成 19+ 語言通用的 MCP 服務</strong>，加上 auto-sync 跟 CLI 雙管道。它的 scope 聚焦在 call graph、比 cbm 窄很多、但聚焦範圍內品質很高。</p>
<p>它的型別解析靠 tree-sitter syntactic：<strong>receiver 是顯式型別宣告或 literal 的 callsite 解得好、receiver 要靠型別推斷的 callsite 會漏</strong>。判斷 codegraph 在自己專案上的可信度，先估專案有多少比例的 call 是 type-inferred receiver——比例高就要配 LSP 工具補位、比例低就放心用。</p>
]]></content:encoded></item><item><title>serena：把 LSP 包成 agent-first MCP 的 symbol-level 編輯方案</title><link>https://tarrragon.github.io/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/</guid><description>&lt;h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題&lt;/h2>
&lt;p>serena 的核心定位是「&lt;strong>把現成 LSP 生態包成適合 agent 用的高階抽象&lt;/strong>」。它不自建 type system、不自寫 parser，直接 spawn 各語言對應的 language server（Dart 用 &lt;code>dart analysis_server&lt;/code>、TS 用 &lt;code>tsserver&lt;/code>、Rust 用 &lt;code>rust-analyzer&lt;/code> 等），把 LSP 的能力轉成 MCP tool。&lt;/p>
&lt;p>設計哲學是 README 自己歸納的「agent-first tool design」：&lt;/p>
&lt;blockquote>
&lt;p>Involves robust high-level abstractions, distinguishing it from approaches that rely on low-level concepts like line numbers or primitive search patterns.&lt;/p>&lt;/blockquote>
&lt;p>換言之，serena 的所有編輯都是 &lt;strong>symbol-level&lt;/strong>——讓 agent 直接用 symbol 語意操作（「把 X function 的 body 整個換掉」、「在 Y class 後面插一段」、「rename Z」），跳過 line number 跟 text patch 這層 raw text 處理。對應的是 LSP 路線本來就有的 symbol 結構與 reference 追蹤。&lt;/p>
&lt;p>跟 tree-sitter 路線的本質分野：tree-sitter 只給結構、不給型別；LSP 給的是「IDE 等級的真型別系統」。代價是 LSP 要每個語言裝對應 language server、執行期 spawn process、per-session 維護狀態。&lt;/p>
&lt;h2 id="部署形態兩個-backend執行期-spawn-lsp">部署形態：兩個 backend、執行期 spawn LSP&lt;/h2>
&lt;p>serena 提供兩個 backend：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Backend&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;th>取捨&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Language Server&lt;/td>
 &lt;td>預設、開源、跨平台&lt;/td>
 &lt;td>要對應語言的 language server 在環境內&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JetBrains Plugin&lt;/td>
 &lt;td>已用 JetBrains IDE 的 paid 使用者&lt;/td>
 &lt;td>借用 IDE 完整能力（debug / breakpoint）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Language Server backend 是 OSS 用戶會接觸的路線。serena 透過 LSP 抽象涵蓋 40+ 語言、實際能力依各語言 LSP 成熟度而定——Python / TypeScript / Go / Rust / Java / C# / Dart 等主流語言由 serena 內建 bootstrap 自動下載 server、冷門語言（如 Liquid / Pascal）需要使用者自己準備 server binary、無 server 的語言視同 fallback 到純文字工具。判讀訊號：跑 &lt;code>activate_project&lt;/code> 後若 serena 沒在背景 spawn 對應 LSP、表示該語言走 fallback 路線、&lt;code>find_referencing_symbols&lt;/code> 等型別敏感 tool 不可用。&lt;/p>
&lt;p>對 Dart 而言：serena 啟動時 spawn &lt;code>dart analysis_server&lt;/code>、跟 Flutter SDK 內附的同一隻。所以 serena 對 Dart 的能力等同 &lt;code>dart analysis_server&lt;/code> 暴露的能力——比 tree-sitter 路線高一個量級。&lt;/p>
&lt;h2 id="per-session-模型與-activate_project">Per-session 模型與 activate_project&lt;/h2>
&lt;p>serena 的 LSP backend 是 &lt;strong>per-session&lt;/strong> 的：&lt;/p></description><content:encoded><![CDATA[<h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題</h2>
<p>serena 的核心定位是「<strong>把現成 LSP 生態包成適合 agent 用的高階抽象</strong>」。它不自建 type system、不自寫 parser，直接 spawn 各語言對應的 language server（Dart 用 <code>dart analysis_server</code>、TS 用 <code>tsserver</code>、Rust 用 <code>rust-analyzer</code> 等），把 LSP 的能力轉成 MCP tool。</p>
<p>設計哲學是 README 自己歸納的「agent-first tool design」：</p>
<blockquote>
<p>Involves robust high-level abstractions, distinguishing it from approaches that rely on low-level concepts like line numbers or primitive search patterns.</p></blockquote>
<p>換言之，serena 的所有編輯都是 <strong>symbol-level</strong>——讓 agent 直接用 symbol 語意操作（「把 X function 的 body 整個換掉」、「在 Y class 後面插一段」、「rename Z」），跳過 line number 跟 text patch 這層 raw text 處理。對應的是 LSP 路線本來就有的 symbol 結構與 reference 追蹤。</p>
<p>跟 tree-sitter 路線的本質分野：tree-sitter 只給結構、不給型別；LSP 給的是「IDE 等級的真型別系統」。代價是 LSP 要每個語言裝對應 language server、執行期 spawn process、per-session 維護狀態。</p>
<h2 id="部署形態兩個-backend執行期-spawn-lsp">部署形態：兩個 backend、執行期 spawn LSP</h2>
<p>serena 提供兩個 backend：</p>
<table>
  <thead>
      <tr>
          <th>Backend</th>
          <th>適用情境</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Language Server</td>
          <td>預設、開源、跨平台</td>
          <td>要對應語言的 language server 在環境內</td>
      </tr>
      <tr>
          <td>JetBrains Plugin</td>
          <td>已用 JetBrains IDE 的 paid 使用者</td>
          <td>借用 IDE 完整能力（debug / breakpoint）</td>
      </tr>
  </tbody>
</table>
<p>Language Server backend 是 OSS 用戶會接觸的路線。serena 透過 LSP 抽象涵蓋 40+ 語言、實際能力依各語言 LSP 成熟度而定——Python / TypeScript / Go / Rust / Java / C# / Dart 等主流語言由 serena 內建 bootstrap 自動下載 server、冷門語言（如 Liquid / Pascal）需要使用者自己準備 server binary、無 server 的語言視同 fallback 到純文字工具。判讀訊號：跑 <code>activate_project</code> 後若 serena 沒在背景 spawn 對應 LSP、表示該語言走 fallback 路線、<code>find_referencing_symbols</code> 等型別敏感 tool 不可用。</p>
<p>對 Dart 而言：serena 啟動時 spawn <code>dart analysis_server</code>、跟 Flutter SDK 內附的同一隻。所以 serena 對 Dart 的能力等同 <code>dart analysis_server</code> 暴露的能力——比 tree-sitter 路線高一個量級。</p>
<h2 id="per-session-模型與-activate_project">Per-session 模型與 activate_project</h2>
<p>serena 的 LSP backend 是 <strong>per-session</strong> 的：</p>
<ul>
<li>沒有持久化 graph DB（不像 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> / <a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a> 把結果寫進 SQLite）</li>
<li>每個 session 啟動時要 <code>activate_project</code>、spawn 對應 language server、warm up index</li>
<li>Session 結束 server 也跟著 terminate，下次重來</li>
</ul>
<p><code>activate_project</code> 的角色是告訴 serena「這個 session 接下來要分析哪個 project root」，serena 才知道要 spawn 哪幾個 language server、index 哪個 workspace。一個 session 內可以切多次 project，但同時只 active 一個。</p>
<p>這個模型的取捨很清楚：</p>
<ul>
<li><strong>好處</strong>：永遠拿到當下最新狀態（不會有 stale index 問題）、不必管 watcher / debounce</li>
<li><strong>代價</strong>：每次 session warm-up 有秒級至分鐘級延遲（大專案 LSP indexing 慢）、跨 session 不能重用結果</li>
</ul>
<p>判讀訊號：第一次查詢回得慢、之後快——這是 LSP indexing warm-up。若每次查都慢、檢查 LSP 是否因記憶體不足重啟。</p>
<h2 id="symbol-level-atomic-edit-的價值">Symbol-level atomic edit 的價值</h2>
<p>serena 的 editing tool 都是 symbol-level：</p>
<ul>
<li><code>replace_symbol_body</code>：取代某個 function / method / class 的 body</li>
<li><code>insert_after_symbol</code> / <code>insert_before_symbol</code>：在指定 symbol 前後插入內容</li>
<li><code>safe_delete_symbol</code>：刪除 symbol 並檢查 reference</li>
<li><code>rename_symbol</code>：rename symbol、自動更新所有 reference（LS backend 限 symbol 範圍、JetBrains backend 額外支援 file / directory 層級重命名）</li>
</ul>
<p>對比 <code>Edit</code> tool 用「old_string / new_string」做 text-level patch：</p>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>text-level edit</th>
          <th>symbol-level edit</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>改 method body</td>
          <td>要 match 整個 body 含縮排與空白</td>
          <td>指定 method 名、給新 body</td>
      </tr>
      <tr>
          <td>Method body 內某行有特殊字元</td>
          <td>容易 escape 錯、match fail</td>
          <td>不受影響、agent 不處理 raw text</td>
      </tr>
      <tr>
          <td>同名 method 在多個 class</td>
          <td>要 match 含 class 名上下文</td>
          <td>用 <code>ClassName/methodName</code> 路徑唯一定位</td>
      </tr>
      <tr>
          <td>Rename 跨檔</td>
          <td>要全 repo grep + 逐檔 patch</td>
          <td>一次 call 完成 + LSP 保證 reference 全更新</td>
      </tr>
  </tbody>
</table>
<p>實務上的價值：<strong>type-sensitive refactor 的事故率大幅降低</strong>。改 method 不會手抖把 indentation 改錯、rename 不會漏改 reference。代價是 symbol 路徑必須寫成包含父層的完整形式（<code>ClassName/methodName</code>）。</p>
<p>判讀訊號：寫 <code>replace_symbol_body</code> 後若 LSP 報 syntax error、先 <code>get_diagnostics_for_file</code> 看具體錯在哪、別直接 retry 同個 patch。</p>
<h2 id="find_referencing_symbolslsp-路線的型別精確-caller-來源">find_referencing_symbols：LSP 路線的型別精確 caller 來源</h2>
<p>對 Dart / Swift / Kotlin 這類 tree-sitter 工具支援薄弱的語言，<code>find_referencing_symbols</code> 是少數能拿到「<strong>型別精確的 caller 清單</strong>」的 MCP tool。</p>
<p>實測對 Dart <code>Money.multiplyByRate</code>（某商業專案、<code>Money</code> 是 extension type）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">serena find_referencing_symbols → 4 個檔案、9 個 callsite
</span></span><span class="line"><span class="ln">2</span><span class="cl">codegraph callers              → 3 個 caller symbol（漏 3 個 callsite）
</span></span><span class="line"><span class="ln">3</span><span class="cl">cbm trace_call_path            → 0 callers（Dart 不在 hybrid resolution 名單）</span></span></code></pre></div><p>差距來源就是型別解析：<code>samplePrice.multiplyByRate(...)</code> 這種 receiver 是 local variable 的 callsite，要知道 <code>samplePrice</code> 的型別是 <code>Money</code> 才能 dispatch 到正確 method。LSP 走 <code>dart analysis_server</code> 拿到完整型別資訊，所以這層 dispatch 是精確的。</p>
<p>下一步路由：對照數字與 5 個實測實驗見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="30-mcp-tool-的分類">30+ MCP tool 的分類</h2>
<p>serena 的 tool 數量比 cbm / codegraph 都多、覆蓋更廣的工作流：</p>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>Tool</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>檢索</td>
          <td><code>find_symbol</code>、<code>get_symbols_overview</code>、<code>find_referencing_symbols</code>、<code>find_declaration</code>、<code>find_implementations</code>、<code>get_diagnostics_for_file</code></td>
      </tr>
      <tr>
          <td>編輯（symbol）</td>
          <td><code>replace_symbol_body</code>、<code>insert_after_symbol</code>、<code>insert_before_symbol</code>、<code>safe_delete_symbol</code>、<code>rename_symbol</code></td>
      </tr>
      <tr>
          <td>編輯（text）</td>
          <td><code>replace_content</code>、<code>search_for_pattern</code></td>
      </tr>
      <tr>
          <td>檔案 / 目錄</td>
          <td><code>list_dir</code>、<code>find_file</code>、<code>read_file</code>、<code>create_text_file</code></td>
      </tr>
      <tr>
          <td>執行</td>
          <td><code>execute_shell_command</code></td>
      </tr>
      <tr>
          <td>Memory</td>
          <td><code>write_memory</code>、<code>read_memory</code>、<code>list_memories</code>、<code>delete_memory</code>、<code>rename_memory</code>、<code>edit_memory</code></td>
      </tr>
      <tr>
          <td>Project</td>
          <td><code>activate_project</code>、<code>get_current_config</code>、<code>onboarding</code>、<code>initial_instructions</code></td>
      </tr>
      <tr>
          <td>Debug</td>
          <td>（僅 JetBrains backend）breakpoint、variable inspection、expression eval</td>
      </tr>
  </tbody>
</table>
<p>幾個值得單獨展開的類別：</p>
<p><strong>檢索類</strong>是 serena 跟 LSP 黏最緊的入口——<code>find_symbol</code> / <code>find_declaration</code> / <code>find_implementations</code> 走 LSP 的 textDocument 命令、<code>find_referencing_symbols</code> 是 LSP <code>references</code> 的 wrapper。這層是 serena 不可替代的核心、所有需要型別精確的查詢都從這走。</p>
<p><strong><code>get_diagnostics_for_file</code></strong> 是把 LSP 的編譯診斷直接暴露給 agent。改完 code 不必跑 build 就能知道有沒有 type error / unused import / missing await。對 type-sensitive refactor 是必備。</p>
<p><strong>Symbol-level edit vs text-level edit 的選用</strong>：symbol-level（<code>replace_symbol_body</code> / <code>insert_after_symbol</code> / <code>safe_delete_symbol</code> / <code>rename_symbol</code>）對「有明確 symbol 邊界的修改」最穩、不會踩到 indentation 或 escape 問題；text-level（<code>replace_content</code> / <code>search_for_pattern</code>）保留給「跨 symbol 邊界、或非 code 內容」的場合（如改 markdown、config、log 字串）。判讀訊號：要動的內容能不能用「ClassName/methodName」這種 symbol path 定位？能就走 symbol-level、不能就 text-level。</p>
<p><strong><code>execute_shell_command</code></strong> 是 LSP-only 工具裡的「逃生門」——LSP 本身不執行命令、但實務上 agent 需要跑 test / build / git status / 任意 CLI 工具來驗證自己的修改。這條等於把 LSP-based 工具補成「能 query 又能執行」的完整 workflow 工具。安全考量：因為它能跑任意 shell command、Claude Code 對 serena 的 trust level 要跟 Bash tool 對齊看待、不要假設它「只是讀取工具」。</p>
<p><strong>Memory system</strong> 採用「跨 session 的 markdown 筆記檔」形式、屬於自由格式存儲。用途接近 agent 的本地長期記憶——存「這個專案的 setup 注意事項」、「上次 refactor 的決策紀錄」、「常用的 codebase pattern」。跟 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 的 <code>manage_adr</code>（結構化 ADR）走相反取向：serena 把 schema 留給使用者自定、manage_adr 給定 ADR 欄位結構。</p>
<p><strong>Project 類</strong>（<code>activate_project</code> / <code>get_current_config</code> / <code>onboarding</code> / <code>initial_instructions</code>）是 serena 對「agent 第一次接觸新專案要先讀什麼」的明確協議。<code>onboarding</code> 讓 agent 主動 read 專案 onboarding doc、<code>initial_instructions</code> 給 agent 一份 serena 自己的使用手冊、<code>activate_project</code> 切 project root、<code>get_current_config</code> 暴露當前 session 的配置給 agent debug。這層降低盲目探索成本、是把 serena 從「LSP wrapper」抬升到「agent-first」的關鍵。</p>
<h2 id="per-session-與持久化-graph-的搭配問題">Per-session 與持久化 graph 的搭配問題</h2>
<p>serena 的 per-session 模型在「<strong>單純查 caller / refactor</strong>」工作流很合適，但對「<strong>自然語言搜尋 / 跨 session 累積 graph context</strong>」就不夠。</p>
<p>實際差距：</p>
<ul>
<li>想用「金額顯示相關」這種概念性 query 找 symbol → serena 沒有 BM25 / 11-signal scoring、只有 <code>search_for_pattern</code>（regex / literal）跟 <code>find_symbol</code>（exact name match）</li>
<li>想跨 session 累積「這個 codebase 有哪些 module」的整體 inventory → serena 每次重 index、沒有持久化的 graph 可查</li>
<li>想做跨 service HTTP_CALLS 鏈接 → serena 沒有這層</li>
</ul>
<p>判讀訊號：搜尋需求若是「我知道某個 symbol 的精確名稱、要找它的 references」就用 serena；若是「我不知道精確名稱、用概念找」要配合 cbm。</p>
<h2 id="安裝行為">安裝行為</h2>
<p>serena 在 Claude Code 是 plugin 形式：在 plugin marketplace enable 即可，不需要單獨 <code>npm i</code>。Plugin 啟動時 serena 會 spawn LSP，第一次 activate 某個 project 時 indexing 完成才能跑 query。</p>
<p>跟 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> / <a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a> 的差異：</p>
<ul>
<li><strong>不寫 PreToolUse hook</strong>、不攔截既有 grep / glob 行為</li>
<li><strong>不在 <code>~/.claude.json</code> 直接加 mcpServers</strong>（plugin 機制管理）</li>
<li><strong>每個 project 要顯式 activate</strong>——第一次 session 進新 project 時 agent 要主動跑 <code>activate_project</code> 或在 plugin config 預設 project root</li>
</ul>
<p>要注意的點：</p>
<p><strong>Language server 缺失時的失敗模式</strong>。對冷門語言（如 Liquid / Pascal）若環境沒裝 language server、<code>activate_project</code> 會回失敗但不會主動裝。需要使用者自己準備 server binary。Dart / TS / Python / Go / Rust 等主流語言 serena 會 bootstrap 處理。</p>
<p><strong>JetBrains backend 是付費</strong>。OSS 用戶只能用 LS backend、得不到 debug 整合那組能力。</p>
<h2 id="適用--不適用情境的判讀">適用 / 不適用情境的判讀</h2>
<p><strong>適用情境</strong>：</p>
<ul>
<li>主力語言有成熟 LSP（Dart / TS / Python / Go / Rust / Java / C# 等）</li>
<li>型別敏感的 refactor 場景（rename / extract method / 跨檔 reference 更新）</li>
<li>要編譯 diagnostic 即時反饋（取代 build / test cycle 的部分功能）</li>
<li>Symbol-level atomic edit 的可靠性比 graph 持久化重要</li>
</ul>
<p><strong>不適用情境</strong>：</p>
<ul>
<li>主力語言 LSP 不成熟或不存在（serena 沒得借力）</li>
<li>需要概念性 / 自然語言搜尋（用 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 的 11-signal scoring）</li>
<li>需要跨 session 累積的 graph context（serena per-session、不持久化）</li>
<li>需要跨 service HTTP/RPC 鏈接（serena 沒這層）</li>
</ul>
<p><strong>搭配建議</strong>：serena 是「<strong>型別精確 + 編輯出口</strong>」的角色。在它擅長的語言上做 caller 追蹤 / refactor、把概念性搜尋讓給 cbm、把日常結構查詢讓給 codegraph。三者怎麼分工見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="結論">結論</h2>
<p>serena 的核心價值在三件事：<strong>直接借 LSP 拿型別精確的 reference</strong>、<strong>symbol-level atomic edit 的可靠性</strong>、<strong>編譯 diagnostic 即時整合</strong>。前兩件對任何成熟 LSP 語言都成立，第三件對「改完 code 想立刻驗 type error」的工作流特別重要。</p>
<p>它的能力上限取決於「<strong>目標語言 LSP 成熟度</strong>」——LSP 強的語言上 serena 是強工具、LSP 弱的語言上 serena 也跟著弱。它的能力下限取決於「<strong>持久化 graph 與自然語言搜尋</strong>」這兩層空白——這兩層要靠別的 MCP 補齊。</p>
]]></content:encoded></item><item><title>三 MCP 工作流與 Dart 實測：cbm / codegraph / serena 的職責分工與三刀流</title><link>https://tarrragon.github.io/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/</guid><description>&lt;h2 id="為什麼需要對照為什麼選-dart">為什麼需要對照、為什麼選 Dart&lt;/h2>
&lt;p>評估 code intelligence MCP 不能只看 README benchmark：每個工具的 benchmark 都選自己擅長的 codebase 跟語言，readme 數字只能參考、不能直接套到自家 stack。&lt;/p>
&lt;p>這次選一個 Dart 商業專案做對照場域有兩個理由：&lt;/p>
&lt;ul>
&lt;li>Dart 是三個工具的「中間地帶」——&lt;a href="https://tarrragon.github.io/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm&lt;/a> 不在 hybrid resolution 名單、&lt;a href="https://tarrragon.github.io/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph&lt;/a> 列為 full support、&lt;a href="https://tarrragon.github.io/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena&lt;/a> 借 &lt;code>dart analysis_server&lt;/code> 有完整 LSP。三條技術路線在同一語言上的能力差距會被最大化。&lt;/li>
&lt;li>Dart 大量用 extension type、generic、factory pattern，這些是 type-inferred dispatch 的高發場景，能逼出每個工具的真實精度差。&lt;/li>
&lt;/ul>
&lt;p>在 Go / TypeScript 上跑同樣對照，結論會反過來——cbm 的 hybrid resolution 在那裡會接近 LSP 精度，三刀流的必要性會降低。所以這篇結論限定「LSP 成熟但 cbm 不在 hybrid resolution 名單」的語言。&lt;/p>
&lt;h2 id="本質差異tree-sitter-syntactic-vs-lsp-type-aware">本質差異：tree-sitter syntactic vs LSP type-aware&lt;/h2>
&lt;p>三個工具在 Dart 上的能力差距，根源是兩條技術路線的本質落差：&lt;/p>
&lt;p>&lt;strong>tree-sitter syntactic&lt;/strong>：只看語法結構。看到 &lt;code>a.b()&lt;/code> 知道有個 method call、不知道 &lt;code>a&lt;/code> 是什麼型別、不知道 &lt;code>b()&lt;/code> 連到哪個 declaration。對 receiver 是 literal 或顯式型別宣告的 callsite 可以解、對 local variable / parameter / 推斷型別的 callsite 會漏。&lt;/p>
&lt;p>&lt;strong>LSP type-aware&lt;/strong>：走 language server 內建的型別推斷引擎。跟 IDE 用同一套後端、能解出 &lt;code>a&lt;/code> 的真實型別、再從 type declaration 找到對應的 method。所以 reference 是型別精確的。&lt;/p>
&lt;p>cbm 的 hybrid type resolution（限 Go / C / C++ / TS / JS）是把 LSP 的型別解析算法 clean-room 重寫進 binary、所以那幾個語言上 cbm 等於有 LSP 級精度但沒 LSP 依賴。Dart 沒得到這個待遇，所以 cbm 在 Dart 上只剩純 syntactic 結構抽取。&lt;/p>
&lt;p>判讀訊號：看一個工具對某語言的能力強弱，問「&lt;strong>它在這語言上做型別解析嗎？&lt;/strong>」——做的話接近 LSP，不做的話只是個結構抽取器。&lt;/p>
&lt;p>這個 framework 建立後、下節展開到 9 個維度的設計對照。&lt;/p>
&lt;h2 id="三個工具的設計差異對照">三個工具的設計差異對照&lt;/h2>
&lt;p>三個工具雖然都是「code intelligence MCP」，設計取向互補：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>&lt;a href="https://tarrragon.github.io/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm&lt;/a>&lt;/th>
 &lt;th>&lt;a href="https://tarrragon.github.io/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph&lt;/a>&lt;/th>
 &lt;th>&lt;a href="https://tarrragon.github.io/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena&lt;/a>&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>解析後端&lt;/td>
 &lt;td>tree-sitter + 自寫 type resolver&lt;/td>
 &lt;td>tree-sitter + per-language query&lt;/td>
 &lt;td>LSP（per-language server）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>語言覆蓋&lt;/td>
 &lt;td>155（vendored grammar）&lt;/td>
 &lt;td>19+（每語言寫 query）&lt;/td>
 &lt;td>視 LSP 支援度（40+）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>持久化&lt;/td>
 &lt;td>SQLite + WAL（可 zstd 匯出為 team artifact）&lt;/td>
 &lt;td>SQLite + FTS5&lt;/td>
 &lt;td>per-session、不持久化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sync 機制&lt;/td>
 &lt;td>背景 git polling&lt;/td>
 &lt;td>native OS file watcher 2s debounce&lt;/td>
 &lt;td>session warm-up&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Type resolution&lt;/td>
 &lt;td>Go / C / C++ / TS / JS 有 hybrid、其他語言只有 syntactic&lt;/td>
 &lt;td>tree-sitter syntactic 為主、聲稱對部分 dynamic dispatch 有解&lt;/td>
 &lt;td>完整 LSP 型別解析&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨 service&lt;/td>
 &lt;td>first-class HTTP_CALLS edge + channel&lt;/td>
 &lt;td>route definition 識別、不做 client URL → server route 比對&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>概念性自然語言搜尋&lt;/td>
 &lt;td>11-signal scoring + camel split&lt;/td>
 &lt;td>symbol pattern match&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Symbol-level 編輯&lt;/td>
 &lt;td>無（純讀）&lt;/td>
 &lt;td>無（純讀）&lt;/td>
 &lt;td>完整（replace_symbol_body / rename）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>編譯 diagnostic&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>有（&lt;code>get_diagnostics_for_file&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的判讀重點：&lt;strong>三者擅長的事不重疊&lt;/strong>。cbm 強在「找東西」、codegraph 強在「日常 call graph + auto-sync」、serena 強在「型別精確 reference + 編輯出口」。&lt;/p></description><content:encoded><![CDATA[<h2 id="為什麼需要對照為什麼選-dart">為什麼需要對照、為什麼選 Dart</h2>
<p>評估 code intelligence MCP 不能只看 README benchmark：每個工具的 benchmark 都選自己擅長的 codebase 跟語言，readme 數字只能參考、不能直接套到自家 stack。</p>
<p>這次選一個 Dart 商業專案做對照場域有兩個理由：</p>
<ul>
<li>Dart 是三個工具的「中間地帶」——<a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 不在 hybrid resolution 名單、<a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a> 列為 full support、<a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a> 借 <code>dart analysis_server</code> 有完整 LSP。三條技術路線在同一語言上的能力差距會被最大化。</li>
<li>Dart 大量用 extension type、generic、factory pattern，這些是 type-inferred dispatch 的高發場景，能逼出每個工具的真實精度差。</li>
</ul>
<p>在 Go / TypeScript 上跑同樣對照，結論會反過來——cbm 的 hybrid resolution 在那裡會接近 LSP 精度，三刀流的必要性會降低。所以這篇結論限定「LSP 成熟但 cbm 不在 hybrid resolution 名單」的語言。</p>
<h2 id="本質差異tree-sitter-syntactic-vs-lsp-type-aware">本質差異：tree-sitter syntactic vs LSP type-aware</h2>
<p>三個工具在 Dart 上的能力差距，根源是兩條技術路線的本質落差：</p>
<p><strong>tree-sitter syntactic</strong>：只看語法結構。看到 <code>a.b()</code> 知道有個 method call、不知道 <code>a</code> 是什麼型別、不知道 <code>b()</code> 連到哪個 declaration。對 receiver 是 literal 或顯式型別宣告的 callsite 可以解、對 local variable / parameter / 推斷型別的 callsite 會漏。</p>
<p><strong>LSP type-aware</strong>：走 language server 內建的型別推斷引擎。跟 IDE 用同一套後端、能解出 <code>a</code> 的真實型別、再從 type declaration 找到對應的 method。所以 reference 是型別精確的。</p>
<p>cbm 的 hybrid type resolution（限 Go / C / C++ / TS / JS）是把 LSP 的型別解析算法 clean-room 重寫進 binary、所以那幾個語言上 cbm 等於有 LSP 級精度但沒 LSP 依賴。Dart 沒得到這個待遇，所以 cbm 在 Dart 上只剩純 syntactic 結構抽取。</p>
<p>判讀訊號：看一個工具對某語言的能力強弱，問「<strong>它在這語言上做型別解析嗎？</strong>」——做的話接近 LSP，不做的話只是個結構抽取器。</p>
<p>這個 framework 建立後、下節展開到 9 個維度的設計對照。</p>
<h2 id="三個工具的設計差異對照">三個工具的設計差異對照</h2>
<p>三個工具雖然都是「code intelligence MCP」，設計取向互補：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th><a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a></th>
          <th><a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a></th>
          <th><a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>解析後端</td>
          <td>tree-sitter + 自寫 type resolver</td>
          <td>tree-sitter + per-language query</td>
          <td>LSP（per-language server）</td>
      </tr>
      <tr>
          <td>語言覆蓋</td>
          <td>155（vendored grammar）</td>
          <td>19+（每語言寫 query）</td>
          <td>視 LSP 支援度（40+）</td>
      </tr>
      <tr>
          <td>持久化</td>
          <td>SQLite + WAL（可 zstd 匯出為 team artifact）</td>
          <td>SQLite + FTS5</td>
          <td>per-session、不持久化</td>
      </tr>
      <tr>
          <td>Sync 機制</td>
          <td>背景 git polling</td>
          <td>native OS file watcher 2s debounce</td>
          <td>session warm-up</td>
      </tr>
      <tr>
          <td>Type resolution</td>
          <td>Go / C / C++ / TS / JS 有 hybrid、其他語言只有 syntactic</td>
          <td>tree-sitter syntactic 為主、聲稱對部分 dynamic dispatch 有解</td>
          <td>完整 LSP 型別解析</td>
      </tr>
      <tr>
          <td>跨 service</td>
          <td>first-class HTTP_CALLS edge + channel</td>
          <td>route definition 識別、不做 client URL → server route 比對</td>
          <td>無</td>
      </tr>
      <tr>
          <td>概念性自然語言搜尋</td>
          <td>11-signal scoring + camel split</td>
          <td>symbol pattern match</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Symbol-level 編輯</td>
          <td>無（純讀）</td>
          <td>無（純讀）</td>
          <td>完整（replace_symbol_body / rename）</td>
      </tr>
      <tr>
          <td>編譯 diagnostic</td>
          <td>無</td>
          <td>無</td>
          <td>有（<code>get_diagnostics_for_file</code>）</td>
      </tr>
  </tbody>
</table>
<p>這張表的判讀重點：<strong>三者擅長的事不重疊</strong>。cbm 強在「找東西」、codegraph 強在「日常 call graph + auto-sync」、serena 強在「型別精確 reference + 編輯出口」。</p>
<p>對照表的維度很多、但實務上踩到事故的多半集中在三個維度，把它們各自展開：</p>
<p><strong>Type resolution 決定 caller 數字的可信度</strong>。Dart / Swift / Kotlin 這類「LSP 完整、但 cbm 走純 syntactic 路線」的語言上、tree-sitter 工具回的 caller 數字是 lower bound（實際值通常更高）。<code>samplePrice.multiplyByRate(...)</code> 這種 type-inferred receiver 是這層差距的主戰場。判讀訊號：對熱門 class 跑同一 query、若 tree-sitter 工具 caller 數比 LSP 工具低過半、type-inferred dispatch 在這語言是主流模式、tree-sitter 結果只能當 starting point。</p>
<p><strong>Sync 機制決定「邊改邊問」是否可用</strong>。codegraph 的 native OS file watcher + 2s debounce 最貼近 IDE、cbm 的背景 git polling 有秒級至分級延遲、serena 的 session warm-up 是「啟動時等一次、之後即時」。事故型態：在 codegraph 改完檔案立刻問 caller 多半 OK、在 cbm 立刻問會拿到 stale graph。判讀訊號：問完 query 對結果存疑時、先檢查工具的 sync 狀態（cbm 跑 <code>index_status</code>、codegraph 跑 <code>codegraph_status</code>、serena 直接重 query）。</p>
<p><strong>持久化模式決定跨 session 的累積成本</strong>。cbm / codegraph 寫 SQLite、跨 session 重用；serena per-session、每次 spawn LSP warm up。對「短任務反覆 ad-hoc 查詢」cbm / codegraph 邊際成本更低、對「會做 symbol-level edit 跟 diagnostic」serena 的 per-session warm up 是必要 cost。判讀訊號：第一次 query 慢、之後快——LSP indexing warm up、正常；每次 query 都慢——LSP 可能因記憶體不足重啟、需排查。</p>
<p>下面的實測是這張表在 Dart 上的數字驗證。</p>
<h2 id="dart-實測對照同題不同工具">Dart 實測對照：同題不同工具</h2>
<p>實測環境：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">專案類型：Dart 商業專案（POS / 零售領域）
</span></span><span class="line"><span class="ln">2</span><span class="cl">Branch：refactor/money-value-object
</span></span><span class="line"><span class="ln">3</span><span class="cl">索引規模：
</span></span><span class="line"><span class="ln">4</span><span class="cl">  cbm:        3,038 nodes,  6,355 edges（Dart 沒 CALLS edge）
</span></span><span class="line"><span class="ln">5</span><span class="cl">  codegraph:  6,244 nodes, 12,223 edges（含 CALLS edge）
</span></span><span class="line"><span class="ln">6</span><span class="cl">  serena:     per-session、無索引統計</span></span></code></pre></div><p>cbm 跟 codegraph 的 nodes 約 2x、edges 約 2x，差異關鍵不在 nodes（cbm 缺 import / enum_member 等次要 node）、而在「<strong>有沒有 CALLS edge</strong>」——這直接決定 caller / impact 類查詢能不能用。</p>
<blockquote>
<p><strong>實測數字的適用範圍</strong>：本節的所有 callsite / caller / impact 數字（含查詢 1-5）都是<strong>單一 Dart 商業專案的內部 baseline</strong>、不保證跨專案重現。Dart 上 type-inferred receiver 比例高的專案會放大三個工具的差距、比例低的專案會縮小差距。換到 Swift / Kotlin / Rust 等語言上、絕對數字會不同但「tree-sitter syntactic vs LSP type-aware」的差距方向通常一致。讀者要套用結論時、先在自家 repo 跑一遍同題對照、看自己的數字落差。</p></blockquote>
<h3 id="查詢-1誰呼叫了-moneymultiplybyrate">查詢 1：誰呼叫了 <code>Money.multiplyByRate</code></h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cbm</td>
          <td>0（hybrid resolution 不含 Dart）</td>
      </tr>
      <tr>
          <td>codegraph</td>
          <td>3 caller symbols（4 個檔案中漏 product.dart 的 3 個 callsite）</td>
      </tr>
      <tr>
          <td>serena</td>
          <td>4 個檔案、9 個 callsite</td>
      </tr>
  </tbody>
</table>
<p>codegraph 漏掉的 3 個 callsite 共同特徵：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// lib/data/models/product/product.dart
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">Money</span> <span class="n">samplePrice</span> <span class="o">=</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">samplePrice</span><span class="p">.</span><span class="n">multiplyByRate</span><span class="p">(</span><span class="n">Decimal</span><span class="p">.</span><span class="n">parse</span><span class="p">(</span><span class="s1">&#39;0.9&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">samplePrice</span><span class="p">.</span><span class="n">multiplyByRate</span><span class="p">(</span><span class="n">Decimal</span><span class="p">.</span><span class="n">parse</span><span class="p">(</span><span class="s1">&#39;0.6&#39;</span><span class="p">));</span></span></span></code></pre></div><p><code>samplePrice</code> 是 local variable、要型別推斷才知道是 <code>Money</code>。tree-sitter 看到的只是 <code>&lt;identifier&gt;.multiplyByRate(...)</code>、解不出 dispatch target。</p>
<p>serena 透過 <code>dart analysis_server</code> 拿到完整型別資訊、知道 <code>samplePrice</code> 宣告是 <code>Money</code>、能精確 dispatch。</p>
<h3 id="查詢-2誰呼叫了-localesymbolconfigformatamount">查詢 2：誰呼叫了 <code>LocaleSymbolConfig.formatAmount</code></h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cbm</td>
          <td>0</td>
      </tr>
      <tr>
          <td>codegraph</td>
          <td>30（<code>--limit 30</code>，預設 20 截斷）</td>
      </tr>
      <tr>
          <td>serena</td>
          <td>5 個檔案、21 個 callsite</td>
      </tr>
  </tbody>
</table>
<p>這題 codegraph 跟 serena 的差距比較小——<code>formatAmount</code> 在很多地方是用顯式 receiver 呼叫（如 <code>LocaleSymbolConfig.cny.formatAmount(...)</code>），tree-sitter 對顯式 receiver 解得到。</p>
<p>兩邊數字的差異主因是 <strong>caller symbol 數 vs callsite 數</strong>的計數單位差：</p>
<ul>
<li>codegraph 算 caller symbol（一個 method 內呼叫幾次都算 1）</li>
<li>serena 算 callsite</li>
</ul>
<p>寫實測 baseline 時這個單位要寫死、否則 3 vs 9 看起來像精度差距、實際上一部分只是計數規則不同。</p>
<h3 id="查詢-3money-符號的內部結構">查詢 3：<code>Money</code> 符號的內部結構</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cbm</td>
          <td>只認得 File / Module、extension type 子結構抽不到</td>
      </tr>
      <tr>
          <td>codegraph</td>
          <td>認得 class 但 extension type 支援度未驗證</td>
      </tr>
      <tr>
          <td>serena</td>
          <td>Namespace kind、3 個 Field、16 個 Method、3 個 Property 都附行號</td>
      </tr>
  </tbody>
</table>
<p>Dart <code>extension type</code> 是相對新的特性、tree-sitter grammar 對它的支援深度不一。serena 走 LSP 直接拿到 <code>dart analysis_server</code> 對 extension type 的完整解析。</p>
<p>對需要「列出某 class / extension 所有 member」的場景、serena 是 Dart 上 LSP 級精度最可信的選項（其他 MCP 在 Dart extension type 上做不到完整 member 列舉）。</p>
<h3 id="查詢-4概念性搜尋金額顯示相關函式">查詢 4：概念性搜尋「金額顯示」相關函式</h3>
<p>對「我不知道精確名稱、只記得功能類別」這種 query：</p>
<table>
  <thead>
      <tr>
          <th>名次</th>
          <th>cbm（11-signal scoring）</th>
          <th>codegraph_search</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1-4</td>
          <td>4 個 <code>formatAmount</code> 實作（兩邊一致）</td>
          <td>4 個 <code>formatAmount</code> 實作（兩邊一致）</td>
      </tr>
      <tr>
          <td>5</td>
          <td><code>externalDisplayMain</code></td>
          <td><code>displayCategories</code></td>
      </tr>
      <tr>
          <td>6</td>
          <td><code>connectExternalDisplay</code></td>
          <td><code>displayTags</code></td>
      </tr>
      <tr>
          <td>7</td>
          <td><code>_buildQuantityDisplay</code></td>
          <td><code>displayName</code></td>
      </tr>
      <tr>
          <td>8</td>
          <td><code>connectExternalDisplay</code>（另一個）</td>
          <td><code>displayCover</code></td>
      </tr>
      <tr>
          <td>9</td>
          <td><code>getBalanceDisplay</code></td>
          <td><code>displayName</code>（另一個）</td>
      </tr>
      <tr>
          <td>10</td>
          <td><code>_buildPriceDisplay</code></td>
          <td><code>displayName</code>（另一個）</td>
      </tr>
  </tbody>
</table>
<p>前 4 名兩邊都抓到核心 <code>formatAmount</code> 實作，第 5 名後分歧明顯：</p>
<ul>
<li>cbm 補進的 <code>getBalanceDisplay</code> / <code>_buildPriceDisplay</code> / <code>connectExternalDisplay</code> 都跟「金額顯示」概念相關（顯示金額 / 顯示餘額 / 外接顯示器）</li>
<li>codegraph 補進的 <code>displayName</code> / <code>displayTags</code> 只是符號名含 &ldquo;display&rdquo; 子字串、跟金額無關</li>
</ul>
<p>差異來源是 cbm 的 11-signal scoring + <code>cbm_camel_split</code> 對 camelCase 切詞做語意切分（<code>getMoneyField</code> → <code>get</code> + <code>money</code> + <code>field</code>）。codegraph 的 search 是 symbol pattern match、沒對自然語言 query 做語意處理。</p>
<p>這題的判讀很關鍵——<strong>cbm 在「找東西」的角色不能被 codegraph 取代</strong>。即使 codegraph 在 Dart 上有可用的 call graph、它的 search 仍然贏不了 cbm 的概念性 query。</p>
<h3 id="查詢-5money-的-impact-範圍--cross-symbol-trace">查詢 5：<code>Money</code> 的 impact 範圍 / cross-symbol trace</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cbm</td>
          <td>無 impact 概念、回不出</td>
      </tr>
      <tr>
          <td>codegraph</td>
          <td>5 個 affected symbol、全在 MoneyFieldRenderer 一檔</td>
      </tr>
      <tr>
          <td>serena</td>
          <td>走 <code>find_referencing_symbols</code> 跨 4 個檔案找完整 reference</td>
      </tr>
  </tbody>
</table>
<p>Money 是該專案大量使用的 value object、實際被使用的檔案橫跨 receipt_data 實作、settlement、cart_item、order_dto 等業務模組。codegraph 只回 1 個檔案 5 個 symbol、嚴重低估 blast radius。</p>
<p>漏掉的原因跟查詢 1 同源——<code>something.multiplyByRate(...)</code>、<code>Money</code> 在 factory 內被隱式構造這些都不在 tree-sitter 能解的範圍。MoneyFieldRenderer 之所以被抓到、是因為它的 field 顯式宣告為 <code>Money</code>，這是少數 tree-sitter syntactic 能抓的場合。</p>
<p>對 cross-symbol trace：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">codegraph_trace(from: &#34;Money/multiplyByRate&#34;, to: &#34;ProductSpecification&#34;)
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ &#34;No direct path&#34;、建議跳到 dynamic dispatch</span></span></code></pre></div><p>graph 上根本沒這條 edge（漏掉的 product.dart 那 3 個 callsite 正是這條 trace 的關鍵跳）、所以 trace 直接失敗。</p>
<p>判讀訊號：<strong>重要 refactor 不能單看 codegraph 的 impact 數字</strong>。要走 serena <code>find_referencing_symbols</code> 二次確認；對 cbm 不在 hybrid resolution 名單的語言、blast radius 必須用 LSP 工具驗證。</p>
<h2 id="三刀流工作流">三刀流工作流</h2>
<p>實測結論：cbm / codegraph / serena 各有不可替代的角色，組合使用才是 Dart 主力專案的合理 stack。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">找東西（不知道精確名稱、概念性 query）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  → cbm search_graph(query=&#34;...&#34;)           ← 11-signal scoring 對概念性 query 最強
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">知道精確名稱、找 caller / callee
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  → codegraph_callers / codegraph_callees   ← auto-sync 2s 反應最快
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  發現結果可能不完整（type-inferred dispatch 多的場合）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  → serena find_referencing_symbols         ← LSP 完整精度補位
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">重要 refactor 確認 blast radius
</span></span><span class="line"><span class="ln">11</span><span class="cl">  → serena find_referencing_symbols         ← 不能單靠 codegraph_impact
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">符號層級的編輯
</span></span><span class="line"><span class="ln">14</span><span class="cl">  → serena replace_symbol_body / rename     ← symbol-level atomic edit
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">跨 service HTTP/RPC 鏈接（若 monorepo 含 client + server）
</span></span><span class="line"><span class="ln">17</span><span class="cl">  → cbm HTTP_CALLS edge                     ← 三個工具中只有 cbm 有這層</span></span></code></pre></div><p>幾個關鍵的判讀原則：</p>
<p><strong>入口跟出口要分清楚</strong>：cbm 是「廣度索引 + 模糊搜尋」的入口、拿到 qualified name 後轉給 serena 做精確查詢與編輯。codegraph 補在中間、做日常結構查詢。</p>
<p><strong>重要 refactor 必走 serena 補位</strong>：codegraph 的 caller / impact 在 Dart 上系統性偏低、不能單看數字判斷影響範圍。決定 rename 或大幅修改 method 之前、用 serena 跑一次 <code>find_referencing_symbols</code> 對齊。</p>
<p><strong>Hook 不要打架</strong>：cbm 會寫 PreToolUse hook 攔截 Grep / Glob / Read / Search（README 描述只擋前兩者、實裝版本含 Read / Search）、codegraph / serena 都不寫 hook。同時用三個工具時、注意 cbm hook 是否誤判把正常的 markdown grep 也擋掉（實測有 false positive）。</p>
<h2 id="對其他語言-stack-怎麼變化">對其他語言 stack 怎麼變化</h2>
<p>這個三刀流結論限定 Dart。不同語言 stack 的真實壓力不一樣、推薦組合也跟著變——把幾個常見 stack 各自展開。</p>
<h3 id="go--typescript--c--c-主力">Go / TypeScript / C / C++ 主力</h3>
<p>這層是 cbm 的甜蜜點：hybrid type resolution 涵蓋這四個語族、CALLS edge 抽得到、cbm 的 caller / blast radius 精度接近 LSP。實務影響是「cbm 在 Dart 上需要 codegraph + serena 補位」的場景大幅縮小——cbm 自己就能處理 caller / impact、加上它原本就強的 11-signal 概念搜尋跟跨 service HTTP_CALLS，等於一個工具撐住「找東西」「caller / impact」「cross-service」三層。</p>
<p>serena 在這個 stack 仍是 symbol-level edit 跟 compile diagnostic 的關鍵來源——cbm 純讀、沒 rename / replace_symbol_body、沒 LSP 診斷整合。所以合理組合是「cbm + serena 雙刀流」、codegraph 的角色被 cbm 取代掉。判讀訊號：在自家 repo 跑 cbm <code>trace_call_path</code> 對 5 個熱門 class、若 caller 數跟 serena 的 <code>find_referencing_symbols</code> 對得上、codegraph 確實可以省下。</p>
<h3 id="swift--kotlin--rust-主力">Swift / Kotlin / Rust 主力</h3>
<p>這層跟 Dart 場景結構接近：serena 透過 sourcekit-lsp / kotlin-language-server / rust-analyzer 能拿到完整型別解析、cbm 不在 hybrid resolution 名單只剩純 syntactic。所以「三刀流」的論證仍適用。</p>
<p>但 codegraph 在這三個語言的 query 品質要實測——19+ 列表內這幾個都列為 supported、實際解析深度因語言成熟度而異。Swift 特別容易踩坑的點是 Objective-C interop（dispatch table 跨語言）跟 protocol extension 的型別推斷、Kotlin 則是 reified generics 跟 inline function、Rust 是 trait method 跟 macro 展開後的 callsite。判讀訊號：對自家專案最常用的 dispatch pattern 寫一個 minimal example、跑 codegraph callers、看抓不抓得到。</p>
<h3 id="python-主力">Python 主力</h3>
<p>三個工具的 Python 支援都成熟、但著力點不同：cbm 對 Python 有完整 hybrid resolution、codegraph 對 Python 是核心支援語言之一（VS Code benchmark 在它的 7 codebase 列表內）、serena 透過 pyright / pylsp 拿型別資訊。</p>
<p>Python 的特殊壓力是 dynamic dispatch（duck typing / monkey patching / metaclass / <strong>getattr</strong>）——這層任何 static 工具都會漏。判讀訊號：對自家 codebase 跑「找 X class 的所有 method 呼叫」、若大量真實 callsite 在 type annotation 缺失的位置、所有工具都只能給 lower bound。實務組合多半雙刀（codegraph + serena）夠用、cbm 對 Python 的不可替代價值在 cross-service HTTP_CALLS（Django / FastAPI 跨 service 場景）。</p>
<h3 id="冷門語言--dslliquid--pascal--svelte-template-等">冷門語言 / DSL（Liquid / Pascal / Svelte template 等）</h3>
<p>這層 serena 多半沒 LSP 可借（除非自備 server）、cbm 純 syntactic（hybrid 名單外）、codegraph 是少數仍有 query 的工具——但 query 品質要看 codegraph 對該語言投入多深、Pascal / Delphi / Liquid 這類列表末段的支援度可能只到 symbol 抽取、callsite 不一定有。</p>
<p>實務上對這層語言、退回 <code>grep + codegraph</code> 比強推三刀流合理——caller / impact 用 codegraph 試、不夠就 grep 補、別期待 LSP 級精度。判讀訊號：若 codegraph status 顯示 indexed file 多但 edges 數明顯偏低（&lt; 1 條 edge per file）、call graph 多半沒抽起來、視同純 syntactic 工具用。</p>
<h3 id="共通的評估方法">共通的評估方法</h3>
<p>無論哪個 stack、第一次裝 MCP 前在自家 repo 跑「找重要 class / function 的所有 caller」這個基準題、把不同工具的數字並列比較、再決定組合。README benchmark 是行銷數字、自家 stack 跑出的數字才是真實 baseline。</p>
<h2 id="評估新-mcp-工具的-checklist">評估新 MCP 工具的 checklist</h2>
<p>從這次踩三個（含一個跳過實裝的 GitNexus）的經驗回推、未來評估新 code intelligence MCP 要先確認：</p>
<p><strong>License</strong>：商業專案要 MIT / Apache 2.0 / BSD。PolyForm Noncommercial 之類限制商業使用的 license 直接刷掉。這條最便宜、最早做、最少人記得做。</p>
<p><strong>目標語言的 call graph 支援</strong>：README 寫「full support」要實測。tree-sitter wrapper 通常只到「結構抽得到」、沒到「call edge 抽得到」。同樣是「有 CALLS edge」、有 type-inferred dispatch 的 syntactic 工具跟有完整 LSP 的差距可能 2-3x callsite 數。</p>
<p><strong>MCP tool 數量不等於能力</strong>：14 個 tool 不一定贏過 10 個。看 caller / impact / find_referencing_symbols 這類核心功能有沒有、品質好不好、勝過 tool 多寡。</p>
<p><strong>是否會自動改 <code>~/.claude/</code> 設定</strong>：大多會。先看 install script 動了哪些檔案、能不能還原、uninstall 是否徹底（cbm uninstall 不清 hook 是踩過的坑）。</p>
<p><strong>是否有 CLI 模式</strong>：有的話本 session 就能實測、不必等 Claude Code 重啟載入 MCP。CLI mode 對「驗證 baseline」特別重要——拿 CLI 結果當 ground truth、再對 MCP 結果做差異比對。</p>
<p><strong>Auto-sync 機制</strong>：file watcher / git polling / 純手動 reindex 差異很大。「邊改邊問」工作流對 sync 延遲很敏感、選錯會踩到 stale graph 的事故。</p>
<h2 id="結論">結論</h2>
<p>對 Dart 主力專案：<strong>三刀流（cbm + codegraph + serena）是合理 stack</strong>。三者擅長的事不重疊、互相補位有明確角色：</p>
<ul>
<li><a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a>：概念性搜尋入口、跨 service HTTP/RPC 鏈接</li>
<li><a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a>：日常 80% 的結構查詢、auto-sync 反應最快</li>
<li><a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a>：型別精確 reference、symbol-level atomic edit、編譯 diagnostic</li>
</ul>
<p>對其他語言 stack、cbm 進入 hybrid resolution 名單後組合會收斂、但 serena 的 symbol edit 跟 diagnostic 角色仍不可取代。</p>
<p>評估方法的更普遍結論：<strong>README benchmark 只是起點、要在自己的 stack 上跑同樣的基準題才算數</strong>。每個工具的 benchmark 都選自己擅長的語言跟 codebase、跨語言遷移結論需要重新驗證。用 5 個查詢做 baseline、把 CLI 數字當 ground truth、再對 MCP 結果做差異對比、是現階段最低成本的工具評估法。</p>
]]></content:encoded></item><item><title>Case-First + Agent Team Review：教學內容的生產流程</title><link>https://tarrragon.github.io/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/</guid><description>&lt;h2 id="這篇要說什麼">這篇要說什麼&lt;/h2>
&lt;p>寫教學文章時、純靠 LLM 自生內容會踩到兩個系統性盲點：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Scope 盲點&lt;/strong>：內容停在「教科書級」結構、漏掉真實事故才會浮現的失敗模式跟設計取捨。&lt;/li>
&lt;li>&lt;strong>準確性盲點&lt;/strong>：把通用 best practice 包裝成「[case] 揭露」、把案例沒講的細節寫成案例事實。&lt;/li>
&lt;/ol>
&lt;p>本文整理在 backend/01 至 backend/07 batch 1 七個模組撰寫過程中浮現的五階段流程：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>完整閱讀案例庫、抽 findings&lt;/strong> — 用案例驅動「該寫什麼」、不只是 LLM 自生&lt;/li>
&lt;li>&lt;strong>基於 findings 建立內容&lt;/strong> — findings 分布到章節、避免硬塞模板&lt;/li>
&lt;li>&lt;strong>Agent team 平行多輪審查&lt;/strong> — 用 3 個專責 reviewer 補 LLM 自盲點&lt;/li>
&lt;li>&lt;strong>修正循環&lt;/strong> — 按檔案批次修 high + 重要 medium、reviewer 抓出問題各章節對應修&lt;/li>
&lt;li>&lt;strong>Polish pass&lt;/strong> — 跨檔系統性 pattern 集中處理（負向骨架掃描、編號漂移、用語不一、cross-link 補漏）&lt;/li>
&lt;/ol>
&lt;p>實作數據：7 個模組（backend/01-07 batch 1）、~45 章 / 385 個 review issue、case fidelity 落在 70-93% 區間、修正後品質升至 0 critical 編造、cross-link 全綠、規範違反 polish pass 後降到單位數低 issue。06 模組後方法論工具化為可觸發 skill、stage 1-5 流程跟 reviewer prompt template、self-scan regex 都封裝成可重用元件。07 模組驗證下「章節已有 routing layer skeleton」的特殊處理（在現有結構內補 case-driven 深化段、不擴成厚重章節）。&lt;/p>
&lt;h2 id="問題llm-自生內容的兩個盲點">問題：LLM 自生內容的兩個盲點&lt;/h2>
&lt;p>純靠 LLM 寫教學章節、容易產出兩種品質風險：&lt;/p>
&lt;p>&lt;strong>Scope 盲點&lt;/strong>：LLM 從訓練資料抽出的內容偏 &lt;em>普遍性&lt;/em>、是「教科書 + 部落格 + 文件」的綜合。但真實工程議題的判讀條件常常來自 &lt;em>特定事故揭露&lt;/em>、不是普遍知識。例：&lt;/p>
&lt;ul>
&lt;li>「DynamoDB GSI 在 backfill 完成前查不到完整資料」這種具體陷阱&lt;/li>
&lt;li>「Super Bowl +50% no sweat 的工程意義是 headroom 提前預留、不是 vendor 神奇」這種反直覺判讀&lt;/li>
&lt;li>「99.99% → 99.999% 是指數成本、遠超直覺的 10x 線性想像」這種規模對照&lt;/li>
&lt;/ul>
&lt;p>純技術知識推導不出來、要看真實案例才會浮現。&lt;/p>
&lt;p>&lt;strong>準確性盲點&lt;/strong>：LLM 寫到「對應 [case]」時、容易把通用 best practice 包裝成案例事實、或把案例沒提到的細節擴寫成「案例揭露」。例（從本文討論的實作中抓出的真實 issue）：&lt;/p>
&lt;ul>
&lt;li>Snowflake 案例描述「異常查詢偵測維度（query 體積 / IP / 跨 schema scan）」、LLM 自生內容寫成「query 體積從 1MB / 天跳到 10GB / 天、來源 IP 從 office network 變 unknown VPS」— 具體數字是 LLM 加上去的、案例沒寫&lt;/li>
&lt;li>Tixcraft 案例策略段建議「composite key」、LLM 自生內容寫成「Tixcraft 用 user_id 分散、不是 event_id」— 案例沒揭露 Tixcraft 實際 partition key 設計&lt;/li>
&lt;/ul>
&lt;p>這兩類盲點都不容易在 self-review 時抓到、因為 LLM 看不出自己內容是否真的對應案例。&lt;/p>
&lt;h2 id="階段-1完整閱讀案例庫抽-findings">階段 1：完整閱讀案例庫、抽 findings&lt;/h2>
&lt;h3 id="為什麼要完整閱讀不能只看-title--description">為什麼要完整閱讀、不能只看 title + description&lt;/h3>
&lt;p>只看 title + description 能做 &lt;em>承接&lt;/em>（建立 link）、但無法做 &lt;em>scope 擴展&lt;/em>（揭露 LLM 不會自生的議題）。case 的 findings 通常埋在 body 的「判讀」段、不在 description 裡。&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要說什麼">這篇要說什麼</h2>
<p>寫教學文章時、純靠 LLM 自生內容會踩到兩個系統性盲點：</p>
<ol>
<li><strong>Scope 盲點</strong>：內容停在「教科書級」結構、漏掉真實事故才會浮現的失敗模式跟設計取捨。</li>
<li><strong>準確性盲點</strong>：把通用 best practice 包裝成「[case] 揭露」、把案例沒講的細節寫成案例事實。</li>
</ol>
<p>本文整理在 backend/01 至 backend/07 batch 1 七個模組撰寫過程中浮現的五階段流程：</p>
<ol>
<li><strong>完整閱讀案例庫、抽 findings</strong> — 用案例驅動「該寫什麼」、不只是 LLM 自生</li>
<li><strong>基於 findings 建立內容</strong> — findings 分布到章節、避免硬塞模板</li>
<li><strong>Agent team 平行多輪審查</strong> — 用 3 個專責 reviewer 補 LLM 自盲點</li>
<li><strong>修正循環</strong> — 按檔案批次修 high + 重要 medium、reviewer 抓出問題各章節對應修</li>
<li><strong>Polish pass</strong> — 跨檔系統性 pattern 集中處理（負向骨架掃描、編號漂移、用語不一、cross-link 補漏）</li>
</ol>
<p>實作數據：7 個模組（backend/01-07 batch 1）、~45 章 / 385 個 review issue、case fidelity 落在 70-93% 區間、修正後品質升至 0 critical 編造、cross-link 全綠、規範違反 polish pass 後降到單位數低 issue。06 模組後方法論工具化為可觸發 skill、stage 1-5 流程跟 reviewer prompt template、self-scan regex 都封裝成可重用元件。07 模組驗證下「章節已有 routing layer skeleton」的特殊處理（在現有結構內補 case-driven 深化段、不擴成厚重章節）。</p>
<h2 id="問題llm-自生內容的兩個盲點">問題：LLM 自生內容的兩個盲點</h2>
<p>純靠 LLM 寫教學章節、容易產出兩種品質風險：</p>
<p><strong>Scope 盲點</strong>：LLM 從訓練資料抽出的內容偏 <em>普遍性</em>、是「教科書 + 部落格 + 文件」的綜合。但真實工程議題的判讀條件常常來自 <em>特定事故揭露</em>、不是普遍知識。例：</p>
<ul>
<li>「DynamoDB GSI 在 backfill 完成前查不到完整資料」這種具體陷阱</li>
<li>「Super Bowl +50% no sweat 的工程意義是 headroom 提前預留、不是 vendor 神奇」這種反直覺判讀</li>
<li>「99.99% → 99.999% 是指數成本、遠超直覺的 10x 線性想像」這種規模對照</li>
</ul>
<p>純技術知識推導不出來、要看真實案例才會浮現。</p>
<p><strong>準確性盲點</strong>：LLM 寫到「對應 [case]」時、容易把通用 best practice 包裝成案例事實、或把案例沒提到的細節擴寫成「案例揭露」。例（從本文討論的實作中抓出的真實 issue）：</p>
<ul>
<li>Snowflake 案例描述「異常查詢偵測維度（query 體積 / IP / 跨 schema scan）」、LLM 自生內容寫成「query 體積從 1MB / 天跳到 10GB / 天、來源 IP 從 office network 變 unknown VPS」— 具體數字是 LLM 加上去的、案例沒寫</li>
<li>Tixcraft 案例策略段建議「composite key」、LLM 自生內容寫成「Tixcraft 用 user_id 分散、不是 event_id」— 案例沒揭露 Tixcraft 實際 partition key 設計</li>
</ul>
<p>這兩類盲點都不容易在 self-review 時抓到、因為 LLM 看不出自己內容是否真的對應案例。</p>
<h2 id="階段-1完整閱讀案例庫抽-findings">階段 1：完整閱讀案例庫、抽 findings</h2>
<h3 id="為什麼要完整閱讀不能只看-title--description">為什麼要完整閱讀、不能只看 title + description</h3>
<p>只看 title + description 能做 <em>承接</em>（建立 link）、但無法做 <em>scope 擴展</em>（揭露 LLM 不會自生的議題）。case 的 findings 通常埋在 body 的「判讀」段、不在 description 裡。</p>
<p>實作中的對照：第一輪 audit 6 個 case、每 case 平均揭露 2.3 個 finding；其中約 7 成是 description 跟 title 看不到、要讀完整 body 才能抽出。例如 DraftKings 案例的「讀寫雙峰錯位」（比賽中讀爆量、payout 時寫爆量）— description 只說「financial ledger」、要讀「核心負載形狀」段才看到雙峰結構。</p>
<h3 id="邊際遞減的判斷">邊際遞減的判斷</h3>
<p>不是所有 case 都要讀。實作中觀察到的遞減曲線：</p>
<table>
  <thead>
      <tr>
          <th>輪次</th>
          <th>讀案例數</th>
          <th>揭露 findings</th>
          <th>平均 / case</th>
          <th>純新議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第一輪</td>
          <td>6</td>
          <td>14</td>
          <td>2.3</td>
          <td>~95%</td>
      </tr>
      <tr>
          <td>第二輪</td>
          <td>5</td>
          <td>15</td>
          <td>3.0</td>
          <td>~85%</td>
      </tr>
      <tr>
          <td>第三輪</td>
          <td>5</td>
          <td>13</td>
          <td>2.6</td>
          <td>~60%</td>
      </tr>
  </tbody>
</table>
<p>第三輪開始 <em>純新議題</em> 比例下降、重複 frame 出現（vendor dogfood 在 3 個 case 都揭露、benchmark 對照基準在 3 個 case 都揭露）。這是停止 audit 的訊號。</p>
<p>判讀條件：</p>
<ul>
<li><strong>繼續 audit</strong>：每 case 至少 1.5 個純新議題、且重複 frame 不超過 30%</li>
<li><strong>停止 audit</strong>：純新議題 &lt; 1 個 / case、重複 frame &gt; 50%、累積 finding 數已涵蓋目標章節主要議題</li>
</ul>
<p>實作中 11/94 cases（~12%）時邊際遞減訊號明顯、16/94 cases（~17%）時停止 audit、抽出 ~42 個 unique findings、足以支撐 6 個章節的 scope 擴展。</p>
<h3 id="findings-抽取方法">Findings 抽取方法</h3>
<p>讀 case 時、把每個段落看成可能的 finding 來源、問三個問題：</p>
<ol>
<li><strong>這段揭露什麼判讀條件</strong>？（是不是純技術推導不易浮現的議題）</li>
<li><strong>這段揭露什麼數字 / 設計細節</strong>？（規模、percentile、partition key 數量、replication lag 量級）</li>
<li><strong>這段揭露什麼失敗模式</strong>？（事故當下會出什麼問題、有什麼反直覺結論）</li>
</ol>
<p>寫進 findings 列表時、要附上 <em>case 來源</em> 跟 <em>該對應到哪個章節</em>。例：</p>
<blockquote>
<p>Finding: 線性擴展是 OLTP 設計最高目標、coordinator 是傳統 OLTP 的擴展瓶頸
來源: 9.C10 Spanner 案例「2 nodes → 45K reads/sec, 4 nodes → 90K reads/sec」段
章節: 1.11 全球分散式 OLTP</p></blockquote>
<p>不寫來源跟章節定位、findings 會變成抽象列表、寫稿時用不上。</p>
<h3 id="case-類型的承接策略">Case 類型的承接策略</h3>
<p>不同 case 類型適合不同承接深度、誤判類型會引發 <em>over-extrapolation</em> 問題。實作中觀察到的兩類 case：</p>
<p><strong>Rich case</strong>（典型：09/07 案例庫中含具體數字、設計細節、遷移路徑的長篇 case）：</p>
<ul>
<li>內容深度：50-200 行、含具體數字、業務情境、引用源</li>
<li>承接方式：可直接引用為事實、case 揭露的具體數字（RPS、延遲、TPS、stale window）可放進章節</li>
<li>例：9.C5 Amazon Ads「90M RPS + 5M writes/sec + 99.999%」可直接寫進 1.10 KV 章節</li>
<li>例：9.C6 Tinder「4700 萬 MAU 配對引擎、cache 是主要服務面」可直接做為 2.1 high-concurrency 的判讀依據</li>
</ul>
<p><strong>Medium case</strong>（06 模組新發現的類別、典型：模組內部 case 庫中含結構化「決策機制」+「可觀測訊號」表、但無具體數字的中篇 case）：</p>
<ul>
<li>內容深度：30-50 行、結構化 5 段（問題場景 / 決策機制 / 可觀測訊號 / 常見陷阱 / 下一步路由）、含 mechanism + 訊號名稱、但不給具體數字</li>
<li>承接方式：用 case 直接列出的 <em>mechanism 名稱</em> 精準引用、比 skeleton 精準、但比 rich 保守</li>
<li>承接句型：「對應 [case]：揭露 N 個機制 — A、B、C、D」</li>
<li>例：6.C1 Amazon Shuffle Sharding 揭露 cell boundary / shuffle sharding / static stability / constant work 四機制、可直接引用機制名稱、但不擴寫到「具體 shard 數量」「具體 cell 大小」等 case 沒提的實作細節</li>
</ul>
<p><strong>Skeleton case</strong>（典型：模組內部 N.Cx 案例庫中只有 frame、無具體數字的短篇 case）：</p>
<ul>
<li>內容深度：10-30 行、只給方向、無具體數字 / taxonomy</li>
<li>承接方式：作為「視角 / 方向」、可引用為「case 揭露 X 議題」、但不引用為「case 揭露 X 具體場景數量」</li>
<li>例：2.C1 Meta Cache Consistency 只有「promotion、shard move、故障恢復」三個方向、不引用為「具體 inconsistency window 數字」</li>
<li>例：3.C9 反例只給「依賴特定 offset / 重試節奏 / idempotency」三個方向、不引用為「4 個具體誤配場景」</li>
</ul>
<p><strong>判讀條件</strong>：</p>
<ul>
<li>看 case 行數 + 內容密度判斷類型</li>
<li>skeleton case 的 finding 寫成「對應 [case] — 揭露 X 方向、以下展開基於通用工程知識補充」</li>
<li>medium case 的 finding 寫成「對應 [case]：揭露 N 個機制 — A、B、C、D」、用 mechanism 名稱精準引用</li>
<li>rich case 的 finding 可寫「對應 [case] — XXX 具體數字 / 設計」</li>
</ul>
<p>實作中（01/02/03 三個模組驗證）、skeleton case 寫成 rich case 對應是 case fidelity reviewer 抓出 over-extrapolation 的主要來源（02 / 03 各 3-4 個 critical 編造都來自此陷阱）。誤判類型 → 編造 case 沒寫的細節 → reviewer 抓出 → 修正成本高。stage 1 抽 findings 時就要 <em>標明 case 類型</em>、stage 2 寫作時依類型決定承接深度。</p>
<p><strong>Rich case 引用的反向風險（04/05 模組新發現）</strong>：rich case 雖然可以引用具體數字、但 case 內常含「觀察層」（具體 fact）跟「判讀層」（作者推論）兩段、引用時要分開處理。05 模組驗證時 case fidelity reviewer 抓出 4 個 high issue 都來自把「判讀層作者推論」寫成「case 揭露的 fact」：</p>
<ul>
<li>9.C12 Riot Games：5.2 寫「揭露 35ms latency 反推 region 部署」、實際 case 的「35ms」是觀察層、「反推 region 部署」是作者判讀層</li>
<li>9.C34 GCP 130K：5.2 寫「揭露 Spanner 替 etcd 才是 K8s 規模極限的關鍵」、實際 case 用更保守的「control plane 極限取決於 storage backend、GCP 用 Spanner 替換 etcd」分兩個點寫</li>
<li>9.C12 Riot：5.2 引用「single-tenant per game 的多 cluster 策略」、漏掉 case 揭露的關鍵歷史轉折「從 multi-tenant cluster 模型改成 single-tenant per game」</li>
</ul>
<p><strong>修法</strong>：rich case 引用時、用「揭露 X 觀察 + 作者判讀 Y」分層標明、避免把推論寫成 fact。或在引用後補一句「（case 中 X 屬作者判讀層、本章引用此推論）」明示分層。</p>
<p>兩類 case 的引用紀律可總結成一個 <em>fact vs derive</em> 分層原則：</p>
<ul>
<li><strong>Skeleton case</strong>：絕大多數內容是 derive（方向 / 議題）、引用時不擴寫成 fact</li>
<li><strong>Rich case</strong>：含 fact（具體數字 / 設計）跟 derive（作者判讀）、引用時分層標明、避免把 derive 升級成 fact</li>
</ul>
<h2 id="階段-2基於-findings-建立內容">階段 2：基於 findings 建立內容</h2>
<h3 id="findings-分布到章節">Findings 分布到章節</h3>
<p>抽完 findings 後、按章節主題分類、看哪個章節缺口最大、哪個 finding 該寫去哪。實作中的分布：</p>
<ul>
<li>1.1 高併發：7 findings</li>
<li>1.5 紅隊：8 findings</li>
<li>1.9 reconciliation：4 findings</li>
<li>1.10 KV：6 findings</li>
<li>1.11 全球分散式：10 findings（最大缺口）</li>
<li>1.6+1.12 migration：5 findings</li>
</ul>
<p>涉及多軸取捨的章節（1.11 一致性 / 可用性 / 成本 / 延遲）暴露最多缺口、純流程章節（1.9）暴露最少。這是 <em>章節結構性質</em> 的差異、不是寫得好壞。</p>
<h3 id="stage-2-寫作前先定-ssot-對應">Stage 2 寫作前先定 SSoT 對應</h3>
<p>當同一 finding 或 frame 在 <em>多個章節</em> 都有用、要在開始寫之前 <em>先定 SSoT 對應</em>、否則 case-driven 擴章必然出現 frame 重複展開。</p>
<p>實作中觀察到的反例（02 / 03 模組都遇到過）：</p>
<ul>
<li><strong>02 cache</strong>：「cache 角色變化」frame 在 2.1 主寫但實際屬模組層級、應在 <code>_index</code>；Tubi 案例在 2.1 / 2.2 / 2.8 三章各自展開 mini-finding；Snap KeyDB 在 2.1 / 2.7 / 2.8 三章重複</li>
<li><strong>03 message-queue</strong>（最嚴重）：「三層語意（delivery / processing / recovery）」在 3.4 / 3.6 / 3.8 三章各自定義；「Slack Kafka+Redis 拓樸」在 3.4 跟 3.8 兩章逐字重複；「規模對照（小 / 中 / 大型）」在 3.4 / 3.6 / 3.8 三章拆用、結論散落讀者拼不出總圖</li>
</ul>
<p><strong>SSoT 對應的判讀順序</strong>：</p>
<ol>
<li>列出所有 cross-chapter findings（出現在多章的 frame）</li>
<li>每個 frame 指定 <em>一個</em> 主寫章節（SSoT）</li>
<li>其他章節 <em>只 link</em>、不展開</li>
<li>SSoT 章節要有完整論述、被引用章節保留簡述跟 cross-link</li>
</ol>
<p><strong>SSoT 選擇標準</strong>：</p>
<ul>
<li>frame 涉及 <em>跨模組層級概念</em> → 寫進 <code>_index.md</code></li>
<li>frame 涉及 <em>單章核心責任</em> → SSoT 為該章</li>
<li>frame 涉及 <em>跨章交接點</em> → 選最相關章節為 SSoT、其他章節 link</li>
</ul>
<p>漏掉這步、reviewer 跨章一致性會抓出 5-10 個 frame 重複 issue、修正成本高（要把已展開內容收斂回 SSoT）。Stage 2 前花 30 分鐘做 SSoT 對應、能省下 Stage 3 數小時的重構工。</p>
<h3 id="避免硬塞模板">避免硬塞模板</h3>
<p>最大的反模式是把多個 findings 硬塞成同一個 table、每 row 一短語、失去情境敘事。</p>
<p>實作中的反例：1.9 章新增「Dual-track IC 5 個角色表」、本來想用表格整齊呈現、但 reviewer 抓出「5 角色平鋪、責任只一行、未展開每角色在真實事故的決策樣態」。修正後拆成：</p>
<ul>
<li>主表格（5 個角色快速對照）</li>
<li>Overall IC 跟 Tech IC 的差異獨立段（300 字）</li>
<li>Data IC 的特殊角色獨立段（300 字、含「為什麼不能讓 Tech IC 兼任」的失誤對照）</li>
<li>事先準備 4 項各自延伸（不只列項目、解釋失效樣態）</li>
</ul>
<p>這樣 <em>每個項目都是情境</em> 而非 <em>硬塞的欄位</em>、符合 AGENTS.md「表格不是終點」原則。</p>
<h3 id="情境敘事的判讀條件">情境敘事的判讀條件</h3>
<p>每段內容寫完後、問三個檢查問題：</p>
<ol>
<li><strong>首句是不是核心原則</strong>？（不是「某 case 揭露 X」、是「X 是什麼、承擔什麼責任」）</li>
<li><strong>是不是用否定句主導</strong>？（「不是 X」「不只 X」開段要回到正向陳述）</li>
<li><strong>這個 finding 在不同情境下是否會變義</strong>？（一個 finding 套到多個情境、要分情境寫、不是套同模板）</li>
</ol>
<h3 id="案例引用的準確性">案例引用的準確性</h3>
<p>寫「對應 [case] — XXX」時、要回 case 原文驗證 XXX 是否真的出現。實作中常見的失分：</p>
<ul>
<li>把 case 沒提到的數字補進去（「30-90 天 baseline」、「1MB→10GB / 天」）</li>
<li>把通用 best practice 寫成案例事實（「Snowflake 之後改為預設強制 MFA」— case 只說「資料平台應預設強制 MFA」、不是描述後續行動）</li>
<li>公開事實但 case 沒寫（「MOVEit 跨上百家客戶」、「LastPass master password 弱可被離線爆破」）</li>
</ul>
<p>寫稿當下不容易抓、要靠階段 3 的 case fidelity reviewer 對照。</p>
<h2 id="階段-3agent-team-平行多輪審查">階段 3：Agent team 平行多輪審查</h2>
<h3 id="為什麼要-agent-team不能交給單一-reviewer">為什麼要 agent team、不能交給單一 reviewer</h3>
<p>單一 reviewer 有兩個限制：</p>
<ol>
<li><strong>維度盲點</strong>：一個 reviewer 同時看寫作規範、案例準確性、跨章一致性、容易 <em>維度互相干擾</em>、最後每個維度都看不深</li>
<li><strong>Context 污染</strong>：reviewer 讀完整 commit + 所有案例 + 所有章節後、自身 context 就被佔滿、給的建議會 <em>對應主 context 也跟著沉重</em></li>
</ol>
<p>解法是用 3 個專責 reviewer、平行 background 跑、各自獨立報告、主 context 只看精煉摘要。</p>
<h3 id="三個維度-reviewer-分工">三個維度 reviewer 分工</h3>
<p>實作中使用的三個 reviewer：</p>
<h4 id="reviewer-a寫作規範審查agentsmd-核心原則">Reviewer A：寫作規範審查（AGENTS.md 核心原則）</h4>
<ul>
<li>對照核心原則先行、正向陳述優先、商業邏輯先於 case、表格不是終點、情境優先於模板、可操作判準等八原則</li>
<li>找首句用否定句切入、表格 / bullet 平鋪沒延伸、表格項硬塞模板等</li>
<li>實作中抓出 25 個 issue</li>
</ul>
<h4 id="reviewer-b案例引用準確性">Reviewer B：案例引用準確性</h4>
<ul>
<li>對照原始 case 內容、驗證「對應 [case] — XXX」斷言是否真的來自案例</li>
<li>識別編造數字、過度推論、把通用 best practice 寫成案例事實</li>
<li>實作中抓出 9 個 issue、包含 3 個 critical 編造</li>
</ul>
<h4 id="reviewer-c跨章一致性">Reviewer C：跨章一致性</h4>
<ul>
<li>跨多章找重複 frame、矛盾說法、失效 cross-link、章節邊界錯位</li>
<li>識別「該在 A 章卻寫在 B 章」、「frame 重複展開沒整併」</li>
<li>實作中抓出 13 個 issue</li>
</ul>
<h3 id="平行-background-跑不佔主-context">平行 background 跑、不佔主 context</h3>
<p>關鍵設計是 3 個 reviewer 並行、各自 background、各自寫 output file、不污染主 context：</p>
<ul>
<li>主 context 只看到「啟動 reviewer」跟「reviewer 完成的彙整報告」</li>
<li>Raw output 跟 reviewer 的 deep dive 留在 output file、需要時 SendMessage 繼續對話</li>
<li>3 個 reviewer 完成時間 ~5-15 分鐘、可以同時跑、不必等</li>
</ul>
<p>實作中 3 個 reviewer 平均 2-3 分鐘完成、主 context 增量 ~3K tokens（彙整 + 47 issue 清單）、相比把所有案例跟章節塞進主 context 做 review 節省 ~80% context。</p>
<h3 id="reviewer-issue-數量的-baseline">Reviewer issue 數量的 baseline</h3>
<p>7 個模組（01 / 02 / 03 / 04 / 05 / 06 / 07 batch 1）驗證後、每模組 reviewer 抓到的 issue 數量在 standards reviewer 抓 pattern 越來越細的趨勢下持續擴大、可作為流程預期：</p>
<table>
  <thead>
      <tr>
          <th>Reviewer 維度</th>
          <th>01</th>
          <th>02</th>
          <th>03</th>
          <th>04</th>
          <th>05</th>
          <th>06</th>
          <th>07 b1</th>
          <th>baseline</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Standards reviewer</td>
          <td>25</td>
          <td>20</td>
          <td>20</td>
          <td>31</td>
          <td>28</td>
          <td>45</td>
          <td>31</td>
          <td>20-45 issue</td>
      </tr>
      <tr>
          <td>Case fidelity reviewer</td>
          <td>9 (88%)</td>
          <td>20 (78%)</td>
          <td>15 (70%)</td>
          <td>6 (92.9%)</td>
          <td>13 (80%)</td>
          <td>11 (88%)</td>
          <td>8 (81%)</td>
          <td>6-20 issue</td>
      </tr>
      <tr>
          <td>Consistency reviewer</td>
          <td>13</td>
          <td>15</td>
          <td>15</td>
          <td>14</td>
          <td>18</td>
          <td>15</td>
          <td>13</td>
          <td>13-18 issue</td>
      </tr>
      <tr>
          <td><strong>總計</strong></td>
          <td><strong>47</strong></td>
          <td><strong>55</strong></td>
          <td><strong>50</strong></td>
          <td><strong>51</strong></td>
          <td><strong>59</strong></td>
          <td><strong>71</strong></td>
          <td><strong>52</strong></td>
          <td><strong>47-71 issue</strong></td>
      </tr>
  </tbody>
</table>
<p><strong>模式觀察</strong>：</p>
<ul>
<li><strong>每模組 issue 數隨 standards reviewer 抓 pattern 越來越細而擴大</strong>：01-03 穩定在 47-55、04/05 推到 51-59、06 推到 71、07 batch 1 回到 52（章節已有 routing skeleton、擴章規模小）。趨勢來自 standards reviewer 抓的 pattern 越來越廣（從負向骨架 → 「核心責任不是」變體 → 「沒有 X 會 Y」鏈式 → 「case 引用段首」框架 → 「case 引用句構同質化」）。</li>
<li><strong>Case fidelity 準確率分布更廣</strong>：04 的 92.9% 來自 skeleton case 嚴守「揭露方向、通用補充」紀律；05 的 80% 因引用 09 rich case 加入「fact vs derive 分層」新失分模式；06 的 88% 屬 medium case 紀律首次套用、揭露「實作層擴寫過頭」失分；07 batch 1 的 81% 揭露「跨 case 合成 frame」新失分類型（reviewer B 2 high 都屬此類）</li>
<li><strong>Consistency reviewer 抓到的 frame 重複跟章節數成正比</strong>：02 / 03 / 04 都有 ~13-18 個一致性 issue、05/06 跨模組 cross-link 密度高仍維持在 baseline 內、07 batch 1 因 7 章規模、issue 13 個落在 baseline 下緣</li>
</ul>
<p><strong>Stage 3 修正成本估算</strong>：</p>
<ul>
<li>Critical（編造、矛盾）：~每個 5-10 分鐘修正、佔 0-5 個（04/05 都 0 critical、紀律已成熟）</li>
<li>High（重複 frame、章節邊界、判讀層 vs fact）：~每個 10-20 分鐘修正、佔 5-14 個</li>
<li>Medium / Low（規範細節、cross-link 補）：~每個 2-5 分鐘修正、佔 35-45 個</li>
<li><strong>總計 ~1.5-2.5 小時 / 模組</strong></li>
</ul>
<p><strong>Stage 4 修正後仍會有 ~30-40% issue 殘留</strong>（low / medium 的 cross-link、編號漂移、用語不一）、屬於系統性 pattern、適合在 Stage 5 polish pass 集中處理（見後段）。</p>
<h3 id="為何要多輪-review不是一次到位">為何要多輪 review、不是一次到位</h3>
<p>第一輪 review 的目的是 <em>找問題</em>、不是 <em>修問題</em>。問題清單列出後、要做兩件事：</p>
<ol>
<li><strong>分類優先序</strong>：critical / high / medium / low、按嚴重度跟修改成本排序</li>
<li><strong>修正循環</strong>：批次修正、避免一個一個改散開、修完再跑驗證</li>
</ol>
<p>修正後可選擇性做第二輪 review、檢查：</p>
<ul>
<li>修正本身有沒有引入新問題</li>
<li>之前 reviewer 漏掉的維度（例：教學性、讀者路徑、實作可行性）</li>
<li>跨 commit 一致性</li>
</ul>
<p>實作中第一輪足夠處理 47 個 issue、第二輪沒進行、留到未來模組（02 cache、03 message queue）累積經驗後再評估是否必要。</p>
<h2 id="修正循環的執行原則">修正循環的執行原則</h2>
<p>47 個 issue 分布到 6 個章節、修正時 <em>按檔案批次</em>、不是按 issue 編號順序。每個檔案一次修完所有相關 issue、減少切換成本：</p>
<ul>
<li>1.5 紅隊章（12 issue）：含 2 個 critical 編造、優先處理</li>
<li>1.10 KV（7 issue）：含 1 個 critical 編造</li>
<li>1.11 全球分散式（5 issue）</li>
<li>1.12 大規模遷移（10 issue）：表格密度最高、最多延伸</li>
<li>1.1 高併發（4 issue）</li>
<li>1.9 reconciliation（5 issue）</li>
</ul>
<p>每個檔案修完後跑一次 <code>mdtools fmt --fix</code> + <code>mdtools cards</code> + <code>mdtools lint</code>、確認該檔內部一致、再進下一檔。最後跑一次跨檔驗證、確認 cross-link 全部對齊。</p>
<h2 id="階段-5polish-pass0405-模組後新增">階段 5：Polish pass（04/05 模組後新增）</h2>
<p>Stage 4 修完 high + 重要 medium 後、仍有 ~30-40% 的 low / medium 殘留、屬於系統性 pattern（負向骨架、編號漂移、cross-link 缺漏、模板化）。這些 issue 不適合按章節批次修、適合用「跨檔系統性掃描」處理 — 這是 polish pass 的核心責任。</p>
<h3 id="polish-pass-的觸發條件">Polish pass 的觸發條件</h3>
<p>Stage 4 後出現以下任一訊號、就該排 polish pass：</p>
<ul>
<li>Standards reviewer 抓出的「不是 X、而是 Y」段首結構超過 5 處（屬寫作習慣、單章修改無效率）</li>
<li>Consistency reviewer 抓出「編號漂移」「失效 link」「用語不一」多處（屬跨檔規範問題）</li>
<li>自掃描漏掉的 pattern 出現在 reviewer report（例：04 自掃描說 pass、reviewer A 抓出 31 個 issue、暴露自掃描 regex 不夠寬）</li>
</ul>
<h3 id="polish-pass-不該做的事">Polish pass 不該做的事</h3>
<ul>
<li><strong>不重寫章節結構</strong>：polish pass 是把現有內容修得更貼合規範、不是重新組織。重寫的觸發條件應該回到 stage 2、不是 polish pass。</li>
<li><strong>不擴大 scope</strong>：原本 4.20 / 5.4 等不在擴充範圍的章節、polish pass 也不動。Polish pass 邊界 = stage 4 修改過的章節集合。</li>
<li><strong>不追求 0 issue</strong>：reviewer 抓的 ~15 個 low 通常可保留為下次擴章節時自然處理。Polish pass 處理「系統性 pattern」、不處理「孤立 issue」。</li>
</ul>
<h3 id="polish-pass-的標準工序">Polish pass 的標準工序</h3>
<p>按系統性 pattern 分批處理、每批跑一次自掃描確認：</p>
<ol>
<li><strong>負向骨架掃描修正</strong>：用更寬泛的 regex <code>不是 |而不是|沒有.*[，、]會</code> 掃描、把「不是 X、而是 Y」「而不是 X」改成正向陳述 + 後置邊界提醒。技術約束敘述（「多人共用 IP 無法區分」）保留。</li>
<li><strong>編號漂移統一</strong>：把 <code>04.X</code> 風格 plain text 改成 <code>[4.X title](url)</code> markdown link、跟 _index 對齊。</li>
<li><strong>表格延伸段補強（關鍵段）</strong>：選 2-3 個最高 impact 表格（判讀訊號表的爭議列、Buffer / Sampling 等選型表）補延伸子段、不全部補（避免擴展超出 scope）。</li>
<li><strong>模板化拆敘事（代表性段）</strong>：選 1-2 個最明顯的「四步驟模板套不同情境」段、拆成情境化敘事、其他保留為下次。</li>
<li><strong>Cross-link 補漏 + ownership 邊界補強</strong>：reviewer C 報告的所有 cross-link 缺漏一次補完、用同一個批次跑 mdtools 驗證。</li>
<li><strong>用語不一統一 + 失效 link 修正</strong>：簡轉繁、<code>/knowledge-cards/</code> vs <code>/section/</code> URL 統一、失效 link 改規劃中或正確路徑。</li>
<li><strong>最終驗證 + commit</strong>：跑 <code>mdtools fmt --fix &amp;&amp; mdtools cards &amp;&amp; mdtools lint</code>、確認全綠、commit。</li>
</ol>
<h3 id="polish-pass-的實作成本">Polish pass 的實作成本</h3>
<p>實作中（04 / 05 polish pass 合併 commit <code>1072087</code>）：</p>
<ul>
<li>處理範圍：11 個檔案、+44 / -29 行</li>
<li>修正項目：~35 個 issue（10 個負向骨架、2 個模板化、3 個編號漂移、3 個表格延伸段、3 個 cross-link、1 個 case 引用結構）</li>
<li>時間：~30-45 分鐘（不重寫、只 pattern match）</li>
<li>剩餘 ~15 個 low 保留下次</li>
</ul>
<p>Polish pass 的 ROI 來自「系統性 pattern 一次處理 vs 散在各章一個個改」的效率差異。每個 pattern 在多章重複出現時、用 grep / rg 跨檔修一輪比每章單獨修快 3-5 倍。</p>
<h3 id="自掃描盲點更新">自掃描盲點更新</h3>
<p>04 流程暴露了一個 self-scan 盲點：原 regex <code>不行|不可以|不要|無法|不能</code> 漏掉「核心責任不是 X、而是 Y」這個變體段首。修正建議：</p>
<ul>
<li>加 <code>^[^|].*責任(不是|並非)</code> 抓「核心責任不是 X」變體</li>
<li>加 <code>^[^|].*[，,]而是</code> 抓「X、而是 Y」結構（已是正常陳述、但段首位置仍是負向骨架）</li>
<li>加 <code>^[^|].*[，,]不是</code> 抓「X、不是 Y」結構</li>
</ul>
<p>把自掃描 regex 視為持續演進的工具、每個 reviewer 抓出新 pattern 就更新一次、避免在下個模組重蹈覆轍。</p>
<h2 id="適用情境跟限制">適用情境跟限制</h2>
<h3 id="適用情境">適用情境</h3>
<ul>
<li><strong>長期累積的教學模組</strong>：6+ 章、跨章引用密集、規範遵循重要</li>
<li><strong>有現成 case 庫</strong>：07/09 累積的 100+ 案例是這套流程的前提、沒案例庫做不到 case-first</li>
<li><strong>品質高於速度</strong>：完整三階段約 3-4 小時 / 模組（stage 2 寫作 ~1.5-2hr + reviewer ~15 分鐘 + stage 3 修正 ~1.5-2hr）、適合長期累積的內容、不適合 one-off 文章</li>
<li><strong>主 context 容量敏感</strong>：reviewer 平行 background 是節省 context 的關鍵設計</li>
</ul>
<h3 id="不適用情境">不適用情境</h3>
<ul>
<li><strong>新主題沒案例庫</strong>：要先建案例庫、不能直接套這流程</li>
<li><strong>單篇短文</strong>：流程的固定成本（讀案例 + 跑 reviewer）對短文 ROI 低</li>
<li><strong>快速迭代原型</strong>：流程偏向 <em>寫一次寫好</em>、不是 <em>快速修改</em></li>
<li><strong>Routing layer / 導讀性質章節</strong>：已含完整 threat scope + 引用標準 + 問題節點表、case 庫不對應或缺位、應跳過本流程、用標準引用 + 通用工程知識補充承接（07 LLM / 治理章節驗證）</li>
<li><strong>Standard framework 比 case 庫成熟的領域</strong>：見下段「Standard-driven 取代 case-driven」</li>
</ul>
<h3 id="standard-driven-取代-case-driven07-llm-章節驗證">Standard-driven 取代 case-driven（07 LLM 章節驗證）</h3>
<p>在標準框架比 case 庫成熟的領域、case-driven 不是預設選擇。LLM 安全章節跑完 5 章驗證後浮現一個 finding：當該領域的 <em>標準框架</em>（如 OWASP LLM Top 10 2025 / NIST AI RMF 1.0 / MITRE ATLAS）已涵蓋 threat 分類、且 case 維護半衰期短於 standard、章節應 <em>用 standard-driven 取代 case-driven</em>。Standard-driven 跟 case-driven 是平行選項、依領域特性選用 — 兩者沒有退化 / 進階關係。</p>
<p><strong>判斷該用哪種策略的四維度</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Case-driven 適用</th>
          <th>Standard-driven 適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>議題穩定度</td>
          <td>高（5+ 年穩定）</td>
          <td>低（&lt; 1 年快速演進）</td>
      </tr>
      <tr>
          <td>Case 公開度</td>
          <td>高（充分的事故公告）</td>
          <td>中或低（vendor disclosure 偏 marketing）</td>
      </tr>
      <tr>
          <td>Standard 成熟度</td>
          <td>中（多用 case 而非 standard）</td>
          <td>高（standard framework 已成型）</td>
      </tr>
      <tr>
          <td>維護半衰期</td>
          <td>長</td>
          <td>短（6 個月過時）</td>
      </tr>
  </tbody>
</table>
<p><strong>典型對照</strong>：</p>
<ul>
<li><em>Case-driven 領域</em>：分散式系統 / 安全控制面 / 可靠性 / 訊息佇列（backend/01-07 batch 1 都屬此類、案例公開充分、半衰期 5+ 年）</li>
<li><em>Standard-driven 領域</em>：LLM 安全（OWASP LLM Top 10 / MITRE ATLAS 已成型、案例 6 個月過時）、新興 compliance（NIST AI RMF）、cloud-native 標準（CNCF baseline）</li>
</ul>
<p><strong>Standard-driven 章節的寫作策略</strong>：</p>
<ol>
<li><strong>章節對齊 standard framework 分類</strong>：用 framework 章節 ID 標明（如 OWASP LLM01 / NIST AI-1.1）取代「對應 [case] —」斷言</li>
<li><strong>加 Last reviewed cadence</strong>：每 quarter 重評估 standard 版本跟章節對應、寫進 frontmatter</li>
<li><strong>「案例觸發參考」段標明「公開案例累積中、值得追蹤的方向」</strong>：不寫「對應 [case] 揭露」斷言、避免引用源不穩定</li>
<li><strong>引用標準時用版本號</strong>：OWASP LLM Top 10 2025 / NIST AI RMF 1.0 / MITRE ATLAS continuous — framework 改版要 trigger 章節重審</li>
</ol>
<p><strong>實證</strong>：07 LLM 章節 5 章已套用 standard-driven 策略：</p>
<ul>
<li>章節 113-137 行、含完整 threat scope + 問題節點表 + 風險邊界</li>
<li>引用 OWASP LLM Top 10 + NIST AI RMF + MITRE ATLAS 取代個別 case 引用</li>
<li>加 <code>Last reviewed: 2026-05-12</code> cadence</li>
<li>「案例觸發參考」段寫「公開案例累積中、值得追蹤的方向」+「事實查核註」</li>
<li>完全不寫「對應 [case] —」斷言、不存在 case fidelity reviewer 該抓的準確性問題</li>
</ul>
<p>對照 backend/01-07 batch 1 的 case-driven 章節、LLM 章節是 <em>用不同方法達到同樣品質</em> — scope 涵蓋真實 production 議題（KV cache 跨租戶、shared prefix optimization、batch 推論順序敏感）、不停在教科書級內容。</p>
<p><strong>何時要從 standard-driven 轉回 case-driven</strong>：</p>
<ul>
<li>該領域累積 5+ 個高可信度 case（vendor disclosure + academic paper + CVE 三來源交叉）</li>
<li>跨章 frame 重複出現、case-driven mechanism 深化能解 SSoT 衝突</li>
<li>出現「等級類似 SolarWinds」的 incident、案例本身夠重、單一 case 即可支撐章節擴章</li>
<li>讀者反饋章節太抽象、需要具體 case 才能理解 mechanism</li>
</ul>
<p>不滿足任一條件時、繼續走 standard-driven、不勉強建 case 庫。</p>
<p><strong>對 case-first-module-workflow skill 的補強</strong>：</p>
<p>skill 之前的「不適用情境」寫「沒 case 庫的新主題（要先建 case 庫）」— 這暗示缺 case 庫一定要先補。07 LLM 章節驗證了第三條路：<em>用 standard-driven 取代</em>、適用 standard framework 比 case 庫成熟的領域。這個 finding 已補進 skill 的「不適用情境」段。</p>
<h3 id="限制">限制</h3>
<ul>
<li><strong>Reviewer 維度有限</strong>：當前 3 個 reviewer 沒覆蓋「教學性」「讀者路徑」「實作可行性」、若主題需要這些維度、要加 reviewer</li>
<li><strong>修正可能引入新 issue</strong>：第一輪 review 後修正、修正本身可能違反規範、若大量修正最好做第二輪</li>
<li><strong>Case 庫品質決定 findings 品質</strong>：case 寫得淺、findings 也淺；case fidelity reviewer 也只能驗證「跟 case 一致」、不能驗證「case 本身對不對」</li>
<li><strong>依賴 LLM agent 平台能力</strong>：流程預設可平行跑 background agent、不是所有 LLM 平台都支援</li>
</ul>
<h2 id="7-個模組驗證後的反覆陷阱">7 個模組驗證後的反覆陷阱</h2>
<p>01 / 02 / 03 / 04 / 05 / 06 / 07 七個模組執行下來、以下陷阱在 <em>多數模組都重複出現</em>、屬於 LLM case-driven 寫作的系統性失分點。本流程下次套用前要 <em>主動防範</em>、不能依賴 stage 3 reviewer 補救（雖然 reviewer 都會抓到、但修正成本高）。</p>
<h3 id="陷阱-1skeleton-case-擴寫成-case-事實">陷阱 1：Skeleton case 擴寫成 case 事實</h3>
<p>當 case 內容簡短（10-30 行、只有 frame 沒有具體數字 / taxonomy）、LLM 寫作時容易把通用知識（具體數字、攻擊向量列表、設計細節）寫成「對應 [case] —」斷言。實際 case 沒寫的。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>01 紅隊：Snowflake「30-90 天 baseline」編造、Tixcraft「partition key 用 user_id」編造</li>
<li>02 cache：Tubi 三層 cache 具體 latency（L1 &lt; 1ms、L2 &lt; 10ms、L3 10-100ms）編造、Redis「100K-200K ops/sec」無來源、KeyDB「5-10x throughput」其實是 case 判讀段非引用源</li>
<li>03 messaging：PayPay「broker 寫入 3K msg/sec」實際 case 寫的是「DynamoDB 寫入 3K msg/sec」（PayPay 用 DynamoDB 不是傳統 broker）、3.C9 case 三個方向被擴寫成「4 個誤配場景」、3.C10 case 「大型服務 DLQ 是診斷入口」完全編造</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Stage 1 抽 findings 時 <em>標明 case 類型</em>（rich vs skeleton）</li>
<li>Stage 2 寫 skeleton case finding 時、用「對應 [case] — 揭露 X 方向、以下展開基於通用工程知識補充」這種 <em>fact vs derive</em> 標記</li>
<li>不要為了「整齊的 4 個攻擊面」「3 個攻擊向量」「5 個誤配場景」這種數字感、把 case 沒寫的 taxonomy 寫成 case 揭露</li>
</ul>
<h3 id="陷阱-2frame-重複展開ssot-不清">陷阱 2：Frame 重複展開（SSoT 不清）</h3>
<p>同一概念在多章 case-driven 擴章時各自展開、形成 frame 重複。讀者跨章讀會踩到重述、結論散落拼不出總圖。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>01：容量三口徑 frame 在 1.1 跟 1.12 重複展開、storage / compute 分離 frame 在 1.1 跟 1.11 重複</li>
<li>02：cache 角色變化 frame 在 2.1 主寫但屬模組層級、應在 _index；Tubi 案例在 2.1 / 2.2 / 2.8 三章 mini-展開</li>
<li>03（最嚴重）：三層語意（delivery / processing / recovery）在 3.4 / 3.6 / 3.8 三章各自定義；Slack Kafka+Redis 拓樸在 3.4 跟 3.8 兩章逐字重複；規模對照在 3.4 / 3.6 / 3.8 三章拆用</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Stage 2 寫作前花 30 分鐘做 SSoT 對應（見前面「Stage 2 寫作前先定 SSoT 對應」段）</li>
<li>列出 cross-chapter frames、指定唯一主寫章節、其他章節只 link</li>
<li>寫每章前問「這個 frame 主寫在哪？我現在寫的是主寫還是 link？」</li>
</ul>
<h3 id="陷阱-3負向陳述--模板化規範系統性失分">陷阱 3：負向陳述 + 模板化（規範系統性失分）</h3>
<p>「不是 X、是 Y」推進論證、L1/L2/L3 三層平鋪、三選一表格、四步驟流程。這兩個原則違反在每模組都重複出現、是 LLM 寫作的反覆模式、stage 3 standards reviewer 每模組會抓 10-20 處。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>01 規範 violation：表格不延伸（7 處）、負向陳述（5 處）、首句結構（4 處）</li>
<li>02 規範 violation：原則 8 模板化（6 處）、原則 2 負向陳述（6 處）、原則 4 表格不延伸（4 處）</li>
<li>03 規範 violation：原則 2 負向陳述（12 處最嚴重）、原則 1 首句結構（5 處）、原則 6 用語節制（2 處）</li>
<li>04 規範 violation：原則 2 負向陳述（12 處最嚴重、含「核心責任不是 X、而是 Y」變體段首）、原則 1 首句結構（9 處）、原則 4 表格不延伸（9 處）</li>
<li>05 規範 violation：原則 2「不是 X、而是 Y」+「沒有 X、會 Y」（10 處）、原則 8 四步驟 / 四層並列模板（7 處）、原則 3 case 引用框架取代商業邏輯先行（6 處）</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Stage 2 寫完後 <em>寫稿端就跑掃描</em>、不等 reviewer：
<ul>
<li><code>rg -n &quot;不行|不可以|不要|無法|不能&quot; &lt;module-path&gt;</code> 找負向骨架（技術約束敘述例外）</li>
<li><code>rg -n &quot;^[^|].*責任(不是|並非)&quot; &lt;module-path&gt;</code> 找「核心責任不是 X」變體段首（04 模組新發現的 pattern）</li>
<li><code>rg -n &quot;^[^|].*[，,]而是|^[^|].*[，,]不是&quot; &lt;module-path&gt;</code> 找對比骨架開段</li>
<li>自查表格：每個 bullet 是否有後文延伸？</li>
<li>自查首句：是否「核心原則先行」而非「對應 [case] 揭露」</li>
</ul>
</li>
<li>模板化（L1/L2/L3、三選一）出現時、先問「這三項是真的對等？還是業務情境不同？」— 不同情境的話拆敘事段、不用表格</li>
</ul>
<h3 id="陷阱-4rich-case-判讀層被當-case-fact-引用0405-模組新發現">陷阱 4：Rich case 判讀層被當 case fact 引用（04/05 模組新發現）</h3>
<p>引用 09 / 07 等 rich case 時、case 內常含「觀察層」（具體 fact）跟「判讀層」（作者推論）兩段。LLM 寫作時容易把兩層壓縮成「揭露 X」、把作者判讀升級為 case fact。</p>
<p>跟陷阱 1（skeleton case 擴寫成 case 事實）的差別：</p>
<ul>
<li><strong>陷阱 1</strong>：case 沒提的細節（具體數字、taxonomy）被寫成 case 揭露</li>
<li><strong>陷阱 4</strong>：case 有提、但屬作者判讀層的內容被寫成 case fact</li>
</ul>
<p><strong>實證</strong>：</p>
<ul>
<li>05 / 9.C12 Riot：5.2 寫「揭露 35ms latency 反推 region 部署」、實際 case 的「35ms」是觀察層、「反推 region 部署」是作者判讀層</li>
<li>05 / 9.C34 GCP：5.2 寫「揭露 Spanner 替 etcd 才是 K8s 規模極限的關鍵」、實際 case 用更保守的「control plane 極限取決於 storage backend、GCP 用 Spanner 替換 etcd」分兩個點寫、章節壓縮 + 強化成硬性結論</li>
<li>05 / 9.C12 Riot：漏掉 case 揭露的關鍵歷史轉折「從 multi-tenant cluster 模型改成 single-tenant per game」</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>引用 rich case 前、先把 case 內的「觀察段」跟「判讀段」分開讀、抽 finding 時各自標明來源層</li>
<li>引用時用「揭露 X 觀察 + 作者判讀 Y」分層寫、或在引用後補一句「（case 中 X 屬作者判讀層、本章引用此推論）」</li>
<li>避免使用「才是 / 必須 / 一定」這類強化詞、保留 case 原文的條件性表述</li>
<li>Stage 3 case fidelity reviewer 的 prompt 要特別點出「判讀層 vs 觀察層」的分界、把這當作 high 級 issue 抓取</li>
</ul>
<h3 id="陷阱-5自掃描盲點累積040506-模組持續顯現">陷阱 5：自掃描盲點累積（04/05/06 模組持續顯現）</h3>
<p>自掃描的 regex 跟 reviewer 抓的 pattern 會逐漸脫節。每個模組 reviewer 會發現新 pattern、self-scan regex 跟著演進、但 reviewer 仍會發現下一個。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>04 自掃描用 <code>不行|不可以|不要|無法|不能</code> 跟「不是 X、是 Y」掃描通過、但 reviewer A 抓出「核心責任不是 X、而是 Y」變體段首（佔 12 處）</li>
<li>05 自掃描通過、但 reviewer A 仍抓出「沒有 X、會 Y」鏈式負向句構 + 「四步驟模板」+ 「case 引用框架取代商業邏輯先行」三類新 pattern</li>
<li>06 self-scan 加了「不是 X、而是 Y」變體 + 「沒有 X 會 Y」、仍漏掉「對應 [case]：揭露 N 個機制」段首取代核心概念句的 pattern（reviewer A 抓 45 issue、其中 11/12 新段都犯這個錯）</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>每個模組 reviewer 抓出新 pattern 後、回頭更新 self-scan regex</li>
<li>把 self-scan 視為持續演進的工具、不是固定 checklist</li>
<li>Stage 5 polish pass 是處理自掃描盲點累積的標準入口（見前段）</li>
<li>06 模組後 self-scan 加 <code>rg -n &quot;^對應 \[&quot; &lt;module-paths&gt;</code> 抓段首 case 引用框架</li>
</ul>
<h3 id="陷阱-6case-引用段首取代核心概念句06-模組新發現">陷阱 6：Case 引用段首取代核心概念句（06 模組新發現）</h3>
<p>LLM 從 case 反推內容時、容易把 case 揭露當概念出發點、寫成「對應 [case]：揭露 N 個機制 — &hellip;」段首結構。讀者尚未理解概念就被丟入案例細節、且跨章讀同句構會感同質。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>06 模組 12 個新段中 11 個用「對應 [case]：揭露 N 個機制」相同句構作為 section 第二段</li>
<li>概念定義句被推到第二段或更後、商業邏輯先於 case 的原則被推翻</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>把 case 引用視為「三段式」結構：概念定義句 → case 引用 → 通用展開</li>
<li>寫每段時、先確認段首是「該概念是什麼、承擔什麼責任」、case 引用退到第二位置</li>
<li>Case 引用句構應變化：寫多章時刻意避免同句構連續超過 3 次</li>
<li>詳見 skill 內部原則卡 <code>principles/case-citation-three-part</code>（對應檔案 <code>.claude/skills/case-first-module-workflow/references/principles/case-citation-three-part.md</code>、屬 skill 內部 reference、不對外暴露）</li>
</ul>
<h3 id="陷阱-7medium-case-實作層擴寫過頭06-模組新發現">陷阱 7：Medium case 實作層擴寫過頭（06 模組新發現）</h3>
<p>Medium case（30-50 行、結構化但無具體數字）首次套用時、容易把 case 沒提的具體實作層擴寫進章節、把通用工程知識掛到 case 名下。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>06 模組 6.12 idempotency-replay 從 S1「key 設計要跟業務邊界一致」一條方向擴寫成「key 來源 / TTL / fallback / 偽造防護 / 5 個 observability 欄位」5 條實作判讀、case 沒提這些細節</li>
<li>06 模組 6.14 dependency-reliability-budget 從 M1 region failover 擴寫成「thundering herd」機制名 + 「先恢復核心 region 最小集合」具體步驟、case 沒提這兩個</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Medium case 引用用 <em>mechanism 名稱</em> 精準引用、不擴寫到 case 沒提的具體實作細節</li>
<li>引用後若要展開實作層、用「以下實作層判讀屬通用工程知識展開、case 本身只給 X 方向」明示分層</li>
<li>Case fidelity reviewer 的 prompt 要特別點出 medium case 的「實作層擴寫」失分類型</li>
</ul>
<h3 id="陷阱-8跨-case-合成-frame-升級成-case-揭露07-模組新發現">陷阱 8：跨 case 合成 frame 升級成 case 揭露（07 模組新發現）</h3>
<p>當段落把多個 case 的失效訊號抽象為更高層 frame（如「跨工具回查壓力」「平台責任切分」）、LLM 會把章節合成的 frame 包裝成 case 揭露。讀者回查 case 時會發現章節說的「case 揭露 X」實際是章節 derive、不是 case 原文框架。</p>
<p>跟陷阱 1（skeleton case 擴寫成 case 事實）跟陷阱 4（rich case 判讀層當 fact）的差別：</p>
<ul>
<li><strong>陷阱 1</strong>：case 沒提的細節（具體數字、taxonomy）被寫成 case 揭露</li>
<li><strong>陷阱 4</strong>：case 有提、但屬作者判讀層的內容被寫成 case fact</li>
<li><strong>陷阱 8</strong>：case <em>單獨</em> 寫的訊號被章節 <em>跨 case 合成</em> 抽象為更高層 frame、frame 本身不在任一 case 原文</li>
</ul>
<p><strong>實證</strong>（07 batch 1 reviewer B 抓的 2 個 high issue）：</p>
<ul>
<li>7.7 跨工具回查壓力：Uber 失效控制面寫「告警串接不足」、Slack 寫「訊號未匯流」— 都是單工具內訊號、章節合成「跨工具回查」axis</li>
<li>7.7 平台責任切分：SolarWinds 失效控制面寫「更新來源信任過於單點」「行為監測難以區分合法元件」— 都是供應鏈信任議題、章節合成「平台 vs 產品 audit 責任分離」frame</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>段落把多 case 抽象為更高層 frame 時、要 explicit 標明「frame 是本章合成、case 原文沒有此 frame」</li>
<li>修法範例：「兩個案例分別在 X 層揭露同類失效訊號 — A case 標明 B、C case 標明 D。本章把兩者抽象為『XXX』是 YYY 視角的合成 frame、非 case 原文框架。」</li>
<li>Stage 3 reviewer B prompt 要明示「跨 case 合成 frame 必須標為本章合成」是 high 級 issue 抓取項</li>
</ul>
<h3 id="陷阱-9case-引用句構同質化07-模組新發現">陷阱 9：Case 引用句構同質化（07 模組新發現）</h3>
<p>即使遵守 case 引用三段式紀律、跨章節 case 引用仍會出現句構同質化。13 處 case 引用 11 處用同一句構「揭露 N 層失效控制面 — A、B、C。案例『可落地檢查點』標明 mechanism 為 X、前提是 Y」。讀者跨章連讀時、會把 case 引用當儀式而非論證。</p>
<p><strong>實證</strong>：07 batch 1 reviewer A 抓出 systemic medium issue (Issue 8.1)、13 段 case 引用 11 段用相同句構。Stage 5 polish pass 主動分流 4 處後狀況改善。</p>
<p><strong>防範</strong>：</p>
<ul>
<li>句構選擇要 <em>跟著 case 類型走</em>、不是隨機變化（case 直接列 N mechanism → 「揭露 N 層」；case 揭露單一壓力 → 「補的失效訊號是 X」；case 揭露對比 → 「揭露兩個層次的對照」）</li>
<li>Stage 5 polish pass 加句構分流為標準工序之一（跟負向骨架同層級）</li>
<li>自掃描 regex <code>^對應 \[</code> 抓不到此類問題（這是符合三段式的引用、只是句構單一）、要靠 stage 5 主動 scan：<code>rg -c &quot;揭露[^。]*失效控制面&quot; &lt;module-paths&gt;</code> 看同句構出現次數、超過 5 處要分流</li>
</ul>
<h3 id="章節已有-routing-skeleton的特殊處理07-模組新發現">「章節已有 routing skeleton」的特殊處理（07 模組新發現）</h3>
<p>07 模組跟 06 / 09 不同之處：章節在 stage 2 前已有完整 routing layer 結構（threat scope / 從本章到實作 / 問題節點表 / 風險邊界 / 案例觸發 / 路由）— stage 2 是在現有結構內補 case-driven 深化段，而非空白擴章。</p>
<p>這個情境下：</p>
<ul>
<li><strong>SSoT 衝突更容易發生</strong>：新段落要跟既有章節結構協調、不只是新增內容。07 batch 1 三個 H issue（C-H1/H2/H3）都是 frame 跟既有章節 / 其他章節新增段衝突</li>
<li><strong>章節寫作邊界要先確認</strong>：補強段聚焦在「現有問題節點表的 mechanism 深化」、不擴成厚重 case-driven 章節（避免章節結構失衡）</li>
<li><strong>Cross-link 密度顯著上升</strong>：補強段要明示「本節聚焦 X 視角、canonical 在 Y 章」、否則 reviewer C 會抓 frame 重複展開</li>
</ul>
<p>判讀條件：</p>
<ul>
<li>章節已有 threat scope / 問題節點表 / 案例觸發段 → 走「補強段」策略、不空白擴章</li>
<li>章節是 routing layer / 導讀性質、不適合 case-driven 深化 → 跳過本流程</li>
<li>章節有 case 庫但 case 主要是 skeleton 型（30 行 frame） → 補強段嚴守「揭露 X 方向、通用補充」紀律、不擴寫實作層</li>
</ul>
<h3 id="衍生-insightreviewer-維度沒覆蓋的部分">衍生 insight：reviewer 維度沒覆蓋的部分</h3>
<p>3 個模組跑下來、發現現有 3 reviewer 維度（規範 / 案例準確性 / 跨章一致性）有未覆蓋的問題：</p>
<ul>
<li><strong>教學性 / 讀者路徑</strong>：章節之間的閱讀順序是否合理？讀者讀完 A 章能不能銜接 B 章？目前沒 reviewer 檢查</li>
<li><strong>判讀條件可操作性</strong>：寫了判讀訊號、但實際工程師能不能用這些訊號做決策？沒 reviewer 驗證</li>
<li><strong>實作可行性</strong>：建議的設計是否真的能落地？跨團隊協調是否現實？需要懂業務的 reviewer</li>
</ul>
<p>未來 6 / 7 / 8 模組執行時、可以考慮加第 4 個 reviewer 維度（教學性 + 實作可行性）。</p>
<h2 id="跟其他寫作流程的差異">跟其他寫作流程的差異</h2>
<p>跟「LLM 自生 + 人工 review」比、本流程的差異：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>LLM 自生 + 人工 review</th>
          <th>Case-first + Agent team</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scope 來源</td>
          <td>訓練資料 + 提示詞</td>
          <td>真實案例 findings</td>
      </tr>
      <tr>
          <td>準確性檢查</td>
          <td>人工讀完對比</td>
          <td>Case fidelity reviewer 自動對照</td>
      </tr>
      <tr>
          <td>規範遵循</td>
          <td>人工 checklist</td>
          <td>Standards reviewer 自動掃描</td>
      </tr>
      <tr>
          <td>跨章一致性</td>
          <td>人工跨檔 grep</td>
          <td>Consistency reviewer 自動檢查</td>
      </tr>
      <tr>
          <td>Context 成本</td>
          <td>低（人工不佔 LLM context）</td>
          <td>中（reviewer 各自佔自己 context、主 context 輕）</td>
      </tr>
      <tr>
          <td>時間成本</td>
          <td>高（人工逐段讀）</td>
          <td>中（reviewer 平行）</td>
      </tr>
      <tr>
          <td>真實事故揭露</td>
          <td>受限於 reviewer 經驗</td>
          <td>受限於案例庫覆蓋</td>
      </tr>
  </tbody>
</table>
<p>跟「LLM 自生 + 自我 review」比：</p>
<ul>
<li>自我 review 抓不到自生內容的盲點（self-blindness）</li>
<li>Agent team 是 <em>不同 instance</em>、不共享 context、能扮演獨立 reviewer</li>
</ul>
<h2 id="下一步">下一步</h2>
<p>本流程在 backend/01 至 backend/07 batch 1 七個模組驗證後（共 ~45 章 / 385 review issue / case fidelity 70-93% 區間）、方法論已工具化為 <code>case-first-module-workflow</code> skill（內部檔 <code>.claude/skills/case-first-module-workflow/</code>、含 stage 1-5 流程、reviewer prompt template、self-scan regex 跟 5 個原則卡）、後續套用到：</p>
<ul>
<li>backend/07 batch 2 LLM 安全：case 庫缺位（OWASP LLM Top 10 + agent injection 公開事件未累積成模組 case）、要先建 LLM case 庫再走 case-first</li>
<li>backend/07 batch 3 治理章節：routing 層 / 導讀性質、case-driven 深化適用度低、做標準 polish pass 即可</li>
<li>backend/08 incident response：跟 04 / 06 / 07 cross-link 密度最高、SSoT 對應規劃壓力最大</li>
<li>其他模組依此類推</li>
</ul>
<p>06 模組是首次套用工具化 skill 的模組、驗證 skill 對 stage 1-2 加速有效、但 reviewer A 仍抓出 45 issue（高於 05 之前 baseline 20-30、推動 v1.2 把 standards reviewer baseline 擴大到 20-45）— 揭露 skill 改進方向（self-scan regex 需要持續演進、case 引用段首結構是 LLM 系統性傾向）。</p>
<p>07 batch 1 驗證下「章節已有 routing skeleton」情境的處理策略：補強段不擴成厚重 case-driven 章節、聚焦 mechanism 深化 + cross-link 對齊。揭露兩個新陷阱（跨 case 合成 frame 升級成 case 揭露、case 引用句構同質化）、補進 skill 跟方法論。</p>
<p>流程本身會在每個模組後 retrospective、看 reviewer 維度是否該調整、findings 抽取方法是否該強化、polish pass 處理 pattern 是否該擴充。目前已知改進方向：</p>
<ul>
<li>加 reviewer：教學性審查（讀者路徑是否清楚、判讀順序是否合理）</li>
<li>強化 findings 抽取：標註 finding 的 <em>泛化程度</em>、避免把 case-specific 細節推為通用結論</li>
<li>Rich / Medium case 引用紀律：把「fact vs derive」分層 + 「mechanism 名稱精準引用」寫進 stage 1 抽 findings 模板、stage 3 case fidelity reviewer prompt 也明示此分界</li>
<li>自掃描 regex 持續演進：每個模組 reviewer 抓出新 pattern 後、回頭加進 self-scan 工具、避免在下個模組重蹈覆轍。06 模組後加 <code>^對應 \[</code> 抓段首 case 引用框架。07 模組後標明 <code>^對應 \[</code> 在三段分離結構下會 false positive、要靠 awk 看 prev line context</li>
<li>Case 引用三段式：把「概念定義 → case 引用 → 通用展開」當段落結構紀律、避免段首被 case 引用取代（06 模組最大宗 systemic 違規）</li>
<li>Case 引用句構分流：07 模組後 stage 5 polish pass 加句構分流為標準工序、避免跨章 13+ 段同句構讀感儀式化</li>
<li>跨 case 合成 frame 紀律：07 模組後 reviewer B prompt 明示「跨 case 合成 frame 必須標為本章合成」是 high 級 issue</li>
<li>加修正後自動 lint：修完不只跑 mdtools、加跑「找首句否定句」「找表格沒延伸」「找模板化並列點」「找段首 case 引用」的自動掃描</li>
</ul>
<p>跟其他寫作協議的整合：本流程跟 <code>compositional-writing</code> skill 互補（後者管 <em>單篇</em> 寫作的原子化跟意圖、本流程管 <em>跨章模組</em> 的 scope 跟一致性）、跟 <code>requirement-protocol</code> skill 互補（後者管 <em>對話協議</em>、本流程管 <em>內容生產</em>）。</p>
]]></content:encoded></item><item><title>Codex 與 Claude Code Statusline 相容設計方法</title><link>https://tarrragon.github.io/blog/record/codex-%E8%88%87-claude-code-statusline-%E7%9B%B8%E5%AE%B9%E8%A8%AD%E8%A8%88%E6%96%B9%E6%B3%95/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/codex-%E8%88%87-claude-code-statusline-%E7%9B%B8%E5%AE%B9%E8%A8%AD%E8%A8%88%E6%96%B9%E6%B3%95/</guid><description>&lt;h2 id="問題錨點">問題錨點&lt;/h2>
&lt;p>Statusline 相容設計的核心責任是把「資料輸入契約」和「畫面渲染邏輯」分開。Claude Code 已經提供 command-backed statusline，會把 session JSON 丟進命令的 stdin；Codex 目前公開的設定則是 &lt;code>tui.status_line&lt;/code> 字串項目陣列，契約停在內建 footer item 的排列與選擇。&lt;/p>
&lt;p>這個差異讓「同一個 statusline 工具同時支援兩邊」要從輸入契約對齊開始。真正要做的是先建立一層輸入正規化：Claude JSON、Codex 既有或未來 JSON、手動測試 JSON 都先轉成同一個內部狀態，再交給同一套 renderer。&lt;/p>
&lt;h2 id="case-first-觀察">Case-first 觀察&lt;/h2>
&lt;p>Case-first 查詢的目的，是先看社群實際卡在哪裡，再決定要改工具還是改使用方式。本次查詢到的案例集中在 OpenAI Codex repo issue 與官方文件，顯示需求已經存在，但 Codex 的 command-backed statusline 仍屬提案或缺口。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Case&lt;/th>
 &lt;th>觀察&lt;/th>
 &lt;th>判讀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://code.claude.com/docs/en/statusline">Claude Code status line 官方文件&lt;/a>&lt;/td>
 &lt;td>Claude Code 的 statusline command 會從 stdin 收到 JSON，stdout 的每一行會顯示成 status area。&lt;/td>
 &lt;td>Claude 端是穩定可用的 producer，工具可依賴 &lt;code>model&lt;/code>、&lt;code>workspace&lt;/code>、&lt;code>context_window&lt;/code>、&lt;code>rate_limits&lt;/code> 這類欄位。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://developers.openai.com/codex/config-reference">OpenAI Codex config reference&lt;/a>&lt;/td>
 &lt;td>&lt;code>tui.status_line&lt;/code> 的型別是 &lt;code>array&amp;lt;string&amp;gt;&lt;/code> 或 &lt;code>null&lt;/code>，用途是排列 footer status-line item identifiers。&lt;/td>
 &lt;td>Codex 端目前公開契約屬於內建項目清單。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://github.com/openai/codex/issues/17827">openai/codex #17827&lt;/a>&lt;/td>
 &lt;td>使用者明確要求 Codex 加入類似 Claude Code 的 &lt;code>statusLine.command&lt;/code>。&lt;/td>
 &lt;td>社群已把 Claude Code statusline 當成對照基準，混用痛點是真實需求。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://github.com/openai/codex/issues/20043">openai/codex #20043&lt;/a>&lt;/td>
 &lt;td>提案列出 Codex 既有 &lt;code>status_line&lt;/code> picker，並要求外部 command 模式、ANSI 顏色與 stdin JSON。&lt;/td>
 &lt;td>未來若 Codex 採納此類設計，statusline 工具需要同時支援 Codex 風格 JSON 與 Claude 欄位。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://github.com/openai/codex/issues/20244">openai/codex #20244&lt;/a>&lt;/td>
 &lt;td>另一個使用者提出 command-backed item 或 persistent banner，並被標為 #17827 的 duplicate。&lt;/td>
 &lt;td>重複 issue 表示需求已經多次出現；相容設計應預留 Codex command input，讓後續定案只需要調整 mapper。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://github.com/openai/codex/issues/21324">openai/codex #21324&lt;/a>&lt;/td>
 &lt;td>使用者在 local branch 實作 context/token usage 狀態項目與進度條。&lt;/td>
 &lt;td>Codex 社群也在補足使用量可視化，但路徑偏向內建 item，和 Claude 的外部 renderer 是兩種不同擴充模型。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="wrap-判讀">WRAP 判讀&lt;/h2>
&lt;p>Anchor Check：目標是讓 &lt;code>cc-statusline&lt;/code> 的核心能力可被兩種工具共用。使用者真正需要的是少維護一套 statusline 邏輯，並在 Codex 具備 command-backed 入口時保留既有 renderer。&lt;/p>
&lt;p>Step 0 資料充足度：足以做工具內部改造，尚不足以宣稱 Codex TUI 目前能直接執行 &lt;code>cc-statusline&lt;/code>。官方文件只保證 &lt;code>tui.status_line&lt;/code> 是字串陣列；社群 issue 裡的 command JSON 仍是提案階段。&lt;/p>
&lt;p>Widen Options：可選方案有三種。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>選項&lt;/th>
 &lt;th>策略&lt;/th>
 &lt;th>適用條件&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>A：只用 Codex 內建 &lt;code>tui.status_line&lt;/code>&lt;/td>
 &lt;td>不改 &lt;code>cc-statusline&lt;/code>，在 Codex 設定內建項目。&lt;/td>
 &lt;td>只需要模型、目錄、git branch、context 這類內建欄位時可用。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B：改 &lt;code>cc-statusline&lt;/code> 成雙 schema renderer&lt;/td>
 &lt;td>保留 Claude JSON，新增 Codex / generic JSON normalization。&lt;/td>
 &lt;td>希望同一套 renderer 服務 Claude、未來 Codex command hook、tmux / wrapper 測試時最划算。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>C：Fork 兩套工具&lt;/td>
 &lt;td>Claude 一套、Codex 一套，各自用不同資料模型。&lt;/td>
 &lt;td>只有在兩邊 UI 契約長期分歧且需求完全不同時才合理。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Reality Test：目前 Codex 的公開設定停在內建 item 排列，所以 B 的立即價值是讓工具「具備 Codex 相容輸入能力」。反向驗證是：若未來 Codex 最終採用完全不同的 command JSON，B 的 normalization 層仍只需新增一個 mapper，renderer 可維持同一套。&lt;/p></description><content:encoded><![CDATA[<h2 id="問題錨點">問題錨點</h2>
<p>Statusline 相容設計的核心責任是把「資料輸入契約」和「畫面渲染邏輯」分開。Claude Code 已經提供 command-backed statusline，會把 session JSON 丟進命令的 stdin；Codex 目前公開的設定則是 <code>tui.status_line</code> 字串項目陣列，契約停在內建 footer item 的排列與選擇。</p>
<p>這個差異讓「同一個 statusline 工具同時支援兩邊」要從輸入契約對齊開始。真正要做的是先建立一層輸入正規化：Claude JSON、Codex 既有或未來 JSON、手動測試 JSON 都先轉成同一個內部狀態，再交給同一套 renderer。</p>
<h2 id="case-first-觀察">Case-first 觀察</h2>
<p>Case-first 查詢的目的，是先看社群實際卡在哪裡，再決定要改工具還是改使用方式。本次查詢到的案例集中在 OpenAI Codex repo issue 與官方文件，顯示需求已經存在，但 Codex 的 command-backed statusline 仍屬提案或缺口。</p>
<table>
  <thead>
      <tr>
          <th>Case</th>
          <th>觀察</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://code.claude.com/docs/en/statusline">Claude Code status line 官方文件</a></td>
          <td>Claude Code 的 statusline command 會從 stdin 收到 JSON，stdout 的每一行會顯示成 status area。</td>
          <td>Claude 端是穩定可用的 producer，工具可依賴 <code>model</code>、<code>workspace</code>、<code>context_window</code>、<code>rate_limits</code> 這類欄位。</td>
      </tr>
      <tr>
          <td><a href="https://developers.openai.com/codex/config-reference">OpenAI Codex config reference</a></td>
          <td><code>tui.status_line</code> 的型別是 <code>array&lt;string&gt;</code> 或 <code>null</code>，用途是排列 footer status-line item identifiers。</td>
          <td>Codex 端目前公開契約屬於內建項目清單。</td>
      </tr>
      <tr>
          <td><a href="https://github.com/openai/codex/issues/17827">openai/codex #17827</a></td>
          <td>使用者明確要求 Codex 加入類似 Claude Code 的 <code>statusLine.command</code>。</td>
          <td>社群已把 Claude Code statusline 當成對照基準，混用痛點是真實需求。</td>
      </tr>
      <tr>
          <td><a href="https://github.com/openai/codex/issues/20043">openai/codex #20043</a></td>
          <td>提案列出 Codex 既有 <code>status_line</code> picker，並要求外部 command 模式、ANSI 顏色與 stdin JSON。</td>
          <td>未來若 Codex 採納此類設計，statusline 工具需要同時支援 Codex 風格 JSON 與 Claude 欄位。</td>
      </tr>
      <tr>
          <td><a href="https://github.com/openai/codex/issues/20244">openai/codex #20244</a></td>
          <td>另一個使用者提出 command-backed item 或 persistent banner，並被標為 #17827 的 duplicate。</td>
          <td>重複 issue 表示需求已經多次出現；相容設計應預留 Codex command input，讓後續定案只需要調整 mapper。</td>
      </tr>
      <tr>
          <td><a href="https://github.com/openai/codex/issues/21324">openai/codex #21324</a></td>
          <td>使用者在 local branch 實作 context/token usage 狀態項目與進度條。</td>
          <td>Codex 社群也在補足使用量可視化，但路徑偏向內建 item，和 Claude 的外部 renderer 是兩種不同擴充模型。</td>
      </tr>
  </tbody>
</table>
<h2 id="wrap-判讀">WRAP 判讀</h2>
<p>Anchor Check：目標是讓 <code>cc-statusline</code> 的核心能力可被兩種工具共用。使用者真正需要的是少維護一套 statusline 邏輯，並在 Codex 具備 command-backed 入口時保留既有 renderer。</p>
<p>Step 0 資料充足度：足以做工具內部改造，尚不足以宣稱 Codex TUI 目前能直接執行 <code>cc-statusline</code>。官方文件只保證 <code>tui.status_line</code> 是字串陣列；社群 issue 裡的 command JSON 仍是提案階段。</p>
<p>Widen Options：可選方案有三種。</p>
<table>
  <thead>
      <tr>
          <th>選項</th>
          <th>策略</th>
          <th>適用條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A：只用 Codex 內建 <code>tui.status_line</code></td>
          <td>不改 <code>cc-statusline</code>，在 Codex 設定內建項目。</td>
          <td>只需要模型、目錄、git branch、context 這類內建欄位時可用。</td>
      </tr>
      <tr>
          <td>B：改 <code>cc-statusline</code> 成雙 schema renderer</td>
          <td>保留 Claude JSON，新增 Codex / generic JSON normalization。</td>
          <td>希望同一套 renderer 服務 Claude、未來 Codex command hook、tmux / wrapper 測試時最划算。</td>
      </tr>
      <tr>
          <td>C：Fork 兩套工具</td>
          <td>Claude 一套、Codex 一套，各自用不同資料模型。</td>
          <td>只有在兩邊 UI 契約長期分歧且需求完全不同時才合理。</td>
      </tr>
  </tbody>
</table>
<p>Reality Test：目前 Codex 的公開設定停在內建 item 排列，所以 B 的立即價值是讓工具「具備 Codex 相容輸入能力」。反向驗證是：若未來 Codex 最終採用完全不同的 command JSON，B 的 normalization 層仍只需新增一個 mapper，renderer 可維持同一套。</p>
<p>Attain Distance：B 的長期成本最低，因為 statusline 最容易變動的是輸入欄位名稱，最穩定的是使用者想看的資訊：專案、環境、輸入法、模型、context、rate limit、git worktree。把欄位差異收斂在 normalization 層，能避免每加入一個工具就複製一次畫面邏輯。</p>
<p>Prepare to be Wrong：若 Codex 不採納外部 command statusline，這次改造仍可用於手動測試、tmux status、其他 wrapper，且不影響 Claude Code 原始入口。若 Codex 採納但欄位名稱不同，新增 mapper 即可。</p>
<p>Tripwire：當 OpenAI Codex 文件把 <code>tui.status_line</code> 從 <code>array&lt;string&gt;</code> 擴充為 command 或 table schema 時，重新檢查 <code>cc-statusline</code> 的 Codex mapper。若 Codex issue #17827 關閉並附帶實作 PR，也應重新校準欄位名稱。</p>
<h2 id="實作策略">實作策略</h2>
<p>相容設計的正確切點是輸入正規化層。<code>cc-statusline</code> 應維持一個內部狀態模型，並接受多種外部 payload：</p>
<table>
  <thead>
      <tr>
          <th>外部 payload</th>
          <th>正規化規則</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Code</td>
          <td>直接讀 <code>model.display_name</code>、<code>workspace.project_dir</code>、<code>context_window.used_percentage</code>、<code>rate_limits</code>。</td>
      </tr>
      <tr>
          <td>Codex proposed / generic</td>
          <td>接受 <code>model</code> 字串、<code>cwd</code> / <code>project_root</code>、<code>context.used_percent</code> / <code>context.remaining_percent</code>、<code>limits.five_hour</code> / <code>limits.weekly</code>。</td>
      </tr>
      <tr>
          <td>手動測試 payload</td>
          <td>只要能提供模型與目錄，就輸出可讀 statusline；缺 rate limit 時自動省略。</td>
      </tr>
  </tbody>
</table>
<p>這個切點保留了 Claude Code 既有功能，因為原本的欄位不需要改名，也不需要改設定檔。新增行為只在非 Claude payload 進來時啟動，屬於向後相容的讀取能力。</p>
<h2 id="操作路由">操作路由</h2>
<p>現在可立即使用的路由是 Claude Code 原設定：在 <code>~/.claude/settings.json</code> 裡設定 <code>statusLine.command</code> 指向 <code>cc-statusline</code>。這條路由使用官方支援的 stdin JSON，適合日常使用。</p>
<p>Codex 目前可立即使用的路由是內建 footer item：在 <code>~/.codex/config.toml</code> 設定 <code>tui.status_line = [...]</code>。這條路由使用 Codex 內建 renderer，能顯示 Codex 已支援的內建狀態。</p>
<p>未來 Codex 若支援 command-backed statusline，路由應該指向同一個 <code>cc-statusline</code> binary。工具端已經能接受 Codex / generic JSON 時，設定層只要補 command 指向，不需要重寫 renderer。</p>
<h2 id="實測記錄2026-05-14">實測記錄（2026-05-14）</h2>
<p>這次排查的核心責任是先確認「工具本身可用」還是「接入路由不對」。先把 binary 行為跟 TUI 設定拆開檢查，才能避免把路由問題誤判成程式 bug。</p>
<h3 id="觀察">觀察</h3>
<ul>
<li><code>cc-statusline</code> 程式已支援 generic/Codex-style payload，手動餵 JSON 可正確輸出模型與 context 資訊。</li>
<li><code>~/.claude/settings.json</code> 使用 <code>statusLine.command</code> 指向 <code>/Users/mac-eric/go/bin/cc-statusline</code>，Claude Code 路由成立。</li>
<li><code>~/.codex/config.toml</code> 的 <code>tui.status_line</code> 是內建 item 陣列，這條路由不會執行外部 binary。</li>
<li>Codex 內建 footer 的實際輸出已觀察到：<code>gpt-5.3-codex medium · Context 100% left · ~/project/blog</code>。</li>
</ul>
<h3 id="判讀">判讀</h3>
<p>Codex 端「沒有生效」的主因是契約邊界：<code>tui.status_line</code> 只負責排列內建欄位，不負責執行 command。<code>cc-statusline</code> 的 renderer 相容能力屬於預留未來入口，不會在現有 Codex 內建 footer 流程自動觸發。</p>
<h3 id="操作">操作</h3>
<p>為了讓 Codex 內建 footer 至少顯示模型與 context 資訊，已調整：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="nx">tui</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">status_line</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;model-with-reasoning&#34;</span><span class="p">,</span> <span class="s2">&#34;context-remaining&#34;</span><span class="p">,</span> <span class="s2">&#34;current-dir&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">status_line_use_colors</span> <span class="p">=</span> <span class="kc">true</span></span></span></code></pre></div><p>這個設定可讓 Codex 使用內建項目顯示 <code>model-with-reasoning</code> 與 context remaining；格式由 Codex 內建 renderer 決定，不等同 <code>cc-statusline</code> 的自訂輸出字串。</p>
<h3 id="驗證指令">驗證指令</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s1">&#39;{&#34;model&#34;:&#34;gpt-5.3-codex&#34;,&#34;reasoning_effort&#34;:&#34;medium&#34;,&#34;project_root&#34;:&#34;~/project/blog&#34;,&#34;context&#34;:{&#34;remaining_percent&#34;:100}}&#39;</span> <span class="p">|</span> /Users/mac-eric/go/bin/cc-statusline</span></span></code></pre></div><p>預期結果是主行包含 <code>gpt-5.3-codex medium</code>，context 顯示為 <code>Context 100% left</code>。這一步驗證的是 binary 能力，不是 Codex 內建 footer contract。</p>
<h2 id="檢查清單">檢查清單</h2>
<ul>
<li>Claude Code 原本的 JSON payload 仍能輸出相同欄位。</li>
<li>Codex / generic payload 不造成 parse error。</li>
<li><code>model</code> 同時支援 object 與 string。</li>
<li><code>context</code> 同時支援 used percentage 與 remaining percentage。</li>
<li>rate limit 缺席時只省略對應 segment，不影響專案、模型、git worktree。</li>
<li>README 明確標示 Codex 目前限制，避免讀者以為 Codex 已能直接執行外部 statusline command。</li>
</ul>
]]></content:encoded></item><item><title>用 Claude Code GitHub Actions 自動除錯 CI 建置失敗</title><link>https://tarrragon.github.io/blog/posts/%E7%94%A8-claude-code-github-actions-%E8%87%AA%E5%8B%95%E9%99%A4%E9%8C%AF-ci-%E5%BB%BA%E7%BD%AE%E5%A4%B1%E6%95%97/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E7%94%A8-claude-code-github-actions-%E8%87%AA%E5%8B%95%E9%99%A4%E9%8C%AF-ci-%E5%BB%BA%E7%BD%AE%E5%A4%B1%E6%95%97/</guid><description>&lt;h2 id="這是什麼">這是什麼&lt;/h2>
&lt;p>&lt;a href="https://github.com/anthropics/claude-code-action">Claude Code GitHub Actions&lt;/a> 讓 Claude 直接參與你的 GitHub 工作流程，主要功能：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>互動式助手&lt;/strong> — 在 PR/Issue 留言 &lt;code>@claude&lt;/code>，Claude 會分析程式碼並回覆&lt;/li>
&lt;li>&lt;strong>自動 Code Review&lt;/strong> — PR 開啟時自動審查變更&lt;/li>
&lt;li>&lt;strong>CI 除錯修復&lt;/strong> — build 失敗時自動分析錯誤並修復&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="這是什麼">這是什麼</h2>
<p><a href="https://github.com/anthropics/claude-code-action">Claude Code GitHub Actions</a> 讓 Claude 直接參與你的 GitHub 工作流程，主要功能：</p>
<ul>
<li><strong>互動式助手</strong> — 在 PR/Issue 留言 <code>@claude</code>，Claude 會分析程式碼並回覆</li>
<li><strong>自動 Code Review</strong> — PR 開啟時自動審查變更</li>
<li><strong>CI 除錯修復</strong> — build 失敗時自動分析錯誤並修復</li>
</ul>
<p>完整功能說明參考 <a href="https://code.claude.com/docs/en/github-actions">官方文件</a>。</p>
<h2 id="設定方式">設定方式</h2>
<h3 id="install-github-app推薦"><code>/install-github-app</code>（推薦）</h3>
<p>在 Claude Code 終端執行 <code>/install-github-app</code>，它會引導你完成所有設定。</p>
<p>流程中的關鍵步驟：</p>
<ol>
<li><strong>選擇 repo</strong> — 指定要安裝的 GitHub repository</li>
<li><strong>安裝 Claude GitHub App</strong> — 自動安裝到指定 repo，授予 Contents、Issues、Pull requests 的 Read &amp; Write 權限</li>
<li><strong>選擇認證方式</strong> — 選擇 <strong>long-life token</strong> 會產生 OAuth token，自動寫入 GitHub Secrets 為 <code>CLAUDE_CODE_OAUTH_TOKEN</code></li>
<li><strong>建立 workflow 檔案</strong> — 自動建立並 push 兩個 workflow：
<ul>
<li><code>claude.yml</code> — <code>@claude</code> 互動回覆</li>
<li><code>claude-code-review.yml</code> — PR 自動 code review</li>
</ul>
</li>
</ol>
<p>完成後不需要額外設定。</p>
<h3 id="手動設定使用-anthropic-api-key">手動設定（使用 Anthropic API Key）</h3>
<p>如果不想用 <code>/install-github-app</code>，可以手動操作：</p>
<ol>
<li>前往 <a href="https://github.com/apps/claude">github.com/apps/claude</a> 安裝 App 到你的 repo</li>
<li>到 repo 的 <strong>Settings → Secrets and variables → Actions</strong>，新增 <code>ANTHROPIC_API_KEY</code></li>
<li>手動建立 workflow 檔案到 <code>.github/workflows/</code></li>
</ol>
<p>兩種認證方式的差異：</p>
<table>
  <thead>
      <tr>
          <th>認證方式</th>
          <th>Secret 名稱</th>
          <th>適用對象</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OAuth Token</td>
          <td><code>CLAUDE_CODE_OAUTH_TOKEN</code></td>
          <td>Pro/Max 用戶，<code>/install-github-app</code> 自動設定</td>
      </tr>
      <tr>
          <td>API Key</td>
          <td><code>ANTHROPIC_API_KEY</code></td>
          <td>直接使用 Anthropic API，需手動到 <a href="https://console.anthropic.com">console.anthropic.com</a> 取得</td>
      </tr>
  </tbody>
</table>
<h2 id="加入-ci-自動除錯">加入 CI 自動除錯</h2>
<p><code>/install-github-app</code> 建立的 workflow 只處理 <code>@claude</code> 互動和 code review。如果你想在 <strong>build 失敗時自動觸發 Claude 修復</strong>，需要修改既有的 deploy workflow。</p>
<p>首先，補上 Claude 需要的權限（原本可能只有 <code>contents: read</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">write       </span><span class="w"> </span><span class="c"># Claude 需要寫入修復後的檔案</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">pull-requests</span><span class="p">:</span><span class="w"> </span><span class="l">write  </span><span class="w"> </span><span class="c"># Claude 可能需要建立 PR</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">issues</span><span class="p">:</span><span class="w"> </span><span class="l">write         </span><span class="w"> </span><span class="c"># Claude 回報結果</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">pages</span><span class="p">:</span><span class="w"> </span><span class="l">write          </span><span class="w"> </span><span class="c"># 原本的 deploy 權限</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write       </span><span class="w"> </span><span class="c"># 原本的 deploy 權限</span></span></span></code></pre></div><p>然後在 build 步驟加入 Claude 除錯邏輯：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># 在原本的 build step 加上 continue-on-error 和 id</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">hugo-build</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">hugo --minify 2&gt;&amp;1 | tee hugo-build-output.txt</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">continue-on-error</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c"># Build 失敗時觸發 Claude 除錯</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Claude Debug on Build Failure</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">steps.hugo-build.outcome == &#39;failure&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">anthropics/claude-code-action@v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="c"># 依你的認證方式擇一</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">claude_code_oauth_token</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="c"># anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">prompt</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="sd">      Hugo build failed. Here is the error output:
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="sd">      $(cat hugo-build-output.txt)
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="sd">      Please analyze the error, find the problematic file(s),
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="sd">      fix the YAML front matter or content issue, and commit the fix.</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">claude_args</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;--max-turns 10&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w"></span><span class="c"># 修復後重新 build 驗證</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Retry build after fix</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">  </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">steps.hugo-build.outcome == &#39;failure&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">hugo --minify</span></span></span></code></pre></div><p>核心設計：</p>
<ol>
<li><code>continue-on-error: true</code> — build 失敗不中斷流程，讓後續 Claude 步驟有機會執行</li>
<li><code>if: steps.hugo-build.outcome == 'failure'</code> — 只在失敗時觸發，正常 build 不消耗 API 額度</li>
<li>修復後重新 <code>hugo --minify</code> 驗證是否成功</li>
</ol>
<h2 id="計費方式">計費方式</h2>
<p>計費取決於你使用哪種認證方式：</p>
<table>
  <thead>
      <tr>
          <th>認證方式</th>
          <th>計費來源</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OAuth Token</td>
          <td><strong>訂閱額度</strong>（Pro/Max）</td>
          <td>跟 claude.ai 網頁、Claude Code CLI、Claude Desktop <strong>共用同一個額度池</strong></td>
      </tr>
      <tr>
          <td>API Key</td>
          <td><strong>獨立 API 計費</strong></td>
          <td>按 token 用量付費，與訂閱額度完全分開</td>
      </tr>
  </tbody>
</table>
<p>OAuth token 的額度是共用的，GitHub Actions 跑多了會擠壓你日常在 claude.ai 和 CLI 的使用額度。如果 CI 觸發頻繁，建議改用 API Key 避免互相影響。</p>
<p>詳細的費率可參考 <a href="https://www.anthropic.com/pricing">Claude 定價頁面</a>。</p>
<h3 id="降低成本的設定">降低成本的設定</h3>
<table>
  <thead>
      <tr>
          <th>設定</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--max-turns 10</code></td>
          <td>限制迭代次數，避免無限循環</td>
      </tr>
      <tr>
          <td>只在 <code>failure</code> 時觸發</td>
          <td>正常 build 不消耗 API 額度</td>
      </tr>
      <tr>
          <td><code>@claude</code> 觸發詞</td>
          <td>互動模式只在明確呼叫時才啟動</td>
      </tr>
  </tbody>
</table>
<h2 id="搭配-claudemd">搭配 CLAUDE.md</h2>
<p>在 repo 根目錄建立 <code>CLAUDE.md</code>，Claude 會自動讀取作為上下文，提升修復準確度。</p>
<h2 id="參考資料">參考資料</h2>
<ul>
<li><a href="https://code.claude.com/docs/en/github-actions">Claude Code GitHub Actions 官方文件</a></li>
<li><a href="https://github.com/anthropics/claude-code-action">claude-code-action GitHub Repo</a></li>
<li><a href="https://github.com/anthropics/claude-code-action/blob/main/docs/setup.md">Setup Guide</a></li>
</ul>]]></content:encoded></item><item><title>用 Hook 系統把開發規範變成自動執行的基礎設施</title><link>https://tarrragon.github.io/blog/record/%E7%94%A8-hook-%E7%B3%BB%E7%B5%B1%E6%8A%8A%E9%96%8B%E7%99%BC%E8%A6%8F%E7%AF%84%E8%AE%8A%E6%88%90%E8%87%AA%E5%8B%95%E5%9F%B7%E8%A1%8C%E7%9A%84%E5%9F%BA%E7%A4%8E%E8%A8%AD%E6%96%BD/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E7%94%A8-hook-%E7%B3%BB%E7%B5%B1%E6%8A%8A%E9%96%8B%E7%99%BC%E8%A6%8F%E7%AF%84%E8%AE%8A%E6%88%90%E8%87%AA%E5%8B%95%E5%9F%B7%E8%A1%8C%E7%9A%84%E5%9F%BA%E7%A4%8E%E8%A8%AD%E6%96%BD/</guid><description>&lt;h3 id="從一個社群文章開始">從一個社群文章開始&lt;/h3>
&lt;p>社群裡有人分享了幾篇關於 Claude Code Hook 的文章，看完第一個反應是：「我低估了 hook 能做的事情。」&lt;/p>
&lt;p>我知道 hook 的存在，但一直把它當成「跑幾個簡單檢查」的輔助工具。那幾篇文章讓我意識到，hook 可以是完整的品質控制基礎設施——在每個關鍵時機介入，執行那些「應該要做但忘記做」的檢查。&lt;/p></description><content:encoded><![CDATA[<h3 id="從一個社群文章開始">從一個社群文章開始</h3>
<p>社群裡有人分享了幾篇關於 Claude Code Hook 的文章，看完第一個反應是：「我低估了 hook 能做的事情。」</p>
<p>我知道 hook 的存在，但一直把它當成「跑幾個簡單檢查」的輔助工具。那幾篇文章讓我意識到，hook 可以是完整的品質控制基礎設施——在每個關鍵時機介入，執行那些「應該要做但忘記做」的檢查。</p>
<h3 id="hook-的執行時機">Hook 的執行時機</h3>
<p>Claude Code Hook 有五個觸發點：<code>SessionStart</code>、<code>UserPromptSubmit</code>、<code>PreToolUse</code>、<code>PostToolUse</code>、<code>Stop</code>，涵蓋整個開發互動的生命週期。</p>
<p>有了這個框架，問題就變成：哪些規範應該在哪個時機點執行？</p>
<h3 id="我們建立了哪些-hook">我們建立了哪些 Hook</h3>
<h4 id="session-啟動檢查">Session 啟動檢查</h4>
<p>每次 session 開始時自動確認：git 遠端是否有需要同步的變更、開發環境依賴是否完整、工作日誌狀態。這些檢查不阻止啟動，只是讓開發者一開始就有完整的情境。</p>
<h5 id="任務逃避偵測">任務逃避偵測</h5>
<p>這是我們設計中比較有趣的一個 hook。它在每次 prompt 提交時執行，掃描內容裡是否出現「太複雜先跳過」、「暫時不處理」這類詞彙，同時也會檢查行為模式：程式碼變更了但測試沒有對應變更、技術債務累積超過閾值等。一旦偵測到逃避行為，會建立一個 block 標記檔案，後續所有工具呼叫都會被阻止，直到問題被正視。</p>
<h6 id="程式異味即時偵測">程式異味即時偵測</h6>
<p>每次檔案編輯後（<code>PostToolUse</code>），我們會即時掃描變更的程式碼。函數超過 30 行、巢狀超過 4 層、參數超過 5 個、依賴數超過 10 個，這些都會觸發記錄並建議重構。這個 hook 採用非阻塞設計——發現問題時記錄，但不中斷開發流程。</p>
<h6 id="版本推進建議">版本推進建議</h6>
<p>在 Claude 完成回應時（<code>Stop</code>），hook 會分析當前的工作狀態：有沒有未提交的變更、工作日誌有沒有標記完成、TodoList 系列是否達成。根據這些狀態，自動建議接下來應該做小版本推進還是繼續開發。</p>
<h6 id="文件同步提醒">文件同步提醒</h6>
<p>程式碼變更後，hook 根據檔案類型判斷哪些文件需要同步更新。API 異動對應 API 文件，架構異動對應架構文件，這類規則很難靠人記，但 hook 記得。</p>
<h3 id="一個反覆遇到的事故">一個反覆遇到的事故</h3>
<p>hook 開發過程中出過同一個事故不只一次。</p>
<p>Claude Code hook 系統的設計是：任何寫入 <code>stderr</code> 的輸出都會被視為 hook error 顯示給使用者。Python 的 <code>logging</code> 模組預設輸出到 stderr，所以即使 hook 正常執行，只要有 logging 輸出，UI 上就會冒出 hook error 警告。</p>
<p>這個問題反覆出現，最後系統性地修復，確立了一條規則：hook 禁止寫入 stderr，所有輸出必須走 stdout。現在這條規則寫進方法論，新 hook 都有對應的驗證：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">grep -r <span class="s2">&#34;sys\.stderr&#34;</span> .claude/hooks/ --include<span class="o">=</span><span class="s2">&#34;*.py&#34;</span></span></span></code></pre></div><p>這個指令應該永遠返回空結果。</p>
<h3 id="模組化的演進">模組化的演進</h3>
<p>剛開始每個 hook 腳本都是獨立的，讀取輸入、處理邏輯、輸出結果各自實作。問題很明顯：讀取 hook 輸入、輸出決策結果這些通用邏輯，在每個腳本裡都重複了一遍。</p>
<p>後來重構引入了共用模組。<code>.claude/lib/</code> 底下現在有幾個核心模組：<code>hook_io.py</code> 負責標準化 I/O，<code>hook_logging.py</code> 負責日誌，<code>config_loader.py</code> 載入配置，<code>git_utils.py</code> 封裝 git 操作。</p>
<p>帶來兩個好處：腳本結構變得簡潔，開發者只需要專注判斷邏輯；共用模組可以寫獨立的單元測試，以前 hook 的正確性很難驗證，現在有了。</p>
<h3 id="幾個設計原則">幾個設計原則</h3>
<p><strong>非阻塞優先</strong>。大部分品質檢查不該阻止開發流程，而是記錄、追蹤、提示。只有真正關鍵的違規——任務逃避、阻止狀態——才完全阻斷操作。</p>
<p><strong>漸進式強制</strong>。從警告到記錄到追蹤到阻止，給開發者理解和修正的機會，不是一刀切拒絕。</p>
<p><strong>可觀測性</strong>。hook 系統自己也需要被監控。有一個 performance monitor hook 專門追蹤其他 hook 的執行時間，超過 5 秒視為需要立即優化。</p>
<p><strong>配置外部化</strong>。品質規則的閾值、代理人分派規則，全部放在 YAML 配置檔，不硬編碼在腳本裡。需要調整只改配置。</p>
<h3 id="現在的感受">現在的感受</h3>
<p>把規範寫進文件和寫進 hook，是完全不同的感受。</p>
<p>寫進文件的規範靠人記。寫進 hook 的規範，在每個關鍵時機自動執行，開發者甚至可以不知道它存在——但規範確實在發生效用。品質基線從依賴個人紀律，變成由系統保證。</p>]]></content:encoded></item></channel></rss>