<?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>MCP on Tarragon</title><link>https://tarrragon.github.io/blog/tags/mcp/</link><description>Recent content in MCP 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/mcp/index.xml" rel="self" type="application/rss+xml"/><item><title>6.2 tool use 與 MCP server 的權限模型</title><link>https://tarrragon.github.io/blog/llm/06-security/tool-use-permission-model/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/06-security/tool-use-permission-model/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/tool-use/" data-link-title="Tool Use" data-link-desc="LLM 透過結構化呼叫外部工具（讀檔、查資料庫、發 API request）來擴展能力的設計、function calling 跟 MCP 是常見實作">Tool use&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP&lt;/a> server 是本地 LLM 對主機資源最大的副作用面。本章把「這個 tool 能做什麼」「MCP server 跑了會碰到什麼檔案」「能不能 rollback」整理成可操作的權限判讀。原理層的副作用範圍 spectrum、可逆性分級見 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">4.3 Tool use 原理&lt;/a>、agent 跟人類審查的協作模型見 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4&lt;/a>；hands-on 驗證「LLM 自己沒 FS / shell 權限、wrapper 才有」見 &lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/permission-boundary/" data-link-title="Hands-on：Ollama 改檔案 / 寫程式碼的權限邊界在哪" data-link-desc="四組對照實驗：Ollama 自己沒 FS / shell 權限、wrapper 才有；--dry-run / --confirm / --auto 三檔審查粒度的取捨">Ollama 改檔案的權限邊界&lt;/a>。隔離技術見 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/sandbox/" data-link-title="Sandbox" data-link-desc="把程式跑在受限制環境的隔離技術、限制檔案 / 網路 / 系統呼叫權限、是 tool use 跟 MCP server 副作用控制的基礎">sandbox&lt;/a> 卡、權限白名單見 backend &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/allowlist/" data-link-title="Allowlist" data-link-desc="說明如何用明確允許條件控制例外放行範圍">allowlist&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/least-privilege/" data-link-title="Least Privilege" data-link-desc="說明身份、服務與人員只應取得完成工作所需的最小權限">least-privilege&lt;/a> 卡。本章 framing 是個人 dev 視角；production agent 場景下 tool use 引發的 prompt injection 後果見 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-prompt-injection-in-agent/" data-link-title="LLM Agent Prompt Injection 後果治理" data-link-desc="production LLM agent 場景的 prompt injection 後果：tool spec 設計、agent loop 限制、review checkpoint、跟 incident workflow 的接合">backend/07 LLM agent prompt injection&lt;/a>。&lt;/p>
&lt;p>讀完本章後、你應該能對自己用的 tool / MCP server 回答：能讀寫哪些路徑、能跑哪些 shell command、能連哪些網路位址、副作用有沒有 dry-run / preview、出錯時怎麼回退。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;ol>
&lt;li>認識 tool use 跟 MCP server 在三層架構中的位置。&lt;/li>
&lt;li>區分「讀取類 tool」跟「副作用類 tool」的權限判讀差異。&lt;/li>
&lt;li>知道個人 dev 場景下、第三方 MCP server 的信任邊界跟驗證流程。&lt;/li>
&lt;li>用「沙箱 / 白名單 / 副作用可逆性」三個維度評估具體 tool / MCP 的風險。&lt;/li>
&lt;li>認識常見的 tool use 副作用洩漏路徑跟對應的最低防護。&lt;/li>
&lt;/ol>
&lt;h2 id="tool-use-跟-mcp-server-在哪一層">tool use 跟 MCP server 在哪一層&lt;/h2>
&lt;p>tool use 跟 MCP server 同時跨&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/three-layer-architecture/" data-link-title="Three-Layer Architecture" data-link-desc="把本地 LLM 工具拆成介面層、推論伺服器層、模型權重層的基礎心智模型">三層架構&lt;/a> 的兩層、但跟模型本身的權限模型分離：&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/llm/knowledge-cards/tool-use/" data-link-title="Tool Use" data-link-desc="LLM 透過結構化呼叫外部工具（讀檔、查資料庫、發 API request）來擴展能力的設計、function calling 跟 MCP 是常見實作">Tool use</a> 跟 <a href="/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP</a> server 是本地 LLM 對主機資源最大的副作用面。本章把「這個 tool 能做什麼」「MCP server 跑了會碰到什麼檔案」「能不能 rollback」整理成可操作的權限判讀。原理層的副作用範圍 spectrum、可逆性分級見 <a href="/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">4.3 Tool use 原理</a>、agent 跟人類審查的協作模型見 <a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4</a>；hands-on 驗證「LLM 自己沒 FS / shell 權限、wrapper 才有」見 <a href="/blog/llm/01-local-llm-services/hands-on/permission-boundary/" data-link-title="Hands-on：Ollama 改檔案 / 寫程式碼的權限邊界在哪" data-link-desc="四組對照實驗：Ollama 自己沒 FS / shell 權限、wrapper 才有；--dry-run / --confirm / --auto 三檔審查粒度的取捨">Ollama 改檔案的權限邊界</a>。隔離技術見 <a href="/blog/llm/knowledge-cards/sandbox/" data-link-title="Sandbox" data-link-desc="把程式跑在受限制環境的隔離技術、限制檔案 / 網路 / 系統呼叫權限、是 tool use 跟 MCP server 副作用控制的基礎">sandbox</a> 卡、權限白名單見 backend <a href="/blog/backend/knowledge-cards/allowlist/" data-link-title="Allowlist" data-link-desc="說明如何用明確允許條件控制例外放行範圍">allowlist</a> 跟 <a href="/blog/backend/knowledge-cards/least-privilege/" data-link-title="Least Privilege" data-link-desc="說明身份、服務與人員只應取得完成工作所需的最小權限">least-privilege</a> 卡。本章 framing 是個人 dev 視角；production agent 場景下 tool use 引發的 prompt injection 後果見 <a href="/blog/backend/07-security-data-protection/llm-prompt-injection-in-agent/" data-link-title="LLM Agent Prompt Injection 後果治理" data-link-desc="production LLM agent 場景的 prompt injection 後果：tool spec 設計、agent loop 限制、review checkpoint、跟 incident workflow 的接合">backend/07 LLM agent prompt injection</a>。</p>
<p>讀完本章後、你應該能對自己用的 tool / MCP server 回答：能讀寫哪些路徑、能跑哪些 shell command、能連哪些網路位址、副作用有沒有 dry-run / preview、出錯時怎麼回退。</p>
<h2 id="本章目標">本章目標</h2>
<ol>
<li>認識 tool use 跟 MCP server 在三層架構中的位置。</li>
<li>區分「讀取類 tool」跟「副作用類 tool」的權限判讀差異。</li>
<li>知道個人 dev 場景下、第三方 MCP server 的信任邊界跟驗證流程。</li>
<li>用「沙箱 / 白名單 / 副作用可逆性」三個維度評估具體 tool / MCP 的風險。</li>
<li>認識常見的 tool use 副作用洩漏路徑跟對應的最低防護。</li>
</ol>
<h2 id="tool-use-跟-mcp-server-在哪一層">tool use 跟 MCP server 在哪一層</h2>
<p>tool use 跟 MCP server 同時跨<a href="/blog/llm/knowledge-cards/three-layer-architecture/" data-link-title="Three-Layer Architecture" data-link-desc="把本地 LLM 工具拆成介面層、推論伺服器層、模型權重層的基礎心智模型">三層架構</a> 的兩層、但跟模型本身的權限模型分離：</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">介面層（VS Code / Continue.dev / CLI）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">推論伺服器（Ollama / llama-server / LM Studio）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">模型（GGUF 權重）
</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">旁邊另一條：
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">MCP server（獨立 process、自己的權限）
</span></span><span class="line"><span class="ln">10</span><span class="cl">  └── 對檔案 / shell / 網路的具體 API</span></span></code></pre></div><p>關鍵特性：</p>
<ol>
<li><strong>模型本身不執行 tool</strong>：模型只生成 tool call JSON、實際執行由「LLM client」（如 Continue.dev、Claude Desktop）跟 MCP server 完成。</li>
<li><strong>MCP server 是獨立程式</strong>：可以是 Node / Python script、可以呼叫任何系統 API、權限上限是「跑該 server 的 user 的權限」。</li>
<li><strong>權限不是模型給的、是 OS / user 給的</strong>：模型再怎麼「同意」執行 <code>rm -rf /</code>、實際上能不能跑取決於 OS 的權限模型跟 MCP server 自己的 sandbox。</li>
</ol>
<blockquote>
<p><strong>事實查核註</strong>：<a href="https://modelcontextprotocol.io">Model Context Protocol（MCP）</a> 是 Anthropic 在 2024 年底發布的開放協議、各家 LLM client 跟 MCP server 實作的成熟度、權限粒度依版本演進。本章描述以 2026 年 5 月主流實作為基準、引用前以 MCP 官方規格跟各 client / server 的 README 為準。</p></blockquote>
<h2 id="讀取類跟副作用類tool-的權限差異">「讀取類」跟「副作用類」tool 的權限差異</h2>
<p>tool 可以粗分成兩類、權限判讀完全不同：</p>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>例子</th>
          <th>主要風險</th>
          <th>個人 dev 場景的接受程度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>讀取類</td>
          <td>read file、grep、search code、查 git log</td>
          <td>把私密內容讀進 prompt、prompt 被洩漏出去</td>
          <td>較高、但要注意 prompt 傳到哪個 LLM</td>
      </tr>
      <tr>
          <td>副作用類</td>
          <td>write file、run shell、git commit、發 HTTP request、操作資料庫</td>
          <td>不可逆改變、損毀檔案、發送請求、洩漏到外部</td>
          <td>較低、需要 preview / confirm / sandbox</td>
      </tr>
  </tbody>
</table>
<p>讀取類的判讀重點是「<strong>讀到的內容會被傳到哪</strong>」：</p>
<ol>
<li>讀到的 code 變 prompt 的一部分、prompt 送到本地模型→沒外洩</li>
<li>同樣 prompt 送到雲端 LLM→傳到雲端、跟雲端 LLM 的資料政策走（見 <a href="/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">6.4 跨雲端 / 本地資料邊界</a>）</li>
<li>讀取會被 log→log 累積、需要管理</li>
</ol>
<p>副作用類的判讀重點是「<strong>可逆性</strong>」：</p>
<ol>
<li>write file 蓋掉原內容→可能無法回復（沒備份的話）</li>
<li>run shell <code>rm</code> / <code>git push</code>→不可逆或需要 force pull 才能還原</li>
<li>發 HTTP request、轉帳、call API→送出去就回不來</li>
<li>操作 production 資料庫→可能影響其他人</li>
</ol>
<h2 id="三個維度評估具體-tool--mcp-的風險">三個維度評估具體 tool / MCP 的風險</h2>
<p>對任何 tool / MCP server、可以用三個維度做初步評估：</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">┌────────────────────────────────────────────────────┐
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">│ 維度一：沙箱                                       │
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│   能做什麼 = 跑該 server 的 user 能做什麼          │
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│   有沒有 chroot / Docker / namespace 隔離？        │
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│                                                    │
</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">│   能讀寫的路徑、能跑的指令、能連的網址有沒有限定？  │
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│   還是 &#34;all paths&#34; / &#34;any shell&#34; / &#34;any URL&#34;？     │
</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">│ 維度三：副作用可逆性                               │
</span></span><span class="line"><span class="ln">11</span><span class="cl">│   出錯能不能 rollback？                            │
</span></span><span class="line"><span class="ln">12</span><span class="cl">│   有沒有 dry-run / preview / confirm？             │
</span></span><span class="line"><span class="ln">13</span><span class="cl">└────────────────────────────────────────────────────┘</span></span></code></pre></div><p>對應的判讀範例：</p>
<table>
  <thead>
      <tr>
          <th>Tool / MCP</th>
          <th>沙箱</th>
          <th>白名單</th>
          <th>副作用可逆性</th>
          <th>個人 dev 評估</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>read_file</code>（讀任意路徑）</td>
          <td>無、user 權限</td>
          <td>無、可讀 user 所有檔案</td>
          <td>N/A（讀取無副作用）</td>
          <td>注意 prompt 走向</td>
      </tr>
      <tr>
          <td><code>read_file</code> 限定 workspace</td>
          <td>無</td>
          <td>有、只讀 workspace</td>
          <td>N/A</td>
          <td>較安全</td>
      </tr>
      <tr>
          <td><code>run_shell</code>（任意指令）</td>
          <td>無</td>
          <td>無</td>
          <td>視指令、<code>rm</code> / <code>git push</code> 不可逆</td>
          <td>高風險</td>
      </tr>
      <tr>
          <td><code>apply_patch</code>（套 diff 到 file）</td>
          <td>無</td>
          <td>限定 workspace</td>
          <td>git stash 可逆、未 stash 不可逆</td>
          <td>中風險、值得用 git track</td>
      </tr>
      <tr>
          <td><code>fetch_url</code>（任意 URL）</td>
          <td>無</td>
          <td>無</td>
          <td>一般 GET 可逆、POST 不可逆</td>
          <td>看具體請求</td>
      </tr>
      <tr>
          <td><code>mcp-server-postgres</code>（直連 DB）</td>
          <td>無</td>
          <td>視 DB user 權限</td>
          <td>改 row 通常可逆、DROP TABLE 不可逆</td>
          <td>DB user 權限要設好</td>
      </tr>
  </tbody>
</table>
<p>實務上、社群常見的 MCP server 多半屬於「白名單較弱」「副作用直接套用」的設計、需要使用者自己加防護。</p>
<h2 id="第三方-mcp-server-的供應鏈信任">第三方 MCP server 的供應鏈信任</h2>
<p>MCP server 是可執行程式碼、信任邊界比 GGUF 模型權重高一個層級。常見的 MCP server 來源：</p>
<ol>
<li><strong>官方 reference server</strong>（如 Anthropic 維護的 <code>@modelcontextprotocol/server-*</code>）：相對較高信任、有官方 maintain。</li>
<li><strong>知名專案的 MCP server</strong>（如 GitHub、Notion、Slack 等公司自己出的）：跟該公司的軟體分發信任度一致。</li>
<li><strong>社群 MCP server</strong>：個人或小團隊維護、信任度視 maintainer 與 download 量、看 code 是基本動作。</li>
</ol>
<p>裝任何 MCP server 前的最低判讀：</p>
<ol>
<li><strong>看 source repo</strong>：是不是知名作者、stars 數、最後 commit 時間、issues 是否活躍。</li>
<li><strong>看實際做什麼</strong>：MCP server 的 README 通常列出提供的 tools、跑起來會碰到的權限。</li>
<li><strong>跑在最小權限環境</strong>：能用 Docker / chroot / <code>nice -n 19</code> 之類就用、不要直接用 root / admin。</li>
<li><strong>不要用 <code>curl | sh</code> 安裝</strong>：用 <code>npm install</code> / <code>pip install</code> / <code>go install</code> 等有 package manager 介入的方式、留下 install log。</li>
</ol>
<blockquote>
<p><strong>事實查核註</strong>：MCP server registry、套件管理工具的供應鏈安全機制依版本演進、Anthropic 跟其他主要 client 廠商可能引入官方 marketplace 或簽章機制、建議引用前以當前 MCP 官方狀態為準。</p></blockquote>
<h2 id="個人-dev-場景的最低防護建議">個人 dev 場景的最低防護建議</h2>
<p>對「我想用 tool use 但又怕 LLM 把檔案搞壞」的工作流、最低防護建議：</p>
<ol>
<li><strong>codebase 用 git track</strong>：所有寫入操作前確認 working tree clean、出問題能 <code>git checkout</code> 還原。<code>git stash</code> 是更輕的選擇。</li>
<li><strong>重要檔案 backup</strong>：dotfile、SSH key、雲端 API key 等不在 git track 範圍的、用 Time Machine / rsync / cloud sync 之類做日常 backup。</li>
<li><strong>跑 LLM agent 時用獨立 user / 容器</strong>：對「想試 agent 但怕」的場景、開個專用 macOS user 或 Docker container、user 沒 sudo、檔案存取限定 workspace。</li>
<li><strong>MCP server 的 config 加白名單</strong>：能設 allowed paths / allowed commands / allowed URLs 的 server 都先設、預設拒絕、按需開放。</li>
<li><strong>看不懂的 tool call 不要 confirm</strong>：Continue.dev / Claude Desktop 等 client 通常會 prompt 使用者確認 tool 執行、看不懂的 JSON 先別按。</li>
</ol>
<h2 id="tool-use-副作用洩漏的常見路徑">tool use 副作用洩漏的常見路徑</h2>
<p>個人 dev 場景常見的 tool use 副作用洩漏路徑：</p>
<ol>
<li><strong>LLM 誤把 secret 寫進 commit</strong>：tool use 帶 <code>git commit</code>、LLM 從 <code>.env</code> 讀到 API key 又寫進 commit message。對應防護：MCP server 加 <code>.env</code> 黑名單、commit hook 掃 secret。</li>
<li><strong>LLM 套用 broken patch 蓋掉檔案</strong>：<code>apply_patch</code> 失敗 / 部分套用、留下無法 compile 的狀態。對應防護：套 patch 前 <code>git stash</code> 或 <code>git add -p</code> 先存 working tree。</li>
<li><strong>LLM 從 issue / PR 內容引發指令</strong>：讀進 issue 的 prompt 內容包含 prompt injection、誘導跑非預期指令。對應防護：tool 跑前明確讓使用者確認（見 <a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">6.3 prompt injection</a>）。</li>
<li><strong>LLM 觸發 production 操作</strong>：MCP server 連到 production DB、LLM 跑 <code>DROP TABLE</code>。對應防護：production credential 絕對不放在 tool use 可達的環境。</li>
</ol>
<h2 id="給讀者的-tool--mcp-評估清單">給讀者的 tool / MCP 評估清單</h2>
<p>每次裝新 MCP server / 啟用新 tool 之前、跑一次評估：</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">[ ] 來源是知名作者 / 官方專案 / 我能 audit 的開源 repo
</span></span><span class="line"><span class="ln">2</span><span class="cl">[ ] README 列出的 tool 列表、跟我的使用情境匹配
</span></span><span class="line"><span class="ln">3</span><span class="cl">[ ] 該 server 跑在最小權限環境（user / sandbox / container）
</span></span><span class="line"><span class="ln">4</span><span class="cl">[ ] 副作用類 tool 有 confirm / preview 機制
</span></span><span class="line"><span class="ln">5</span><span class="cl">[ ] workspace 內容受 git track、能 rollback
</span></span><span class="line"><span class="ln">6</span><span class="cl">[ ] 不放 production credential / SSH key 在該 server 可達的環境
</span></span><span class="line"><span class="ln">7</span><span class="cl">[ ] 啟用後跑簡單測試、確認 tool call 行為符合預期</span></span></code></pre></div><h2 id="下一章">下一章</h2>
<p>下一章：<a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">6.3 IDE 場景的 prompt injection</a>、處理 tool use 副作用最常見的觸發來源。</p>
]]></content:encoded></item><item><title>模組四：LLM 應用層原理</title><link>https://tarrragon.github.io/blog/llm/04-applications/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>狀態&lt;/strong>：大綱階段、部分章節待完成內容。&lt;/p>&lt;/blockquote>
&lt;p>本模組整理 LLM 應用層的核心原理：模型裝起來、能對話之後、要怎麼跟外部世界互動、怎麼組成可用的工作流、怎麼測它跑得對不對。模組零到模組三建立的是「模型本身」的心智模型；本模組建立的是「模型作為系統元件」的心智模型。&lt;/p>
&lt;p>寫這個模組的核心約束是「&lt;strong>只寫不會過時的部分&lt;/strong>」。LangChain、LlamaIndex、aider、Cline 等工具半年一個世代、寫具體 API 半年後就過時；但「retrieval 在做什麼」「為什麼 LLM 需要 tool use」「agent loop 為什麼會失敗」「eval 軸怎麼選」這些原理跨工具世代都成立。本模組刻意避開具體實作教學、把焦點放在跨世代的設計取捨。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/prompt-techniques-landscape/" data-link-title="4.0 Prompt 技術光譜：手法分類、取捨、組合模式" data-link-desc="Zero-shot / few-shot、chain-of-thought、role / template、reflection 等 prompt 技術的分類與取捨、何時 stack 何時不要 stack、跟 fine-tune / RAG / chaining 的邊界">4.0&lt;/a>&lt;/td>
 &lt;td>Prompt 技術光譜&lt;/td>
 &lt;td>三軸（context / 推理 / 格式）+ 四維 trade-off + stack 判讀 + 跟 fine-tune/RAG/chaining 的邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &amp;#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1&lt;/a>&lt;/td>
 &lt;td>RAG 原理：retrieval + augmentation 模式&lt;/td>
 &lt;td>為什麼要外掛知識、語意相似 vs 字面相似、chunking 取捨、失敗的根本原因&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/rag-retrieval-enhancements/" data-link-title="4.2 RAG 檢索增強：query rewriting / HyDE / multi-step / context packing" data-link-desc="Query 端增強（rewriting / expansion / HyDE）、multi-step iterative retrieval、retrieve 後的 context packing（dedup / ordering / summarization）、adaptive retrieval：vanilla RAG 不夠時的下一層工具箱">4.2&lt;/a>&lt;/td>
 &lt;td>RAG 檢索增強：query rewriting / HyDE / multi-step / packing&lt;/td>
 &lt;td>四層增強分類、何時 stack 何時不要、adaptive retrieval&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">4.3&lt;/a>&lt;/td>
 &lt;td>Tool use 原理：LLM 跟外部世界互動&lt;/td>
 &lt;td>structured output 是橋、function calling 取捨、為什麼小模型 tool use 崩&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4&lt;/a>&lt;/td>
 &lt;td>Agent 架構原理&lt;/td>
 &lt;td>Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、人類審查模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/human-ai-collaboration/" data-link-title="4.5 人機協作拓樸：何時人介入、怎麼介入" data-link-desc="Centaur vs Cyborg 工作模式、jagged frontier、HITL 三種觸發時機（pre-act / mid-stream / post-hoc）、確認流程的設計避免橡皮圖章化">4.5&lt;/a>&lt;/td>
 &lt;td>人機協作拓樸：何時人介入、怎麼介入&lt;/td>
 &lt;td>Centaur vs Cyborg、jagged frontier、HITL 三時機（pre-act / mid-stream / post-hoc）、避免橡皮圖章化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">4.6&lt;/a>&lt;/td>
 &lt;td>應用層協議：function calling / structured output / MCP&lt;/td>
 &lt;td>三者層級差異、為什麼出現 MCP、組合工作流&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/workflow-patterns/" data-link-title="4.7 Workflow 編排模式" data-link-desc="Pipeline / router / parallel / reflection：多 LLM call 組合的四種基本模式與退化條件">4.7&lt;/a>&lt;/td>
 &lt;td>Workflow 編排模式&lt;/td>
 &lt;td>Pipeline / router / parallel / reflection 四種基本模式、退化條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/multi-agent-topology/" data-link-title="4.8 Multi-Agent 拓樸：flat / hierarchical / agent-as-tool" data-link-desc="從 multi-call workflow 走到 multi-agent system 的判讀、flat vs hierarchical 拓樸、agent-as-tool 的 MCP 視角、specialization 跟 orchestration overhead 的取捨">4.8&lt;/a>&lt;/td>
 &lt;td>Multi-Agent 拓樸&lt;/td>
 &lt;td>Flat / hierarchical / agent-as-tool、specialization gain vs orchestration overhead、特有失敗模式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/production-resource-planning/" data-link-title="4.9 Production 部署的資源評估原理" data-link-desc="從本地單 user 到 production multi-tenant：concurrent users、cost model、observability、SLA、capacity planning 的設計取捨">4.9&lt;/a>&lt;/td>
 &lt;td>Production 部署的資源評估原理&lt;/td>
 &lt;td>6 個 dimension：concurrency / latency / cost / storage / observability / reliability&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/artifact-management/" data-link-title="4.10 衍生產物管理原理：什麼進 git、什麼不該" data-link-desc="LLM 應用的 source / derived / external 三類產物對應 git / build cache / registry、與 production 部署的 reproducibility / cost / share 取捨">4.10&lt;/a>&lt;/td>
 &lt;td>衍生產物管理原理：什麼進 git、什麼不該&lt;/td>
 &lt;td>Source / derived / external 三分類、&lt;code>.gitignore&lt;/code> 設計模式、prompt + eval 版本管理、production deployment 對接&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/long-context-engineering/" data-link-title="4.11 Long context engineering" data-link-desc="128K / 1M context 模型怎麼用：claimed vs effective context、lost-in-the-middle、context 設計策略、Long context vs RAG 取捨">4.11&lt;/a>&lt;/td>
 &lt;td>Long context engineering&lt;/td>
 &lt;td>claimed vs effective context、lost-in-the-middle、跟 RAG 的取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &amp;#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">4.12&lt;/a>&lt;/td>
 &lt;td>Embedding model 內部&lt;/td>
 &lt;td>contrastive learning、選型、MTEB、in-domain fine-tune&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/eval-design-framework/" data-link-title="4.13 Eval 設計座標系：三軸、八象限、何時測什麼" data-link-desc="Eval 設計三軸（objective↔subjective / component↔end-to-end / quantitative↔qualitative）、八象限的對應 eval 工具、軸選錯的訊號、跟 benchmarking / LLM-as-judge / tracing 的關係">4.13&lt;/a>&lt;/td>
 &lt;td>Eval 設計座標系：三軸、八象限&lt;/td>
 &lt;td>Objective / component / quantitative 三軸 × 工具選擇、軸誤選的訊號、eval 演化路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14&lt;/a>&lt;/td>
 &lt;td>Benchmarking 與評估方法論&lt;/td>
 &lt;td>capability vs performance、in-house benchmark、&lt;code>llama-bench&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/vision-in-coding-workflow/" data-link-title="4.15 Vision in coding workflow：本地 VLM 怎麼接寫 code" data-link-desc="VLM 在 coding 工作流的 use cases、本地 VLM 選型、跟雲端 VLM 的分工、Continue.dev / Ollama 整合現狀">4.15&lt;/a>&lt;/td>
 &lt;td>Vision in coding workflow&lt;/td>
 &lt;td>VLM 在 coding 場景的 use cases、本地 VLM 選型、IDE 整合現狀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">4.16&lt;/a>&lt;/td>
 &lt;td>靜態 / serverless RAG deployment&lt;/td>
 &lt;td>沒 backend 的 RAG 四方案、API key 暴露、CORS、abuse、SaaS 供應鏈、跟模組六 routing&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/coding-agent-harness/" data-link-title="4.17 Coding agent harness：scaffold / context engineering / subagent" data-link-desc="Coding agent 的內部設計：scaffold vs harness 分層、context budget 25% 規則、subagent 拓樸、跟 Claude Code / Cursor / Aider 的 mapping">4.17&lt;/a>&lt;/td>
 &lt;td>Coding agent harness&lt;/td>
 &lt;td>Scaffold vs harness 分層、context budget 25% 規則、subagent 設計、跟 Claude Code / Cursor / Aider 的 mapping&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/prompt-caching-engineering/" data-link-title="4.18 Prompt caching 工程實務：cost / latency 最大槓桿" data-link-desc="Prompt cache 怎麼運作、cache_control 設計、coding agent 跟 long-context 的 cache pattern、anti-pattern 跟 cache miss 訊號">4.18&lt;/a>&lt;/td>
 &lt;td>Prompt caching 工程實務&lt;/td>
 &lt;td>Cache breakpoint 設計、coding agent / RAG 場景 pattern、anti-pattern、cost / latency 槓桿&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/agent-memory-architecture/" data-link-title="4.19 Agent memory 分層架構" data-link-desc="Agent 在 context window 之外管理長期狀態的設計：working / short-term / long-term episodic / semantic / procedural 五個層次、寫入時機、retrieval 設計、失敗模式">4.19&lt;/a>&lt;/td>
 &lt;td>Agent memory 分層架構&lt;/td>
 &lt;td>Working / session / episodic / semantic / procedural 四層、寫入時機、retrieval 設計、失敗模式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20&lt;/a>&lt;/td>
 &lt;td>LLM tracing 與 observability&lt;/td>
 &lt;td>OTel GenAI semconv、cost / latency / failure debug、trace → eval 閉環&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21&lt;/a>&lt;/td>
 &lt;td>LLM-as-Judge 評估方法&lt;/td>
 &lt;td>Rubric 設計、pairwise vs direct、三大 bias 緩解、calibration、跟 production trace 的閉環&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22&lt;/a>&lt;/td>
 &lt;td>RAG storage 工程&lt;/td>
 &lt;td>四層可替換結構、storage 演化階梯、升級判讀訊號、index 生命週期、dependency 約束&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/hands-on/" data-link-title="4.x Hands-on：端到端案例" data-link-desc="把模組四的所有原理串成具體 case study：從 task decomposition、workflow 設計、eval 設計到 iteration loop">Hands-on&lt;/a>&lt;/td>
 &lt;td>端到端案例：把所有原理串成具體 case study&lt;/td>
 &lt;td>Customer support agent 從 task decomposition 到 eval 全流程&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="為什麼這個順序">為什麼這個順序&lt;/h2>
&lt;p>本模組章節順序的設計脈絡：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>狀態</strong>：大綱階段、部分章節待完成內容。</p></blockquote>
<p>本模組整理 LLM 應用層的核心原理：模型裝起來、能對話之後、要怎麼跟外部世界互動、怎麼組成可用的工作流、怎麼測它跑得對不對。模組零到模組三建立的是「模型本身」的心智模型；本模組建立的是「模型作為系統元件」的心智模型。</p>
<p>寫這個模組的核心約束是「<strong>只寫不會過時的部分</strong>」。LangChain、LlamaIndex、aider、Cline 等工具半年一個世代、寫具體 API 半年後就過時；但「retrieval 在做什麼」「為什麼 LLM 需要 tool use」「agent loop 為什麼會失敗」「eval 軸怎麼選」這些原理跨工具世代都成立。本模組刻意避開具體實作教學、把焦點放在跨世代的設計取捨。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/llm/04-applications/prompt-techniques-landscape/" data-link-title="4.0 Prompt 技術光譜：手法分類、取捨、組合模式" data-link-desc="Zero-shot / few-shot、chain-of-thought、role / template、reflection 等 prompt 技術的分類與取捨、何時 stack 何時不要 stack、跟 fine-tune / RAG / chaining 的邊界">4.0</a></td>
          <td>Prompt 技術光譜</td>
          <td>三軸（context / 推理 / 格式）+ 四維 trade-off + stack 判讀 + 跟 fine-tune/RAG/chaining 的邊界</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1</a></td>
          <td>RAG 原理：retrieval + augmentation 模式</td>
          <td>為什麼要外掛知識、語意相似 vs 字面相似、chunking 取捨、失敗的根本原因</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/rag-retrieval-enhancements/" data-link-title="4.2 RAG 檢索增強：query rewriting / HyDE / multi-step / context packing" data-link-desc="Query 端增強（rewriting / expansion / HyDE）、multi-step iterative retrieval、retrieve 後的 context packing（dedup / ordering / summarization）、adaptive retrieval：vanilla RAG 不夠時的下一層工具箱">4.2</a></td>
          <td>RAG 檢索增強：query rewriting / HyDE / multi-step / packing</td>
          <td>四層增強分類、何時 stack 何時不要、adaptive retrieval</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">4.3</a></td>
          <td>Tool use 原理：LLM 跟外部世界互動</td>
          <td>structured output 是橋、function calling 取捨、為什麼小模型 tool use 崩</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4</a></td>
          <td>Agent 架構原理</td>
          <td>Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、人類審查模型</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/human-ai-collaboration/" data-link-title="4.5 人機協作拓樸：何時人介入、怎麼介入" data-link-desc="Centaur vs Cyborg 工作模式、jagged frontier、HITL 三種觸發時機（pre-act / mid-stream / post-hoc）、確認流程的設計避免橡皮圖章化">4.5</a></td>
          <td>人機協作拓樸：何時人介入、怎麼介入</td>
          <td>Centaur vs Cyborg、jagged frontier、HITL 三時機（pre-act / mid-stream / post-hoc）、避免橡皮圖章化</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">4.6</a></td>
          <td>應用層協議：function calling / structured output / MCP</td>
          <td>三者層級差異、為什麼出現 MCP、組合工作流</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/workflow-patterns/" data-link-title="4.7 Workflow 編排模式" data-link-desc="Pipeline / router / parallel / reflection：多 LLM call 組合的四種基本模式與退化條件">4.7</a></td>
          <td>Workflow 編排模式</td>
          <td>Pipeline / router / parallel / reflection 四種基本模式、退化條件</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/multi-agent-topology/" data-link-title="4.8 Multi-Agent 拓樸：flat / hierarchical / agent-as-tool" data-link-desc="從 multi-call workflow 走到 multi-agent system 的判讀、flat vs hierarchical 拓樸、agent-as-tool 的 MCP 視角、specialization 跟 orchestration overhead 的取捨">4.8</a></td>
          <td>Multi-Agent 拓樸</td>
          <td>Flat / hierarchical / agent-as-tool、specialization gain vs orchestration overhead、特有失敗模式</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/production-resource-planning/" data-link-title="4.9 Production 部署的資源評估原理" data-link-desc="從本地單 user 到 production multi-tenant：concurrent users、cost model、observability、SLA、capacity planning 的設計取捨">4.9</a></td>
          <td>Production 部署的資源評估原理</td>
          <td>6 個 dimension：concurrency / latency / cost / storage / observability / reliability</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/artifact-management/" data-link-title="4.10 衍生產物管理原理：什麼進 git、什麼不該" data-link-desc="LLM 應用的 source / derived / external 三類產物對應 git / build cache / registry、與 production 部署的 reproducibility / cost / share 取捨">4.10</a></td>
          <td>衍生產物管理原理：什麼進 git、什麼不該</td>
          <td>Source / derived / external 三分類、<code>.gitignore</code> 設計模式、prompt + eval 版本管理、production deployment 對接</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/long-context-engineering/" data-link-title="4.11 Long context engineering" data-link-desc="128K / 1M context 模型怎麼用：claimed vs effective context、lost-in-the-middle、context 設計策略、Long context vs RAG 取捨">4.11</a></td>
          <td>Long context engineering</td>
          <td>claimed vs effective context、lost-in-the-middle、跟 RAG 的取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">4.12</a></td>
          <td>Embedding model 內部</td>
          <td>contrastive learning、選型、MTEB、in-domain fine-tune</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/eval-design-framework/" data-link-title="4.13 Eval 設計座標系：三軸、八象限、何時測什麼" data-link-desc="Eval 設計三軸（objective↔subjective / component↔end-to-end / quantitative↔qualitative）、八象限的對應 eval 工具、軸選錯的訊號、跟 benchmarking / LLM-as-judge / tracing 的關係">4.13</a></td>
          <td>Eval 設計座標系：三軸、八象限</td>
          <td>Objective / component / quantitative 三軸 × 工具選擇、軸誤選的訊號、eval 演化路徑</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14</a></td>
          <td>Benchmarking 與評估方法論</td>
          <td>capability vs performance、in-house benchmark、<code>llama-bench</code></td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/vision-in-coding-workflow/" data-link-title="4.15 Vision in coding workflow：本地 VLM 怎麼接寫 code" data-link-desc="VLM 在 coding 工作流的 use cases、本地 VLM 選型、跟雲端 VLM 的分工、Continue.dev / Ollama 整合現狀">4.15</a></td>
          <td>Vision in coding workflow</td>
          <td>VLM 在 coding 場景的 use cases、本地 VLM 選型、IDE 整合現狀</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/static-and-serverless-rag-deployment/" data-link-title="4.16 靜態 / serverless RAG deployment：架構選擇與資安取捨" data-link-desc="沒 backend 的場景怎麼做 RAG：四種 deployment 方案、API key 暴露問題、CORS / abuse / 第三方信任、跟模組六的 routing">4.16</a></td>
          <td>靜態 / serverless RAG deployment</td>
          <td>沒 backend 的 RAG 四方案、API key 暴露、CORS、abuse、SaaS 供應鏈、跟模組六 routing</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/coding-agent-harness/" data-link-title="4.17 Coding agent harness：scaffold / context engineering / subagent" data-link-desc="Coding agent 的內部設計：scaffold vs harness 分層、context budget 25% 規則、subagent 拓樸、跟 Claude Code / Cursor / Aider 的 mapping">4.17</a></td>
          <td>Coding agent harness</td>
          <td>Scaffold vs harness 分層、context budget 25% 規則、subagent 設計、跟 Claude Code / Cursor / Aider 的 mapping</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/prompt-caching-engineering/" data-link-title="4.18 Prompt caching 工程實務：cost / latency 最大槓桿" data-link-desc="Prompt cache 怎麼運作、cache_control 設計、coding agent 跟 long-context 的 cache pattern、anti-pattern 跟 cache miss 訊號">4.18</a></td>
          <td>Prompt caching 工程實務</td>
          <td>Cache breakpoint 設計、coding agent / RAG 場景 pattern、anti-pattern、cost / latency 槓桿</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/agent-memory-architecture/" data-link-title="4.19 Agent memory 分層架構" data-link-desc="Agent 在 context window 之外管理長期狀態的設計：working / short-term / long-term episodic / semantic / procedural 五個層次、寫入時機、retrieval 設計、失敗模式">4.19</a></td>
          <td>Agent memory 分層架構</td>
          <td>Working / session / episodic / semantic / procedural 四層、寫入時機、retrieval 設計、失敗模式</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20</a></td>
          <td>LLM tracing 與 observability</td>
          <td>OTel GenAI semconv、cost / latency / failure debug、trace → eval 閉環</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21</a></td>
          <td>LLM-as-Judge 評估方法</td>
          <td>Rubric 設計、pairwise vs direct、三大 bias 緩解、calibration、跟 production trace 的閉環</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22</a></td>
          <td>RAG storage 工程</td>
          <td>四層可替換結構、storage 演化階梯、升級判讀訊號、index 生命週期、dependency 約束</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/hands-on/" data-link-title="4.x Hands-on：端到端案例" data-link-desc="把模組四的所有原理串成具體 case study：從 task decomposition、workflow 設計、eval 設計到 iteration loop">Hands-on</a></td>
          <td>端到端案例：把所有原理串成具體 case study</td>
          <td>Customer support agent 從 task decomposition 到 eval 全流程</td>
      </tr>
  </tbody>
</table>
<h2 id="為什麼這個順序">為什麼這個順序</h2>
<p>本模組章節順序的設計脈絡：</p>
<ol>
<li><strong>先 4.0 Prompt 技術光譜</strong>：within-call 增強是後續所有設計的基底、先建立「prompt 層能做什麼、邊界在哪」的座標。</li>
<li><strong>接 4.1 RAG 原理 + 4.2 RAG 檢索增強</strong>：應用層最常見的模式、把「LLM + 外部知識」這個基本組合走過一遍、概念對映到每個讀者都用過的 <code>@codebase</code> 等實務經驗。</li>
<li><strong>再 4.3 Tool use</strong>：RAG 是「LLM 讀外部資料」、Tool use 是「LLM 對外部世界做事」、兩條延伸方向自然接續。</li>
<li><strong>再 4.4 Agent 架構 + 4.5 人機協作</strong>：把 Tool use 從「單次呼叫」延伸到「自主多步」、自然進入 agent；agent 自主後立刻面對人類介入時機問題。</li>
<li><strong>再 4.6 應用層協議</strong>：前面章節涉及 function calling、structured output、MCP 等術語、本章把這三個概念放回正確的層級、避免混為一談。</li>
<li><strong>再 4.7 Workflow + 4.8 Multi-agent</strong>：上層整合、把多 LLM call 跟多 agent 組合的設計模式整理成跨 framework 不變的概念地圖。</li>
<li><strong>4.9 起進入 production / 細節</strong>：部署資源、衍生產物管理、long context、embedding 內部、eval / benchmarking、tracing、judge——每個都是 production 場景遇到的具體議題。</li>
<li><strong>最後 hands-on</strong>：把上述所有原理串成具體案例、看「實際做的時候、原理怎麼落」。</li>
</ol>
<p>每章可以單獨讀、但若你是第一次接觸 LLM 應用層、照順序讀最不容易迷路。</p>
<h2 id="跟其他模組的分工">跟其他模組的分工</h2>
<table>
  <thead>
      <tr>
          <th>模組</th>
          <th>角度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模組零</td>
          <td>操作層心智模型：模型放哪、怎麼選工具</td>
      </tr>
      <tr>
          <td>模組一</td>
          <td>工具層：具體裝 Ollama / Continue.dev</td>
      </tr>
      <tr>
          <td>模組二</td>
          <td>數學工具：線性代數、機率、最佳化</td>
      </tr>
      <tr>
          <td>模組三</td>
          <td>理論機制：模型內部運作</td>
      </tr>
      <tr>
          <td>模組四</td>
          <td><strong>應用層原理</strong>：模型作為系統元件、跟外部世界互動的設計取捨</td>
      </tr>
  </tbody>
</table>
<h2 id="適合的讀者">適合的讀者</h2>
<table>
  <thead>
      <tr>
          <th>你的背景</th>
          <th>適合程度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫過 Ollama + Continue.dev、想懂「然後呢」</td>
          <td>直接適合、從 4.0 依序讀</td>
      </tr>
      <tr>
          <td>已經試過 LangChain / aider / Cline、想看原理</td>
          <td>直接適合、本模組補足「為什麼這樣設計」的視角</td>
      </tr>
      <tr>
          <td>想做 LLM 應用開發</td>
          <td>重點讀 4.0、4.1–4.3、4.4–4.5、4.7–4.8、4.13</td>
      </tr>
      <tr>
          <td>只想用本地 LLM 寫 code、不做應用</td>
          <td>跳過本模組無妨、模組零 + 模組一已足夠</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本模組內的主題">不在本模組內的主題</h2>
<ol>
<li><strong>具體 framework 教學</strong>：LangChain、LlamaIndex 等的 API 用法、隨版本變、交給官方文件。</li>
<li><strong>具體 prompt 寫法</strong>：跨模型跨任務不可遷移、本模組 4.0 寫的是 prompt 技術 landscape 的結構、不是具體寫法。</li>
<li><strong>具體 agent 工具配置</strong>：aider、Cline 等的安裝設定、隨工具版本變、見 <a href="/blog/llm/01-local-llm-services/extension-paths/" data-link-title="1.6 延伸方向：Web UI、coding agent、產圖" data-link-desc="日常路徑跑穩後可以玩的延伸：Open WebUI、aider、ComfyUI；先把基底跑穩再進階">1.6 延伸方向</a> 的入口資訊。</li>
<li><strong>訓練 / fine-tuning</strong>：屬於改變模型本身、見 <a href="/blog/llm/03-theoretical-foundations/training-pipeline/" data-link-title="3.4 訓練流程：pre-train → SFT → RLHF" data-link-desc="LLM 的三階段訓練：預訓練、指令微調、人類反饋強化學習；各階段目標與最新替代方案">3.4 訓練流程</a>。</li>
</ol>
]]></content:encoded></item><item><title>Hands-on：用 blog content 寫一個最小 MCP server</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/mcp-demo/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/mcp-demo/</guid><description>&lt;p>本篇把 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">4.6 應用層協議&lt;/a> 的 MCP 概念落到一個可跑的最小實作：用 stdio JSON-RPC 暴露兩個 tool（&lt;code>search_blog&lt;/code>、&lt;code>read_chunk&lt;/code>）、客戶端 spawn server 跟它對話、驗證 protocol initialize / tools/list / tools/call / error 四個基本流程。實作刻意只用 Python stdlib、不依賴 MCP SDK、為的是把 wire protocol 看清楚、跟 4.3 的「server 協議層」framing 對應。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>驗證日期&lt;/strong>：2026-05-12
&lt;strong>環境&lt;/strong>：Python 3.11+、stdlib only（json / subprocess / urllib）
&lt;strong>依賴&lt;/strong>：RAG demo 的 &lt;code>index.pkl&lt;/code>（&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &amp;#43; cosine retrieval &amp;#43; Ollama chat、validating 4.0 RAG 原理">見 RAG demo&lt;/a>）
&lt;strong>協議版本&lt;/strong>：MCP &lt;code>2025-03-26&lt;/code>&lt;/p>&lt;/blockquote>
&lt;h2 id="mcp-是什麼層的東西">MCP 是什麼層的東西&lt;/h2>
&lt;p>回顧 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">4.6 應用層協議&lt;/a> 的層級劃分：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Function calling&lt;/strong>：模型訓練建立的能力（模型層）。&lt;/li>
&lt;li>&lt;strong>Structured output&lt;/strong>：sampling 階段約束（推論層）。&lt;/li>
&lt;li>&lt;strong>MCP&lt;/strong>：LLM application ↔ 外部 tool server 的協議（架構層）。&lt;/li>
&lt;/ul>
&lt;p>MCP 不管「模型怎麼呼叫工具」、它管「工具怎麼被暴露給 application」。本 demo 寫的是 server 端：server 不知道是哪個 LLM 在用它、不假設客戶端用 function calling 還是 structured output、它只專注「把 tool 透過 JSON-RPC 暴露出去」。&lt;/p>
&lt;p>這跟 &lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/openai-compatible-api/" data-link-title="0.3 OpenAI 相容 API" data-link-desc="為什麼幾乎所有本地 LLM 工具不用改就能切到本地：背後是同一套 API 形狀">OpenAI 相容 API&lt;/a> 的設計哲學一致：定義最小可用標準、讓生態繞著標準長。&lt;/p>
&lt;h2 id="前置設定">前置設定&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>項目&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Ollama + &lt;code>nomic-embed-text&lt;/code>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/ollama-setup/" data-link-title="Hands-on：安裝 Ollama &amp;#43; 拉第一個 Gemma 模型" data-link-desc="brew install ollama、launchd service、ollama pull、curl 驗證 OpenAI 相容 API">Ollama 安裝&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RAG index（&lt;code>index.pkl&lt;/code>）&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &amp;#43; cosine retrieval &amp;#43; Ollama chat、validating 4.0 RAG 原理">RAG demo&lt;/a> 跑過 &lt;code>ingest.py&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Python&lt;/td>
 &lt;td>3.11+&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不需要安裝 MCP SDK——本 demo 手寫 JSON-RPC 處理、為了 inspection 透明度。Production server 建議改用 &lt;a href="https://github.com/modelcontextprotocol">官方 SDK&lt;/a>（Python / TypeScript 都有）、處理 framing、capability negotiation、transport edge cases。&lt;/p></description><content:encoded><![CDATA[<p>本篇把 <a href="/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">4.6 應用層協議</a> 的 MCP 概念落到一個可跑的最小實作：用 stdio JSON-RPC 暴露兩個 tool（<code>search_blog</code>、<code>read_chunk</code>）、客戶端 spawn server 跟它對話、驗證 protocol initialize / tools/list / tools/call / error 四個基本流程。實作刻意只用 Python stdlib、不依賴 MCP SDK、為的是把 wire protocol 看清楚、跟 4.3 的「server 協議層」framing 對應。</p>
<blockquote>
<p><strong>驗證日期</strong>：2026-05-12
<strong>環境</strong>：Python 3.11+、stdlib only（json / subprocess / urllib）
<strong>依賴</strong>：RAG demo 的 <code>index.pkl</code>（<a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">見 RAG demo</a>）
<strong>協議版本</strong>：MCP <code>2025-03-26</code></p></blockquote>
<h2 id="mcp-是什麼層的東西">MCP 是什麼層的東西</h2>
<p>回顧 <a href="/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">4.6 應用層協議</a> 的層級劃分：</p>
<ul>
<li><strong>Function calling</strong>：模型訓練建立的能力（模型層）。</li>
<li><strong>Structured output</strong>：sampling 階段約束（推論層）。</li>
<li><strong>MCP</strong>：LLM application ↔ 外部 tool server 的協議（架構層）。</li>
</ul>
<p>MCP 不管「模型怎麼呼叫工具」、它管「工具怎麼被暴露給 application」。本 demo 寫的是 server 端：server 不知道是哪個 LLM 在用它、不假設客戶端用 function calling 還是 structured output、它只專注「把 tool 透過 JSON-RPC 暴露出去」。</p>
<p>這跟 <a href="/blog/llm/00-foundations/openai-compatible-api/" data-link-title="0.3 OpenAI 相容 API" data-link-desc="為什麼幾乎所有本地 LLM 工具不用改就能切到本地：背後是同一套 API 形狀">OpenAI 相容 API</a> 的設計哲學一致：定義最小可用標準、讓生態繞著標準長。</p>
<h2 id="前置設定">前置設定</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ollama + <code>nomic-embed-text</code></td>
          <td><a href="/blog/llm/01-local-llm-services/hands-on/ollama-setup/" data-link-title="Hands-on：安裝 Ollama &#43; 拉第一個 Gemma 模型" data-link-desc="brew install ollama、launchd service、ollama pull、curl 驗證 OpenAI 相容 API">Ollama 安裝</a></td>
      </tr>
      <tr>
          <td>RAG index（<code>index.pkl</code>）</td>
          <td><a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">RAG demo</a> 跑過 <code>ingest.py</code></td>
      </tr>
      <tr>
          <td>Python</td>
          <td>3.11+</td>
      </tr>
  </tbody>
</table>
<p>不需要安裝 MCP SDK——本 demo 手寫 JSON-RPC 處理、為了 inspection 透明度。Production server 建議改用 <a href="https://github.com/modelcontextprotocol">官方 SDK</a>（Python / TypeScript 都有）、處理 framing、capability negotiation、transport edge cases。</p>
<h2 id="mcp-協議的最小子集">MCP 協議的最小子集</h2>
<p><a href="/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP server</a> 要 handle 的核心 method：</p>
<table>
  <thead>
      <tr>
          <th>Method</th>
          <th>角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>initialize</code></td>
          <td>Client 跟 server 握手、交換 protocol version + capability</td>
      </tr>
      <tr>
          <td><code>notifications/initialized</code></td>
          <td>Client 通知 handshake 完成（notification、無 response）</td>
      </tr>
      <tr>
          <td><code>tools/list</code></td>
          <td>Client 問 server 有哪些 tool</td>
      </tr>
      <tr>
          <td><code>tools/call</code></td>
          <td>Client 呼叫某 tool、傳 arguments</td>
      </tr>
  </tbody>
</table>
<p>四個 method 之外、還可以暴露 resources / prompts / sampling、本 demo 只做 tools。</p>
<h2 id="server-實作">Server 實作</h2>
<p>完整檔案：<code>scripts/mcp-demo/blog_mcp_server.py</code>、約 150 行。</p>
<h3 id="主迴圈讀-stdin分派-method寫-stdout">主迴圈：讀 stdin、分派 method、寫 stdout</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">log</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[blog-mcp-demo] starting, index=</span><span class="si">{</span><span class="n">INDEX_PATH</span><span class="si">}</span><span class="s2">, tools=</span><span class="si">{</span><span class="nb">list</span><span class="p">(</span><span class="n">TOOLS</span><span class="o">.</span><span class="n">keys</span><span class="p">())</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">line</span> <span class="o">=</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">line</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="n">msg</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">line</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">except</span> <span class="n">json</span><span class="o">.</span><span class="n">JSONDecodeError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="n">log</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  parse error: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">method</span> <span class="o">=</span> <span class="n">msg</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;method&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">rid</span> <span class="o">=</span> <span class="n">msg</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;id&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="n">params</span> <span class="o">=</span> <span class="n">msg</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;params&#34;</span><span class="p">,</span> <span class="p">{})</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="n">log</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  → </span><span class="si">{</span><span class="n">method</span><span class="si">}</span><span class="s2"> (id=</span><span class="si">{</span><span class="n">rid</span><span class="si">}</span><span class="s2">)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">if</span> <span class="n">method</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">HANDLERS</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="n">respond</span><span class="p">(</span><span class="n">rid</span><span class="p">,</span> <span class="n">error</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;code&#34;</span><span class="p">:</span> <span class="o">-</span><span class="mi">32601</span><span class="p">,</span> <span class="s2">&#34;message&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;Method not found: </span><span class="si">{</span><span class="n">method</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="n">handler</span> <span class="o">=</span> <span class="n">HANDLERS</span><span class="p">[</span><span class="n">method</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="k">if</span> <span class="n">handler</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">            <span class="k">continue</span>  <span class="c1"># notification, no response expected</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">            <span class="n">result</span> <span class="o">=</span> <span class="n">handler</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">            <span class="n">respond</span><span class="p">(</span><span class="n">rid</span><span class="p">,</span> <span class="n">result</span><span class="o">=</span><span class="n">result</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">            <span class="n">log</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  ✗ handler error: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="n">respond</span><span class="p">(</span><span class="n">rid</span><span class="p">,</span> <span class="n">error</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;code&#34;</span><span class="p">:</span> <span class="o">-</span><span class="mi">32000</span><span class="p">,</span> <span class="s2">&#34;message&#34;</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)})</span></span></span></code></pre></div><p><strong>每段做什麼</strong>：</p>
<ol>
<li><strong><code>log(...)</code> 開機訊息</strong>：印到 stderr（不是 stdout）、讓人類能看到 server 啟動了、什麼 tools 可用。stdout 完全保留給 JSON-RPC 用。</li>
<li><strong><code>for line in sys.stdin</code></strong>：MCP 的 stdio transport 是 line-delimited JSON—— 每個 message 一行、<code>\n</code> 結束。Python 的 file iteration 自動按行切。</li>
<li><strong><code>line.strip()</code> + <code>if not line</code></strong>：空行 skip（不是 protocol error、只是 idle）。</li>
<li><strong><code>json.loads(line)</code></strong> with <code>try / except</code>：parse 失敗（malformed input）不 crash、log error 繼續下一行。Protocol 訊息該是合法 JSON、parse error 表示 client 出錯。</li>
<li><strong><code>msg.get(&quot;method&quot;)</code> / <code>msg.get(&quot;id&quot;)</code> / <code>msg.get(&quot;params&quot;, {})</code></strong>：JSON-RPC 2.0 標準三個欄位。<code>get</code> 而不是 <code>[]</code>、避免 KeyError；params 預設空 dict、後面 handler 可以安全 <code>.get(&quot;xxx&quot;)</code>。</li>
<li><strong><code>if method not in HANDLERS: respond(rid, error={&quot;code&quot;: -32601, ...})</code></strong>：未知 method 回標準 JSON-RPC error <code>-32601</code>（Method not found）。Client 知道這個 method 不能用、但 server 不死。</li>
<li><strong><code>if handler is None: continue</code></strong>：notification（如 <code>notifications/initialized</code>）對應的 handler 是 <code>None</code>、不該回 response。</li>
<li><strong><code>try: result = handler(params); respond(rid, result=result)</code></strong>：呼叫 handler、把結果回給 client。</li>
<li><strong><code>except Exception as e: ... respond(rid, error={&quot;code&quot;: -32000, ...})</code></strong>：handler 內部錯誤回 <code>-32000</code>（generic server error）。確保 server 任何時候都不 crash、即使工具 bug 也讓 client 拿到 error response。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼用 line-delimited JSON、不是 length-prefixed</strong>：MCP spec 規定 stdio transport 是 newline-delimited。length-prefixed 是 LSP 的做法、解析複雜（要先讀 Content-Length header 再讀 N bytes）；newline-delimited 用 <code>for line in sys.stdin</code> 一行解決。</li>
<li><strong>為什麼 stderr 不能寫 stdout</strong>：stdio transport 的 invariant——stdout 是 protocol channel、只能寫 JSON-RPC message。任何 stray print() / debug output 進 stdout、會被 client parse JSON 時炸（「multiple JSON values on one line」或 invalid JSON）。所有 log / debug / progress message 必須走 stderr。寫錯這條 server 看起來不工作、debug 很久才找到。</li>
<li><strong>為什麼 dispatch 用 dict-of-handlers 而不是 if/elif chain</strong>：擴充性。加新 method 只要往 <code>HANDLERS</code> dict 加一項、不用改 main loop。也讓 dispatch logic 跟 method 實作分離、容易測試。</li>
<li><strong>為什麼每個 handler 都用 try/except 包</strong>：「single point of failure」設計——任何 handler 例外不影響其他 method。Server 應該是 long-running daemon、不能因為一個 tool bug 死掉。</li>
<li><strong>為什麼 errors 用 JSON-RPC error code 而不是 HTTP-style status</strong>：JSON-RPC 2.0 標準。<code>-32700</code> parse error、<code>-32600</code> invalid request、<code>-32601</code> method not found、<code>-32602</code> invalid params、<code>-32603</code> internal error、<code>-32000</code> to <code>-32099</code> 留給應用層自訂。</li>
</ul>
<h3 id="工具search_blog">工具：search_blog</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">tool_search_blog</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">top_k</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">records</span> <span class="o">=</span> <span class="n">load_index</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">q_vec</span> <span class="o">=</span> <span class="n">embed</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">scored</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="p">((</span><span class="n">cosine</span><span class="p">(</span><span class="n">q_vec</span><span class="p">,</span> <span class="n">r</span><span class="p">[</span><span class="s2">&#34;embedding&#34;</span><span class="p">]),</span> <span class="n">r</span><span class="p">)</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">records</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">)[:</span><span class="n">top_k</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">results</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="s2">&#34;source&#34;</span><span class="p">:</span> <span class="n">r</span><span class="p">[</span><span class="s2">&#34;source&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="s2">&#34;chunk_index&#34;</span><span class="p">:</span> <span class="n">r</span><span class="p">[</span><span class="s2">&#34;chunk_index&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="s2">&#34;score&#34;</span><span class="p">:</span> <span class="nb">round</span><span class="p">(</span><span class="n">score</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="s2">&#34;preview&#34;</span><span class="p">:</span> <span class="n">r</span><span class="p">[</span><span class="s2">&#34;text&#34;</span><span class="p">][:</span><span class="mi">160</span><span class="p">]</span> <span class="o">+</span> <span class="p">(</span><span class="s2">&#34;...&#34;</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">r</span><span class="p">[</span><span class="s2">&#34;text&#34;</span><span class="p">])</span> <span class="o">&gt;</span> <span class="mi">160</span> <span class="k">else</span> <span class="s2">&#34;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">for</span> <span class="n">score</span><span class="p">,</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">scored</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="p">[{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;text&#34;</span><span class="p">,</span> <span class="s2">&#34;text&#34;</span><span class="p">:</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">results</span><span class="p">,</span> <span class="n">ensure_ascii</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">2</span><span class="p">)}]}</span></span></span></code></pre></div><p><strong>每段做什麼</strong>：</p>
<ol>
<li><strong><code>records = load_index()</code></strong>：lazy load <code>index.pkl</code>、第一次 call 載入記憶體、後續直接用 cached。Server 啟動時 lazy load 而不是 import 時 load、讓 server 即使在 Ollama 還沒起 / index 不存在時也能 boot（之後 call 才會報 error）。</li>
<li><strong><code>q_vec = embed(query)</code></strong>：把 query 轉成 768 維向量、呼叫 Ollama embedding API、跟 RAG demo 的 <code>embed</code> 是同一個 function。</li>
<li><strong><code>sorted((...) for r in records, key=lambda x: x[0], reverse=True)[:top_k]</code></strong>：generator expression + sorted 一次完成「算分 → 排序 → 取 top-K」。</li>
<li><strong><code>results = [{...} for score, r in scored]</code></strong>：把 top-K 整理成 client 友善的 dict 結構、含 source、chunk_index、score、preview（前 160 字 + 省略號）。</li>
<li><strong><code>{&quot;content&quot;: [{&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: json.dumps(...)}]}</code></strong>：MCP <code>tools/call</code> 標準 response 格式——<code>content</code> 是 array、每個元素 type + payload。<code>type: &quot;text&quot;</code> 是文字 content、<code>text</code> 是實際內容（這裡是 JSON 字串、讓 LLM 可以 parse）。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼 generator expression 而非 list comprehension</strong>：<code>(... for r in records)</code> 是 generator、<code>sorted</code> 直接消費、不會在記憶體中建中間 list。對 463 records 影響不大、但展現 memory-efficient pattern。</li>
<li><strong>為什麼 preview 切到 160 字</strong>：兩件事的平衡——讓 LLM 看到的 search result 短（不淹沒 LLM 的 context）、但夠判讀（160 中文字約 80 token、能看出 chunk 是不是相關）。如果 LLM 要完整內容、再 call <code>read_chunk</code>。</li>
<li><strong>為什麼回傳 JSON 字串、不是 nested object</strong>：MCP <code>content</code> 規定每個 element 是 <code>{type, payload}</code>、<code>type: &quot;text&quot;</code> 的 <code>text</code> 必須是 string、不能直接放 nested object。要傳結構化資料、就把它 <code>json.dumps</code> 成字串。LLM 看到後可以自己 parse。</li>
<li><strong>為什麼 <code>ensure_ascii=False</code></strong>：預設 <code>json.dumps</code> 把非 ASCII 字元（如中文）轉成 <code>\uXXXX</code>、難讀。<code>ensure_ascii=False</code> 直接輸出 UTF-8、LLM 也能直接讀懂、節省 token 數（一個中文字 1 token vs 6 token 的 <code>中</code>）。</li>
<li><strong>為什麼 <code>round(score, 4)</code></strong>：score 是 float、原始可能是 <code>0.7497284598827362</code>、長且無意義。<code>round(score, 4)</code> 保留 4 位小數、<code>0.7497</code>、夠精確、wire size 短。</li>
</ul>
<h3 id="工具read_chunk">工具：read_chunk</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">tool_read_chunk</span><span class="p">(</span><span class="n">source</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">chunk_index</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">records</span> <span class="o">=</span> <span class="n">load_index</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">records</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="k">if</span> <span class="n">r</span><span class="p">[</span><span class="s2">&#34;source&#34;</span><span class="p">]</span> <span class="o">==</span> <span class="n">source</span> <span class="ow">and</span> <span class="n">r</span><span class="p">[</span><span class="s2">&#34;chunk_index&#34;</span><span class="p">]</span> <span class="o">==</span> <span class="n">chunk_index</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">            <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="p">[{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;text&#34;</span><span class="p">,</span> <span class="s2">&#34;text&#34;</span><span class="p">:</span> <span class="n">r</span><span class="p">[</span><span class="s2">&#34;text&#34;</span><span class="p">]}]}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="p">[{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;text&#34;</span><span class="p">,</span> <span class="s2">&#34;text&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;Not found: </span><span class="si">{</span><span class="n">source</span><span class="si">}</span><span class="s2">#chunk</span><span class="si">{</span><span class="n">chunk_index</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">}],</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">        <span class="s2">&#34;isError&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">    <span class="p">}</span></span></span></code></pre></div><p><strong>每段做什麼</strong>：</p>
<ol>
<li><strong><code>for r in records: if r[&quot;source&quot;] == source and r[&quot;chunk_index&quot;] == chunk_index: return ...</code></strong>：linear scan 找匹配的 record、找到回完整 text。</li>
<li><strong>找不到時 <code>return {... &quot;isError&quot;: True}</code></strong>：MCP 標準的「tool 內部失敗」訊號。<code>isError: True</code> 告訴 client「這個 tool call 失敗了」、<code>content</code> 內是 human-readable error message。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼 linear scan 而不是 dict lookup</strong>：可以改用 <code>{(source, chunk_index): record}</code> dict 變 O(1)。但 463 records 的 linear scan 是 &lt; 1ms、optimize 不值得。Production 跟 vector DB 整合時、retrieval 系統自帶 indexing。</li>
<li><strong>為什麼 <code>isError: True</code> 而不是 JSON-RPC error</strong>：分兩種錯誤：
<ul>
<li><strong>Protocol error</strong>：method 不存在、params 不合法、JSON parse 失敗——回 JSON-RPC <code>error</code> 物件。</li>
<li><strong>Tool semantic error</strong>：method OK、params OK、但 tool 邏輯上不能 complete（找不到資料、外部 service down）——回 normal response 加 <code>isError: True</code>。
MCP 設計這層分離、讓 client / LLM 區分「我做錯了」（協議層）跟「資料不存在」（語意層）。Production 設計工具時要仔細區分。</li>
</ul>
</li>
</ul>
<h3 id="tool-描述用-json-schema">Tool 描述用 JSON Schema</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">TOOLS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="s2">&#34;search_blog&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="s2">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Semantic search over blog content. Returns top-K relevant chunks with source paths.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="s2">&#34;inputSchema&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;object&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="s2">&#34;properties&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                <span class="s2">&#34;query&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span><span class="p">,</span> <span class="s2">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Natural language query&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                <span class="s2">&#34;top_k&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;integer&#34;</span><span class="p">,</span> <span class="s2">&#34;default&#34;</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="s2">&#34;minimum&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;maximum&#34;</span><span class="p">:</span> <span class="mi">20</span><span class="p">},</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="p">},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="s2">&#34;required&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;query&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="s2">&#34;fn&#34;</span><span class="p">:</span> <span class="k">lambda</span> <span class="n">args</span><span class="p">:</span> <span class="n">tool_search_blog</span><span class="p">(</span><span class="n">args</span><span class="p">[</span><span class="s2">&#34;query&#34;</span><span class="p">],</span> <span class="n">args</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;top_k&#34;</span><span class="p">,</span> <span class="mi">5</span><span class="p">)),</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="s2">&#34;read_chunk&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="s2">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Read the full text of a specific chunk by source path and chunk index.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="s2">&#34;inputSchema&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;object&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">            <span class="s2">&#34;properties&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">                <span class="s2">&#34;source&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span><span class="p">,</span> <span class="s2">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Markdown file path relative to content/&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">                <span class="s2">&#34;chunk_index&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;integer&#34;</span><span class="p">,</span> <span class="s2">&#34;minimum&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">            <span class="p">},</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">            <span class="s2">&#34;required&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;source&#34;</span><span class="p">,</span> <span class="s2">&#34;chunk_index&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="s2">&#34;fn&#34;</span><span class="p">:</span> <span class="k">lambda</span> <span class="n">args</span><span class="p">:</span> <span class="n">tool_read_chunk</span><span class="p">(</span><span class="n">args</span><span class="p">[</span><span class="s2">&#34;source&#34;</span><span class="p">],</span> <span class="n">args</span><span class="p">[</span><span class="s2">&#34;chunk_index&#34;</span><span class="p">]),</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>每個 field 角色</strong>：</p>
<ol>
<li><strong><code>description</code></strong>：給 LLM 看的、解釋這個 tool 解什麼問題。LLM 看 description 決定何時 call。<strong>這是模型 follow tool 的最主要訊號</strong>——寫得清晰具體、模型用得對。</li>
<li><strong><code>inputSchema</code></strong>：JSON Schema、描述 tool 接受的參數結構。LLM application 用這個 schema 約束 LLM 生成「合法的呼叫」。</li>
<li><strong><code>properties</code></strong>：每個參數的型別 + 約束。</li>
<li><strong><code>required</code></strong>：必填參數清單。LLM 漏掉時、client 端可以 reject、不會浪費 round-trip。</li>
<li><strong><code>default</code></strong>：可選參數的預設值。傳的時候不給、tool 就用 default。</li>
<li><strong><code>minimum</code> / <code>maximum</code></strong>：數值約束。<code>top_k</code> 設 1-20 是因為 &lt; 1 沒意義、&gt; 20 浪費 retrieval。</li>
<li><strong><code>fn</code></strong>：實際 dispatch 用的 callable。本 demo 用 lambda 把 <code>args</code> dict 轉成 positional / keyword call。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼 description 要具體</strong>：LLM 看 description 決定 call 時機。「search the blog」對 LLM 來說太模糊（搜什麼？找什麼？）、改成「Semantic search over blog content. Returns top-K relevant chunks with source paths.」明確描述輸入跟輸出形狀、LLM 能判讀「使用者問技術問題時該 call 這個」。</li>
<li><strong>為什麼 schema 用 JSON Schema、不是自訂格式</strong>：JSON Schema 是 web 標準、所有 LLM application 都認識、跨 framework 可移植。也是 <a href="/blog/llm/knowledge-cards/function-calling/" data-link-title="Function Calling" data-link-desc="模型訓練階段建立的「呼叫工具」能力：知道何時該呼叫、傳什麼參數">function calling</a> 跟 <a href="/blog/llm/04-applications/tool-use-principles/" data-link-title="4.3 Tool use 原理：LLM 跟外部世界互動" data-link-desc="Structured output 是 LLM 跨入工程系統的橋、function calling 取捨、為什麼本地小模型 tool use 表現崩潰">Tool use 原理</a> 的 schema 描述語言。</li>
<li><strong>為什麼 <code>required</code> 跟 <code>default</code> 兩個機制</strong>：對 LLM 看的 prompt 越清楚越好。<code>required</code> 告訴 LLM「不傳這個會錯」、<code>default</code> 告訴 LLM「可不傳、預設值是 X」。沒分清的話、LLM 可能總是傳所有參數、雜訊多。</li>
<li><strong>為什麼 <code>fn</code> 用 lambda 包</strong>：實際 tool function 是 positional args、但 client 送的是 dict。lambda 把 dict 拆成 function call 的 args。也方便將來如果 tool function signature 變、只要改 lambda 不用改 dispatcher。</li>
</ul>
<h2 id="client-實作測試用">Client 實作（測試用）</h2>
<p>完整檔案：<code>scripts/mcp-demo/test_client.py</code>。實際 production 用 Claude Desktop / Cursor 等 MCP-capable application。本 demo 寫一個 stdio client、模擬 application 行為：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">proc</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">Popen</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="p">[</span><span class="n">sys</span><span class="o">.</span><span class="n">executable</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">SERVER</span><span class="p">)],</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">stdin</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">text</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">bufsize</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">)</span>
</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"><span class="k">def</span> <span class="nf">send</span><span class="p">(</span><span class="n">method</span><span class="p">,</span> <span class="n">params</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">rid</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">msg</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;jsonrpc&#34;</span><span class="p">:</span> <span class="s2">&#34;2.0&#34;</span><span class="p">,</span> <span class="s2">&#34;method&#34;</span><span class="p">:</span> <span class="n">method</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="n">params</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">msg</span><span class="p">[</span><span class="s2">&#34;params&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">params</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">if</span> <span class="n">rid</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="n">msg</span><span class="p">[</span><span class="s2">&#34;id&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">rid</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">proc</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">proc</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">if</span> <span class="n">rid</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="k">return</span> <span class="kc">None</span>  <span class="c1"># notification</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="n">line</span> <span class="o">=</span> <span class="n">proc</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">readline</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">line</span><span class="p">)</span></span></span></code></pre></div><p><strong>每個參數做什麼</strong>：</p>
<ol>
<li><strong><code>subprocess.Popen([sys.executable, str(SERVER)], ...)</code></strong>：spawn server 當 child process。用 <code>sys.executable</code> 確保用同一個 Python interpreter（避免 venv 跟系統 Python 混用）。</li>
<li><strong><code>stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE</code></strong>：三條 pipe 都接到 client、讓我們能讀寫 server 的 stdio。</li>
<li><strong><code>text=True</code></strong>：自動處理 str ↔ bytes 編碼、直接讀寫字串、不用手動 encode/decode。預設是 binary mode。</li>
<li><strong><code>bufsize=1</code></strong>：line buffering、每寫一行就 flush。沒這個的話、Python 預設 block buffering（4KB 才 flush）、client 寫的 message server 看不到、整個卡住。</li>
<li><strong><code>proc.stdin.write(json.dumps(msg) + &quot;\n&quot;)</code></strong>：寫 JSON 訊息、結尾加 <code>\n</code>（line-delimited）。</li>
<li><strong><code>proc.stdin.flush()</code></strong>：強制立刻送出。即使有 <code>bufsize=1</code>、明確 flush 是好習慣、避免任何 buffer 累積。</li>
<li><strong><code>if rid is None: return None</code></strong>：notification 不該等 response。</li>
<li><strong><code>line = proc.stdout.readline()</code> + <code>json.loads(line)</code></strong>：讀一行 response、parse。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼 stdio 而不是 socket / HTTP</strong>：MCP stdio transport 的主要場景是「application spawn server」(Claude Desktop 開 Python 進程當 MCP server)。Stdio 自然形成 1-to-1 ownership、不需要 port allocation、不需要 auth。HTTP transport 也存在、用在 multi-client 場景。</li>
<li><strong>為什麼 <code>bufsize=1</code> 這麼關鍵</strong>：Python 預設 stdio buffer 4KB。如果 server / client 任一邊寫了 short message 但沒 fill 4KB、message 不會被另一邊看到、protocol 卡死。看起來是 hang、debug 困難。<code>bufsize=1</code> 強制 line buffering、解決這個 deadlock。</li>
<li><strong>為什麼 <code>text=True</code></strong>：JSON-RPC 都是文字、binary mode 要手動 <code>.encode()</code> / <code>.decode()</code>、增加複雜度。<code>text=True</code> 自動處理 UTF-8。</li>
</ul>
<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"><span class="nb">cd</span> ~/Projects/blog
</span></span><span class="line"><span class="ln">2</span><span class="cl">python3 scripts/mcp-demo/test_client.py</span></span></code></pre></div><ul>
<li><code>cd ~/Projects/blog</code>：切到 repo 根、讓 SERVER 路徑相對解析正確。</li>
<li><code>python3 scripts/mcp-demo/test_client.py</code>：跑 test client、它會 spawn server 跟它對話。</li>
</ul>
<p>預期看到五個階段：</p>
<h3 id="1-initialize握手">1. initialize（握手）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="err">===</span> <span class="mi">1</span><span class="err">.</span> <span class="err">initialize</span> <span class="err">===</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;jsonrpc&#34;</span><span class="p">:</span> <span class="s2">&#34;2.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nt">&#34;result&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nt">&#34;protocolVersion&#34;</span><span class="p">:</span> <span class="s2">&#34;2025-03-26&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nt">&#34;capabilities&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;tools&#34;</span><span class="p">:</span> <span class="p">{}},</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nt">&#34;serverInfo&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;blog-mcp-demo&#34;</span><span class="p">,</span> <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="s2">&#34;0.1.0&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>Protocol 意義</strong>：</p>
<ul>
<li><code>protocolVersion</code>：server 支援的 MCP 版本。Client 要 negotiate（自己 cap 較新時要 downgrade）。</li>
<li><code>capabilities.tools: {}</code>：server 宣告「我支援 tools 功能」、空 object 表示沒額外 sub-feature。Client 拿到後知道可以 call <code>tools/list</code>。</li>
<li><code>serverInfo</code>：server 識別資訊、給 client 顯示用（debug、logging）。</li>
<li><code>id: 1</code>：對應 client 送的 request id、讓 client 知道這個 response 是哪個 request 的。</li>
</ul>
<h3 id="2-toolslist">2. tools/list</h3>
<p>Server 回兩個 tool 的完整 schema：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;tools&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;search_blog&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nt">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Semantic search over blog content...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nt">&#34;inputSchema&#34;</span><span class="p">:</span> <span class="p">{</span><span class="err">...JSON</span> <span class="err">Schema...</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;read_chunk&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="nt">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Read the full text of a specific chunk...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="nt">&#34;inputSchema&#34;</span><span class="p">:</span> <span class="p">{</span><span class="err">...</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>Protocol 意義</strong>：這個輸出就是 LLM application 會塞給 LLM 的 tool 描述。LLM application 把這份 schema 用 <a href="/blog/llm/knowledge-cards/function-calling/" data-link-title="Function Calling" data-link-desc="模型訓練階段建立的「呼叫工具」能力：知道何時該呼叫、傳什麼參數">function calling</a> 機制給模型看、模型決定何時呼叫、傳什麼參數。Server 跟模型之間靠這層 schema 對齊、模型不直接呼叫 server、是經 application 中介。</p>
<h3 id="3-toolscall-search_blog">3. tools/call: search_blog</h3>
<p>Client 送：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;method&#34;</span><span class="p">:</span> <span class="s2">&#34;tools/call&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;params&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;search_blog&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nt">&#34;arguments&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;query&#34;</span><span class="p">:</span> <span class="s2">&#34;什麼是 KV cache？&#34;</span><span class="p">,</span> <span class="nt">&#34;top_k&#34;</span><span class="p">:</span> <span class="mi">3</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="mi">3</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>params</code> 包兩件事：</p>
<ul>
<li><code>name</code>：要 call 的 tool 名（matches <code>tools/list</code> 內某個 tool）。</li>
<li><code>arguments</code>：實際傳給 tool 的 dict、結構符合該 tool 的 <code>inputSchema</code>。</li>
</ul>
<p>Server 回 cosine 搜尋結果（preview）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">{</span><span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;llm/00-foundations/hardware-memory-budget.md&#34;</span><span class="p">,</span> <span class="nt">&#34;chunk_index&#34;</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="nt">&#34;score&#34;</span><span class="p">:</span> <span class="mf">0.7497</span><span class="p">,</span> <span class="nt">&#34;preview&#34;</span><span class="p">:</span> <span class="s2">&#34;| Context 長度 | KV cache 估算...&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">{</span><span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;llm/00-foundations/why-llm-feels-slow.md&#34;</span><span class="p">,</span> <span class="nt">&#34;chunk_index&#34;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span> <span class="nt">&#34;score&#34;</span><span class="p">:</span> <span class="mf">0.7212</span><span class="p">,</span> <span class="nt">&#34;preview&#34;</span><span class="p">:</span> <span class="s2">&#34;...&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="p">{</span><span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;llm/03-theoretical-foundations/attention-mechanism.md&#34;</span><span class="p">,</span> <span class="nt">&#34;chunk_index&#34;</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span> <span class="nt">&#34;score&#34;</span><span class="p">:</span> <span class="mf">0.7176</span><span class="p">,</span> <span class="nt">&#34;preview&#34;</span><span class="p">:</span> <span class="s2">&#34;...&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">]</span></span></span></code></pre></div><p>實測命中合理——KV cache 相關段落都被找到。</p>
<h3 id="4-toolscall-read_chunk">4. tools/call: read_chunk</h3>
<p>Client 用 search 拿到的 source + chunk_index、call <code>read_chunk</code> 拿完整內容：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;method&#34;</span><span class="p">:</span> <span class="s2">&#34;tools/call&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;params&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;read_chunk&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nt">&#34;arguments&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;llm/00-foundations/hardware-memory-budget.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="nt">&#34;chunk_index&#34;</span><span class="p">:</span> <span class="mi">5</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Server 回該 chunk 的完整 markdown 文字。這實現了「search → read」的兩段流程——避免 search 一次就把所有 chunk 完整內容塞給 LLM（context 暴炸）、讓 LLM 自己看 preview 決定要 deep dive 哪個。</p>
<h3 id="5-錯誤路徑">5. 錯誤路徑</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="err">===</span> <span class="mi">5</span><span class="err">.</span> <span class="err">unknown</span> <span class="err">method</span> <span class="err">(error</span> <span class="err">path)</span> <span class="err">===</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">{</span><span class="nt">&#34;jsonrpc&#34;</span><span class="p">:</span> <span class="s2">&#34;2.0&#34;</span><span class="p">,</span> <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="nt">&#34;error&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;code&#34;</span><span class="p">:</span> <span class="mi">-32601</span><span class="p">,</span> <span class="nt">&#34;message&#34;</span><span class="p">:</span> <span class="s2">&#34;Method not found: does/not/exist&#34;</span><span class="p">}}</span></span></span></code></pre></div><p><code>-32601</code> 是 JSON-RPC 標準 error code for unknown method。Server 對未知 method 回標準 error、不 crash。Client 知道這個 method 不能用、繼續其他操作。</p>
<h2 id="跟-claude-desktop--cursor-整合">跟 Claude Desktop / Cursor 整合</h2>
<p>把這個 server 接到實際 MCP-capable application：</p>
<h3 id="claude-desktop">Claude Desktop</h3>
<p>編輯 <code>~/Library/Application Support/Claude/claude_desktop_config.json</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;mcpServers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nt">&#34;blog-search&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;/path/to/python3&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;&lt;absolute-path-to-blog&gt;/scripts/mcp-demo/blog_mcp_server.py&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>每個 field 做什麼</strong>：</p>
<ul>
<li><code>mcpServers</code>：MCP server 註冊表、key 是任意名稱（client 識別用）。</li>
<li><code>command</code>：spawn 用的 executable path。要寫絕對路徑、Claude Desktop 啟動時的 PATH 可能不含 <code>python3</code>。</li>
<li><code>args</code>：傳給 command 的 args list。第一個是 script path。</li>
</ul>
<p><strong>為什麼這樣設計</strong>：Claude Desktop 啟動時讀這個 config、對每個 server 用 <code>subprocess.spawn(command, args)</code> 起 child process、用 stdio 跟它對話。跟本 demo 的 <code>test_client.py</code> 做的事完全一樣、只是改成 GUI application 而已。</p>
<p>重啟 Claude Desktop 後、在對話框問「用 search_blog 找 KV cache 相關段落」、Claude 會自動 call tool 並用結果回答。</p>
<h3 id="cursor">Cursor</h3>
<p><code>.cursor/mcp.json</code>（per-project）或全域設定類似結構。具體欄位看當下版本文件。</p>
<p>兩種整合的共通點：<strong>MCP server 自己不變</strong>、只要 application 端配置 path 跟 args、整合就完成。這正是 4.3 章節 N×M → N+M 的具體展現——本 server 不為任何特定 application 客製化、就能被多個 application 接到。</p>
<h2 id="觀察跟原理對應">觀察跟原理對應</h2>
<p>回到 <a href="/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">4.6 應用層協議</a> 的三層 framing：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>本 demo 是否實作</th>
          <th>怎麼實作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模型能力</td>
          <td>不在本 demo 範圍</td>
          <td>LLM application 自己決定用 GPT/Claude/Gemma</td>
      </tr>
      <tr>
          <td>Sampling 約束</td>
          <td>不在本 demo 範圍</td>
          <td>application + 推論伺服器配合</td>
      </tr>
      <tr>
          <td>Server 協議</td>
          <td><strong>本 demo 焦點</strong></td>
          <td>JSON-RPC over stdio + tools/list / tools/call</td>
      </tr>
  </tbody>
</table>
<p>這個分離正是 MCP 的核心收益：server 寫好之後、用什麼 LLM 跟它互動跟 server 無關。換掉 LLM、換掉 application、server code 完全不動。</p>
<h2 id="何時這份-demo-會過時">何時這份 demo 會過時</h2>
<ul>
<li><strong>MCP protocol version</strong>：目前用 <code>2025-03-26</code>、未來會更新、但「server 暴露 tool 給 application」的 framing 不變。</li>
<li><strong>JSON-RPC 細節</strong>：可能 transport 形式增加（HTTP / WebSocket）、stdio 不會消失。</li>
<li><strong>Tool 描述格式</strong>：JSON Schema 是 web 通用標準、不會被換掉。</li>
</ul>
<p>實作換代時、可以把手寫 JSON-RPC 換成官方 SDK、tool 內部邏輯（embedding / cosine / pickle）依需求換、但 protocol 骨架（initialize / tools/list / tools/call）會保留。</p>
<h2 id="跑這個-demo-的指令總結">跑這個 demo 的指令總結</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"><span class="c1"># 前置：確認 Ollama 跑著、index.pkl 存在</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ollama list <span class="p">|</span> grep nomic-embed-text
</span></span><span class="line"><span class="ln">3</span><span class="cl">ls scripts/rag-demo/index.pkl</span></span></code></pre></div><ul>
<li><code>ollama list</code>：列已下載 model、<code>grep</code> 過濾出 embedding model。沒看到表示要先 <code>ollama pull nomic-embed-text</code>。</li>
<li><code>ls scripts/rag-demo/index.pkl</code>：確認 RAG ingest 跑過、index 存在。沒看到要先跑 <code>python3 scripts/rag-demo/ingest.py</code>。</li>
</ul>





<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="c1"># 自動測試 MCP server</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">python3 scripts/mcp-demo/test_client.py</span></span></code></pre></div><ul>
<li>跑 test_client、spawn server、依序送 5 個 request 驗證 protocol。stdout 印 protocol 對話、stderr 印 server log。看到全部 5 階段 OK 就成功。</li>
</ul>





<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="c1"># 手動跟 server 互動（看 protocol 原始 wire format）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">python3 scripts/mcp-demo/blog_mcp_server.py
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 然後手打：{&#34;jsonrpc&#34;:&#34;2.0&#34;,&#34;id&#34;:1,&#34;method&#34;:&#34;initialize&#34;,&#34;params&#34;:{}}</span></span></span></code></pre></div><ul>
<li>直接 invoke server、它讀 stdin 等 request。手打 JSON-RPC 訊息、看 server 回。是學 protocol 最直接的方式——你會看到 wire format 真實長相、跟自動 client 包裝後不一樣。</li>
</ul>
<p>完整 source 在 <code>scripts/mcp-demo/</code>、約 250 行 Python、stdlib only。</p>
<p>跟其他 hands-on 章節的關係：完整 hands-on 系列見 <a href="/blog/llm/01-local-llm-services/hands-on/" data-link-title="Hands-on：本地 AI 工具實作筆記" data-link-desc="Ollama / ComfyUI / Whisper / Piper TTS：實際安裝、驗證、跑通的紀錄。隨工具版本演化、跟 1.x 原理章節互補。">Hands-on 章節索引</a>、本 demo 依賴的索引由 <a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">RAG demo</a> ingest 產生、MCP + RAG 同跑的記憶體 / 程序預算見 <a href="/blog/llm/01-local-llm-services/hands-on/rag-mcp-resources/" data-link-title="Hands-on：RAG / MCP 的資源 footprint" data-link-desc="RAG ingest / query / MCP server 三階段的 RAM / 磁碟 / process 實測、多模型並存的 RAM 衝突、本地 LLM 跑 RAG 跟單純 chat 的差異">RAG + MCP resource footprint</a>、術語見 <a href="/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP</a>。</p>
]]></content:encoded></item><item><title>4.6 應用層協議：function calling / structured output / MCP</title><link>https://tarrragon.github.io/blog/llm/04-applications/application-protocols/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/application-protocols/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/function-calling/" data-link-title="Function Calling" data-link-desc="模型訓練階段建立的「呼叫工具」能力：知道何時該呼叫、傳什麼參數">Function calling&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/structured-output/" data-link-title="Structured Output" data-link-desc="讓 LLM 輸出可被 parser 穩定消費的推論階段設計：JSON mode、schema-guided decoding、grammar 約束都屬於這一層">structured output&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP&lt;/a> 是 LLM 應用落地時最常被混為一談的三個術語。三者解的問題層級完全不同：function calling 是&lt;strong>模型能力&lt;/strong>（訓練階段建立）、structured output 是**&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/sampling-constraint/" data-link-title="Sampling Constraint" data-link-desc="推論時限制下一個 token 候選集合的控制手段，用來把模型生成導向合法格式或特定選項">sampling 約束&lt;/a>&lt;strong>（推論階段控制）、MCP 是&lt;/strong>server 協議**（架構層標準化）。把三者放回正確層級、應用設計就會變清楚；混為一談會看到「我啟用了 function calling 為什麼還需要 structured output」「MCP 跟 function calling 衝突嗎」這類根本誤解。&lt;/p>
&lt;p>本章把三者的層級差異拆開、解釋為什麼會出現 MCP、跟它們在實際應用中怎麼組合。具體 spec 細節（OpenAI function calling JSON 格式、Anthropic tools API、MCP server 實作）不在本章——這些半年一變、本章寫的是「換 spec 之後仍成立」的概念結構。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後你能：&lt;/p>
&lt;ol>
&lt;li>用一句話分別說清楚三者解什麼問題。&lt;/li>
&lt;li>看到「啟用 function calling」「設定 structured output」「裝 MCP server」這些句子時、知道在說哪一層。&lt;/li>
&lt;li>判斷一個 LLM 應用該用哪幾個組合、什麼情境只需要一部分。&lt;/li>
&lt;li>解釋為什麼 MCP 會出現、它複用了哪個成功模式。&lt;/li>
&lt;/ol>
&lt;h2 id="三個概念的層級差異">三個概念的層級差異&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>概念&lt;/th>
 &lt;th>解的問題&lt;/th>
 &lt;th>在哪一層&lt;/th>
 &lt;th>跟模型訓練的關係&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Function calling&lt;/td>
 &lt;td>模型怎麼「知道」要呼叫工具&lt;/td>
 &lt;td>模型能力&lt;/td>
 &lt;td>訓練時建立、寫進權重&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Structured output&lt;/td>
 &lt;td>模型輸出怎麼被 parser 確定性消費&lt;/td>
 &lt;td>Sampling 約束&lt;/td>
 &lt;td>推論時控制、跟訓練無關&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MCP&lt;/td>
 &lt;td>LLM application 怎麼接外部 tool&lt;/td>
 &lt;td>Server 協議&lt;/td>
 &lt;td>不涉模型、純架構標準&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三者正交、可獨立或組合：&lt;/p>
&lt;ul>
&lt;li>用 function calling 但不用 structured output：訓練過 tool use 的模型直接呼叫工具、靠模型自律輸出合法 JSON。&lt;/li>
&lt;li>用 structured output 但不用 function calling：模型沒訓練過 tool use、用 prompt + grammar 強制輸出合法格式。&lt;/li>
&lt;li>用 MCP 但不用 function calling：MCP 標準化 tool 的暴露方式、模型用什麼機制呼叫不重要。&lt;/li>
&lt;li>三者都用：function calling 讓模型穩、structured output 約束格式、MCP 提供 tool ecosystem。&lt;/li>
&lt;/ul>
&lt;p>把這張表記熟、再看 LLM 應用相關討論、會發現「這個工具支援 function calling」「我的應用要 MCP」這類句子實際在說不同層級。&lt;/p>
&lt;h2 id="function-calling-是模型能力">Function Calling 是模型能力&lt;/h2>
&lt;p>Function calling 是模型在訓練階段建立的能力：&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/training-pipeline/" data-link-title="3.4 訓練流程：pre-train → SFT → RLHF" data-link-desc="LLM 的三階段訓練：預訓練、指令微調、人類反饋強化學習；各階段目標與最新替代方案">SFT 階段&lt;/a>大量「使用者 query + 該呼叫什麼工具 + 傳什麼參數」的範例、讓模型學會「看到 query 知道何時呼叫、怎麼呼叫」。&lt;/p>
&lt;p>判讀模型 function calling 強弱的訊號：&lt;/p>
&lt;ul>
&lt;li>該呼叫時呼叫、不該呼叫時不呼叫的準確度。&lt;/li>
&lt;li>呼叫格式合法率（不亂寫 JSON）。&lt;/li>
&lt;li>參數準確度（type 正確、value 合理）。&lt;/li>
&lt;li>多工具情況下選對工具的準確度。&lt;/li>
&lt;/ul>
&lt;p>這四個訊號跨模型差異大、根因是訓練資料分佈：&lt;/p>
&lt;ul>
&lt;li>OpenAI / Anthropic 旗艦模型 SFT 階段 function calling 範例大量、表現穩定。&lt;/li>
&lt;li>Llama 3 / Gemma 4 / Qwen3 開源旗艦模型 SFT 階段也加 function calling、但範例量不一、表現有落差。&lt;/li>
&lt;li>小型開源模型（&amp;lt; 14B）function calling 訓練嚴重不足；tool schema 複雜、多工具選擇、巢狀參數時失敗率高、單一工具 + 平坦 schema 仍可用。&lt;/li>
&lt;/ul>
&lt;p>理解這點的價值：看到「這個模型支援 function calling」的宣稱、要追問「&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/training-example-coverage/" data-link-title="Training Example Coverage" data-link-desc="訓練資料中的任務範例是否覆蓋足夠情境，決定模型在 function calling、格式輸出與邊界案例上的穩定性">訓練範例 coverage&lt;/a> 多廣」、不是 binary 的支援 / 不支援、是 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/capability-spectrum/" data-link-title="Capability Spectrum" data-link-desc="把模型能力視為連續光譜而非支援 / 不支援二分，用覆蓋度、穩定性與失敗模式判讀真實可用性">spectrum&lt;/a> 的訓練深度。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/llm/knowledge-cards/function-calling/" data-link-title="Function Calling" data-link-desc="模型訓練階段建立的「呼叫工具」能力：知道何時該呼叫、傳什麼參數">Function calling</a>、<a href="/blog/llm/knowledge-cards/structured-output/" data-link-title="Structured Output" data-link-desc="讓 LLM 輸出可被 parser 穩定消費的推論階段設計：JSON mode、schema-guided decoding、grammar 約束都屬於這一層">structured output</a>、<a href="/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP</a> 是 LLM 應用落地時最常被混為一談的三個術語。三者解的問題層級完全不同：function calling 是<strong>模型能力</strong>（訓練階段建立）、structured output 是**<a href="/blog/llm/knowledge-cards/sampling-constraint/" data-link-title="Sampling Constraint" data-link-desc="推論時限制下一個 token 候選集合的控制手段，用來把模型生成導向合法格式或特定選項">sampling 約束</a><strong>（推論階段控制）、MCP 是</strong>server 協議**（架構層標準化）。把三者放回正確層級、應用設計就會變清楚；混為一談會看到「我啟用了 function calling 為什麼還需要 structured output」「MCP 跟 function calling 衝突嗎」這類根本誤解。</p>
<p>本章把三者的層級差異拆開、解釋為什麼會出現 MCP、跟它們在實際應用中怎麼組合。具體 spec 細節（OpenAI function calling JSON 格式、Anthropic tools API、MCP server 實作）不在本章——這些半年一變、本章寫的是「換 spec 之後仍成立」的概念結構。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後你能：</p>
<ol>
<li>用一句話分別說清楚三者解什麼問題。</li>
<li>看到「啟用 function calling」「設定 structured output」「裝 MCP server」這些句子時、知道在說哪一層。</li>
<li>判斷一個 LLM 應用該用哪幾個組合、什麼情境只需要一部分。</li>
<li>解釋為什麼 MCP 會出現、它複用了哪個成功模式。</li>
</ol>
<h2 id="三個概念的層級差異">三個概念的層級差異</h2>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>解的問題</th>
          <th>在哪一層</th>
          <th>跟模型訓練的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Function calling</td>
          <td>模型怎麼「知道」要呼叫工具</td>
          <td>模型能力</td>
          <td>訓練時建立、寫進權重</td>
      </tr>
      <tr>
          <td>Structured output</td>
          <td>模型輸出怎麼被 parser 確定性消費</td>
          <td>Sampling 約束</td>
          <td>推論時控制、跟訓練無關</td>
      </tr>
      <tr>
          <td>MCP</td>
          <td>LLM application 怎麼接外部 tool</td>
          <td>Server 協議</td>
          <td>不涉模型、純架構標準</td>
      </tr>
  </tbody>
</table>
<p>三者正交、可獨立或組合：</p>
<ul>
<li>用 function calling 但不用 structured output：訓練過 tool use 的模型直接呼叫工具、靠模型自律輸出合法 JSON。</li>
<li>用 structured output 但不用 function calling：模型沒訓練過 tool use、用 prompt + grammar 強制輸出合法格式。</li>
<li>用 MCP 但不用 function calling：MCP 標準化 tool 的暴露方式、模型用什麼機制呼叫不重要。</li>
<li>三者都用：function calling 讓模型穩、structured output 約束格式、MCP 提供 tool ecosystem。</li>
</ul>
<p>把這張表記熟、再看 LLM 應用相關討論、會發現「這個工具支援 function calling」「我的應用要 MCP」這類句子實際在說不同層級。</p>
<h2 id="function-calling-是模型能力">Function Calling 是模型能力</h2>
<p>Function calling 是模型在訓練階段建立的能力：<a href="/blog/llm/03-theoretical-foundations/training-pipeline/" data-link-title="3.4 訓練流程：pre-train → SFT → RLHF" data-link-desc="LLM 的三階段訓練：預訓練、指令微調、人類反饋強化學習；各階段目標與最新替代方案">SFT 階段</a>大量「使用者 query + 該呼叫什麼工具 + 傳什麼參數」的範例、讓模型學會「看到 query 知道何時呼叫、怎麼呼叫」。</p>
<p>判讀模型 function calling 強弱的訊號：</p>
<ul>
<li>該呼叫時呼叫、不該呼叫時不呼叫的準確度。</li>
<li>呼叫格式合法率（不亂寫 JSON）。</li>
<li>參數準確度（type 正確、value 合理）。</li>
<li>多工具情況下選對工具的準確度。</li>
</ul>
<p>這四個訊號跨模型差異大、根因是訓練資料分佈：</p>
<ul>
<li>OpenAI / Anthropic 旗艦模型 SFT 階段 function calling 範例大量、表現穩定。</li>
<li>Llama 3 / Gemma 4 / Qwen3 開源旗艦模型 SFT 階段也加 function calling、但範例量不一、表現有落差。</li>
<li>小型開源模型（&lt; 14B）function calling 訓練嚴重不足；tool schema 複雜、多工具選擇、巢狀參數時失敗率高、單一工具 + 平坦 schema 仍可用。</li>
</ul>
<p>理解這點的價值：看到「這個模型支援 function calling」的宣稱、要追問「<a href="/blog/llm/knowledge-cards/training-example-coverage/" data-link-title="Training Example Coverage" data-link-desc="訓練資料中的任務範例是否覆蓋足夠情境，決定模型在 function calling、格式輸出與邊界案例上的穩定性">訓練範例 coverage</a> 多廣」、不是 binary 的支援 / 不支援、是 <a href="/blog/llm/knowledge-cards/capability-spectrum/" data-link-title="Capability Spectrum" data-link-desc="把模型能力視為連續光譜而非支援 / 不支援二分，用覆蓋度、穩定性與失敗模式判讀真實可用性">spectrum</a> 的訓練深度。</p>
<h2 id="structured-output-是-sampling-約束">Structured Output 是 Sampling 約束</h2>
<p><a href="/blog/llm/knowledge-cards/structured-output/" data-link-title="Structured Output" data-link-desc="讓 LLM 輸出可被 parser 穩定消費的推論階段設計：JSON mode、schema-guided decoding、grammar 約束都屬於這一層">Structured output</a> 是推論階段的技巧、跟模型訓練無關：在 <a href="/blog/llm/03-theoretical-foundations/sampling-and-decoding/" data-link-title="3.5 Sampling 與 Decoding 策略" data-link-desc="Greedy、beam search、top-k、top-p、temperature、min-p：模型輸出後怎麼挑下一個 token">sampling</a>（從機率分佈挑下一個 token 的步驟）時對每個 token 做 <a href="/blog/llm/knowledge-cards/grammar/" data-link-title="Grammar" data-link-desc="描述合法字串形狀的形式規則，在 structured output 中用來限制 LLM 每一步可輸出的 token">grammar</a> / schema 約束、不合法 token 的機率（logit、token 機率的對數）被歸零、把不合法輸出的可能性壓到不會被 sample。</p>
<p>主要實作機制（適用 / 限制條件附在每項下）：</p>
<ul>
<li><strong>JSON mode</strong>：每步 sampling 過濾、只允許「保持 JSON 仍合法」的 token。適用：絕大多數 OpenAI 相容 API 都有支援；限制：只保 JSON 合法、不保 schema 對位。</li>
<li><strong>Grammar-constrained sampling</strong>：用 <a href="/blog/llm/knowledge-cards/grammar/" data-link-title="Grammar" data-link-desc="描述合法字串形狀的形式規則，在 structured output 中用來限制 LLM 每一步可輸出的 token">grammar</a>（描述合法語法的形式化規則、實作上常用 <a href="/blog/llm/knowledge-cards/bnf/" data-link-title="BNF（Backus-Naur Form）" data-link-desc="用遞迴產生式描述語法的經典記法，是 CFG、parser 與 grammar-constrained sampling 常見的基礎表示">BNF</a> 或 <a href="/blog/llm/knowledge-cards/lark-grammar/" data-link-title="Lark Grammar" data-link-desc="Lark parser 使用的 EBNF-like grammar 格式，常被 structured output 工具拿來描述自訂輸出語法">Lark grammar</a>）描述完整輸出形狀、推論時逐 token 過濾。適用：需要嚴格自訂格式（<a href="/blog/llm/knowledge-cards/dsl/" data-link-title="DSL（Domain-Specific Language）" data-link-desc="為特定業務或技術領域設計的小語言，在 LLM 應用中常作為可解析、可驗證、可執行的中介輸出">DSL</a>、特定 query language）；限制：要伺服器層支援（llama.cpp、vLLM 有、有些雲端 API 沒）。</li>
<li><strong>Schema-guided</strong>：依 JSON Schema 動態決定每步允許哪些 token、強制 enum / type / required 等約束。適用：複雜結構化資料；限制：實作複雜度高、跨伺服器一致性差。</li>
<li><strong>Logit bias</strong>：對特定 token 加 bias、間接引導 sampling、最弱但最靈活的方式。適用：簡單的 token 黑名單 / 白名單；限制：無法保證結構合法。</li>
</ul>
<p>優勢相對 function calling：</p>
<ul>
<li><strong>跨模型可移植</strong>：不依賴模型訓練、任何能跑 sampling 的模型都能上。</li>
<li><strong>可任意自訂格式</strong>：不限於 OpenAI 或某 provider 的 function spec、想定義什麼 schema 都行。</li>
<li><strong>保證 100% 合法輸出</strong>：grammar 約束下不可能輸出 invalid JSON。</li>
</ul>
<p>代價：</p>
<ul>
<li><strong>約束太嚴可能跟模型「自然」輸出衝突</strong>：模型本來想說 A、grammar 強制只能說 B、品質會降。</li>
<li><strong>實作成本</strong>：grammar 解析跟動態 logit mask 在推論伺服器要支援、不是所有 server 都成熟。</li>
<li><strong>跟模型訓練脫鉤</strong>：模型「不知道」自己被約束、可能還是用沒用 function calling 訓練的「猜測」方式生成。</li>
</ul>
<p>實務上 structured output 跟 function calling 經常組合：function calling 訓練讓模型「自然」傾向合法輸出、structured output 約束兜底保證「真的合法」。</p>
<h2 id="mcp-是-server-協議">MCP 是 Server 協議</h2>
<p>MCP（Model Context Protocol、2024 年由 Anthropic 提出）是「LLM application ↔ 外部 tool server 之間的標準化協議」。它不在模型能力層、不在 sampling 層、是更高層的架構規範。</p>
<p>要理解 MCP 的定位、回顧 LLM 生態的歷史問題：</p>
<p>每個 LLM application（Cursor、Continue.dev、Claude Desktop、aider 等）要接每個 tool（檔案系統、資料庫、search、自訂 API），都得寫 adapter。N 個 application × M 個 tool 的整合成本是 N×M、生態擴張時成本爆炸。</p>
<p>MCP 把這個成本拆成兩段：</p>
<ul>
<li><strong>LLM application 端</strong>：實作 MCP client（一次）、之後支援任意 MCP server。</li>
<li><strong>Tool 端</strong>：實作 MCP server（一次）、之後被任意 MCP client 接到。</li>
</ul>
<p>整合成本從 N×M 降到 N+M。同樣的 ecosystem effect 跟模組零的 <a href="/blog/llm/00-foundations/openai-compatible-api/" data-link-title="0.3 OpenAI 相容 API" data-link-desc="為什麼幾乎所有本地 LLM 工具不用改就能切到本地：背後是同一套 API 形狀">OpenAI 相容 API</a> 一樣——標準化中介把生態整合複雜度從乘法降到加法。</p>
<p>MCP 涵蓋的「server 該提供什麼」包括：</p>
<ul>
<li>Tool 註冊（這個 server 提供哪些 tool）。</li>
<li>Tool schema（每個 tool 的參數定義）。</li>
<li>Tool 呼叫協議（呼叫方式 + 回應格式）。</li>
<li>Resource 暴露（檔案、文件等讀取資源）。</li>
<li>Prompt template 共享（reusable system prompt）。</li>
</ul>
<p>這些都在 protocol 層、模型怎麼用 tool（function calling 還是 structured output）不在 MCP 規範範圍——MCP 不管你模型強不強、它只管「tool 怎麼被暴露」。</p>
<h2 id="為什麼會出現-mcp">為什麼會出現 MCP</h2>
<p>MCP 是 LLM application 生態擴張到一定程度後的必然產物。觀察生態演化：</p>
<ul>
<li><strong>2023 早期</strong>：每個 LLM app 各自寫工具整合、Cursor 接 file system、Continue.dev 接 codebase、aider 接 git——各自的 adapter 邏輯互不通用。</li>
<li><strong>2024 中期</strong>：function calling spec 標準化（OpenAI 跟 Anthropic 各自定義）、解決「模型怎麼呼叫工具」、但「工具怎麼暴露給 application」還是各家自己處理。</li>
<li><strong>2024 底</strong>：Anthropic 提 MCP、把「工具暴露」也標準化、補完 ecosystem 拼圖。</li>
</ul>
<p>複用 OpenAI 相容 API 的成功模式：</p>
<ul>
<li><a href="/blog/llm/knowledge-cards/openai-compatible-api/" data-link-title="OpenAI 相容 API" data-link-desc="本地推論伺服器跟雲端 OpenAI 共用的 API 形狀標準">OpenAI 相容 API</a>：標準化「介面層 ↔ <a href="/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器</a>」、所有 IDE plugin 都接這個。</li>
<li>MCP：標準化「LLM application ↔ tool server」、所有 application 都接這個。</li>
</ul>
<p>兩者都採用同個策略：定義最小可用標準、讓生態繞著標準長、所有 player 受益。</p>
<p>MCP 成熟度判讀訊號（不固化在某一個時間點、用這幾個 signal 重新評估）：</p>
<ul>
<li><strong>Application 採納範圍</strong>：主要 LLM application（Claude Desktop、Cursor、Continue.dev、其他主流 IDE / chat 介面）是否原生支援。</li>
<li><strong>Tool server catalog 規模</strong>：社群維護的 MCP server 數量跟覆蓋範圍（檔案系統、git、Slack、雲端 API 等是否都有現成 server）。</li>
<li><strong>本地推論生態接入度</strong>：Ollama、LM Studio 等本地伺服器是否原生支援 MCP（或仍以 OpenAI 相容 API 為主）。</li>
<li><strong>跨平台一致性</strong>：Windows / macOS / Linux 上的 MCP server 行為是否一致、SDK 是否穩定。</li>
</ul>
<p>四個訊號全部成熟前、MCP 仍處於「主要 application 支援、本地生態剛開始接」的擴張期；訊號逐步達標後、預期會像 OpenAI 相容 API 一樣成為應用層的默認標準。</p>
<p>它跟 function calling 的關係：MCP 提供 tool 的暴露機制、模型怎麼呼叫這些 tool 仍走 function calling（如果模型支援）或 structured output（如果用約束）。三者疊加而非互斥。</p>
<h2 id="三者組合的實際工作流">三者組合的實際工作流</h2>
<p>一個完整 LLM application 的典型 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">使用者 prompt
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">LLM application（Claude Desktop / Cursor / 自家應用）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  ↓ (MCP client、列出所有可用 tool)
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">MCP server pool（檔案系統 server、git server、自家 API server...）
</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">LLM application 把 tool 描述塞進 prompt
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">推論伺服器（OpenAI API / Ollama / Anthropic API）
</span></span><span class="line"><span class="ln">10</span><span class="cl">  ↓ (function calling 訓練 + structured output 約束)
</span></span><span class="line"><span class="ln">11</span><span class="cl">模型輸出：「我要呼叫 tool X、參數是 Y」
</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">LLM application 用 MCP 把呼叫送到對應 server
</span></span><span class="line"><span class="ln">14</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln">15</span><span class="cl">Server 執行、回應
</span></span><span class="line"><span class="ln">16</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln">17</span><span class="cl">LLM application 把結果塞進 context、回到推論伺服器繼續</span></span></code></pre></div><p>三者各司其職：</p>
<ul>
<li><strong>Function calling</strong> 讓模型穩定輸出工具呼叫（訓練支撐）。</li>
<li><strong>Structured output</strong> 兜底保證呼叫格式合法（sampling 約束）。</li>
<li><strong>MCP</strong> 提供 tool ecosystem、application 不用為每個 tool 寫專屬 adapter（架構標準）。</li>
</ul>
<p>少了任一個都還能跑、但效率跟生態擴展性降一級：</p>
<ul>
<li>沒 function calling、靠 prompt + structured output、跨模型品質不穩。判讀訊號：同 prompt 在不同模型上 tool 呼叫格式錯誤率差 30% 以上。</li>
<li>沒 structured output、靠模型自律、偶有失敗。判讀訊號：&lt; 30B 模型在複雜 schema 下 JSON 合法率 &lt; 90%。</li>
<li>沒 MCP、每個 application 自己寫所有 tool 整合、ecosystem 不可規模化。判讀訊號：團隊維護 &gt; 5 個 tool adapter、每換 LLM provider 重寫一輪。</li>
</ul>
<h2 id="常見的組合誤用">常見的組合誤用</h2>
<p>三者組合在以下情境會失敗、是判讀「我的應用為何不穩」的常見候選：</p>
<ul>
<li><strong>Structured output 蓋過 function calling 訓練</strong>：模型訓練時用 Anthropic tools 格式、應用強制套 OpenAI function spec 的 grammar、模型輸出「合法但語意空洞」的 JSON（schema 對、欄位填湊數）。修法：用模型訓練過的 spec、避免在 grammar 層強制改寫。</li>
<li><strong>MCP server 在 prompt context 撐爆 tool 描述</strong>：MCP server 暴露幾十個 tool、每個都有 schema 跟 description、全塞進 system prompt 把 context budget 耗光。修法：dynamic tool selection（先讓 LLM 看「tool 摘要」選相關的、再把選中 tool 的詳細 schema 塞進 context）。</li>
<li><strong>Function calling + structured output 兩邊 schema 不一致</strong>：模型訓練的 function spec 跟 application 套的 JSON schema 欄位不對、模型輸出符合訓練 spec 但不符合 application schema、parser 失敗。修法：grammar 直接從 function spec 生、避免人工維護兩份。</li>
<li><strong>MCP server 沒做 input validation、prompt injection 通過 tool 結果污染 context</strong>：tool 回的內容沒檢查、惡意內容（如 PR 留言中的「請執行 rm -rf」）被模型當指令執行。修法：tool 輸出做 sanitization、可疑內容用 sandbox 標籤包起來、模型 prompt 明確區分「使用者指令」vs「tool 結果」。個人 dev 在自己機器上跑 MCP server 的權限模型（檔案系統 / shell / 網路存取邊界、第三方 MCP 信任）見 <a href="/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">6.2</a>；IDE 場景中 codebase / 外部文件 / 剪貼簿等 prompt injection 攻擊面見 <a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">6.3</a>。</li>
</ul>
<h2 id="何時可以只用一部分">何時可以只用一部分</h2>
<p>三者組合的需求視場景而定：</p>
<ul>
<li><strong>單純 structured 輸出</strong>（不呼叫工具）：只需 structured output、不需 function calling / MCP。例：把使用者輸入分類成 enum、輸出固定 schema 的 JSON。</li>
<li><strong>In-process tool</strong>（直接 Python function）：function calling + 簡單 dispatcher、不需 MCP。應用規模小時最直接。</li>
<li><strong>跨 application 共用 tool</strong>：才需要 MCP。如果你只寫自己用的 app、in-process 比 MCP 簡單。</li>
<li><strong>用較弱模型</strong>：可能只用 structured output、跳過 function calling。</li>
</ul>
<p>三者的「最小可用組合」視應用複雜度而定。早期應用通常從 function calling 開始、規模化後加 MCP、品質要求高時加 structured output 兜底——演化路徑不必一步到位。</p>
<h2 id="何時過時--何時不過時">何時過時 / 何時不過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>三個層級的分界（模型能力 / sampling 約束 / server 協議）。</li>
<li>N×M → N+M 的標準化收益、跟 OpenAI 相容 API 的對應。</li>
<li>三者疊加而非互斥的設計取捨。</li>
<li>「最小可用組合」的判讀框架。</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>MCP 是 2024-2025 才標準化的協議、未來 5 年可能演化或被新協議補充（協議層更新慢、但會更新）。</li>
<li>各家 function calling spec 的具體格式（OpenAI / Anthropic / 開放標準會持續細化）。</li>
<li>Structured output 的具體實作（grammar engines / JSON mode 會持續優化）。</li>
<li>哪些工具有 MCP server 可用（生態 catalog 會擴展）。</li>
</ul>
<p>看到新協議或新 spec 時、回到本章三層 framing 問：它解的是哪一層？能不能跟既有的另兩層組合？這個問題的答案能很快定位新東西在 stack 中的位置。</p>
<h2 id="下一章">下一章</h2>
<p>下一章：<a href="/blog/llm/04-applications/workflow-patterns/" data-link-title="4.7 Workflow 編排模式" data-link-desc="Pipeline / router / parallel / reflection：多 LLM call 組合的四種基本模式與退化條件">4.7 Workflow 編排模式</a>、把多 LLM call 組合的設計模式整理出來。</p>
]]></content:encoded></item><item><title>Hands-on：RAG / MCP 的資源 footprint</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-mcp-resources/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-mcp-resources/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/resource-management/" data-link-title="Hands-on：LLM 運行中 &amp;#43; 結束的資源管理" data-link-desc="RAM / 磁碟 / port 三個 dimension 的觀察跟釋放、Ollama keep_alive 跟 ComfyUI 兩種 lifecycle 對比、實測釋放數字">Resource management 章&lt;/a> 講的是 Ollama / ComfyUI 等&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器&lt;/a>的 lifecycle。但&lt;strong>跑 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP&lt;/a> 應用&lt;/strong>比單純 chat 多吃幾倍資源——&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/embedding-model/" data-link-title="Embedding Model" data-link-desc="把文字轉成向量的模型：用於 codebase 索引與語意搜尋">embedding model&lt;/a>、chat model、index 檔、subprocess、tool 邏輯——而且不同階段（ingest vs query）的瓶頸不一樣。&lt;/p>
&lt;p>本篇紀錄 &lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &amp;#43; cosine retrieval &amp;#43; Ollama chat、validating 4.0 RAG 原理">RAG demo&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/mcp-demo/" data-link-title="Hands-on：用 blog content 寫一個最小 MCP server" data-link-desc="stdio JSON-RPC、stdlib-only Python、暴露 blog content 給 LLM 用、validating 4.3 應用層協議">MCP demo&lt;/a> 跑起來的實測資源 footprint、提供本地多模型並存的 baseline、給寫 production 應用前的 sanity check。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>驗證日期&lt;/strong>：2026-05-12
&lt;strong>環境&lt;/strong>：M4 Pro 32 GB、Ollama 0.23.2、Python 3.14
&lt;strong>Corpus&lt;/strong>：本 blog 的 &lt;code>content/llm/&lt;/code>、71 個 markdown 檔、463 chunks&lt;/p>&lt;/blockquote>
&lt;h2 id="各階段資源-footprint">各階段資源 footprint&lt;/h2>
&lt;p>RAG / MCP 工作流通常分三階段、各自吃不同資源：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>主要資源消耗&lt;/th>
 &lt;th>持續時間&lt;/th>
 &lt;th>是否常駐&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>RAG ingest&lt;/strong>&lt;/td>
 &lt;td>embedding model RAM + CPU + 磁碟寫&lt;/td>
 &lt;td>one-shot（corpus 更動時跑）&lt;/td>
 &lt;td>否&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>RAG query&lt;/strong>&lt;/td>
 &lt;td>index 載入 RAM + chat model RAM + GPU&lt;/td>
 &lt;td>per-request&lt;/td>
 &lt;td>retrieval index 常駐&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>MCP server&lt;/strong>&lt;/td>
 &lt;td>subprocess 永久跑、tool 呼叫時動態載資源&lt;/td>
 &lt;td>session 內常駐&lt;/td>
 &lt;td>是&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不同階段的瓶頸不一樣、優化目標也不同。&lt;/p>
&lt;h2 id="rag-ingest-階段one-shot-但批次密集">RAG Ingest 階段：one-shot 但批次密集&lt;/h2>
&lt;p>跑 &lt;code>python3 scripts/rag-demo/ingest.py&lt;/code> 時：&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">Found 71 markdown files under content/llm
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> [10/71] 86 chunks in 4.5s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> [20/71] 181 chunks in 8.6s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> [70/71] 461 chunks in 22.2s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">Wrote 463 records to scripts/rag-demo/index.pkl (22.3s)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實測資源消耗：&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/llm/01-local-llm-services/hands-on/resource-management/" data-link-title="Hands-on：LLM 運行中 &#43; 結束的資源管理" data-link-desc="RAM / 磁碟 / port 三個 dimension 的觀察跟釋放、Ollama keep_alive 跟 ComfyUI 兩種 lifecycle 對比、實測釋放數字">Resource management 章</a> 講的是 Ollama / ComfyUI 等<a href="/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器</a>的 lifecycle。但<strong>跑 <a href="/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a> / <a href="/blog/llm/knowledge-cards/mcp/" data-link-title="MCP（Model Context Protocol）" data-link-desc="LLM application ↔ 外部 tool server 之間的標準化協議、複用 OpenAI 相容 API 的成功模式">MCP</a> 應用</strong>比單純 chat 多吃幾倍資源——<a href="/blog/llm/knowledge-cards/embedding-model/" data-link-title="Embedding Model" data-link-desc="把文字轉成向量的模型：用於 codebase 索引與語意搜尋">embedding model</a>、chat model、index 檔、subprocess、tool 邏輯——而且不同階段（ingest vs query）的瓶頸不一樣。</p>
<p>本篇紀錄 <a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">RAG demo</a> 跟 <a href="/blog/llm/01-local-llm-services/hands-on/mcp-demo/" data-link-title="Hands-on：用 blog content 寫一個最小 MCP server" data-link-desc="stdio JSON-RPC、stdlib-only Python、暴露 blog content 給 LLM 用、validating 4.3 應用層協議">MCP demo</a> 跑起來的實測資源 footprint、提供本地多模型並存的 baseline、給寫 production 應用前的 sanity check。</p>
<blockquote>
<p><strong>驗證日期</strong>：2026-05-12
<strong>環境</strong>：M4 Pro 32 GB、Ollama 0.23.2、Python 3.14
<strong>Corpus</strong>：本 blog 的 <code>content/llm/</code>、71 個 markdown 檔、463 chunks</p></blockquote>
<h2 id="各階段資源-footprint">各階段資源 footprint</h2>
<p>RAG / MCP 工作流通常分三階段、各自吃不同資源：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>主要資源消耗</th>
          <th>持續時間</th>
          <th>是否常駐</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>RAG ingest</strong></td>
          <td>embedding model RAM + CPU + 磁碟寫</td>
          <td>one-shot（corpus 更動時跑）</td>
          <td>否</td>
      </tr>
      <tr>
          <td><strong>RAG query</strong></td>
          <td>index 載入 RAM + chat model RAM + GPU</td>
          <td>per-request</td>
          <td>retrieval index 常駐</td>
      </tr>
      <tr>
          <td><strong>MCP server</strong></td>
          <td>subprocess 永久跑、tool 呼叫時動態載資源</td>
          <td>session 內常駐</td>
          <td>是</td>
      </tr>
  </tbody>
</table>
<p>不同階段的瓶頸不一樣、優化目標也不同。</p>
<h2 id="rag-ingest-階段one-shot-但批次密集">RAG Ingest 階段：one-shot 但批次密集</h2>
<p>跑 <code>python3 scripts/rag-demo/ingest.py</code> 時：</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">Found 71 markdown files under content/llm
</span></span><span class="line"><span class="ln">2</span><span class="cl">  [10/71] 86 chunks in 4.5s
</span></span><span class="line"><span class="ln">3</span><span class="cl">  [20/71] 181 chunks in 8.6s
</span></span><span class="line"><span class="ln">4</span><span class="cl">  ...
</span></span><span class="line"><span class="ln">5</span><span class="cl">  [70/71] 461 chunks in 22.2s
</span></span><span class="line"><span class="ln">6</span><span class="cl">Wrote 463 records to scripts/rag-demo/index.pkl (22.3s)</span></span></code></pre></div><p>實測資源消耗：</p>
<table>
  <thead>
      <tr>
          <th>資源</th>
          <th>數字</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RAM（峰值）</td>
          <td>~600 MB</td>
          <td>nomic-embed-text 模型 (274 MB) + Python runtime + 累積 records (~200 MB)</td>
      </tr>
      <tr>
          <td>磁碟寫</td>
          <td><code>index.pkl</code> ~3.7 MB</td>
          <td>463 records、每筆含 chunk text + 768-dim float embedding</td>
      </tr>
      <tr>
          <td>CPU + GPU</td>
          <td>Ollama 推 embedding、Apple Silicon Metal backend</td>
          <td>22 秒處理 463 個 chunk、平均 ~21 chunk/sec</td>
      </tr>
      <tr>
          <td>網路</td>
          <td>0</td>
          <td>完全本地推論</td>
      </tr>
  </tbody>
</table>
<p><strong>Ingest 階段的特性</strong>：</p>
<ul>
<li><strong>One-shot</strong>：corpus 不變不用重跑、index 寫一次永久用。</li>
<li><strong>吃 CPU 多於 RAM</strong>：產生 embedding 是 forward pass、瓶頸在 GPU 算力、RAM 沒太大壓力。</li>
<li><strong>磁碟寫小</strong>：每 chunk 約 8 KB（text 部分 ~5 KB + embedding 768 floats × 4 bytes = ~3 KB）、463 chunks 總共 ~3.7 MB。</li>
<li><strong>可平行</strong>：sequential <code>embed(chunk)</code> 是最慢實作、用 batching API（如果 Ollama 支援）或多 worker、能快 5-10x。</li>
</ul>
<p><strong>規模 extrapolation</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Corpus 大小</th>
          <th>預估 ingest 時間</th>
          <th>index.pkl 大小</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>71 docs / 463 chunks（本 blog）</td>
          <td>22 秒</td>
          <td>3.7 MB</td>
      </tr>
      <tr>
          <td>1000 docs / ~7000 chunks（中型 codebase）</td>
          <td>~5 分鐘</td>
          <td>~55 MB</td>
      </tr>
      <tr>
          <td>10000 docs / ~70000 chunks（大型 codebase）</td>
          <td>~50 分鐘</td>
          <td>~550 MB</td>
      </tr>
      <tr>
          <td>100K docs / ~700K chunks（公司 wiki）</td>
          <td>~8 小時</td>
          <td>~5.5 GB</td>
      </tr>
  </tbody>
</table>
<p>10K docs 以上就應該考慮：</p>
<ul>
<li><a href="/blog/llm/knowledge-cards/batching/" data-link-title="Batching" data-link-desc="多 request 一起跑、攤平 model load 成本：production LLM inference 的核心優化、決定 throughput vs latency 取捨">Batching</a> embedding（單次 request 送 50 個 chunks）</li>
<li>並行 worker（Python multiprocessing、4-8 worker）</li>
<li>換 <a href="/blog/llm/knowledge-cards/vector-database/" data-link-title="Vector Database" data-link-desc="為高維向量 (embedding) 設計的儲存 &#43; 近似最近鄰 (ANN) 檢索系統：RAG 從 prototype 跨到 production 的關鍵元件">vector database</a>（避免把全部資料用 pickle 塞 RAM）</li>
</ul>
<h2 id="rag-query-階段retrieval-加-generation">RAG Query 階段：retrieval 加 generation</h2>
<p>跑 <code>python3 scripts/rag-demo/query.py --show-retrieved &quot;問題&quot;</code> 時：</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">Loaded 463 chunks from scripts/rag-demo/index.pkl
</span></span><span class="line"><span class="ln">2</span><span class="cl">=== Retrieved chunks ===
</span></span><span class="line"><span class="ln">3</span><span class="cl">  0.870  llm/knowledge-cards/transformer.md#chunk2
</span></span><span class="line"><span class="ln">4</span><span class="cl">  ...
</span></span><span class="line"><span class="ln">5</span><span class="cl">（LLM 生成 response）</span></span></code></pre></div><p>實測資源消耗（單次 query）：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>RAM 增量</th>
          <th>時間</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>載 index.pkl 到 RAM</td>
          <td>3.7 MB（小 corpus）/ MB 級（大 corpus）</td>
          <td>&lt; 1 秒</td>
      </tr>
      <tr>
          <td>embed query</td>
          <td>0（已載入的 nomic-embed-text）</td>
          <td>200 ms</td>
      </tr>
      <tr>
          <td>cosine over 463 chunks</td>
          <td>純 Python 計算、暫時用 ~10 MB</td>
          <td>50 ms</td>
      </tr>
      <tr>
          <td>載 chat model（gemma3:1b）</td>
          <td>~1 GB（首次）/ 0（已 cached）</td>
          <td>5-10 秒（首次）/ 0（cached）</td>
      </tr>
      <tr>
          <td>生成 response</td>
          <td>0 額外</td>
          <td>5-30 秒（看 model + prompt 長度）</td>
      </tr>
  </tbody>
</table>
<p><strong>Query 階段的特性</strong>：</p>
<ul>
<li><strong>第一次 cold start</strong>：要載 chat model 進 RAM、5-10 秒首字延遲。</li>
<li><strong>後續 query 都快</strong>：embedding model + chat model 都在 RAM、retrieval 毫秒級、只剩 generation 時間。</li>
<li><strong>RAM 占用 = embedding model + chat model + index</strong>：
<ul>
<li>463 chunks: 274 MB + chat model + 3.7 MB ≈ chat model + 280 MB</li>
<li>100K chunks: 274 MB + chat model + ~800 MB 進 RAM、加上 mmap pickle 額外開銷</li>
</ul>
</li>
<li><strong>瓶頸是 chat model</strong>：retrieval 部分快、瓶頸完全在 generation。</li>
</ul>
<p><strong>多模型並存</strong>（embedding + chat）：</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"><span class="c1"># 看當前 RAM 占用</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ollama ps
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># NAME                       SIZE      UNTIL</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># nomic-embed-text:latest    274 MB    4 minutes from now</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># gemma3:4b                  5.5 GB    4 minutes from now</span></span></span></code></pre></div><p>兩個 model 都載入時、Ollama RAM 占用約 6 GB。Ollama 的 <code>OLLAMA_KEEP_ALIVE</code>（預設 5 分鐘）會 idle 後分別 unload 兩個 model。</p>
<p><strong>規模 sanity check</strong>：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>RAM 需求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純 chat（gemma3:1b）</td>
          <td>~1 GB</td>
      </tr>
      <tr>
          <td>RAG with gemma3:1b + nomic-embed-text + 小 index</td>
          <td>~1.5 GB</td>
      </tr>
      <tr>
          <td>RAG with gemma3:4b + nomic-embed-text + 中型 index</td>
          <td>~6 GB</td>
      </tr>
      <tr>
          <td>RAG with gemma4:31b + nomic-embed-text + 大 index</td>
          <td>~20 GB</td>
      </tr>
  </tbody>
</table>
<p>跑 RAG 比 chat 額外要 ~300-1000 MB（embedding model + index）、不會太重。</p>
<h2 id="mcp-server-階段subprocess-常駐">MCP Server 階段：subprocess 常駐</h2>
<p>跑 <code>python3 scripts/mcp-demo/test_client.py</code> 時、client 會 spawn <code>blog_mcp_server.py</code> 當 child process。</p>
<p>實測：</p>
<table>
  <thead>
      <tr>
          <th>資源</th>
          <th>數字</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Subprocess RAM</td>
          <td>~50 MB</td>
          <td>Python runtime + index.pkl mmap</td>
      </tr>
      <tr>
          <td>stdio pipe 數量</td>
          <td>3（stdin、stdout、stderr）</td>
          <td>每 spawn 一個 server 都要 3 FD</td>
      </tr>
      <tr>
          <td>持續時間</td>
          <td>client 在跑就在跑</td>
          <td>client 結束時 SIGPIPE 自動結束 server</td>
      </tr>
  </tbody>
</table>
<p><strong>MCP server 的特性</strong>：</p>
<ul>
<li><strong>每個 client spawn 一個 server</strong>：Claude Desktop 開 5 個 MCP server、就有 5 個 Python subprocess。</li>
<li><strong>Index lazy load</strong>：本 demo <code>load_index()</code> 第一次 call 才 read pickle、之後 cached。Cold start 第一次 tool call 稍慢。</li>
<li><strong>Process lifecycle 在 client 端</strong>：client 死了、stdin EOF、server 自然結束。Client 沒清乾淨 spawn 多次就 leak process。</li>
</ul>





<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="c1"># 看當前所有 MCP server</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ps aux <span class="p">|</span> grep blog_mcp_server <span class="p">|</span> grep -v grep
</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"><span class="c1"># 如果 client crash 留下 zombie：</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pkill -f <span class="s2">&#34;blog_mcp_server.py&#34;</span></span></span></code></pre></div><p><strong>多 MCP server 並存</strong>（如 Claude Desktop 接 git server + filesystem server + custom server）：</p>
<table>
  <thead>
      <tr>
          <th>Server</th>
          <th>RAM</th>
          <th>主要負載</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>git MCP server</td>
          <td>~30 MB</td>
          <td>shell 呼叫</td>
      </tr>
      <tr>
          <td>filesystem MCP server</td>
          <td>~30 MB</td>
          <td>fs 操作</td>
      </tr>
      <tr>
          <td>blog_mcp_server（本 demo）</td>
          <td>~50 MB（含 index）</td>
          <td>embedding + retrieval</td>
      </tr>
      <tr>
          <td>5 個 server 同時</td>
          <td>~200 MB</td>
          <td>累積</td>
      </tr>
  </tbody>
</table>
<p>200 MB 在 32 GB Mac 上不顯眼、但 16 GB Mac + 多 MCP server + 大 chat model 就可能擠到。</p>
<h2 id="rag--mcp-整合完整應用-stack">RAG + MCP 整合：完整應用 stack</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">User 在 Claude Desktop 打字
</span></span><span class="line"><span class="ln">2</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">Claude Desktop (~200 MB)
</span></span><span class="line"><span class="ln">4</span><span class="cl">  ↓ MCP stdio
</span></span><span class="line"><span class="ln">5</span><span class="cl">blog_mcp_server.py (~50 MB)
</span></span><span class="line"><span class="ln">6</span><span class="cl">  ↓ HTTP /api/embeddings + /v1/chat/completions
</span></span><span class="line"><span class="ln">7</span><span class="cl">Ollama daemon (~200 MB)
</span></span><span class="line"><span class="ln">8</span><span class="cl">  ↓ load
</span></span><span class="line"><span class="ln">9</span><span class="cl">nomic-embed-text 模型 (~274 MB) + 主 chat model (~6 GB)</span></span></code></pre></div><p>整體 RAM 占用範圍：</p>
<table>
  <thead>
      <tr>
          <th>配置</th>
          <th>估算</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Minimal（gemma3:1b + 小 index）</td>
          <td>~1.7 GB</td>
      </tr>
      <tr>
          <td>Standard（gemma3:4b + 中 index）</td>
          <td>~6.5 GB</td>
      </tr>
      <tr>
          <td>Heavy（gemma4:31b + 大 index + 多 MCP server）</td>
          <td>~22 GB</td>
      </tr>
  </tbody>
</table>
<p>跟 <a href="/blog/llm/01-local-llm-services/hands-on/resource-management/" data-link-title="Hands-on：LLM 運行中 &#43; 結束的資源管理" data-link-desc="RAM / 磁碟 / port 三個 dimension 的觀察跟釋放、Ollama keep_alive 跟 ComfyUI 兩種 lifecycle 對比、實測釋放數字">resource-management 章</a> 比、RAG / MCP 加 ~500 MB-1 GB overhead 在 chat 之上、是合理的 tradeoff（換來 retrieval + tool use 能力）。</p>
<h2 id="各資源類型的關鍵指標">各資源類型的關鍵指標</h2>
<p>整理三 dimension 的關鍵指標跟監控方式：</p>
<h3 id="ram">RAM</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="c1"># 看 Ollama 載了哪些 model</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ollama ps
</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"><span class="c1"># 看所有 LLM-related process</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">ps aux <span class="p">|</span> grep -E <span class="s2">&#34;ollama|comfyui|mcp&#34;</span> <span class="p">|</span> grep -v grep <span class="p">|</span> awk <span class="s1">&#39;{print $4, $11, $12, $13}&#39;</span> <span class="p">|</span> sort -rn
</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"><span class="c1"># 系統整體</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">vm_stat <span class="p">|</span> head -3</span></span></code></pre></div><p><strong>告警閾值</strong>：</p>
<ul>
<li>RAM 占用 &gt; 80% 系統總量：開始考慮 unload model 或關掉 ComfyUI</li>
<li>看到 swap 增加（<code>vm_stat | grep &quot;Swapouts&quot;</code>）：已經 swap、要立刻減少 model</li>
</ul>
<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="c1"># Ollama models 累積</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">du -sh ~/.ollama/models
</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"><span class="c1"># RAG index 累積（多個 corpus）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">du -sh scripts/rag-demo/index*.pkl 2&gt;/dev/null
</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"><span class="c1"># ComfyUI checkpoints / VAE / LoRA / etc</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">du -sh ~/Projects/ComfyUI/models/*</span></span></code></pre></div><p><strong>累積評估</strong>：</p>
<ul>
<li>Ollama: 每 model 1-20 GB、半年累積容易破 50 GB</li>
<li>RAG index: 每 100K chunks ~800 MB、多 corpus 累積要管</li>
<li>ComfyUI: 每 checkpoint 4-7 GB、加 LoRA / VAE / ControlNet 等可達 50+ GB</li>
</ul>
<h3 id="process--port">Process / Port</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="c1"># 一鍵 audit 所有 LLM service</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">for</span> p in <span class="m">11434</span> <span class="m">1234</span> <span class="m">8080</span> <span class="m">8188</span> 8000<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;=== port </span><span class="nv">$p</span><span class="s2"> ===&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  lsof -i :<span class="nv">$p</span> 2&gt;/dev/null <span class="p">|</span> head -2
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">done</span>
</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"><span class="c1"># 找 zombie subprocess（沒 parent 的 mcp server）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">ps aux <span class="p">|</span> grep <span class="s2">&#34;mcp_server&#34;</span> <span class="p">|</span> grep -v grep</span></span></code></pre></div><p><strong>告警訊號</strong>：</p>
<ul>
<li>同 port 兩個 process listen：明顯有 zombie、要 kill</li>
<li>多個 mcp_server PPID = 1（被 reparent 到 init）：原 client 死了沒清乾淨</li>
</ul>
<h2 id="rag-應用的長期累積管理">RAG 應用的長期累積管理</h2>
<p>跑超過幾週、會累積：</p>
<table>
  <thead>
      <tr>
          <th>累積物</th>
          <th>為什麼累積</th>
          <th>怎麼清</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multiple <code>index.pkl</code></td>
          <td>跑不同 corpus 各建 index、舊的沒刪</td>
          <td><code>find scripts -name 'index*.pkl' -mtime +30 -delete</code></td>
      </tr>
      <tr>
          <td>Ollama models</td>
          <td>試了不同 model 沒清</td>
          <td>看 <code>ollama list</code> modified 欄、<code>ollama rm</code> 不用的</td>
      </tr>
      <tr>
          <td>Python <code>__pycache__</code></td>
          <td>每次跑 script 累積</td>
          <td><code>.gitignore</code> 已包、本地 <code>find . -name __pycache__ -exec rm -rf {} +</code></td>
      </tr>
      <tr>
          <td>Embedding cache</td>
          <td>如果你寫了 embedding cache 機制</td>
          <td>各自清理策略</td>
      </tr>
  </tbody>
</table>
<p>清理 idiom：</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"><span class="c1"># 每月跑一次的 cleanup</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">llm-rag-cleanup<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;[*] Old indexes (&gt;30 days):&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  find scripts -name <span class="s1">&#39;index*.pkl&#39;</span> -mtime +30 -ls
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;[*] Ollama models (review):&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  ollama list
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;[*] Python caches:&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  find ~/Projects -name __pycache__ -type d <span class="p">|</span> head -10
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="o">}</span></span></span></code></pre></div><h2 id="跟-production-的差距預告">跟 production 的差距預告</h2>
<p>本篇紀錄的數字、是「single-user、single-machine、no concurrency」的 baseline。Production 場景多了幾個維度：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>本地</th>
          <th>Production</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>並發 user</td>
          <td>1</td>
          <td>10-10000</td>
      </tr>
      <tr>
          <td>Index 大小</td>
          <td>&lt; 100 MB</td>
          <td>TB 級</td>
      </tr>
      <tr>
          <td>Model serving</td>
          <td>Ollama 1 process</td>
          <td>vLLM / TGI / Triton 多 worker</td>
      </tr>
      <tr>
          <td>Vector storage</td>
          <td>pickle</td>
          <td>Pinecone / Weaviate / pgvector</td>
      </tr>
      <tr>
          <td>Latency 要求</td>
          <td>秒級 OK</td>
          <td>p50 &lt; 500ms、p99 &lt; 2s</td>
      </tr>
      <tr>
          <td>Cost model</td>
          <td>一次性硬體</td>
          <td>$/request、$/token</td>
      </tr>
      <tr>
          <td>Observability</td>
          <td>tail log</td>
          <td>metrics / traces / dashboards</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>crash → 自己重啟</td>
          <td>99.9% uptime SLA</td>
      </tr>
  </tbody>
</table>
<p>Production 視角詳細展開見 <a href="/blog/llm/04-applications/production-resource-planning/" data-link-title="4.9 Production 部署的資源評估原理" data-link-desc="從本地單 user 到 production multi-tenant：concurrent users、cost model、observability、SLA、capacity planning 的設計取捨">4.9 Production 部署的資源評估原理</a>。</p>
<h2 id="何時這篇會過時">何時這篇會過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>三階段 footprint 分類（ingest / query / server）</li>
<li>RAM / 磁碟 / process 三 dimension 的監控指令</li>
<li>多模型並存的 RAM 預估方法</li>
<li>長期累積管理 idiom</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體 RAM / 磁碟數字（隨模型架構、量化方法演化）</li>
<li><code>OLLAMA_KEEP_ALIVE</code> 等具體環境變數名</li>
<li>哪些 vector DB 主流（會持續演化）</li>
</ul>
<p>讀的時候若 RAM 占用跟本篇對不上、可能是新 model 架構效率改變、用同樣方法量自己環境的 baseline 即可。</p>
<p>跟其他 hands-on 章節的關係：完整 hands-on 系列見 <a href="/blog/llm/01-local-llm-services/hands-on/" data-link-title="Hands-on：本地 AI 工具實作筆記" data-link-desc="Ollama / ComfyUI / Whisper / Piper TTS：實際安裝、驗證、跑通的紀錄。隨工具版本演化、跟 1.x 原理章節互補。">Hands-on 章節索引</a>、實作配對見 <a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">RAG demo</a> 跟 <a href="/blog/llm/01-local-llm-services/hands-on/mcp-demo/" data-link-title="Hands-on：用 blog content 寫一個最小 MCP server" data-link-desc="stdio JSON-RPC、stdlib-only Python、暴露 blog content 給 LLM 用、validating 4.3 應用層協議">MCP demo</a>、Ollama / ComfyUI 共用的 lifecycle 管理見 <a href="/blog/llm/01-local-llm-services/hands-on/resource-management/" data-link-title="Hands-on：LLM 運行中 &#43; 結束的資源管理" data-link-desc="RAM / 磁碟 / port 三個 dimension 的觀察跟釋放、Ollama keep_alive 跟 ComfyUI 兩種 lifecycle 對比、實測釋放數字">Resource management</a>、Apple Silicon 統一記憶體預算原理見 <a href="/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">0.5 記憶體預算</a>。</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"><span class="c1"># 1. RAG ingest 階段 RAM 量</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ollama ps  <span class="c1"># 先看 baseline</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">python3 scripts/rag-demo/ingest.py <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">INGEST_PID</span><span class="o">=</span><span class="nv">$!</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ollama ps  <span class="c1"># 看 embedding model 載入後</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">vm_stat <span class="p">|</span> head -3
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">wait</span> <span class="nv">$INGEST_PID</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 2. RAG query 階段 RAM 量</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">ollama ps  <span class="c1"># 看 idle 後 unload</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">python3 scripts/rag-demo/query.py --show-retrieved <span class="s2">&#34;test query&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">ollama ps  <span class="c1"># 看 chat model 載入</span>
</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"><span class="c1"># 3. MCP server 階段 process / RAM</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">python3 scripts/mcp-demo/test_client.py <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nv">CLIENT_PID</span><span class="o">=</span><span class="nv">$!</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">sleep <span class="m">2</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">ps aux <span class="p">|</span> grep blog_mcp_server <span class="p">|</span> grep -v grep
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nb">wait</span> <span class="nv">$CLIENT_PID</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># 4. 完成釋放</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">ollama list <span class="p">|</span> tail -n +2 <span class="p">|</span> awk <span class="s1">&#39;{print $1}&#39;</span> <span class="p">|</span> xargs -I <span class="o">{}</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="se"></span>  curl -s http://localhost:11434/api/generate -d <span class="s2">&#34;{\&#34;model\&#34;:\&#34;{}\&#34;,\&#34;keep_alive\&#34;:0}&#34;</span></span></span></code></pre></div>]]></content:encoded></item><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></channel></rss>