<?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>Ollama on Tarragon</title><link>https://tarrragon.github.io/blog/tags/ollama/</link><description>Recent content in Ollama on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 12 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/ollama/index.xml" rel="self" type="application/rss+xml"/><item><title>模組一：本地 LLM 服務的安裝與應用</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/</guid><description>&lt;p>本模組的核心目標是把 &lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/" data-link-title="模組零：基礎知識與心智模型" data-link-desc="建立本地 LLM 的心智模型、釐清 MLX / MTP / oMLX 等常被混淆的術語、Apple Silicon 記憶體現實">模組零&lt;/a> 的心智模型落地到實際安裝步驟與工作流。網路上多數本地 LLM 教學是「列三個工具裝法」，缺乏選型脈絡與期望管理；本模組會先回答「為什麼選這個」，再給「怎麼裝」與「裝完之後該調哪些設定」。&lt;/p>
&lt;p>讀完本模組後，你應該能在自己的 Mac 上裝好一個本地 LLM 工作流，並知道它的能力邊界、什麼時候該切回雲端。&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/01-local-llm-services/ollama/" data-link-title="1.0 Ollama：主流推論伺服器" data-link-desc="一行 brew 裝完、ollama run 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on localhost:11434">1.0&lt;/a>&lt;/td>
 &lt;td>Ollama：主流推論伺服器&lt;/td>
 &lt;td>一行 brew 裝完、&lt;code>ollama run&lt;/code> 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on 11434&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/lm-studio/" data-link-title="1.1 LM Studio：GUI 探索模型" data-link-desc="GUI 取向的本地推論伺服器：內建模型瀏覽器、speculative decoding 設定面板、適合探索新模型">1.1&lt;/a>&lt;/td>
 &lt;td>LM Studio：GUI 探索模型&lt;/td>
 &lt;td>內建模型瀏覽器、speculative decoding 設定面板、適合探索新模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/llama-cpp/" data-link-title="1.2 llama.cpp：底層推論引擎" data-link-desc="GGUF 格式、量化、MTP 仍 beta；多數讀者不需要直接接觸，Ollama 已經包好">1.2&lt;/a>&lt;/td>
 &lt;td>llama.cpp：底層引擎&lt;/td>
 &lt;td>直接面對 GGUF 與量化選項、MTP 仍 beta、需要進階設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/vscode-continue-integration/" data-link-title="1.3 VS Code &amp;#43; Continue.dev 整合" data-link-desc="安裝 Continue 擴充套件、config.json 設定、Cmd&amp;#43;L 對話 / Cmd&amp;#43;I 行內編輯快捷鍵">1.3&lt;/a>&lt;/td>
 &lt;td>VS Code + Continue.dev 整合&lt;/td>
 &lt;td>安裝擴充套件、config.json 設定、Cmd+L / Cmd+I 快捷鍵&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">1.4&lt;/a>&lt;/td>
 &lt;td>寫 code 場景的模型選型優先順序&lt;/td>
 &lt;td>Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨理由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/expectation-management/" data-link-title="1.5 期望管理：本地 LLM 的擅長領域與分工" data-link-desc="本地 LLM 是免費的初階 pair programmer：辨識它的擅長領域、跟雲端旗艦做結構性分工">1.5&lt;/a>&lt;/td>
 &lt;td>期望管理：本地 LLM 的擅長領域與分工&lt;/td>
 &lt;td>本地是免費的初階 pair programmer，不是 Claude 替代品；混用是現階段正解&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/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&lt;/a>&lt;/td>
 &lt;td>延伸方向：Web UI、coding agent、產圖&lt;/td>
 &lt;td>先把寫 code 跑穩，再評估 Open WebUI、aider 等延伸；產圖另闢戰場&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">1.7&lt;/a>&lt;/td>
 &lt;td>排錯方法論：用三層架構做故障定位&lt;/td>
 &lt;td>先定位哪一層壞、log 角色差異、最小可重現、跨層級誤判模式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/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&lt;/a>&lt;/td>
 &lt;td>實作筆記：Ollama / ComfyUI / Whisper / Piper TTS / RAG / MCP&lt;/td>
 &lt;td>實際安裝指令、驗證流程、跟 1.x 原理章節互補的當下快照&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="推論伺服器選型總表">推論伺服器選型總表&lt;/h2>
&lt;p>模組零已建立的三層架構視角告訴你 Ollama、LM Studio、llama.cpp 都屬於&lt;strong>伺服器層&lt;/strong>。本模組要回答的是這三者的具體差異：&lt;/p></description><content:encoded><![CDATA[<p>本模組的核心目標是把 <a href="/blog/llm/00-foundations/" data-link-title="模組零：基礎知識與心智模型" data-link-desc="建立本地 LLM 的心智模型、釐清 MLX / MTP / oMLX 等常被混淆的術語、Apple Silicon 記憶體現實">模組零</a> 的心智模型落地到實際安裝步驟與工作流。網路上多數本地 LLM 教學是「列三個工具裝法」，缺乏選型脈絡與期望管理；本模組會先回答「為什麼選這個」，再給「怎麼裝」與「裝完之後該調哪些設定」。</p>
<p>讀完本模組後，你應該能在自己的 Mac 上裝好一個本地 LLM 工作流，並知道它的能力邊界、什麼時候該切回雲端。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/ollama/" data-link-title="1.0 Ollama：主流推論伺服器" data-link-desc="一行 brew 裝完、ollama run 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on localhost:11434">1.0</a></td>
          <td>Ollama：主流推論伺服器</td>
          <td>一行 brew 裝完、<code>ollama run</code> 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on 11434</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/lm-studio/" data-link-title="1.1 LM Studio：GUI 探索模型" data-link-desc="GUI 取向的本地推論伺服器：內建模型瀏覽器、speculative decoding 設定面板、適合探索新模型">1.1</a></td>
          <td>LM Studio：GUI 探索模型</td>
          <td>內建模型瀏覽器、speculative decoding 設定面板、適合探索新模型</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/llama-cpp/" data-link-title="1.2 llama.cpp：底層推論引擎" data-link-desc="GGUF 格式、量化、MTP 仍 beta；多數讀者不需要直接接觸，Ollama 已經包好">1.2</a></td>
          <td>llama.cpp：底層引擎</td>
          <td>直接面對 GGUF 與量化選項、MTP 仍 beta、需要進階設定</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/vscode-continue-integration/" data-link-title="1.3 VS Code &#43; Continue.dev 整合" data-link-desc="安裝 Continue 擴充套件、config.json 設定、Cmd&#43;L 對話 / Cmd&#43;I 行內編輯快捷鍵">1.3</a></td>
          <td>VS Code + Continue.dev 整合</td>
          <td>安裝擴充套件、config.json 設定、Cmd+L / Cmd+I 快捷鍵</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">1.4</a></td>
          <td>寫 code 場景的模型選型優先順序</td>
          <td>Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨理由</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/expectation-management/" data-link-title="1.5 期望管理：本地 LLM 的擅長領域與分工" data-link-desc="本地 LLM 是免費的初階 pair programmer：辨識它的擅長領域、跟雲端旗艦做結構性分工">1.5</a></td>
          <td>期望管理：本地 LLM 的擅長領域與分工</td>
          <td>本地是免費的初階 pair programmer，不是 Claude 替代品；混用是現階段正解</td>
      </tr>
      <tr>
          <td><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></td>
          <td>延伸方向：Web UI、coding agent、產圖</td>
          <td>先把寫 code 跑穩，再評估 Open WebUI、aider 等延伸；產圖另闢戰場</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">1.7</a></td>
          <td>排錯方法論：用三層架構做故障定位</td>
          <td>先定位哪一層壞、log 角色差異、最小可重現、跨層級誤判模式</td>
      </tr>
      <tr>
          <td><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></td>
          <td>實作筆記：Ollama / ComfyUI / Whisper / Piper TTS / RAG / MCP</td>
          <td>實際安裝指令、驗證流程、跟 1.x 原理章節互補的當下快照</td>
      </tr>
  </tbody>
</table>
<h2 id="推論伺服器選型總表">推論伺服器選型總表</h2>
<p>模組零已建立的三層架構視角告訴你 Ollama、LM Studio、llama.cpp 都屬於<strong>伺服器層</strong>。本模組要回答的是這三者的具體差異：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Ollama</th>
          <th>LM Studio</th>
          <th>llama.cpp</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>介面</td>
          <td>CLI + REST API</td>
          <td>GUI + REST API</td>
          <td>CLI only（server 子命令需自編譯）</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>低（一行裝完）</td>
          <td>低（一鍵安裝）</td>
          <td>中高（編譯、量化、參數要自己選）</td>
      </tr>
      <tr>
          <td>模型瀏覽器</td>
          <td>命令列 <code>ollama list</code>，registry 在網頁</td>
          <td>GUI 內建，直接搜尋下載</td>
          <td>沒有，要自己去 Hugging Face 下載</td>
      </tr>
      <tr>
          <td>Gemma 4 MTP（2026/5）</td>
          <td>v0.23.1 內建</td>
          <td>支援，要在 UI 開啟 speculative</td>
          <td>仍 beta，drafter 整合是 feature request</td>
      </tr>
      <tr>
          <td>適合誰</td>
          <td>多數工程師、想快速開始</td>
          <td>GUI 派、探索模型階段</td>
          <td>進階使用者、研究、特殊量化</td>
      </tr>
      <tr>
          <td>同台共存</td>
          <td>可以，預設 port 11434</td>
          <td>可以，預設 port 1234</td>
          <td>可以，預設 port 8080</td>
      </tr>
  </tbody>
</table>
<p>讀完本表後的決策建議是：<strong>先裝 Ollama，跑穩後再評估其他</strong>。LM Studio 可以同時裝來探索模型，但日常主力建議 Ollama；llama.cpp 暫時不需要直接接觸（Ollama 內部已經用 llama.cpp）。</p>
<h2 id="為什麼這個順序">為什麼這個順序</h2>
<p>本模組章節順序的設計脈絡：</p>
<ol>
<li><strong>先 1.0 Ollama</strong>：學習曲線最低、生態最成熟、Gemma 4 MTP 一鍵支援。多數讀者裝完這個就能開始用。</li>
<li><strong>再 1.1 LM Studio</strong>：給「想要可視化探索」的讀者另一條路；也可以跟 Ollama 並存。</li>
<li><strong>接 1.2 llama.cpp</strong>：澄清網路上「llama.cpp 才是真本地」的迷思，給進階讀者完整背景。</li>
<li><strong>再 1.3 VS Code + Continue.dev</strong>：把伺服器接到日常工作環境，這才是寫 code 的真正起點。</li>
<li><strong>然後 1.4 模型選型</strong>：伺服器跑起來後該裝哪個模型，給優先順序。</li>
<li><strong>再 1.5 期望管理</strong>：用一週後該怎麼判斷「值不值得繼續用」「什麼時候切雲端」。</li>
<li><strong>最後 1.6 延伸方向</strong>：日常路徑穩了再玩 Web UI、coding agent、產圖。</li>
</ol>
<p>每一章可以單獨讀，但若你是第一次接觸本地 LLM，照順序讀最不容易迷路。</p>
<h2 id="一個小時的最短路徑">一個小時的最短路徑</h2>
<p>如果你沒時間讀完整本模組、只想用一小時搞定本地 LLM 寫 code 的最基本工作流，下面是最短路徑：</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"># 1. 裝 Ollama（5 分鐘）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">brew install ollama
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">ollama serve <span class="p">&amp;</span>
</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"><span class="c1"># 2. 拉模型（首次下載約 20 ~ 30 分鐘，看網速）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">ollama run gemma4:31b-coding-mtp-bf16
</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 class="c1"># 3. 在 VS Code 裝 Continue 擴充套件（2 分鐘）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 4. 設定 ~/.continue/config.json（5 分鐘）</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 5. 試用 Cmd+L（對話）、Cmd+I（行內編輯）（剩下時間）</span></span></span></code></pre></div><p>需要 32GB+ Mac 才能流暢跑這個 model；16GB / 24GB 請改用 <a href="/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">1.4 模型選型</a> 的對照表選對應大小的模型。完整步驟在 <a href="/blog/llm/01-local-llm-services/ollama/" data-link-title="1.0 Ollama：主流推論伺服器" data-link-desc="一行 brew 裝完、ollama run 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on localhost:11434">1.0 Ollama</a> 跟 <a href="/blog/llm/01-local-llm-services/vscode-continue-integration/" data-link-title="1.3 VS Code &#43; Continue.dev 整合" data-link-desc="安裝 Continue 擴充套件、config.json 設定、Cmd&#43;L 對話 / Cmd&#43;I 行內編輯快捷鍵">1.3 VS Code + Continue.dev</a>。</p>
<h2 id="跑穩之後該做什麼">跑穩之後該做什麼</h2>
<p>裝完不是終點。本地 LLM 跟雲端的差別在於「需要持續調教」。跑穩後建議的後續工作：</p>
<ol>
<li><strong>用一週實測</strong>：把日常工作流真實餵進去、記錄通過率與痛點、用真實任務當判讀依據而非示範任務。</li>
<li><strong>建立切換習慣</strong>：明確哪些任務交給本地、哪些切雲端。詳見 <a href="/blog/llm/01-local-llm-services/expectation-management/" data-link-title="1.5 期望管理：本地 LLM 的擅長領域與分工" data-link-desc="本地 LLM 是免費的初階 pair programmer：辨識它的擅長領域、跟雲端旗艦做結構性分工">1.5 期望管理</a>。</li>
<li><strong>觀察記憶體與發熱</strong>：開 Activity Monitor 看記憶體 swap 狀態、機殼溫度是否過高。</li>
<li><strong>追新模型</strong>：本地模型發布速度很快、每 2 ~ 3 個月會有新候選、值得追蹤。</li>
<li><strong>判斷是否升級硬體</strong>：用一個月後若限制都來自記憶體、再評估升級 Mac；先確認痛點再投資硬體。</li>
</ol>
<h2 id="不在本模組內的主題">不在本模組內的主題</h2>
<p>本模組不討論：</p>
<ol>
<li>訓練、fine-tuning、LoRA 微調 — 跟「跑現成模型」是不同的工程問題。</li>
<li>部署到雲端 GPU、Linux server — 本指南範圍只在 Apple Silicon Mac。</li>
<li>Cursor、Windsurf、Cline 等其他 IDE 整合 — Continue.dev 是與本地 LLM 整合最成熟的選擇，其他工具的整合度視版本而定。</li>
<li>詳細的 benchmark 跑分方法 — 本指南只引用官方數據，自己跑分屬於另一個工程主題。</li>
</ol>
<p>需要這些主題時請另尋專門資源；硬塞進來只會讓「Mac 本地寫 code」這條最短路徑被淹沒。</p>
]]></content:encoded></item><item><title>Hands-on：用 blog content 當 corpus 跑 RAG</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-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/rag-demo/</guid><description>&lt;p>本篇把 &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 RAG 原理&lt;/a> 的概念落到一個能跑的最小實作：用本 blog 的 &lt;code>content/llm/&lt;/code> 當 corpus、Ollama 的 &lt;code>nomic-embed-text&lt;/code> 做 embedding、&lt;code>gemma3:1b&lt;/code> 做生成、兩個 Python 檔案完成 ingest + query 整條鏈。實作刻意保持 minimal、為的是把每一段都看清楚、跟原理對應。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>驗證日期&lt;/strong>：2026-05-12
&lt;strong>環境&lt;/strong>：macOS、Ollama 0.23.2、&lt;code>nomic-embed-text&lt;/code>、&lt;code>gemma3:1b&lt;/code>
&lt;strong>Corpus&lt;/strong>：本 blog 的 &lt;code>content/llm/&lt;/code>、71 個 markdown 檔
&lt;strong>結果&lt;/strong>：22 秒索引 463 個 chunk、retrieval 命中率好、generation 受 1B 模型能力限制——剛好示範「retrieval 跟 generation 各自會失敗」的兩段式失敗模式&lt;/p>&lt;/blockquote>
&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;/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>Embedding 模型&lt;/td>
 &lt;td>&lt;code>ollama pull nomic-embed-text&lt;/code>（274 MB、768 維）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Chat 模型&lt;/td>
 &lt;td>&lt;code>ollama pull gemma3:1b&lt;/code>（815 MB）。能力弱但夠驗證流程；上 31B 級才能拿到「真正能用」的 answer 品質&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Python&lt;/td>
 &lt;td>3.11+（標準 lib &lt;code>urllib&lt;/code> / &lt;code>pickle&lt;/code> 即可、不需要外部依賴）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="驗證-embedding-api-可用">驗證 embedding API 可用&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">curl -s http://localhost:11434/api/embeddings &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;nomic-embed-text&amp;#34;,&amp;#34;prompt&amp;#34;:&amp;#34;hello world&amp;#34;}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> python3 -c &lt;span class="s2">&amp;#34;import json,sys; r=json.load(sys.stdin); print(&amp;#39;dim:&amp;#39;, len(r[&amp;#39;embedding&amp;#39;]))&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>逐項說明：&lt;/p>
&lt;ul>
&lt;li>&lt;code>curl -s&lt;/code>：&lt;code>-s&lt;/code> 是 silent 模式、不顯示下載進度條（不然會混進 stdout、後面 python parse 會炸）。&lt;/li>
&lt;li>&lt;code>http://localhost:11434/api/embeddings&lt;/code>：用 Ollama &lt;strong>原生&lt;/strong> embedding endpoint。也有 &lt;code>/v1/embeddings&lt;/code>（OpenAI 相容）、但原生回應結構較簡（直接 &lt;code>{&amp;quot;embedding&amp;quot;: [...]}&lt;/code>、不是 OpenAI 那種 &lt;code>{&amp;quot;data&amp;quot;: [{&amp;quot;embedding&amp;quot;: [...]}]}&lt;/code> 巢狀）。本 demo 用原生、parse 更直接。&lt;/li>
&lt;li>&lt;code>-d '{&amp;quot;model&amp;quot;:&amp;quot;...&amp;quot;,&amp;quot;prompt&amp;quot;:&amp;quot;...&amp;quot;}'&lt;/code>：JSON payload。&lt;code>model&lt;/code> 是 Ollama tag、&lt;code>prompt&lt;/code> 是要 embed 的文字。&lt;/li>
&lt;li>&lt;code>python3 -c &amp;quot;...&amp;quot;&lt;/code>：stdin 接 curl 輸出、parse JSON、印 embedding 長度。&lt;/li>
&lt;li>&lt;strong>為什麼測 &lt;code>dim: 768&lt;/code>&lt;/strong>：&lt;code>nomic-embed-text&lt;/code> 模型架構決定 embedding 維度是 768。每次 embed 任何文字都會回固定 768 維向量、是 retrieval 的基本資料形狀。看到 &lt;code>dim: 768&lt;/code> 表示：API 通了、模型載入了、輸出形狀對。&lt;/li>
&lt;/ul>
&lt;h2 id="設計取捨">設計取捨&lt;/h2>
&lt;p>實作前先對齊 &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 RAG 原理&lt;/a> 提的設計取捨、決定每段怎麼做：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>取捨點&lt;/th>
 &lt;th>本 demo 的選擇&lt;/th>
 &lt;th>Trade-off&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Chunking 粒度&lt;/td>
 &lt;td>段落感知 + 軟 token cap（~400 token）&lt;/td>
 &lt;td>簡單、保留段落邊界；不做語意 chunking&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Embedding 模型&lt;/td>
 &lt;td>&lt;code>nomic-embed-text&lt;/code>（768 維）&lt;/td>
 &lt;td>主流、Ollama 內建、英文為主；中文混合場景仍可運作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>向量儲存&lt;/td>
 &lt;td>Python pickle 檔&lt;/td>
 &lt;td>463 chunks 用 in-memory 完全夠；何時該換見 &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 RAG storage 工程&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retrieval&lt;/td>
 &lt;td>Cosine similarity、top-K&lt;/td>
 &lt;td>無 hybrid、無 re-ranker；夠驗證、品質受 embedding 限制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Generation&lt;/td>
 &lt;td>&lt;code>gemma3:1b&lt;/code> 純 Ollama OpenAI 相容 API&lt;/td>
 &lt;td>1B 模型能力弱、會編造；用來示範 retrieval 跟 generation 兩段分離&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些選擇都對應到 4.0 章節的「會變的部分」清單——可預期半年後 embedding 模型有新選擇、chunking 有更好策略、re-ranker 變主流。但骨架（retrieval + augmentation 兩段式）不變。&lt;/p></description><content:encoded><![CDATA[<p>本篇把 <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 RAG 原理</a> 的概念落到一個能跑的最小實作：用本 blog 的 <code>content/llm/</code> 當 corpus、Ollama 的 <code>nomic-embed-text</code> 做 embedding、<code>gemma3:1b</code> 做生成、兩個 Python 檔案完成 ingest + query 整條鏈。實作刻意保持 minimal、為的是把每一段都看清楚、跟原理對應。</p>
<blockquote>
<p><strong>驗證日期</strong>：2026-05-12
<strong>環境</strong>：macOS、Ollama 0.23.2、<code>nomic-embed-text</code>、<code>gemma3:1b</code>
<strong>Corpus</strong>：本 blog 的 <code>content/llm/</code>、71 個 markdown 檔
<strong>結果</strong>：22 秒索引 463 個 chunk、retrieval 命中率好、generation 受 1B 模型能力限制——剛好示範「retrieval 跟 generation 各自會失敗」的兩段式失敗模式</p></blockquote>
<h2 id="前置設定">前置設定</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>來源 / 指令</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ollama 跑著</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>Embedding 模型</td>
          <td><code>ollama pull nomic-embed-text</code>（274 MB、768 維）</td>
      </tr>
      <tr>
          <td>Chat 模型</td>
          <td><code>ollama pull gemma3:1b</code>（815 MB）。能力弱但夠驗證流程；上 31B 級才能拿到「真正能用」的 answer 品質</td>
      </tr>
      <tr>
          <td>Python</td>
          <td>3.11+（標準 lib <code>urllib</code> / <code>pickle</code> 即可、不需要外部依賴）</td>
      </tr>
  </tbody>
</table>
<h3 id="驗證-embedding-api-可用">驗證 embedding API 可用</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">curl -s http://localhost:11434/api/embeddings <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -d <span class="s1">&#39;{&#34;model&#34;:&#34;nomic-embed-text&#34;,&#34;prompt&#34;:&#34;hello world&#34;}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  <span class="p">|</span> python3 -c <span class="s2">&#34;import json,sys; r=json.load(sys.stdin); print(&#39;dim:&#39;, len(r[&#39;embedding&#39;]))&#34;</span></span></span></code></pre></div><p>逐項說明：</p>
<ul>
<li><code>curl -s</code>：<code>-s</code> 是 silent 模式、不顯示下載進度條（不然會混進 stdout、後面 python parse 會炸）。</li>
<li><code>http://localhost:11434/api/embeddings</code>：用 Ollama <strong>原生</strong> embedding endpoint。也有 <code>/v1/embeddings</code>（OpenAI 相容）、但原生回應結構較簡（直接 <code>{&quot;embedding&quot;: [...]}</code>、不是 OpenAI 那種 <code>{&quot;data&quot;: [{&quot;embedding&quot;: [...]}]}</code> 巢狀）。本 demo 用原生、parse 更直接。</li>
<li><code>-d '{&quot;model&quot;:&quot;...&quot;,&quot;prompt&quot;:&quot;...&quot;}'</code>：JSON payload。<code>model</code> 是 Ollama tag、<code>prompt</code> 是要 embed 的文字。</li>
<li><code>python3 -c &quot;...&quot;</code>：stdin 接 curl 輸出、parse JSON、印 embedding 長度。</li>
<li><strong>為什麼測 <code>dim: 768</code></strong>：<code>nomic-embed-text</code> 模型架構決定 embedding 維度是 768。每次 embed 任何文字都會回固定 768 維向量、是 retrieval 的基本資料形狀。看到 <code>dim: 768</code> 表示：API 通了、模型載入了、輸出形狀對。</li>
</ul>
<h2 id="設計取捨">設計取捨</h2>
<p>實作前先對齊 <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 RAG 原理</a> 提的設計取捨、決定每段怎麼做：</p>
<table>
  <thead>
      <tr>
          <th>取捨點</th>
          <th>本 demo 的選擇</th>
          <th>Trade-off</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Chunking 粒度</td>
          <td>段落感知 + 軟 token cap（~400 token）</td>
          <td>簡單、保留段落邊界；不做語意 chunking</td>
      </tr>
      <tr>
          <td>Embedding 模型</td>
          <td><code>nomic-embed-text</code>（768 維）</td>
          <td>主流、Ollama 內建、英文為主；中文混合場景仍可運作</td>
      </tr>
      <tr>
          <td>向量儲存</td>
          <td>Python pickle 檔</td>
          <td>463 chunks 用 in-memory 完全夠；何時該換見 <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 RAG storage 工程</a></td>
      </tr>
      <tr>
          <td>Retrieval</td>
          <td>Cosine similarity、top-K</td>
          <td>無 hybrid、無 re-ranker；夠驗證、品質受 embedding 限制</td>
      </tr>
      <tr>
          <td>Generation</td>
          <td><code>gemma3:1b</code> 純 Ollama OpenAI 相容 API</td>
          <td>1B 模型能力弱、會編造；用來示範 retrieval 跟 generation 兩段分離</td>
      </tr>
  </tbody>
</table>
<p>這些選擇都對應到 4.0 章節的「會變的部分」清單——可預期半年後 embedding 模型有新選擇、chunking 有更好策略、re-ranker 變主流。但骨架（retrieval + augmentation 兩段式）不變。</p>
<h2 id="ingest把-corpus-變索引">Ingest：把 corpus 變索引</h2>
<p>完整檔案：<code>scripts/rag-demo/ingest.py</code>（本 repo 下）。三段 function：切 chunk、embed、走訪 + 持久化。</p>
<h3 id="1-slice_markdown段落感知的-chunk-切割">1. <code>slice_markdown</code>：段落感知的 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">slice_markdown</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">soft_token_cap</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">400</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">paragraphs</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">re</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\n\s*\n&#34;</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">strip</span><span class="p">()]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">chunks</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">buf</span><span class="p">,</span> <span class="n">buf_len</span> <span class="o">=</span> <span class="p">[],</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">paragraphs</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">plen</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span>  <span class="c1"># char-count / 2 ≈ token (CJK + English heuristic)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">if</span> <span class="n">buf</span> <span class="ow">and</span> <span class="n">buf_len</span> <span class="o">+</span> <span class="n">plen</span> <span class="o">&gt;</span> <span class="n">soft_token_cap</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="n">chunks</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">buf</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="n">buf</span><span class="p">,</span> <span class="n">buf_len</span> <span class="o">=</span> <span class="p">[],</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">buf</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="n">buf_len</span> <span class="o">+=</span> <span class="n">plen</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="n">buf</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">chunks</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">buf</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">return</span> <span class="n">chunks</span></span></span></code></pre></div><p><strong>每段做什麼</strong>：</p>
<ol>
<li><strong><code>re.split(r&quot;\n\s*\n&quot;, text)</code></strong>：用「空白行」當分隔符切段落。<code>\n\s*\n</code> 比 <code>\n\n</code> 寬一點、允許中間有 whitespace（空白、tab）。Markdown 段落的標準分隔是空白行、這個 regex 捕捉所有段落邊界。</li>
<li><strong><code>[p.strip() for ... if p.strip()]</code></strong>：每段去除前後空白、過濾掉純空段落。</li>
<li><strong><code>buf, buf_len = [], 0</code></strong>：累積一個正在構建的 chunk。<code>buf</code> 是段落 list、<code>buf_len</code> 是該 chunk 的 token 累計估算。</li>
<li><strong><code>plen = len(p) / 2</code></strong>：估算這段的 token 數。</li>
<li><strong><code>if buf and buf_len + plen &gt; soft_token_cap</code></strong>：「greedy pack」邏輯——如果加上這段就會超過 cap、把目前 buffer flush 成一個 chunk、再開新 buffer 裝這段。</li>
<li><strong><code>if buf: chunks.append(...)</code></strong>：迴圈結束後、最後一個 buffer 還沒 flush、補上。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼 paragraph-aware、不是固定 token cap</strong>：<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 RAG 原理</a> 提的 chunking 設計取捨——固定 token cap 容易切過句子或段落中間、語意被截斷。Paragraph-aware 切在自然邊界、保留段落內語意完整。</li>
<li><strong>為什麼 <code>soft</code> token cap（軟限制）而不是硬切</strong>：硬切會把一個 800-token 段落切成兩半；軟切讓「目前 chunk + 下一段超過 cap」時 flush 目前 chunk、下一段獨立成新 chunk（即使超過 cap 也保留段落完整）。代價：個別 chunk 可能超過 cap、retrieval 拿到的塊較大、但內容完整。</li>
<li><strong>為什麼 <code>len(p) / 2</code> 估 token</strong>：英文約 4 字元 / token、中文約 1.5 字元 / token、混合平均 / 2 在兩種場景都合理。要精確用 tokenizer（如 <code>tiktoken</code>）、但 demo 不需要——這個 heuristic 在 ±20% 內、夠用來做 chunking 決策。</li>
<li><strong>為什麼 <code>\n\n</code>.join(buf)`</strong>：flush 成 chunk 時、段落間保留空白行分隔、讀者看到 chunk 仍是合法 markdown 結構、不是平鋪文字。</li>
</ul>
<h3 id="2-embed呼叫-ollama-embedding-api">2. <code>embed</code>：呼叫 Ollama embedding API</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">embed</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">payload</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span><span class="s2">&#34;model&#34;</span><span class="p">:</span> <span class="s2">&#34;nomic-embed-text&#34;</span><span class="p">,</span> <span class="s2">&#34;prompt&#34;</span><span class="p">:</span> <span class="n">text</span><span class="p">})</span><span class="o">.</span><span class="n">encode</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">req</span> <span class="o">=</span> <span class="n">urllib</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">Request</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="s2">&#34;http://localhost:11434/api/embeddings&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">data</span><span class="o">=</span><span class="n">payload</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;Content-Type&#34;</span><span class="p">:</span> <span class="s2">&#34;application/json&#34;</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="k">with</span> <span class="n">urllib</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">60</span><span class="p">)</span> <span class="k">as</span> <span class="n">resp</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">9</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">resp</span><span class="o">.</span><span class="n">read</span><span class="p">())[</span><span class="s2">&#34;embedding&#34;</span><span class="p">]</span></span></span></code></pre></div><p><strong>每行做什麼</strong>：</p>
<ol>
<li><strong><code>payload = json.dumps(...).encode()</code></strong>：把 dict 轉成 JSON 字串、再 encode 成 bytes。HTTP body 必須是 bytes、不能直接傳 str。</li>
<li><strong><code>urllib.request.Request(...)</code></strong>：建立 HTTP request 物件。沒寫 <code>method</code> 預設是 GET、但有 <code>data</code> 參數會自動變 POST。</li>
<li><strong><code>headers={&quot;Content-Type&quot;: &quot;application/json&quot;}</code></strong>：告訴 server payload 是 JSON。少了這個、Ollama 可能 parse 不出 body。</li>
<li><strong><code>urlopen(req, timeout=60)</code></strong>：發送 request、<code>timeout=60</code> 是 socket-level timeout（連線 + 讀取總共最多 60 秒）。</li>
<li><strong><code>json.loads(resp.read())[&quot;embedding&quot;]</code></strong>：讀回應 body、parse JSON、取 <code>embedding</code> 欄位（768 維 list of float）。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼用 stdlib <code>urllib</code> 而不是 <code>requests</code></strong>：完全沒有外部 dependency、<code>urllib</code> 是 Python stdlib 內建。<code>requests</code> 較友善但要 <code>pip install</code>、本 demo 想 minimal。</li>
<li><strong>為什麼 timeout=60</strong>：embed 一段文字通常 &lt; 200ms、60 秒夠 buffer 意外（首次 model 載入記憶體可能 5-10 秒）。設無限會在 Ollama 掛掉時整個 script hang。</li>
<li><strong>為什麼 <code>/api/embeddings</code>、不是 <code>/v1/embeddings</code></strong>：兩者都可。原生 endpoint 回應結構平、parse 直接（<code>r[&quot;embedding&quot;]</code>）；OpenAI 相容回應較巢狀（<code>r[&quot;data&quot;][0][&quot;embedding&quot;]</code>）。對 demo、寫法簡單較重要。</li>
</ul>
<h3 id="3-走訪--持久化">3. 走訪 + 持久化</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">md_files</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">content_root</span><span class="o">.</span><span class="n">rglob</span><span class="p">(</span><span class="s2">&#34;*.md&#34;</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="p">[]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">for</span> <span class="n">md</span> <span class="ow">in</span> <span class="n">md_files</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">text</span> <span class="o">=</span> <span class="n">md</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">text</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^---\n.*?\n---\n&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="n">text</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">re</span><span class="o">.</span><span class="n">DOTALL</span><span class="p">)</span>  <span class="c1"># 去掉 frontmatter</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">chunks</span> <span class="o">=</span> <span class="n">slice_markdown</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">for</span> <span class="n">j</span><span class="p">,</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">chunks</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">vec</span> <span class="o">=</span> <span class="n">embed</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">records</span><span class="o">.</span><span class="n">append</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="s2">&#34;source&#34;</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">md</span><span class="o">.</span><span class="n">relative_to</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">content_root</span><span class="o">.</span><span class="n">parent</span><span class="p">)),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="s2">&#34;chunk_index&#34;</span><span class="p">:</span> <span class="n">j</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="s2">&#34;text&#34;</span><span class="p">:</span> <span class="n">chunk</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="s2">&#34;embedding&#34;</span><span class="p">:</span> <span class="n">vec</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;scripts/rag-demo/index.pkl&#34;</span><span class="p">,</span> <span class="s2">&#34;wb&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">pickle</span><span class="o">.</span><span class="n">dump</span><span class="p">(</span><span class="n">records</span><span class="p">,</span> <span class="n">f</span><span class="p">)</span></span></span></code></pre></div><p><strong>每段做什麼</strong>：</p>
<ol>
<li><strong><code>args.content_root.rglob(&quot;*.md&quot;)</code></strong>：recursive glob、回 <code>Path</code> iterator、找出 <code>content_root</code> 下所有 <code>.md</code> 檔（含子目錄）。</li>
<li><strong><code>sorted(...)</code></strong>：排序、讓每次 ingest 順序穩定（git diff 比較友善、retrieval 結果可重現）。</li>
<li><strong><code>text.read_text(encoding=&quot;utf-8&quot;)</code></strong>：讀檔、明確指定 UTF-8（中文 markdown 必要、否則 macOS / Linux 預設可能不一致）。</li>
<li><strong><code>re.sub(r&quot;^---\n.*?\n---\n&quot;, &quot;&quot;, text, count=1, flags=re.DOTALL)</code></strong>：去掉 Hugo frontmatter。
<ul>
<li><code>^---\n</code>：開頭 <code>---\n</code>。</li>
<li><code>.*?</code>：non-greedy match、配到下一個 <code>---</code> 就停。</li>
<li><code>\n---\n</code>：closing fence。</li>
<li><code>count=1</code>：只 strip 第一個（檔案中可能有其他 <code>---</code> 是水平分隔線、不要誤殺）。</li>
<li><code>flags=re.DOTALL</code>：讓 <code>.</code> 也匹配換行符（預設 <code>.</code> 不匹配 <code>\n</code>、規 frontmatter 跨行就吃不到）。</li>
</ul>
</li>
<li><strong><code>records.append({...})</code></strong>：每個 chunk 一個 record、含 source path、chunk index、原文、embedding。</li>
<li><strong><code>md.relative_to(args.content_root.parent)</code></strong>：把絕對 path 變成 <code>llm/00-foundations/xxx.md</code> 形式、retrieval 顯示時短、跨機器可移植。</li>
<li><strong><code>pickle.dump(records, f)</code></strong>：把整個 records list 序列化到 binary 檔。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼要 strip frontmatter</strong>：Frontmatter 是 <code>title</code>、<code>date</code>、<code>tags</code> 等 metadata、不是文章正文。embed 進去會稀釋向量語意（讓「date」「2026-05-11」等 keyword 影響相似度計算）。Strip 後 embedding 只 capture 內容語意。</li>
<li><strong>為什麼 records 是 list of dict 而不是 numpy array</strong>：兩個原因。(1) 每個 record 含 source / chunk_index / text / embedding 四種異質欄位、numpy 處理不直接。(2) 463 chunks 規模、純 Python list 跑 cosine 也只是毫秒級、不需要 vectorize。十萬 chunk 以上才考慮 numpy array + batched dot product。</li>
<li><strong>為什麼 pickle 而不是 JSON</strong>：embedding 是 768-float list、JSON 序列化會把每個 float 變成 ASCII 字串（每個 ~20 bytes）、檔案大很多、parse 也慢。Pickle 是 binary format、保留原本資料結構、檔案小、loader 快。代價：pickle 有 Python 版本相依、跨語言不能讀——但本 demo 索引只給自家 query.py / mcp_server.py 用、可接受。</li>
<li><strong>為什麼存 <code>text</code> 跟 <code>embedding</code>、不只 embedding</strong>：retrieval 要回 chunk 原文給 LLM 看、不能只有 source path（不然每次 query 還要再讀檔）。這裡的 corpus 檔案就是 <a href="/blog/llm/knowledge-cards/retrieval-source/" data-link-title="Retrieval Source" data-link-desc="RAG 從哪個 corpus、index、tool 或外部系統取回內容，決定來源可信度、freshness、權限與引用責任">retrieval source</a>；Pickle 多存原文成本低（~100 byte / chunk）、查詢時方便很多。</li>
</ul>
<h3 id="跑-ingest">跑 ingest</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">cd</span> ~/Projects/blog
</span></span><span class="line"><span class="ln">2</span><span class="cl">python3 scripts/rag-demo/ingest.py</span></span></code></pre></div><ul>
<li><code>cd ~/Projects/blog</code>：切到 repo 根、讓相對路徑 <code>content/llm</code> 對得到 corpus、<code>scripts/rag-demo/index.pkl</code> 對得到 output 位置。</li>
<li><code>python3 scripts/rag-demo/ingest.py</code>：跑 ingest script、預設讀 <code>content/llm/</code>、寫 <code>scripts/rag-demo/index.pkl</code>。</li>
</ul>
<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">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>463 chunks、22 秒、平均 ~21 chunks/sec。瓶頸是 sequential API call、用 async / batch 能快 5-10 倍、但這個量級不值得。</p>
<h2 id="queryretrieval--augmentation--generation">Query：retrieval + augmentation + generation</h2>
<p>完整檔案：<code>scripts/rag-demo/query.py</code>。三段。</p>
<h3 id="1-cosine-similarity--top-k-retrieval">1. Cosine similarity + top-K retrieval</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">cosine</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">dot</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">x</span> <span class="o">*</span> <span class="n">y</span> <span class="k">for</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">na</span> <span class="o">=</span> <span class="n">math</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="nb">sum</span><span class="p">(</span><span class="n">x</span> <span class="o">*</span> <span class="n">x</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">a</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">nb</span> <span class="o">=</span> <span class="n">math</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="nb">sum</span><span class="p">(</span><span class="n">y</span> <span class="o">*</span> <span class="n">y</span> <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="n">b</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="n">dot</span> <span class="o">/</span> <span class="p">(</span><span class="n">na</span> <span class="o">*</span> <span class="n">nb</span><span class="p">)</span> <span class="k">if</span> <span class="n">na</span> <span class="ow">and</span> <span class="n">nb</span> <span class="k">else</span> <span class="mf">0.0</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="k">def</span> <span class="nf">retrieve</span><span class="p">(</span><span class="n">records</span><span class="p">,</span> <span class="n">query_vec</span><span class="p">,</span> <span class="n">top_k</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">scored</span> <span class="o">=</span> <span class="p">[(</span><span class="n">cosine</span><span class="p">(</span><span class="n">query_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"> 9</span><span class="cl">    <span class="n">scored</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><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 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">10</span><span class="cl">    <span class="k">return</span> <span class="n">scored</span><span class="p">[:</span><span class="n">top_k</span><span class="p">]</span></span></span></code></pre></div><p><strong>每行做什麼</strong>：</p>
<ol>
<li><strong><code>dot = sum(x * y for x, y in zip(a, b))</code></strong>：兩個向量的內積（dot product）。<code>zip(a, b)</code> 把兩個 list 對位配對、generator expression 算每對相乘、sum 加起來。</li>
<li><strong><code>na = math.sqrt(sum(x * x for x in a))</code></strong>：a 的 L2 norm（歐氏範數）—— <code>sqrt(x1² + x2² + ... + xn²)</code>。</li>
<li><strong><code>nb = math.sqrt(sum(y * y for y in b))</code></strong>：b 的 L2 norm。</li>
<li><strong><code>return dot / (na * nb) if na and nb else 0.0</code></strong>：cosine = dot / (||a|| × ||b||)。三元運算子防 zero division——若任一向量是零向量、na 或 nb 為 0、回 0.0 而不是 crash。</li>
<li><strong><code>scored = [(cosine(query_vec, r[&quot;embedding&quot;]), r) for r in records]</code></strong>：對每個 record 算相似度、組成 (score, record) tuple 的 list。</li>
<li><strong><code>scored.sort(key=lambda x: x[0], reverse=True)</code></strong>：按 score 從大到小排序。<code>key=lambda x: x[0]</code> 取 tuple 第一個元素（score）當排序 key。</li>
<li><strong><code>return scored[:top_k]</code></strong>：取前 K 個。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼 cosine 而不是純 dot product</strong>：純 dot product 受向量長度影響——長向量自動拿高分、跟「相似度」無關。Cosine 把向量正規化到單位長度、純看方向、是「語意相似」的標準衡量。語意相似 embedding 應該方向相近、長度差異不重要。</li>
<li><strong>為什麼用 <code>math.sqrt</code> 而不是 <code>**0.5</code></strong>：兩者數學等價、但 <code>math.sqrt</code> 用 C-level 實作、CPython 中比 Python 級 <code>**0.5</code> 快幾倍。對 463 chunks 影響不大、但 production scale 會放大差異——習慣寫 <code>math.sqrt</code> 的好。</li>
<li><strong>為什麼 <code>if na and nb else 0.0</code></strong>：防禦性程式設計。理論上 embedding 不會是零向量（模型架構保證有非零權重）、但邊界情況（空輸入、API 出錯回 placeholder）可能出現、避免 ZeroDivisionError 整個 query 失敗。回 0.0 表示「無法判斷相似度」、retrieval 排序時自然排到最後。</li>
<li><strong>為什麼 sort 全部、不用 heap</strong>：463 records、Python sort 是 O(n log n)、毫秒級。<code>heapq.nlargest(top_k, ...)</code> 是 O(n log k)、在 k=4、n=463 上實測幾乎沒差。十萬 record 以上才看到顯著差別。</li>
<li><strong>為什麼用 list of tuple、不用 numpy</strong>：跟 ingest 同樣的理由——小規模不需要 vectorize、純 Python 清楚。</li>
</ul>
<h3 id="2-建-augmented-prompt">2. 建 augmented prompt</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">context_blocks</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln"> 2</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">retrieved</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">context_blocks</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="sa">f</span><span class="s2">&#34;[來源：</span><span class="si">{</span><span class="n">r</span><span class="p">[</span><span class="s1">&#39;source&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">#chunk</span><span class="si">{</span><span class="n">r</span><span class="p">[</span><span class="s1">&#39;chunk_index&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2"> 相似度：</span><span class="si">{</span><span class="n">score</span><span class="si">:</span><span class="s2">.3f</span><span class="si">}</span><span class="s2">]</span><span class="se">\n</span><span class="si">{</span><span class="n">r</span><span class="p">[</span><span class="s1">&#39;text&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">)</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="n">system</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="s2">&#34;你是一個技術文件問答助手。&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="s2">&#34;依下方 context 內容回答問題、不要編造 context 外的事實。&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="s2">&#34;若 context 不足以回答、明確說『資料不足』。&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="s2">&#34;回答末尾列出引用的來源 path。&#34;</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="n">user</span> <span class="o">=</span> <span class="s2">&#34;## Context</span><span class="se">\n\n</span><span class="s2">&#34;</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">---</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">context_blocks</span><span class="p">)</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">## Question</span><span class="se">\n\n</span><span class="si">{</span><span class="n">question</span><span class="si">}</span><span class="s2">&#34;</span>
</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"><span class="n">messages</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">{</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;system&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="n">system</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">{</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="n">user</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">]</span></span></span></code></pre></div><p><strong>每行做什麼</strong>：</p>
<ol>
<li><strong><code>f&quot;[來源：{...} 相似度：{score:.3f}]\n{r['text']}&quot;</code></strong>：每個 retrieved chunk 加 header 標明出處跟相似度、再接原文。<code>:.3f</code> 是 score 格式化到三位小數。</li>
<li><strong><code>&quot;\n\n---\n\n&quot;.join(context_blocks)</code></strong>：用 <code>---</code> 水平分隔線分隔各 chunk、視覺上清楚。</li>
<li><strong><code>{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system}</code></strong>：system message 給 LLM 設定角色 + 約束。</li>
<li><strong><code>{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user}</code></strong>：user message 含 context 跟 question、是 LLM 實際讀的內容。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼 <a href="/blog/llm/knowledge-cards/system-prompt/" data-link-title="System Prompt" data-link-desc="LLM application 中由開發者預設、不直接顯示給使用者的指令層、定義模型的角色、行為規範、輸出格式">system prompt</a> 約束四件事</strong>（角色、忠於 context、資料不足時明說、引用來源）：
<ul>
<li><strong>角色</strong>：「技術文件問答助手」框定模型行為、減少 off-topic 回應。</li>
<li><strong>忠於 context</strong>：對抗 RAG 最常見的失敗模式——LLM 看到 context 但用自己訓練的 knowledge 補完、結果跟 corpus 不一致。明確要求 follow context 能降低（雖然不能完全消除、見實測 1）。</li>
<li><strong>資料不足時明說</strong>：避免 LLM「硬要回答」造成 hallucination。對 weak model 這條 follow 度差、但對 large model 有效。</li>
<li><strong>引用來源</strong>：traceability。讀者能回查 corpus、驗證模型答案。</li>
</ul>
</li>
<li><strong>為什麼 <code>## Context</code> / <code>## Question</code> 結構</strong>：用 markdown heading 結構幫助 LLM 區分「我要讀什麼」「我要回答什麼」。比平鋪文字穩定（即使對小模型）。</li>
<li><strong>為什麼把 retrieved chunks 全塞 user message、不分開</strong>：MCP / function calling 的更現代做法是把 retrieved 結果做成 tool response、模型主動 call retrieval tool。本 demo 不引入 tool use、直接塞 prompt 較單純——能說明 RAG 核心（augmentation）不必牽扯 tool use。</li>
</ul>
<h3 id="3-呼叫-chat-completions">3. 呼叫 chat completions</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">chat</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span> <span class="n">model</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">payload</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span><span class="s2">&#34;model&#34;</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span> <span class="s2">&#34;messages&#34;</span><span class="p">:</span> <span class="n">messages</span><span class="p">,</span> <span class="s2">&#34;stream&#34;</span><span class="p">:</span> <span class="kc">False</span><span class="p">})</span><span class="o">.</span><span class="n">encode</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">req</span> <span class="o">=</span> <span class="n">urllib</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">Request</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="s2">&#34;http://localhost:11434/v1/chat/completions&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">data</span><span class="o">=</span><span class="n">payload</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;Content-Type&#34;</span><span class="p">:</span> <span class="s2">&#34;application/json&#34;</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="k">with</span> <span class="n">urllib</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">180</span><span class="p">)</span> <span class="k">as</span> <span class="n">resp</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">9</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">resp</span><span class="o">.</span><span class="n">read</span><span class="p">())[</span><span class="s2">&#34;choices&#34;</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s2">&#34;message&#34;</span><span class="p">][</span><span class="s2">&#34;content&#34;</span><span class="p">]</span></span></span></code></pre></div><p><strong>每行做什麼</strong>：</p>
<ol>
<li><strong><code>json.dumps({&quot;model&quot;: ..., &quot;messages&quot;: ..., &quot;stream&quot;: False}).encode()</code></strong>：構造 OpenAI 相容 chat completions request body。<code>stream: False</code> 讓 server 等生成完再一次回、不要 SSE 串流。</li>
<li><strong><code>/v1/chat/completions</code></strong>：OpenAI 相容 endpoint、跟雲端 OpenAI 完全同樣 schema。</li>
<li><strong><code>timeout=180</code></strong>：3 分鐘、給長 context + 慢模型空間。</li>
<li><strong><code>[&quot;choices&quot;][0][&quot;message&quot;][&quot;content&quot;]</code></strong>：parse OpenAI 標準 response 結構、取第一個 choice 的 content。</li>
</ol>
<p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong>為什麼 <code>stream: False</code></strong>：demo 要把完整 answer 印出、不需要 incremental display。<code>stream: True</code> 要寫 SSE parser、複雜。Production 互動式 UI 才需要 streaming。</li>
<li><strong>為什麼 timeout=180、不是 60</strong>：1B 模型 + 4 個 retrieved chunks 的 context、prefill 可能要 5-30 秒、生成 100-500 token 又要 5-20 秒、保守設 3 分鐘。embed function 用 60 是因為 embedding 是純 forward pass、單一 token 量級操作、不需要這麼長。</li>
<li><strong>為什麼 <code>/v1/...</code> 而不是 <code>/api/...</code></strong>：chat completions 走 OpenAI 相容 endpoint、生態都用這個格式（Continue.dev、Cursor、各家 SDK）。embedding 用 <code>/api/...</code> 是因為原生 schema 簡單；chat 用 <code>/v1/...</code> 是因為 message-based 結構是 OpenAI 標準、跨工具互通。</li>
</ul>
<h2 id="實測結果retrieval-對generation-弱">實測結果：retrieval 對、generation 弱</h2>
<h3 id="測試-1什麼是-mtp為什麼對寫-code-場景特別有效">測試 1：「什麼是 MTP？為什麼對寫 code 場景特別有效？」</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">python3 scripts/rag-demo/query.py --show-retrieved <span class="s2">&#34;什麼是 MTP？為什麼對寫 code 場景特別有效？&#34;</span></span></span></code></pre></div><p><code>--show-retrieved</code> 是個 flag、開啟後在 stderr 印 retrieved chunks 跟 score、答案還是進 stdout。是 debug 跟教學用、不會影響 LLM 看到的 prompt。</p>
<p>Retrieval：</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">0.870  llm/knowledge-cards/transformer.md#chunk2
</span></span><span class="line"><span class="ln">2</span><span class="cl">0.825  llm/03-theoretical-foundations/sampling-and-decoding.md#chunk8
</span></span><span class="line"><span class="ln">3</span><span class="cl">0.782  llm/knowledge-cards/ttft.md#chunk1
</span></span><span class="line"><span class="ln">4</span><span class="cl">0.771  llm/knowledge-cards/mtp.md#chunk2</span></span></code></pre></div><p>四個 chunk 都跟問題相關、相似度合理。MTP 卡確實被命中（雖然不是 top-1、是因為 transformer.md 該段提到 MTP）。</p>
<p>Generation（1B 模型）：</p>
<blockquote>
<p>MTP 僅指使用 Ollama 進行 Coding 模型訓練與部署、它是一種系統性的方式&hellip;
來源：<a href="https://llm.dev/mti/">llm.dev</a></p></blockquote>
<p><strong>錯</strong>：1B 模型編造了「MTP 僅指使用 Ollama」這個事實（不對、MTP 是 Google 為 Gemma 釋出的、跟 Ollama 沒直接關係）、來源 URL 也是 hallucination。</p>
<h3 id="測試-2mcp-跟-function-calling-有什麼差別">測試 2：「MCP 跟 function calling 有什麼差別？」</h3>
<p>Retrieval：</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">0.721  llm/04-applications/application-protocols.md#chunk2
</span></span><span class="line"><span class="ln">2</span><span class="cl">0.704  llm/04-applications/application-protocols.md#chunk1
</span></span><span class="line"><span class="ln">3</span><span class="cl">0.702  llm/04-applications/application-protocols.md#chunk0
</span></span><span class="line"><span class="ln">4</span><span class="cl">0.693  llm/knowledge-cards/function-calling.md#chunk1</span></span></code></pre></div><p>完美命中——4.3 應用層協議章節三個 chunk + function-calling 卡。</p>
<p>Generation：模型把幾段重複拼接、framing 跟原文有出入、但比測試 1 好（因為 context 涵蓋直接答案）。</p>
<h2 id="觀察跟原理對應">觀察跟原理對應</h2>
<p>這個 demo 剛好示範 <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 RAG 原理</a> 提的兩段式失敗模式：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>表現</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Retrieval</td>
          <td>命中率好、找到對的 chunks</td>
          <td><code>nomic-embed-text</code> 對技術文件覆蓋好、cosine 對短 query 也 OK</td>
      </tr>
      <tr>
          <td>Generation</td>
          <td>內容有時編造、不忠於 context、來源亂寫</td>
          <td><code>gemma3:1b</code> 模型容量不足以可靠 follow system prompt</td>
      </tr>
  </tbody>
</table>
<p>換 31B+ 模型 generation 會改善很多——這也是 4.0 章節提到「retrieval 跟下游 LLM 訓練分佈不一致」會放大失敗的具體例子。寫 RAG 系統時、generation 失敗不一定是「retrieval 沒給對 context」、可能是「模型不夠強」。</p>
<h2 id="何時這份-demo-會過時">何時這份 demo 會過時</h2>
<ul>
<li><strong>Ollama API 形狀</strong>：短期內不會變（生態都依賴）。</li>
<li><strong><code>nomic-embed-text</code> / <code>gemma3:1b</code> 具體 tag</strong>：預期會被新模型取代、但 retrieval + augmentation 結構不變。</li>
<li><strong>Chunking heuristic</strong>：簡單 char-count / 2 很粗、半年後若有便宜的 token counter 直接接會更準。</li>
<li><strong>Pickle 儲存</strong>：production 場景建議換 vector DB、本 demo 是教學用。</li>
</ul>
<p>實作換代時、保留 ingest / retrieve / augment / generate 四段、各段內部換工具即可——這四段是 RAG 的骨架、跨工具世代不變。</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"># 一次性建索引（每次 corpus 變動才需要重建）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> ~/Projects/blog
</span></span><span class="line"><span class="ln">3</span><span class="cl">python3 scripts/rag-demo/ingest.py</span></span></code></pre></div><ul>
<li><code>cd</code>：切到 repo 根、relative path 對得到。</li>
<li><code>python3 ingest.py</code>：跑索引、預設讀 <code>content/llm/</code>、寫 <code>scripts/rag-demo/index.pkl</code>。每次 corpus 變動才需要重跑、不變的話 index 就一直用。</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"># 查詢（任意次）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">python3 scripts/rag-demo/query.py --show-retrieved <span class="s2">&#34;你的問題&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">python3 scripts/rag-demo/query.py --top-k <span class="m">5</span> --model gemma3:1b <span class="s2">&#34;問題&#34;</span></span></span></code></pre></div><ul>
<li><code>--show-retrieved</code>：教學 / debug 用、列 retrieved chunks 跟 score 到 stderr。</li>
<li><code>--top-k 5</code>：取 top 5 instead of 預設 4。chunks 越多 context 越長、TTFT 越久、但訊息越完整。</li>
<li><code>--model gemma3:1b</code>：指定 chat model。換 <code>gemma3:4b</code>、<code>gemma4:31b-coding-mtp-bf16</code> 等 generation 品質會大幅改善。</li>
</ul>
<p>完整 source 在 <code>scripts/rag-demo/</code> 下、200 行 Python、無外部 dependency。</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>、把 retrieval 包成 MCP server 暴露給 LLM application 見 <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>、RAG + MCP 同跑的記憶體 / 程序預算見 <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/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a> 跟 <a href="/blog/llm/knowledge-cards/embedding-model/" data-link-title="Embedding Model" data-link-desc="把文字轉成向量的模型：用於 codebase 索引與語意搜尋">embedding model</a>。</p>
]]></content:encoded></item><item><title>Hands-on：Ollama 改檔案 / 寫程式碼的權限邊界在哪</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/permission-boundary/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/permission-boundary/</guid><description>&lt;p>「Ollama 自己改檔案要不要 sudo？」「叫它寫 &lt;code>rm -rf&lt;/code> 會直接刪嗎？」這類問題的答案來自一個根本事實：&lt;strong>LLM 是 pure function、文字進、文字出、本身沒任何 file system / shell / network 副作用&lt;/strong>。改檔案、刪檔案、發網路請求、執行 shell command——全部由 &lt;strong>wrapper 或人類&lt;/strong>做。LLM 「以為」自己做了什麼、跟實際發生什麼是兩件事。&lt;/p>
&lt;p>本篇用四組對照實驗證明這個事實、再展開 wrapper 三檔審查粒度的設計取捨。這跟 &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;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 Agent 跟人類審查的協作模型&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理&lt;/a> 三個原則章節對應、實作層的權限與供應鏈判讀對應 &lt;a href="https://tarrragon.github.io/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 tool use 與 MCP server 的權限模型&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0 模型供應鏈與信任邊界&lt;/a>。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>驗證日期&lt;/strong>：2026-05-12
&lt;strong>環境&lt;/strong>：Ollama 0.23.2、&lt;code>gemma3:1b&lt;/code>、Python stdlib
&lt;strong>檔案位置&lt;/strong>：&lt;code>scripts/permission-demo/edit_with_llm.py&lt;/code>&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼這個問題重要">為什麼這個問題重要&lt;/h2>
&lt;p>直覺常見的誤判：&lt;/p>
&lt;ul>
&lt;li>「LLM 寫了 &lt;code>rm -rf&lt;/code> 我電腦會壞」——錯。LLM 寫指令不代表執行。&lt;/li>
&lt;li>「Ollama API 改我檔案要 sudo」——錯。Ollama API 根本碰不到檔案。&lt;/li>
&lt;li>「我跑 wrapper 就讓 LLM 改檔案、應該有 confirm 機制吧」——錯。Confirm 機制完全是 wrapper 開發者自己決定要不要寫、LLM 不知道、不在乎。&lt;/li>
&lt;/ul>
&lt;p>理解這個邊界、後續設計 LLM 應用的權限模型才有 ground truth。錯誤的 mental model 會導致兩種 failure：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>過度恐懼&lt;/strong>：因為怕 LLM「亂改」、把所有 LLM 互動關起來、放棄自動化收益。&lt;/li>
&lt;li>&lt;strong>過度信任&lt;/strong>：相信 LLM「不會做壞事」、給 wrapper 自動執行權限、結果小模型亂解 instruction 把資料毀掉。&lt;/li>
&lt;/ol>
&lt;p>實際上權限設計的判讀錨點是：&lt;strong>這個動作有沒有副作用、誰執行&lt;/strong>。LLM 永遠不執行、所以權限不在 LLM 層；wrapper 執行、所以權限完全在 wrapper 設計。&lt;/p>
&lt;h2 id="test-1直接-api-問改檔案看會發生什麼">Test 1：直接 API 問改檔案、看會發生什麼&lt;/h2>
&lt;p>挑一個檔案（token 卡片）、用 curl 送 chat completions、prompt 寫「修改這個檔案」、然後 check 檔案 mtime 跟 md5：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 修改前 snapshot&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">stat -f &lt;span class="s2">&amp;#34;%m %N&amp;#34;&lt;/span> content/llm/knowledge-cards/token.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">md5 -q content/llm/knowledge-cards/token.md
&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">&lt;span class="c1"># 用 system prompt「假裝你有 file 權限」、user 直接指明路徑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">curl -s http://localhost:11434/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;:&amp;#34;gemma3:1b&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;:[
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;:&amp;#34;system&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;You can modify files. The user provides a file. You modify it.&amp;#34;},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Please modify /Users/.../token.md to add a sentence...&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s1"> &amp;#34;stream&amp;#34;:false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="c1"># 修改後 snapshot&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">stat -f &lt;span class="s2">&amp;#34;%m %N&amp;#34;&lt;/span> content/llm/knowledge-cards/token.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">md5 -q content/llm/knowledge-cards/token.md&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>實測結果&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>「Ollama 自己改檔案要不要 sudo？」「叫它寫 <code>rm -rf</code> 會直接刪嗎？」這類問題的答案來自一個根本事實：<strong>LLM 是 pure function、文字進、文字出、本身沒任何 file system / shell / network 副作用</strong>。改檔案、刪檔案、發網路請求、執行 shell command——全部由 <strong>wrapper 或人類</strong>做。LLM 「以為」自己做了什麼、跟實際發生什麼是兩件事。</p>
<p>本篇用四組對照實驗證明這個事實、再展開 wrapper 三檔審查粒度的設計取捨。這跟 <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>、<a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent 跟人類審查的協作模型</a>、<a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理</a> 三個原則章節對應、實作層的權限與供應鏈判讀對應 <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 tool use 與 MCP server 的權限模型</a> 跟 <a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0 模型供應鏈與信任邊界</a>。</p>
<blockquote>
<p><strong>驗證日期</strong>：2026-05-12
<strong>環境</strong>：Ollama 0.23.2、<code>gemma3:1b</code>、Python stdlib
<strong>檔案位置</strong>：<code>scripts/permission-demo/edit_with_llm.py</code></p></blockquote>
<h2 id="為什麼這個問題重要">為什麼這個問題重要</h2>
<p>直覺常見的誤判：</p>
<ul>
<li>「LLM 寫了 <code>rm -rf</code> 我電腦會壞」——錯。LLM 寫指令不代表執行。</li>
<li>「Ollama API 改我檔案要 sudo」——錯。Ollama API 根本碰不到檔案。</li>
<li>「我跑 wrapper 就讓 LLM 改檔案、應該有 confirm 機制吧」——錯。Confirm 機制完全是 wrapper 開發者自己決定要不要寫、LLM 不知道、不在乎。</li>
</ul>
<p>理解這個邊界、後續設計 LLM 應用的權限模型才有 ground truth。錯誤的 mental model 會導致兩種 failure：</p>
<ol>
<li><strong>過度恐懼</strong>：因為怕 LLM「亂改」、把所有 LLM 互動關起來、放棄自動化收益。</li>
<li><strong>過度信任</strong>：相信 LLM「不會做壞事」、給 wrapper 自動執行權限、結果小模型亂解 instruction 把資料毀掉。</li>
</ol>
<p>實際上權限設計的判讀錨點是：<strong>這個動作有沒有副作用、誰執行</strong>。LLM 永遠不執行、所以權限不在 LLM 層；wrapper 執行、所以權限完全在 wrapper 設計。</p>
<h2 id="test-1直接-api-問改檔案看會發生什麼">Test 1：直接 API 問改檔案、看會發生什麼</h2>
<p>挑一個檔案（token 卡片）、用 curl 送 chat completions、prompt 寫「修改這個檔案」、然後 check 檔案 mtime 跟 md5：</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"># 修改前 snapshot</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">stat -f <span class="s2">&#34;%m %N&#34;</span> content/llm/knowledge-cards/token.md
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">md5 -q content/llm/knowledge-cards/token.md
</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"><span class="c1"># 用 system prompt「假裝你有 file 權限」、user 直接指明路徑</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">curl -s http://localhost:11434/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s1">    &#34;model&#34;:&#34;gemma3:1b&#34;,
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s1">    &#34;messages&#34;:[
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s1">      {&#34;role&#34;:&#34;system&#34;,&#34;content&#34;:&#34;You can modify files. The user provides a file. You modify it.&#34;},
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s1">      {&#34;role&#34;:&#34;user&#34;,&#34;content&#34;:&#34;Please modify /Users/.../token.md to add a sentence...&#34;}
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s1">    ],
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s1">    &#34;stream&#34;:false
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s1">  }&#39;</span>
</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"><span class="c1"># 修改後 snapshot</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">stat -f <span class="s2">&#34;%m %N&#34;</span> content/llm/knowledge-cards/token.md
</span></span><span class="line"><span class="ln">19</span><span class="cl">md5 -q content/llm/knowledge-cards/token.md</span></span></code></pre></div><p><strong>實測結果</strong>：</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">=== Before ===
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">1778508712 content/llm/knowledge-cards/token.md
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">d9f2d822f7458af62399076a94ef20f6
</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><span class="line"><span class="ln"> 6</span><span class="cl">Okay, here&#39;s the modified content of `/Users/.../token.md`...
</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">=== After ===
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">1778508712 content/llm/knowledge-cards/token.md  ← mtime same
</span></span><span class="line"><span class="ln">10</span><span class="cl">d9f2d822f7458af62399076a94ef20f6                  ← md5 same</span></span></code></pre></div><p>mtime 沒變、md5 沒變、檔案內容完全沒動。但 LLM 用「Okay, here&rsquo;s the modified content」這種口氣回答——它<strong>以為</strong>自己改了、實際上只生成了一段 markdown 文字。</p>
<p><strong>結論</strong>：Ollama HTTP API 是 stateless、pure function。輸入 messages、輸出 message content。整個過程沒寫進 socket 以外的任何地方。</p>
<p>為什麼會這樣設計：</p>
<ul>
<li><strong>沙箱本來就在 API 邊界</strong>：HTTP server 接 request、跑 forward pass、回 response。期間沒呼叫 <code>fs.write()</code> / <code>subprocess.run()</code> / 任何 effectful API。</li>
<li><strong><a href="/blog/llm/knowledge-cards/system-prompt/" data-link-title="System Prompt" data-link-desc="LLM application 中由開發者預設、不直接顯示給使用者的指令層、定義模型的角色、行為規範、輸出格式">system prompt</a> 不是權限授予</strong>：「You can modify files」這句話對模型來說只是文字 context、不會真的給它 file access。Prompt 是「LLM 內部的 context」、不是「runtime capability」。</li>
<li><strong>訓練資料讓 LLM 「以為」自己有能力</strong>：LLM 訓練資料含大量「使用者問問題、AI 改檔案」的範例（如 GitHub Copilot agent traces、tool-use SFT 資料）、模型學會用「我已經改了」這種語氣回答——是 mimic、不是真正的 action。</li>
</ul>
<h2 id="test-2寫-wrapper-用-dry-run-模式安全處理">Test 2：寫 wrapper 用 &ndash;dry-run 模式安全處理</h2>
<p>權限不在 LLM、在 wrapper。寫一個 100 行的 wrapper、看怎麼設計 permission gates。完整檔案：<code>scripts/permission-demo/edit_with_llm.py</code>。</p>
<p>核心 architecture：</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="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="c1"># 1. 讀檔（wrapper 用自己的 fs 權限）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">original</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span>
</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">    <span class="c1"># 2. 送 LLM、拿回提議的新內容</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">chat</span><span class="p">([</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="p">{</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;system&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="s2">&#34;You modify text files. Output ONLY ...&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="p">{</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;File: </span><span class="si">{</span><span class="n">args</span><span class="o">.</span><span class="n">file</span><span class="si">}</span><span class="se">\n</span><span class="s2">Content:</span><span class="se">\n</span><span class="si">{</span><span class="n">original</span><span class="si">}</span><span class="se">\n</span><span class="s2">Instruction: </span><span class="si">{</span><span class="n">args</span><span class="o">.</span><span class="n">instruction</span><span class="si">}</span><span class="s2">&#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="n">new_content</span> <span class="o">=</span> <span class="n">extract_code_block</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="c1"># 3. Diff（純讀、永遠 safe、不需 gate）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">diff</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">difflib</span><span class="o">.</span><span class="n">unified_diff</span><span class="p">(</span><span class="n">original</span><span class="o">.</span><span class="n">splitlines</span><span class="p">(</span><span class="o">...</span><span class="p">),</span> <span class="n">new_content</span><span class="o">.</span><span class="n">splitlines</span><span class="p">(</span><span class="o">...</span><span class="p">)))</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="n">diff</span><span class="p">)</span>
</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">    <span class="c1"># 4. PERMISSION GATE：wrapper 決定要不要 apply</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">auto</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">args</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="n">new_content</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">elif</span> <span class="n">args</span><span class="o">.</span><span class="n">confirm</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="nb">input</span><span class="p">(</span><span class="s2">&#34;Apply? [y/N] &#34;</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;y&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">            <span class="n">args</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="n">new_content</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">else</span><span class="p">:</span>  <span class="c1"># --dry-run，預設</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="k">pass</span>  <span class="c1"># 不寫</span></span></span></code></pre></div><p><strong>為什麼這樣設計</strong>：</p>
<ul>
<li><strong><code>extract_code_block</code></strong>：嘗試 well-formed <code>```lang\n...\n```</code> regex、失敗 fallback 到 <code>```lang\n...$</code> 寬鬆版。小模型（1B）常忘記結尾 fence、寬鬆才能用。寫嚴格 regex 失敗時直接 abort、是另一種 permission gate（不應用 = 安全）。</li>
<li><strong>永遠先印 diff</strong>：diff 是純讀操作、無副作用、永遠 safe。讓使用者先看 LLM 提議了什麼、再決定要不要 apply。</li>
<li><strong><code>args.auto</code> 在 <code>elif</code> 鏈最前面、<code>dry-run</code> 預設</strong>：強迫使用者明示 opt-in 才會寫檔。預設不寫、是「safe default」設計原則。</li>
</ul>
<p>跑 <code>--dry-run</code> 預設、看實際發生：</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">python3 scripts/permission-demo/edit_with_llm.py <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  content/llm/knowledge-cards/token.md <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  <span class="s2">&#34;把開頭第一段最後加一句『Token 是 embedding 的輸入單位』&#34;</span></span></span></code></pre></div><p>實測輸出（1B 模型）：</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">[+] Asking gemma3:1b to: &#39;把開頭第一段最後加一句「Token 是 embedding 的輸入單位」&#39;
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">[+] Proposed diff:
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">--- a/token.md
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">+++ b/token.md
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">@@ -6,16 +6,4 @@
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"> tags: [&#34;llm&#34;, &#34;knowledge-cards&#34;]
</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">-Token 的核心概念是「LLM 內部處理文字的最小單位」...（整段刪除）
</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">-## 概念位置
</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">+Token 是 embedding 的輸入單位。
</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">[+] --dry-run: file unchanged. Use --confirm or --auto to apply.</span></span></code></pre></div><p><strong>驚悚發現</strong>：1B 模型完全沒理解「加一句」、把整篇刪掉只剩一行。但 <code>--dry-run</code> 不寫檔、檔案安全。</p>
<p><strong>重點</strong>：</p>
<ul>
<li>LLM 行為糟、但 wrapper 設計安全、結果 OK。</li>
<li>把同樣 instruction 餵 31B+ 模型結果會合理——模型能力決定 LLM 端品質、wrapper 設計決定<strong>最差情況的後果</strong>。</li>
<li>在 wrapper 端永遠假設 LLM 會亂改、設計 safe default、是 defensive programming。</li>
</ul>
<h2 id="test-3--confirm-模式step-by-step-審查">Test 3：<code>--confirm</code> 模式、step-by-step 審查</h2>
<p><code>--confirm</code> mode 印 diff、問 y/N、user 確認才寫：</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">python3 scripts/permission-demo/edit_with_llm.py <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  content/llm/knowledge-cards/token.md <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  <span class="s2">&#34;加一句說明&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --confirm</span></span></code></pre></div><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">[+] Proposed diff:
</span></span><span class="line"><span class="ln">2</span><span class="cl">--- a/token.md
</span></span><span class="line"><span class="ln">3</span><span class="cl">+++ b/token.md
</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">
</span></span><span class="line"><span class="ln">6</span><span class="cl">[?] Apply this change to content/llm/.../token.md? [y/N] _</span></span></code></pre></div><p>使用者看 diff 發現「整篇被刪了」、按 N、檔案安全。</p>
<p><strong>這個 mode 對應的副作用範圍</strong>：<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> 提的 spectrum：</p>
<table>
  <thead>
      <tr>
          <th>等級</th>
          <th>副作用</th>
          <th>適合 mode</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>純讀（grep、git status）</td>
          <td><code>--dry-run</code> 或無 gate</td>
      </tr>
      <tr>
          <td>2</td>
          <td>寫 sandbox / staging</td>
          <td><code>--dry-run</code> + 人類事後審</td>
      </tr>
      <tr>
          <td>3</td>
          <td>寫本地持久化（如 commit、edit 檔）</td>
          <td><code>--confirm</code></td>
      </tr>
      <tr>
          <td>4</td>
          <td>寫共享 / production（push、deploy）</td>
          <td><code>--confirm</code> 強制</td>
      </tr>
      <tr>
          <td>5</td>
          <td>操作真實世界（發 email、買股票）</td>
          <td><code>--confirm</code> + 額外 audit</td>
      </tr>
  </tbody>
</table>
<p>本 demo 改 markdown 是等級 3（寫本地檔）、<code>--confirm</code> 是合適粒度。改 production code 或 git push 是等級 4 / 5、<code>--confirm</code> 該強制不該 optional。</p>
<h2 id="test-4--auto-模式危險自動化">Test 4：<code>--auto</code> 模式、危險自動化</h2>
<p><code>--auto</code> 不問直接寫：</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">cp /tmp/token-orig.md content/llm/knowledge-cards/token.md  <span class="c1"># 還原</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">python3 scripts/permission-demo/edit_with_llm.py <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  content/llm/knowledge-cards/token.md <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  <span class="s2">&#34;加一句說明&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --auto</span></span></code></pre></div><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">[!] --auto mode: writing without confirmation
</span></span><span class="line"><span class="ln">2</span><span class="cl">[+] wrote content/llm/knowledge-cards/token.md</span></span></code></pre></div><p>檔案內容變成：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl">---
</span></span><span class="line"><span class="ln">2</span><span class="cl">title: &#34;Token&#34;
</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></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">Token 是 embedding 的輸入單位。</span></span></code></pre></div><p>整篇刪光、只剩一句。<strong>沒人 catch 到、commit + push 出去就是 production 災難</strong>。</p>
<p><strong><code>--auto</code> mode 適合什麼場景</strong>：</p>
<ul>
<li>LLM 任務範圍狹窄、可預測（如 format JSON、補 type annotation 給已有 type stub）。</li>
<li>配合 git workflow（每次 auto edit 都自動 commit、出問題 git revert）。</li>
<li>CI / batch processing、人類事後審 PR。</li>
</ul>
<p><strong><code>--auto</code> mode 不適合什麼場景</strong>：</p>
<ul>
<li>任務開放性高（「改寫這段讓它更清楚」）。</li>
<li>不可逆環境（直接寫 production DB / 發 email）。</li>
<li>用弱模型（&lt; 14B）跑、行為不穩。</li>
</ul>
<p>設計 wrapper 時、把 <code>--auto</code> 設成顯式 opt-in、預設保持 dry-run / confirm 等較保守模式。本 demo 的 mutually_exclusive 設計（<code>-g.add_mutually_exclusive_group()</code>）保證三種 mode 只能擇一、避免歧義。</p>
<h2 id="test-5llm-寫-shell-command誰執行">Test 5：LLM 寫 shell command、誰執行？</h2>
<p>改檔案是「直接副作用」、寫 shell command 是「間接副作用」——同樣的問題：誰真的執行？</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">curl -s http://localhost:11434/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s1">    &#34;model&#34;:&#34;gemma3:1b&#34;,
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s1">    &#34;messages&#34;:[{&#34;role&#34;:&#34;user&#34;,&#34;content&#34;:&#34;Give me a single shell command to find and delete all .log files in my home directory.&#34;}],
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s1">    &#34;stream&#34;:false
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s1">  }&#39;</span> <span class="p">|</span> python3 -c <span class="s2">&#34;import json,sys; print(json.load(sys.stdin)[&#39;choices&#39;][0][&#39;message&#39;][&#39;content&#39;])&#34;</span></span></span></code></pre></div><p>LLM 回：</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">```bash
</span></span><span class="line"><span class="ln">2</span><span class="cl">find ~ -name &#34;*.log&#34; -delete
</span></span><span class="line"><span class="ln">3</span><span class="cl">```</span></span></code></pre></div><p>這是個有破壞性的指令。檢查 home 下 .log 還在不在：</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">find ~ -maxdepth <span class="m">3</span> -name <span class="s2">&#34;*.log&#34;</span> 2&gt;/dev/null <span class="p">|</span> head -5
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># /Users/tarragon/.npm/_logs/2026-05-11T15_33_34_348Z-debug-0.log</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># /Users/tarragon/.npm/_logs/2026-05-11T11_58_08_827Z-debug-0.log</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># ...</span></span></span></code></pre></div><p>都還在。LLM「給了」rm 指令、但沒人執行。</p>
<p><strong>執行路徑只有兩種</strong>：</p>
<ol>
<li><strong>人類 paste 到 shell</strong>：人是執行者、權限是 user&rsquo;s shell session permission。Audit trail：terminal history。</li>
<li><strong>Wrapper 程式 <code>subprocess.run(...)</code></strong>：wrapper 是執行者、權限是 wrapper process 的 capability。Audit trail：wrapper 的 log。</li>
</ol>
<p>LLM 永遠不是執行者。所以「LLM 寫了 rm -rf」這個句子不能成立——它只能「生成了 rm -rf 字串」。</p>
<p><strong>Agent 場景的 stake</strong>：<a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent 架構</a> 提到 agent loop = 「LLM 提議 → tool 執行 → 結果回 LLM → 下一輪」。Tool 執行那一步是 wrapper 做的、LLM 只看到結果。Agent 框架是否安全、完全看 tool 怎麼設計：</p>
<ul>
<li><strong>Tool 限制範圍</strong>：read-only file system access、不暴露 shell→ 即使 LLM 想跑 <code>rm -rf</code> 也沒對應 tool、無法執行。</li>
<li><strong>Tool 暴露 <code>bash</code> tool</strong>：給 LLM 一個「執行任意 shell command」的 tool。LLM 提議什麼 wrapper 都跑——這時 wrapper 設計失誤等同把鑰匙直接交給 LLM。</li>
<li><strong>Tool 暴露 <code>bash</code> tool + per-command confirm</strong>：每個 shell 呼叫前 wrapper 暫停、問人類「該不該執行」。對開發 / 探索環境合理、production 自動化流程會被互動卡住、不適用。</li>
</ul>
<h2 id="對照claude-code--cursor--aider-的權限模型">對照：Claude Code / Cursor / aider 的權限模型</h2>
<p>不同 LLM application 在權限 gate 上的設計選擇：</p>
<table>
  <thead>
      <tr>
          <th>Application</th>
          <th>File edit</th>
          <th>Shell exec</th>
          <th>預設審查粒度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Code（CLI）</td>
          <td>可、有 PreToolUse hook 可攔截</td>
          <td>可、有 hook</td>
          <td>中（部分自動、部分 prompt）</td>
      </tr>
      <tr>
          <td>Cursor</td>
          <td>可、agent mode</td>
          <td>可（agent terminal）</td>
          <td>中、agent 行為可調</td>
      </tr>
      <tr>
          <td>aider</td>
          <td>可、直接 diff + commit</td>
          <td>可（<code>--auto-commits</code> mode）</td>
          <td>中、預設 commit 前 diff</td>
      </tr>
      <tr>
          <td>Continue.dev</td>
          <td>inline edit（user 按 Cmd+;）</td>
          <td>不直接 exec</td>
          <td>高（user 必須 explicit）</td>
      </tr>
      <tr>
          <td>Open WebUI（純 chat）</td>
          <td>不</td>
          <td>不</td>
          <td>N/A（無 wrapper）</td>
      </tr>
      <tr>
          <td>自寫 wrapper（如本 demo）</td>
          <td>看設計</td>
          <td>看設計</td>
          <td>看設計</td>
      </tr>
  </tbody>
</table>
<p><strong>共通 pattern</strong>：所有「自動 edit / exec」的 app 都有某種 confirm 或 hook 機制。沒有 confirm 的 app 等於把寫 production 的鑰匙交給 LLM。</p>
<p><strong>選 application 時看的維度</strong>：</p>
<ul>
<li>預設 mode 是什麼？（auto / confirm / dry-run）</li>
<li>哪些動作會自動執行、哪些會 prompt？</li>
<li>有沒有 audit log、能不能 review LLM 改了什麼？</li>
<li>萬一 LLM 行為崩、怎麼 rollback？（git revert、snapshot、undo stack）</li>
</ul>
<h2 id="設計自家-wrapper-的權限模型">設計自家 wrapper 的權限模型</h2>
<p>如果你寫的是「LLM 自動處理 X」這種 wrapper、權限設計的 checklist：</p>
<ol>
<li><strong>副作用分級</strong>：把可能的動作分到 <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 spectrum 等級 1-5</a>。</li>
<li><strong>預設 dry-run</strong>：不確定就不寫。Apply 必須 opt-in。</li>
<li><strong>永遠印 diff / preview</strong>：用戶才能 catch LLM 亂改。</li>
<li><strong>Confirm 在不可逆操作</strong>：等級 3+ 永遠 prompt、等級 4+ 強制 prompt + 額外 audit。</li>
<li><strong>Audit log</strong>：每個 wrapper 動作寫 log（時間、user、action、result）。出問題能追溯。</li>
<li><strong>Rollback path</strong>：git commit、backup、snapshot 任選一種、必有。</li>
<li><strong>限制 tool 範圍</strong>：給 LLM 暴露最少 tool、不暴露 shell。需要 shell 限制白名單。</li>
<li><strong>小模型加更保守 gate</strong>：1B 模型亂改機率高、保留 <code>--dry-run</code> 或 <code>--confirm</code> 即可、避免 <code>--auto</code>；31B+ 較穩、可給 auto + audit。</li>
</ol>
<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 跑著、gemma3:1b 已 pull</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ollama list <span class="p">|</span> grep gemma3:1b
</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"># 備份要測試的檔案</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">cp content/llm/knowledge-cards/token.md /tmp/token-orig.md
</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"># Mode 1：dry-run（預設、最安全）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">python3 scripts/permission-demo/edit_with_llm.py <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  content/llm/knowledge-cards/token.md <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  <span class="s2">&#34;加一句說明&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># Mode 2：confirm（互動審查、適合中等風險）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">python3 scripts/permission-demo/edit_with_llm.py <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  content/llm/knowledge-cards/token.md <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  <span class="s2">&#34;加一句說明&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --confirm
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># Mode 3：auto（無確認、危險、僅 batch 用）</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">python3 scripts/permission-demo/edit_with_llm.py <span class="se">\
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="se"></span>  content/llm/knowledge-cards/token.md <span class="se">\
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="se"></span>  <span class="s2">&#34;加一句說明&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="se"></span>  --auto
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"># 還原</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">cp /tmp/token-orig.md content/llm/knowledge-cards/token.md</span></span></code></pre></div><h2 id="何時這篇會過時">何時這篇會過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>LLM HTTP API 是 pure function、無副作用——這個事實在所有「分離 inference server / wrapper / client」的架構都成立。</li>
<li>權限 gate 在 wrapper / application 層——是 software architecture invariant、不是 LLM 特性。</li>
<li>副作用範圍 spectrum 跟人類審查粒度的對應。</li>
<li><code>--dry-run</code> / <code>--confirm</code> / <code>--auto</code> 三檔的設計取捨。</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體 LLM application 的 default mode（Cursor / aider / Claude Code 都會持續調整）。</li>
<li>哪個模型「不會亂改」的 ranking（隨模型能力提升而變）。</li>
<li>MCP / tool spec 細節（會持續演化、但「tool 是 wrapper 暴露」的本質不變）。</li>
</ul>
<p>讀這篇若指令跑不過、可能是 wrapper script API 微調、但「測試 LLM 是不是 pure function」這個方法本身永遠成立——拿任何 LLM API、送任何 prompt、check 檔案 mtime / md5、就能驗證。</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>、副作用範圍 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 loop 跟人類審查的協作見 <a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent 架構</a>、Tool use / MCP server 權限模型的個人 dev 視角見 <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>、術語見 <a href="/blog/llm/knowledge-cards/sandbox/" data-link-title="Sandbox" data-link-desc="把程式跑在受限制環境的隔離技術、限制檔案 / 網路 / 系統呼叫權限、是 tool use 跟 MCP server 副作用控制的基礎">Sandbox</a>。</p>
]]></content:encoded></item><item><title>Hands-on：跨資料夾風格 follow 任務的模型對比</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/instruction-following-test/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/instruction-following-test/</guid><description>&lt;p>本篇是個讓本地 LLM 在「&lt;strong>讀兩個資料夾、學風格、寫新章節&lt;/strong>」任務上自我評估的實驗。任務本身內容無關緊要（隨便挑了一份私人創作資料夾）、要看的是&lt;strong>不同模型在 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/instruction-following/" data-link-title="Instruction Following" data-link-desc="模型遵守任務範圍、格式、限制與停止條件的能力，是評估 instruction-tuned 模型能否落地的核心訊號">instruction following&lt;/a> / format consistency / 篇幅控制三個維度的差距&lt;/strong>。&lt;/p>
&lt;p>實驗跑了四個本地模型對比：&lt;/p>
&lt;ul>
&lt;li>&lt;code>gemma3:1b&lt;/code>（815 MB、舊代 / 小）&lt;/li>
&lt;li>&lt;code>gemma3:4b&lt;/code>（3.3 GB、舊代 / 中）&lt;/li>
&lt;li>&lt;code>qwen3:8b&lt;/code>（5.2 GB、跨家族 / 大）&lt;/li>
&lt;li>&lt;code>gemma4:e4b&lt;/code>（9.6 GB、新代 / 中、bf16）&lt;/li>
&lt;/ul>
&lt;p>對應 &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 Agent 架構&lt;/a> 「規劃能力是雲端旗艦的明顯強項、本地小模型的明顯弱項」這條觀察、用具體 structural metrics 驗證、並揭示**「最新世代 + 較大 size」未必比「跨家族 / 較強訓練」勝出**。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>驗證日期&lt;/strong>：2026-05-12
&lt;strong>環境&lt;/strong>：Ollama 0.23.2、Apple Silicon、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/gpu-compute-backend/" data-link-title="GPU Compute Backend" data-link-desc="GPU 加速計算的底層 API 介面（CUDA / ROCm / Vulkan / Metal / SYCL）、決定推論軟體能否用 GPU 跑得快">MPS backend&lt;/a>
&lt;strong>任務&lt;/strong>：讀資料夾 A（風格參考、5 章已寫完）+ 資料夾 B（同類型、5 章已寫完、需寫 v06）→ 為 B 生成 v06
&lt;strong>評估方式&lt;/strong>：純 structural metrics、不評論內容品質&lt;/p>&lt;/blockquote>
&lt;h2 id="任務設計">任務設計&lt;/h2>
&lt;p>兩個資料夾結構：&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">A/ B/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">├── README.md ├── README.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">├── v01_XXX.md ├── v01_XXX.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">├── v02_XXX.md ├── v02_XXX.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">├── v03_XXX.md ├── v03_XXX.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">├── v04_XXX.md ├── v04_XXX.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">└── v05_XXX.md └── v05_XXX.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> └── v06_XXX.md ← 要生成&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個資料夾用&lt;strong>不同 markdown 格式&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>A 風格：&lt;code># 標題&lt;/code>（H1）+ &lt;code>## 場景設定&lt;/code> 段 + 結尾 &lt;code>**【本章結束】**&lt;/code>&lt;/li>
&lt;li>B 風格：&lt;code>## v0X｜&amp;lt;主題&amp;gt;（&amp;lt;角色1&amp;gt;×&amp;lt;角色2&amp;gt;）&lt;/code>（H2）+ 直接敘事、無結尾 marker&lt;/li>
&lt;/ul>
&lt;p>LLM 看完 A + B 後、要寫 B 的 v06——&lt;strong>必須 follow B 的格式、不是 A 的&lt;/strong>。是個 format discrimination 測試。&lt;/p>
&lt;h2 id="評估維度">評估維度&lt;/h2>
&lt;p>純 structural、不涉內容：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>測法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>篇幅控制&lt;/td>
 &lt;td>char count、跟 B 既有 v01-v05 平均比&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>段落結構&lt;/td>
 &lt;td>paragraph count、avg paragraph char&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Markdown heading&lt;/td>
 &lt;td>H1 / H2 count、是否寫對 v06 title 格式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結尾 marker&lt;/td>
 &lt;td>是否誤加 A 風格的「&lt;strong>【本章結束】&lt;/strong>」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>角色 fidelity&lt;/td>
 &lt;td>提到 B 兩個主角名次數（太少 = 內容偏離）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨資料夾串戲&lt;/td>
 &lt;td>提到 A 資料夾角色名次數（contamination）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對話 follow&lt;/td>
 &lt;td>「對話行」（行首是 &lt;code>「&lt;/code>）數量、跟 baseline 比&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生成時間&lt;/td>
 &lt;td>從送 prompt 到收完整 response&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不評估的：&lt;/p></description><content:encoded><![CDATA[<p>本篇是個讓本地 LLM 在「<strong>讀兩個資料夾、學風格、寫新章節</strong>」任務上自我評估的實驗。任務本身內容無關緊要（隨便挑了一份私人創作資料夾）、要看的是<strong>不同模型在 <a href="/blog/llm/knowledge-cards/instruction-following/" data-link-title="Instruction Following" data-link-desc="模型遵守任務範圍、格式、限制與停止條件的能力，是評估 instruction-tuned 模型能否落地的核心訊號">instruction following</a> / format consistency / 篇幅控制三個維度的差距</strong>。</p>
<p>實驗跑了四個本地模型對比：</p>
<ul>
<li><code>gemma3:1b</code>（815 MB、舊代 / 小）</li>
<li><code>gemma3:4b</code>（3.3 GB、舊代 / 中）</li>
<li><code>qwen3:8b</code>（5.2 GB、跨家族 / 大）</li>
<li><code>gemma4:e4b</code>（9.6 GB、新代 / 中、bf16）</li>
</ul>
<p>對應 <a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent 架構</a> 「規劃能力是雲端旗艦的明顯強項、本地小模型的明顯弱項」這條觀察、用具體 structural metrics 驗證、並揭示**「最新世代 + 較大 size」未必比「跨家族 / 較強訓練」勝出**。</p>
<blockquote>
<p><strong>驗證日期</strong>：2026-05-12
<strong>環境</strong>：Ollama 0.23.2、Apple Silicon、<a href="/blog/llm/knowledge-cards/gpu-compute-backend/" data-link-title="GPU Compute Backend" data-link-desc="GPU 加速計算的底層 API 介面（CUDA / ROCm / Vulkan / Metal / SYCL）、決定推論軟體能否用 GPU 跑得快">MPS backend</a>
<strong>任務</strong>：讀資料夾 A（風格參考、5 章已寫完）+ 資料夾 B（同類型、5 章已寫完、需寫 v06）→ 為 B 生成 v06
<strong>評估方式</strong>：純 structural metrics、不評論內容品質</p></blockquote>
<h2 id="任務設計">任務設計</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">A/                          B/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── README.md               ├── README.md
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── v01_XXX.md              ├── v01_XXX.md
</span></span><span class="line"><span class="ln">4</span><span class="cl">├── v02_XXX.md              ├── v02_XXX.md
</span></span><span class="line"><span class="ln">5</span><span class="cl">├── v03_XXX.md              ├── v03_XXX.md
</span></span><span class="line"><span class="ln">6</span><span class="cl">├── v04_XXX.md              ├── v04_XXX.md
</span></span><span class="line"><span class="ln">7</span><span class="cl">└── v05_XXX.md              └── v05_XXX.md
</span></span><span class="line"><span class="ln">8</span><span class="cl">                            └── v06_XXX.md  ← 要生成</span></span></code></pre></div><p>兩個資料夾用<strong>不同 markdown 格式</strong>：</p>
<ul>
<li>A 風格：<code># 標題</code>（H1）+ <code>## 場景設定</code> 段 + 結尾 <code>**【本章結束】**</code></li>
<li>B 風格：<code>## v0X｜&lt;主題&gt;（&lt;角色1&gt;×&lt;角色2&gt;）</code>（H2）+ 直接敘事、無結尾 marker</li>
</ul>
<p>LLM 看完 A + B 後、要寫 B 的 v06——<strong>必須 follow B 的格式、不是 A 的</strong>。是個 format discrimination 測試。</p>
<h2 id="評估維度">評估維度</h2>
<p>純 structural、不涉內容：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>測法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>篇幅控制</td>
          <td>char count、跟 B 既有 v01-v05 平均比</td>
      </tr>
      <tr>
          <td>段落結構</td>
          <td>paragraph count、avg paragraph char</td>
      </tr>
      <tr>
          <td>Markdown heading</td>
          <td>H1 / H2 count、是否寫對 v06 title 格式</td>
      </tr>
      <tr>
          <td>結尾 marker</td>
          <td>是否誤加 A 風格的「<strong>【本章結束】</strong>」</td>
      </tr>
      <tr>
          <td>角色 fidelity</td>
          <td>提到 B 兩個主角名次數（太少 = 內容偏離）</td>
      </tr>
      <tr>
          <td>跨資料夾串戲</td>
          <td>提到 A 資料夾角色名次數（contamination）</td>
      </tr>
      <tr>
          <td>對話 follow</td>
          <td>「對話行」（行首是 <code>「</code>）數量、跟 baseline 比</td>
      </tr>
      <tr>
          <td>生成時間</td>
          <td>從送 prompt 到收完整 response</td>
      </tr>
  </tbody>
</table>
<p>不評估的：</p>
<ul>
<li>內容品質、文筆好壞</li>
<li>敘事邏輯是否合理</li>
<li>角色塑造是否生動</li>
</ul>
<p>純 structural 評估的好處是 reproducible、不需 reviewer 主觀判斷、可自動跑。</p>
<h2 id="baselineb-既有-v01-v05-的-metrics">Baseline：B 既有 v01-v05 的 metrics</h2>
<p>B 資料夾 5 個既有章節的平均：</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>Average</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>char count</td>
          <td>~933</td>
      </tr>
      <tr>
          <td>paragraph count</td>
          <td>~32</td>
      </tr>
      <tr>
          <td>avg paragraph chars</td>
          <td>~29</td>
      </tr>
      <tr>
          <td>dialogue lines</td>
          <td>~7</td>
      </tr>
      <tr>
          <td>H1 used</td>
          <td>0（全部用 H2）</td>
      </tr>
      <tr>
          <td>H2 used</td>
          <td>1</td>
      </tr>
      <tr>
          <td>結尾「<strong>【本章結束】</strong>」</td>
          <td>全部 False</td>
      </tr>
      <tr>
          <td>Cross leak</td>
          <td>全部 0</td>
      </tr>
      <tr>
          <td>主角名提及（合計）</td>
          <td>~60</td>
      </tr>
  </tbody>
</table>
<p>這是 LLM 該模仿的目標。</p>
<h2 id="四個模型的結果">四個模型的結果</h2>
<p>四個 model 跑同樣 prompt、同樣輸入內容。</p>
<h3 id="對比表">對比表</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Baseline</th>
          <th><code>gemma3:1b</code></th>
          <th><code>gemma3:4b</code></th>
          <th><code>qwen3:8b</code></th>
          <th><code>gemma4:e4b</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>模型大小</strong></td>
          <td>—</td>
          <td>815 MB</td>
          <td>3.3 GB</td>
          <td>5.2 GB</td>
          <td>9.6 GB（bf16）</td>
      </tr>
      <tr>
          <td><strong>發布世代</strong></td>
          <td>—</td>
          <td>Gemma 3</td>
          <td>Gemma 3</td>
          <td>Qwen 3</td>
          <td><strong>Gemma 4（2026/4）</strong></td>
      </tr>
      <tr>
          <td>char count</td>
          <td>~933</td>
          <td>4324（4.6×）</td>
          <td>1330</td>
          <td><strong>951（1.02×）</strong></td>
          <td>679</td>
      </tr>
      <tr>
          <td>paragraph count</td>
          <td>~32</td>
          <td>145</td>
          <td>29</td>
          <td><strong>36</strong></td>
          <td>11</td>
      </tr>
      <tr>
          <td>avg paragraph chars</td>
          <td>~29</td>
          <td>30</td>
          <td>46</td>
          <td><strong>26</strong></td>
          <td>62</td>
      </tr>
      <tr>
          <td>H1 = 0</td>
          <td>符合</td>
          <td>不符（1）</td>
          <td>符合</td>
          <td>符合</td>
          <td>不符（1）</td>
      </tr>
      <tr>
          <td>H2 = 1</td>
          <td>符合</td>
          <td>不符（0）</td>
          <td>符合</td>
          <td>符合</td>
          <td>不符（3）</td>
      </tr>
      <tr>
          <td>v06 title 格式</td>
          <td>—</td>
          <td>不符</td>
          <td>符合</td>
          <td>符合</td>
          <td>不符</td>
      </tr>
      <tr>
          <td>結尾 marker</td>
          <td>False</td>
          <td>符合</td>
          <td>符合</td>
          <td>符合</td>
          <td>符合</td>
      </tr>
      <tr>
          <td>Cross leak</td>
          <td>0</td>
          <td>無（0）</td>
          <td>無（0）</td>
          <td>無（0）</td>
          <td>無（0）</td>
      </tr>
      <tr>
          <td>dialogue lines</td>
          <td>~7</td>
          <td>4</td>
          <td><strong>0</strong></td>
          <td><strong>7</strong></td>
          <td>0</td>
      </tr>
      <tr>
          <td>主角名提及（合計）</td>
          <td>~60</td>
          <td>286</td>
          <td>24</td>
          <td><strong>27</strong></td>
          <td><strong>0</strong></td>
      </tr>
      <tr>
          <td><strong>通過項目</strong></td>
          <td>—</td>
          <td><strong>2 / 7</strong></td>
          <td><strong>6 / 7</strong></td>
          <td><strong>7 / 7</strong></td>
          <td><strong>1 / 7</strong></td>
      </tr>
      <tr>
          <td>生成時間</td>
          <td>—</td>
          <td>41.8s</td>
          <td>36.5s</td>
          <td>97.5s</td>
          <td>43.5s</td>
      </tr>
  </tbody>
</table>
<h3 id="各模型觀察">各模型觀察</h3>
<p><strong><code>gemma3:1b</code>（815 MB）</strong>：</p>
<ul>
<li>篇幅 4.6× 失控、段落數 4.5× 超標、用 H1 而不是 H2。</li>
<li>顯示 1B 模型對「2000-3000 字」這種 numeric instruction 沒有有效執行能力、會一直生成到 context 限制。</li>
<li>但 cross leak 0、結尾 marker 也沒誤加——「不要 X」這類 negative instruction follow 較成功。</li>
</ul>
<p><strong><code>gemma3:4b</code>（3.3 GB）</strong>：</p>
<ul>
<li>篇幅 / 段落 / heading 結構全 OK、明顯比 1B 大幅改善。</li>
<li><strong>dialogue lines = 0</strong>：完全沒寫對話、整篇純敘事。表示 4B 抓到字面 structural feature、但沒抓到「對話 driven 敘事」這個 stylistic feature。</li>
<li>主角名提及 24 次（baseline ~60）—內容偏短、提及次數偏低、但比例合理。</li>
</ul>
<p><strong><code>qwen3:8b</code>（5.2 GB、跨家族）</strong>：</p>
<ul>
<li><strong>唯一 7/7 全 pass 的模型</strong>——篇幅完美匹配（951 vs ~933）、段落數合理（36 vs ~32）、heading 對、對話 7 行完全等於 baseline。</li>
<li>跨家族 + 大一級的組合表現質變，比同家族下一級的 4B 模型大幅提升。</li>
<li>代價：生成時間 97.5s、約是 4B 模型的 2.7×。</li>
</ul>
<p><strong><code>gemma4:e4b</code>（9.6 GB、新代）</strong>：</p>
<ul>
<li><strong>驚人的 1/7、最差表現</strong>——比 1B 還少通過項目。</li>
<li><strong>主角名提及 0</strong>：完全沒寫角色名、純抽象敘述「某一方」「另一方」。</li>
<li><strong>dialogue 0</strong>：沒對話。</li>
<li><strong>生成內容是「劇情大綱建議」而非實際章節</strong>：含「劇情核心思路」「預計情緒強度」「寫作切入點建議」等 meta-text。</li>
<li>輸出末尾「<strong>（此為結構化建議、等待具體的指令後、將會生成與風格一致的劇情內容。）</strong>」——明示它把 prompt 理解成「給建議框架、等下一步」。</li>
</ul>
<h3 id="strict-prompt-retest揭示-internal-alignment">Strict prompt retest：揭示 internal alignment</h3>
<p>懷疑 1/7 可能是「prompt 不夠強硬」、用 strict prompt 重跑 <code>gemma4:e4b</code>。Strict 加了八條規則、明示：</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">- 直接從 `## v06｜...` 開頭、不寫前言
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 絕對不可寫「劇情核心思路」「預計情緒強度」「寫作切入點」等 meta-text
</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></span><span class="line"><span class="ln">5</span><span class="cl">- ...</span></span></code></pre></div><p>Strict prompt 結果：</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>原 prompt</th>
          <th>strict prompt</th>
          <th>變化</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>char count</td>
          <td>679</td>
          <td>660</td>
          <td>相同量級</td>
      </tr>
      <tr>
          <td>H1 = 0</td>
          <td>不符（1）</td>
          <td>符合</td>
          <td><strong>改善</strong></td>
      </tr>
      <tr>
          <td>H2 = 1</td>
          <td>不符（3）</td>
          <td>符合</td>
          <td><strong>改善</strong></td>
      </tr>
      <tr>
          <td>v06 title 格式</td>
          <td>不符</td>
          <td>符合</td>
          <td><strong>改善</strong></td>
      </tr>
      <tr>
          <td>meta-text 出現</td>
          <td>有</td>
          <td>無</td>
          <td><strong>改善</strong></td>
      </tr>
      <tr>
          <td>dialogue lines</td>
          <td>0</td>
          <td>3</td>
          <td><strong>改善</strong></td>
      </tr>
      <tr>
          <td><strong>主角名提及</strong></td>
          <td><strong>0</strong></td>
          <td><strong>0</strong></td>
          <td><strong>未改善</strong></td>
      </tr>
      <tr>
          <td><strong>通過項目</strong></td>
          <td><strong>1 / 7</strong></td>
          <td><strong>4 / 7</strong></td>
          <td><strong>+3</strong></td>
      </tr>
  </tbody>
</table>
<p>從 1/7 → 4/7、prompt 強化明顯有用。但<strong>主角名提及兩次都 0</strong>、即使 strict prompt 明示「強制提到角色名」、模型仍用「兩人」「彼此」「對方」抽象稱呼。</p>
<p>這比「模型不會 follow」更精確、是兩個層次的 follow 差別：</p>
<ul>
<li><strong>Surface level instruction</strong>（heading 格式、不要 meta-text、要對話）：model 願意 follow strict prompt。</li>
<li><strong>Semantic level instruction</strong>（在這個情境用具名角色）：model 有 <strong>internal alignment 抗拒</strong>、即使 prompt 明示也不 follow。</li>
</ul>
<p>Gemma 4 e4b 是 device-deployable edge variant、RLHF 可能特別針對「敏感情境下的人物識別」做 alignment。這個 alignment 比 prompt-level instruction follow 更深、是 hard line、不能用 prompt engineering 繞過。</p>
<h2 id="關鍵觀察">關鍵觀察</h2>
<h3 id="model-size-不是唯一因素訓練-alignment-更重要">Model size 不是唯一因素、訓練 alignment 更重要</h3>
<p>最反直覺的結果：</p>
<ul>
<li><code>gemma4:e4b</code>（9.6 GB、最新世代）原 prompt 通過 <strong>1/7</strong>、strict prompt 通過 <strong>4/7</strong>。</li>
<li><code>gemma3:4b</code>（3.3 GB、舊一代）通過 <strong>6/7</strong>。</li>
<li><code>qwen3:8b</code>（5.2 GB、跨家族）通過 <strong>7/7</strong>。</li>
</ul>
<p>「最大 + 最新」不等於「最好 follow instruction」。在這個任務上、ranking 是：</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">qwen3:8b &gt; gemma3:4b &gt; gemma3:1b ≈ gemma4:e4b (strict) &gt; gemma4:e4b (default)</span></span></code></pre></div><p>可能因素：</p>
<ol>
<li><strong>訓練資料分佈差異</strong>：Qwen 系列訓練資料含大量中文、對中文 instruction follow 更穩。</li>
<li><strong>Edge variant 的 alignment 設計</strong>：<code>gemma4:e4b</code> 是 device-deployable edge variant、RLHF 可能特別在敏感情境用 conservative output。Strict prompt 能改善 surface-level（heading、meta-text、對話）、但 semantic-level（具名角色）有 hard line 不能繞過。</li>
<li><strong>跨家族效應 &gt; 跨代效應</strong>：Qwen vs Gemma（不同家族）比 Gemma 3 vs Gemma 4（同家族跨代）影響更大。</li>
</ol>
<h3 id="兩層-instruction-follow">兩層 instruction follow</h3>
<p><code>gemma4:e4b</code> 的 strict prompt retest 揭示一個重要區分：</p>
<ul>
<li><strong>Surface-level instruction</strong>（heading 格式、不要 meta-text、要對話）：可以用 strict prompt 改善、prompt engineering 有效。</li>
<li><strong>Semantic-level alignment</strong>（特定情境的角色處理、敏感主題的表述方式）：是 RLHF 階段建立的 hard line、prompt engineering 繞不過。</li>
</ul>
<p>設計應用時要意識：<strong>「LLM follow 不了 instruction」可能不是能力問題、是 alignment 問題</strong>。模型訓練時被刻意 align 不做某些事、即使 prompt 明示也不會做。發現這種情況、改換 model（或 less-aligned variant）會比繼續調 prompt 更省時間。</p>
<h3 id="最新世代的標籤可能誤導">「最新世代」的標籤可能誤導</h3>
<p>Gemma 4 是 2026/4/2 才發布的最新代、size 也夠大、但在這個 instruction following 任務上<strong>輸給 6 個月前發布的 Gemma 3 4b</strong>。</p>
<p>設計應用 / 選模型時、實測對自己 task 的表現比「最新 / 最大」標籤可靠。Benchmark ranking（如 LMSYS Chatbot Arena）反映平均表現、未必 reflect 你的 narrow 任務。本實驗示範了「自己跑一次」比「看 benchmark」更可靠的判讀方法。</p>
<h3 id="structural-feature-跟-stylistic-feature-兩層">Structural feature 跟 stylistic feature 兩層</h3>
<p>跨四個模型一致觀察：</p>
<ul>
<li><strong>Structural feature</strong>（heading level、結尾 marker、不要 cross leak）：所有模型多少都抓到。</li>
<li><strong>Stylistic feature</strong>（對話 driven 敘事、篇幅精準）：差異極大、Qwen3 8B 完美、其他三個都有明顯失分。</li>
</ul>
<p>這對應 <a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent</a> 的「規劃 vs 字面 follow」差距——字面 instruction 容易、stylistic mimic 困難。寫應用時、預期 follow「形式約束」（output JSON、結尾 signature）跟 follow「風格約束」（用簡潔口吻、bullet 而非段落）兩種 instruction 的成功率不同。</p>
<h3 id="cross-pairing-leak全-0">Cross-pairing leak：全 0</h3>
<p>四個模型 cross leak 都 0——表示「不要混角色」這個 instruction 兩個都 follow 成功。可能因素：</p>
<ul>
<li>角色名是名詞、模型 generation 時容易 constrain。</li>
<li>Prompt 已明示「為 B 寫」、模型沒被 A 角色名干擾。</li>
</ul>
<p>如果改成模糊 instruction（「混合 A、B 風格」）、leak 可能會出現——本實驗沒涵蓋這個 case。</p>
<h3 id="生成時間size--時間">生成時間：size ≠ 時間</h3>
<p>四個模型的生成時間：</p>
<table>
  <thead>
      <tr>
          <th>模型</th>
          <th>size</th>
          <th>時間</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>gemma3:1b</td>
          <td>815 MB</td>
          <td>41.8s</td>
      </tr>
      <tr>
          <td>gemma3:4b</td>
          <td>3.3 GB</td>
          <td>36.5s</td>
      </tr>
      <tr>
          <td>qwen3:8b</td>
          <td>5.2 GB</td>
          <td><strong>97.5s</strong></td>
      </tr>
      <tr>
          <td>gemma4:e4b</td>
          <td>9.6 GB</td>
          <td>43.5s</td>
      </tr>
  </tbody>
</table>
<p>意外發現：</p>
<ol>
<li><strong>1B 比 4B 慢</strong>：因為 1B 生成 4324 字、4B 生成 1330 字、總 token 量決定總時間、不是 model size。</li>
<li><strong>qwen3:8b 慢 2.7×</strong>：8B 的 forward pass 較慢、加上 generation 量級正常、總時間最長。</li>
<li><strong>gemma4:e4b 跟 1B 相近</strong>：generation 短（679 字）、抵消 model 較大的開銷。</li>
</ol>
<p><a href="/blog/llm/knowledge-cards/tokens-per-second/" data-link-title="Tokens Per Second" data-link-desc="LLM 每秒能生成幾個 token：生字速度的標準量化指標">tokens per second</a> 跟 total latency 是兩件事——decode 速度快但生成太多 token、未必更快完成任務。</p>
<h2 id="對寫應用的啟示">對寫應用的啟示</h2>
<ol>
<li><strong>「最新最大」≠ 「最好 follow」</strong>：選模型實測自己 task、benchmark / size 只是輔助訊號。</li>
<li><strong>本地小模型（&lt; 3B）做需要 follow 結構規則的任務、要嚴格驗證</strong>：用 structural metrics 自動 check、目視判斷模型「看起來有做到」的可靠度低。</li>
<li><strong>Edge variant 可能有 special behavior</strong>：device-deployable variant 可能 RLHF 偏向 conservative、不一定適合所有任務。</li>
<li><strong>跨家族對比比同家族升 size 收益大</strong>：Qwen3 8B vs Gemma3 4B 比 Gemma3 4B vs Gemma3 1B 改善更明顯。</li>
<li><strong>「形式跟風格」分開驗證</strong>：應用層的 validation 分維度 score、比一次評全部更可解讀。</li>
</ol>
<h2 id="跑這個實驗的-framework">跑這個實驗的 framework</h2>
<p>通用流程（不放具體 script、會綁定 corpus 內容）：</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">1. 準備兩個資料夾、A 是風格參考、B 是 work-in-progress
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 寫 helper script 把兩個資料夾完整內容 + 任務說明做成 prompt
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 跑多個 model 各一次（同 prompt、不同 model）
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 對輸出計算 structural metrics（char count、paragraph、heading、dialogue lines）
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. 跟 B 既有章節的 baseline metrics 對比
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. 列通過 / 失敗矩陣</span></span></code></pre></div><p>關鍵設計選擇：</p>
<ul>
<li><strong>A 跟 B 風格故意不一樣</strong>：才能驗證 LLM 是否分辨「該 follow 哪個」。</li>
<li><strong>不評估內容品質</strong>：純 structural 評估 reproducible、不需 reviewer 主觀判斷。</li>
<li><strong>baseline 用既有章節算</strong>：B 自己的 v01-v05 是「正確答案」的 reference。</li>
<li><strong>跑多個跨家族 / 跨世代 / 跨 size 模型</strong>：避免「只測一個就下結論」的偏差。</li>
</ul>
<h2 id="何時這份對比會過時">何時這份對比會過時</h2>
<ul>
<li><strong>具體模型 ranking</strong>：新模型發布後 ranking 會變、特別是新版 Gemma 4 / Qwen 4 / Llama 4 等推出時。</li>
<li><strong>「Gemma 4 edge 表現差」這個觀察</strong>：可能隨後續 fine-tune 或新版改善。</li>
</ul>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>Model size 不是 instruction following 的唯一因素——這個現象在所有 LLM 都存在。</li>
<li>Structural vs stylistic 兩層 follow 難度不同。</li>
<li>跨家族對比比同家族升 size 收益大、這個現象可能持續。</li>
<li>純 metrics-based 評估比主觀判斷可重現。</li>
<li>「自己跑一次」比「看 benchmark」更可靠的判讀邏輯。</li>
</ul>
<p>未來想擴展、可以加入更多維度（如反向 retrieval：把生成內容當 query、看能不能找回原資料夾；或 perplexity-based 評估）。</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/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">Model selection priority</a>、模型 tag 命名規則見 <a href="/blog/llm/knowledge-cards/model-tag/" data-link-title="Model Tag" data-link-desc="Ollama 等推論伺服器用來定位特定模型版本的命名規則">Model tag</a>、跑多模型的記憶體預算見 <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>。</p>
]]></content:encoded></item><item><title>Hands-on：LLM 運行中 + 結束的資源管理</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/resource-management/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/resource-management/</guid><description>&lt;p>跑本地 LLM 的核心 invariant 跟雲端不一樣：&lt;strong>Mac 是 shared resource、不是 dedicated GPU&lt;/strong>。雲端 inference server 跑進 dedicated container、結束 instance 自然回收所有資源；本地&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>跑在你日常用的 Mac、跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/unified-memory/" data-link-title="Unified Memory Architecture" data-link-desc="Apple Silicon 讓 CPU / GPU / NE 共用同一塊記憶體：跑大模型的優勢來源">統一記憶體&lt;/a> 共享同一塊容量，忘記管理會 silently 吃光 RAM、磁碟、port、最後讓系統變慢甚至 swap。&lt;/p>
&lt;p>本篇紀錄三個 dimension（RAM / 磁碟 / port）的觀察工具跟釋放姿勢、對比 Ollama 跟 ComfyUI 兩種典型 lifecycle、加上實測釋放數字。對應 &lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理&lt;/a>「每個 hop 都要 audit」這條思維——資源管理也是 hop 級的 audit、不是「裝完就忘」。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>驗證日期&lt;/strong>：2026-05-12
&lt;strong>環境&lt;/strong>：macOS 14、Apple Silicon、Ollama 0.23.2、ComfyUI 0.21.0、SDXL base 1.0&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼這事重要">為什麼這事重要&lt;/h2>
&lt;p>雲端 inference：&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">Container start → load model → serve requests → container stop → 所有 RAM / 磁碟 / port 自動回收&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>本地 inference：&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">brew services start → load model on demand → serve → ??? → 你忘記 stop
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> → RAM / 磁碟一直被佔
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> → 下次重開機才釋放&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>具體會踩到的問題：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>RAM&lt;/strong>：18 GB SDXL 模型載入後不會自動卸、即使 ComfyUI idle、Python process 仍占 RAM&lt;/li>
&lt;li>&lt;strong>磁碟&lt;/strong>：&lt;code>ollama pull&lt;/code> 累積、&lt;code>~/.ollama/models/blobs&lt;/code> 半年可長到 50 GB+、不主動清不會減&lt;/li>
&lt;li>&lt;strong>Port&lt;/strong>：上次 crash 的 &lt;code>ollama serve&lt;/code> 進程沒乾淨清、port 11434 還占著、下次啟動報「address already in use」&lt;/li>
&lt;li>&lt;strong>GPU / Metal&lt;/strong>：模型載入後 Metal context 佔住、跟其他 GPU-using app（影片剪輯、遊戲）競爭&lt;/li>
&lt;/ul>
&lt;h2 id="三個-dimension--觀察工具">三個 dimension + 觀察工具&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Dimension&lt;/th>
 &lt;th>觀察指令&lt;/th>
 &lt;th>看什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>RAM&lt;/td>
 &lt;td>&lt;code>vm_stat | head -5&lt;/code>&lt;/td>
 &lt;td>Pages free（每 page 16 KB）、空閒越多越好&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RAM（per process）&lt;/td>
 &lt;td>Activity Monitor 或 &lt;code>ps aux | sort -k6 -rn | head&lt;/code>&lt;/td>
 &lt;td>哪個 process 佔最多記憶體&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>磁碟&lt;/td>
 &lt;td>&lt;code>df -h ~ | tail -1&lt;/code>&lt;/td>
 &lt;td>系統 volume 剩餘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>磁碟（per dir）&lt;/td>
 &lt;td>&lt;code>du -sh ~/.ollama/models/blobs&lt;/code>&lt;/td>
 &lt;td>LLM models 累積量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Port&lt;/td>
 &lt;td>&lt;code>lsof -i :11434&lt;/code>&lt;/td>
 &lt;td>誰在 listen 該 port&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Process&lt;/td>
 &lt;td>&lt;code>ps aux | grep -i ollama | grep -v grep&lt;/code>&lt;/td>
 &lt;td>Ollama / ComfyUI / Python 跑哪幾個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ollama loaded models&lt;/td>
 &lt;td>&lt;code>ollama ps&lt;/code>&lt;/td>
 &lt;td>哪些 model 在 RAM、size、idle timer&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>實測：剛 kill 完 ComfyUI（SDXL + Python venv）後、&lt;code>vm_stat&lt;/code> 看到 free pages 從 619K 變 1090K（每 page 16 KB）、約 &lt;strong>+7.5 GB RAM 釋放&lt;/strong>——這就是 SDXL + ComfyUI process 一直占的記憶體量。&lt;/p></description><content:encoded><![CDATA[<p>跑本地 LLM 的核心 invariant 跟雲端不一樣：<strong>Mac 是 shared resource、不是 dedicated GPU</strong>。雲端 inference server 跑進 dedicated container、結束 instance 自然回收所有資源；本地<a href="/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器</a>跑在你日常用的 Mac、跟 <a href="/blog/llm/knowledge-cards/unified-memory/" data-link-title="Unified Memory Architecture" data-link-desc="Apple Silicon 讓 CPU / GPU / NE 共用同一塊記憶體：跑大模型的優勢來源">統一記憶體</a> 共享同一塊容量，忘記管理會 silently 吃光 RAM、磁碟、port、最後讓系統變慢甚至 swap。</p>
<p>本篇紀錄三個 dimension（RAM / 磁碟 / port）的觀察工具跟釋放姿勢、對比 Ollama 跟 ComfyUI 兩種典型 lifecycle、加上實測釋放數字。對應 <a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理</a>「每個 hop 都要 audit」這條思維——資源管理也是 hop 級的 audit、不是「裝完就忘」。</p>
<blockquote>
<p><strong>驗證日期</strong>：2026-05-12
<strong>環境</strong>：macOS 14、Apple Silicon、Ollama 0.23.2、ComfyUI 0.21.0、SDXL base 1.0</p></blockquote>
<h2 id="為什麼這事重要">為什麼這事重要</h2>
<p>雲端 inference：</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">Container start → load model → serve requests → container stop → 所有 RAM / 磁碟 / port 自動回收</span></span></code></pre></div><p>本地 inference：</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">brew services start → load model on demand → serve → ??? → 你忘記 stop
</span></span><span class="line"><span class="ln">2</span><span class="cl">                                              → RAM / 磁碟一直被佔
</span></span><span class="line"><span class="ln">3</span><span class="cl">                                              → 下次重開機才釋放</span></span></code></pre></div><p>具體會踩到的問題：</p>
<ul>
<li><strong>RAM</strong>：18 GB SDXL 模型載入後不會自動卸、即使 ComfyUI idle、Python process 仍占 RAM</li>
<li><strong>磁碟</strong>：<code>ollama pull</code> 累積、<code>~/.ollama/models/blobs</code> 半年可長到 50 GB+、不主動清不會減</li>
<li><strong>Port</strong>：上次 crash 的 <code>ollama serve</code> 進程沒乾淨清、port 11434 還占著、下次啟動報「address already in use」</li>
<li><strong>GPU / Metal</strong>：模型載入後 Metal context 佔住、跟其他 GPU-using app（影片剪輯、遊戲）競爭</li>
</ul>
<h2 id="三個-dimension--觀察工具">三個 dimension + 觀察工具</h2>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th>觀察指令</th>
          <th>看什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RAM</td>
          <td><code>vm_stat | head -5</code></td>
          <td>Pages free（每 page 16 KB）、空閒越多越好</td>
      </tr>
      <tr>
          <td>RAM（per process）</td>
          <td>Activity Monitor 或 <code>ps aux | sort -k6 -rn | head</code></td>
          <td>哪個 process 佔最多記憶體</td>
      </tr>
      <tr>
          <td>磁碟</td>
          <td><code>df -h ~ | tail -1</code></td>
          <td>系統 volume 剩餘</td>
      </tr>
      <tr>
          <td>磁碟（per dir）</td>
          <td><code>du -sh ~/.ollama/models/blobs</code></td>
          <td>LLM models 累積量</td>
      </tr>
      <tr>
          <td>Port</td>
          <td><code>lsof -i :11434</code></td>
          <td>誰在 listen 該 port</td>
      </tr>
      <tr>
          <td>Process</td>
          <td><code>ps aux | grep -i ollama | grep -v grep</code></td>
          <td>Ollama / ComfyUI / Python 跑哪幾個</td>
      </tr>
      <tr>
          <td>Ollama loaded models</td>
          <td><code>ollama ps</code></td>
          <td>哪些 model 在 RAM、size、idle timer</td>
      </tr>
  </tbody>
</table>
<p>實測：剛 kill 完 ComfyUI（SDXL + Python venv）後、<code>vm_stat</code> 看到 free pages 從 619K 變 1090K（每 page 16 KB）、約 <strong>+7.5 GB RAM 釋放</strong>——這就是 SDXL + ComfyUI process 一直占的記憶體量。</p>
<h2 id="ollama-的-lifecycleauto-unload-模式">Ollama 的 lifecycle（auto-unload 模式）</h2>
<p>Ollama 走「按需 load / idle unload」設計：</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">brew services start ollama          → daemon 啟動、沒 model 載入、RAM 占用 ~200 MB
</span></span><span class="line"><span class="ln">2</span><span class="cl">                                     port 11434 listening
</span></span><span class="line"><span class="ln">3</span><span class="cl">ollama run gemma3:4b &#34;hello&#34;        → 把 model 載入 RAM (~4-5 GB)
</span></span><span class="line"><span class="ln">4</span><span class="cl">                                     立刻 generate response
</span></span><span class="line"><span class="ln">5</span><span class="cl">                                     model 留在 RAM
</span></span><span class="line"><span class="ln">6</span><span class="cl">(idle 5 分鐘、無新 request)         → Ollama 自動 unload model
</span></span><span class="line"><span class="ln">7</span><span class="cl">                                     RAM 釋放、daemon 仍跑著
</span></span><span class="line"><span class="ln">8</span><span class="cl">ollama run gemma3:4b &#34;next&#34;         → 重新 load model（~5-10 秒）、generate
</span></span><span class="line"><span class="ln">9</span><span class="cl">brew services stop ollama           → daemon 結束、port 釋放</span></span></code></pre></div><p><strong>關鍵參數 <code>OLLAMA_KEEP_ALIVE</code></strong>（環境變數、預設 <code>5m</code>）：</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"># 看當前 loaded models</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         ID              SIZE      PROCESSOR    UNTIL</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># gemma3:4b    a2af6cc3eb7f    5.5 GB    100% Metal   4 minutes from now</span>
</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 class="c1"># 啟動時調 keep_alive（持續佔 RAM 直到 ollama 重啟）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nv">OLLAMA_KEEP_ALIVE</span><span class="o">=</span>-1 brew services restart ollama
</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"># 啟動時讓 model 用完立即 unload</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">OLLAMA_KEEP_ALIVE</span><span class="o">=</span><span class="m">0</span> brew services restart ollama</span></span></code></pre></div><p>選 keep_alive 的 trade-off：</p>
<table>
  <thead>
      <tr>
          <th>設定</th>
          <th>RAM 占用</th>
          <th>首字延遲</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>0</code></td>
          <td>最低（generate 完立即釋放）</td>
          <td>高（每次都重 load）</td>
          <td>偶爾用、RAM 緊張</td>
      </tr>
      <tr>
          <td><code>5m</code>（預設）</td>
          <td>中（活躍用占住、閒 5 分鐘後釋放）</td>
          <td>低（活躍期不重 load）</td>
          <td>大多場景</td>
      </tr>
      <tr>
          <td><code>-1</code></td>
          <td>高（永久占住）</td>
          <td>最低</td>
          <td>整天頻繁用、RAM 充裕</td>
      </tr>
  </tbody>
</table>
<p><strong>主動 unload 指令</strong>：</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"># 把 idle 的 model 立刻從 RAM 卸掉、但 daemon 仍跑</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl -s http://localhost:11434/api/generate <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -d <span class="s1">&#39;{&#34;model&#34;: &#34;gemma3:4b&#34;, &#34;keep_alive&#34;: 0}&#39;</span>
</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"><span class="c1"># 或關掉整個 daemon</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">brew services stop ollama</span></span></code></pre></div><h2 id="comfyui-的-lifecycle持續占用模式">ComfyUI 的 lifecycle（持續占用模式）</h2>
<p>ComfyUI 走完全不同模式：<strong>model 載入後一直在 RAM、直到 server process 結束</strong>。沒有 auto-unload 機制。</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">python main.py                      → ComfyUI server start、port 8188 listening
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">                                     RAM ~3 GB（Python venv + 框架）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">第一次 Queue Prompt (用 SDXL)        → 載入 sd_xl_base_1.0.safetensors (~6 GB)
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">                                     RAM 跳到 ~9-10 GB
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                                     generate 完成、model 留在 RAM
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">連續多張生成                          → 維持 ~9-10 GB、沒 unload
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">idle 1 小時                          → 仍 ~9-10 GB（沒 timer）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">切到 ControlNet workflow             → 多載 ControlNet model (~2 GB)、ComfyUI 自動 swap
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                                     RAM 暫升、SD 部分可能被 evict 到 disk
</span></span><span class="line"><span class="ln">10</span><span class="cl">Ctrl+C / pkill                       → process 結束、RAM 完全釋放</span></span></code></pre></div><p>要釋放 ComfyUI 占的 RAM、<strong>唯一方法是結束 server</strong>：</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"># 找 PID</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ps aux <span class="p">|</span> grep <span class="s2">&#34;ComfyUI/main.py&#34;</span> <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"># 優雅關（讓它 cleanup）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">pkill -INT -f <span class="s2">&#34;ComfyUI/main.py&#34;</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"># 強制 kill（如果上面沒反應、最多等 5 秒再強制）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">pkill -KILL -f <span class="s2">&#34;ComfyUI/main.py&#34;</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="c1"># 確認 port 釋放</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">lsof -i :8188 <span class="p">|</span> head -3</span></span></code></pre></div><p>實測：M4 Pro 32GB、SDXL base 載入後 ComfyUI process 占 ~8 GB RAM；<code>pkill -9</code> 後 <code>vm_stat</code> 顯示 free pages 增加 ~470K page（<strong>7.5 GB 釋放</strong>）。</p>
<h3 id="為什麼-ollama-跟-comfyui-設計不同">為什麼 Ollama 跟 ComfyUI 設計不同</h3>
<table>
  <thead>
      <tr>
          <th>因素</th>
          <th>Ollama 設計</th>
          <th>ComfyUI 設計</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要使用模式</td>
          <td>API 服務、IDE plugin 透過 HTTP 用</td>
          <td>互動 GUI、user 連續調 prompt</td>
      </tr>
      <tr>
          <td>Model 切換頻率</td>
          <td>高（不同任務換不同 model）</td>
          <td>低（一次 session 通常一個 model）</td>
      </tr>
      <tr>
          <td>User 期待的 latency</td>
          <td>低首字延遲（IDE 補完場景）</td>
          <td>高 throughput（連續生圖）</td>
      </tr>
      <tr>
          <td>結論</td>
          <td>Auto-unload 釋 RAM 給其他 model</td>
          <td>持續載入避免重複 load 浪費</td>
      </tr>
  </tbody>
</table>
<p>兩種設計都 valid、適合不同使用模式。理解差異後就知道 ComfyUI 一直占 RAM「不是 bug」、是設計選擇。</p>
<h2 id="跟其他本地-server-對比">跟其他本地 server 對比</h2>
<table>
  <thead>
      <tr>
          <th>Server</th>
          <th>Auto-unload</th>
          <th>主動 unload 指令</th>
          <th>占 RAM 觀察</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ollama</td>
          <td>有（5 分鐘 idle）</td>
          <td><code>keep_alive: 0</code> 或 stop daemon</td>
          <td><code>ollama ps</code></td>
      </tr>
      <tr>
          <td>LM Studio</td>
          <td>無（GUI 主動關閉 model 才釋）</td>
          <td>GUI Eject Model</td>
          <td>Activity Monitor</td>
      </tr>
      <tr>
          <td>llama.cpp <code>llama-server</code></td>
          <td>無</td>
          <td>kill process</td>
          <td><code>lsof -i :8080</code></td>
      </tr>
      <tr>
          <td>ComfyUI</td>
          <td>無</td>
          <td>kill process</td>
          <td><code>ps aux | grep ComfyUI</code></td>
      </tr>
      <tr>
          <td>oMLX</td>
          <td>有（per model 可配）</td>
          <td>API endpoint</td>
          <td>server log</td>
      </tr>
  </tbody>
</table>
<p><strong>結論</strong>：只有 Ollama 跟 oMLX 內建 auto-unload、其他都要手動釋放。GUI server（LM Studio）通常給 user 一個「Eject」按鈕、CLI server 通常要 kill process。</p>
<h2 id="標準釋放程序">標準釋放程序</h2>
<p>寫 code 完一天結束、要釋放所有資源、按下表順序操作：</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"># 1. 確認當前狀態（記下要還回去多少 RAM）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">vm_stat <span class="p">|</span> head -3
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">df -h ~ <span class="p">|</span> tail -1
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">ollama ps
</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|llama-server&#34;</span> <span class="p">|</span> grep -v grep
</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"># 2. 釋放當前載入的 LLM models（Ollama）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">brew services stop ollama
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 或保留 daemon、只 unload model：</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># curl -s http://localhost:11434/api/generate -d &#39;{&#34;model&#34;: &#34;&lt;your model&gt;&#34;, &#34;keep_alive&#34;: 0}&#39;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 3. 結束 ComfyUI / 其他 GUI server</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">pkill -INT -f <span class="s2">&#34;ComfyUI/main.py&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">14</span><span class="cl">pkill -INT -f <span class="s2">&#34;llama-server&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">15</span><span class="cl">sleep <span class="m">5</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 強制（如果上面沒清乾淨）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">pkill -KILL -f <span class="s2">&#34;ComfyUI/main.py&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">18</span><span class="cl">pkill -KILL -f <span class="s2">&#34;llama-server&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># 4. 驗證所有 port 釋放</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">lsof -i :11434 -i :1234 -i :8080 -i :8188 -i :8000 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> head
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># 5. 確認釋放量</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">vm_stat <span class="p">|</span> head -3
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"># free pages 該明顯增加</span></span></span></code></pre></div><h3 id="容易出錯的釋放方式">容易出錯的「釋放方式」</h3>
<ul>
<li><strong><code>killall Python</code></strong>：會 kill 所有 Python process、包括其他 dev tool（如 jupyter、Django）。用 <code>pkill -f &quot;ComfyUI/main.py&quot;</code> 等明確 pattern。</li>
<li><strong><code>rm -rf ~/.ollama</code></strong>：會清掉所有 model registry、下次要重 pull 全部 model。Cleanup 用 <code>ollama rm &lt;model&gt;</code> 才精準。</li>
<li><strong><code>brew uninstall ollama</code></strong>：直接卸載 Ollama 本身、過 reinstall 麻煩。Stop service 就夠。</li>
<li><strong>重開機釋放</strong>：work 但太重、會中斷其他工作。用 process-level 操作即可。</li>
</ul>
<h2 id="磁碟長期累積管理">磁碟長期累積管理</h2>
<p>Models 一旦 <code>pull</code> 進 <code>~/.ollama/models/blobs</code>、不主動 <code>rm</code> 不會減少。半年累積可長到 50 GB+。</p>
<p>Ollama models 只是磁碟大戶之一。整台 Mac 突然被吃光、要從哪裡查起的全機診斷順序（先排除快照浮動、再用實際佔用值逐層找大戶），見 <a href="/blog/other/macos-%E7%A3%81%E7%A2%9F%E7%A9%BA%E9%96%93%E8%A2%AB%E5%90%83%E5%85%89%E7%9A%84%E8%A8%BA%E6%96%B7%E6%B5%81%E7%A8%8B/" data-link-title="macOS 磁碟空間被吃光的診斷流程" data-link-desc="Mac 空間莫名歸零、清 cache 沒救、或空間掉了又回來時的排查順序。避開 sparse 假大小和本地快照浮動的誤判。含 disk-report 腳本。">macOS 磁碟空間診斷流程</a>——那篇的佔用大戶表也會把 ollama 列為其中一項、再連回本篇的專屬清理 idiom。</p>
<h3 id="觀察累積">觀察累積</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Ollama models 總占用</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">du -sh ~/.ollama/models/blobs
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 4.1G    /Users/tarragon/.ollama/models/blobs</span>
</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"><span class="c1"># 逐 model 看大小</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="c1"># NAME                       ID              SIZE      MODIFIED</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># gemma4:e4b                 c6eb396dbd59    9.6 GB    Less than a second ago</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># nomic-embed-text:latest    0a109f422b47    274 MB    3 hours ago</span>
</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"><span class="c1"># ComfyUI checkpoints 累積</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">du -sh ~/.ollama ~/Projects/ComfyUI/models 2&gt;/dev/null
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 4.2G    /Users/tarragon/.ollama</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 7.0G    /Users/tarragon/Projects/ComfyUI/models</span></span></span></code></pre></div><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"># 刪掉很久沒用的 model</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ollama rm &lt;model-tag&gt;
</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"># 一次清掉所有 Ollama models（保留 daemon）</span>
</span></span><span class="line"><span class="ln"> 5</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> ollama rm <span class="o">{}</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"># 看 ComfyUI checkpoints 哪些可清</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ls -lh ~/Projects/ComfyUI/models/checkpoints/
</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="c1"># 手動刪不要的 .safetensors（小心、不能 undo）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">rm ~/Projects/ComfyUI/models/checkpoints/&lt;old-model&gt;.safetensors</span></span></code></pre></div><h3 id="磁碟管理-idiom">磁碟管理 idiom</h3>
<p>定期（每月或磁碟剩 &lt; 20% 時）做：</p>
<ol>
<li><code>du -sh ~/.ollama ~/Projects/ComfyUI/models</code> 看當前累積</li>
<li><code>ollama list</code> 看哪些 model 沒在用（看 <code>MODIFIED</code> 欄、太舊的考慮刪）</li>
<li>刪實驗用的 model、保留 daily-driver</li>
<li>ComfyUI checkpoints 同樣 review</li>
</ol>
<h2 id="port--process-排錯">Port / Process 排錯</h2>
<h3 id="啟動報address-already-in-use">啟動報「address already in use」</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"># 找誰占</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">lsof -i :11434
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># COMMAND  PID  USER   ...   NAME</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># ollama   xxx  ...    ...   TCP localhost:11434 (LISTEN)</span>
</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 class="c1"># 看是不是 zombie process</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ps aux <span class="p">|</span> grep <span class="k">$(</span>lsof -ti :11434 <span class="p">|</span> head -1<span class="k">)</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"># 清掉</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nb">kill</span> -9 <span class="k">$(</span>lsof -ti :11434<span class="k">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 或重啟 service（會自動清舊 instance）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">brew services restart ollama</span></span></code></pre></div><h3 id="ollama-daemon-掛了不知道">Ollama daemon 掛了不知道</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"># 健康檢查</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">curl -s http://localhost:11434/api/version
</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"># 沒回應、看 service 狀態</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">brew services list <span class="p">|</span> grep ollama
</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">brew services start ollama
</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="c1"># 看 log</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">tail -50 /opt/homebrew/var/log/ollama.log</span></span></code></pre></div><h3 id="comfyui-看似跑著但-queue-不動">ComfyUI 看似跑著但 Queue 不動</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"># 看 stdout / stderr log</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">tail -30 /tmp/comfyui.log  <span class="c1"># 如果啟動時 redirect 到 log</span>
</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"># 看是不是 GPU / Metal stuck（極少見、但 SDXL 大量並發可能踩到）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 解法：kill + 重啟</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">pkill -9 -f <span class="s2">&#34;ComfyUI/main.py&#34;</span></span></span></code></pre></div><p>完整排錯流程跟「先確認哪一層壞」見 <a href="/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">1.7 排錯方法論</a>。</p>
<h2 id="觀察記憶體佔用實測對照">觀察記憶體佔用：實測對照</h2>
<p>跑這幾步紀錄 baseline → load model → kill 的 RAM 變化：</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"># Baseline</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">vm_stat <span class="p">|</span> grep <span class="s2">&#34;Pages free&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># Pages free:                              1090076.   ← ~17 GB free</span>
</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"><span class="c1"># 啟動 Ollama + load 4B model</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">brew services start ollama
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ollama run gemma3:4b <span class="s2">&#34;hello&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ollama ps
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># NAME       SIZE     PROCESSOR    UNTIL</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># gemma3:4b  5.5 GB   100% Metal   4 minutes from now</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">vm_stat <span class="p">|</span> grep <span class="s2">&#34;Pages free&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># Pages free:                               750000.   ← 跌 ~5 GB（model 載入）</span>
</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"><span class="c1"># 額外啟動 ComfyUI + load SDXL</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">nohup python main.py &gt; /tmp/comfyui.log 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># 在 GUI 上 Queue Prompt 跑一次 SDXL generation</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">vm_stat <span class="p">|</span> grep <span class="s2">&#34;Pages free&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># Pages free:                               280000.   ← 再跌 ~7.5 GB（SDXL 載入 + Python venv）</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"># kill 全部</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">brew services stop ollama
</span></span><span class="line"><span class="ln">23</span><span class="cl">pkill -9 -f <span class="s2">&#34;ComfyUI/main.py&#34;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">sleep <span class="m">3</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">vm_stat <span class="p">|</span> grep <span class="s2">&#34;Pages free&#34;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># Pages free:                              1090000.   ← 回到 baseline</span></span></span></code></pre></div><p>每 page 16 KB、所以 free pages 數字 × 16 KB = 實際 free RAM bytes。</p>
<h2 id="自動化釋放launchd--shell-alias">自動化釋放：launchd / shell alias</h2>
<p>寫個 shell function 一鍵 cleanup：</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"># 加進 ~/.zshrc</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">llm-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;[*] Stopping Ollama...&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  brew services stop ollama 2&gt;/dev/null
</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 class="nb">echo</span> <span class="s2">&#34;[*] Killing ComfyUI...&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  pkill -INT -f <span class="s2">&#34;ComfyUI/main.py&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  sleep <span class="m">3</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  pkill -KILL -f <span class="s2">&#34;ComfyUI/main.py&#34;</span> 2&gt;/dev/null
</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">  <span class="nb">echo</span> <span class="s2">&#34;[*] Killing other model servers...&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  pkill -KILL -f <span class="s2">&#34;llama-server&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">13</span><span class="cl">  pkill -KILL -f <span class="s2">&#34;lm-studio-server&#34;</span> 2&gt;/dev/null
</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">  <span class="nb">echo</span> <span class="s2">&#34;[*] Verifying ports...&#34;</span>
</span></span><span class="line"><span class="ln">16</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">17</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">18</span><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;[*] Free RAM:&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  vm_stat <span class="p">|</span> grep <span class="s2">&#34;Pages free&#34;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="o">}</span></span></span></code></pre></div><p>完事打 <code>llm-cleanup</code> 一鍵釋放、不用記每個 process 怎麼 kill。</p>
<h2 id="何時這篇會過時">何時這篇會過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>RAM / 磁碟 / port 三個 dimension 是長期 invariant、用什麼 LLM server 都成立。</li>
<li>「Mac 是 shared resource、需要主動管理」這個 framing。</li>
<li>Ollama 跟 ComfyUI 兩種典型 lifecycle 對比（auto-unload vs persistent）。</li>
<li>觀察工具（<code>vm_stat</code>、<code>lsof</code>、<code>ps</code>、<code>du</code>、Activity Monitor）是 macOS 系統 API、不會 deprecate。</li>
<li>標準釋放程序、自動化 shell function 模式。</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體 model size / RAM 占用數字（隨模型架構演化）。</li>
<li><code>OLLAMA_KEEP_ALIVE</code> 等具體環境變數名（Ollama API 演化）。</li>
<li>ComfyUI 可能加 auto-unload feature（社群有 issue 在討論）。</li>
</ul>
<p>讀的時候若指令跑不過、先 <code>--help</code> 看當前版本 flag；釋放 RAM 的「kill process」這個機制本身永遠成立。</p>
<h2 id="跟其他-hands-on-章節的關係">跟其他 hands-on 章節的關係</h2>
<ul>
<li><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>：介紹 <code>brew services start/stop</code>、本篇延伸 lifecycle 細節</li>
<li><a href="/blog/llm/01-local-llm-services/hands-on/comfyui-setup/" data-link-title="Hands-on：安裝 ComfyUI &#43; SDXL base" data-link-desc="git clone、venv、pip install requirements、SDXL safetensors 放哪、--listen 啟動 server、瀏覽器 workflow 驗證">ComfyUI 安裝</a>：介紹 ComfyUI 啟動、本篇延伸 RAM 占用 + 釋放</li>
<li><a href="/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">1.7 排錯方法論</a>：用三層架構定位故障、本篇是 lifecycle 視角的補完</li>
<li><a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理</a>：「每個 hop 都要 audit」延伸到資源層</li>
</ul>
<p>整體心法：本地 LLM 工作流跟雲端不一樣、要主動管理 lifecycle、不能裝完就忘。</p>
]]></content:encoded></item><item><title>Hands-on：用本地 LLM 跑 judge harness（最小可行版）</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/local-llm-judge-harness/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/local-llm-judge-harness/</guid><description>&lt;p>&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 LLM-as-judge&lt;/a> 寫的是原理。本篇用 Ollama / LM Studio 在本地跑一個最小可行的 judge harness、對自己工作流的真實案例做 systematic eval。隱私敏感場景特別合用 — eval 資料（user query、agent output、可能含 PII）不需要送雲端。&lt;/p>
&lt;p>本篇 framing 是「&lt;strong>真的能跑、不只跑 demo&lt;/strong>」、所以包含：硬體預算估算、judge model 選型、bias 緩解、calibration 流程、跟 production trace 串接的延伸；術語對應 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/llm-as-judge/" data-link-title="LLM-as-Judge" data-link-desc="用 LLM 評估另一個 LLM 的輸出品質、production eval 的主流方法、500-5000× 成本降但有 bias 要處理">LLM-as-Judge&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/llm-tracing/" data-link-title="LLM Tracing" data-link-desc="把 LLM 應用的每次 LLM call / tool call / memory op 編成結構化 span、用 OpenTelemetry GenAI semantic conventions 標準化">LLM Tracing&lt;/a>。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>驗證日期&lt;/strong>：2026-05-12
&lt;strong>環境&lt;/strong>：M4 Max 64GB / 或 24GB+ VRAM PC + Ollama
&lt;strong>Judge model&lt;/strong>：DeepSeek-R1-Distill-Qwen-32B 或 QwQ-32B（reasoning model 當 judge 更穩）&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼用本地-llm-當-judge">為什麼用本地 LLM 當 judge&lt;/h2>
&lt;p>跟雲端 judge（GPT-5 / Claude 4）對比：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>本地 judge&lt;/th>
 &lt;th>雲端 judge&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Cost&lt;/td>
 &lt;td>0（電費）&lt;/td>
 &lt;td>$0.001-0.01 per item&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>隱私&lt;/td>
 &lt;td>完全本地、eval 資料不出機器&lt;/td>
 &lt;td>送雲端、依政策&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Latency&lt;/td>
 &lt;td>視硬體、reasoning model 30B 約 30-60s&lt;/td>
 &lt;td>API call 5-30s&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>品質上限&lt;/td>
 &lt;td>本地 30B reasoning 接近 2024 雲端中段&lt;/td>
 &lt;td>雲端旗艦上限高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大量 batch&lt;/td>
 &lt;td>慢但 zero cost&lt;/td>
 &lt;td>快但 cost 累積&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判讀：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>大量 production trace eval（千筆以上）+ 隱私敏感&lt;/strong> → 本地 judge&lt;/li>
&lt;li>&lt;strong>少量 high-stake eval（&amp;lt; 50 筆）&lt;/strong> → 雲端旗艦 judge&lt;/li>
&lt;li>&lt;strong>A/B test 快速 iterate&lt;/strong> → 雲端（latency 重要）&lt;/li>
&lt;/ul>
&lt;h2 id="硬體預算">硬體預算&lt;/h2>
&lt;p>Judge model 選擇看硬體：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>硬體&lt;/th>
 &lt;th>適合 judge model&lt;/th>
 &lt;th>預期 latency / item&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>M4 Pro 24GB / 4090 16GB&lt;/td>
 &lt;td>Qwen2.5-32B Q4 或 DeepSeek-R1-Distill-14B&lt;/td>
 &lt;td>30-60s&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>M4 Pro 36GB&lt;/td>
 &lt;td>DeepSeek-R1-Distill-Qwen-32B Q4&lt;/td>
 &lt;td>60-120s&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>M4 Max 48-64GB / 5090 24GB&lt;/td>
 &lt;td>QwQ-32B 或 DeepSeek-R1-Distill-Qwen-32B Q6&lt;/td>
 &lt;td>60-180s（含 reasoning trace）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>M4 Max 128GB / 多卡 PC&lt;/td>
 &lt;td>Llama 3.3 70B 或 Qwen3-72B&lt;/td>
 &lt;td>120-300s&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>注意：reasoning model 的 thinking trace 拉長 latency、跑大量 batch 要規劃時間（100 item × 60s = 100 min）。&lt;/p></description><content:encoded><![CDATA[<p><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 LLM-as-judge</a> 寫的是原理。本篇用 Ollama / LM Studio 在本地跑一個最小可行的 judge harness、對自己工作流的真實案例做 systematic eval。隱私敏感場景特別合用 — eval 資料（user query、agent output、可能含 PII）不需要送雲端。</p>
<p>本篇 framing 是「<strong>真的能跑、不只跑 demo</strong>」、所以包含：硬體預算估算、judge model 選型、bias 緩解、calibration 流程、跟 production trace 串接的延伸；術語對應 <a href="/blog/llm/knowledge-cards/llm-as-judge/" data-link-title="LLM-as-Judge" data-link-desc="用 LLM 評估另一個 LLM 的輸出品質、production eval 的主流方法、500-5000× 成本降但有 bias 要處理">LLM-as-Judge</a> 與 <a href="/blog/llm/knowledge-cards/llm-tracing/" data-link-title="LLM Tracing" data-link-desc="把 LLM 應用的每次 LLM call / tool call / memory op 編成結構化 span、用 OpenTelemetry GenAI semantic conventions 標準化">LLM Tracing</a>。</p>
<blockquote>
<p><strong>驗證日期</strong>：2026-05-12
<strong>環境</strong>：M4 Max 64GB / 或 24GB+ VRAM PC + Ollama
<strong>Judge model</strong>：DeepSeek-R1-Distill-Qwen-32B 或 QwQ-32B（reasoning model 當 judge 更穩）</p></blockquote>
<h2 id="為什麼用本地-llm-當-judge">為什麼用本地 LLM 當 judge</h2>
<p>跟雲端 judge（GPT-5 / Claude 4）對比：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>本地 judge</th>
          <th>雲端 judge</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cost</td>
          <td>0（電費）</td>
          <td>$0.001-0.01 per item</td>
      </tr>
      <tr>
          <td>隱私</td>
          <td>完全本地、eval 資料不出機器</td>
          <td>送雲端、依政策</td>
      </tr>
      <tr>
          <td>Latency</td>
          <td>視硬體、reasoning model 30B 約 30-60s</td>
          <td>API call 5-30s</td>
      </tr>
      <tr>
          <td>品質上限</td>
          <td>本地 30B reasoning 接近 2024 雲端中段</td>
          <td>雲端旗艦上限高</td>
      </tr>
      <tr>
          <td>大量 batch</td>
          <td>慢但 zero cost</td>
          <td>快但 cost 累積</td>
      </tr>
  </tbody>
</table>
<p>判讀：</p>
<ul>
<li><strong>大量 production trace eval（千筆以上）+ 隱私敏感</strong> → 本地 judge</li>
<li><strong>少量 high-stake eval（&lt; 50 筆）</strong> → 雲端旗艦 judge</li>
<li><strong>A/B test 快速 iterate</strong> → 雲端（latency 重要）</li>
</ul>
<h2 id="硬體預算">硬體預算</h2>
<p>Judge model 選擇看硬體：</p>
<table>
  <thead>
      <tr>
          <th>硬體</th>
          <th>適合 judge model</th>
          <th>預期 latency / item</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>M4 Pro 24GB / 4090 16GB</td>
          <td>Qwen2.5-32B Q4 或 DeepSeek-R1-Distill-14B</td>
          <td>30-60s</td>
      </tr>
      <tr>
          <td>M4 Pro 36GB</td>
          <td>DeepSeek-R1-Distill-Qwen-32B Q4</td>
          <td>60-120s</td>
      </tr>
      <tr>
          <td>M4 Max 48-64GB / 5090 24GB</td>
          <td>QwQ-32B 或 DeepSeek-R1-Distill-Qwen-32B Q6</td>
          <td>60-180s（含 reasoning trace）</td>
      </tr>
      <tr>
          <td>M4 Max 128GB / 多卡 PC</td>
          <td>Llama 3.3 70B 或 Qwen3-72B</td>
          <td>120-300s</td>
      </tr>
  </tbody>
</table>
<p>注意：reasoning model 的 thinking trace 拉長 latency、跑大量 batch 要規劃時間（100 item × 60s = 100 min）。</p>
<p><strong>何時不適合用本地 judge</strong>：</p>
<ol>
<li><strong>硬體低於 M4 Pro 24GB / 4090 16GB</strong>（如 M1/M2 16GB、無獨立 GPU PC）：跑 32B reasoning model 太緊、強行跑會 swap、latency 爆 5-10×。改用 14B instruct model（如 Qwen2.5-14B Q4）作 judge、或直接走雲端 judge</li>
<li><strong>Batch × latency &gt; 你可接受的等待時間</strong>：100 item × 60s/item = 100 min；500 item × 120s = 17 hr。預估超過 4 hr 時改雲端 batch API</li>
<li><strong>eval 任務太 nuanced</strong>：細粒度倫理 / 法律 / 高 stake 判讀、本地 32B distill 能力不夠、用雲端旗艦 judge 或人工 review</li>
<li><strong>calibration 階段</strong>：第一次跑、要快速 iterate rubric、雲端 judge latency 短（5-30s）更適合 iterate</li>
</ol>
<h2 id="整體流程">整體流程</h2>





<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">1. 蒐集 eval dataset    → JSONL：每行一個 (input, output) 待評
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 設計 rubric         → 評分維度、scale、明確 anti-pattern
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 寫 judge prompt     → 4 段式（task / input-output / rubric / format）
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 跑 harness          → 對每筆 input call judge、parse JSON output
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. Aggregate 結果      → 算平均分數、找 outlier、看 reasoning
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. Calibration（可選）  → 跟 human eval 比對、調 rubric
</span></span><span class="line"><span class="ln">7</span><span class="cl">7. 跟 production trace 串接 → 定期跑 production sample</span></span></code></pre></div><h2 id="step-1蒐集-eval-dataset">Step 1：蒐集 eval dataset</h2>
<p>JSONL format（每行一筆）：</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 class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;001&#34;</span><span class="p">,</span> <span class="nt">&#34;input&#34;</span><span class="p">:</span> <span class="s2">&#34;用 Python 寫 fibonacci function&#34;</span><span class="p">,</span> <span class="nt">&#34;output&#34;</span><span class="p">:</span> <span class="s2">&#34;def fib(n):\n    if n &lt;= 1:\n        return n\n    return fib(n-1) + fib(n-2)&#34;</span><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;id&#34;</span><span class="p">:</span> <span class="s2">&#34;002&#34;</span><span class="p">,</span> <span class="nt">&#34;input&#34;</span><span class="p">:</span> <span class="s2">&#34;解釋這段 code 在做什麼：[code]&#34;</span><span class="p">,</span> <span class="nt">&#34;output&#34;</span><span class="p">:</span> <span class="s2">&#34;這段 code 實作了 ...&#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;id&#34;</span><span class="p">:</span> <span class="s2">&#34;003&#34;</span><span class="p">,</span> <span class="nt">&#34;input&#34;</span><span class="p">:</span> <span class="s2">&#34;[bug 描述]&#34;</span><span class="p">,</span> <span class="nt">&#34;output&#34;</span><span class="p">:</span> <span class="s2">&#34;[suggested fix]&#34;</span><span class="p">}</span></span></span></code></pre></div><p>來源：</p>
<ul>
<li>過往 Continue.dev / Cursor 跟 LLM 的對話 log</li>
<li>Production agent 的 trace（手動 export 或 LangSmith / Phoenix dump）</li>
<li>自己 hand-craft 30-100 個典型 case</li>
</ul>
<p>放在 <code>data/eval.jsonl</code>。</p>
<h2 id="step-2設計-rubric">Step 2：設計 rubric</h2>
<p>依任務類型設計、coding 任務的範例 rubric：</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">1. Correctness（程式碼能否運作、邏輯是否正確）：1-5
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">2. Style（是否符合 codebase convention、習慣命名）：1-5
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">3. Completeness（是否完整解決 user request）：1-5
</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">- 5：完美無瑕、可直接 merge
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">- 4：小修可用、整體正確
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">- 3：方向正確、需 substantial 修改
</span></span><span class="line"><span class="ln">10</span><span class="cl">- 2：部分對、主要邏輯有錯
</span></span><span class="line"><span class="ln">11</span><span class="cl">- 1：完全錯、誤導使用者
</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">明確不加分（緩解 verbosity bias）：
</span></span><span class="line"><span class="ln">14</span><span class="cl">- 冗長 / verbose（同樣正確的短答 = 長答）
</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">- 「我希望這有幫助」這類禮貌話
</span></span><span class="line"><span class="ln">17</span><span class="cl">- 過多 markdown 修飾（不加分）</span></span></code></pre></div><h2 id="step-3judge-prompt-模板">Step 3：Judge prompt 模板</h2>
<p>寫成 file <code>prompts/judge.txt</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">你是 LLM 輸出品質評估員、要評估 coding assistant 對使用者請求的回答品質。
</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">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">User request:
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">{input}
</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">Assistant response:
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">{output}
</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">評分維度（每維 1-5、加總用 overall）：
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">1. Correctness：程式碼能否運作、邏輯正確
</span></span><span class="line"><span class="ln">13</span><span class="cl">   5: 完美無瑕
</span></span><span class="line"><span class="ln">14</span><span class="cl">   4: 小修可用
</span></span><span class="line"><span class="ln">15</span><span class="cl">   3: 方向正確、需 substantial 修改
</span></span><span class="line"><span class="ln">16</span><span class="cl">   2: 部分對、主要邏輯有錯
</span></span><span class="line"><span class="ln">17</span><span class="cl">   1: 完全錯
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">2. Style：符合 codebase convention
</span></span><span class="line"><span class="ln">20</span><span class="cl">   1-5 同 scale
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">3. Completeness：完整解決 user request
</span></span><span class="line"><span class="ln">23</span><span class="cl">   1-5 同 scale
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">明確不加分項：
</span></span><span class="line"><span class="ln">26</span><span class="cl">- 冗長 / verbose（同樣正確的短答 = 長答）
</span></span><span class="line"><span class="ln">27</span><span class="cl">- 道歉 / 開場白
</span></span><span class="line"><span class="ln">28</span><span class="cl">- 「我希望這有幫助」這類禮貌話
</span></span><span class="line"><span class="ln">29</span><span class="cl">- 過多 markdown 修飾
</span></span><span class="line"><span class="ln">30</span><span class="cl">
</span></span><span class="line"><span class="ln">31</span><span class="cl">請依下列 JSON 輸出（不要加額外文字、不要 markdown code fence）：
</span></span><span class="line"><span class="ln">32</span><span class="cl">{
</span></span><span class="line"><span class="ln">33</span><span class="cl">  &#34;correctness&#34;: &lt;1-5&gt;,
</span></span><span class="line"><span class="ln">34</span><span class="cl">  &#34;style&#34;: &lt;1-5&gt;,
</span></span><span class="line"><span class="ln">35</span><span class="cl">  &#34;completeness&#34;: &lt;1-5&gt;,
</span></span><span class="line"><span class="ln">36</span><span class="cl">  &#34;reasoning&#34;: &#34;&lt;簡短解釋、&lt; 100 字&gt;&#34;,
</span></span><span class="line"><span class="ln">37</span><span class="cl">  &#34;overall&#34;: &lt;1-5&gt;
</span></span><span class="line"><span class="ln">38</span><span class="cl">}</span></span></code></pre></div><h2 id="step-4跑-harness">Step 4：跑 harness</h2>
<p>Python 最小可行版：</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="c1"># judge_harness.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</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 class="n">JUDGE_MODEL</span> <span class="o">=</span> <span class="s2">&#34;deepseek-r1:32b&#34;</span>  <span class="c1"># 或 qwq:32b</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">OLLAMA_URL</span> <span class="o">=</span> <span class="s2">&#34;http://localhost:11434/v1/chat/completions&#34;</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="k">def</span> <span class="nf">load_dataset</span><span class="p">(</span><span class="n">path</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Load JSONL eval dataset.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">return</span> <span class="p">[</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 class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">f</span> <span class="k">if</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">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">def</span> <span class="nf">load_prompt_template</span><span class="p">(</span><span class="n">path</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="n">Path</span><span class="p">(</span><span class="n">path</span><span class="p">)</span><span class="o">.</span><span class="n">read_text</span><span class="p">()</span>
</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"><span class="k">def</span> <span class="nf">call_judge</span><span class="p">(</span><span class="n">prompt</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Call Ollama judge model、回 raw response text.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">OLLAMA_URL</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="s2">&#34;model&#34;</span><span class="p">:</span> <span class="n">JUDGE_MODEL</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="s2">&#34;messages&#34;</span><span class="p">:</span> <span class="p">[{</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="n">prompt</span><span class="p">}],</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="s2">&#34;temperature&#34;</span><span class="p">:</span> <span class="mf">0.1</span><span class="p">,</span>  <span class="c1"># judge 用低 temperature 穩定</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="s2">&#34;stream&#34;</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="p">},</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">600</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="k">return</span> <span class="n">resp</span><span class="o">.</span><span class="n">json</span><span class="p">()[</span><span class="s2">&#34;choices&#34;</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s2">&#34;message&#34;</span><span class="p">][</span><span class="s2">&#34;content&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="k">def</span> <span class="nf">parse_judge_output</span><span class="p">(</span><span class="n">text</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Parse judge 回的 JSON、容錯處理（reasoning model 可能加 &lt;think&gt; 標記）。&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="c1"># 跳過 reasoning trace</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="k">if</span> <span class="s2">&#34;&lt;/think&gt;&#34;</span> <span class="ow">in</span> <span class="n">text</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="n">text</span> <span class="o">=</span> <span class="n">text</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;&lt;/think&gt;&#34;</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="c1"># 找 JSON 區塊</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="n">start</span> <span class="o">=</span> <span class="n">text</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;{&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="n">end</span> <span class="o">=</span> <span class="n">text</span><span class="o">.</span><span class="n">rfind</span><span class="p">(</span><span class="s2">&#34;}&#34;</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="k">if</span> <span class="n">start</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="ow">or</span> <span class="n">end</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">39</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">text</span><span class="p">[</span><span class="n">start</span><span class="p">:</span><span class="n">end</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">40</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="p">:</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="k">def</span> <span class="nf">run_harness</span><span class="p">(</span><span class="n">dataset_path</span><span class="p">,</span> <span class="n">prompt_template_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">    <span class="n">dataset</span> <span class="o">=</span> <span class="n">load_dataset</span><span class="p">(</span><span class="n">dataset_path</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">    <span class="n">template</span> <span class="o">=</span> <span class="n">load_prompt_template</span><span class="p">(</span><span class="n">prompt_template_path</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">
</span></span><span class="line"><span class="ln">47</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">48</span><span class="cl">    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">item</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">dataset</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">        <span class="n">prompt</span> <span class="o">=</span> <span class="n">template</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">input</span><span class="o">=</span><span class="n">item</span><span class="p">[</span><span class="s2">&#34;input&#34;</span><span class="p">],</span> <span class="n">output</span><span class="o">=</span><span class="n">item</span><span class="p">[</span><span class="s2">&#34;output&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">        <span class="n">raw</span> <span class="o">=</span> <span class="n">call_judge</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">        <span class="n">parsed</span> <span class="o">=</span> <span class="n">parse_judge_output</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">
</span></span><span class="line"><span class="ln">53</span><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">            <span class="s2">&#34;id&#34;</span><span class="p">:</span> <span class="n">item</span><span class="p">[</span><span class="s2">&#34;id&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">            <span class="s2">&#34;scores&#34;</span><span class="p">:</span> <span class="n">parsed</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">            <span class="s2">&#34;raw_judge_output&#34;</span><span class="p">:</span> <span class="n">raw</span><span class="p">[:</span><span class="mi">500</span><span class="p">],</span>  <span class="c1"># 保留前 500 字便於 debug</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">        <span class="n">results</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[</span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s2">/</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">dataset</span><span class="p">)</span><span class="si">}</span><span class="s2">] id=</span><span class="si">{</span><span class="n">item</span><span class="p">[</span><span class="s1">&#39;id&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2"> overall=</span><span class="si">{</span><span class="n">parsed</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;overall&#39;</span><span class="p">)</span> <span class="k">if</span> <span class="n">parsed</span> <span class="k">else</span> <span class="s1">&#39;FAIL&#39;</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl">
</span></span><span class="line"><span class="ln">61</span><span class="cl">    <span class="c1"># 寫出 JSONL</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">output_path</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">        <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl">            <span class="n">f</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">r</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">65</span><span class="cl">
</span></span><span class="line"><span class="ln">66</span><span class="cl">    <span class="c1"># Aggregate</span>
</span></span><span class="line"><span class="ln">67</span><span class="cl">    <span class="n">valid</span> <span class="o">=</span> <span class="p">[</span><span class="n">r</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">results</span> <span class="k">if</span> <span class="n">r</span><span class="p">[</span><span class="s2">&#34;scores&#34;</span><span class="p">]]</span>
</span></span><span class="line"><span class="ln">68</span><span class="cl">    <span class="k">if</span> <span class="n">valid</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">69</span><span class="cl">        <span class="n">avg</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">r</span><span class="p">[</span><span class="s2">&#34;scores&#34;</span><span class="p">][</span><span class="s2">&#34;overall&#34;</span><span class="p">]</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">valid</span><span class="p">)</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">valid</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">70</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">Aggregate: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">valid</span><span class="p">)</span><span class="si">}</span><span class="s2">/</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">results</span><span class="p">)</span><span class="si">}</span><span class="s2"> valid、avg overall = </span><span class="si">{</span><span class="n">avg</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">71</span><span class="cl">
</span></span><span class="line"><span class="ln">72</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">73</span><span class="cl">    <span class="n">run_harness</span><span class="p">(</span><span class="s2">&#34;data/eval.jsonl&#34;</span><span class="p">,</span> <span class="s2">&#34;prompts/judge.txt&#34;</span><span class="p">,</span> <span class="s2">&#34;results/eval.jsonl&#34;</span><span class="p">)</span></span></span></code></pre></div><p>跑：</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"># 先確認 judge model 已 pull</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ollama pull deepseek-r1:32b
</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"># 跑 harness</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">python judge_harness.py</span></span></code></pre></div><h2 id="step-5aggregate-跟看-outlier">Step 5：Aggregate 跟看 outlier</h2>
<p>跑完後 results/eval.jsonl 含每筆評分跟 reasoning。看哪些是 outlier：</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"># 找 overall &lt; 3 的 case（低分、值得 review）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">jq <span class="s1">&#39;select(.scores.overall &lt; 3)&#39;</span> results/eval.jsonl
</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"># 看 reasoning 找系統性問題</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">jq <span class="s1">&#39;.scores.reasoning&#39;</span> results/eval.jsonl <span class="p">|</span> sort -u</span></span></code></pre></div><p>判讀：</p>
<ul>
<li><strong>多數 score 4-5、少數 1-2</strong>：整體品質好、focus 在低分 case 找 fix</li>
<li><strong>多數 score 2-3</strong>：系統性問題、改 prompt / model / agent design</li>
<li><strong>分數分佈兩極（很多 5 很多 1）</strong>：可能是 task difficulty 分群、stratified analysis</li>
</ul>
<h2 id="step-6calibration可選但推薦">Step 6：Calibration（可選但推薦）</h2>
<p>跟 human eval 比對、確認 judge 對齊：</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">1. 從 dataset 抽 30 個（覆蓋 difficulty / score 分佈）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 自己 human eval（依同樣 rubric）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 對比 judge 跟 human 的 overall score
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 算 Spearman correlation
</span></span><span class="line"><span class="ln">5</span><span class="cl">   - &gt; 0.7：judge 對齊夠好、可信
</span></span><span class="line"><span class="ln">6</span><span class="cl">   - 0.5-0.7：部分問題、改 rubric
</span></span><span class="line"><span class="ln">7</span><span class="cl">   - &lt; 0.5：judge 不可信、換 model 或重寫 rubric</span></span></code></pre></div><p>低 correlation 的常見原因：</p>
<ul>
<li>Rubric 太 vague、judge 自由發揮</li>
<li>Judge model 能力不夠（換更強 judge）</li>
<li>Verbosity / position bias 沒緩解</li>
<li>Eval task 跟 judge 訓練分佈差距大</li>
</ul>
<h2 id="step-7跟-production-trace-串接延伸">Step 7：跟 production trace 串接（延伸）</h2>
<p>把 <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 LLM tracing</a> 蒐集的 production trace export 成 JSONL、定期跑 judge：</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"># 假設用 Langfuse self-host</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">langfuse <span class="nb">export</span> --filter <span class="s2">&#34;user_feedback=negative&#34;</span> --output traces.jsonl
</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"># 轉成 eval format</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">python convert_trace_to_eval.py traces.jsonl &gt; data/eval-from-prod.jsonl
</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"># 跑 judge</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">python judge_harness.py</span></span></code></pre></div><p>這是 production quality engineering 閉環的本地版本、隱私敏感場景的 cost-free alternative。</p>
<h2 id="失敗模式">失敗模式</h2>
<ol>
<li><strong>Judge 不輸出合法 JSON</strong>：reasoning model 可能在 <code>&lt;think&gt;...&lt;/think&gt;</code> 後仍加 markdown / 解釋</li>
</ol>
<p><strong>緩解</strong>：parse 時跳 <code>&lt;think&gt;</code> 段、容錯處理、或開 <a href="/blog/llm/knowledge-cards/constrained-decoding/" data-link-title="Constrained Decoding" data-link-desc="推論時用 grammar 強制 LLM 輸出符合特定格式（JSON / regex / CFG）的 sampling 機制、把不合法 token 的機率歸零">constrained decoding</a>（llama.cpp grammar）</p>
<ol start="2">
<li><strong>Latency 太長、batch 跑不完</strong>：reasoning model 32B 每 item 60-120s、100 item 要 2 小時</li>
</ol>
<p><strong>緩解</strong>：用較小 judge model（如 Qwen2.5-32B instruct、非 reasoning）、或拆 batch 並行</p>
<ol start="3">
<li><strong>Judge bias 沒緩解</strong>：本地 judge 跟雲端 judge 都會有 verbosity / position bias</li>
</ol>
<p><strong>緩解</strong>：rubric 寫明、pairwise 換位置跑 2 次</p>
<ol start="4">
<li><strong>本地 judge 能力上限</strong>：30B distill 對 nuanced case 判讀不如雲端旗艦</li>
</ol>
<p><strong>緩解</strong>：critical case 加 spot human review、或混用本地（量大）+ 雲端（精選 sample）</p>
<h2 id="跟其他章節的關係">跟其他章節的關係</h2>
<ul>
<li>原理層的 LLM-as-judge 設計見 <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></li>
<li>Production trace 串接見 <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 tracing</a></li>
<li>Reasoning model 選型見 <a href="/blog/llm/03-theoretical-foundations/reasoning-models/" data-link-title="3.8 Reasoning models：test-time compute paradigm" data-link-desc="Chain-of-thought 從 prompting 技巧演化成訓練 paradigm、reasoning model 的內部運作、本地可跑的選項與適用任務">3.8</a></li>
<li>隱私 / 跨雲端邊界判讀見 <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>Benchmark 跟 in-house eval 的層次見 <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></li>
</ul>
]]></content:encoded></item><item><title>LLM 寫 code 工程實務指南：從心智模型到應用架構</title><link>https://tarrragon.github.io/blog/llm/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/</guid><description>&lt;p>本指南的核心目標是把「LLM 在寫 code 工作流的完整工程地圖」拆成可決策、可實作、可期望管理的工程問題。範圍覆蓋四條讀者旅程：(1) 在自己機器跑本地 LLM 寫 code 的最短可行路徑（Mac 或 PC）、(2) 想懂 LLM 內部運作機制（數學 + 理論基礎）、(3) 想做 LLM 應用開發（RAG / agent / tool use / VLM / benchmarking / 靜態 deployment）、(4) 關心 LLM 工作流的安全議題（本地 dev 視角 + 靜態網站視角）。網路上的 LLM 文章常把推論框架、加速技巧、應用模式、安全議題混為一談；本指南先把這些名詞放回正確的層級、再回答各層的具體取捨。&lt;/p>
&lt;p>本指南預設讀者已經會用過雲端 LLM（ChatGPT、Claude）、熟悉終端機操作、想以工程視角理解 LLM。&lt;strong>寫 code 場景是主要使用例、但模組二 / 三 / 四 / 六多數章節跨場景通用&lt;/strong>：想懂 reasoning model / RAG / embedding model 內部、即使不裝本地 LLM 也能讀。硬體前提分兩條路線：Apple Silicon Mac（M1 ~ M4、統一記憶體）走模組一；Windows / Linux + 獨立 GPU（NVIDIA / AMD、獨立 VRAM + 系統 RAM）走模組五。文章不販賣 LLM 焦慮、也不誇大本地能取代雲端的程度；它的責任是給每條讀者旅程的最短可行路徑、並標出每個階段的取捨。&lt;/p>
&lt;p>模組零（心智模型）是所有讀者旅程的共同前置。模組一跟模組五是「裝本地 LLM」的兩條硬體路線、依平台選一條；想懂底層走模組二跟模組三（跟硬體無關、含 reasoning model / speculative decoding 等推論細節）；想看 LLM 作為系統元件走模組四（12 章涵蓋 RAG、tool use、agent、應用層協議、workflow、production resource、long context、embedding model、benchmarking、vision、靜態 deployment）；本地工作流跑穩想看安全議題走模組六（個人 dev 視角的供應鏈、伺服器綁定、tool use 權限、prompt injection、跨雲端邊界、production routing）。&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;/td>
 &lt;td>本地 vs 雲端的差異、為何 LLM 生字慢、三層架構（介面 / 伺服器 / 模型）、&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;/td>
 &lt;td>雲端 GPU 租用、AGI 預測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>術語澄清&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">MLX&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">MTP&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">oMLX&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/speculative-decoding/" data-link-title="Speculative Decoding" data-link-desc="用小模型猜未來 token、大模型並行驗證的加速技巧">speculative decoding&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/moe-cpu-offload/" data-link-title="MoE CPU 卸載" data-link-desc="把 Mixture-of-Experts 模型不活躍的專家層權重放在系統 RAM、用到再走 PCIe 拉回 GPU、讓有限 VRAM 跑得了更大模型">MoE CPU 卸載&lt;/a>&lt;/td>
 &lt;td>post-training fine-tuning 細節&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mac 硬體現實&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">記憶體預算與模型大小&lt;/a>、量化選擇、首字延遲、風扇與功耗&lt;/td>
 &lt;td>雲端 GPU 租用、資料中心訓練&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PC 硬體現實&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/05-discrete-gpu/vram-ram-budget/" data-link-title="5.0 VRAM &amp;#43; RAM 分層預算" data-link-desc="PC 獨立 GPU 場景的記憶體預算判讀：VRAM 是快的世界、RAM 是大的世界、PCIe 把兩個世界連起來">VRAM + RAM 分層預算&lt;/a>、MoE 專家層 CPU 卸載、KV cache 量化、PCIe 頻寬限制&lt;/td>
 &lt;td>多卡 NVLink、資料中心級分散式推論&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本地推論伺服器&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/ollama/" data-link-title="1.0 Ollama：主流推論伺服器" data-link-desc="一行 brew 裝完、ollama run 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on localhost:11434">Ollama&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/lm-studio/" data-link-title="1.1 LM Studio：GUI 探索模型" data-link-desc="GUI 取向的本地推論伺服器：內建模型瀏覽器、speculative decoding 設定面板、適合探索新模型">LM Studio&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/llama-cpp/" data-link-title="1.2 llama.cpp：底層推論引擎" data-link-desc="GGUF 格式、量化、MTP 仍 beta；多數讀者不需要直接接觸，Ollama 已經包好">llama.cpp&lt;/a>（Mac + PC 通用）&lt;/td>
 &lt;td>vLLM、TGI、Triton 等資料中心級 inference server&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>編輯器整合&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/vscode-continue-integration/" data-link-title="1.3 VS Code &amp;#43; Continue.dev 整合" data-link-desc="安裝 Continue 擴充套件、config.json 設定、Cmd&amp;#43;L 對話 / Cmd&amp;#43;I 行內編輯快捷鍵">Continue.dev + VS Code&lt;/a>、Cursor 對應關係&lt;/td>
 &lt;td>JetBrains 全套整合、Vim / Emacs 進階 plugin&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>模型挑選&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">coding 場景的模型優先順序&lt;/a>、量化等級對體感影響&lt;/td>
 &lt;td>benchmark 跑分方法論的完整推導&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>期望管理&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/expectation-management/" data-link-title="1.5 期望管理：本地 LLM 的擅長領域與分工" data-link-desc="本地 LLM 是免費的初階 pair programmer：辨識它的擅長領域、跟雲端旗艦做結構性分工">本地 LLM 的擅長領域與分工&lt;/a>、混用雲端的時機&lt;/td>
 &lt;td>LLM 通用能力評估、AGI 預測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>數學基礎&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/02-math-foundations/linear-algebra-for-llm/" data-link-title="2.0 線性代數：向量、矩陣、空間" data-link-desc="LLM 內部運算的基底：向量、矩陣、向量空間、內積、norm、矩陣乘法的角色">線性代數&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/02-math-foundations/probability-and-information/" data-link-title="2.1 機率與資訊論" data-link-desc="LLM 輸出的本質是機率分佈：softmax、cross-entropy、KL divergence、perplexity 在訓練與推論中的角色">機率與資訊論&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/02-math-foundations/calculus-and-optimization/" data-link-title="2.2 微積分與最佳化" data-link-desc="從 gradient、chain rule 到 SGD / Adam：LLM 訓練如何更新數十億參數">最佳化&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/02-math-foundations/numerical-precision/" data-link-title="2.3 數值精度與量化的數學依據" data-link-desc="fp32 / bf16 / fp16 / int8 / int4 的差別、量化能省哪些 bits、品質衰減從哪裡來">數值精度&lt;/a> 在 LLM 中的角色&lt;/td>
 &lt;td>完整數學證明、測度論等屬於數學系範圍的主題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>理論基礎&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/neural-network-basics/" data-link-title="3.0 神經網路基礎" data-link-desc="從單一 neuron 到 multi-layer：weights、activation function、forward / backward pass 的角色">神經網路&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/embedding-spaces/" data-link-title="3.1 Embedding 空間" data-link-desc="token 怎麼變成向量、為什麼相似 token 在向量空間中靠近、embedding 是怎麼學出來的">embedding&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/attention-mechanism/" data-link-title="3.2 Attention 機制" data-link-desc="Query / Key / Value、scaled dot-product attention、multi-head attention：Transformer 的核心運算">attention&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/transformer-architecture/" data-link-title="3.3 Transformer 架構細節" data-link-desc="Decoder-only 結構、Transformer block、positional encoding、layer norm、residual stream">Transformer&lt;/a>、&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 的三階段訓練：預訓練、指令微調、人類反饋強化學習；各階段目標與最新替代方案">訓練流程&lt;/a>、&lt;a href="https://tarrragon.github.io/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&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/tokenization-algorithms/" data-link-title="3.6 Tokenization：BPE、SentencePiece、Tiktoken" data-link-desc="把文字切成 token 的算法：為什麼不同模型切出不同 token 數、tokenizer 選擇對能力的影響">tokenization&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/cross-language-tokenization/" data-link-title="3.7 跨語言場景的 tokenizer 與訓練分佈原理" data-link-desc="為什麼模型對不同語言表現不一致：tokenizer &amp;#43; 訓練資料分佈雙因素、語言選擇取捨">跨語言原理&lt;/a>&lt;/td>
 &lt;td>多模態擴展、最新研究細節交給 Stanford CS25&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>應用層原理&lt;/td>
 &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 失敗的根本原因">RAG&lt;/a>、&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 表現崩潰">Tool use&lt;/a>、&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 不適合、跟人類審查的協作模型">Agent 架構&lt;/a>、&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 協議，三者的層級差異與組合方式">應用層協議&lt;/a>、&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 組合的四種基本模式與退化條件">Workflow 編排&lt;/a>、&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 的設計取捨">Production resource&lt;/a>、&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 取捨">Artifact 管理&lt;/a>&lt;/td>
 &lt;td>具體 framework 教學（LangChain / LlamaIndex）、prompt engineering&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>進階理論&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/reasoning-models/" data-link-title="3.8 Reasoning models：test-time compute paradigm" data-link-desc="Chain-of-thought 從 prompting 技巧演化成訓練 paradigm、reasoning model 的內部運作、本地可跑的選項與適用任務">Reasoning models&lt;/a>（o1 / R1 / QwQ 風格）、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/speculative-decoding-internals/" data-link-title="3.9 Speculative decoding 內部：drafter / 驗證 / 加速上限" data-link-desc="speculative decoding 的演算法細節、drafter 跟 target 怎麼配對、acceptance rate 怎麼決定實際加速、MTP 跟 EAGLE 等變體">Speculative decoding 內部&lt;/a>（drafter / MTP / EAGLE）&lt;/td>
 &lt;td>完整 paper 推導、最新研究 frontier&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>進階應用&lt;/td>
 &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 取捨">Long context engineering&lt;/a>、&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">Embedding model 內部&lt;/a>、&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、量測本地推論速度的完整方法論">Benchmarking&lt;/a>、&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 整合現狀">Vision in coding&lt;/a>、&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">靜態 / serverless RAG deployment&lt;/a>&lt;/td>
 &lt;td>完整 LangChain / LlamaIndex 教學&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Fine-tuning&lt;/td>
 &lt;td>原理（&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/lora/" data-link-title="LoRA" data-link-desc="Low-Rank Adaptation：凍住原模型權重、只訓兩個小矩陣的 parameter-efficient fine-tuning">LoRA&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/qlora/" data-link-title="QLoRA" data-link-desc="把 base model 量化到 4-bit &amp;#43; LoRA fine-tune 的組合、消費級 GPU 也能 fine-tune 大模型">QLoRA&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/catastrophic-forgetting/" data-link-title="Catastrophic Forgetting" data-link-desc="Fine-tune 模型時、新訓練資料覆蓋掉原本學到的能力的現象、LoRA / 資料 mixing 是主要緩解">catastrophic forgetting&lt;/a>）+ &lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/local-fine-tuning/" data-link-title="Hands-on：用 QLoRA 在本機 fine-tune coding 模型" data-link-desc="Apple Silicon Mac / PC 獨立 GPU 上跑 QLoRA fine-tune 的完整流程：環境、資料、訓練、evaluation、合併、部署到 Ollama">本機 hands-on&lt;/a>&lt;/td>
 &lt;td>完整資料工程、large-scale distributed fine-tune&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>隱私 / 安全&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">隱私資料流&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">本地 dev 安全模組&lt;/a>（供應鏈 / 伺服器綁定 / tool use / prompt injection / 跨雲端邊界 / production routing）、&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">靜態網站 RAG 資安&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">排錯方法論&lt;/a>&lt;/td>
 &lt;td>企業合規逐條檢核、SOC 2 / HIPAA 流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>進一步學習&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/02-math-foundations/going-deeper-math/" data-link-title="2.4 想學更深：推薦公開課程" data-link-desc="MIT、Stanford、Harvard 等公開課程：數學基礎跟 LLM 預備知識的完整學習路線">數學公開課推薦&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">LLM 理論公開課推薦&lt;/a>&lt;/td>
 &lt;td>（交給推薦的課程跟書籍）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;p>本指南分成七個模組加一組前置卡片（111 張）。讀者依目的選讀、不需要從頭到尾全讀：&lt;/p></description><content:encoded><![CDATA[<p>本指南的核心目標是把「LLM 在寫 code 工作流的完整工程地圖」拆成可決策、可實作、可期望管理的工程問題。範圍覆蓋四條讀者旅程：(1) 在自己機器跑本地 LLM 寫 code 的最短可行路徑（Mac 或 PC）、(2) 想懂 LLM 內部運作機制（數學 + 理論基礎）、(3) 想做 LLM 應用開發（RAG / agent / tool use / VLM / benchmarking / 靜態 deployment）、(4) 關心 LLM 工作流的安全議題（本地 dev 視角 + 靜態網站視角）。網路上的 LLM 文章常把推論框架、加速技巧、應用模式、安全議題混為一談；本指南先把這些名詞放回正確的層級、再回答各層的具體取捨。</p>
<p>本指南預設讀者已經會用過雲端 LLM（ChatGPT、Claude）、熟悉終端機操作、想以工程視角理解 LLM。<strong>寫 code 場景是主要使用例、但模組二 / 三 / 四 / 六多數章節跨場景通用</strong>：想懂 reasoning model / RAG / embedding model 內部、即使不裝本地 LLM 也能讀。硬體前提分兩條路線：Apple Silicon Mac（M1 ~ M4、統一記憶體）走模組一；Windows / Linux + 獨立 GPU（NVIDIA / AMD、獨立 VRAM + 系統 RAM）走模組五。文章不販賣 LLM 焦慮、也不誇大本地能取代雲端的程度；它的責任是給每條讀者旅程的最短可行路徑、並標出每個階段的取捨。</p>
<p>模組零（心智模型）是所有讀者旅程的共同前置。模組一跟模組五是「裝本地 LLM」的兩條硬體路線、依平台選一條；想懂底層走模組二跟模組三（跟硬體無關、含 reasoning model / speculative decoding 等推論細節）；想看 LLM 作為系統元件走模組四（12 章涵蓋 RAG、tool use、agent、應用層協議、workflow、production resource、long context、embedding model、benchmarking、vision、靜態 deployment）；本地工作流跑穩想看安全議題走模組六（個人 dev 視角的供應鏈、伺服器綁定、tool use 權限、prompt injection、跨雲端邊界、production routing）。</p>
<h2 id="教材邊界">教材邊界</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>放在本指南</th>
          <th>不放在本指南</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>心智模型</td>
          <td>本地 vs 雲端的差異、為何 LLM 生字慢、三層架構（介面 / 伺服器 / 模型）、<a href="/blog/llm/00-foundations/openai-compatible-api/" data-link-title="0.3 OpenAI 相容 API" data-link-desc="為什麼幾乎所有本地 LLM 工具不用改就能切到本地：背後是同一套 API 形狀">OpenAI 相容 API</a></td>
          <td>雲端 GPU 租用、AGI 預測</td>
      </tr>
      <tr>
          <td>術語澄清</td>
          <td><a href="/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">MLX</a>、<a href="/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">MTP</a>、<a href="/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">oMLX</a>、<a href="/blog/llm/knowledge-cards/speculative-decoding/" data-link-title="Speculative Decoding" data-link-desc="用小模型猜未來 token、大模型並行驗證的加速技巧">speculative decoding</a>、<a href="/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化</a>、<a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a>、<a href="/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT</a>、<a href="/blog/llm/knowledge-cards/moe-cpu-offload/" data-link-title="MoE CPU 卸載" data-link-desc="把 Mixture-of-Experts 模型不活躍的專家層權重放在系統 RAM、用到再走 PCIe 拉回 GPU、讓有限 VRAM 跑得了更大模型">MoE CPU 卸載</a></td>
          <td>post-training fine-tuning 細節</td>
      </tr>
      <tr>
          <td>Mac 硬體現實</td>
          <td><a href="/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">記憶體預算與模型大小</a>、量化選擇、首字延遲、風扇與功耗</td>
          <td>雲端 GPU 租用、資料中心訓練</td>
      </tr>
      <tr>
          <td>PC 硬體現實</td>
          <td><a href="/blog/llm/05-discrete-gpu/vram-ram-budget/" data-link-title="5.0 VRAM &#43; RAM 分層預算" data-link-desc="PC 獨立 GPU 場景的記憶體預算判讀：VRAM 是快的世界、RAM 是大的世界、PCIe 把兩個世界連起來">VRAM + RAM 分層預算</a>、MoE 專家層 CPU 卸載、KV cache 量化、PCIe 頻寬限制</td>
          <td>多卡 NVLink、資料中心級分散式推論</td>
      </tr>
      <tr>
          <td>本地推論伺服器</td>
          <td><a href="/blog/llm/01-local-llm-services/ollama/" data-link-title="1.0 Ollama：主流推論伺服器" data-link-desc="一行 brew 裝完、ollama run 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on localhost:11434">Ollama</a>、<a href="/blog/llm/01-local-llm-services/lm-studio/" data-link-title="1.1 LM Studio：GUI 探索模型" data-link-desc="GUI 取向的本地推論伺服器：內建模型瀏覽器、speculative decoding 設定面板、適合探索新模型">LM Studio</a>、<a href="/blog/llm/01-local-llm-services/llama-cpp/" data-link-title="1.2 llama.cpp：底層推論引擎" data-link-desc="GGUF 格式、量化、MTP 仍 beta；多數讀者不需要直接接觸，Ollama 已經包好">llama.cpp</a>（Mac + PC 通用）</td>
          <td>vLLM、TGI、Triton 等資料中心級 inference server</td>
      </tr>
      <tr>
          <td>編輯器整合</td>
          <td><a href="/blog/llm/01-local-llm-services/vscode-continue-integration/" data-link-title="1.3 VS Code &#43; Continue.dev 整合" data-link-desc="安裝 Continue 擴充套件、config.json 設定、Cmd&#43;L 對話 / Cmd&#43;I 行內編輯快捷鍵">Continue.dev + VS Code</a>、Cursor 對應關係</td>
          <td>JetBrains 全套整合、Vim / Emacs 進階 plugin</td>
      </tr>
      <tr>
          <td>模型挑選</td>
          <td><a href="/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">coding 場景的模型優先順序</a>、量化等級對體感影響</td>
          <td>benchmark 跑分方法論的完整推導</td>
      </tr>
      <tr>
          <td>期望管理</td>
          <td><a href="/blog/llm/01-local-llm-services/expectation-management/" data-link-title="1.5 期望管理：本地 LLM 的擅長領域與分工" data-link-desc="本地 LLM 是免費的初階 pair programmer：辨識它的擅長領域、跟雲端旗艦做結構性分工">本地 LLM 的擅長領域與分工</a>、混用雲端的時機</td>
          <td>LLM 通用能力評估、AGI 預測</td>
      </tr>
      <tr>
          <td>數學基礎</td>
          <td><a href="/blog/llm/02-math-foundations/linear-algebra-for-llm/" data-link-title="2.0 線性代數：向量、矩陣、空間" data-link-desc="LLM 內部運算的基底：向量、矩陣、向量空間、內積、norm、矩陣乘法的角色">線性代數</a>、<a href="/blog/llm/02-math-foundations/probability-and-information/" data-link-title="2.1 機率與資訊論" data-link-desc="LLM 輸出的本質是機率分佈：softmax、cross-entropy、KL divergence、perplexity 在訓練與推論中的角色">機率與資訊論</a>、<a href="/blog/llm/02-math-foundations/calculus-and-optimization/" data-link-title="2.2 微積分與最佳化" data-link-desc="從 gradient、chain rule 到 SGD / Adam：LLM 訓練如何更新數十億參數">最佳化</a>、<a href="/blog/llm/02-math-foundations/numerical-precision/" data-link-title="2.3 數值精度與量化的數學依據" data-link-desc="fp32 / bf16 / fp16 / int8 / int4 的差別、量化能省哪些 bits、品質衰減從哪裡來">數值精度</a> 在 LLM 中的角色</td>
          <td>完整數學證明、測度論等屬於數學系範圍的主題</td>
      </tr>
      <tr>
          <td>理論基礎</td>
          <td><a href="/blog/llm/03-theoretical-foundations/neural-network-basics/" data-link-title="3.0 神經網路基礎" data-link-desc="從單一 neuron 到 multi-layer：weights、activation function、forward / backward pass 的角色">神經網路</a>、<a href="/blog/llm/03-theoretical-foundations/embedding-spaces/" data-link-title="3.1 Embedding 空間" data-link-desc="token 怎麼變成向量、為什麼相似 token 在向量空間中靠近、embedding 是怎麼學出來的">embedding</a>、<a href="/blog/llm/03-theoretical-foundations/attention-mechanism/" data-link-title="3.2 Attention 機制" data-link-desc="Query / Key / Value、scaled dot-product attention、multi-head attention：Transformer 的核心運算">attention</a>、<a href="/blog/llm/03-theoretical-foundations/transformer-architecture/" data-link-title="3.3 Transformer 架構細節" data-link-desc="Decoder-only 結構、Transformer block、positional encoding、layer norm、residual stream">Transformer</a>、<a href="/blog/llm/03-theoretical-foundations/training-pipeline/" data-link-title="3.4 訓練流程：pre-train → SFT → RLHF" data-link-desc="LLM 的三階段訓練：預訓練、指令微調、人類反饋強化學習；各階段目標與最新替代方案">訓練流程</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>、<a href="/blog/llm/03-theoretical-foundations/tokenization-algorithms/" data-link-title="3.6 Tokenization：BPE、SentencePiece、Tiktoken" data-link-desc="把文字切成 token 的算法：為什麼不同模型切出不同 token 數、tokenizer 選擇對能力的影響">tokenization</a>、<a href="/blog/llm/03-theoretical-foundations/cross-language-tokenization/" data-link-title="3.7 跨語言場景的 tokenizer 與訓練分佈原理" data-link-desc="為什麼模型對不同語言表現不一致：tokenizer &#43; 訓練資料分佈雙因素、語言選擇取捨">跨語言原理</a></td>
          <td>多模態擴展、最新研究細節交給 Stanford CS25</td>
      </tr>
      <tr>
          <td>應用層原理</td>
          <td><a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">RAG</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>、<a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">Agent 架構</a>、<a href="/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">應用層協議</a>、<a href="/blog/llm/04-applications/workflow-patterns/" data-link-title="4.7 Workflow 編排模式" data-link-desc="Pipeline / router / parallel / reflection：多 LLM call 組合的四種基本模式與退化條件">Workflow 編排</a>、<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 的設計取捨">Production resource</a>、<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 取捨">Artifact 管理</a></td>
          <td>具體 framework 教學（LangChain / LlamaIndex）、prompt engineering</td>
      </tr>
      <tr>
          <td>進階理論</td>
          <td><a href="/blog/llm/03-theoretical-foundations/reasoning-models/" data-link-title="3.8 Reasoning models：test-time compute paradigm" data-link-desc="Chain-of-thought 從 prompting 技巧演化成訓練 paradigm、reasoning model 的內部運作、本地可跑的選項與適用任務">Reasoning models</a>（o1 / R1 / QwQ 風格）、<a href="/blog/llm/03-theoretical-foundations/speculative-decoding-internals/" data-link-title="3.9 Speculative decoding 內部：drafter / 驗證 / 加速上限" data-link-desc="speculative decoding 的演算法細節、drafter 跟 target 怎麼配對、acceptance rate 怎麼決定實際加速、MTP 跟 EAGLE 等變體">Speculative decoding 內部</a>（drafter / MTP / EAGLE）</td>
          <td>完整 paper 推導、最新研究 frontier</td>
      </tr>
      <tr>
          <td>進階應用</td>
          <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 取捨">Long context engineering</a>、<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">Embedding model 內部</a>、<a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">Benchmarking</a>、<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 整合現狀">Vision in coding</a>、<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">靜態 / serverless RAG deployment</a></td>
          <td>完整 LangChain / LlamaIndex 教學</td>
      </tr>
      <tr>
          <td>Fine-tuning</td>
          <td>原理（<a href="/blog/llm/knowledge-cards/lora/" data-link-title="LoRA" data-link-desc="Low-Rank Adaptation：凍住原模型權重、只訓兩個小矩陣的 parameter-efficient fine-tuning">LoRA</a> / <a href="/blog/llm/knowledge-cards/qlora/" data-link-title="QLoRA" data-link-desc="把 base model 量化到 4-bit &#43; LoRA fine-tune 的組合、消費級 GPU 也能 fine-tune 大模型">QLoRA</a> / <a href="/blog/llm/knowledge-cards/catastrophic-forgetting/" data-link-title="Catastrophic Forgetting" data-link-desc="Fine-tune 模型時、新訓練資料覆蓋掉原本學到的能力的現象、LoRA / 資料 mixing 是主要緩解">catastrophic forgetting</a>）+ <a href="/blog/llm/01-local-llm-services/hands-on/local-fine-tuning/" data-link-title="Hands-on：用 QLoRA 在本機 fine-tune coding 模型" data-link-desc="Apple Silicon Mac / PC 獨立 GPU 上跑 QLoRA fine-tune 的完整流程：環境、資料、訓練、evaluation、合併、部署到 Ollama">本機 hands-on</a></td>
          <td>完整資料工程、large-scale distributed fine-tune</td>
      </tr>
      <tr>
          <td>隱私 / 安全</td>
          <td><a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">隱私資料流</a>、<a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">本地 dev 安全模組</a>（供應鏈 / 伺服器綁定 / tool use / prompt injection / 跨雲端邊界 / production routing）、<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">靜態網站 RAG 資安</a>、<a href="/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">排錯方法論</a></td>
          <td>企業合規逐條檢核、SOC 2 / HIPAA 流程</td>
      </tr>
      <tr>
          <td>進一步學習</td>
          <td><a href="/blog/llm/02-math-foundations/going-deeper-math/" data-link-title="2.4 想學更深：推薦公開課程" data-link-desc="MIT、Stanford、Harvard 等公開課程：數學基礎跟 LLM 預備知識的完整學習路線">數學公開課推薦</a>、<a href="/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">LLM 理論公開課推薦</a></td>
          <td>（交給推薦的課程跟書籍）</td>
      </tr>
  </tbody>
</table>
<h2 id="學習路線">學習路線</h2>
<p>本指南分成七個模組加一組前置卡片（111 張）。讀者依目的選讀、不需要從頭到尾全讀：</p>
<ul>
<li><strong>想用 Apple Silicon Mac 裝本地 LLM 寫 code</strong>：讀模組零 + 模組一（最短路徑）</li>
<li><strong>想用 Windows / Linux + 獨立 GPU 裝</strong>：讀模組零 + 模組五</li>
<li><strong>想懂 LLM 內部原理</strong>：模組二（數學） + 模組三（理論、含 reasoning models / speculative decoding）— 跟硬體無關</li>
<li><strong>想做 LLM 應用開發（含 RAG / agent / VLM / 靜態 deployment）</strong>：模組四（12 章、跨工具世代不變的原理）— 跟硬體無關</li>
<li><strong>想懂本地工作流的安全議題</strong>：模組一 / 五跑穩後接模組六（個人 dev 視角）</li>
<li><strong>想選 RAG 的 storage 方案（pickle / vector DB / hosted SaaS）</strong>：直接看 <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 RAG storage 工程</a></li>
<li><strong>想在靜態網站加 RAG / 智能搜尋</strong>：直接看 <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 靜態 / serverless RAG deployment</a></li>
<li><strong>想在本機 fine-tune 模型</strong>：模組三 3.4 訓練流程原理 → <a href="/blog/llm/01-local-llm-services/hands-on/local-fine-tuning/" data-link-title="Hands-on：用 QLoRA 在本機 fine-tune coding 模型" data-link-desc="Apple Silicon Mac / PC 獨立 GPU 上跑 QLoRA fine-tune 的完整流程：環境、資料、訓練、evaluation、合併、部署到 Ollama">本機 QLoRA hands-on</a></li>
<li><strong>想跟最新進展接軌</strong>：讀完模組後進推薦的公開課程跟 paper（模組二 2.4 + 模組三 3.10）</li>
</ul>
<h3 id="前置知識卡片"><a href="/blog/llm/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理本地 LLM 寫 code 場景所需的概念詞彙">前置知識卡片</a></h3>
<p>用原子化卡片整理 <a href="/blog/llm/knowledge-cards/token/" data-link-title="Token" data-link-desc="LLM 處理文字時的最小單位：介於字元與單字之間">token</a>、<a href="/blog/llm/knowledge-cards/autoregressive/" data-link-title="Autoregressive" data-link-desc="LLM 一次生成一個 token、把已生成內容作為下一次輸入的架構">自回歸</a>、<a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a>、<a href="/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化</a>、<a href="/blog/llm/knowledge-cards/speculative-decoding/" data-link-title="Speculative Decoding" data-link-desc="用小模型猜未來 token、大模型並行驗證的加速技巧">speculative decoding</a>、<a href="/blog/llm/knowledge-cards/mtp/" data-link-title="Multi-Token Prediction (MTP)" data-link-desc="Google 為 Gemma 系列釋出的 speculative decoding 工程化實作">MTP</a>、<a href="/blog/llm/knowledge-cards/mlx/" data-link-title="MLX" data-link-desc="Apple 釋出的 Apple Silicon 數值運算 framework：類似 PyTorch / JAX 的 Mac 對應物">MLX</a>、<a href="/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器</a>、<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/memory-bandwidth/" data-link-title="Memory Bandwidth" data-link-desc="記憶體每秒能讀寫多少 bytes：決定本地 LLM 生字速度的真正瓶頸">memory bandwidth</a>、<a href="/blog/llm/knowledge-cards/unified-memory/" data-link-title="Unified Memory Architecture" data-link-desc="Apple Silicon 讓 CPU / GPU / NE 共用同一塊記憶體：跑大模型的優勢來源">統一記憶體</a>、<a href="/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT</a>、<a href="/blog/llm/knowledge-cards/prefill/" data-link-title="Prefill" data-link-desc="Prompt 首次處理時的計算階段：把整段輸入跑過模型、產生 KV cache">prefill</a>、<a href="/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">context window</a>、<a href="/blog/llm/knowledge-cards/transformer/" data-link-title="Transformer" data-link-desc="寫 code 用的 LLM 神經網路架構：基於 attention 機制、自回歸生成 token">Transformer</a>、<a href="/blog/llm/knowledge-cards/diffusion/" data-link-title="Diffusion" data-link-desc="產圖用的生成式 AI 架構：跟寫 code 用的 Transformer 是不同路線">Diffusion</a> 等核心概念。章節文章專注情境推導、術語背景交由卡片維持一致。</p>
<h3 id="模組零基礎知識與心智模型"><a href="/blog/llm/00-foundations/" data-link-title="模組零：基礎知識與心智模型" data-link-desc="建立本地 LLM 的心智模型、釐清 MLX / MTP / oMLX 等常被混淆的術語、Apple Silicon 記憶體現實">模組零：基礎知識與心智模型</a></h3>
<p>整理本地 vs 雲端 LLM 的差異、自回歸架構與記憶體頻寬瓶頸、介面 / 伺服器 / 模型三層心智模型、OpenAI 相容 API 為何重要、MLX / MTP / oMLX 三個容易搞混的術語、Apple Silicon Mac 記憶體與模型大小的對應關係、判讀本地 LLM 資訊的五個框架。</p>
<h3 id="模組一本地-llm-服務的安裝與應用"><a href="/blog/llm/01-local-llm-services/" data-link-title="模組一：本地 LLM 服務的安裝與應用" data-link-desc="Ollama、LM Studio、llama.cpp 的安裝與差異、VS Code &#43; Continue.dev 整合、模型選型與期望管理">模組一：本地 LLM 服務的安裝與應用</a></h3>
<p>整理 Ollama、LM Studio、llama.cpp 三個主流推論伺服器的現況差異與安裝路徑、用 Continue.dev 把本地 LLM 接到 VS Code 的完整步驟、寫 code 場景下模型選型的優先順序、本地模型的期望管理、想進一步玩 coding agent、Web UI、產圖時的延伸方向。</p>
<h3 id="模組二llm-的數學基礎"><a href="/blog/llm/02-math-foundations/" data-link-title="模組二：LLM 的數學基礎" data-link-desc="整理 LLM 推論背後需要理解的線性代數、機率與資訊論、最佳化、數值精度等數學概念">模組二：LLM 的數學基礎</a></h3>
<p>整理 LLM 推論背後的數學工具：<a href="/blog/llm/02-math-foundations/linear-algebra-for-llm/" data-link-title="2.0 線性代數：向量、矩陣、空間" data-link-desc="LLM 內部運算的基底：向量、矩陣、向量空間、內積、norm、矩陣乘法的角色">線性代數</a>（向量、矩陣、空間）、<a href="/blog/llm/02-math-foundations/probability-and-information/" data-link-title="2.1 機率與資訊論" data-link-desc="LLM 輸出的本質是機率分佈：softmax、cross-entropy、KL divergence、perplexity 在訓練與推論中的角色">機率與資訊論</a>（softmax、cross-entropy、KL、perplexity）、<a href="/blog/llm/02-math-foundations/calculus-and-optimization/" data-link-title="2.2 微積分與最佳化" data-link-desc="從 gradient、chain rule 到 SGD / Adam：LLM 訓練如何更新數十億參數">微積分與最佳化</a>（gradient、SGD / Adam）、<a href="/blog/llm/02-math-foundations/numerical-precision/" data-link-title="2.3 數值精度與量化的數學依據" data-link-desc="fp32 / bf16 / fp16 / int8 / int4 的差別、量化能省哪些 bits、品質衰減從哪裡來">數值精度</a>（fp32 / bf16 / Q4 / Q8 的取捨）。每章末尾接到<a href="/blog/llm/02-math-foundations/going-deeper-math/" data-link-title="2.4 想學更深：推薦公開課程" data-link-desc="MIT、Stanford、Harvard 等公開課程：數學基礎跟 LLM 預備知識的完整學習路線">公開課推薦</a>。</p>
<h3 id="模組三llm-的理論基礎"><a href="/blog/llm/03-theoretical-foundations/" data-link-title="模組三：LLM 的理論基礎" data-link-desc="從神經網路、embedding、attention、Transformer 架構、訓練到 sampling：LLM 內部運作的完整理論圖像">模組三：LLM 的理論基礎</a></h3>
<p>整理 LLM 內部運作機制、共 11 章：<a href="/blog/llm/03-theoretical-foundations/neural-network-basics/" data-link-title="3.0 神經網路基礎" data-link-desc="從單一 neuron 到 multi-layer：weights、activation function、forward / backward pass 的角色">神經網路基礎</a>、<a href="/blog/llm/03-theoretical-foundations/embedding-spaces/" data-link-title="3.1 Embedding 空間" data-link-desc="token 怎麼變成向量、為什麼相似 token 在向量空間中靠近、embedding 是怎麼學出來的">embedding 空間</a>、<a href="/blog/llm/03-theoretical-foundations/attention-mechanism/" data-link-title="3.2 Attention 機制" data-link-desc="Query / Key / Value、scaled dot-product attention、multi-head attention：Transformer 的核心運算">attention 機制</a>、<a href="/blog/llm/03-theoretical-foundations/transformer-architecture/" data-link-title="3.3 Transformer 架構細節" data-link-desc="Decoder-only 結構、Transformer block、positional encoding、layer norm、residual stream">Transformer 架構</a>、<a href="/blog/llm/03-theoretical-foundations/training-pipeline/" data-link-title="3.4 訓練流程：pre-train → SFT → RLHF" data-link-desc="LLM 的三階段訓練：預訓練、指令微調、人類反饋強化學習；各階段目標與最新替代方案">訓練流程</a>（pre-train → SFT → RLHF / DPO）、<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>、<a href="/blog/llm/03-theoretical-foundations/tokenization-algorithms/" data-link-title="3.6 Tokenization：BPE、SentencePiece、Tiktoken" data-link-desc="把文字切成 token 的算法：為什麼不同模型切出不同 token 數、tokenizer 選擇對能力的影響">tokenization 算法</a>、<a href="/blog/llm/03-theoretical-foundations/cross-language-tokenization/" data-link-title="3.7 跨語言場景的 tokenizer 與訓練分佈原理" data-link-desc="為什麼模型對不同語言表現不一致：tokenizer &#43; 訓練資料分佈雙因素、語言選擇取捨">跨語言場景原理</a>、<a href="/blog/llm/03-theoretical-foundations/reasoning-models/" data-link-title="3.8 Reasoning models：test-time compute paradigm" data-link-desc="Chain-of-thought 從 prompting 技巧演化成訓練 paradigm、reasoning model 的內部運作、本地可跑的選項與適用任務">Reasoning models</a>（o1 / R1 / QwQ 等 test-time compute paradigm）、<a href="/blog/llm/03-theoretical-foundations/speculative-decoding-internals/" data-link-title="3.9 Speculative decoding 內部：drafter / 驗證 / 加速上限" data-link-desc="speculative decoding 的演算法細節、drafter 跟 target 怎麼配對、acceptance rate 怎麼決定實際加速、MTP 跟 EAGLE 等變體">Speculative decoding 內部</a>（drafter / MTP / EAGLE）。每章末尾接到<a href="/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">公開課推薦</a>（Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI）。</p>
<h3 id="模組四llm-應用層原理"><a href="/blog/llm/04-applications/" data-link-title="模組四：LLM 應用層原理" data-link-desc="Prompt 技術光譜、RAG、tool use、agent、應用層協議、人機協作、multi-agent、workflow 編排、eval 設計：跨工具不變的概念地圖">模組四：LLM 應用層原理</a></h3>
<p>整理 LLM 作為系統元件的設計原理、共 12 章：<a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">RAG</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>、<a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">agent 架構</a>、<a href="/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">應用層協議</a>、<a href="/blog/llm/04-applications/workflow-patterns/" data-link-title="4.7 Workflow 編排模式" data-link-desc="Pipeline / router / parallel / reflection：多 LLM call 組合的四種基本模式與退化條件">workflow 編排模式</a>、<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 的設計取捨">Production resource planning</a>、<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 取捨">衍生產物管理</a>、<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 取捨">Long context engineering</a>、<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">Embedding model 內部</a>、<a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">Benchmarking 方法論</a>、<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 整合現狀">Vision in coding workflow</a>（本地 VLM 接 IDE）、<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">靜態 / serverless RAG deployment</a>（沒 backend 場景）。本模組刻意只寫跨工具世代不變的原理、避開 LangChain / LlamaIndex 等具體 framework 教學。</p>
<h3 id="模組五windows--linux--獨立-gpu"><a href="/blog/llm/05-discrete-gpu/" data-link-title="模組五：Windows / Linux &#43; 獨立 GPU" data-link-desc="消費級 PC（Windows / Linux &#43; NVIDIA / AMD 獨立 GPU）跑本地 LLM 的硬體判讀、MoE CPU 卸載、KV cache 量化與 llama.cpp 調參">模組五：Windows / Linux + 獨立 GPU</a></h3>
<p>整理消費級 PC（Windows / Linux + NVIDIA / AMD 獨立 GPU）跑本地 LLM 的硬體判讀模型與工程選項：<a href="/blog/llm/05-discrete-gpu/vram-ram-budget/" data-link-title="5.0 VRAM &#43; RAM 分層預算" data-link-desc="PC 獨立 GPU 場景的記憶體預算判讀：VRAM 是快的世界、RAM 是大的世界、PCIe 把兩個世界連起來">VRAM + RAM 分層預算</a>、MoE 模型的 <a href="/blog/llm/knowledge-cards/moe-cpu-offload/" data-link-title="MoE CPU 卸載" data-link-desc="把 Mixture-of-Experts 模型不活躍的專家層權重放在系統 RAM、用到再走 PCIe 拉回 GPU、讓有限 VRAM 跑得了更大模型">CPU 卸載策略</a>（<code>--n-cpu-moe</code>）、KV cache 量化（K=Q8 / V=Q4）跟 context 長度的權衡、llama.cpp 在 PC 上的調參空間。本模組跟模組一是平行的硬體路線、共用模組零的心智模型跟卡片。</p>
<h3 id="模組六本地-llm-的安全與權限"><a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六：本地 LLM 的安全與權限</a></h3>
<p>整理個人 dev 在自己機器上跑本地 LLM 的安全議題：<a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">模型供應鏈與信任邊界</a>、<a href="/blog/llm/06-security/inference-server-binding/" data-link-title="6.1 推論伺服器的綁定與暴露範圍" data-link-desc="個人 dev 場景下 llama-server / Ollama / LM Studio 的 bind address 判讀：127.0.0.1 vs LAN vs 反代、預設安全、誤開放給內網的後果">推論伺服器的綁定與暴露範圍</a>、<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 信任、副作用的可逆性">tool use 與 MCP server 的權限模型</a>、<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 場景的差異">IDE 場景的 prompt injection</a>、<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 設定、隱私資料流、按敏感度分流的判讀">跨雲端 / 本地的資料邊界</a>、<a href="/blog/llm/06-security/routing-to-production-security/" data-link-title="6.5 跨進 production 的 routing 中樞" data-link-desc="個人 dev → 團隊 → production LLM 服務的三層演化、跟 backend/07 對應卡片的 routing 清單">跨進 production 的 routing 中樞</a>。framing 是個人 dev 視角、不是 enterprise 資安管理；production / 多租戶 LLM 服務的特殊資安議題見 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">Backend 模組七 資安與資料保護</a> 的 LLM 相關章節。</p>
<h2 id="模組之間怎麼配合">模組之間怎麼配合</h2>
<table>
  <thead>
      <tr>
          <th>模組</th>
          <th>角度</th>
          <th>跟其他模組的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模組零</td>
          <td>操作層心智模型</td>
          <td>是模組一跟模組五的共同前置</td>
      </tr>
      <tr>
          <td>模組一</td>
          <td>工具層、Mac 實際安裝</td>
          <td>用模組零的詞彙、跟模組三的理論互補</td>
      </tr>
      <tr>
          <td>模組二</td>
          <td>數學工具</td>
          <td>提供模組三需要的數學詞彙、跟硬體平台無關</td>
      </tr>
      <tr>
          <td>模組三</td>
          <td>理論機制</td>
          <td>用模組二的工具拼出完整 LLM、跟硬體平台無關</td>
      </tr>
      <tr>
          <td>模組四</td>
          <td>應用層原理</td>
          <td>用前面模組建的詞彙、看 LLM 作為系統元件</td>
      </tr>
      <tr>
          <td>模組五</td>
          <td>工具層、PC 獨立 GPU</td>
          <td>跟模組一平行、用模組零的詞彙、處理 VRAM 場景</td>
      </tr>
      <tr>
          <td>模組六</td>
          <td>安全層、個人 dev 視角</td>
          <td>在模組一 / 五的工作流上加安全判讀、cross-link backend/07 通用資安卡片</td>
      </tr>
  </tbody>
</table>
<p>模組二跟模組三可並讀。閱讀模組三遇到陌生數學詞時跳回模組二補完、再回模組三繼續。模組四在前面模組之上、但讀者熟悉 LLM 應用詞彙也可直接從這裡讀起。模組一跟模組五依硬體選一條主路線、共用模組零的心智模型與 <a href="/blog/llm/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理本地 LLM 寫 code 場景所需的概念詞彙">knowledge-cards</a>。模組六在模組一 / 五跑穩後接、處理「跑起來後該注意什麼」。</p>
<h2 id="適合的讀者">適合的讀者</h2>
<table>
  <thead>
      <tr>
          <th>背景</th>
          <th>適合程度</th>
          <th>建議起點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用過 ChatGPT / Claude、沒碰過本地模型</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/00-foundations/" data-link-title="模組零：基礎知識與心智模型" data-link-desc="建立本地 LLM 的心智模型、釐清 MLX / MTP / oMLX 等常被混淆的術語、Apple Silicon 記憶體現實">模組零</a> 從頭讀</td>
      </tr>
      <tr>
          <td>裝過 Ollama 但被網路上的術語混淆</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">MLX / MTP / oMLX 區分</a> + <a href="/blog/llm/00-foundations/info-judgment-frames/" data-link-title="0.6 判讀本地 LLM 資訊的五個框架" data-link-desc="本地 LLM 資訊更新快，學會用版本、層級、變數、能力、資料流五個框架評估文章與宣稱">判讀框架</a></td>
      </tr>
      <tr>
          <td>想知道 24GB / 32GB Mac 該選哪個模型</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">硬體記憶體預算</a> + <a href="/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">模型選型</a></td>
      </tr>
      <tr>
          <td>想用本地 LLM 完全取代 Claude / GPT-5</td>
          <td>部分適合</td>
          <td><a href="/blog/llm/01-local-llm-services/expectation-management/" data-link-title="1.5 期望管理：本地 LLM 的擅長領域與分工" data-link-desc="本地 LLM 是免費的初階 pair programmer：辨識它的擅長領域、跟雲端旗艦做結構性分工">期望管理</a> 先看完再決定</td>
      </tr>
      <tr>
          <td>想懂 LLM 內部運作機制</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/03-theoretical-foundations/" data-link-title="模組三：LLM 的理論基礎" data-link-desc="從神經網路、embedding、attention、Transformer 架構、訓練到 sampling：LLM 內部運作的完整理論圖像">模組三 理論基礎</a> 從頭讀（含 reasoning models / speculative decoding）</td>
      </tr>
      <tr>
          <td>想懂背後的數學</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/02-math-foundations/" data-link-title="模組二：LLM 的數學基礎" data-link-desc="整理 LLM 推論背後需要理解的線性代數、機率與資訊論、最佳化、數值精度等數學概念">模組二 數學基礎</a> 從頭讀</td>
      </tr>
      <tr>
          <td>想懂 o1 / DeepSeek-R1 等 reasoning model 怎麼運作</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/03-theoretical-foundations/reasoning-models/" data-link-title="3.8 Reasoning models：test-time compute paradigm" data-link-desc="Chain-of-thought 從 prompting 技巧演化成訓練 paradigm、reasoning model 的內部運作、本地可跑的選項與適用任務">3.8 Reasoning models</a> 從頭讀</td>
      </tr>
      <tr>
          <td>想做 LLM 應用開發（RAG / agent / tool use）</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/04-applications/" data-link-title="模組四：LLM 應用層原理" data-link-desc="Prompt 技術光譜、RAG、tool use、agent、應用層協議、人機協作、multi-agent、workflow 編排、eval 設計：跨工具不變的概念地圖">模組四</a> 從 4.0 RAG 依序讀</td>
      </tr>
      <tr>
          <td>想在自家 Hugo / Astro 等靜態網站加 RAG</td>
          <td>直接適合</td>
          <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 靜態 / serverless RAG deployment</a>（含資安取捨）</td>
      </tr>
      <tr>
          <td>想用 VLM 看截圖 / 設計稿輔助寫 code</td>
          <td>直接適合</td>
          <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 Vision in coding workflow</a></td>
      </tr>
      <tr>
          <td>想評估 LLM benchmark 數字、做 in-house eval</td>
          <td>直接適合</td>
          <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 Benchmarking 方法論</a></td>
      </tr>
      <tr>
          <td>想在本機 fine-tune 模型懂自家 codebase 慣例</td>
          <td>直接適合</td>
          <td><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> 原理 + <a href="/blog/llm/01-local-llm-services/hands-on/local-fine-tuning/" data-link-title="Hands-on：用 QLoRA 在本機 fine-tune coding 模型" data-link-desc="Apple Silicon Mac / PC 獨立 GPU 上跑 QLoRA fine-tune 的完整流程：環境、資料、訓練、evaluation、合併、部署到 Ollama">QLoRA hands-on</a></td>
      </tr>
      <tr>
          <td>想做 large-scale fine-tune / 從頭訓練</td>
          <td>部分適合</td>
          <td>讀完模組三後進入 <a href="/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">推薦的公開課程</a> 跟 Stanford CS336</td>
      </tr>
      <tr>
          <td>用 Windows / Linux + NVIDIA / AMD 獨立 GPU 跑本地 LLM</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/00-foundations/" data-link-title="模組零：基礎知識與心智模型" data-link-desc="建立本地 LLM 的心智模型、釐清 MLX / MTP / oMLX 等常被混淆的術語、Apple Silicon 記憶體現實">模組零</a> 建心智模型 + <a href="/blog/llm/05-discrete-gpu/" data-link-title="模組五：Windows / Linux &#43; 獨立 GPU" data-link-desc="消費級 PC（Windows / Linux &#43; NVIDIA / AMD 獨立 GPU）跑本地 LLM 的硬體判讀、MoE CPU 卸載、KV cache 量化與 llama.cpp 調參">模組五</a> 處理 VRAM 預算、MoE 卸載、KV cache 量化</td>
      </tr>
      <tr>
          <td>想知道本地 LLM 跑起來後的安全議題</td>
          <td>直接適合</td>
          <td><a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六</a> 個人 dev 視角的安全與權限</td>
      </tr>
      <tr>
          <td>想把 LLM 部署成 production 服務、處理服務化資安</td>
          <td>部分適合</td>
          <td>個人視角見 <a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六</a>；production 場景見 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">Backend 模組七 資安</a> 的 LLM 相關章節</td>
      </tr>
      <tr>
          <td>想在資料中心級 GPU（H100 / H200 / B200）部署</td>
          <td>部分適合</td>
          <td>心智模型跟 <a href="/blog/llm/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理本地 LLM 寫 code 場景所需的概念詞彙">knowledge-cards</a> 通用；vLLM / TGI / Triton 等資料中心 inference server 另尋專門教材</td>
      </tr>
      <tr>
          <td>想跑 Stable Diffusion / Midjourney 等產圖</td>
          <td>跟主題不同</td>
          <td>產圖是 Diffusion 架構、見 <a href="/blog/llm/knowledge-cards/diffusion/" data-link-title="Diffusion" data-link-desc="產圖用的生成式 AI 架構：跟寫 code 用的 Transformer 是不同路線">Diffusion 卡片</a>、另尋 ComfyUI / Draw Things 教材</td>
      </tr>
  </tbody>
</table>
<h2 id="用語約定">用語約定</h2>
<p>本指南使用的關鍵術語在第一次出現時都附原文。為避免歧義，下列詞彙在本指南內固定指涉：</p>
<ol>
<li><strong>本地 LLM</strong>：跑在使用者自己機器（Mac 或 PC）上的大型語言模型推論、prompt 留在本機。</li>
<li><strong>推論伺服器</strong>（inference server）：負責載入模型權重、處理 prompt、產生 token 的常駐程式、例如 Ollama、LM Studio 內建 server、llama.cpp <code>server</code>。</li>
<li><strong>介面層</strong>：使用者實際打字互動的工具、例如 VS Code + Continue.dev、CLI、Web UI。介面層透過 API 跟推論伺服器溝通。</li>
<li><strong>模型</strong>（model）：權重檔本身、例如 <code>gemma4:31b</code>、<code>qwen3-coder:30b</code>。模型可以在不同推論伺服器之間共用、前提是格式相容。</li>
<li><strong>量化</strong>（quantization）：把模型權重從高精度（如 bf16）壓成低精度（如 Q4）以減少記憶體佔用、代價是少許品質下降。</li>
</ol>
<h2 id="不在本指南內的主題">不在本指南內的主題</h2>
<p>本指南不討論：</p>
<ul>
<li><strong>Speech / audio LLM</strong>：跟核心文字 LLM 是不同方向、本指南不涵蓋。Vision（VLM）原本不放、但因 coding 工作流的 vision use case 進入主流、補上 <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 Vision in coding workflow</a>；video LLM 仍不放。</li>
<li><strong>資料中心訓練的工程細節</strong>：data parallelism、ZeRO、tensor parallelism 等屬於專門課程的範圍。</li>
<li><strong>向量資料庫的 vendor 比較</strong>（Pinecone vs Weaviate vs Chroma 等）：vendor 格局半年一變、不適合寫入教材。RAG 的 storage 工程原理（升級判讀、index 生命週期、dependency 約束）見 <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 RAG storage 工程</a>。</li>
<li><strong>Kubernetes / 資料中心級分散式推論</strong>：跟個人機器本地 LLM 方向不同、需另尋專門教材。</li>
<li><strong>多卡 NVLink、tensor parallelism</strong>：消費級 PC 場景通常單卡、本指南不涵蓋多卡分散式推論。</li>
</ul>
<p>若讀完本指南後想往這些方向走：</p>
<ol>
<li><strong>想做 <a href="/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a> 應用</strong>：先把 Ollama + Continue.dev 跑穩、再讀 <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 RAG 原理</a> 建立設計取捨判讀、或 <a href="/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">模組三 3.8 推薦</a> 的 DeepLearning.AI short courses。</li>
<li><strong>想跑 coding <a href="/blog/llm/knowledge-cards/agent/" data-link-title="LLM Agent" data-link-desc="把控制流交給 LLM 的應用模式：自主決策、跨多步呼叫工具、人類角色從主導變監督">agent</a></strong>：先讀 <a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent 架構原理</a> 建立判讀、再看 <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> 了解 aider、Cline 等工具的定位差異。</li>
<li><strong>想跑產圖模型</strong>：<a href="/blog/llm/knowledge-cards/diffusion/" data-link-title="Diffusion" data-link-desc="產圖用的生成式 AI 架構：跟寫 code 用的 Transformer 是不同路線">Diffusion</a> 跟 Transformer 是不同架構、請另尋 ComfyUI / Draw Things / Diffusers 教材。</li>
<li><strong>想自己訓練 / fine-tune</strong>：讀完模組三、進入 Karpathy zero-to-hero、Stanford CS336、Hugging Face NLP Course 等<a href="/blog/llm/03-theoretical-foundations/going-deeper-theory/" data-link-title="3.11 想學更深：推薦公開課程" data-link-desc="Karpathy、Stanford CS224N / CS25 / CS336、DeepLearning.AI、Hugging Face：LLM 理論深入學習的完整路線">推薦資源</a>。</li>
</ol>
<hr>
<p><em>文件版本：v0.7.0</em>
<em>最後更新：2026-05-12</em>
<em>系列狀態：七個模組 + 125 張知識卡片。模組零（9 章）/ 一（10 章 + hands-on、含 QLoRA + judge harness）/ 二（5 章）/ 三（12 章、含 reasoning / speculative / constrained decoding）/ 四（17 章、含 long context / embedding / benchmarking / VLM / 靜態 deployment / coding agent harness / prompt caching / agent memory / tracing / LLM-as-judge）/ 五（7 章）/ 六（7 章、含 OWASP 對照）。</em></p>
]]></content:encoded></item><item><title>Hands-on：本地 AI 工具實作筆記</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/</guid><description>&lt;p>本子資料夾收錄本地 AI 工具的實際安裝跟驗證紀錄。跟 1.x 原理章節的關係：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>1.x 原理章節&lt;/th>
 &lt;th>Hands-on 紀錄&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>為什麼選 Ollama&lt;/td>
 &lt;td>實際 &lt;code>brew install&lt;/code> + &lt;code>ollama pull&lt;/code> 流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Speculative decoding 原理&lt;/td>
 &lt;td>MTP 模型實際載入 + 速度量測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ComfyUI 在生態的位置&lt;/td>
 &lt;td>實際 git clone + Python 環境 + 模型路徑配置&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>本資料夾的內容&lt;strong>會隨工具版本演化&lt;/strong>：指令、目錄結構、相依套件版本都會變。寫的時間戳記在每篇開頭、版本資訊在 frontmatter。跟 1.x 原理章節的差別是「原理跨工具世代不變、實作筆記是當下這版的快照」。&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>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/quickstart/" data-link-title="Hands-on Quickstart：clone repo 後跑通所有 demo" data-link-desc="4 步驟跑通 RAG / MCP / permission demo 的 setup 跟驗證指令、整合 hands-on 系列所有章節的 prerequisite">Quickstart：clone repo 後跑通所有 demo&lt;/a>&lt;/td>
 &lt;td>4 步驟整合 setup、跑 RAG / MCP / permission demo、跨 hands-on 系列導讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &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 安裝 + Gemma 模型&lt;/a>&lt;/td>
 &lt;td>brew install、ollama pull、curl 驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/comfyui-setup/" data-link-title="Hands-on：安裝 ComfyUI &amp;#43; SDXL base" data-link-desc="git clone、venv、pip install requirements、SDXL safetensors 放哪、--listen 啟動 server、瀏覽器 workflow 驗證">ComfyUI + Stable Diffusion XL&lt;/a>&lt;/td>
 &lt;td>git clone、Python 環境、SDXL 模型放哪&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/whisper-setup/" data-link-title="Hands-on：安裝 whisper.cpp 做語音轉文字" data-link-desc="brew install whisper-cpp、下載 GGML model、Metal 加速、ffmpeg 餵 WAV、484ms 完成 7 秒音訊轉錄">Whisper 語音轉文字&lt;/a>&lt;/td>
 &lt;td>&lt;code>brew install whisper-cpp&lt;/code> + Metal 加速、GGML 模型選擇、&lt;code>whisper-cli&lt;/code> + ffmpeg 驗證轉錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/piper-tts-setup/" data-link-title="Hands-on：安裝 Piper TTS 做文字轉語音" data-link-desc="pip install piper-tts、ONNX voice model、stdin 餵文字、WAV 輸出、跟 Whisper 互為 round-trip 驗證">Piper TTS 文字轉語音&lt;/a>&lt;/td>
 &lt;td>下載 binary、voice 選擇、wav 輸出&lt;/td>
 &lt;/tr>
 &lt;tr>
 &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：用 blog content 當 corpus&lt;/a>&lt;/td>
 &lt;td>embedding + retrieval、串 Ollama&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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 server demo：暴露 blog content&lt;/a>&lt;/td>
 &lt;td>最小 MCP server、給 LLM 用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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 三檔審查粒度的取捨">權限邊界實驗：LLM 改檔案 / 寫 shell 誰執行&lt;/a>&lt;/td>
 &lt;td>LLM 是 pure function、wrapper 才是權限 gate、&lt;code>--dry-run&lt;/code> / &lt;code>--confirm&lt;/code> / &lt;code>--auto&lt;/code> 取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/instruction-following-test/" data-link-title="Hands-on：跨資料夾風格 follow 任務的模型對比" data-link-desc="1B / 4B / 8B / 跨代 4B 在「讀風格參考、follow 既有格式、寫新章節」任務上的 structural metrics 對比、揭示 model size 不是唯一因素">跨資料夾風格 follow 任務的 model size 對比&lt;/a>&lt;/td>
 &lt;td>1B vs 4B 在「讀資料夾、follow 既有格式、寫新章節」任務上的 structural metrics phase transition&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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 對比、實測釋放數字">LLM 運行中 + 結束的資源管理&lt;/a>&lt;/td>
 &lt;td>RAM / 磁碟 / port 三 dimension 觀察、Ollama auto-unload vs ComfyUI persistent lifecycle、實測釋放數字、自動化 cleanup shell function&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/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 的資源 footprint&lt;/a>&lt;/td>
 &lt;td>RAG ingest / query / MCP server 三階段 RAM / 磁碟 / process 實測、多模型並存 RAM 衝突、長期累積管理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="通用前置">通用前置&lt;/h2>
&lt;p>所有工具都假設你的 Mac 滿足：&lt;/p></description><content:encoded><![CDATA[<p>本子資料夾收錄本地 AI 工具的實際安裝跟驗證紀錄。跟 1.x 原理章節的關係：</p>
<table>
  <thead>
      <tr>
          <th>1.x 原理章節</th>
          <th>Hands-on 紀錄</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>為什麼選 Ollama</td>
          <td>實際 <code>brew install</code> + <code>ollama pull</code> 流程</td>
      </tr>
      <tr>
          <td>Speculative decoding 原理</td>
          <td>MTP 模型實際載入 + 速度量測</td>
      </tr>
      <tr>
          <td>ComfyUI 在生態的位置</td>
          <td>實際 git clone + Python 環境 + 模型路徑配置</td>
      </tr>
  </tbody>
</table>
<p>本資料夾的內容<strong>會隨工具版本演化</strong>：指令、目錄結構、相依套件版本都會變。寫的時間戳記在每篇開頭、版本資訊在 frontmatter。跟 1.x 原理章節的差別是「原理跨工具世代不變、實作筆記是當下這版的快照」。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/hands-on/quickstart/" data-link-title="Hands-on Quickstart：clone repo 後跑通所有 demo" data-link-desc="4 步驟跑通 RAG / MCP / permission demo 的 setup 跟驗證指令、整合 hands-on 系列所有章節的 prerequisite">Quickstart：clone repo 後跑通所有 demo</a></td>
          <td>4 步驟整合 setup、跑 RAG / MCP / permission demo、跨 hands-on 系列導讀</td>
      </tr>
      <tr>
          <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 安裝 + Gemma 模型</a></td>
          <td>brew install、ollama pull、curl 驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/hands-on/comfyui-setup/" data-link-title="Hands-on：安裝 ComfyUI &#43; SDXL base" data-link-desc="git clone、venv、pip install requirements、SDXL safetensors 放哪、--listen 啟動 server、瀏覽器 workflow 驗證">ComfyUI + Stable Diffusion XL</a></td>
          <td>git clone、Python 環境、SDXL 模型放哪</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/hands-on/whisper-setup/" data-link-title="Hands-on：安裝 whisper.cpp 做語音轉文字" data-link-desc="brew install whisper-cpp、下載 GGML model、Metal 加速、ffmpeg 餵 WAV、484ms 完成 7 秒音訊轉錄">Whisper 語音轉文字</a></td>
          <td><code>brew install whisper-cpp</code> + Metal 加速、GGML 模型選擇、<code>whisper-cli</code> + ffmpeg 驗證轉錄</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/hands-on/piper-tts-setup/" data-link-title="Hands-on：安裝 Piper TTS 做文字轉語音" data-link-desc="pip install piper-tts、ONNX voice model、stdin 餵文字、WAV 輸出、跟 Whisper 互為 round-trip 驗證">Piper TTS 文字轉語音</a></td>
          <td>下載 binary、voice 選擇、wav 輸出</td>
      </tr>
      <tr>
          <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：用 blog content 當 corpus</a></td>
          <td>embedding + retrieval、串 Ollama</td>
      </tr>
      <tr>
          <td><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 server demo：暴露 blog content</a></td>
          <td>最小 MCP server、給 LLM 用</td>
      </tr>
      <tr>
          <td><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 三檔審查粒度的取捨">權限邊界實驗：LLM 改檔案 / 寫 shell 誰執行</a></td>
          <td>LLM 是 pure function、wrapper 才是權限 gate、<code>--dry-run</code> / <code>--confirm</code> / <code>--auto</code> 取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/01-local-llm-services/hands-on/instruction-following-test/" data-link-title="Hands-on：跨資料夾風格 follow 任務的模型對比" data-link-desc="1B / 4B / 8B / 跨代 4B 在「讀風格參考、follow 既有格式、寫新章節」任務上的 structural metrics 對比、揭示 model size 不是唯一因素">跨資料夾風格 follow 任務的 model size 對比</a></td>
          <td>1B vs 4B 在「讀資料夾、follow 既有格式、寫新章節」任務上的 structural metrics phase transition</td>
      </tr>
      <tr>
          <td><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 對比、實測釋放數字">LLM 運行中 + 結束的資源管理</a></td>
          <td>RAM / 磁碟 / port 三 dimension 觀察、Ollama auto-unload vs ComfyUI persistent lifecycle、實測釋放數字、自動化 cleanup shell function</td>
      </tr>
      <tr>
          <td><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 的資源 footprint</a></td>
          <td>RAG ingest / query / MCP server 三階段 RAM / 磁碟 / process 實測、多模型並存 RAM 衝突、長期累積管理</td>
      </tr>
  </tbody>
</table>
<h2 id="通用前置">通用前置</h2>
<p>所有工具都假設你的 Mac 滿足：</p>
<ul>
<li>Apple Silicon Mac（M1 / M2 / M3 / M4）</li>
<li>macOS 14 (Sonoma) 或以上</li>
<li>Homebrew 安裝完成（<code>brew --version</code> 可看版本）</li>
<li>至少 16 GB 統一記憶體（24 GB+ 較順）</li>
<li>至少 20 GB 可用磁碟空間（本系列總共會佔約 15 GB）</li>
</ul>
<p>需要 Python 環境的工具（ComfyUI、Whisper）會用 venv 隔離、不污染系統 Python。</p>
<h2 id="驗證紀錄環境">驗證紀錄環境</h2>
<p>本系列的指令在以下環境驗證：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>版本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>macOS</td>
          <td>Darwin 24.3.0（Sonoma 14.x）</td>
      </tr>
      <tr>
          <td>Homebrew</td>
          <td>由 <code>/opt/homebrew/bin/brew</code> 提供</td>
      </tr>
      <tr>
          <td>Python</td>
          <td>3.x（系統或 pyenv 都可）</td>
      </tr>
      <tr>
          <td>驗證日期</td>
          <td>2026-05-11</td>
      </tr>
  </tbody>
</table>
<p>換 Mac 規格、換 macOS 版本、半年後再讀本系列、指令可能要小調整、但<strong>前置設定的種類跟驗證步驟的結構</strong>通常不變。看到指令跑不過時、回 1.7 <a href="/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">排錯方法論</a> 的三層架構定位、不要把錯誤訊息當絕對。</p>
]]></content:encoded></item><item><title>1.0 Ollama：主流推論伺服器</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/ollama/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/ollama/</guid><description>&lt;p>Ollama 是本地 LLM 生態的&lt;strong>主流推論伺服器&lt;/strong>、承擔三個責任：模型管理（拉、存、列、刪）、推論執行（呼叫 &lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/llama-cpp/" data-link-title="1.2 llama.cpp：底層推論引擎" data-link-desc="GGUF 格式、量化、MTP 仍 beta；多數讀者不需要直接接觸，Ollama 已經包好">llama.cpp&lt;/a> backend）、API 暴露（預設 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/port-and-localhost/" data-link-title="Port 與 Localhost" data-link-desc="TCP port 與 listen address 如何決定 API server 的對外暴露範圍">&lt;code>localhost:11434&lt;/code>&lt;/a> 上的 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/openai-compatible-api/" data-link-title="OpenAI 相容 API" data-link-desc="本地推論伺服器跟雲端 OpenAI 共用的 API 形狀標準">OpenAI 相容 API&lt;/a> 與原生 API）。它的設計取捨偏向「拿來就跑」、把 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/gguf/" data-link-title="GGUF" data-link-desc="llama.cpp 生態定義的模型權重格式：把權重、tokenizer、metadata 打包成單一檔案">GGUF 格式&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a> 等底層細節都包進 CLI、使用者面對的只有 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/model-tag/" data-link-title="Model Tag" data-link-desc="Ollama 等推論伺服器用來定位特定模型版本的命名規則">model tag&lt;/a> 跟幾個指令。&lt;/p>
&lt;p>對「在 VS Code 接本地 LLM 寫 code」這條最短路徑、Ollama 多半是唯一需要的伺服器層。本章先給 5 分鐘可跑通的最短路徑、再展開日常使用所需的模型管理跟 API 細節、最後才進階主題（背景常駐、MTP 加速、安全暴露、版本升級）。已經把 Ollama 跑起來的讀者可以直接跳到&lt;a href="#%e6%97%a5%e5%b8%b8%e4%bd%bf%e7%94%a8%e6%a8%a1%e5%9e%8b%e7%ae%a1%e7%90%86%e8%88%87-api-%e5%bd%a2%e7%8b%80">日常使用&lt;/a>或&lt;a href="#%e6%8e%92%e9%8c%af%e5%bf%ab%e9%80%9f%e5%88%a4%e8%ae%80">排錯&lt;/a>。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>裝好 Ollama 並驗證它正在跑。&lt;/li>
&lt;li>用 CLI 拉一個模型並開始對話。&lt;/li>
&lt;li>用 curl 驗證 OpenAI 相容 API 在 11434 正常回應。&lt;/li>
&lt;li>看懂 model tag 命名規則、選對 Gemma 4 MTP 版本。&lt;/li>
&lt;li>排查 port 撞、記憶體不足、模型載入慢、cache 過大等情境。&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-ollama-跑起來">最短路徑：5 分鐘把 Ollama 跑起來&lt;/h2>
&lt;p>最短路徑的設計目標是「裝、跑、驗證三步、其他細節留到日常使用段」。三個指令用到的 macOS 工具分別是 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/homebrew/" data-link-title="Homebrew" data-link-desc="macOS 上社群維護的套件管理器、用一行指令安裝 CLI 工具與背景服務">Homebrew 套件管理器&lt;/a>（&lt;code>brew install&lt;/code>）跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/shell-background-process/" data-link-title="Shell 背景 Process" data-link-desc="終端機 process 的前景 / 背景生命週期、訊號控制、找出佔用 port 的 process">shell 前景 process&lt;/a>（&lt;code>ollama serve&lt;/code> 預設前景跑、&lt;code>Ctrl+C&lt;/code> 結束）。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">brew install ollama
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 啟動 server（前景跑、Ctrl+C 結束）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">ollama serve
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 在另一個 terminal 拉一個小模型驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">ollama run gemma3:1b&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第三步首次執行會下載權重（約 815 MB、頻寬足夠的話 1 ~ 3 分鐘）、下載完自動進入 REPL：&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">&amp;gt;&amp;gt;&amp;gt; 寫一個 Python function 計算 fibonacci
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">def fibonacci(n):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> if n &amp;lt;= 1:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> return n
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> return fibonacci(n - 1) + fibonacci(n - 2)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&amp;gt;&amp;gt;&amp;gt; /bye&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>驗證 server 正常聽 11434：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">curl http://localhost:11434/api/version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 回 {&amp;#34;version&amp;#34;:&amp;#34;0.23.x&amp;#34;}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>驗證 OpenAI 相容 API 可以做 chat completion：&lt;/p></description><content:encoded><![CDATA[<p>Ollama 是本地 LLM 生態的<strong>主流推論伺服器</strong>、承擔三個責任：模型管理（拉、存、列、刪）、推論執行（呼叫 <a href="/blog/llm/01-local-llm-services/llama-cpp/" data-link-title="1.2 llama.cpp：底層推論引擎" data-link-desc="GGUF 格式、量化、MTP 仍 beta；多數讀者不需要直接接觸，Ollama 已經包好">llama.cpp</a> backend）、API 暴露（預設 <a href="/blog/llm/knowledge-cards/port-and-localhost/" data-link-title="Port 與 Localhost" data-link-desc="TCP port 與 listen address 如何決定 API server 的對外暴露範圍"><code>localhost:11434</code></a> 上的 <a href="/blog/llm/knowledge-cards/openai-compatible-api/" data-link-title="OpenAI 相容 API" data-link-desc="本地推論伺服器跟雲端 OpenAI 共用的 API 形狀標準">OpenAI 相容 API</a> 與原生 API）。它的設計取捨偏向「拿來就跑」、把 <a href="/blog/llm/knowledge-cards/gguf/" data-link-title="GGUF" data-link-desc="llama.cpp 生態定義的模型權重格式：把權重、tokenizer、metadata 打包成單一檔案">GGUF 格式</a>、<a href="/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化</a>、<a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a> 等底層細節都包進 CLI、使用者面對的只有 <a href="/blog/llm/knowledge-cards/model-tag/" data-link-title="Model Tag" data-link-desc="Ollama 等推論伺服器用來定位特定模型版本的命名規則">model tag</a> 跟幾個指令。</p>
<p>對「在 VS Code 接本地 LLM 寫 code」這條最短路徑、Ollama 多半是唯一需要的伺服器層。本章先給 5 分鐘可跑通的最短路徑、再展開日常使用所需的模型管理跟 API 細節、最後才進階主題（背景常駐、MTP 加速、安全暴露、版本升級）。已經把 Ollama 跑起來的讀者可以直接跳到<a href="#%e6%97%a5%e5%b8%b8%e4%bd%bf%e7%94%a8%e6%a8%a1%e5%9e%8b%e7%ae%a1%e7%90%86%e8%88%87-api-%e5%bd%a2%e7%8b%80">日常使用</a>或<a href="#%e6%8e%92%e9%8c%af%e5%bf%ab%e9%80%9f%e5%88%a4%e8%ae%80">排錯</a>。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>裝好 Ollama 並驗證它正在跑。</li>
<li>用 CLI 拉一個模型並開始對話。</li>
<li>用 curl 驗證 OpenAI 相容 API 在 11434 正常回應。</li>
<li>看懂 model tag 命名規則、選對 Gemma 4 MTP 版本。</li>
<li>排查 port 撞、記憶體不足、模型載入慢、cache 過大等情境。</li>
</ol>
<h2 id="最短路徑5-分鐘把-ollama-跑起來">最短路徑：5 分鐘把 Ollama 跑起來</h2>
<p>最短路徑的設計目標是「裝、跑、驗證三步、其他細節留到日常使用段」。三個指令用到的 macOS 工具分別是 <a href="/blog/llm/knowledge-cards/homebrew/" data-link-title="Homebrew" data-link-desc="macOS 上社群維護的套件管理器、用一行指令安裝 CLI 工具與背景服務">Homebrew 套件管理器</a>（<code>brew install</code>）跟 <a href="/blog/llm/knowledge-cards/shell-background-process/" data-link-title="Shell 背景 Process" data-link-desc="終端機 process 的前景 / 背景生命週期、訊號控制、找出佔用 port 的 process">shell 前景 process</a>（<code>ollama serve</code> 預設前景跑、<code>Ctrl+C</code> 結束）。</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"># 1. 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew install ollama
</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"># 2. 啟動 server（前景跑、Ctrl+C 結束）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">ollama serve
</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"># 3. 在另一個 terminal 拉一個小模型驗證</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">ollama run gemma3:1b</span></span></code></pre></div><p>第三步首次執行會下載權重（約 815 MB、頻寬足夠的話 1 ~ 3 分鐘）、下載完自動進入 REPL：</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">&gt;&gt;&gt; 寫一個 Python function 計算 fibonacci
</span></span><span class="line"><span class="ln">2</span><span class="cl">def fibonacci(n):
</span></span><span class="line"><span class="ln">3</span><span class="cl">    if n &lt;= 1:
</span></span><span class="line"><span class="ln">4</span><span class="cl">        return n
</span></span><span class="line"><span class="ln">5</span><span class="cl">    return fibonacci(n - 1) + fibonacci(n - 2)
</span></span><span class="line"><span class="ln">6</span><span class="cl">&gt;&gt;&gt; /bye</span></span></code></pre></div><p>驗證 server 正常聽 11434：</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">curl http://localhost:11434/api/version
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 回 {&#34;version&#34;:&#34;0.23.x&#34;}</span></span></span></code></pre></div><p>驗證 OpenAI 相容 API 可以做 chat completion：</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">curl http://localhost:11434/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s1">    &#34;model&#34;: &#34;gemma3:1b&#34;,
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s1">    &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Hello&#34;}],
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s1">    &#34;stream&#34;: false
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s1">  }&#39;</span></span></span></code></pre></div><p>回應 JSON 包含 <code>choices[0].message.content</code>、最短路徑就完成。實際寫 code 用的模型大小通常是 14B / 31B 級、選型詳見 <a href="/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">1.4 模型選型優先順序</a>；完整安裝紀錄含 <a href="/blog/llm/knowledge-cards/launchd-service/" data-link-title="launchd Service" data-link-desc="macOS 原生的服務管理機制、把 process 註冊成自動啟動的 daemon 或 agent">launchd service</a> 設定見 <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">Hands-on：Ollama 安裝</a>。</p>
<h2 id="日常使用模型管理與-api-形狀">日常使用：模型管理與 API 形狀</h2>
<h3 id="模型管理指令">模型管理指令</h3>
<p>Ollama 用四個指令覆蓋日常模型管理。每個指令承擔一個語意責任：</p>
<table>
  <thead>
      <tr>
          <th>指令</th>
          <th>責任</th>
          <th>何時使用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ollama pull &lt;tag&gt;</code></td>
          <td>只下載權重、不啟動對話</td>
          <td>CI / 自動化、先下載再離線使用</td>
      </tr>
      <tr>
          <td><code>ollama run &lt;tag&gt;</code></td>
          <td>下載（若還沒）+ 啟動對話 REPL</td>
          <td>互動驗證、快速試模型</td>
      </tr>
      <tr>
          <td><code>ollama list</code></td>
          <td>列出已下載模型與大小</td>
          <td>檢查磁碟用量、確認模型存在</td>
      </tr>
      <tr>
          <td><code>ollama rm &lt;tag&gt;</code></td>
          <td>刪除模型權重與 registry metadata</td>
          <td>釋出 SSD 空間</td>
      </tr>
  </tbody>
</table>
<p>模型權重存在 <code>~/.ollama/models/</code>、單一大模型（30B+）可能佔 18 ~ 30 GB、累積超過 100 GB 很常見。清理路徑統一用 <code>ollama rm</code>、Ollama 會同步更新 registry metadata、後續 <code>ollama list</code> 與 <code>ollama pull</code> 才能正確判斷既存模型狀態。</p>
<h3 id="model-tag-命名規則">Model tag 命名規則</h3>
<p><a href="/blog/llm/knowledge-cards/model-tag/" data-link-title="Model Tag" data-link-desc="Ollama 等推論伺服器用來定位特定模型版本的命名規則">Model tag</a> 是 Ollama 的模型定位符、形式為 <code>family:size-variant-quantization</code>。同一個 model family 可能有十幾個 tag、對應不同參數量、訓練變體跟量化等級。</p>
<table>
  <thead>
      <tr>
          <th>範例</th>
          <th>拆解</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>gemma4:e4b</code></td>
          <td>Gemma 4、E4B（edge dense）、預設量化</td>
      </tr>
      <tr>
          <td><code>gemma4:31b-instruct-q5_K_M</code></td>
          <td>Gemma 4、31B、instruct-tuned、Q5_K_M 量化</td>
      </tr>
      <tr>
          <td><code>gemma4:31b-coding-mtp-bf16</code></td>
          <td>Gemma 4、31B、coding 特化、含 MTP drafter、bf16</td>
      </tr>
      <tr>
          <td><code>qwen3-coder:30b</code></td>
          <td>Qwen3-Coder、30B 參數、預設量化</td>
      </tr>
      <tr>
          <td><code>llama3.3:70b-instruct-q4_K_M</code></td>
          <td>Llama 3.3、70B、instruct、Q4_K_M</td>
      </tr>
  </tbody>
</table>
<p>選 tag 時的兩個判讀重點：variant（<code>instruct</code> / <code>coding</code> 等用途特化、影響回應風格）、quantization（量化等級、影響記憶體佔用與品質、見 <a href="/blog/llm/01-local-llm-services/llama-cpp/#gguf-%e6%a0%bc%e5%bc%8f%e8%88%87%e9%87%8f%e5%8c%96%e6%a8%99%e7%b1%a4" data-link-title="1.2 llama.cpp：底層推論引擎" data-link-desc="GGUF 格式、量化、MTP 仍 beta；多數讀者不需要直接接觸，Ollama 已經包好">1.2 llama.cpp 的量化標籤對照</a>）。完整 tag 清單在 <a href="https://ollama.com/library">ollama.com/library</a>。寫 code 場景的推薦選擇詳見 <a href="/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">1.4 模型選型</a>。</p>
<h3 id="兩套-api選哪一套">兩套 API：選哪一套</h3>
<p>Ollama 在 11434 同時提供兩套 API、用途互補：</p>
<table>
  <thead>
      <tr>
          <th>路徑前綴</th>
          <th>目的</th>
          <th>適合誰</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/v1/…</code></td>
          <td>OpenAI 相容、用 <code>messages</code> 結構</td>
          <td>IDE plugin（Continue.dev 等）、CLI 工具、想無痛切換 cloud / local</td>
      </tr>
      <tr>
          <td><code>/api/…</code></td>
          <td>Ollama 原生、支援模型管理</td>
          <td>想動態切換模型、寫 model 管理腳本</td>
      </tr>
  </tbody>
</table>
<p>寫 code 場景多半用 <code>/v1/…</code>、因為 IDE plugin 預設講這套形狀。詳細協定背景見 <a href="/blog/llm/00-foundations/openai-compatible-api/" data-link-title="0.3 OpenAI 相容 API" data-link-desc="為什麼幾乎所有本地 LLM 工具不用改就能切到本地：背後是同一套 API 形狀">0.3 OpenAI 相容 API</a>。</p>
<p>驗證 streaming 回應：</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">curl http://localhost:11434/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s1">    &#34;model&#34;: &#34;gemma3:1b&#34;,
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s1">    &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Count 1 to 5&#34;}],
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s1">    &#34;stream&#34;: true
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s1">  }&#39;</span></span></span></code></pre></div><p>Streaming 回應是一連串 <code>data: {...}</code> 行、每行一個 token chunk。Ollama 原生 <code>/api/generate</code> 還支援 <code>num_predict</code>、<code>temperature</code>、<code>stop</code> 等細項、IDE plugin 內部會自行轉換、終端使用者通常用不到。</p>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<p>進階段的特色是「沒有它最短路徑仍能跑、但搞懂後體驗大幅提升」。最短路徑只想跑通的讀者可以先跳到<a href="#%e6%8e%92%e9%8c%af%e5%bf%ab%e9%80%9f%e5%88%a4%e8%ae%80">排錯</a>、需要時再回來。</p>
<h3 id="背景常駐launchd-service">背景常駐：launchd service</h3>
<p><code>ollama serve</code> 預設<a href="/blog/llm/knowledge-cards/shell-background-process/" data-link-title="Shell 背景 Process" data-link-desc="終端機 process 的前景 / 背景生命週期、訊號控制、找出佔用 port 的 process">在前景跑</a>、terminal 關掉就停。日常使用建議讓 Ollama 開機自動啟動、用 macOS 的 <a href="/blog/llm/knowledge-cards/launchd-service/" data-link-title="launchd Service" data-link-desc="macOS 原生的服務管理機制、把 process 註冊成自動啟動的 daemon 或 agent">launchd service</a> 機制：</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">brew services start ollama</span></span></code></pre></div><p>這個指令做兩件事、決定 Ollama 之後的行為：</p>
<ol>
<li>寫一個 launchd plist 到 <code>~/Library/LaunchAgents/homebrew.mxcl.ollama.plist</code></li>
<li>立刻啟動 ollama serve、之後重開機自動拉起</li>
</ol>
<p>launchd 是 macOS 原生的服務管理機制、把 process 註冊成 daemon / agent、由系統負責生命週期。<code>brew services</code> 是 <a href="/blog/llm/knowledge-cards/homebrew/" data-link-title="Homebrew" data-link-desc="macOS 上社群維護的套件管理器、用一行指令安裝 CLI 工具與背景服務">Homebrew</a> 對 launchd 的封裝、把 plist 模板跟啟動指令簡化成一行。Log 統一寫到 <code>/opt/homebrew/var/log/ollama.log</code>（Apple Silicon Mac）、出問題第一步先看這個檔。</p>
<p>對應的服務管理指令：</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">brew services stop ollama      <span class="c1"># 停掉、保留 plist</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew services restart ollama   <span class="c1"># 升級後重啟</span></span></span></code></pre></div><p>完整 plist 內容與 log 範例見 <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">Hands-on：Ollama 安裝</a>。</p>
<h3 id="gemma-4-mtp-一鍵加速">Gemma 4 MTP 一鍵加速</h3>
<p><a href="/blog/llm/knowledge-cards/mtp/" data-link-title="Multi-Token Prediction (MTP)" data-link-desc="Google 為 Gemma 系列釋出的 speculative decoding 工程化實作">Multi-Token Prediction（MTP）</a> 是 <a href="/blog/llm/knowledge-cards/speculative-decoding/" data-link-title="Speculative Decoding" data-link-desc="用小模型猜未來 token、大模型並行驗證的加速技巧">speculative decoding</a> 的具體實作、用一個小 <a href="/blog/llm/knowledge-cards/drafter-model/" data-link-title="Drafter Model" data-link-desc="speculative decoding 中用來快速猜未來 token 的小模型">drafter</a> 預測多個 token、再由 target model 驗證、coding 任務有 2 ~ 3 倍加速。Ollama v0.23.1（2026/5/7 釋出）內建 Gemma 4 的 MTP 一鍵支援、啟用方式只需要 pull 對應 model tag：</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">ollama run gemma4:31b-coding-mtp-bf16</span></span></code></pre></div><p>這個 tag 內含 target model（31B）跟 drafter（Google 釋出的官方小模型）、Ollama 自動把兩個 model 載入記憶體、推論時並行驗證。記憶體佔用約 18 GB（drafter 約 1 GB、其餘為 target）、適合 32GB+ Mac。詳細原理見 <a href="/blog/llm/00-foundations/mlx-mtp-omlx/" data-link-title="0.4 MLX / MTP / oMLX 的區別" data-link-desc="三個常被混為一談的術語：framework、加速技巧、特化 server，疊加而非互斥">0.4 MLX / MTP / oMLX</a>。</p>
<p>判讀 MTP tag 時的三個重點：</p>
<ol>
<li><strong>Tag 裡的 <code>bf16</code> 描述的是 drafter 精度</strong>。Target model 內部已套用量化、實際佔用約 18 GB、跟「整個 31B 用 bf16 跑、要 60+ GB」是兩件事。</li>
<li><strong>加速幅度跟任務 pattern 預測度成正比</strong>。Coding（pattern 強）2 ~ 3 倍、純創意寫作或隨機字串生成大約 1.5 倍。</li>
<li><strong>品質由 target model 保證</strong>。Drafter 猜錯時 target 會拒絕該預測、最終輸出跟「直接由 target 生成」一致、drafter 只影響速度。</li>
</ol>
<h3 id="模型常駐keep_alive">模型常駐：keep_alive</h3>
<p><code>ollama run</code> 第一次跑某個 model 時、需要 30 ~ 60 秒把權重從 SSD 載入記憶體；後續對話則用 cached 權重、快得多。Ollama 預設把載入的 model 留在記憶體 5 分鐘（<code>keep_alive</code> 預設值）、長時間不用會被 unload 釋放記憶體。</p>
<p>長時間穩定使用的場景可以延長 keep_alive：</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="nv">OLLAMA_KEEP_ALIVE</span><span class="o">=</span>-1 ollama serve     <span class="c1"># 永久保留</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">OLLAMA_KEEP_ALIVE</span><span class="o">=</span>2h ollama serve     <span class="c1"># 保留 2 小時</span></span></span></code></pre></div><p><code>-1</code> 設定會持續佔用記憶體、適合「整天頻繁用」的工作流；偶爾用一次的場景保持預設、讓系統自動釋放更省記憶體。</p>
<h3 id="對外暴露與信任邊界">對外暴露與信任邊界</h3>
<p>預設 Ollama 只聽 <a href="/blog/llm/knowledge-cards/port-and-localhost/" data-link-title="Port 與 Localhost" data-link-desc="TCP port 與 listen address 如何決定 API server 的對外暴露範圍"><code>127.0.0.1</code></a>、外部裝置連不上。讓 LAN 內其他機器（例如桌機跑 server、筆電當 client）能用、把 listen address 改成 <code>0.0.0.0</code>：</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="nv">OLLAMA_HOST</span><span class="o">=</span>0.0.0.0:11434 ollama serve</span></span></code></pre></div><p>這個設定把 Ollama 暴露在整個區網、任何同網路裝置都能呼叫 API。信任邊界的三種典型情境：</p>
<ul>
<li><strong>家用 / 信任的辦公網路</strong>：風險低、可以直接開</li>
<li><strong>公共 Wi-Fi、共用網路</strong>：透過 SSH tunnel 把 11434 隧道到遠端、或加防火牆規則限制 source IP</li>
<li><strong>暴露到 Internet</strong>：需要 reverse proxy 加 auth、Ollama 本身沒有內建身分認證</li>
</ul>
<p>完整資料流判讀見 <a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私 / 資安資料流</a>、綁定模式（loopback / LAN / reverse proxy + auth）跟誤開放後的具體後果見 <a href="/blog/llm/06-security/inference-server-binding/" data-link-title="6.1 推論伺服器的綁定與暴露範圍" data-link-desc="個人 dev 場景下 llama-server / Ollama / LM Studio 的 bind address 判讀：127.0.0.1 vs LAN vs 反代、預設安全、誤開放給內網的後果">6.1 推論伺服器的綁定與暴露範圍</a>。</p>
<h3 id="版本管理">版本管理</h3>
<p>Ollama 釋出節奏快、每兩三週可能加新功能或修嚴重 bug。升級流程：</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">brew upgrade ollama
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew services restart ollama   <span class="c1"># 若用 launchd service 跑</span></span></span></code></pre></div><p>升級前先看 <a href="https://github.com/ollama/ollama/releases">release notes</a>、確認三件事：</p>
<ol>
<li>是否引入 breaking API change（IDE plugin 可能要對應更新）</li>
<li>是否棄用舊 model tag（拉新 tag 取代）</li>
<li>是否帶來想要的新功能（例如新模型支援、加速優化）</li>
</ol>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<p>排錯段的設計是「先給操作原則、再列觸發條件」、讓讀者快速定位現象屬於哪一類。</p>
<h3 id="port-11434-已被佔用">Port 11434 已被佔用</h3>
<p>操作原則：先檢查是不是舊 Ollama 還在跑、再決定 kill 或換 port。<a href="/blog/llm/knowledge-cards/shell-background-process/" data-link-title="Shell 背景 Process" data-link-desc="終端機 process 的前景 / 背景生命週期、訊號控制、找出佔用 port 的 process"><code>lsof</code> / <code>pkill</code> 的角色</a>是找出佔用方並送終止訊號。</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">lsof -i :11434          <span class="c1"># 看誰佔 11434</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pkill -f <span class="s2">&#34;ollama serve&#34;</span> <span class="c1"># 確認是舊 Ollama 才 kill</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">ollama serve <span class="p">&amp;</span>          <span class="c1"># 重啟、&amp; 是把 process 丟背景</span></span></span></code></pre></div><p>需要兩個 Ollama 並存的場景、改 port 啟動：</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="nv">OLLAMA_HOST</span><span class="o">=</span>127.0.0.1:11435 ollama serve</span></span></code></pre></div><p>IDE plugin 的 <code>apiBase</code> 也要對應改成 11435。</p>
<h3 id="記憶體不足模型崩潰">記憶體不足、模型崩潰</h3>
<p>操作原則：先用 <code>ollama ps</code> 看實際載入了什麼、再對照 <a href="/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">0.5 記憶體預算</a> 決定降級。</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">ollama ps
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># NAME           ID      SIZE     PROCESSOR    UNTIL</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># gemma4:31b...  abc123  18 GB    100% GPU     5 minutes from now</span></span></span></code></pre></div><p>模型大小超過 Mac 記憶體預算時的可選路徑：</p>
<ul>
<li>換較小 model（例如 31B → 14B）</li>
<li>換較激進量化（例如 Q5_K_M → Q4_K_M）</li>
<li>縮短 context window（在 IDE plugin 端設定）</li>
</ul>
<h3 id="模型載入很慢">模型載入很慢</h3>
<p>操作原則：第一次載入慢屬於正常、後續呼叫如果還是慢、檢查 keep_alive 設定。</p>
<p>第一次載入 18 GB 權重需要 30 ~ 60 秒、屬於 SSD → RAM 的真實 I/O 時間。如果發現「每次第一個請求都慢」、表示 keep_alive 太短、模型每次被 unload 又重新載入。延長 keep_alive 解決：</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="nv">OLLAMA_KEEP_ALIVE</span><span class="o">=</span>1h ollama serve</span></span></code></pre></div><p>代價是模型常駐記憶體、其他應用可用記憶體變少。</p>
<h3 id="model-cache-過大佔滿-ssd">Model cache 過大佔滿 SSD</h3>
<p>操作原則：清理用 <code>ollama rm &lt;tag&gt;</code>、Ollama 才會同步更新 registry metadata。</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">ollama list             <span class="c1"># 看哪些 model 佔空間</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ollama rm &lt;tag&gt;         <span class="c1"># 刪除單一 model</span></span></span></code></pre></div><p>手動 <code>rm -rf ~/.ollama/models/</code> 會留下 registry metadata 不一致、後續 <code>ollama list</code> 出錯、<code>ollama pull</code> 也可能誤判已存在。需要完全重置的場景、用：</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">brew services stop ollama
</span></span><span class="line"><span class="ln">2</span><span class="cl">rm -rf ~/.ollama
</span></span><span class="line"><span class="ln">3</span><span class="cl">brew services start ollama</span></span></code></pre></div><p>這會清掉所有 model 跟設定、重新從零開始。</p>
<h2 id="跟其他伺服器並存">跟其他伺服器並存</h2>
<p>Ollama 設計上可以跟 LM Studio、llama.cpp 同時在一台 Mac 跑、預設 port 不同：</p>
<table>
  <thead>
      <tr>
          <th>伺服器</th>
          <th>預設 port</th>
          <th>適合主力場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ollama</td>
          <td>11434</td>
          <td>日常寫 code、CLI 工作流</td>
      </tr>
      <tr>
          <td>LM Studio</td>
          <td>1234</td>
          <td>GUI 探索新模型、視覺化參數</td>
      </tr>
      <tr>
          <td>llama.cpp</td>
          <td>8080</td>
          <td>底層研究、自訂量化</td>
      </tr>
      <tr>
          <td>oMLX</td>
          <td>8000</td>
          <td>特化 MLX 場景</td>
      </tr>
  </tbody>
</table>
<p>並存的好處是「主力穩定跑 Ollama、實驗模型用 LM Studio」、Continue.dev 等介面層可以同時設多個 model、UI 上下拉切換。並存設定範例見 <a href="/blog/llm/01-local-llm-services/lm-studio/#%e8%88%87-ollama-%e4%b8%a6%e5%ad%98" data-link-title="1.1 LM Studio：GUI 探索模型" data-link-desc="GUI 取向的本地推論伺服器：內建模型瀏覽器、speculative decoding 設定面板、適合探索新模型">1.1 LM Studio</a>。</p>
<h2 id="下一章">下一章</h2>
<p>下一章可選擇：</p>
<ul>
<li>想對比 GUI 派的選擇：<a href="/blog/llm/01-local-llm-services/lm-studio/" data-link-title="1.1 LM Studio：GUI 探索模型" data-link-desc="GUI 取向的本地推論伺服器：內建模型瀏覽器、speculative decoding 設定面板、適合探索新模型">1.1 LM Studio</a></li>
<li>想了解底層 / Ollama 跟 llama.cpp 的關係：<a href="/blog/llm/01-local-llm-services/llama-cpp/" data-link-title="1.2 llama.cpp：底層推論引擎" data-link-desc="GGUF 格式、量化、MTP 仍 beta；多數讀者不需要直接接觸，Ollama 已經包好">1.2 llama.cpp</a></li>
<li>直接進入 VS Code 整合：<a href="/blog/llm/01-local-llm-services/vscode-continue-integration/" data-link-title="1.3 VS Code &#43; Continue.dev 整合" data-link-desc="安裝 Continue 擴充套件、config.json 設定、Cmd&#43;L 對話 / Cmd&#43;I 行內編輯快捷鍵">1.3 VS Code + Continue.dev</a></li>
</ul>
]]></content:encoded></item><item><title>Hands-on：安裝 Ollama + 拉第一個 Gemma 模型</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/ollama-setup/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/ollama-setup/</guid><description>&lt;p>本篇紀錄在 Apple Silicon Mac 上裝 Ollama 並拉一個小模型驗證的完整流程。指令在 macOS 14 (Sonoma) / Homebrew 提供的環境下驗證。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>驗證日期&lt;/strong>：2026-05-11
&lt;strong>Ollama 版本&lt;/strong>：0.23.2
&lt;strong>示範模型&lt;/strong>：&lt;code>gemma3:1b&lt;/code>（約 815 MB、選最小可運行的 Gemma 變體當驗證對象）&lt;/p>&lt;/blockquote>
&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>macOS 版本&lt;/td>
 &lt;td>&lt;code>sw_vers -productVersion&lt;/code>&lt;/td>
 &lt;td>14.x 或更新&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Apple Silicon&lt;/td>
 &lt;td>&lt;code>uname -m&lt;/code>&lt;/td>
 &lt;td>&lt;code>arm64&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Homebrew&lt;/td>
 &lt;td>&lt;code>brew --version&lt;/code>&lt;/td>
 &lt;td>4.x（任何近期版）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>磁碟空間&lt;/td>
 &lt;td>&lt;code>df -h ~&lt;/code>&lt;/td>
 &lt;td>至少 3 GB 剩餘給 runtime + 1B 模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>port 11434&lt;/td>
 &lt;td>&lt;code>lsof -i :11434&lt;/code>&lt;/td>
 &lt;td>無輸出（port 沒被佔）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>表中 &lt;code>brew --version&lt;/code> 這關若還沒過、代表 Homebrew 沒裝。新機從零的安裝順序（Homebrew、PATH、bash）見 &lt;a href="https://tarrragon.github.io/blog/other/macos-%E6%96%B0%E6%A9%9F%E5%9F%BA%E7%A4%8E%E5%BB%BA%E8%A8%AD%E5%A5%97%E4%BB%B6%E7%AE%A1%E7%90%86%E8%88%87%E5%80%8B%E4%BA%BA-bin-%E7%9A%84%E8%A8%AD%E5%AE%9A%E9%A0%86%E5%BA%8F/" data-link-title="macOS 新機基礎建設：套件管理與個人 bin 的設定順序" data-link-desc="重灌或換機後底層基礎建設的依賴順序，免得後面工具裝不起來或路徑互相找不到。">macOS 新機基礎建設&lt;/a>。&lt;/p>
&lt;p>選 1B 模型只是為了驗證流程、能力很弱、實際寫 code 場景請用 14B / 31B 級。模型大小跟記憶體 / 磁碟對應關係見 &lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">0.5 Apple Silicon 記憶體預算&lt;/a>。&lt;/p>
&lt;h2 id="安裝-ollama">安裝 Ollama&lt;/h2>
&lt;p>用 Homebrew 安裝、是 macOS 上最直接的路徑：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">brew install ollama&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>執行時間在 broadband 大約 30 秒到 2 分鐘、視 dependency cache 是否已有（Ollama 依賴 mlx-c 等 Apple Silicon 加速函式庫、首次裝較久）。&lt;/p>
&lt;p>裝完看到的 caveat 訊息：&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">To start ollama now and restart at login:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> brew services start ollama
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">Or, if you don&amp;#39;t want/need a background service you can just run:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> OLLAMA_FLASH_ATTENTION=&amp;#34;1&amp;#34; OLLAMA_KV_CACHE_TYPE=&amp;#34;q8_0&amp;#34; /opt/homebrew/opt/ollama/bin/ollama serve&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩種啟動模式：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>launchd service&lt;/strong>（推薦日常用）：開機自動啟動、跑在背景。&lt;/li>
&lt;li>&lt;strong>前景手動跑&lt;/strong>：terminal 開著、關掉就停。&lt;/li>
&lt;/ul>
&lt;p>驗證 binary 路徑：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">which ollama
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 應該回 /opt/homebrew/bin/ollama&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="啟動-ollama-service">啟動 Ollama Service&lt;/h2>
&lt;p>選 launchd service 模式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">brew services start ollama&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預期輸出：&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">==&amp;gt; Successfully started `ollama` (label: homebrew.mxcl.ollama)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個動作做兩件事：&lt;/p>
&lt;ol>
&lt;li>註冊一個 launchd plist（macOS 開機自啟動 / 背景服務的設定檔、見 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/launchd-service/" data-link-title="launchd Service" data-link-desc="macOS 原生的服務管理機制、把 process 註冊成自動啟動的 daemon 或 agent">launchd-service 卡片&lt;/a>）到 &lt;code>~/Library/LaunchAgents/homebrew.mxcl.ollama.plist&lt;/code>。&lt;/li>
&lt;li>立刻啟動 ollama serve、之後重開機自動啟動。&lt;/li>
&lt;/ol>
&lt;p>驗證 server 真的在跑：&lt;/p></description><content:encoded><![CDATA[<p>本篇紀錄在 Apple Silicon Mac 上裝 Ollama 並拉一個小模型驗證的完整流程。指令在 macOS 14 (Sonoma) / Homebrew 提供的環境下驗證。</p>
<blockquote>
<p><strong>驗證日期</strong>：2026-05-11
<strong>Ollama 版本</strong>：0.23.2
<strong>示範模型</strong>：<code>gemma3:1b</code>（約 815 MB、選最小可運行的 Gemma 變體當驗證對象）</p></blockquote>
<h2 id="前置設定">前置設定</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>檢查指令</th>
          <th>預期</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>macOS 版本</td>
          <td><code>sw_vers -productVersion</code></td>
          <td>14.x 或更新</td>
      </tr>
      <tr>
          <td>Apple Silicon</td>
          <td><code>uname -m</code></td>
          <td><code>arm64</code></td>
      </tr>
      <tr>
          <td>Homebrew</td>
          <td><code>brew --version</code></td>
          <td>4.x（任何近期版）</td>
      </tr>
      <tr>
          <td>磁碟空間</td>
          <td><code>df -h ~</code></td>
          <td>至少 3 GB 剩餘給 runtime + 1B 模型</td>
      </tr>
      <tr>
          <td>port 11434</td>
          <td><code>lsof -i :11434</code></td>
          <td>無輸出（port 沒被佔）</td>
      </tr>
  </tbody>
</table>
<p>表中 <code>brew --version</code> 這關若還沒過、代表 Homebrew 沒裝。新機從零的安裝順序（Homebrew、PATH、bash）見 <a href="/blog/other/macos-%E6%96%B0%E6%A9%9F%E5%9F%BA%E7%A4%8E%E5%BB%BA%E8%A8%AD%E5%A5%97%E4%BB%B6%E7%AE%A1%E7%90%86%E8%88%87%E5%80%8B%E4%BA%BA-bin-%E7%9A%84%E8%A8%AD%E5%AE%9A%E9%A0%86%E5%BA%8F/" data-link-title="macOS 新機基礎建設：套件管理與個人 bin 的設定順序" data-link-desc="重灌或換機後底層基礎建設的依賴順序，免得後面工具裝不起來或路徑互相找不到。">macOS 新機基礎建設</a>。</p>
<p>選 1B 模型只是為了驗證流程、能力很弱、實際寫 code 場景請用 14B / 31B 級。模型大小跟記憶體 / 磁碟對應關係見 <a href="/blog/llm/00-foundations/hardware-memory-budget/" data-link-title="0.5 Apple Silicon 記憶體預算" data-link-desc="記憶體決定能跑什麼，Q4 量化下的可運作模型對照與系統保留">0.5 Apple Silicon 記憶體預算</a>。</p>
<h2 id="安裝-ollama">安裝 Ollama</h2>
<p>用 Homebrew 安裝、是 macOS 上最直接的路徑：</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">brew install ollama</span></span></code></pre></div><p>執行時間在 broadband 大約 30 秒到 2 分鐘、視 dependency cache 是否已有（Ollama 依賴 mlx-c 等 Apple Silicon 加速函式庫、首次裝較久）。</p>
<p>裝完看到的 caveat 訊息：</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">To start ollama now and restart at login:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  brew services start ollama
</span></span><span class="line"><span class="ln">3</span><span class="cl">Or, if you don&#39;t want/need a background service you can just run:
</span></span><span class="line"><span class="ln">4</span><span class="cl">  OLLAMA_FLASH_ATTENTION=&#34;1&#34; OLLAMA_KV_CACHE_TYPE=&#34;q8_0&#34; /opt/homebrew/opt/ollama/bin/ollama serve</span></span></code></pre></div><p>兩種啟動模式：</p>
<ul>
<li><strong>launchd service</strong>（推薦日常用）：開機自動啟動、跑在背景。</li>
<li><strong>前景手動跑</strong>：terminal 開著、關掉就停。</li>
</ul>
<p>驗證 binary 路徑：</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">which ollama
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 應該回 /opt/homebrew/bin/ollama</span></span></span></code></pre></div><h2 id="啟動-ollama-service">啟動 Ollama Service</h2>
<p>選 launchd service 模式：</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">brew services start ollama</span></span></code></pre></div><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">==&gt; Successfully started `ollama` (label: homebrew.mxcl.ollama)</span></span></code></pre></div><p>這個動作做兩件事：</p>
<ol>
<li>註冊一個 launchd plist（macOS 開機自啟動 / 背景服務的設定檔、見 <a href="/blog/llm/knowledge-cards/launchd-service/" data-link-title="launchd Service" data-link-desc="macOS 原生的服務管理機制、把 process 註冊成自動啟動的 daemon 或 agent">launchd-service 卡片</a>）到 <code>~/Library/LaunchAgents/homebrew.mxcl.ollama.plist</code>。</li>
<li>立刻啟動 ollama serve、之後重開機自動啟動。</li>
</ol>
<p>驗證 server 真的在跑：</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">curl -s http://localhost:11434/api/version</span></span></code></pre></div><p>預期回：</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 class="nt">&#34;version&#34;</span><span class="p">:</span><span class="s2">&#34;0.23.2&#34;</span><span class="p">}</span></span></span></code></pre></div><p>看到這個 JSON 就證明三件事：Ollama daemon 跑了、port 11434 通了、API 結構正確。</p>
<h2 id="拉第一個模型">拉第一個模型</h2>
<p>Ollama 用 <code>ollama pull</code> 從官方 registry 下載模型：</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">ollama pull gemma3:1b</span></span></code></pre></div><p>Gemma 3 1B 約 815 MB、broadband 約 1-2 分鐘下載。下載過程顯示多階段：</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">pulling 7cd4618c1faf: 100% ▕██████████████████▏ 815 MB
</span></span><span class="line"><span class="ln">2</span><span class="cl">pulling e0a42594d802: 100% ▕██████████████████▏  358 B
</span></span><span class="line"><span class="ln">3</span><span class="cl">pulling dd084c7d92a3: 100% ▕██████████████████▏  8.4 KB
</span></span><span class="line"><span class="ln">4</span><span class="cl">pulling 3116c5225075: 100% ▕██████████████████▏   77 B
</span></span><span class="line"><span class="ln">5</span><span class="cl">pulling 120007c81bf8: 100% ▕██████████████████▏  492 B
</span></span><span class="line"><span class="ln">6</span><span class="cl">verifying sha256 digest
</span></span><span class="line"><span class="ln">7</span><span class="cl">writing manifest
</span></span><span class="line"><span class="ln">8</span><span class="cl">success</span></span></code></pre></div><p>幾個 hash blob 分別是：模型權重（最大那個）、tokenizer、template、license metadata 等。Ollama 把這些統一管理、放在 <code>~/.ollama/models/</code>。</p>
<p>驗證模型已下載：</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">ollama list</span></span></code></pre></div><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">NAME         ID              SIZE      MODIFIED
</span></span><span class="line"><span class="ln">2</span><span class="cl">gemma3:1b    8648f39daa8f    815 MB    35 seconds ago</span></span></code></pre></div><h2 id="驗證-openai-相容-api">驗證 OpenAI 相容 API</h2>
<p>OpenAI 相容 API 是下游所有工具（IDE plugin、RAG pipeline、MCP server、<a href="/blog/llm/01-local-llm-services/vscode-continue-integration/" data-link-title="1.3 VS Code &#43; Continue.dev 整合" data-link-desc="安裝 Continue 擴充套件、config.json 設定、Cmd&#43;L 對話 / Cmd&#43;I 行內編輯快捷鍵">Continue.dev</a> 等）依賴的介面 contract、驗證它能正常回應、整個 stack 才走得通：</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">curl -s http://localhost:11434/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s1">    &#34;model&#34;: &#34;gemma3:1b&#34;,
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s1">    &#34;messages&#34;: [{&#34;role&#34;:&#34;user&#34;,&#34;content&#34;:&#34;Reply in one short sentence: what is 2+2?&#34;}],
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s1">    &#34;stream&#34;: false
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s1">  }&#39;</span></span></span></code></pre></div><p>預期回 JSON、<code>choices[0].message.content</code> 是模型回答（如 <code>&quot;2 + 2 = 4&quot;</code>）。看到合理回答就證明：</p>
<ol>
<li>Ollama 跟模型權重對接好。</li>
<li>OpenAI 相容 API 格式正常（IDE plugin 可以接）。</li>
<li>推論流程整條通。</li>
</ol>
<p>常見的失敗回應跟下一步：</p>
<ul>
<li><strong><code>{&quot;error&quot;:&quot;model 'gemma3:1b' not found, try pulling it first&quot;}</code></strong>：先跑 <code>ollama pull gemma3:1b</code>、確認 <code>ollama list</code> 看到該 tag。</li>
<li><strong><code>curl: (7) Failed to connect to localhost port 11434: Connection refused</code></strong>：server 沒在跑、回 <code>brew services list</code> 看 status、若是 stopped 跑 <code>brew services start ollama</code>。</li>
<li><strong><code>{&quot;error&quot;:&quot;json: cannot unmarshal ...&quot;}</code></strong>：請求格式錯（例如 messages 寫成 string 不是 array）、檢查 JSON body。</li>
<li><strong>連得上但長時間沒回應</strong>：第一次載入大 model 需要 30 ~ 60 秒、看 <code>~/.ollama/logs/server.log</code> 確認是否還在 loading。</li>
</ul>
<p>用內建 CLI 互動模式也行：</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">ollama run gemma3:1b</span></span></code></pre></div><p>進入 REPL、可以打字對話。<code>/bye</code> 離開。</p>
<p>第一次跑 <code>ollama run</code> 會把模型載入記憶體（1B 模型大約 1-2 秒）、之後對話延遲低。如果幾分鐘沒用、模型會被 unload 釋放記憶體、下次 run 又要等載入。控制行為的環境變數是 <code>OLLAMA_KEEP_ALIVE</code>（預設 5 分鐘）。</p>
<h2 id="常見前置設定問題">常見前置設定問題</h2>
<h3 id="port-11434-被佔用">Port 11434 被佔用</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">lsof -i :11434</span></span></code></pre></div><p>若已有 process 占用、可能是先前手動跑過 <code>ollama serve</code> 沒關。kill 後再 start service：</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">pkill -f <span class="s2">&#34;ollama serve&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew services restart ollama</span></span></code></pre></div><h3 id="ollama-command-not-found裝完還是找不到"><code>ollama: command not found</code>（裝完還是找不到）</h3>
<p>Homebrew 在 Apple Silicon 預設裝到 <code>/opt/homebrew/bin</code>、shell PATH 應該已含。若沒含：</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="nb">echo</span> <span class="nv">$PATH</span> <span class="p">|</span> tr <span class="s1">&#39;:&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> grep homebrew
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 若沒看到 /opt/homebrew/bin、要加進 ~/.zshrc：</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;export PATH=&#34;/opt/homebrew/bin:$PATH&#34;&#39;</span> &gt;&gt; ~/.zshrc
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">source</span> ~/.zshrc</span></span></code></pre></div><h3 id="server-啟動但-curl-失敗">Server 啟動但 curl 失敗</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">brew services list <span class="p">|</span> grep ollama</span></span></code></pre></div><p>若 status 不是 <code>started</code>、看 log：</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">tail -50 /opt/homebrew/var/log/ollama.log</span></span></code></pre></div><p>常見原因：port 衝突、權限問題、上次 crash 沒清乾淨。</p>
<p>完整排錯流程見 <a href="/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">1.7 排錯方法論</a>。</p>
<h2 id="之後想做的事">之後想做的事</h2>
<ul>
<li><strong>接 VS Code</strong>：見 <a href="/blog/llm/01-local-llm-services/vscode-continue-integration/" data-link-title="1.3 VS Code &#43; Continue.dev 整合" data-link-desc="安裝 Continue 擴充套件、config.json 設定、Cmd&#43;L 對話 / Cmd&#43;I 行內編輯快捷鍵">1.3 VS Code + Continue.dev 整合</a>。設定 <code>apiBase: http://localhost:11434</code> 就能用。</li>
<li><strong>跑更大模型</strong>：32GB+ Mac 推薦 <code>gemma4:31b-coding-mtp-bf16</code>（18 GB）。模型選擇見 <a href="/blog/llm/01-local-llm-services/model-selection-priority/" data-link-title="1.4 寫 code 場景的模型選型優先順序" data-link-desc="Gemma 4 31B MTP → Qwen3-Coder 30B → Qwen3 14B → gpt-oss 20B 的取捨與適用情境">1.4 模型選型優先順序</a>。</li>
<li><strong>加 embedding</strong>：codebase 索引要 embedding 模型：<code>ollama pull nomic-embed-text</code>（274 MB）、見 <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 RAG 原理</a>。</li>
</ul>
<h2 id="升級--移除">升級 / 移除</h2>
<p>升級：</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">brew upgrade ollama
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew services restart ollama</span></span></code></pre></div><p>完整移除：</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">brew services stop ollama
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew uninstall ollama
</span></span><span class="line"><span class="ln">3</span><span class="cl">rm -rf ~/.ollama  <span class="c1"># 清模型 cache（可選）</span></span></span></code></pre></div><h2 id="何時這篇會過時">何時這篇會過時</h2>
<ul>
<li><code>brew install ollama</code> 安裝方式跟 OpenAI 相容 API 形狀短期內不會變（生態都依賴）。</li>
<li><code>gemma3:1b</code> 這個具體 tag 預期會被新模型取代、但「拉一個小模型驗證流程」的方法不變。</li>
<li>launchd service 機制是 macOS 系統 API、不會 deprecate。</li>
</ul>
<p>讀的時候若 <code>brew install</code> 跑失敗、查 Ollama GitHub release notes；其餘驗證步驟結構通用。</p>
]]></content:encoded></item></channel></rss>