分類: LLM 應用

  • 本地 AI 模型完整實測:五款模型 × 兩台機器 × 三種設定,找出真正的上限

    重點摘要

    • Mini PC(Ryzen 7 4700U):gemma4:e4b 最快(1.45 tok/s),qwen3:14b 最完整(7/7 題全答);Bonsai 8B 因 AVX-512 需求完全無法使用
    • 換一台機器差多少?:同款 gemma4:e4b,MacBook Air M3 跑出 9.75 tok/s,是 Mini PC 的 6.7 倍;Q2 輸出從截斷 300 tokens 變成完整 2218 tokens
    • 開 Thinking 差多少?:速度幾乎不變(-6%),但 Q2 程式碼輸出 +65%,Q7 技術解釋 +124%,品質接近 GPT-3.5

    你想在本地跑 AI 模型,但不知道哪款模型值得裝?硬體夠不夠?開 Thinking 模式到底有沒有差?這篇文章用實際跑出來的數據回答這三個問題——五款模型 × 兩台機器 × 三種設定,全部實測,沒有廣告。

    測試環境分別是一台平價 Mini PC(Ryzen 7 4700U,16GB RAM,CPU-only)和 MacBook Air M3(24GB 統一記憶體)。七道測試題涵蓋:文字理解、Python 程式碼生成、SQL 查詢、TCP 技術解釋等真實使用情境。

    測試環境規格

    項目 Mini PC MacBook Air M3
    處理器 AMD Ryzen 7 4700U Apple M3
    記憶體 16GB DDR4(CPU-only) 24GB 統一記憶體
    顯示卡 AMD Vega 7 iGPU,僅 128MB VRAM(不可用) Apple GPU(共享統一記憶體)
    推論框架 Ollama + llama.cpp(CPU 模式) Ollama + llama.cpp(Metal)
    特殊限制 無 AVX-512,部分模型無法執行 無限制

    重點摘要表(先看這三張表)

    表 1:Mini PC 五款模型速度與完成度對比

    模型 平均速度 完成度 主要問題
    Bonsai 8B 0.001 tok/s 完全不可用 需要 AVX-512,CPU 不支援
    qwen3:4b 0.78 tok/s 5/7(部分截斷) Q2/Q5 在 300 token 限制下截斷或夾雜文字
    qwen3.5:9b 0.57 tok/s 5/7 Q6/Q7 需延長 timeout 至 1800s
    qwen3:14b 0.58 tok/s 7/7 全答 無截斷問題,但速度慢
    gemma4:e4b 1.45 tok/s 5/7(舊測試 600 上限) Q2/Q4 在 600 token 舊限制下截斷

    表 2:同款 gemma4:e4b,硬體差距

    指標 Mini PC(CPU) MacBook Air M3 差距
    平均速度 1.45 tok/s 9.75 tok/s 6.7×
    Q2 輸出長度 600 tokens(截斷) 2218 tokens(完整) 記憶體夠,才不截斷
    Q4 輸出長度 600 tokens(截斷) 1043 tokens(完整) SQL 查詢完整輸出
    最大 num_ctx 受 16GB 限制 65536+ 長文件處理能力天差地別

    表 3:Mac 上同款模型,三種設定對比

    設定 平均速度 Q2 輸出 Q7 輸出 適合場景
    Mini PC 舊測試(600 limit) 1.45 tok/s 截斷 309 tokens 快速查詢
    Mac think:false,無限制 9.75 tok/s 2218 tokens ✅ 442 tokens 日常程式碼
    Mac Thinking 開啟,無限制 9.16 tok/s(-6%) 3670 tokens ✅ 990 tokens 複雜推理、技術解釋

    第一層:Mini PC 上,哪個模型值得跑?

    在只有 CPU 推論的環境下,選模型就是在「速度」與「品質」之間做取捨。以下是完整的三維評估。

    Bonsai 8B:直接淘汰

    Bonsai 8B 的速度是 0.001 tok/s——不是很慢,是根本無法執行。原因是它的量化版本依賴 AVX-512 指令集,而 Ryzen 7 4700U 不支援 AVX-512(只有 AVX2)。llama.cpp 在這種情況下會退回軟體模擬,速度接近零。如果你的機器是 Intel 第 11 代以後或 AMD Zen 4 以後,才有機會跑 Bonsai 8B。

    qwen3:4b:最快但有截斷風險

    qwen3:4b 在 Q1~Q7 七個量化等級測試中,平均跑出 0.78 tok/s,是 CPU 上可用模型裡的最高速。但在 num_predict=300 的限制下,Q2(程式碼生成)和 Q5(格式輸出)出現截斷或夾雜不相關文字的問題。如果你只需要短問短答,qwen3:4b Q6 量化(約 21 元台幣月費 API 等級)是最划算的選擇。

    qwen3.5:9b vs qwen3:14b:9B 更快但 14B 更可靠

    qwen3.5:9b 平均 0.57 tok/s,但 Q6/Q7 題遇到了 timeout 問題——需要將請求超時設定延長到 1800 秒才能完成。原因是 9B 模型在複雜任務上思考時間較長,但預設 timeout 不夠。

    qwen3:14b 同樣 0.58 tok/s,卻跑出 7/7 完整答題率。它的 Q2 完整輸出 500 tokens、Q4 完整輸出 500 tokens,沒有截斷。代價是記憶體佔用更高,在 16GB 機器上跑 14B 模型時需要注意 KV Cache 可能 OOM(記憶體不足),建議設定 num_ctx 不超過 4096。

    gemma4:e4b:速度最快的完整模型

    gemma4:e4b 平均 1.45 tok/s,是所有可用模型中最快的,幾乎是 qwen3:14b 的 2.5 倍。在舊測試的 600 token 限制下,Q2 和 Q4 被截斷。但如果移除限制(num_predict=-1),這個問題就不存在——這正是下一層要說的。

    Mini PC 選模型建議:

    • 追求速度 → gemma4:e4b(1.45 tok/s,移除 token 限制)
    • 追求品質 → qwen3:14b(7/7 完整,0.58 tok/s)
    • 省記憶體 → qwen3:4b Q3/Q4(短任務夠用)
    • 不建議 → Bonsai 8B(AVX-512 門檻)、qwen3.5:9b(需調 timeout)

    第二層:同款模型,換台機器差多少?

    用同款 gemma4:e4b,在 Mini PC 和 MacBook Air M3 上各跑一輪,看看換硬體能得到什麼。

    速度差 6.7 倍,不只是快慢的問題

    Mini PC 平均 1.45 tok/s,Mac 平均 9.75 tok/s。這個差距背後的原因是架構:Mini PC 用 x86 CPU 做矩陣運算,效率遠低於 Apple Silicon 的 Neural Engine + 統一記憶體架構。M3 的統一記憶體讓 CPU 和 GPU 共享同一塊 24GB,模型權重可以直接放在 GPU 能讀取的記憶體,不需要搬移。

    記憶體夠,輸出才完整

    這是硬體差距最直接的體現:Q2 要求生成一個能解析多種日期格式的 Python 函式,Mini PC 在 600 token 限制下就截斷了(回答還在中途),而 Mac 無限制跑出 2218 tokens 的完整函式

    Q4 要求生成帶有 CTE 和 Window Function 的複雜 SQL,Mini PC 截斷,Mac 輸出完整 1043 tokens 含說明。這不是模型能力的差異,是記憶體和 KV Cache 空間的差異。

    24GB 統一記憶體的隱藏優勢:num_ctx 可拉到 65536+

    num_ctx 決定模型能「看到」的上下文長度。Mini PC 的 16GB RAM 在跑 gemma4:e4b 時,實際可用的 KV Cache 空間有限,num_ctx 設太高就 OOM。Mac 的 24GB 統一記憶體可以輕鬆設定 num_ctx=65536,意味著可以貼入整個程式碼檔案、長文件、對話紀錄,模型不會「忘記」前面說了什麼。這個差距在實際工作流中比速度差距更重要。

    第三層:同台機器,調設定差多少?

    在 MacBook Air M3 上,用同款 gemma4:e4b 比較三種設定:舊測試的 600 token 限制、無限制(think:false)、開啟 Thinking 模式。

    速度幾乎不變,但輸出長度和品質大幅提升

    think:false 無限制:平均 9.75 tok/s。Thinking 開啟無限制:平均 9.16 tok/s。速度只差 6%,但輸出品質差距大:

    • Q2(Python 程式碼):2218 tokens → 3670 tokens(+65%),Thinking 版本多了完整的邊界條件處理和說明文字
    • Q4(SQL 查詢):1043 tokens → 1413 tokens(+35%),多了欄位說明和效能備注
    • Q7(TCP 三次握手):442 tokens → 990 tokens(+124%),Thinking 版本多了為何需要三次的深層解釋

    Thinking 模式讓模型在回答前先「想」一遍,輸出的答案更完整、更有條理。對於技術問題、程式碼生成、複雜推理,這個設定幾乎是免費的品質提升。

    gemma4:e4b 全力展開:這就是本地 LLM 的真實上限

    以下展示 Thinking 模式在 Mac 上的實際輸出,讓你判斷品質是否符合你的需求。

    Q2 Thinking:完整的多格式日期解析函式

    問題:「寫一個 Python 函式,能解析 YYYY-MM-DD、YYY/MM/DD(民國年)、YYYYMMDD(8位)、YYYMMDD(7位民國年)四種格式。」

    Thinking 模式輸出 3670 tokens,以下是完整的函式程式碼:

    from datetime import datetime
    import re
    
    def parse_mixed_date_string(date_str: str) -> datetime:
        if not isinstance(date_str, str):
            raise TypeError("輸入必須是字串 (string) 格式。")
        date_str = date_str.strip()
    
        # 模式 A: YYYY-MM-DD
        if re.fullmatch(r'\d{4}-\d{2}-\d{2}', date_str):
            try:
                return datetime.strptime(date_str, "%Y-%m-%d")
            except ValueError:
                pass
    
        # 模式 B: YYY/MM/DD(含民國年)
        if re.search(r'[/]', date_str):
            parts = date_str.split('/')
            if len(parts) == 3:
                year, month, day = parts[0], parts[1], parts[2]
                try:
                    temp_date_str = f"{year.zfill(4)}-{month.zfill(2)}-{day.zfill(2)}"
                    return datetime.strptime(temp_date_str, "%Y-%m-%d")
                except ValueError:
                    pass
    
        # 模式 C: YYYYMMDD(8位)
        if re.fullmatch(r'\d{8}', date_str):
            try:
                return datetime.strptime(date_str, "%Y%m%d")
            except ValueError:
                pass
    
        # 模式 D: YYYMMDD(7位民國年)
        if re.fullmatch(r'\d{7}', date_str):
            try:
                year = date_str[:-4]
                month = date_str[-4:-2]
                day = date_str[-2:]
                temp_date_str = f"{year.zfill(4)}-{month}-{day}"
                return datetime.strptime(temp_date_str, "%Y-%m-%d")
            except ValueError:
                pass
    
        raise ValueError(f"無法識別或解析的日期格式:'{date_str}'")

    非 Thinking 模式在同題只輸出 2218 tokens,函式邏輯正確但缺少邊界案例說明和型別標注說明。Thinking 版本多的那 1452 tokens 全是有用的:錯誤處理說明、邊界條件討論、使用範例。

    Q4 Thinking:帶 CTE 和視窗函數的複雜 SQL

    問題:「找出每個城市近 30 天消費金額前三名的顧客,輸出城市、姓名、消費總額。」

    WITH RecentSpending AS (
        SELECT o.customer_id, SUM(o.amount) AS total_spending
        FROM orders o
        WHERE o.created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
        GROUP BY o.customer_id
    ),
    RankedCustomers AS (
        SELECT c.name, c.city, rs.total_spending,
            RANK() OVER (PARTITION BY c.city ORDER BY rs.total_spending DESC) as city_rank
        FROM RecentSpending rs
        JOIN customers c ON rs.customer_id = c.id
    )
    SELECT city, name, total_spending
    FROM RankedCustomers
    WHERE city_rank <= 3
    ORDER BY city, city_rank;

    Thinking 版本(1413 tokens)在 SQL 後面額外附上了欄位說明對照表、RANK vs DENSE_RANK 的差異說明、以及在資料量大時建議加索引的備注。這種「自動補充說明」的行為,在程式碼審查或教學場景特別有用。

    Q7 Thinking:TCP 三次握手的技術解釋

    問題:「解釋 TCP 三次握手的過程,並說明為什麼需要三次而不是兩次。」

    Thinking 模式的回答(節錄,990 tokens):

    TCP 三次握手是在兩台設備開始傳輸實際資料之前,建立穩定可靠連線的過程:

    1. Client → Server (SYN):「我想連線,序列號從 X 開始」
    2. Server → Client (SYN-ACK):「我收到了,我也準備好,序列號從 Y 開始」
    3. Client → Server (ACK):「我收到你的確認,正式連線」

    為何需要三次而不是兩次:三次握手確保雙方的「發送能力」和「接收能力」都得到驗證。第三次 ACK 讓客戶端確認成功收到伺服器的肯定回應,確保雙方對初始序列號達成共識。若只有兩次,Server 無法確認 Client 是否真的收到了 SYN-ACK,可能導致半開連線(half-open connection)堆積。

    非 Thinking 模式(442 tokens)的回答只覆蓋了步驟本身,沒有解釋半開連線問題。Thinking 版本的 990 tokens 多出了協議設計的「為什麼」。

    技術踩坑筆記

    坑 1:AVX-512 問題不是模型 bug,是 CPU 選錯了

    Bonsai 8B 需要 AVX-512,AMD Ryzen 4000 系列(Zen 2 架構)不支援。解法:換用 AVX2 相容的量化版本,或換到支援 AVX-512 的 CPU(Intel 11th Gen+、AMD Zen 4+)。在買 Mini PC 跑本地 LLM 之前,先確認 CPU 指令集支援情況。

    坑 2:num_predict=300 在 CPU 機器上是陷阱

    設 num_predict=300 看起來是「省時間」,但會讓程式碼生成等長輸出任務的測試結果完全失效。正確做法是設 num_predict=-1(無限制),然後觀察模型自然停止的位置。如果真的需要截斷,至少設到 1000 以上再做程式碼類測試。

    坑 3:qwen3.5:9b 的 timeout 問題

    qwen3.5:9b 在 Q6(長文生成)和 Q7(技術解釋)上,Ollama 預設的請求 timeout 不夠,導致連線中斷而不是模型輸出完成。解法:在呼叫 API 時設定 timeout 參數為 1800 秒,或在 Ollama 的環境變數中調整 OLLAMA_TIMEOUT。

    坑 4:KV Cache OOM 發生在 num_ctx 設太高時

    在 16GB 機器上跑 qwen3:14b,如果 num_ctx 設到 8192 以上,KV Cache 的記憶體需求會超過可用 RAM,導致 OOM 或系統卡死。建議 16GB RAM 跑 14B 模型時,num_ctx 不超過 4096;跑 4B 模型時,num_ctx 可以到 8192。

    坑 5:think:false 是必要的,否則輸出會混入思考過程

    qwen3 系列模型如果不設定 think:false,輸出會包含 <think> 標籤包裹的推理過程,混在正式答案裡,對程式解析造成困擾。在 Ollama API 呼叫時加上 "options": {"think": false},或使用 /set parameter think false。只有在你明確需要 Thinking 輸出時才開啟。

    坑 6:num_predict 是物理截斷,不是智慧壓縮

    很多人以為設 num_predict=600 會讓模型「給出精簡版本」,實際上不是。模型不知道 num_predict 這個參數的存在——它只是一個一個往下生成 token,到達上限時被外力硬切斷。結果是:程式碼寫到一半沒有結尾大括號、SQL 少了 WHERE 條件、解釋說到一半消失。

    這次測試的 Q2(日期解析函數)和 Q4(SQL 排名查詢)在 num_predict=300 時全數截斷就是這個原因。移除限制(num_predict=-1)之後,模型自然停止,輸出完整。

    坑 7:有些內容本質上無法壓縮到指定長度內

    假設你問模型「給我一份完整 Kafka 設定檔,限制 50 個 token 內」——這兩個要求本身就互相矛盾。一份能正常運作的 Kafka 設定檔,光是必要欄位就需要遠超 50 token。模型沒有辦法把磚頭塞進比磚頭小的洞。

    面對不可壓縮的內容,有三種做法:(1)直接移除 token 上限;(2)分步請求——先要最精簡模板,確認結構後再要完整版;(3)在 prompt 明確說「輸出必須完整可執行,不要省略任何欄位」,但前提是 token 上限本身要夠大。

    補充:不同模型怎麼面對衝突指令?

    這裡有個值得理解的差異:本地模型(Ollama + llama.cpp)的 token 限制來自 API 參數,是硬體截斷,模型本身完全不知道有這個限制存在。雲端模型(Claude、GPT-4 等)的「限制」則來自 prompt 文字指令,模型讀到這個指令後會嘗試推理你的真實意圖。

    情境 本地模型(Ollama) 雲端模型(Claude)
    限制來源 num_predict API 參數 prompt 文字指令
    遇到「50 token 內給完整設定檔」 不知道有衝突,第 50 個 token 硬截 判斷兩個要求互相矛盾,主動說明,給完整版
    答案冗長可縮短的情況 按字數截斷,不壓縮 推理目標,給出精簡版本
    答案本質不可壓縮 截斷,輸出殘缺內容 告知無法在限制內完整輸出,給出建議
    GPT / Gemini 回答為什麼那麼短? 不是 token 限制,是 System Prompt + RLHF 訓練偏好所致

    這個差異的實際意義是:在本地 LLM 環境下,token limit 是你唯一能控制輸出長度的工具,設太小就會截斷。雲端模型則更像是在和一個理解你意圖的人對話——你不需要精準計算 token,只需要把你真正想要的說清楚。

    進階應用:讓本地 LLM 記住對話上下文

    本地 LLM 的 API 呼叫預設是無狀態的——每次送出的是獨立的單輪問答,模型不記得你上一題問了什麼。如果你想做多輪對話助理、程式碼審查工具、或任何需要「記住脈絡」的應用,就需要自己管理對話歷史。

    為什麼不能無限加長 messages?

    標準做法是把整段對話歷史塞進 messages 陣列一起送出。但對話越長,messages 陣列越大,最終超過 num_ctx 上限,前面的對話就會被硬截斷——模型在不知情的狀況下「失憶」,不會告訴你它看不到前面的內容。

    解法不是把 num_ctx 設更大(那只是延後問題),而是主動管理 messages 陣列:用摘要壓縮舊對話,只保留近期原文加上一段精簡的歷史摘要。

    三種管理策略比較

    方式 num_ctx 用量 記憶效果 適合場景
    無管理(全部塞) 持續增長,最終截斷 前期對話被硬切,模型不自知 短對話、單次任務
    Sliding Window(只保留近 N 輪) 固定 早期資訊完全消失 客服機器人、無需長記憶的助理
    摘要壓縮(推薦) 固定,摘要極短 保留關鍵結論、數字、決策 開發助理、長程任務、知識型問答

    摘要壓縮的運作方式

    核心思路是:用同一個本地模型來摘要自己的舊對話。超過門檻後,把早期輪次壓縮成一段文字,之後每次送出時帶著「摘要 + 近期原文」,而不是全部歷史。

    第 1-8 輪:原文保存在 messages[]
    
    第 9 輪觸發壓縮:
      old[1-5輪] → summarize() → "重點:xxx, yyy, zzz"
      messages 只留 [6-8輪原文] + 新問題
    
    第 9 輪實際送出的內容:
      system: "對話背景摘要:重點 xxx, yyy, zzz"
      user(6): ...  ai(6): ...
      user(7): ...  ai(7): ...
      user(8): ...  ai(8): ...
      user(9): 現在的問題

    完整 Python 實作(本地 Ollama 版)

    import json, urllib.request
    
    MAC_URL = "http://192.168.1.xxx:11434/api/chat"  # Mac 的 Ollama,Mini PC 不跑本地模型
    MODEL  = "gemma4:e4b"
    
    def call_model(messages, think=False):
        payload = {
            "model": MODEL,
            "messages": messages,
            "stream": False,
            "think": think,
            "options": {"num_ctx": 4096, "num_predict": -1}
        }
        data = json.dumps(payload).encode()
        req = urllib.request.Request(
            MAC_URL, data=data,
            headers={"Content-Type": "application/json"}
        )
        with urllib.request.urlopen(req, timeout=300) as r:
            return json.loads(r.read())["message"]["content"]
    
    def summarize(messages):
        """把舊對話丟給模型,壓縮成條列式重點"""
        history_text = "\n".join(
            f"{'User' if m['role'] == 'user' else 'AI'}: {m['content']}"
            for m in messages
        )
        prompt = f"""以下是一段對話記錄,請用條列式摘要最重要的資訊、結論、已確認的事實。
    保留具體數字、決策、技術細節。100字以內。
    
    對話:
    {history_text}
    
    摘要:"""
        return call_model([{"role": "user", "content": prompt}])
    
    class ChatSession:
        def __init__(self, keep_recent=4, compress_threshold=8):
            self.messages = []
            self.summary = ""               # 累積摘要
            self.keep_recent = keep_recent             # 保留最近幾輪原文
            self.compress_threshold = compress_threshold   # 超過幾輪就壓縮
    
        def chat(self, user_input):
            self.messages.append({"role": "user", "content": user_input})
    
            # 超過門檻 → 壓縮舊對話
            if len(self.messages) > self.compress_threshold:
                old = self.messages[:-self.keep_recent]
                new_summary = summarize(old)
    
                # 把舊摘要 + 新摘要合併
                self.summary = f"{self.summary}\n{new_summary}".strip()
                self.messages = self.messages[-self.keep_recent:]
                print(f"[已壓縮,摘要更新:{len(self.summary)} chars]")
    
            # 組合本次送出的 messages
            send_messages = []
            if self.summary:
                send_messages.append({
                    "role": "system",
                    "content": f"對話背景摘要(已發生的重點):\n{self.summary}"
                })
            send_messages.extend(self.messages)
    
            response = call_model(send_messages)
            self.messages.append({"role": "assistant", "content": response})
            return response
    
    # 使用方式
    if __name__ == "__main__":
        session = ChatSession(keep_recent=4, compress_threshold=8)
        while True:
            user = input("你:").strip()
            if user.lower() == "exit":
                break
            reply = session.chat(user)
            print(f"AI:{reply}")
            if session.summary:
                print(f"[背景摘要:{len(session.summary)} chars]")

    效能調優:摘要用小模型,回答用大模型

    摘要這個步驟本身也消耗一次推理呼叫。如果 Mac 上同時有快慢兩個模型,可以分工:快的模型做摘要,慢的(品質更好的)做正式回答:

    MAIN_MODEL    = "qwen3:14b"   # 回答主要問題,品質優先
    SUMMARY_MODEL = "qwen3:4b"   # 做摘要,速度優先(簡單任務夠用)
    
    def summarize(messages):
        # 使用小模型做摘要
        payload = {
            "model": SUMMARY_MODEL,
            ...
        }

    這樣摘要時間從數十秒縮短到幾秒,而主要對話品質不受影響。MacBook Air M3 速度夠快(9+ tok/s),用同一個模型做摘要也無妨。

    日常應用:把 Mac 當成你的私人 AI 主機

    gemma4:e4b 在 MacBook Air M3 上跑出 9+ tok/s,這個速度對互動式日常使用完全夠用——不是只能跑 benchmark,而是可以當成隨時待命的個人助理。重點是:所有資料留在本機,不送雲端,不計費。

    架構很簡單:Mini PC 負責發問和顯示,Mac 負責思考和回答。Mini PC 本身不跑模型,只是一個入口。

    你(Mini PC)→ 問題 → Mac gemma4 → 回答 → 你
    
    Mini PC:入口,不思考
    Mac:腦子,負責推理

    兩個最常用的場景

    場景一:快速摘要

    把一篇文章、一份 log、一段程式碼丟給 Mac,要它用幾句話說重點。不需要 Claude 等級的推理,gemma4 速度更快、更省錢(免費)。

    #!/usr/bin/env python3
    # summarize.py — 從 stdin 讀內容,打到 Mac gemma4 要摘要
    # 用法:cat article.txt | python3 summarize.py
    #       cat error.log   | python3 summarize.py --prompt "這份 log 的錯誤原因是什麼"
    
    import json, sys, urllib.request, argparse
    
    MAC_URL = "http://192.168.1.xxx:11434/api/chat"
    MODEL   = "gemma4:e4b"
    
    parser = argparse.ArgumentParser()
    parser.add_argument("--prompt", default="請用5句話以內摘要以下內容的重點:")
    args = parser.parse_args()
    
    content = sys.stdin.read().strip()
    if not content:
        print("ERROR: no input"); sys.exit(1)
    
    payload = {
        "model": MODEL,
        "messages": [{"role": "user", "content": f"{args.prompt}\n\n{content}"}],
        "stream": False, "think": False,
        "options": {"num_ctx": 8192, "num_predict": -1},
    }
    req = urllib.request.Request(
        MAC_URL, data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=300) as r:
        print(json.loads(r.read())["message"]["content"])
    # 使用範例
    cat ~/Downloads/article.txt   | python3 summarize.py
    cat /var/log/app.log          | python3 summarize.py --prompt "這份 log 有什麼異常?"
    git diff HEAD~5               | python3 summarize.py --prompt "這幾個 commit 改了什麼?"

    場景二:快速產程式驗證

    想驗證一個想法、寫一段臨時腳本、或確認某個 API 用法——不需要開 IDE,直接從命令列問 Mac,幾秒鐘拿到可以跑的程式碼片段。

    #!/usr/bin/env python3
    # ask.py — 命令列直接問 Mac,拿回程式碼或答案
    # 用法:python3 ask.py "寫一個 Python 函數,把 list 裡的重複元素移除但保留順序"
    #       python3 ask.py "用 curl 怎麼測試一個需要 Bearer token 的 API"
    
    import json, sys, urllib.request
    
    MAC_URL = "http://192.168.1.xxx:11434/api/chat"
    MODEL   = "gemma4:e4b"
    
    question = " ".join(sys.argv[1:])
    if not question:
        print("Usage: python3 ask.py \"your question\""); sys.exit(1)
    
    payload = {
        "model": MODEL,
        "messages": [{"role": "user", "content": question}],
        "stream": False, "think": False,
        "options": {"num_ctx": 4096, "num_predict": -1},
    }
    req = urllib.request.Request(
        MAC_URL, data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=300) as r:
        print(json.loads(r.read())["message"]["content"])
    # 使用範例
    python3 ask.py "寫一個 bash script,每天早上備份 ~/Documents 到外接硬碟"
    python3 ask.py "Python requests 怎麼設定 retry 和 timeout"
    python3 ask.py "這個 SQL 有什麼問題:SELECT * FROM orders WHERE date > NOW() - 30"

    什麼時候還是要 Claude

    任務 Mac gemma4 Claude API
    文章/log 摘要 ✅ 夠用,免費
    快速程式片段 ✅ 夠用,快
    翻譯、改寫 ✅ 夠用
    私人資料(不想送雲端) ✅ 最佳選擇
    程式碼審查(跨檔案) ❌ 沒有 context
    複雜架構決策 ❌ 推理不足
    Agent Team 自動化開發 ⚡ 出草稿 ✅ 審查整合

    原則是:不需要記憶 codebase、不需要複雜推理的任務,都可以先試 Mac。速度快、免費、資料不出門。遇到 Mac 答不好的,再升到 Claude。

    進階應用二:Mini PC + Mac 混合架構,讓 Agent Team 更有效率

    角色定義(固定,不隨任務改變)
    
    Mini PC  → 純指揮中心:跑 Claude Code、管理 Agent Team、處理串接邏輯
               不跑任何本地模型,資源用在穩定性和協調上
    
    Mac      → 推理後端:跑 Ollama + gemma4:e4b
               只負責生成,不做決策
    
    Claude API → 審查 + 架構:程式碼審查、複雜邏輯、跨檔案推理
                 Mini PC 透過網路呼叫,不在本地
    
    規則:Mac 不在線 → fallback 給 Claude API,不是 Mini PC 自己跑

    當你用 Claude Code 的 Agent Team 跑自動化程式開發時,會面對一個現實問題:Claude API 的費用隨 token 用量線性增長,而很多任務其實不需要 Claude 的完整推理能力——DTO 生成、CRUD 樣板、SQL migration 這類結構性重複工作,本地的 gemma4:e4b 就能處理。

    解法是把 Mac 當成 Agent Team 的「草稿後端」:Claude Agent 負責架構決策和程式碼審查,Mac gemma4 負責產生第一版草稿,再由 Claude 驗證整合。

    架構分工

    任務類型 交給誰 原因
    DTO / model class Mac gemma4 結構固定,重複性高
    CRUD endpoints 樣板 Mac gemma4 Pattern 固定,不需要推理
    SQL migration Mac gemma4 有範本可循
    Unit test 骨架 Mac gemma4 快速產出結構,Claude 填邏輯
    複雜業務邏輯 Claude sonnet 需要跨檔案理解,Mac 沒有 context
    安全相關程式碼 Claude sonnet/opus 不可靠的輸出風險太高
    架構決策 / Code Review Claude opus 需要深度推理與判斷

    前置設定:讓 Mac 的 Ollama 對區網開放

    Ollama 預設只監聽本機。在 Mac 上把它開放給區網,Mini PC 才能連進來:

    # Mac 上執行(停掉 Ollama app 後)
    OLLAMA_HOST=0.0.0.0 ollama serve
    
    # 從 Mini PC 驗證是否連得到(換成 Mac 的區網 IP)
    curl http://192.168.1.xxx:11434/api/tags

    不想暴露 port 的話,用 SSH Tunnel:Mini PC 上執行 ssh -L 11435:localhost:11434 [email protected] -N,之後打 localhost:11435 就等於打 Mac 的 Ollama。

    工具腳本:mac_draft.py

    Agent 透過 Bash tool 呼叫這支腳本,傳入任務描述,拿回草稿程式碼。腳本會自動檢查 Mac 是否在線,不在線就回傳 exit code 1,讓 Agent 自行處理 fallback。

    #!/usr/bin/env python3
    """
    mac_draft.py — Call Mac's local gemma4 for code draft generation.
    
    Usage:
        python3 mac_draft.py "write a SQLAlchemy User model with id, name, email"
        python3 mac_draft.py --task "CRUD for User" --context "FastAPI, SQLAlchemy async"
    
    Exit codes:
        0 = success, draft printed to stdout
        1 = Mac unreachable → fallback: implement with Claude directly
        2 = model error
    """
    import json, sys, urllib.request, urllib.error, argparse
    
    MAC_HOST = "http://192.168.1.xxx:11434"   # 改成 Mac 的實際 IP
    MODEL    = "gemma4:e4b"
    TIMEOUT  = 600
    
    SYSTEM_PROMPT = """You are a code generation assistant. Output ONLY code —
    no explanations, no markdown fences, no comments unless essential.
    The output will be reviewed and integrated by another agent."""
    
    def check_reachable():
        try:
            with urllib.request.urlopen(f"{MAC_HOST}/api/tags", timeout=5):
                return True
        except Exception:
            return False
    
    def generate(task, context=""):
        prompt = f"Context: {context}\n\nTask: {task}" if context else task
        payload = {
            "model": MODEL,
            "messages": [
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user",   "content": prompt},
            ],
            "stream": False, "think": False,
            "options": {"num_ctx": 4096, "num_predict": -1},
        }
        data = json.dumps(payload).encode()
        req  = urllib.request.Request(
            f"{MAC_HOST}/api/chat", data=data,
            headers={"Content-Type": "application/json"},
        )
        with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
            return json.loads(r.read())["message"]["content"]
    
    parser = argparse.ArgumentParser()
    parser.add_argument("task", nargs="?")
    parser.add_argument("--task", dest="task_flag")
    parser.add_argument("--context", default="")
    args = parser.parse_args()
    
    task = args.task or args.task_flag
    if not task:
        print("ERROR: no task provided", file=sys.stderr); sys.exit(1)
    
    if not check_reachable():
        print(f"MAC_UNREACHABLE: {MAC_HOST}. Fallback: implement with Claude.",
              file=sys.stderr); sys.exit(1)
    
    try:
        print(generate(task, args.context))
    except Exception as e:
        print(f"ERROR: {e}", file=sys.stderr); sys.exit(2)

    Agent 的實際使用流程

    # Agent (sonnet) 在 Bash tool 中這樣呼叫:
    
    # 1. 請 Mac 出草稿
    draft=$(python3 ~/llm-benchmark/scripts/mac_draft.py \
      --task "generate SQLAlchemy User model" \
      --context "PostgreSQL, async, Pydantic v2")
    
    # 2. 檢查是否成功
    if [ $? -ne 0 ]; then
      echo "Mac unavailable, implementing directly"
      # Claude 自己寫
    fi
    
    # 3. 草稿給 Claude 審查後整合進 codebase
    echo "$draft"  # Claude 讀到這裡,決定是否採用、修改哪裡

    告訴 Agents 這條規則:寫入 AGENTS.md

    Claude Code 的 Agent Team 每個 subagent 啟動時沒有對話歷史。規則要寫進 AGENTS.md,agent 才會在每次任務開始時讀到它。在專案的 AGENTS.md 加上這個區塊:

    ## Mac Draft Resource (Local LLM Offload)
    
    Mac (gemma4:e4b) is available as a fast code draft generator.
    Tool: python3 ~/llm-benchmark/scripts/mac_draft.py
    
    Use Mac draft BEFORE writing code yourself for:
    - DTO / model class boilerplate      ✅
    - CRUD endpoints (standard pattern)  ✅
    - SQL migration scripts              ✅
    - Unit test scaffolding              ✅
    
    Do NOT use Mac draft for:
    - Complex business logic             ❌ (no codebase context)
    - Security-sensitive code            ❌ (unreliable)
    - Cross-file refactoring             ❌ (no context)
    - Architecture decisions             ❌ (use opus)
    
    Workflow:
    1. Call mac_draft.py with task description
    2. exit code 1 (MAC_UNREACHABLE) → implement with Claude directly
    3. Review draft: check patterns, imports, logic, security
    4. Integrate into codebase
    
    Mac generates the shape. Claude ensures it fits.

    這樣每個 subagent 都會知道「遇到樣板類任務先叫 Mac 出草稿」,不需要每次重新交代規則。

    結論與推薦

    本次測試跨越三個維度,每層都有明確的答案:

    Mini PC + Mac 混合架構的定位

    Mini PC 的角色是指揮中心,不是推理引擎。它跑 Claude Code、管理 Agent Team、處理串接邏輯,資源用在穩定性和協調上。推理工作全部交給 Mac 的 gemma4:e4b。

    • 日常問答 / 摘要:Mini PC 發問 → Mac gemma4 回答,免費、快速、資料不出門
    • 草稿程式碼:Agent 呼叫 mac_draft.py → Mac 出草稿 → Claude 審查整合
    • 複雜推理 / 架構決策:直接用 Claude API,不走 Mac
    • Mac 不在線:fallback 給 Claude API,Mini PC 本身不需要跑任何模型

    如果你的情境是 Mini PC 獨立運作(沒有 Mac),模型選擇建議:gemma4:e4b 速度最快(1.45 tok/s)、qwen3:14b 完成度最高(7/7 全答)、qwen3:4b 最省記憶體。但這個架構的速度上限就是 CPU 推論,不如 Mac 的 Apple Silicon。

    MacBook Air M3 的最佳設定

    • 日常程式碼:think:false,num_predict=-1,9.75 tok/s,輸出完整
    • 複雜推理/技術解釋:Thinking 開啟,速度只慢 6%,品質提升明顯
    • 長文件處理:設定 num_ctx=65536,充分利用 24GB 統一記憶體

    本地 LLM 的真實上限在哪裡?

    gemma4:e4b 在 MacBook Air M3 + Thinking 模式下,輸出品質接近 GPT-3.5 的水準——在程式碼生成、SQL 查詢、技術解釋這些結構清晰的任務上。它不是 GPT-4,創意寫作和跨領域推理還是有差距,但對開發者的日常工作場景已經夠用。

    真正的瓶頸不是模型大小,而是硬體架構。同款模型在 Apple Silicon 上跑出的效果,在 x86 CPU 上根本發揮不出來。如果你認真考慮本地 LLM,MacBook Air M3 是目前性價比最高的入門選擇;Mini PC 路線則需要搭配 NVIDIA GPU(VRAM ≥ 8GB)才能真正發揮。

  • 叫了 20 個 AI 專家 Review,最致命的 Bug 卻是「沒讀上次的筆記」

    重點摘要

    • 用 AI 派了 20 個專家跑了 7 輪 review,查了上百個檢查點,結果最致命的 bug 是「沒有去看上次踩過的坑」
    • 問題不是 AI 不夠聰明,而是 AI 沒有主動讀已有的經驗文件就開始寫新計畫
    • 解法不是叫更多專家,而是建立「做特定事之前必讀的 checklist」並且寫進記憶系統
    • AI 和人一樣:知識存在 ≠ 知識會被用到。差距在於流程,不在於能力

    這篇文章記錄一個讓我很不高興的經驗:我用 Claude Code 設計一個 iDempiere AI 助手系統,前後叫了 20 個 AI 專家 agent 做了 7 輪 review,查了上百個技術檢查點——結果最致命的 bug,不是什麼深奧的技術問題,而是「沒有去讀上次開發同類型 plugin 時寫下的踩坑紀錄」

    這件事讓我思考一個更根本的問題:我到底該怎麼跟 AI 協作,才能讓它真正用到已有的經驗?

    發生了什麼事?

    我在開發一個 iDempiere ERP 的 AI 問答助手。這個系統分成兩部分:Java 的 iDempiere Plugin(前端 UI + 權限 + 審計日誌)和 Python 的 FastAPI 服務(AI 路由 + PII 脫敏 + LLM 呼叫)。

    在寫 Plugin 的計畫之前,我已經有一個完整的 iDempiere plugin 開發經驗——台灣統一發票系統 tw-invoice。那個專案踩了超過 24 個坑,每一個都花了我好幾個小時 debug,而且全部記錄在 CLAUDE.md 裡

    但是當 Claude 開始寫 AI 助手的 Plugin 計畫時,它完全沒有去讀那份文件。它是從「一般 iDempiere 知識」出發寫的,而不是從「我們一起踩過的坑」出發。

    7 輪 review 查了什麼?漏了什麼?

    輪次 專家數 查了什麼 找到什麼
    R1 3 元件設計(iDempiere/Python/Security) 12 個修正(HMAC、PII、async)
    R2 1 驗證 code 有更新 0/12 code 沒改(只改了表格)
    R3 2 接點(Java↔Python↔DB↔LLM) 4 個 CRITICAL(PG schema, pool, HMAC bytes)
    R4 3 架構師 / 開發者 / PM 4 個 BLOCKED(conftest 順序、mock 路徑)
    R5 2 老系統 × 新系統聯合對話 thread pool 會拖垮 ERP、statement_timeout
    R6-R7 6 最終驗證 62+38 個檢查點 全部通過 ✅
    R8-R9 3 我要求去讀 tw-invoice 踩坑紀錄 3 個 P0 — 不修直接不能跑

    你看到問題了嗎?前 7 輪 review 用了 20 個專家 agent,查了上百個檢查點,全部通過。但只有在我「要求 Claude 去讀舊專案的踩坑紀錄」之後,才發現 3 個會直接讓 plugin 無法啟動的致命 bug。

    那 3 個致命 bug 是什麼?

    Bug 後果 tw-invoice 有記錄嗎?
    MANIFEST.MF 缺 org.adempiere.plugin.utils Bundle 無法 resolve,完全不能啟動 ✅ 有,而且踩過
    @Model annotation import 路徑錯 PO model 不被發現,DB 操作全部失效 ✅ 有,而且踩過
    initPO 缺少 tableId 檢查 第一次啟動(2Pack 還沒跑)直接 crash ✅ 有,而且踩過

    三個 bug 都是 tw-invoice 踩過且記錄過的。經驗就躺在那裡,但沒有被讀取。

    問題出在哪?不是 AI 不聰明

    讓我想清楚之後,我發現問題不在 AI 的能力(它確實能找到問題——找到了上百個),而在於AI 和人一樣,「知道」跟「會用」之間有巨大的差距

    三層問題

    1. AI 不知道要去看 — 寫新 plugin 計畫時,它沒有主動去讀 tw-invoice 的 CLAUDE.md。它有能力讀,但沒有觸發「我應該先去看看上次踩了什麼坑」的念頭。
    2. 我也不知道要提醒它 — 我以為「派 20 個專家 review」已經夠全面了。我不知道這些專家不會自動去讀歷史紀錄,除非我明確要求。
    3. 專家 review 的盲點 — 專家只看「這份文件本身有沒有問題」,不會跨專案比對「上次做類似的事踩了什麼坑」。他們審的是邏輯一致性,不是經驗傳承。

    解法:不是更多專家,而是「做事之前的 checklist」

    派再多專家也沒用,如果他們不知道要看哪些歷史紀錄。真正的解法是在開始工作之前,就讓 AI 讀取相關的經驗教訓。

    我最終建立了兩份「跨專案強制 checklist」,存在 Claude 的記憶系統裡:

    記憶檔案 觸發條件 內容
    idempiere-plugin-pitfalls.md 寫任何 iDempiere plugin 之前 MANIFEST.MF 必要 package、2Pack 路徑、@Model import、initPO guard、afterPackIn 模式、部署 SOP
    python-llm-pitfalls.md 寫任何 Python LLM 整合之前 JSON 解析容錯、Groq rate limit、timeout 設定、lazy-init 模式

    這兩份檔案在 MEMORY.md 索引裡標記為 🔴 MANDATORY。每次新對話載入時,AI 會看到這個索引,知道「做 iDempiere plugin 工作之前,先讀 pitfalls 檔案」。

    你該怎麼做?給 AI 使用者的具體建議

    如果你也用 Claude Code(或任何 AI coding assistant)做重複性的專案工作,以下是我的教訓:

    1. 踩坑之後立刻寫進 CLAUDE.md

    不是「之後再整理」,是修完 bug 的那一刻就寫。寫三行就好:什麼坑、為什麼踩到、怎麼修的。這個我有做,tw-invoice 的 CLAUDE.md 記了 24 個坑。問題出在下一步。

    2. 建立跨專案的 pitfalls 記憶(這是我缺的那一步)

    經驗寫在專案 A 的 CLAUDE.md 裡,專案 B 不會自動讀到。你需要把「通用教訓」抽出來,放到 Claude 的記憶系統(~/.claude/projects/memory/),這樣每個新專案都能讀到。

    # MEMORY.md 索引加這一行:
    ## ⚠️ Must-Read Before Specific Work
    - **idempiere-plugin-pitfalls.md** 🔴 MANDATORY — 寫任何 plugin 前必讀

    3. Review 之前,先問「你有沒有讀過上次的紀錄?」

    不要假設 AI 會自動做這件事。它不會。你需要明確說:「先去看 tw-invoice 的 CLAUDE.md 取經,然後再來審這份計畫。」

    4. 專家 review ≠ 經驗傳承

    20 個專家能找到「這份文件本身有沒有邏輯錯誤」,但找不到「上次做類似的事踩了什麼坑」。這兩件事是不同的能力,需要不同的觸發方式。

    • 專家 review:「這份計畫有沒有 bug?」→ 邏輯驗證
    • 經驗傳承:「上次做類似的事踩了什麼坑?」→ 歷史比對

    你需要兩者都做,而且經驗傳承要在 review 之前。否則 review 再怎麼嚴謹,也只是在一個有缺陷的基礎上做驗證。

    AI 協作的本質:知識存在 ≠ 知識被用到

    這次經驗讓我想通一件事:AI 的問題跟人的問題是一樣的

    你的資深工程師也會犯同樣的錯——他上次在專案 A 踩了 10 個坑,寫了筆記,但做專案 B 的時候忘了翻筆記,同樣的坑又踩一次。差別在於人有「直覺」(模糊地記得「好像上次有遇過類似的」),AI 沒有這種模糊記憶。AI 要嘛讀了文件就完美執行,要嘛沒讀文件就完全不知道。

    所以跟 AI 協作的核心不是「讓 AI 更聰明」,而是讓正確的資訊在正確的時間出現在 AI 面前。這是一個資訊流設計問題,不是 AI 能力問題。

    我的 AI 協作框架(修正版)

    開始新專案
        ↓
    1. 讀 MEMORY.md 索引(AI 自動做)
        ↓
    2. 有沒有「Must-Read」標記的 pitfalls 檔案?
       → 有:讀完再動手(AI 必須被觸發)
       → 沒有:判斷是否需要建立一個
        ↓
    3. 讀舊專案的 CLAUDE.md(我要明確要求)
        ↓
    4. 寫計畫(現在才開始)
        ↓
    5. Review(專家驗證邏輯 + 歷史比對)
        ↓
    6. 踩到新坑 → 立刻寫進 CLAUDE.md + 更新 pitfalls 記憶

    關鍵改變:步驟 2 和 3 是我之前跳過的。我以為步驟 5 的 review 會涵蓋一切,但 review 只能驗證邏輯,不能傳承經驗。

    更深的問題:專案爆炸之後,你連「要叫 AI 去看哪裡」都不知道

    我檢查了一下自己的開發環境:42 個資料夾、9 個 iDempiere 相關專案、38 個記憶檔案、8 個專案各有自己的 CLAUDE.md(共 1578 行經驗紀錄)。

    這代表什麼?我已經快到「我自己都不知道我有什麼」的臨界點了。

    這次我還記得「tw-invoice 有踩坑紀錄」所以能叫 AI 去讀。但再過半年呢?再多 10 個專案呢?到時候我連「有一份紀錄存在」都不記得,更不可能叫 AI 去參考。而 AI 自己不會主動翻遍 42 個資料夾找相關經驗。

    這就是Sample → 大系統模式的致命陷阱:

    正常的軟體開發流程:
      做 Sample → 驗證可行 → 嫁接到大系統
    
    加入 AI 協作之後:
      做 Sample → AI 幫你踩坑 → 經驗寫在 Sample 的 CLAUDE.md
      → 做大系統 → AI「不知道」Sample 的經驗存在
      → 同樣的坑再踩一次
      → 你修完寫進大系統的 CLAUDE.md
      → 下一個專案又不知道...
    
    無限循環。

    跟資深用戶合作的隱藏風險

    還有一個我不想承認但必須說的事:跟資深用戶合作,AI 反而更容易犯錯。

    因為你太懂技術,我傾向「快速產出」而不是「慢慢確認」。你一聽就懂的東西,我就跳過解釋直接做。結果跳過的步驟裡,就藏著「你以為我知道、我以為你知道、但其實沒人確認」的盲區。

    如果你是新手,我反而會更謹慎——每一步確認、每個假設驗證。但跟資深用戶合作,雙方都太有信心,踩煞車的人就消失了。

    知識分層:什麼該鎖、什麼該開

    還有一個企業層面的問題:當經驗從「人的腦子」搬到「.md 檔案」,它變得可複製了。新人 clone repo 就能拿到所有踩坑紀錄。這對知識傳承是好事,但對機密控管是風險。

    解法是分層:

    層級 內容 存在哪 被帶走的風險
    公開技術層 CLAUDE.md、架構規則、coding style Git repo 低(跟 source code 等價)
    團隊經驗層 踩坑紀錄、設計文件、SOP Git repo 中(加速競爭對手,但不是核心機密)
    個人記憶層 跨專案 pitfalls、用戶偏好 ~/.claude/(本機) 低(不在 repo 裡,但可手動複製)
    營運機密層 API key、商業邏輯、客戶資料 .env / 公司內部系統 高(必須嚴格管控)

    但現實是:目前沒有任何 AI 開發工具提供這種分層管理。 Claude Code 的記憶系統是平的——所有 .md 檔案放在同一個目錄,沒有權限控制、沒有加密、沒有存取日誌。這是整個產業還沒解決的問題。

    真正需要的:「做特定事之前必須讀什麼」的自動化

    我現在的解法是「手動建立 pitfalls 記憶 + 在 MEMORY.md 標記 MANDATORY」。但這依賴兩件事:

    1. 我記得去標記 — 如果我忘了把新的 pitfalls 抽出來建立跨專案記憶,下次還是會踩坑
    2. AI 會去讀標記 — 目前是靠 MEMORY.md 索引,但沒有強制機制。AI 「應該」讀,但「應該」跟「一定會」之間有差距

    理想的解法是什麼?類似 Git hooks 的機制:

    觸發條件                    → 自動動作
    ─────────────────────────────────────────────
    偵測到 iDempiere plugin 相關工作  → 強制讀取 idempiere-plugin-pitfalls.md
    偵測到 Python LLM 整合          → 強制讀取 python-llm-pitfalls.md
    偵測到新專案建立                → 掃描所有已有專案的 CLAUDE.md,提取相關經驗
    偵測到跟舊專案同類型的工作       → 自動列出「相關專案清單」讓用戶確認

    這個機制目前不存在。Claude Code 有 hooks,但是是 shell command 層級的,不是「語意理解」層級的。它能在 tool call 前後跑 script,但不能理解「這次的工作跟上次的 tw-invoice 是同類型的,應該先去參考」。

    在這個機制出現之前,唯一的防線就是你自己:你必須記得提醒 AI 去讀歷史,而且你必須知道歷史在哪。當你的專案多到你自己都不記得有哪些,這條防線就會失守。

    最終解法:領域腦(Domain Brain)

    經過上面所有的分析,我最終做了一件事:把 42 個專案、1578 行 CLAUDE.md、38 個記憶檔案的經驗,按「技術領域」濃萃成 7 份領域腦

    之前(按專案切,散落各處):
      tw-invoice/CLAUDE.md     → 24 個坑(OSGi + 2Pack + PO + REST 混在一起)
      module-ui/CLAUDE.md      → ZK + REST + 測試(370 行)
      skin-ui/CLAUDE.md        → WAB + Vue + 測試(507 行)
      langgraph-duo/           → Python LLM 整合
      analyst/                 → 爬蟲 + pandas + 回測
      → 你要知道「哪個專案有哪些經驗」才能讓 AI 去讀
    
    之後(按領域切,濃萃在一處):
      brain/idempiere-osgi-bundle.md   ← 所有 OSGi 的坑(來自 4 個專案)
      brain/idempiere-2pack.md         ← 所有 2Pack 的坑
      brain/idempiere-po-model.md      ← 所有 PO Model 的坑
      brain/idempiere-rest-api.md      ← 所有 REST API 的坑
      brain/python-llm-integration.md  ← 所有 LLM 整合的坑
      brain/python-crawler-data.md     ← 所有爬蟲/資料的坑
      brain/design-principles.md       ← 跨語言設計原則
      → 你只需要說「我要做 plugin」,AI 就讀 OSGi + 2Pack + PO 三份腦

    為什麼這比「叫 AI 去看舊專案」好?

    面向 看舊專案 CLAUDE.md 領域腦
    你需要記得什麼 哪個專案跟現在的相關 只需要知道「我在做什麼類型的事」
    新專案踩了新坑 寫進該專案的 CLAUDE.md 萃取到對應的領域腦(所有未來專案受益)
    專案 B 比專案 A 先做完 A 不知道 B 的經驗 B 的經驗已在領域腦,A 自動受益
    專案數量爆炸 越多越容易漏 領域腦數量固定(技術領域有限)
    AI 專家 review 每次從零開始 站在所有歷史經驗之上審查

    最關鍵的一行:領域腦的數量不會隨專案數量增長。你可以有 100 個專案,但「iDempiere OSGi」的領域腦就是一份。新經驗加進去,舊經驗不會消失。專案可以刪掉,經驗永遠留著。

    閉環:專家 review 終於有意義了

    之前(斷裂的):
      專家 review → 找到問題 → 修進當前專案 → 下個專案又不知道
                                                  ↑ 斷在這裡
    
    之後(閉環的):
      專家 review(讀領域腦 + 當前文件)
           ↓
      找到問題 → 修進當前專案
               → 同時萃取到對應的領域腦
           ↓
      下個專案開始前讀領域腦
           ↓
      專家 review(讀更新過的領域腦 + 當前文件)
           ↓
      ↻ 經驗循環,不斷累積

    這才是 AI 專家 review 真正有意義的前提:他們站在所有歷史經驗之上做審查,而不是每次都從零開始

    現實的限制

    領域腦不是完美解法。它依然有幾個問題:

    • 萃取是手動的 — 目前沒有工具能自動從 CLAUDE.md 提取教訓並分類到領域腦。我是派 AI agent 讀完所有檔案後人工整理的。
    • 維護需要紀律 — 踩了新坑要記得更新領域腦,不只是寫進專案的 CLAUDE.md。如果忘了這一步,循環又斷了。
    • 領域邊界不總是清楚 — 一個 bug 可能同時涉及 OSGi、2Pack、和 PO Model。要判斷放哪個腦,或者放多份。
    • Token 成本 — 領域腦加起來約 2000-3000 tokens。每次新對話讀取相關的 2-3 份,約 $0.005-$0.015。每月 $5-15,可以接受。

    但即使有這些限制,領域腦依然比「靠人記得哪個專案有哪些經驗」好太多了。因為人的記憶會隨專案數量退化,領域腦不會

    完整解法:Domain Brain 宣告 + fix: 驅動更新

    經過不斷推敲,最終方案有三個核心機制:

    機制一:每個專案用一行宣告自己需要哪些腦

    # 每個專案的 CLAUDE.md 開頭加一行:
    
    idempiere-tw-ai-assistant/CLAUDE.md:
      ## Domain Brain: osgi-bundle, 2pack, po-model, python-llm-integration
    
    analyst/CLAUDE.md:
      ## Domain Brain: python-crawler-data, design-principles
    
    新專案/CLAUDE.md:
      ## Domain Brain: python-crawler-data, design-principles

    為什麼不用資料夾名稱比對?因為 analyst/ 裡有爬蟲也有 API 也有 SQLAlchemy——資料夾名稱不等於技術領域。為什麼不用關鍵字比對?因為「API」出現在 iDempiere REST、Groq LLM、爬蟲、自己的 FastAPI 四種 context 裡,關鍵字比對直接崩潰。

    讓專案自己宣告是最可靠的——你看得到、可以改、一行字。AI 讀到 CLAUDE.md 就知道要載入哪些腦,不用猜。

    機制二:fix: commit 驅動更新

    新經驗怎麼回到領域腦?不是靠事後整理,而是靠 fix: commit 當觸發點:

    每次 AI 寫出 fix: 開頭的 commit message:
      1. STOP — 不要急著做下一個 task
      2. 問自己:「這個 fix 會不會在其他專案也發生?」
      3. 是 → 當場更新對應的領域腦(不是之後,是現在)
      4. 領域腦更新後,所有未來專案的 review 都能受益

    機制三:專家 review 帶著腦

    派專家 review 時:
      1. 專家讀該專案的 CLAUDE.md → 看到 Domain Brain 宣告
      2. 專家讀對應的腦 → 帶著所有歷史 bug 經驗
      3. 審查當前文件 → 站在經驗之上,不是從零開始
      4. 找到新問題 → 修完 → 更新腦
      5. 下一個專案的專家 → 拿到更新過的腦
      ↻ 循環

    三個機制合在一起的效果

    你要做的事 AI 自動做的事
    新專案加一行 Domain Brain(6 個字) 讀對應的腦、帶著經驗開始工作
    不需要做任何事 fix: commit 時自動判斷是否更新腦
    說「派專家審查」 專家帶著最新的腦去審查
    偶爾說「把這個更新到腦」 當場更新,所有未來專案受益

    還是不完美的地方

    • 你自己 debug 沒跟 AI 說的坑 — AI 不知道,無法更新腦。你得養成習慣說一句「把這個更新到腦」
    • 其他同事的經驗 — 除非他們也更新領域腦,否則知識在他們腦子裡消失
    • 新領域出現 — 7 份腦不夠用了(比如加了 DevOps 或 mobile)→ 建新的腦檔案
    • AI 判斷 fix: 是否該更新腦 — 還是靠判斷,可能漏。但比「完全沒有機制」好太多

    結語

    叫再多專家進來 review,如果他們不知道要看歷史紀錄,就跟你請了 20 個新員工來審查、但不給他們看前任的交接文件一樣。當你的專案多到連自己都記不清有哪些,而 AI 又不會主動翻遍你的 42 個資料夾找經驗——這時候你需要的不是更聰明的 AI,而是一個能自動把經驗送到 AI 面前的系統。領域腦不是完美的系統,但它把「靠人記住 42 個專案的經驗」變成「靠 7 份按領域整理的文件」。專案會越來越多,但技術領域是有限的。這就是為什麼領域腦能 scale,而按專案管理經驗不能。

    跟 AI 協作的真正技巧不是「讓 AI 更聰明」或「叫更多 agent」,而是設計一個流程,讓正確的經驗在正確的時間被讀取。這聽起來很簡單,但直到你踩了同一個坑兩次,你才會真正理解為什麼需要這麼做。

    相關閱讀:iDempiere Plugin 開發完整指南:踩遍台灣統一發票的 10 個坑 | LangGraph 多模型實戰:從零到 Production 的完整教學 | Agent Team 穩定的關鍵:spawn 之前先建好兩份文

  • LangGraph 多模型實戰:從零到 Production 的完整教學

    重點摘要

    • LangGraph 讓你把不同 AI 模型串成自動化流水線:Claude 負責「想」,Groq Llama 負責「寫」,各司其職
    • 本文從零開始,帶你走完四個階段:基本流水線 → 智慧路由 → Streaming + 容錯 → FastAPI 部署
    • 每個階段都有完整可執行的程式碼,跟著做就能跑
    • 核心區別:Claude Code / Cursor 是「你的工具」,LangGraph 是「你造工具的材料」——當你要造產品給別人用時才需要它
    • 實測結果:比全用 Claude Sonnet 省 75% 成本,同時保留深度分析的品質

    這篇文章是給誰看的?

    如果你符合以下任一情況,這篇文章就是為你寫的:

    • 你想做一個 AI chatbot(客服、內部助手、產品功能),但不知道怎麼開始
    • 你已經在用 Claude / GPT,但覺得全部用最貴的模型太浪費錢
    • 你聽過「多模型協作」但不確定具體怎麼實作
    • 你想知道 LangGraph 跟你平常用的 Claude Code / Cursor / Copilot 到底差在哪

    本文從零開始,帶你走完四個階段,每個階段都有完整可執行的程式碼。你可以在任何一個階段停下來——不是每個人都需要走到 production。

    先搞清楚:這跟 Claude Code / Cursor / Copilot 完全不同

    在往下讀之前,先釐清最容易搞混的一件事:

    面向 Claude Code / Cursor / Copilot LangGraph
    本質 開發者工具 — 你用它寫 code 開發框架 — 你用它造產品
    使用者是誰 你自己(開發者) 你的客戶 / 你的系統 / 你的團隊
    使用場景 日常寫程式、debug、重構 建 chatbot、自動化流程、API 服務
    互動方式 人 ↔ AI 即時對話 程式碼自動跑,可以無人值守
    模型選擇 工具幫你選好(通常固定一個) 你自己決定哪個步驟用哪個模型
    計費方式 月費訂閱(工具包了) 純 API 按量計費,你自己控制成本

    用一個比喻:Claude Code 是你請了一個很強的工程師坐在旁邊幫你寫 code;LangGraph 是你在蓋一條自動化產線,產線上有不同的機器人各司其職

    如果你只是日常寫程式,用 Claude Code 就好,不需要 LangGraph。往下讀之前,確認你的需求是「造一個東西給別人用」,而不是「讓自己寫 code 更快」。

    為什麼不同的 AI 模型要分工合作?

    沒有一個模型什麼都最好。每個模型有不同的強項和定價:

    能力 Claude Sonnet Groq Llama 70B Groq Llama 8B
    架構設計 / Code Review ⭐ 最強 普通
    程式碼生成 ⭐ 強且快 普通
    簡單問答 大材小用 大材小用 ⭐ 夠用且極便宜
    生成速度 ~50 tok/s ~300 tok/s ~800 tok/s
    費用 (Output/1M tokens) $15.00 $0.79 $0.08

    核心邏輯:讓擅長「想」的模型去想,讓擅長「做」的模型去做,讓便宜的模型處理瑣事。就像軟體團隊裡,架構師出規格、工程師寫程式、實習生回答簡單問題一樣。

    LangGraph 是什麼?一分鐘看懂

    LangGraph 是 LangChain 團隊開發的有狀態流程編排框架。三個核心概念:

    1. Node(節點)— 一個步驟,比如「用 Sonnet 設計」或「用 Llama 實作」
    2. Edge(邊)— 步驟之間的連線,決定「做完 A 接著做 B」
    3. State(狀態)— 所有節點共享的資料,比如設計規格、程式碼、審查結果

    把這三個組合起來,就是一個可以自動跑的流水線。你定義「什麼步驟做什麼、什麼條件走什麼路」,框架負責執行。

    環境準備(所有階段共用)

    開始之前,你需要準備兩個 API key 和安裝套件。這一步做完,後面四個階段都不用再設定。

    1. 取得 API Key(兩個都有免費額度)

    ⚠️ 注意:這是 API key,跟 Claude.ai 的月費訂閱、GitHub Copilot 的訂閱完全無關。API 是按用量計費的。

    2. 建立專案

    mkdir langgraph-duo && cd langgraph-duo
    
    # 安裝套件
    pip install langgraph langchain-anthropic langchain-groq python-dotenv
    
    # 建立 .env 檔案(填入你的 key)
    cat > .env << 'EOF'
    ANTHROPIC_API_KEY=sk-ant-xxxx
    GROQ_API_KEY=gsk_xxxx
    EOF

    3. 驗證連線

    python3 -c "
    from dotenv import load_dotenv; load_dotenv()
    from langchain_anthropic import ChatAnthropic
    from langchain_groq import ChatGroq
    
    sonnet = ChatAnthropic(model='claude-sonnet-4-6', max_tokens=50)
    llama = ChatGroq(model='llama-3.3-70b-versatile', max_tokens=50)
    
    print('Sonnet:', sonnet.invoke('Say OK').content)
    print('Llama:', llama.invoke('Say OK').content)
    "

    兩行都印出 OK,就可以開始了。

    第一階段:Duo 流水線(設計 → 實作 → 審查)

    目標:讓 Claude Sonnet 設計規格,Groq Llama 寫程式碼,Sonnet 再審查品質。審查不通過自動重做,最多 3 次。

    適合場景:批量生成程式碼、自動化 code review、需要品質把關的程式碼生成。

    Task → [Sonnet 設計] → [Llama 實作] → [Sonnet 審查]
                                ↑               |
                                └── 未通過 ──────┘  (最多 3 次)
                                      ↓ 通過
                                     END

    完整程式碼:duo.py

    from typing import TypedDict
    from dotenv import load_dotenv
    from langgraph.graph import StateGraph, END
    from langchain_anthropic import ChatAnthropic
    from langchain_groq import ChatGroq
    from langchain_core.messages import HumanMessage, SystemMessage
    
    load_dotenv()
    
    # 模型分工:Sonnet 想,Llama 做
    sonnet = ChatAnthropic(model="claude-sonnet-4-6", max_tokens=4096)
    llama = ChatGroq(model="llama-3.3-70b-versatile", max_tokens=4096)
    
    # 所有節點共享的狀態
    class AgentState(TypedDict):
        task: str             # 原始需求
        design: str           # Sonnet 的設計規格
        code: str             # Llama 的實作
        review: str           # Sonnet 的審查結果
        revision_notes: str   # 修改指示(給重試用)
        approved: bool        # 是否通過
        attempt: int          # 重試次數
    
    MAX_ATTEMPTS = 3
    
    # Node 1: Sonnet 設計
    def design_node(state):
        response = sonnet.invoke([
            SystemMessage(content="You are a senior architect. Produce a precise technical spec with function signatures, edge cases, and pseudocode."),
            HumanMessage(content=f"Request: {state['task']}")
        ])
        return {"design": response.content}
    
    # Node 2: Llama 實作
    def implement_node(state):
        prompt = f"Spec:\n{state['design']}"
        if state.get("revision_notes"):
            prompt += f"\n\nFix these issues:\n{state['revision_notes']}"
        response = llama.invoke([
            SystemMessage(content="Implement per spec. Output only Python code."),
            HumanMessage(content=prompt)
        ])
        return {"code": response.content, "attempt": state.get("attempt", 0) + 1}
    
    # Node 3: Sonnet 審查
    def review_node(state):
        response = sonnet.invoke([
            SystemMessage(content="Review code vs spec. Reply VERDICT: APPROVED or REJECTED with details."),
            HumanMessage(content=f"Spec:\n{state['design']}\n\nCode:\n{state['code']}")
        ])
        review = response.content
        approved = "APPROVED" in review.upper()
        return {"review": review, "approved": approved,
                "revision_notes": "" if approved else review}
    
    # 條件路由:通過 → 結束,未通過 → 回去重做
    def should_continue(state):
        if state["approved"] or state["attempt"] >= MAX_ATTEMPTS:
            return "end"
        return "revise"
    
    # 組裝 Graph
    workflow = StateGraph(AgentState)
    workflow.add_node("design", design_node)
    workflow.add_node("implement", implement_node)
    workflow.add_node("review", review_node)
    workflow.set_entry_point("design")
    workflow.add_edge("design", "implement")
    workflow.add_edge("implement", "review")
    workflow.add_conditional_edges("review", should_continue,
                                   {"end": END, "revise": "implement"})
    app = workflow.compile()
    
    # 跑!
    result = app.invoke({
        "task": "Write a Python function that reads a CSV and returns column averages as a dict",
        "design": "", "code": "", "review": "",
        "revision_notes": "", "approved": False, "attempt": 0,
    })
    
    print("=== CODE ===")
    print(result["code"])
    print(f"\n{'✅ APPROVED' if result['approved'] else '⚠️ BEST EFFORT'} after {result['attempt']} attempt(s)")

    實測結果

    階段 模型 結果
    🧠 Design Claude Sonnet 4.6 產出 4 個參數、10 個 edge case、9 步 pseudocode 的完整規格
    ⚡ Implement Groq Llama 70B 完整 Python 函數,含 type hints、docstring、error handling
    🔍 Review Claude Sonnet 4.6 VERDICT: APPROVED — 第一次就通過,沒有重試

    到這裡你已經有一個能跑的多模型流水線了。如果你的需求是「批量生成程式碼 + 自動品質把關」,可以停在這個階段。

    第二階段:Smart Router(自動選模型的聊天機器人)

    目標:做一個像 ChatGPT 一樣的對話介面,但底下不是固定一個模型,而是自動根據問題類型選最適合的模型。

    適合場景:客服 chatbot、團隊內部 AI 助手、需要控制 API 成本的聊天服務。

    你的問題 → [Llama 8B 分類器] → 判斷類型
                                      |
                      ┌────────────────┼────────────────┐
                      ↓                ↓                ↓
                [Claude Sonnet]  [Llama 70B]      [Llama 8B]
                 深度分析          寫程式            簡單問答
                      ↓                ↓                ↓
                      └────────────────┼────────────────┘
                                       ↓
                                    回答你

    分類器怎麼運作?

    分類器用最便宜的 Llama 8B(每次呼叫不到 $0.0001)讀取問題,輸出一個 JSON 判斷結果。分類規則:

    分類 路由到 觸發條件 費用 (Output/1M)
    🧠 深度思考 Claude Sonnet 架構分析、比較權衡、code review、規劃 $15.00
    ⚡ 寫程式 Llama 70B 實作函數、生成腳本、重構、修 bug $0.79
    💨 快速回答 Llama 8B 打招呼、簡單問答、定義、基礎算數 $0.08

    核心程式碼

    import json
    from langgraph.graph import StateGraph, END
    
    # 分類器:Llama 8B 讀問題,判斷該走哪條路
    def router_node(state):
        response = llama_8b.invoke([
            SystemMessage(content="""Classify into one category:
    - "sonnet": complex reasoning, analysis, architecture
    - "llama_70b": write code, implement, fix bugs
    - "llama_8b": greetings, simple facts, casual chat
    Reply ONLY JSON: {"route": "...", "reason": "..."}"""),
            HumanMessage(content=state["question"])
        ])
        parsed = json.loads(response.content)
        return {"route": parsed["route"]}
    
    # 條件路由:根據分類結果,走不同的回答節點
    workflow = StateGraph(RouterState)
    workflow.add_node("router", router_node)
    workflow.add_node("sonnet", answer_sonnet)
    workflow.add_node("llama_70b", answer_llama_70b)
    workflow.add_node("llama_8b", answer_llama_8b)
    
    workflow.set_entry_point("router")
    workflow.add_conditional_edges("router", lambda s: s["route"], {
        "sonnet": "sonnet",
        "llama_70b": "llama_70b",
        "llama_8b": "llama_8b",
    })
    app = workflow.compile()

    實測分類準確度:7/7

    問題 路由結果 正確?
    Hi! 💨 Llama 8B
    1+1=? 💨 Llama 8B
    Write a Python quicksort ⚡ Llama 70B
    Compare microservices vs monolith 🧠 Sonnet
    What is Docker? 💨 Llama 8B
    幫我寫一個 REST API ⚡ Llama 70B
    分析 Redis cache vs CDN 優缺點 🧠 Sonnet

    到這裡你有一個能自動選模型的聊天機器人了。但它還缺兩個東西:回答會一次全部吐出(不是逐字顯示),而且 Groq 掛了就整個壞掉。第三階段解決這兩個問題。

    第三階段:Streaming + Fallback(讓它不會掛)

    目標:回答逐字出現(像 ChatGPT 一樣流暢),而且模型掛了自動切換備用模型。

    為什麼這一步很重要:沒有 Streaming 的 chatbot,使用者體驗像 2010 年的網頁——按下送出,等 5 秒,突然一大段文字出現。沒有 Fallback 的服務,Groq 一限速(免費版每分鐘 30 次),你的整個服務就掛了。

    Streaming:體感延遲降 25 倍

    方式 使用者體驗 程式碼差別
    invoke()(非串流) 等 5 秒 → 突然出現整段文字 response = model.invoke(messages)
    stream()(串流) 0.2 秒開始出字 → 像打字一樣流暢 for chunk in model.stream(messages)
    # 只需要把 .invoke() 改成 .stream()
    # 然後迭代每個 chunk 即時輸出
    
    for chunk in model.stream(messages):
        print(chunk.content, end="", flush=True)  # flush=True 強制即時顯示

    Fallback:模型掛了自動切換

    主要模型 Fallback 模型 切換代價
    🧠 Sonnet ⚡ Llama 70B 分析品質略降,速度更快
    ⚡ Llama 70B 🧠 Sonnet 速度略慢,品質更高
    💨 Llama 8B ⚡ Llama 70B 稍慢稍貴,但一定能回答
    # Fallback 模式:try 主要模型,失敗自動切備用
    try:
        for chunk in primary_model.stream(messages):
            print(chunk.content, end="", flush=True)
    except Exception:
        print("⚠️ Primary failed, switching to fallback...")
        for chunk in fallback_model.stream(messages):
            print(chunk.content, end="", flush=True)

    完整的 router_stream.py 把 Streaming + Fallback + 對話歷史 + 使用統計整合在一起,不到 200 行。跑起來就是一個帶有自動模型切換的 terminal 聊天機器人。

    到這裡你有一個穩定的、體驗流暢的聊天機器人了。但它還是跑在你的 terminal 裡,只有你能用。第四階段讓它變成任何人都能呼叫的 API 服務。

    第四階段:FastAPI 部署(讓別人能用)

    目標:把 chatbot 包成 HTTP API,讓網頁、App、Line bot、Slack bot 都能呼叫。

    為什麼是 FastAPI:Python 生態最主流的 API 框架,原生支援 async、自動生成 API 文件、社群龐大。

    API 端點設計

    端點 方式 適合場景
    POST /chat 非串流,回傳完整 JSON Slack/Line bot、後端呼叫、批次處理
    POST /chat/stream SSE 串流,逐 token 推送 網頁聊天窗、需要即時體感的 UI
    GET /health 健康檢查 負載平衡器、監控系統
    GET /sessions/{id} 取得對話歷史 Debug、對話紀錄查詢
    DELETE /sessions/{id} 清除對話 使用者開始新對話

    啟動與測試

    # 安裝額外套件
    pip install fastapi uvicorn sse-starlette
    
    # 啟動 server
    python server.py
    # → 🤖 Smart Router API running on http://localhost:8900
    
    # 測試非串流
    curl -X POST http://localhost:8900/chat \
      -H "Content-Type: application/json" \
      -d '{"message": "什麼是 Docker?", "session_id": "test-user"}'
    
    # 回應範例:
    # {
    #   "answer": "Docker 是一個容器化平台...",
    #   "model_used": "llama_8b",
    #   "route_reason": "simple definition question",
    #   "session_id": "test-user",
    #   "fallback_used": false,
    #   "elapsed_seconds": 0.85
    # }

    前端怎麼接 SSE 串流?

    如果你要做網頁聊天窗,前端只需要幾行 JavaScript:

    // 瀏覽器原生 EventSource API
    const response = await fetch('/chat/stream', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({message: '寫一個排序函數', session_id: 'user-1'})
    });
    
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    
    while (true) {
      const {done, value} = await reader.read();
      if (done) break;
      const text = decoder.decode(value);
      // 每個 chunk 到了就即時顯示在畫面上
      document.getElementById('chat').innerHTML += text;
    }

    為什麼用 SSE 而不是 WebSocket?

    聊天場景是「使用者送一次訊息、AI 回一次」的單向推送。SSE 比 WebSocket 更適合:

    • 更簡單 — 單向傳輸,不需要管雙向連線
    • 瀏覽器原生 — EventSource API 內建自動重連
    • 穿透力好 — 走標準 HTTP,能通過代理和 CDN
    • 夠用 — 使用者的訊息用 POST 送,AI 的回應用 SSE 推

    四個階段的完整對照

    階段 檔案 新增能力 適合誰 可以停在這嗎?
    1. Duo 流水線 duo.py 設計→實作→審查 批量生成程式碼
    2. Smart Router router.py 自動選模型 個人實驗、學習
    3. Streaming + Fallback router_stream.py 逐字輸出 + 自動容錯 團隊內部使用
    4. FastAPI 部署 server.py HTTP API + SSE + Session 對外服務、產品整合

    費用比較:到底能省多少?

    假設一天 100 個問題,其中 20% 深度分析、30% 寫程式、50% 簡單問答:

    方案 月成本估算 品質
    全部用 Claude Sonnet ~$54 最高,但簡單題大材小用
    Smart Router(自動切換) ~$13.50 深度題用 Sonnet,其餘用 Llama
    全部用 Groq Llama 70B ~$4.20 最便宜,但分析品質弱

    Smart Router 方案比全用 Sonnet 省 75%,同時保留了深度分析任務的 Sonnet 品質。規模越大差距越明顯——1000 個使用者的 SaaS 產品,月省 $3000+。

    什麼場景適合?什麼場景不適合?

    ✅ 適合的場景

    1. 客服 Chatbot — 70% 簡單題用 Llama 8B 秒回,複雜題自動升級到 Sonnet
    2. 團隊 AI 助手 — 接 Slack,PM 問策略用 Sonnet,工程師要 code 用 Llama 70B
    3. 自動化 Pipeline — CI/CD 中的 AI code review,PR 提交自動跑
    4. SaaS 產品 — 加 AI 功能但要控成本,簡單摘要用 Llama,深度分析用 Sonnet
    5. 批量內容生成 — 50 篇產品描述:Sonnet 定規範 → Llama 批量寫 → Sonnet 抽檢

    ❌ 不適合的場景

    • 日常寫程式 — 用 Claude Code 或 Cursor 就好,不需要 LangGraph
    • 一次性分析 — 直接貼給 Claude 問就好,不需要搭 pipeline
    • 不需要控成本 — 個人使用月花不到 $10,Smart Router 省下的錢不值得建置成本

    三個問題判斷法

    在決定要不要用之前,問自己:

    1. 使用者是誰? — 你自己 → 不需要。別人(客戶/團隊/系統)→ 繼續看
    2. 會跑多少次? — 幾次 → 直接呼叫 API。幾百幾千次 → LangGraph 有意義
    3. 需要品質分級嗎? — 所有問題都要最高品質 → 用最強模型。不同問題可以不同品質 → Smart Router

    三個都答「後者」才值得用 LangGraph。

    走完四個階段之後,還有什麼?

    如果你的服務要上正式商業環境,還有幾個面向需要處理:

    面向 做什麼 不做的後果
    RAG(檢索增強) 接向量資料庫,讓 AI 查你的文件回答 AI 只能回答通用知識,不懂你的業務
    評估(LangSmith) 追蹤每次呼叫的路由、延遲、成本 不知道 Router 分類準不準,成本失控
    Session 持久化 用 Redis 存對話歷史(目前在記憶體) Server 重啟,所有對話消失
    認證 API key 或 JWT 驗證 任何人都能呼叫你的 API,幫你花錢
    Prompt Injection 防護 驗證使用者輸入,防止惡意 prompt 使用者讓 AI 做不該做的事
    Docker 容器化 打包成 Docker image 部署 環境不一致,部署困難
    Subgraph 嵌套 Router 判斷「寫程式」→ 啟動整個 Duo 流水線 只能單步回答,沒有品質把關

    完整專案結構

    langgraph-duo/
    ├── .env                  # API keys(不要 commit)
    ├── .gitignore            # 排除 .env
    ├── requirements.txt      # 所有套件
    ├── duo.py                # 階段 1:設計→實作→審查 流水線
    ├── duo_notebook.ipynb    # 階段 1 的 Jupyter 版
    ├── router.py             # 階段 2:Smart Router 基本版
    ├── router_stream.py      # 階段 3:+ Streaming + Fallback
    └── server.py             # 階段 4:FastAPI HTTP API

    所有程式碼都有詳細的中英文註解,說明每個模型的選用原因和適用場景。從哪個階段開始都可以,每個檔案都是獨立可執行的。

    總結

    LangGraph 多模型流水線的核心價值不是「用 AI 寫程式更快」——如果只是速度,直接用一個模型最快。它的價值在於把 AI 當成團隊來管理:讓擅長設計的去設計、擅長實作的去實作、擅長審查的去審查。

    從 Duo 流水線到 Smart Router,再到帶有 Streaming、Fallback、API 部署的生產版本,每一步都是從「能跑」走向「能上線」的必經之路。你不需要一次走完四個階段——先跑通第一階段,確認這個架構對你有用,再往下走。

    相關閱讀:LangChain/LangGraph 深度分析:架構師、顧問、個人公司的實戰指南 | Claude Code Agent Teams:從穩定執行到自動化代碼審查的完整指南 | 舊系統整合場景下,會用 vs 不會用 Claude Code 的差距

  • Claude Code Agent Teams:從穩定執行到自動化代碼審查的完整指南

    Claude Code Agent Teams:從穩定執行到自動化代碼審查的完整指南

    本文整合三部分內容:(1) Agent Teams 的穩定執行框架;(2) 全局代碼審查規則系統的設計與實施;(3) 自動化雙 Agent 審查機制(架構師 + PM)的完整實現方案。這是一份實踐導向的指南,涵蓋從資源評估、權限配置、到自動觸發流程的所有細節。

    第一部分:Agent Teams 的三層穩定執行框架

    1.1 評估層(Assessment)— 前置資源檢查

    每次建立 Agent Team 前,必須執行三個評估動作:

    1. 記憶體現狀:執行 free -h 或查看 Docker stats,確認可用容量
    2. 計算容量:根據模型選擇計算最大並行 agent 數(opus ~1GB、sonnet ~600MB、haiku ~400MB)
    3. 決策與確認:列出 agent 角色、模型、預估記憶體,取得用戶確認後才建立

    模型選擇口訣

    • 需要「想」 → Opus(架構決策、安全設計、業務邏輯、複雜 SQL、Code Review)
    • 需要「做」 → Sonnet(CRUD 實作、API 對接、測試撰寫、文件撰寫)
    • 需要「找」 → Haiku(檔案掃描、配置對比、API 整合)

    1.2 確認層(Confirmation)— 用戶決策前置

    建立 Team 前,必須向用戶列出清晰的角色定義和資源計劃,包括:

    • 各 agent 的角色名稱與職責
    • 指定的 AI 模型及其預估記憶體
    • 總計容量和系統可用容量
    • 並行數量和預期執行順序

    只有在用戶明確確認後,才啟動 TeamCreate 和後續任務建立。這一步防止了資源耗盡、權限誤配、以及 agent 之間通訊不暢的根本原因。

    1.3 執行層(Staged Execution)— 分階段序列化

    即使資源充足,也不應讓所有 agent 同時轟炸執行。分階段策略包括:

    1. 第 1 階段:啟動基礎設施 agent(如資源管理、環境配置)
    2. 第 2 階段:啟動 researcher agent,進行資訊搜集與分析
    3. 第 3 階段:啟動 architect agent,設計方案
    4. 第 4 階段:啟動 developer agent,進行實作
    5. 第 5 階段:啟動驗證 agent,進行測試與審查

    各階段 agent 完成任務後,主動進入「空閒」狀態並通知團隊。不進行「拉取」操作,而是由前置 agent 完成後才推進下一階段。這種設計確保:

    • 前置依賴滿足後才啟動
    • 消息傳遞清晰,無需輪詢
    • 系統記憶體壓力均勻分散
    • 故障範圍可控,易於除錯

    第二部分:全局代碼審查規則系統架構

    2.1 雙層規則設計

    代碼審查規則分為兩層,確保通用性和專案適應性的平衡:

    • 全局層~/.claude/memory/):所有項目共享的代碼品質標準(SOLID 原則、Design Pattern、代碼異味、測試規範、語言特定規範)
    • 項目層項目/.claude/code-review-rules.md):專案特定的業務邏輯規則、設計稿映射、外部驗證清單

    2.2 全局規則內容結構

    code-review-global-rules.md(SOLID、Pattern、Code Smell、Testing)

    • SOLID 五項原則的定義和違反示例
    • Design Pattern 反面示例(God Object、Feature Envy 等)
    • 代碼異味檢查清單(N+1、Magic Number、過長方法、過多參數、重複代碼、複雜邏輯)
    • 單元測試最低標準(80%+ 覆蓋、邊界情況、Mock vs Real Dependency)

    code-review-languages.md(Java、Python、JavaScript/TypeScript、SQL)

    • 命名規範(類名、方法名、變數名、常數、包名)
    • 包結構約定(分層架構:domain/service/repository/validator 等)
    • 異常處理規範
    • 語言特定工具規範(Lombok、OSGi、Type Hint、Docstring 等)

    code-review-feedback.md(架構師與 PM Agent 職責和對焦框架)

    • 架構師 Agent:SOLID 遵循度(1-5 分)、Design Pattern、Code Smell、測試覆蓋、異常處理、語言規範
    • PM Agent:功能 vs 設計稿對齐、API 返回值、邊界情況、業務邏輯、用戶體驗、外部驗證需求
    • 雙方對焦框架:架構師問「代碼設計能支持你檢查的功能嗎?」,PM 反問「這些功能需求會造成架構問題嗎?」

    code-review-process.md(自動觸發、Agent 角色、聯合報告格式)

    • 觸發點:Task marked as completed
    • Agent 角色 Prompt(初版):架構師評估、PM 評估、聯合對話
    • 聯合報告格式:分離的評估 + 統一優先級排序 (P0/P1/P2)
    • 最終判定:Pass / Needs Work / Fail

    2.3 項目層規則(台灣發票系統範例)

    項目規則分為三類:

    A. 業務邏輯規則(優先級最高)
    • 進項稅計算公式(進項稅金額 = 發票金額 × 5%,DECIMAL(19,2) 向下取整)
    • 三聯式(不含稅)vs 二聯式(含稅)的會計處理邏輯
    • 進項稅折讓的強制申報期限(當期內必須申報,無延後例外)
    • 兼營營業人的比例扣抵公式(可扣抵進項稅 = 總進項稅 × (應稅銷售額 / 總銷售額))
    • 發票號碼連續性檢查和遺失空白發票記錄
    • 發票開立日期 → 申報期間的映射(1-2月→第1期、3-4月→第2期 等,共六期)
    B. 設計稿對齐規則(優先級次之)
    • API 端點的返回格式和欄位定義
    • 邊界情況的預期響應(零金額、負數、超大金額)
    • 數據型態和範圍檢查
    C. 外部驗證清單(優先級最低,但定期更新)
    • 進項稅可扣抵期限是否仍為 10 年
    • 電子發票強制規定的生效狀態
    • 設計稿版本確認
    • 技術規範(iDempiere PO 模型、OSGi Bundle)

    第三部分:自動化雙 Agent 審查機制的實現

    3.1 TaskCompleted Hook 配置

    .claude/settings.local.json 中配置 TaskCompleted Hook,每當任務標記完成時,自動觸發架構師和 PM 兩個 agent 進行深度審查:

    {
      "hooks": {
        "TaskCompleted": [
          {
            "hooks": [
              {
                "type": "agent",
                "prompt": "Conduct comprehensive code review: (1) Architecture: SOLID principles (1-5), design patterns, code smells (P0/P1/P2), testing, exceptions. (2) Functional: spec alignment, API contracts, boundary cases, business logic (Taiwan tax rules). Provide unified P0/P1/P2 priority with Pass/Needs Work/Fail decision and file:line recommendations.",
                "model": "opus",
                "statusMessage": "Running code review on completed task...",
                "timeout": 300
              }
            ]
          }
        ]
      }
    }

    3.2 架構師 Agent 職責細節

    架構師 Agent 按以下維度進行評估,每項 1-5 分自評:

    1. SOLID 原則遵循度:檢查 S(單一職責)、O(開閉)、L(里氏替換)、I(接口隔離)、D(依賴倒轉)五項是否遵循
    2. Design Pattern 應用恰當性:是否應用了合適的設計模式,是否存在過度設計
    3. 代碼異味程度:N+1 查詢、Magic Number、過長方法(>20 行)、過多參數(>3 個)、重複代碼、複雜邏輯(圈複雜度 > 10)
    4. 測試覆蓋度:目標 80%+,是否涵蓋邊界情況(null、empty、max/min)
    5. 異常處理完整性:是否避免過寬泛的異常捕捉,異常信息是否有意義,是否吞掉異常
    6. 語言特定規範遵循度:根據 code-review-languages.md 檢查命名、包結構、工具使用規範

    輸出:各維度評分 + 具體改進建議(標記優先級 P0/P1/P2 和代碼位置)

    3.3 PM Agent 職責細節(含 QA)

    PM Agent 進行務實檢查,確保功能實現和業務邏輯正確性:

    1. 功能 vs 設計稿對齐:設計稿的所有功能點是否實現,是否有範圍蠕動
    2. API 返回值對齐:返回欄位是否與文檔一致,資料型態是否正確,必需欄位是否都有
    3. 邊界情況涵蓋:null 輸入、空集合、最大值/最小值、負數、零值是否處理
    4. 業務邏輯正確性:根據項目規則(進項稅計算、發票號碼、申報期間映射等)是否正確
    5. 用戶體驗:錯誤提示是否清楚,用戶流程是否順暢,是否有令人困惑的行為
    6. 外部驗證需求:是否需要查證業務規定(優先 C)、設計稿(優先 A)、技術規範(優先 B)

    輸出:功能層缺陷清單、QA 項目、待驗證項(註明 C/A/B 優先級)

    3.4 聯合報告與優先級統一排序

    架構師和 PM 分別完成評估後,進行深度對話:

    • 架構師問 PM:「代碼結構能支持你檢查的功能嗎?」
    • PM 反問架構師:「這些功能需求會造成架構問題嗎?」

    基於對話結果,生成聯合報告:

    • P0(Critical):N+1 查詢、安全漏洞、死鎖風險、未處理的邊界情況、違反台灣稅務規則
    • P1(Important):過長方法、重複代碼、Magic Number、缺失測試、不清晰的錯誤提示
    • P2(Nice to Have):輔助方法提取、考慮設計模式、日誌增強

    最終判定

    • Pass:P0 問題 0 個,P1 問題可接受(≤2 個或不影響核心功能)
    • ⚠️ Needs Work:P0 問題 1-3 個,或 P1 問題 >3 個,或測試覆蓋度 <70%
    • Fail:P0 問題 ≥3 個,或安全漏洞,或違反重要法規

    第四部分:審查流程與反覆迭代

    4.1 外部驗證流程

    當 PM Agent 提出待驗證項時,按優先級啟動查詢:

    優先級 C(業務邏輯)— 需人工驗證
    • 用戶透過稅務官網或法規文件進行人工查證(AI 查詢易出錯)
    • 驗證結果更新到項目規則
    • 重新審查涉及的代碼
    優先級 A(設計稿)— 查閱項目文檔
    • 查閱設計文件、API 文檔
    • 如無對應文件,提示用戶補充
    • 確認後更新 code-review-rules.md
    優先級 B(技術規範)— 查詢官方文檔
    • 查詢 iDempiere、OSGi 官方文檔
    • 標記為參考(變動機會低)

    4.2 修改與重審流程

    當報告判定為 Needs Work 或 Fail 時:

    1. 開發者查看報告,收到 P0/P1/P2 改進清單
    2. 開發者修改代碼並完成新測試
    3. 重新標記 Task 為 completed(或手動觸發 /code-review-force)
    4. 自動重審(流程回到架構師 + PM 評估)
    5. 迴圈進行,直到最終判定 Pass

    第五部分:系統集成與迭代計劃

    5.1 全局記憶結構

    所有規則和流程信息保存在用戶的全局記憶庫中:

    • ~/.claude/memory/code-review-global-rules.md — SOLID、Pattern、Code Smell、Testing
    • ~/.claude/memory/code-review-languages.md — Java、Python、JS、SQL 規範
    • ~/.claude/memory/code-review-feedback.md — 架構師/PM 職責與對焦框架
    • ~/.claude/memory/code-review-process.md — 自動觸發、報告格式、判定標準

    5.2 項目級別記憶結構

    • 項目/.claude/code-review-rules.md — 業務邏輯規則 + 設計稿映射 + 外部驗證清單
    • 項目/.claude/settings.local.json — TaskCompleted Hook 配置

    5.3 三個迭代階段

    Phase 1(現在)
    • 建立全局規則知識庫(4 個記憶文件)
    • 編寫項目層規則(code-review-rules.md)
    • 配置 TaskCompleted Hook
    • 在項目中試用審查機制
    Phase 2(2-4 週後)
    • 根據實際審查結果優化規則(哪些規則發現了 bug、哪些規則太寬泛)
    • 改進 Agent Prompt(增加上下文、更清晰的指導)
    • 補充項目層規則細節(特別是邊界情況和特殊規則)
    Phase 3(1-2 月後)
    • 考慮集成到 CI/CD(每次 PR 自動審查)
    • 累積審查報告,形成項目代碼品質歷史
    • 調整 Hook 超時和 Agent 模型選擇

    第六部分:常見問題與實踐建議

    Q1:如果審查花太長時間怎麼辦?

    A:初版 timeout 設為 300 秒。如果超時,可調整為 600 秒,或簡化 Agent Prompt 去掉某些維度。對大型代碼改動,可分成多個小 Task,每個分別審查。

    Q2:外部驗證項目誰負責查證?

    A:初版由用戶或 PM 手動查證(特別是業務邏輯 C 項)。後續可考慮集成更多自動化(如 WebSearch for C 項、文檔連接 for A 項)。

    Q3:PM Agent 如何知道設計稿的內容?

    A:初版需在 code-review-rules.md 的 B 項中維護設計稿摘要(API 返回格式、欄位定義、邊界情況)。後續可考慮直接連接設計文件或 API 文檔鏈接。

    Q4:審查報告輸出到哪裡?

    A:初版直接輸出到對話(用戶可複製保存或導出)。後續可考慮存到 Task comment、Markdown 文件或項目文檔。

    Q5:如何確保 Agent Teams 穩定運行?

    A:遵循三層框架:

    • 評估層:前置資源檢查和確認,不盲目建立 Team
    • 確認層:列出角色、模型、容量,取得用戶確認
    • 執行層:分階段序列化,避免並行轟炸,讓 agent 主動進入空閒狀態而不是被拉取

    以及定期檢查記憶體、監控 agent 通訊、設置合理的超時時間。

    結論

    穩定可信的 Agent Teams 不是靠運氣,而是靠紮實的前置規劃、清晰的資源評估、以及精心設計的執行流程。本文提供的三層框架(評估→確認→分階段執行)確保了 Team 運行的穩定性。配合全局規則庫 + 項目規則 + 自動審查機制,可以建立一套完整的代碼品質保證體系,既有通用性,也有靈活性。

    從 Phase 1 的試用開始,逐步優化規則和 Agent Prompt,經過 2-4 週的實踐,可以達到穩定的審查效果。最終目標是讓代碼審查成為自動化流程的一部分,開發者無需手動申請審查,系統自動評估和反饋,確保每個 Task 完成時都經過架構師和 PM 的雙重把關。

  • 【實戰指南】如何為 AI 代理準備資料?Agent Spec 設計指南 — 從 SDD 到代理規格書的完整轉化

    在上兩篇文章中,我們解析了《2026 Agentic Coding Trends Report》的 8 大趨勢,也實際跑了一次雙代理流水線。但實戰中最常被問到的問題是:「我到底要準備什麼資料給 AI 代理?」

    答案不是傳統的 SDD(Software Design Document)。50 頁的 Word 文件會讓代理 timeout。你需要的是一種為 AI 代理設計的輕量規格書——Agent Spec


    一、為什麼 SDD 不適合代理?

    傳統 SDD(給人看的) Agent Spec(給 AI 看的)
    長度 50+ 頁 200 行以內
    包含 背景、歷史、會議記錄、UML 圖 只有「做什麼」「不做什麼」「怎樣算完成」
    表達方式 文字描述 + 圖表 程式碼範例 + 簽名契約
    閱讀方式 多人在不同時間反覆閱讀 一個代理在一次 context 中完整消化
    設計原則 越詳細越好 越精準越好(長度是敵人)

    核心差異:SDD 追求完整性,Agent Spec 追求精準性。代理不需要知道「為什麼要做這個功能」的三頁背景故事,它需要知道「輸入什麼、輸出什麼、邊界怎麼處理」。


    二、Agent Spec 的五個區塊

    一份文件,五個區塊,控制在 200 行以內。以下用「訂單處理功能」為例完整示範:

    區塊一:WHAT — 做什麼(必要)

    這是最核心的區塊。不要寫散文,直接給輸入輸出契約

    ## 功能規格
    
    ### 輸入輸出契約(最重要的部分)
    - 函數簽名:`def process_order(order: Order) -> Receipt`
    - 輸入範例:
      {"item_id": "A001", "qty": 3, "user_id": "U100"}
    - 輸出範例:
      {"receipt_id": "R001", "total": 150.0, "status": "confirmed"}
    - 錯誤回傳:
      raises InsufficientStockError when qty > available
      raises DuplicateOrderError when same order within 1 second
    
    ### 行為規則
    1. 庫存不足時拒絕訂單,不要部分出貨
    2. 同一用戶同一秒內的重複訂單要去重
    3. 金額計算精度到小數點後 2 位
    

    為什麼這樣寫有效?因為代理最擅長的是「根據契約實作」。函數簽名 + 輸入輸出範例 + 邊界條件 = 代理需要的全部資訊。業務背景、產品需求文件、PM 的原始 ticket——這些人類需要,代理不需要。

    區塊二:HOW — 技術約束(必要)

    告訴代理已經有什麼可以直接用。這是最容易被忽略、但最能提升品質的資訊:

    ## 技術約束
    
    ### 必須使用
    - 語言:Python 3.11
    - 框架:FastAPI
    - 資料庫:已有的 PostgreSQL,用 SQLAlchemy ORM
    - 現有模組:from app.models import Order, Product(已存在,直接用)
    
    ### 現有介面(可以直接呼叫的)
    - db.session.get(Product, product_id) -> 查商品
    - db.session.add(receipt) -> 寫入收據
    - redis_client.get(f"order:{user_id}:{hash}") -> 去重檢查
    - from app.services.product_service import get_price -> 取得商品價格
    
    ### 專案結構(讓代理知道檔案放哪裡)
    src/
      models/order.py       -- 資料模型(已存在)
      services/             -- 業務邏輯放這裡
      routes/orders.py      -- API 路由(要在這裡加 endpoint)
    

    為什麼這麼重要?沒有這個區塊,代理會自己發明資料庫連接方式、自己建立 model class、自己設計專案結構——然後跟你的現有程式碼完全不相容。告訴它「已經有什麼」,它就會「用已經有的」。

    區塊三:DON’T — 禁止事項(必要)

    從我們的雙代理實驗中學到的最重要教訓:禁止比允許更重要。AI 的創造空間是無限的,你無法列舉所有「應該做」的事,但你可以明確劃定「絕對不能做」的紅線。

    ## 禁止
    
    ### 絕對不可(違反 = 程式碼作廢)
    - 不可直接寫 SQL(必須用 ORM)
    - 不可在 API handler 裡寫業務邏輯(必須放 service 層)
    - 不可 catch 所有 Exception(必須指定具體類型)
    - 不可新增資料表(只能用現有的 schema)
    - 不可使用 global 變數
    
    ### 不要(違反 = 需要修改)
    - 不要新增依賴套件
    - 不要修改現有的 model 定義
    - 不要寫 print debug(用 logging)
    - 不要硬編碼設定值(用 config)
    

    分層禁止的好處:「絕對不可」是紅線——審查代理看到這些違規應該直接判 FAIL。「不要」是黃線——可以在審查中要求修改。這種分層讓自動化審查變得可行。

    區塊四:DONE — 怎樣算完成(必要)

    這個區塊定義了機器可以驗證的完成標準。如果完成標準只有人能判斷(「程式碼要寫得漂亮」),那自動化流水線永遠跑不完。

    ## 完成標準
    
    ### 代理自檢清單
    - [ ] 所有公開函數都有 type hint 和 docstring
    - [ ] 沒有用到禁止清單中的任何項目
    - [ ] 能處理:正常訂單、庫存不足、重複訂單、無效商品 ID
    
    ### 測試要求
    - [ ] pytest 測試覆蓋率 > 85%
    - [ ] 包含正常路徑、邊界條件、錯誤處理測試
    - [ ] 併發測試(至少 5 個 thread 同時下單)
    
    ### 驗證指令(代理或流水線可以直接執行的)
      pytest tests/ -v --tb=short
      flake8 src/ --max-line-length=120
      mypy src/ --strict
    

    關鍵:給出可以直接跑的驗證指令。這讓流水線可以自動驗證——不需要人來判斷「完成了沒有」,直接跑指令看 return code。

    區塊五:CONTEXT — 最小必要上下文(選用)

    只有當代理需要理解「這個功能在系統中的位置」時才加這個區塊。注意:只放代理需要知道的,不放背景故事。

    ## 上下文
    
    ### 相關檔案(給路徑,不要描述內容)
    - 資料模型:src/models/order.py
    - 現有服務層:src/services/product_service.py(參考這個的寫法)
    - API 路由:src/routes/orders.py(要在這裡加 endpoint)
    
    ### 資料流
    用戶 -> POST /orders -> OrderService.process() -> DB write -> return Receipt
    
    ### 注意事項
    - 這個功能是 v2 重構,舊的 process_order_legacy() 已廢棄但還在程式碼中
    - 資料庫連線用 app.db.get_session(),不要自己建連線
    

    三、不同任務複雜度對應的準備量

    任務複雜度 範例 需要的區塊 Spec 行數 代理配置
    簡單 加一個 API endpoint WHAT + DON’T ~50 行 單代理
    中等 實作一個演算法模組 WHAT + HOW + DON’T + DONE ~100 行 雙代理(寫+審)
    複雜 跨模組的功能開發 全部五個區塊 ~150 行 三代理(寫+審+測)
    大型 多服務協作的功能 拆成多份子任務 Spec 每份 ~80 行 Agent Team + Orchestrator

    大型任務的關鍵:不是寫一份更大的 Spec,而是拆成多份小 Spec。


    四、Agent Team 的致命問題:為什麼會卡死?

    在我們的雙代理實驗中,跑了 5 次才成功。多代理場景下問題更嚴重。以下是常見的卡死模式和解法:

    卡死模式 1:連鎖 Timeout

    # 串聯 3 個代理,每個 300s timeout = 最壞 900s
    Orchestrator -> Agent A (300s) -> Agent B (300s) -> Agent C (300s)
    
    # 如果 Agent A timeout 了,B 和 C 都在空等
    

    解法:每個代理獨立 timeout + 能平行的就平行

    # 架構代理串行(後面都依賴它)
    design = call_agent("architect", spec, timeout=120)
    
    # 實作和測試平行(互不依賴)
    with ThreadPoolExecutor(max_workers=2) as pool:
        impl = pool.submit(call_agent, "implementer", design, timeout=120)
        test = pool.submit(call_agent, "tester", design, timeout=120)
    

    卡死模式 2:髒輸出污染下游

    # Agent A 輸出中文說明而非程式碼
    # Agent B 拿到垃圾 -> 產出垃圾 -> Agent C 也跟著壞
    Agent A: "我建議三種方案..." -> Agent B: "這不是程式碼?" -> Agent C: timeout
    

    解法:每個代理之間加「驗證閘門」

    # 不要直接傳遞
    output_b = call_agent(agent_b, output_a)  # 危險!
    
    # 加驗證閘門
    code = extract_code(output_a)
    if not code:
        raise PipelineError("Agent A did not produce valid code")
    if not code.startswith(("import ", "from ", "class ", "def ")):
        raise PipelineError("Agent A output is not valid Python")
    output_b = call_agent(agent_b, code)  # 只傳驗證過的內容
    

    卡死模式 3:Spec 太長導致代理失焦

    # 500 行的大 Spec -> 代理只看前 100 行,後面的禁止事項被忽略
    call_agent(agent, massive_500_line_spec)  # -> 產出違規程式碼
    

    解法:拆分子任務,每份 Spec 控制在 50-100 行

    # 拆成多份獨立的小 Spec
    subtasks = [
        {"agent": "backend",  "spec": order_service_spec},    # 50 行
        {"agent": "backend",  "spec": order_endpoint_spec},    # 30 行
        {"agent": "tester",   "spec": order_test_spec},        # 40 行
    ]
    
    # 逐個執行,每個都有獨立的驗證
    for task in subtasks:
        output = call_agent(task["agent"], task["spec"], timeout=120)
        validate(output)
    

    五、正確的擴展路徑

    不要一步跳到 5 個代理的複雜系統。正確的路徑是:

    Level 0 -> Level 1:加一個 Reviewer

    # 你現在就可以做的最小改變
    code = call_agent("implementer", spec)
    review = call_agent("reviewer", code)  # 就這一步
    if "FAIL" in review:
        code = call_agent("implementer", f"Fix:\n{review}\n\n{code}")
    

    Level 1 -> Level 2:加自動測試

    # 在審查後加 Tester + 實際執行
    tests = call_agent("tester", code)
    result = run_pytest(code, tests)  # 機器驗證,不靠 AI 判斷
    

    Level 2 -> Level 3:平行化

    # 找出可以平行的步驟
    # 實作和測試可以同時進行(都只依賴 spec,不互相依賴)
    impl_future = pool.submit(call_agent, "implementer", spec)
    test_future = pool.submit(call_agent, "tester", spec)
    

    Level 3 -> Level 4:Agent Team

    # 用 Python Orchestrator(不是 AI)管理流程
    # AI 做思考,Python 做控制
    class Orchestrator:
        def run(self, spec):
            design = self.call_agent("architect", spec)
            self.validate(design)           # 驗證閘門
    
            impl, tests = self.parallel(    # 平行執行
                ("implementer", design),
                ("tester", design)
            )
            self.validate(impl)             # 驗證閘門
    
            review = self.call_agent("reviewer", impl)
            if "FAIL" in review:
                impl = self.call_agent("implementer", fix_prompt)
    
            self.run_tests(impl, tests)     # 機器驗證
    

    六、Agent Spec 模板(直接複用)

    以下是可以直接複製使用的模板,填入你的內容即可:

    # Agent Spec: [功能名稱]
    
    ## WHAT - 做什麼
    ### 輸入輸出契約
    - 函數/API 簽名:
    - 輸入範例:
    - 輸出範例:
    - 錯誤情況:
    
    ### 行為規則
    1. ...
    2. ...
    
    ## HOW - 技術約束
    ### 必須使用
    - 語言/框架:
    - 現有模組(直接 import):
    
    ### 現有介面(可以直接呼叫的)
    - ...
    
    ## DON'T - 禁止
    ### 絕對不可
    - ...
    
    ### 不要
    - ...
    
    ## DONE - 完成標準
    ### 自檢清單
    - [ ] ...
    
    ### 驗證指令
      pytest ...
      lint ...
    
    ## CONTEXT - 上下文(選用)
    ### 相關檔案
    - ...
    
    ### 資料流
    A -> B -> C -> D
    

    七、一句話總結

    SDD 是給人在不同時間反覆閱讀的。
    Agent Spec 是給 AI 在一次 context 中完整消化的。
    精準 > 詳細,200 行 > 2000 行。

    本文是《2026 Agentic Coding Trends Report》深度解析系列的第三篇。系列文章: