<?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>4.x Hands-on：端到端案例 on Tarragon</title><link>https://tarrragon.github.io/blog/llm/04-applications/hands-on/</link><description>Recent content in 4.x Hands-on：端到端案例 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 14 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/llm/04-applications/hands-on/index.xml" rel="self" type="application/rss+xml"/><item><title>Case Study：customer support agent 從 task decomposition 到 eval</title><link>https://tarrragon.github.io/blog/llm/04-applications/hands-on/customer-support-case-study/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/hands-on/customer-support-case-study/</guid><description>&lt;p>本案例的責任是把模組四前面所有原理章節串成一個端到端的設計過程、示範&lt;strong>遇到實際 LLM 應用任務時、設計反射動作的順序&lt;/strong>。每段都標出引用哪章原理、讓讀者看到 principle 章節怎麼落到具體工作。&lt;/p>
&lt;p>用作走查的任務：PM 交派「做一個 customer support agent、能處理用戶查詢、必要時自動完成操作（如改地址）。」本案例聚焦「改地址」這個高頻 query type 走完整流程。&lt;/p>
&lt;h2 id="本案例的設計反射">本案例的設計反射&lt;/h2>
&lt;p>整個流程分七階段：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>觀察人類工作流&lt;/strong>：訪談、決定 task decomposition&lt;/li>
&lt;li>&lt;strong>典範定位&lt;/strong>：哪段該 deterministic、哪段該 fuzzy&lt;/li>
&lt;li>&lt;strong>工作流設計&lt;/strong>：每個 step 選對應的 LLM / tool / RAG / HITL 形態&lt;/li>
&lt;li>&lt;strong>協議跟自主度決定&lt;/strong>：是 single agent / multi-call / multi-agent&lt;/li>
&lt;li>&lt;strong>Trace instrumentation&lt;/strong>：哪些資訊要記&lt;/li>
&lt;li>&lt;strong>Eval 設計&lt;/strong>：先選座標、再選工具&lt;/li>
&lt;li>&lt;strong>Iteration loop&lt;/strong>：error analysis → 修哪一層 → 看 metric 收斂&lt;/li>
&lt;/ol>
&lt;p>初次設計 LLM 應用時最常省略階段 1、2、5、6、直接跳到階段 3 開始寫 prompt——這條路會走進「prompt 改了 20 版、無法判讀有沒有變好」的迭代無收斂。本案例強調的是設計反射動作的順序、不是寫 prompt 技巧。&lt;/p>
&lt;h2 id="階段-1觀察人類工作流">階段 1：觀察人類工作流&lt;/h2>
&lt;p>PM 給的任務描述是「處理用戶查詢」、但「查詢」涵蓋的範圍可能很大。第一個反射動作是&lt;strong>坐在客服旁邊觀察兩天&lt;/strong>、不是打開 IDE。&lt;/p>
&lt;p>實際做的事：&lt;/p>
&lt;ul>
&lt;li>統計收到的 query 類型分佈（退款 / 改地址 / 查詢訂單狀態 / 抱怨 / 開放問題各佔多少）。&lt;/li>
&lt;li>看每類 query 的 human resolution 流程（哪幾步、要查哪些系統、要遵守哪些 policy）。&lt;/li>
&lt;li>看哪幾類 query 是 high volume + low complexity（最值得自動化）、哪幾類是 low volume + high complexity（自動化 ROI 差）。&lt;/li>
&lt;li>記下 human 在哪些 step 卡住、哪些 step 反覆需要查同樣資料。&lt;/li>
&lt;/ul>
&lt;p>訪談結束、你得到一張 task decomposition map。本案例假設聚焦在「用戶請求改地址」這個高頻 query type：&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">User: 「我搬家了、訂單編號 #12345、新地址是 ___」
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">1. 解析意圖 + 抽取訊息（訂單編號、新地址）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">2. 查訂單狀態（已出貨？未出貨？已送達？）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">3. 查 policy（這個訂單狀態 + user tier 能不能改地址？）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">4. 若可：執行改地址（呼叫物流 / 庫存 API）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">5. 若不可：解釋為什麼、給替代方案
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">6. 草擬回覆 email、發出&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>引用原理：這個 decomposition 本身對應 &lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/deterministic-vs-fuzzy-engineering/" data-link-title="0.8 Deterministic vs Fuzzy Engineering：軟體設計典範的位移" data-link-desc="傳統 deterministic 軟體跟 fuzzy LLM 軟體在資料、邏輯、分解、實驗成本四個維度的根本差異、以及哪段該 deterministic、哪段該 fuzzy 的決策框架">0.8 fuzzy engineering&lt;/a>（&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/deterministic-vs-fuzzy/" data-link-title="Deterministic vs Fuzzy engineering" data-link-desc="LLM 軟體 vs 傳統軟體在資料 / 邏輯 / 行為一致性 / 實驗成本四維度的典範差異、決定哪段該包 guardrail">deterministic-vs-fuzzy&lt;/a> 卡）的「先分解任務、再判讀每段該 deterministic 還是 fuzzy」。&lt;/p>
&lt;h2 id="階段-2典範定位">階段 2：典範定位&lt;/h2>
&lt;p>對每個 step 做典範定位（deterministic / fuzzy）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Step&lt;/th>
 &lt;th>典範&lt;/th>
 &lt;th>為什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1. 解析意圖 + 抽取訊息&lt;/td>
 &lt;td>Fuzzy&lt;/td>
 &lt;td>自由文字 input、需要 LLM 理解&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2. 查訂單狀態&lt;/td>
 &lt;td>Deterministic&lt;/td>
 &lt;td>結構化 query（給 order_id、回 status）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3. 查 policy&lt;/td>
 &lt;td>Deterministic&lt;/td>
 &lt;td>規則可窮舉、policy as code&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4. 執行改地址&lt;/td>
 &lt;td>Deterministic&lt;/td>
 &lt;td>API call、有 schema 跟錯誤碼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5. 解釋 / 給替代方案&lt;/td>
 &lt;td>Fuzzy&lt;/td>
 &lt;td>要寫人話、要 tailored to 情境&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>6. 草擬 email + 發出&lt;/td>
 &lt;td>Fuzzy（草擬）+ Deterministic（發送）&lt;/td>
 &lt;td>寫 email 是 fuzzy、發 API call 是 deterministic&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判讀的重點是&lt;strong>邊界各歸各位&lt;/strong>：規則跟政策走 code、人話跟意圖解析走 LLM。&lt;/p></description><content:encoded><![CDATA[<p>本案例的責任是把模組四前面所有原理章節串成一個端到端的設計過程、示範<strong>遇到實際 LLM 應用任務時、設計反射動作的順序</strong>。每段都標出引用哪章原理、讓讀者看到 principle 章節怎麼落到具體工作。</p>
<p>用作走查的任務：PM 交派「做一個 customer support agent、能處理用戶查詢、必要時自動完成操作（如改地址）。」本案例聚焦「改地址」這個高頻 query type 走完整流程。</p>
<h2 id="本案例的設計反射">本案例的設計反射</h2>
<p>整個流程分七階段：</p>
<ol>
<li><strong>觀察人類工作流</strong>：訪談、決定 task decomposition</li>
<li><strong>典範定位</strong>：哪段該 deterministic、哪段該 fuzzy</li>
<li><strong>工作流設計</strong>：每個 step 選對應的 LLM / tool / RAG / HITL 形態</li>
<li><strong>協議跟自主度決定</strong>：是 single agent / multi-call / multi-agent</li>
<li><strong>Trace instrumentation</strong>：哪些資訊要記</li>
<li><strong>Eval 設計</strong>：先選座標、再選工具</li>
<li><strong>Iteration loop</strong>：error analysis → 修哪一層 → 看 metric 收斂</li>
</ol>
<p>初次設計 LLM 應用時最常省略階段 1、2、5、6、直接跳到階段 3 開始寫 prompt——這條路會走進「prompt 改了 20 版、無法判讀有沒有變好」的迭代無收斂。本案例強調的是設計反射動作的順序、不是寫 prompt 技巧。</p>
<h2 id="階段-1觀察人類工作流">階段 1：觀察人類工作流</h2>
<p>PM 給的任務描述是「處理用戶查詢」、但「查詢」涵蓋的範圍可能很大。第一個反射動作是<strong>坐在客服旁邊觀察兩天</strong>、不是打開 IDE。</p>
<p>實際做的事：</p>
<ul>
<li>統計收到的 query 類型分佈（退款 / 改地址 / 查詢訂單狀態 / 抱怨 / 開放問題各佔多少）。</li>
<li>看每類 query 的 human resolution 流程（哪幾步、要查哪些系統、要遵守哪些 policy）。</li>
<li>看哪幾類 query 是 high volume + low complexity（最值得自動化）、哪幾類是 low volume + high complexity（自動化 ROI 差）。</li>
<li>記下 human 在哪些 step 卡住、哪些 step 反覆需要查同樣資料。</li>
</ul>
<p>訪談結束、你得到一張 task decomposition map。本案例假設聚焦在「用戶請求改地址」這個高頻 query type：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">User: 「我搬家了、訂單編號 #12345、新地址是 ___」
</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">1. 解析意圖 + 抽取訊息（訂單編號、新地址）
</span></span><span class="line"><span class="ln">4</span><span class="cl">2. 查訂單狀態（已出貨？未出貨？已送達？）
</span></span><span class="line"><span class="ln">5</span><span class="cl">3. 查 policy（這個訂單狀態 + user tier 能不能改地址？）
</span></span><span class="line"><span class="ln">6</span><span class="cl">4. 若可：執行改地址（呼叫物流 / 庫存 API）
</span></span><span class="line"><span class="ln">7</span><span class="cl">5. 若不可：解釋為什麼、給替代方案
</span></span><span class="line"><span class="ln">8</span><span class="cl">6. 草擬回覆 email、發出</span></span></code></pre></div><p>引用原理：這個 decomposition 本身對應 <a href="/blog/llm/00-foundations/deterministic-vs-fuzzy-engineering/" data-link-title="0.8 Deterministic vs Fuzzy Engineering：軟體設計典範的位移" data-link-desc="傳統 deterministic 軟體跟 fuzzy LLM 軟體在資料、邏輯、分解、實驗成本四個維度的根本差異、以及哪段該 deterministic、哪段該 fuzzy 的決策框架">0.8 fuzzy engineering</a>（<a href="/blog/llm/knowledge-cards/deterministic-vs-fuzzy/" data-link-title="Deterministic vs Fuzzy engineering" data-link-desc="LLM 軟體 vs 傳統軟體在資料 / 邏輯 / 行為一致性 / 實驗成本四維度的典範差異、決定哪段該包 guardrail">deterministic-vs-fuzzy</a> 卡）的「先分解任務、再判讀每段該 deterministic 還是 fuzzy」。</p>
<h2 id="階段-2典範定位">階段 2：典範定位</h2>
<p>對每個 step 做典範定位（deterministic / fuzzy）：</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>典範</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. 解析意圖 + 抽取訊息</td>
          <td>Fuzzy</td>
          <td>自由文字 input、需要 LLM 理解</td>
      </tr>
      <tr>
          <td>2. 查訂單狀態</td>
          <td>Deterministic</td>
          <td>結構化 query（給 order_id、回 status）</td>
      </tr>
      <tr>
          <td>3. 查 policy</td>
          <td>Deterministic</td>
          <td>規則可窮舉、policy as code</td>
      </tr>
      <tr>
          <td>4. 執行改地址</td>
          <td>Deterministic</td>
          <td>API call、有 schema 跟錯誤碼</td>
      </tr>
      <tr>
          <td>5. 解釋 / 給替代方案</td>
          <td>Fuzzy</td>
          <td>要寫人話、要 tailored to 情境</td>
      </tr>
      <tr>
          <td>6. 草擬 email + 發出</td>
          <td>Fuzzy（草擬）+ Deterministic（發送）</td>
          <td>寫 email 是 fuzzy、發 API call 是 deterministic</td>
      </tr>
  </tbody>
</table>
<p>判讀的重點是<strong>邊界各歸各位</strong>：規則跟政策走 code、人話跟意圖解析走 LLM。</p>
<ul>
<li>Policy check 寫成 code（如「user tier + 訂單狀態 → 能否改地址」是 deterministic 規則）。對應反例：把規則塞進 prompt 讓 LLM 判斷、會偶爾跳過規則或誤判 tier。</li>
<li>「能不能做」這類 yes/no 走規則。對應反例：用 LLM 算判斷、debug 困難且非確定性。</li>
<li>「Helpful 的回覆」走 LLM 寫。對應反例：在 code 內 hard-code 模板、變成僵化的客服機器人腔。</li>
</ul>
<p>最容易混的邊界在 step 6：「草擬 email」是 fuzzy（要寫人話、tailor to 情境）、「發送 email」是 deterministic（呼叫 API、處理錯誤碼）。把這兩件事拆開、草擬可以 retry / 改 prompt 不影響發送邏輯、發送有結構化 error 不被 LLM hallucinate 蓋過。Step 4「執行改地址」也類似：tool call 本身 deterministic、但是否該 call 的判讀回到 step 3 的 policy check。</p>
<p>引用原理：<a href="/blog/llm/00-foundations/deterministic-vs-fuzzy-engineering/" data-link-title="0.8 Deterministic vs Fuzzy Engineering：軟體設計典範的位移" data-link-desc="傳統 deterministic 軟體跟 fuzzy LLM 軟體在資料、邏輯、分解、實驗成本四個維度的根本差異、以及哪段該 deterministic、哪段該 fuzzy 的決策框架">0.8 fuzzy engineering</a> 的「哪段該 deterministic / 哪段該 fuzzy」決策框架、特別是反模式「邊界用錯」段。</p>
<h2 id="階段-3工作流設計">階段 3：工作流設計</h2>
<p>對每個 step 選對應的工具：</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>設計選擇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. 解析意圖 + 抽取訊息</td>
          <td>Vanilla LLM call + structured output（output 強制 JSON schema：intent / order_id / new_address）</td>
      </tr>
      <tr>
          <td>2. 查訂單狀態</td>
          <td>Tool call → 內部 order API</td>
      </tr>
      <tr>
          <td>3. 查 policy</td>
          <td>Tool call → policy engine（純 deterministic、不過 LLM）</td>
      </tr>
      <tr>
          <td>4. 執行改地址</td>
          <td>Tool call → logistics API、寫操作前要 pre-act HITL（高風險 + 不可逆）</td>
      </tr>
      <tr>
          <td>5. 解釋 / 給替代方案</td>
          <td>LLM call + few-shot（從 case 庫 retrieve「類似情境怎麼解釋」、配 RAG）</td>
      </tr>
      <tr>
          <td>6. 草擬 email + 發出</td>
          <td>LLM call 寫 email + structured output 含 subject/body、發送透過 email API</td>
      </tr>
  </tbody>
</table>
<p>兩個容易選錯的 step 展開：</p>
<p><strong>Step 1 為何要 structured output、不是純 prompt 解析</strong>：抽取結果要餵 step 2-4 的 deterministic tool、order_id 抽錯就整個流程斷。純 prompt 描述「請輸出 JSON」是弱保證、structured output / constrained decoding 是強保證（見 <a href="/blog/llm/03-theoretical-foundations/constrained-decoding-internals/" data-link-title="3.10 Constrained decoding 內部：grammar mask 跟性能取捨" data-link-desc="Constrained decoding 的內部運作：token mask 計算、JSON schema / regex / CFG 三種 grammar、XGrammar pre-compile 機制、性能反而加速">3.10 constrained decoding 內部</a>）。Trade-off：強格式可能犧牲表達彈性、但這個 step 不需要彈性、要的是可靠。</p>
<p><strong>Step 5 為何配 RAG 而非純 few-shot</strong>：客服 case 涵蓋多種情境（訂單已出貨 / 已送達 / VIP / 一般 user / 不同國家 policy）、固定 few-shot 範例 cover 不全。RAG 從歷史 case 庫即時 retrieve 最相似的解釋範例、屬於 <a href="/blog/llm/04-applications/prompt-techniques-landscape/" data-link-title="4.0 Prompt 技術光譜：手法分類、取捨、組合模式" data-link-desc="Zero-shot / few-shot、chain-of-thought、role / template、reflection 等 prompt 技術的分類與取捨、何時 stack 何時不要 stack、跟 fine-tune / RAG / chaining 的邊界">4.0 prompt 技術光譜</a> context 軸的 retrieval-augmented prompting。</p>
<p>引用原理：</p>
<ul>
<li>Step 1 的 structured output → <a href="/blog/llm/04-applications/application-protocols/" data-link-title="4.6 應用層協議：function calling / structured output / MCP" data-link-desc="三個常被混為一談的概念：模型能力、sampling 約束、server 協議，三者的層級差異與組合方式">4.6 應用層協議</a></li>
<li>Step 2-4 的 tool 設計 → <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></li>
<li>Step 4 的 pre-act HITL → <a href="/blog/llm/04-applications/human-ai-collaboration/" data-link-title="4.5 人機協作拓樸：何時人介入、怎麼介入" data-link-desc="Centaur vs Cyborg 工作模式、jagged frontier、HITL 三種觸發時機（pre-act / mid-stream / post-hoc）、確認流程的設計避免橡皮圖章化">4.5 人機協作拓樸</a> pre-act 段。對比講座 Workera appeal 是 post-hoc、本案例選 pre-act 是因為改地址不可逆 + 物流影響大、必須在執行前審</li>
<li>Step 5 的 RAG → <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/04-applications/prompt-techniques-landscape/" data-link-title="4.0 Prompt 技術光譜：手法分類、取捨、組合模式" data-link-desc="Zero-shot / few-shot、chain-of-thought、role / template、reflection 等 prompt 技術的分類與取捨、何時 stack 何時不要 stack、跟 fine-tune / RAG / chaining 的邊界">4.0 prompt 技術光譜</a> context 軸</li>
</ul>
<h2 id="階段-4協議跟自主度決定">階段 4：協議跟自主度決定</h2>
<p>這個工作流的控制流是線性的（1→2→3→4→5→6）、有條件分支（step 3 結果決定走 4 還是 5）、但每步順序固定。判讀：</p>
<p><strong>該用什麼結構</strong>：</p>
<ul>
<li><strong>不適用 Multi-agent</strong>：步驟順序固定、角色差異不大、orchestration overhead 純增。</li>
<li><strong>不適用 Single agent loop（model 自決下一步）</strong>：本案例假設 single-turn / 短多 turn、步驟順序明確、不需要 agent 自決。若 user 互動多輪 + turn 數不固定（如 user 中途補資訊、改主意、追問）、可考慮 agent loop。</li>
<li><strong>採用 Multi-call pipeline + router</strong>：寫成 deterministic pipeline、step 3 後有 router 分流。</li>
</ul>
<p>引用原理：</p>
<ul>
<li><a href="/blog/llm/04-applications/multi-agent-topology/" data-link-title="4.8 Multi-Agent 拓樸：flat / hierarchical / agent-as-tool" data-link-desc="從 multi-call workflow 走到 multi-agent system 的判讀、flat vs hierarchical 拓樸、agent-as-tool 的 MCP 視角、specialization 跟 orchestration overhead 的取捨">4.8 multi-agent 拓樸</a> 的「先 multi-call、不夠再 multi-agent」反射</li>
<li><a href="/blog/llm/04-applications/workflow-patterns/" data-link-title="4.7 Workflow 編排模式" data-link-desc="Pipeline / router / parallel / reflection：多 LLM call 組合的四種基本模式與退化條件">4.7 workflow patterns</a> 的 pipeline + router 模式</li>
<li><a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 agent 架構</a> 的「先 single-call、不夠再 agent」反射</li>
</ul>
<p><strong>自主度</strong>：</p>
<ul>
<li>Step 1（parse）、5（解釋）、6（草擬 email）：full auto。</li>
<li>Step 2、3（查訂單、查 policy）：full auto（read-only）。</li>
<li>Step 4（執行改地址）：pre-act HITL（高風險 + 不可逆）、有 diff show、user 可以 reject。</li>
<li>Step 6（發 email）：可選 pre-act HITL（看公司風格、保守版要審 email、激進版自動發）。</li>
</ul>
<h2 id="階段-5trace-instrumentation">階段 5：Trace Instrumentation</h2>
<p>工作流上線前、先設計要記哪些資訊。<strong>Eval 跟 debug 都靠 trace、沒 trace 後面什麼都做不了</strong>。</p>
<p>每個 step 要記：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Input（完整）</td>
          <td>Debug 時要重現</td>
      </tr>
      <tr>
          <td>Output（完整）</td>
          <td>比對預期、做 regression set</td>
      </tr>
      <tr>
          <td>Latency</td>
          <td>找 bottleneck</td>
      </tr>
      <tr>
          <td>Token cost</td>
          <td>算成本</td>
      </tr>
      <tr>
          <td>Step name + version</td>
          <td>追蹤是哪個版本的 prompt / tool</td>
      </tr>
      <tr>
          <td>Decision branch</td>
          <td>Step 3 的 router 走哪邊</td>
      </tr>
      <tr>
          <td>Error（若有）</td>
          <td>結構化 error、不是 string</td>
      </tr>
  </tbody>
</table>
<p>整段 trace 要綁同一個 conversation_id、可以後面 join 起來看完整流程。</p>
<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>。</p>
<h2 id="階段-6eval-設計">階段 6：Eval 設計</h2>
<p>先選座標、再選工具。對本案例的每個 eval 需求、用 <a href="/blog/llm/04-applications/eval-design-framework/" data-link-title="4.13 Eval 設計座標系：三軸、八象限、何時測什麼" data-link-desc="Eval 設計三軸（objective↔subjective / component↔end-to-end / quantitative↔qualitative）、八象限的對應 eval 工具、軸選錯的訊號、跟 benchmarking / LLM-as-judge / tracing 的關係">4.13 三軸座標</a> 定位。下面列的 threshold 數字（95%、80%、≥4 等）是 illustrative、實際數字隨產品 baseline、user 容忍度、業務代價而定、不是通用標準。</p>
<h3 id="eval-1step-1-抽取準不準">Eval 1：Step 1 抽取準不準</h3>
<ul>
<li><strong>三軸</strong>：Objective（有 ground truth）+ Component（測單 step）+ Quantitative（accuracy）。</li>
<li><strong>工具</strong>：寫 100 個有標註的 query、跑 step 1、看 extraction accuracy（order_id 對 + new_address 對的比例）。</li>
<li><strong>Threshold</strong>：&lt; 95% 不上線。</li>
</ul>
<h3 id="eval-2step-2-4-tool-call-行為正確">Eval 2：Step 2-4 tool call 行為正確</h3>
<ul>
<li><strong>三軸</strong>：Objective + Component + Quantitative。</li>
<li><strong>工具</strong>：mock API、給 step 2-4 各 50 個 case、看 tool call 參數對不對、返回值處理對不對。</li>
<li><strong>Threshold</strong>：100%（這是 deterministic 行為、不該有錯）。</li>
</ul>
<h3 id="eval-3step-5-解釋品質">Eval 3：Step 5 解釋品質</h3>
<ul>
<li><strong>三軸</strong>：Subjective（沒有單一正解）+ Component + Quantitative。</li>
<li><strong>工具</strong>：LLM-as-judge with rubric（clarity / helpfulness / tone）、scale 1-5、aggregate average。</li>
<li><strong>Threshold</strong>：average ≥ 4、no 1-2 比例 &lt; 5%。</li>
</ul>
<h3 id="eval-4step-6-email-品質">Eval 4：Step 6 email 品質</h3>
<ul>
<li><strong>三軸</strong>：Subjective + Component + Quantitative + 加 Qualitative human review。</li>
<li><strong>工具</strong>：LLM judge 給分 + 每週抽 20 封 human review、看是否有 hallucinate 承諾、是否符合公司 tone。</li>
<li><strong>Threshold</strong>：judge 平均 ≥ 4、human review 沒有 critical issue。</li>
</ul>
<h3 id="eval-5e2e-success-rate">Eval 5：E2E success rate</h3>
<ul>
<li><strong>三軸</strong>：Objective + End-to-end + Quantitative。</li>
<li><strong>工具</strong>：跑 200 個 representative case、看「完整完成 + user 沒申訴」的比例。</li>
<li><strong>Threshold</strong>：≥ 85% baseline、降到 &lt; 80% alert。</li>
</ul>
<h3 id="eval-6user-滿意度">Eval 6：User 滿意度</h3>
<ul>
<li><strong>三軸</strong>：Subjective + End-to-end + Quantitative。</li>
<li><strong>工具</strong>：每次互動結束顯示 thumbs up/down + optional 留言、追蹤 weekly。</li>
<li><strong>Threshold</strong>：thumbs up rate &gt; 80%、appeal rate &lt; 5%。</li>
</ul>
<h3 id="eval-7failure-mode-pattern持續做">Eval 7：Failure mode pattern（持續做）</h3>
<ul>
<li><strong>三軸</strong>：Objective / Subjective + End-to-end + Qualitative。</li>
<li><strong>工具</strong>：每週讀 50 個 sampled traces + 100% 讀 failure / appeal traces、找 emerging pattern。</li>
<li><strong>產出</strong>：bug ticket、prompt 修改 hypothesis、policy 補強 hypothesis。</li>
</ul>
<p>引用原理：</p>
<ul>
<li>三軸座標 → <a href="/blog/llm/04-applications/eval-design-framework/" data-link-title="4.13 Eval 設計座標系：三軸、八象限、何時測什麼" data-link-desc="Eval 設計三軸（objective↔subjective / component↔end-to-end / quantitative↔qualitative）、八象限的對應 eval 工具、軸選錯的訊號、跟 benchmarking / LLM-as-judge / tracing 的關係">4.13 eval design framework</a></li>
<li>LLM judge rubric → <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></li>
<li>Trace 接 eval → <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></li>
</ul>
<h2 id="階段-7iteration-loop">階段 7：Iteration Loop</h2>
<p>上線後、不是「等出問題」、是<strong>持續 iteration</strong>。典型 iteration cycle：</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">Production trace + eval result
</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">[Error analysis：找 emerging pattern]
</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">   Hypothesis：哪一層有問題？
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">   ├── Prompt 層 → 改 prompt → A/B test → 看 eval 收斂
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   ├── Tool 層   → 改 tool / schema → 跑 component eval → 收斂
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">   ├── RAG 層    → 改 chunking / query rewriting → 跑 [retrieval recall](/llm/knowledge-cards/retrieval-recall/) → 收斂
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">   ├── Policy 層 → 改 deterministic rule → 跑 step 3 component eval → 收斂
</span></span><span class="line"><span class="ln">10</span><span class="cl">   └── Model 層  → 換 model → 跑全 eval set → 收斂
</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">[改動進 production]
</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">[Frozen baseline 留著、新版本跟它比、漂移看得見]</span></span></code></pre></div><p>判讀「該改哪一層」的反射：</p>
<table>
  <thead>
      <tr>
          <th>失敗訊號</th>
          <th>該改的層</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Step 1 抽錯訊息</td>
          <td>Prompt / structured output schema</td>
      </tr>
      <tr>
          <td>Tool call 參數錯</td>
          <td>Prompt 內 tool description / few-shot</td>
      </tr>
      <tr>
          <td>Tool 跑掛</td>
          <td>Tool 實作（不是 LLM 問題）</td>
      </tr>
      <tr>
          <td>RAG retrieve 不到相關案例</td>
          <td>Chunking / embedding / query rewriting</td>
      </tr>
      <tr>
          <td>Policy judgment 錯</td>
          <td>Deterministic rule（不是 LLM 問題）</td>
      </tr>
      <tr>
          <td>Email tone 不對</td>
          <td>Prompt（role / few-shot）</td>
      </tr>
      <tr>
          <td>Email hallucinate 承諾</td>
          <td>Output validator（不只是 prompt）</td>
      </tr>
      <tr>
          <td>整體 latency 太高</td>
          <td>找 trace bottleneck、可能要 cache / 並行</td>
      </tr>
  </tbody>
</table>
<p>引用原理：</p>
<ul>
<li>Prompt 跟 model 層的失敗診斷 → <a href="/blog/llm/04-applications/prompt-techniques-landscape/" data-link-title="4.0 Prompt 技術光譜：手法分類、取捨、組合模式" data-link-desc="Zero-shot / few-shot、chain-of-thought、role / template、reflection 等 prompt 技術的分類與取捨、何時 stack 何時不要 stack、跟 fine-tune / RAG / chaining 的邊界">4.0 prompt 技術光譜</a> systematic vs random error</li>
<li>整體 fuzzy / deterministic 邊界判讀 → <a href="/blog/llm/00-foundations/deterministic-vs-fuzzy-engineering/" data-link-title="0.8 Deterministic vs Fuzzy Engineering：軟體設計典範的位移" data-link-desc="傳統 deterministic 軟體跟 fuzzy LLM 軟體在資料、邏輯、分解、實驗成本四個維度的根本差異、以及哪段該 deterministic、哪段該 fuzzy 的決策框架">0.8</a></li>
</ul>
<h2 id="五個容易遺漏的設計反射">五個容易遺漏的設計反射</h2>
<p>實務上常常省略這五個反射動作、走進無收斂迭代：</p>
<h3 id="反射一先觀察再開-ide">反射一：先觀察、再開 IDE</h3>
<p>階段 1 的價值是把 task decomposition 跟真實人類工作流對齊。沒這層對齊、寫出來的 prompt 跟 tool 拆法跟 reality 偏離、三天後重做。階段 1 的兩天比階段 3 的兩週值得。對應反例：「我先寫個 prompt 試試」、跳過觀察直接寫 code。</p>
<h3 id="反射二policy-寫成-codellm-只解析意圖">反射二：Policy 寫成 code、LLM 只解析意圖</h3>
<p>判斷類規則（user tier、訂單狀態、可否操作）走 deterministic code、LLM 只負責「user 想做什麼」這層意圖抽取。這條邊界讓 debug 容易、規則更新不用 prompt iteration。對應反例：「LLM、請判斷這個訂單能不能改地址、規則如下：&hellip;」——把判斷塞進 prompt、debug 困難、規則漂移無從追蹤。對應 <a href="/blog/llm/00-foundations/deterministic-vs-fuzzy-engineering/" data-link-title="0.8 Deterministic vs Fuzzy Engineering：軟體設計典範的位移" data-link-desc="傳統 deterministic 軟體跟 fuzzy LLM 軟體在資料、邏輯、分解、實驗成本四個維度的根本差異、以及哪段該 deterministic、哪段該 fuzzy 的決策框架">0.8</a> 的「邊界用錯」反模式。</p>
<h3 id="反射三trace-是-day-1-設計">反射三：Trace 是 day-1 設計</h3>
<p>從第一天就把 input / output / latency / token / step name / decision branch / error 進 trace、綁同一個 conversation_id。Eval 跟 debug 都靠 trace、沒 trace 後面什麼都做不了。對應反例：「先讓系統跑起來、之後再加 trace」——出 bug 時 debug 從零開始、production trace 不可回溯。</p>
<h3 id="反射四deterministic-行為用-deterministic-check">反射四：Deterministic 行為用 deterministic check</h3>
<p>有 ground truth 的行為（抽取對不對、API 參數對不對、JSON schema 合不合）用 Python 函數驗證、判斷成本低、精度高。LLM judge 留給沒 ground truth 的 subjective 行為。對應反例：用 LLM judge 測「step 1 抽取對不對」——cost 翻倍、精度反而不如 deterministic check。對應 <a href="/blog/llm/04-applications/eval-design-framework/" data-link-title="4.13 Eval 設計座標系：三軸、八象限、何時測什麼" data-link-desc="Eval 設計三軸（objective↔subjective / component↔end-to-end / quantitative↔qualitative）、八象限的對應 eval 工具、軸選錯的訊號、跟 benchmarking / LLM-as-judge / tracing 的關係">4.13</a> 軸誤選一。</p>
<h3 id="反射五保留-frozen-baseline">反射五：保留 frozen baseline</h3>
<p><a href="/blog/llm/knowledge-cards/frozen-baseline/" data-link-title="Frozen baseline" data-link-desc="Eval 系統中固定特定 prompt &#43; model 當長期對照、讓行為漂移可見的標準作法">Frozen baseline</a> 是把某個特定 prompt + 特定 model 跑 production 一段時間後 freeze 起來、每次新版本都跟它比、漂移看得見。對應反例：每次只跟「上一版」比、半年後累積漂移完全不可見、「整體變好了沒」無從回答。</p>
<h2 id="跟其他章節的對應總表">跟其他章節的對應總表</h2>
<p>本案例每階段引用的原理章節彙整：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>引用章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. 觀察人類工作流</td>
          <td><a href="/blog/llm/00-foundations/deterministic-vs-fuzzy-engineering/" data-link-title="0.8 Deterministic vs Fuzzy Engineering：軟體設計典範的位移" data-link-desc="傳統 deterministic 軟體跟 fuzzy LLM 軟體在資料、邏輯、分解、實驗成本四個維度的根本差異、以及哪段該 deterministic、哪段該 fuzzy 的決策框架">0.8 fuzzy engineering</a></td>
      </tr>
      <tr>
          <td>2. 典範定位</td>
          <td><a href="/blog/llm/00-foundations/deterministic-vs-fuzzy-engineering/" data-link-title="0.8 Deterministic vs Fuzzy Engineering：軟體設計典範的位移" data-link-desc="傳統 deterministic 軟體跟 fuzzy LLM 軟體在資料、邏輯、分解、實驗成本四個維度的根本差異、以及哪段該 deterministic、哪段該 fuzzy 的決策框架">0.8 fuzzy engineering</a></td>
      </tr>
      <tr>
          <td>3. 工作流設計（prompt / tool / RAG / HITL）</td>
          <td><a href="/blog/llm/04-applications/prompt-techniques-landscape/" data-link-title="4.0 Prompt 技術光譜：手法分類、取捨、組合模式" data-link-desc="Zero-shot / few-shot、chain-of-thought、role / template、reflection 等 prompt 技術的分類與取捨、何時 stack 何時不要 stack、跟 fine-tune / RAG / chaining 的邊界">4.0</a>、<a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1</a>、<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/human-ai-collaboration/" data-link-title="4.5 人機協作拓樸：何時人介入、怎麼介入" data-link-desc="Centaur vs Cyborg 工作模式、jagged frontier、HITL 三種觸發時機（pre-act / mid-stream / post-hoc）、確認流程的設計避免橡皮圖章化">4.5</a></td>
      </tr>
      <tr>
          <td>4. 結構決定（multi-call vs agent vs multi-agent）</td>
          <td><a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4</a>、<a href="/blog/llm/04-applications/workflow-patterns/" data-link-title="4.7 Workflow 編排模式" data-link-desc="Pipeline / router / parallel / reflection：多 LLM call 組合的四種基本模式與退化條件">4.7</a>、<a href="/blog/llm/04-applications/multi-agent-topology/" data-link-title="4.8 Multi-Agent 拓樸：flat / hierarchical / agent-as-tool" data-link-desc="從 multi-call workflow 走到 multi-agent system 的判讀、flat vs hierarchical 拓樸、agent-as-tool 的 MCP 視角、specialization 跟 orchestration overhead 的取捨">4.8</a></td>
      </tr>
      <tr>
          <td>5. Trace instrumentation</td>
          <td><a href="/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20 LLM tracing</a></td>
      </tr>
      <tr>
          <td>6. Eval 設計</td>
          <td><a href="/blog/llm/04-applications/eval-design-framework/" data-link-title="4.13 Eval 設計座標系：三軸、八象限、何時測什麼" data-link-desc="Eval 設計三軸（objective↔subjective / component↔end-to-end / quantitative↔qualitative）、八象限的對應 eval 工具、軸選錯的訊號、跟 benchmarking / LLM-as-judge / tracing 的關係">4.13 eval framework</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、量測本地推論速度的完整方法論">4.14</a>、<a href="/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21</a></td>
      </tr>
      <tr>
          <td>7. Iteration loop</td>
          <td><a href="/blog/llm/04-applications/prompt-techniques-landscape/" data-link-title="4.0 Prompt 技術光譜：手法分類、取捨、組合模式" data-link-desc="Zero-shot / few-shot、chain-of-thought、role / template、reflection 等 prompt 技術的分類與取捨、何時 stack 何時不要 stack、跟 fine-tune / RAG / chaining 的邊界">4.0 prompt 光譜</a> systematic vs random error 段</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步">下一步</h2>
<p>返回：<a href="/blog/llm/04-applications/" data-link-title="模組四：LLM 應用層原理" data-link-desc="Prompt 技術光譜、RAG、tool use、agent、應用層協議、人機協作、multi-agent、workflow 編排、eval 設計：跨工具不變的概念地圖">模組四首頁</a>、或回到 <a href="/blog/llm/04-applications/hands-on/" data-link-title="4.x Hands-on：端到端案例" data-link-desc="把模組四的所有原理串成具體 case study：從 task decomposition、workflow 設計、eval 設計到 iteration loop">hands-on 索引</a>。</p>
]]></content:encoded></item><item><title>Case Study：Blog 語意搜尋從 pickle 到 production</title><link>https://tarrragon.github.io/blog/llm/04-applications/hands-on/blog-vector-search/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/hands-on/blog-vector-search/</guid><description>&lt;p>本案例記錄一個技術 blog（2,738 篇 markdown、24,216 chunks）的語意搜尋工具從 demo 到 production 的完整過程。每段標出對應 &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;/p>
&lt;blockquote>
&lt;p>&lt;strong>實測日期&lt;/strong>：2026-07-01
&lt;strong>環境&lt;/strong>：macOS Apple Silicon、Ollama 0.7.x、&lt;code>nomic-embed-text&lt;/code>（768 維）
&lt;strong>Corpus&lt;/strong>：&lt;code>content/&lt;/code> 全量 2,738 個 markdown 檔、24,216 chunks
&lt;strong>前置 demo&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &amp;#43; cosine retrieval &amp;#43; Ollama chat、validating 4.0 RAG 原理">rag-demo&lt;/a>（pickle、463 chunks）&lt;/p>&lt;/blockquote>
&lt;h3 id="讀法建議">讀法建議&lt;/h3>
&lt;p>本案例用 Go 重寫了 RAG storage 層，Go 實作細節佔不少篇幅。依你的背景選讀法：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Python 開發者、想選自己專案的 storage 方案&lt;/strong>：先跳到「通用可複製流程」（語言無關的五步驟）→「四方案 benchmark」→「二次選型評估」（結論/理由/前提三層框架），這三段跨語言可遷移。Go 實作段（架構、效能優化）可 skim。&lt;/li>
&lt;li>&lt;strong>Go 開發者、想做類似工具&lt;/strong>：從頭讀，每段都跟你相關。&lt;/li>
&lt;li>&lt;strong>只想看選型框架、不管實作&lt;/strong>：直接跳「二次選型評估」。&lt;/li>
&lt;/ul>
&lt;h2 id="從-demo-到-production-的重寫動機">從 demo 到 production 的重寫動機&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &amp;#43; cosine retrieval &amp;#43; Ollama chat、validating 4.0 RAG 原理">rag-demo&lt;/a> 用 Python pickle 跑通了 RAG 概念驗證：71 篇 → 463 chunks → pickle 儲存 → cosine retrieval → Ollama 生成。概念層完全正確（4.1 的 retrieval + augmentation 骨架），但作為這個 blog 的日常工具有三個&lt;strong>專案特有的&lt;/strong>限制：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>工具鏈語言不同&lt;/strong>：blog 的核心工具是 Go（lint / fmt / cards），加 Python dependency 讓其他維護者 clone 後多一步環境設定。Python 專案不會有這個問題 — pickle 綁 Python 對 Python 專案是優點而非缺點。&lt;/li>
&lt;li>&lt;strong>只索引部分 corpus&lt;/strong>：rag-demo 只跑 &lt;code>content/llm/&lt;/code>（71 篇），blog 全量有 2,738 篇、24 個 section。&lt;/li>
&lt;li>&lt;strong>Demo 定位&lt;/strong>：ingest.py / query.py 是教學程式碼，不是維護工具（沒有 status、沒有 section filter）。&lt;/li>
&lt;/ol>
&lt;p>這是一次&lt;strong>完整重寫&lt;/strong>、不是漸進升級 — rag-demo 的 Python 程式碼不會被修改或遷移，而是用 Go 重新實作相同的 RAG pipeline（chunk → embed → store → search）、保留相同的概念架構。rag-demo 作為教學 demo 繼續存在。&lt;/p>
&lt;p>升級目標：一個跟 &lt;code>mdtools&lt;/code> 同級的 Go CLI 工具，能對全量 content 做語意搜尋，其他維護者 clone 後 &lt;code>go build&lt;/code> 即可用。完整原始碼在 &lt;code>scripts/blogsearch/&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<p>本案例記錄一個技術 blog（2,738 篇 markdown、24,216 chunks）的語意搜尋工具從 demo 到 production 的完整過程。每段標出對應 <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> 的哪個判讀步驟，讓讀者看到原理章的框架怎麼落到具體決策。</p>
<blockquote>
<p><strong>實測日期</strong>：2026-07-01
<strong>環境</strong>：macOS Apple Silicon、Ollama 0.7.x、<code>nomic-embed-text</code>（768 維）
<strong>Corpus</strong>：<code>content/</code> 全量 2,738 個 markdown 檔、24,216 chunks
<strong>前置 demo</strong>：<a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">rag-demo</a>（pickle、463 chunks）</p></blockquote>
<h3 id="讀法建議">讀法建議</h3>
<p>本案例用 Go 重寫了 RAG storage 層，Go 實作細節佔不少篇幅。依你的背景選讀法：</p>
<ul>
<li><strong>Python 開發者、想選自己專案的 storage 方案</strong>：先跳到「通用可複製流程」（語言無關的五步驟）→「四方案 benchmark」→「二次選型評估」（結論/理由/前提三層框架），這三段跨語言可遷移。Go 實作段（架構、效能優化）可 skim。</li>
<li><strong>Go 開發者、想做類似工具</strong>：從頭讀，每段都跟你相關。</li>
<li><strong>只想看選型框架、不管實作</strong>：直接跳「二次選型評估」。</li>
</ul>
<h2 id="從-demo-到-production-的重寫動機">從 demo 到 production 的重寫動機</h2>
<p><a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">rag-demo</a> 用 Python pickle 跑通了 RAG 概念驗證：71 篇 → 463 chunks → pickle 儲存 → cosine retrieval → Ollama 生成。概念層完全正確（4.1 的 retrieval + augmentation 骨架），但作為這個 blog 的日常工具有三個<strong>專案特有的</strong>限制：</p>
<ol>
<li><strong>工具鏈語言不同</strong>：blog 的核心工具是 Go（lint / fmt / cards），加 Python dependency 讓其他維護者 clone 後多一步環境設定。Python 專案不會有這個問題 — pickle 綁 Python 對 Python 專案是優點而非缺點。</li>
<li><strong>只索引部分 corpus</strong>：rag-demo 只跑 <code>content/llm/</code>（71 篇），blog 全量有 2,738 篇、24 個 section。</li>
<li><strong>Demo 定位</strong>：ingest.py / query.py 是教學程式碼，不是維護工具（沒有 status、沒有 section filter）。</li>
</ol>
<p>這是一次<strong>完整重寫</strong>、不是漸進升級 — rag-demo 的 Python 程式碼不會被修改或遷移，而是用 Go 重新實作相同的 RAG pipeline（chunk → embed → store → search）、保留相同的概念架構。rag-demo 作為教學 demo 繼續存在。</p>
<p>升級目標：一個跟 <code>mdtools</code> 同級的 Go CLI 工具，能對全量 content 做語意搜尋，其他維護者 clone 後 <code>go build</code> 即可用。完整原始碼在 <code>scripts/blogsearch/</code>。</p>
<h2 id="選型過程對應-422-演化階梯--工程約束">選型過程（對應 4.22 演化階梯 + 工程約束）</h2>
<h3 id="第一軸規模判讀">第一軸：規模判讀</h3>
<p>全量 content 產生 24,216 chunks（原本估計 ~1,500）。按 4.22 判讀樹，24K 落在「10K-100K → HNSW 或 brute-force」區間。預估 vs 實際的 16 倍落差揭露一個教訓：<strong>估計 chunk 數不能用篇數乘以常數</strong>，要看每篇的實際長度跟 chunking 策略。</p>
<h3 id="第二軸工程約束本專案特有">第二軸：工程約束（本專案特有）</h3>
<p>以下四個 constraint 反映<strong>這個 blog 專案的偏好</strong>、不是通用判準。換一組 constraint 會篩出完全不同的方案 — Python 專案不會有「Go 單 binary」constraint、已有 Docker 的團隊不會排斥外部 server。讀者套用時應先列出自己專案的 constraint、不是照搬這張表。</p>
<table>
  <thead>
      <tr>
          <th>Constraint</th>
          <th>砍掉什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Go 單 binary</td>
          <td>Python-only 方案（pickle / FAISS）</td>
      </tr>
      <tr>
          <td>不要 CGo</td>
          <td>sqlite-vec（需要 <code>mattn/go-sqlite3</code>）</td>
      </tr>
      <tr>
          <td>不要外部 server</td>
          <td>Qdrant / Weaviate / Pinecone</td>
      </tr>
      <tr>
          <td>Ollama 原生</td>
          <td>OpenAI / Cohere embedding（多一個 API key）</td>
      </tr>
  </tbody>
</table>
<p>剩餘選項：<strong>Go + flat file + brute-force</strong>。</p>
<h3 id="第三軸延遲容忍">第三軸：延遲容忍</h3>
<p>CLI 工具、每天用幾次、不是 API server。&lt; 500ms 可接受。</p>
<p>結論：選階段二（flat file），brute-force cosine。</p>
<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">scripts/blogsearch/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── main.go                     # CLI: ingest / query / status
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">├── cmd/
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│   ├── ingest.go               # walk content/ → chunk → embed → store
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│   ├── query.go                # load → embed query → cosine top-K → lazy load text
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│   └── status.go               # index stats
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">└── internal/
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    ├── chunk/chunk.go           # paragraph-aware markdown chunking
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    ├── embed/embed.go           # Ollama HTTP API wrapper
</span></span><span class="line"><span class="ln">10</span><span class="cl">    ├── search/search.go         # brute-force cosine similarity
</span></span><span class="line"><span class="ln">11</span><span class="cl">    └── store/store.go           # 三檔案 binary store</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"># 語意搜尋</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">./bin/blogsearch query <span class="s2">&#34;retry 策略&#34;</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"># 只搜特定 section</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">./bin/blogsearch query -section backend <span class="s2">&#34;connection pool 設定&#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"># 查 index 狀態</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">./bin/blogsearch status</span></span></code></pre></div><h3 id="storage-格式三檔案分離">Storage 格式（三檔案分離）</h3>





<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">.blogsearch/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── vectors.bin    # float32 binary（70.9 MB）— bulk read + unsafe.Slice 零拷貝
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── meta.json      # compact metadata 不含 text（7.3 MB）
</span></span><span class="line"><span class="ln">4</span><span class="cl">└── texts.bin      # length-prefixed chunk text（19.2 MB）— top-K 才 lazy load</span></span></code></pre></div><p>分離 text 的設計理由：query 時只需要 vectors + metadata 做 cosine search（78 MB），top-K 結果才從 texts.bin 按 offset 讀取 5 筆 text。省掉 19 MB 的 JSON 解析。</p>
<h2 id="效能優化歷程">效能優化歷程</h2>
<h3 id="初版95-秒">初版：9.5 秒</h3>
<p>初版用逐 4-byte Read 載入 vectors.bin（17.5M 次 <code>f.Read(buf)</code>），加上 27MB 的 index.json（含所有 chunk text）一次 JSON 解析。</p>
<h3 id="優化版034-秒28x">優化版：0.34 秒（28x）</h3>
<p>三項改動：</p>
<table>
  <thead>
      <tr>
          <th>改動</th>
          <th>從</th>
          <th>到</th>
          <th>效果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>vectors.bin 讀法</td>
          <td>逐 4-byte Read</td>
          <td><code>os.ReadFile</code> + <code>unsafe.Slice</code></td>
          <td>I/O call 17.5M → 1</td>
      </tr>
      <tr>
          <td>metadata 格式</td>
          <td>含 text（27 MB）</td>
          <td>不含 text（7.3 MB）</td>
          <td>JSON parse 快 4x</td>
      </tr>
      <tr>
          <td>text 載入</td>
          <td>全量</td>
          <td>top-K lazy load（只讀 5 筆）</td>
          <td>省 19 MB 讀取</td>
      </tr>
  </tbody>
</table>
<p>瓶頸分析：0.34 秒裡、embedding API call（Ollama）約 77ms、file I/O + JSON parse 約 200ms、cosine 計算約 50ms。cosine 計算只佔 15%。</p>
<h2 id="通用可複製流程抽掉-goblog">通用可複製流程（抽掉 Go/blog）</h2>
<p>本案例的 Go 實作細節（<code>unsafe.Slice</code>、<code>os.ReadFile</code>）是語言特定的、但背後的流程步驟跨語言通用：</p>
<ol>
<li><strong>Walk corpus</strong>：遞迴掃描目標目錄的所有文件（markdown / code / 任意文字）</li>
<li><strong>Chunk</strong>：段落感知分割、soft token cap、保留語意邊界（原理見 <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 Chunking</a>）</li>
<li><strong>Embed</strong>：對每個 chunk 呼叫 embedding API（本地 Ollama 或 cloud API），得到固定維度向量</li>
<li><strong>Store</strong>：向量 + metadata + text 分離存檔（binary vectors / compact JSON / lazy-load text）</li>
<li><strong>Search</strong>：embed query → brute-force cosine → top-K → lazy load text for display</li>
</ol>
<p>Python 實作同流程只是把第 4 步的 binary 檔換成 pickle / FAISS index / SQLite DB、第 5 步的 cosine 換成 numpy / FAISS / sqlite-vec query。Node.js / Rust 同理。</p>
<p>關鍵優化原則也跨語言：「分離向量與文字、query 時只載入向量、top-K 才載入文字」讓 I/O 量從 ~98MB 降到 ~78MB、JSON parse 從 27MB 降到 7MB。這個原則用什麼語言實作都有效。</p>
<h2 id="四方案同-corpus-benchmark">四方案同 corpus Benchmark</h2>
<p>用同一個 corpus（24,216 chunks、768 維、nomic-embed-text）比較四種 storage 方案。Benchmark 腳本在 <code>scripts/blogsearch-bench/bench.py</code>。</p>
<h3 id="前置依賴">前置依賴</h3>
<p>Benchmark 腳本讀 Go 工具產生的 index（<code>.blogsearch/</code> 下的 <code>vectors.bin</code> + <code>meta.json</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="nb">cd</span> scripts/blogsearch <span class="o">&amp;&amp;</span> go build -o ../../bin/blogsearch .   <span class="c1"># build Go 工具</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ollama serve <span class="p">&amp;</span>                                                  <span class="c1"># 啟動 Ollama</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">ollama pull nomic-embed-text                                    <span class="c1"># pull embedding model</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">./bin/blogsearch ingest -content content -out .blogsearch       <span class="c1"># 建 index（~4 分鐘）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">uv run --with sqlite-vec --with faiss-cpu --with numpy <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  scripts/blogsearch-bench/bench.py --index .blogsearch         <span class="c1"># 跑 benchmark</span></span></span></code></pre></div><p>若無 Go 環境，可用自己的 Python embedding 腳本產生相同格式的 <code>vectors.bin</code>（little-endian float32、n × dim 連續排列）+ <code>meta.json</code>（<code>{&quot;dim&quot;: 768, &quot;count&quot;: n, &quot;metas&quot;: [...]}</code>），benchmark 腳本只讀這兩個檔案、不依賴 Go binary 本身。Corpus 格式無硬性要求，任何目錄下的 <code>.md</code> 檔案都可索引。</p>
<h3 id="方法論">方法論</h3>
<ul>
<li><strong>Embedding</strong>：四方案共用同一組 embedding（從 Go index 載入），排除 embedding model 差異</li>
<li><strong>Query</strong>：同一句 query（&ldquo;RAG storage 選型&rdquo;），跑 5 次取 median</li>
<li><strong>Ingest 時間</strong>：只計 storage 操作（不含 embedding），Go 方案含 embedding 不可分離故標 —</li>
<li><strong>環境</strong>：macOS Apple Silicon、Python 3.12、Go 1.25</li>
</ul>
<h3 id="結果">結果</h3>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>Ingest（純 storage）</th>
          <th>Query（median）</th>
          <th>Index 大小</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Go + flat file</td>
          <td>—</td>
          <td>151ms</td>
          <td>97.4 MB</td>
      </tr>
      <tr>
          <td>Python sqlite-vec</td>
          <td>2.9s</td>
          <td>19ms</td>
          <td>75.3 MB</td>
      </tr>
      <tr>
          <td>Python FAISS flat</td>
          <td>40ms</td>
          <td>1.8ms</td>
          <td>in-memory</td>
      </tr>
      <tr>
          <td>Python FAISS HNSW</td>
          <td>23.3s</td>
          <td>0.5ms</td>
          <td>in-memory</td>
      </tr>
  </tbody>
</table>
<h3 id="三個關鍵發現">三個關鍵發現</h3>
<p><strong>延遲瓶頸在 I/O 和實作、不在演算法</strong>。Go flat file 的 151ms 裡、cosine 計算約 50ms、file I/O 約 100ms。FAISS flat 用 numpy BLAS 做同樣的 brute-force cosine、純計算 1.8ms — 計算層差約 28 倍（Go pure loop vs BLAS 向量化指令），加上 I/O 後端到端差 84 倍。</p>
<p><strong>HNSW 的 query 加速在此規模 ROI 低</strong>。FAISS HNSW query 0.5ms vs flat 1.8ms、每次省 1.3ms。但 HNSW build 要 23.3s。每天查 100 次、要 179 天才回本 build 成本（23.3s ÷ 0.13s/天）。4.22 的判讀結論（「此規模 brute-force 夠用」）被數據驗證。</p>
<p><strong>sqlite-vec 的 19ms 是「DB overhead 換功能」</strong>。比 FAISS flat 慢 10 倍、但多了 SQL metadata filter、transaction 保護、disk persistence。對「需要 filter 但不想維運 server」的場景有意義。</p>
<h3 id="讀數據的注意事項">讀數據的注意事項</h3>
<ul>
<li>Go 151ms 含 file I/O（每次 query 重載 78MB）；如果做 daemon mode（常駐、載入一次），query 會降到 ~50ms（純 cosine + overhead）</li>
<li>FAISS 數字是 in-memory baseline（index 已載入），不含 index 檔案的載入時間</li>
<li>sqlite-vec 數字含 disk I/O（每次 query 從 SQLite 讀取），是 persistent storage 的真實代價</li>
<li>四方案都不含 Ollama embedding call 時間（~77ms），實際端到端延遲要加上</li>
</ul>
<h2 id="二次選型評估同結論理由鏈翻轉">二次選型評估：同結論、理由鏈翻轉</h2>
<p>Benchmark 數據出來後，80 倍效能差距讓原始選型（Go + flat file）受到質疑：「是否該換 Python + FAISS 或 sqlite-vec？」重新用 WRAP 框架評估，結論相同（維持 Go），但理由鏈完全不同。</p>
<h3 id="第一次選型的理由事前">第一次選型的理由（事前）</h3>
<p>「Go 工具鏈統一（mdtools 是 Go）+ 單 binary 分發（clone 後 <code>go build</code> 即可）。」</p>
<h3 id="實測推翻的前提">實測推翻的前提</h3>
<table>
  <thead>
      <tr>
          <th>原始假設</th>
          <th>實測</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Corpus ~1,500 chunks</td>
          <td>24,216 chunks（16 倍）</td>
      </tr>
      <tr>
          <td>Brute-force &lt; 10ms</td>
          <td>Go 151ms（I/O 瓶頸、不是計算）</td>
      </tr>
      <tr>
          <td>語言效能差異不大</td>
          <td>Go pure cosine vs numpy BLAS 差 80 倍</td>
      </tr>
      <tr>
          <td>「工具鏈統一」很重要</td>
          <td>mdtools（pre-commit、延遲敏感）跟 blogsearch（手動 CLI、每天幾次）使用模式不同，強制統一語言是用「同一棟建築」邏輯要求「不同用途房間用同一種建材」</td>
      </tr>
  </tbody>
</table>
<p>第一次的理由鏈幾乎全數被推翻。如果只看理由，應該換方案。</p>
<h3 id="第二次選型的理由事後">第二次選型的理由（事後）</h3>
<p>重新評估時加入三個第一次沒有的變數：</p>
<p><strong>端到端延遲 vs in-memory benchmark</strong>。84 倍是端到端的數字（Go 151ms 含 I/O vs FAISS 1.8ms in-memory）。但 FAISS 從 disk 載入 index 也要 ~100-200ms，端到端差距縮小到 2 倍。sqlite-vec 是唯一不需要全量載入的方案（disk-based HNSW、端到端 19ms），差距從「84 倍」變成「8 倍」。</p>
<p><strong>使用頻率決定 ROI</strong>。CLI 工具、每天 ~10 次手動 query。每次省 130ms（151 vs 19），一天省 1.3 秒。重寫投入 2-3 小時，回本時間 ≈ 19 年。注意這個計算對頻率極敏感：每天 100 次（如被整合進 MCP server 當 agent 工具）回本縮短到 1.9 年、每天 1000 次則 69 天。上方 HNSW ROI 也用每天 100 次計算 — 兩處頻率假設不同是因為比較對象不同（HNSW build 成本 vs 語言重寫成本），但讀者套到自己場景時應先確定自己的查詢頻率。</p>
<p><strong>Ingest 瓶頸在 Ollama API、跟語言無關</strong>。~4 分鐘的 ingest 裡、embedding API call 佔 95% 以上。換 Python 不會改善 ingest 速度。</p>
<h3 id="維持的理由是痛點不存在">維持的理由是「痛點不存在」</h3>
<p>維持 Go 的理由是<strong>改善的絕對收益太小、投入回不了本</strong> — 151ms 對 CLI 使用模式不構成痛點，與「Go 好」或「工具鏈統一」無關。</p>
<h3 id="這個翻轉的教學意義">這個翻轉的教學意義</h3>
<p>正確的結論配錯誤的理由是脆弱的。第一次 WRAP 的結論（選 Go）在當時是對的，但理由鏈（工具鏈統一、&lt; 10ms）被實測推翻後，如果不重新建立正確的理由鏈，下次環境變動（比如 blogsearch 從 CLI 變成 API server）就會用已失效的理由做出錯誤判斷。</p>
<p>判讀工具選型時，要區分三層：</p>
<ol>
<li><strong>結論</strong>：選什麼方案</li>
<li><strong>理由</strong>：為什麼選（可能被推翻）</li>
<li><strong>前提</strong>：理由依賴的假設（規模、使用模式、效能數字）</li>
</ol>
<p>前提變了、理由就要重建，即使結論沒變。寫進決策紀錄時，三層都要記 — 只記結論的話，下次重新評估時沒有判讀基礎。</p>
<p>區分「正當理由重建」跟「動機性推理」（先有結論再找理由）的判準：新理由是否在看到數據之前也能成立？本例的「130ms 對 CLI 不痛」在實測前也成立（CLI 使用模式本來就低頻），所以是正當重建。如果新理由只能在看到特定數字之後才講得通（如「151ms 剛好在 200ms 閾值內」——但閾值是事後設的），就是 post-hoc rationalization。</p>
<h3 id="觸發換方案的訊號">觸發換方案的訊號</h3>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>門檻</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 延遲不可接受</td>
          <td>&gt; 500ms</td>
          <td>先加 mmap（最小改動）</td>
      </tr>
      <tr>
          <td>使用模式改變</td>
          <td>從 CLI 變 API server</td>
          <td>換 Python sqlite-vec</td>
      </tr>
      <tr>
          <td>查詢頻率跳增</td>
          <td>被整合進 MCP server / agent 工具</td>
          <td>評估 daemon mode 或換 sqlite-vec</td>
      </tr>
      <tr>
          <td>Corpus 規模跳增</td>
          <td>&gt; 50K chunks</td>
          <td>重跑 benchmark</td>
      </tr>
      <tr>
          <td>需要原生 metadata filter</td>
          <td>code filter 維護成本過高</td>
          <td>換 Python sqlite-vec</td>
      </tr>
  </tbody>
</table>
<h2 id="embedding-model-選型對應-412-constraint-優先序">Embedding model 選型（對應 4.12 constraint 優先序）</h2>
<p>選 <code>nomic-embed-text</code> 的理由鏈：</p>
<ol>
<li><strong>Ollama 原生支援</strong>：<code>ollama pull</code> 一行、不需要額外 Python library 或 API key</li>
<li><strong>體積小</strong>：274 MB、跟 chat model 共用記憶體不打架</li>
<li><strong>已有驗證基線</strong>：rag-demo 用同一個模型跑過 463 chunks、retrieval 命中率確認可用</li>
<li><strong>768 維 sweet spot</strong>：24K chunks × 768 dim × 4 bytes = 70.9 MB，brute-force 可行</li>
</ol>
<p>未來如果 CJK retrieval 品質不夠（目前可用但未做系統性評估），<code>multilingual-e5-large</code> 或 <code>bge-m3</code> 是備選。換模型只需改 <code>embed.go</code> 的 Model 變數 + 重新 <code>blogsearch ingest</code>（4.22 的「四層可替換」設計）。</p>
<h2 id="cjk-混合-chunking-觀察">CJK 混合 Chunking 觀察</h2>
<p>Blog 內容是繁體中文 + 英文術語混合。Chunking 策略沿用 rag-demo 的 paragraph-aware split（空白行切段、soft token cap 400）。</p>
<p>Token 估算用 <code>len(s) / 2</code> 的 heuristic（CJK 字元多算一次）。不精確但 chunking 只需要粗略估算。跟 tokenizer 精確計算的差異在 ±20%、對 chunking 品質影響小於 chunk 邊界選擇的影響。</p>
<p>實際觀察：24,216 chunks 的 retrieval 品質在語意搜尋場景（「哪些文章跟 retry 有關」「RAG storage 選型」）表現良好。keyword 精確搜尋場景（「找 RFC 7807」）表現較弱 — 這是 embedding-only retrieval 的已知限制（見 4.1 的語意 vs 字面相似度對比），未來可加 BM25 做 hybrid search。</p>
<h2 id="跟其他章節的對應">跟其他章節的對應</h2>
<table>
  <thead>
      <tr>
          <th>本案例的段落</th>
          <th>對應原理章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>選型過程</td>
          <td><a href="/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22 演化階梯 + 工程約束</a></td>
      </tr>
      <tr>
          <td>二次選型評估</td>
          <td><a href="/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22 同 corpus 實測比較</a></td>
      </tr>
      <tr>
          <td>Embedding 選型</td>
          <td><a href="/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">4.12 實務選型 constraint 優先序</a></td>
      </tr>
      <tr>
          <td>Chunking</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 失敗的根本原因">4.1 Chunking 策略對比</a></td>
      </tr>
      <tr>
          <td>Benchmark 方法論</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>Storage 格式設計</td>
          <td><a href="/blog/llm/04-applications/artifact-management/" data-link-title="4.10 衍生產物管理原理：什麼進 git、什麼不該" data-link-desc="LLM 應用的 source / derived / external 三類產物對應 git / build cache / registry、與 production 部署的 reproducibility / cost / share 取捨">4.10 衍生產物管理</a></td>
      </tr>
      <tr>
          <td>Retrieval 品質</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 失敗的根本原因">4.1 Retrieval 失敗根因</a></td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item></channel></rss>