標籤: Multi-Model Orchestration

  • 你的 AI Agent Team 為什麼越做越小?視角驅動的編制方法論

    重點摘要

    • 你以為的「3 個 Agent 上限」可能是創傷的外推,不是設計 — 來自一次 OOM 事故的過度修正,結果反過來殺死你專案的角色細分工
    • 正確的限制是記憶體預算,不是數量 — 16GB 機器可用 ~11GB agent 預算,混合模型(Opus/Sonnet/Haiku)可以安全跑 8+ 個專職 agent
    • 但更深的問題是:角色不是頭銜,是視角 — 你該問「這個計畫需要哪些思考視角」,不是「我有幾個 slot」
    • 三個永遠不能省的視角:使用者、PM、測試者 — 即使是純技術、只有 Python、只有你一個人的專案,這三個視角的思考必須發生
    • 視角打分模型:每個視角用「風險 × 範圍」打 0-9 分,高分獨立成 agent,低分 fold 到其他 agent 的 prompt
    • 反直覺發現:小任務(3 行 API)比大任務更需要資深視角(架構師、運維、測試),因為實作已經不是風險,「該不該存在」才是
    • 壞規則會透過 template anchoring 自我強化 — 修了 runtime 規則還不夠,必須連 template 一起改

    我的 AGENTS.md template 有一條規則:Agent Team 最多 3 個同時跑。這條規則是對的 — 直到我發現它正在偷偷殺死我的專案設計。

    這篇文章是一趟很誠實的反思紀錄。從一次 16GB 機器 OOM 當機開始,到發現我對 AI Agent Team 的所有規則其實都建立在錯誤的抽象層上。結論不是「解除限制、大膽開 agent」,而是更根本的東西:角色根本不是我以為的那個東西

    第一層:我的規則其實是創傷,不是設計

    事情是這樣開始的。2026 年 3 月 3 日,我嘗試在 16GB 的 Mini PC 上同時跑 9 個 Opus agent 做大型重構,結果系統 OOM 當機。從那一天起,我的 MEMORY.md 多了一條 Iron Rule:「Agent Team 最多 3 個同時跑」。

    這條規則是理性的。9 個 Opus × 1GB ≈ 9GB,加上系統和 Docker 的常態佔用,16GB 當然吃不下。3 個是「經驗上安全」的數字。但我沒注意到一件事:這條規則是在「全 Opus」的情境下歸納出來的。我直接把它當成所有情境的通用上限,沒考慮混合模型的狀況。

    更糟的是,這條規則開始滲透到我的 AGENTS.md template、我的專案計畫、我對每個新專案的初始設計。結果是:即使某個專案明明應該有架構師、後端、前端、Kafka、SQL、Test、PM、QA 八個角色,我也會「收斂」到「全端 + 檢查者 + 架構師」三人組。我不是選擇了簡化設計,是我被一條舊規則綁架了設計想像力

    第二層:記憶體預算制取代數量制

    第一個修正很直覺:限制應該是記憶體總量,不是 agent 數量。16GB 機器扣掉系統和 Docker 的 ~5GB,還剩 ~11GB 可以給 agent 用。而不同模型的記憶體占用差很多:

    模型 記憶體 適合任務類型
    Opus ~1.0 GB 架構決策、跨檔案推理、複雜邏輯、資深思考
    Sonnet ~0.6 GB 實作、API、測試、文件
    Haiku ~0.4 GB 檔案掃描、config 對照、簡單查詢

    換算一下:一個架構師(Opus 1.0)+ 兩個後端(Sonnet × 2 = 1.2)+ 前端(Sonnet 0.6)+ SQL(Sonnet 0.6)+ Kafka(Sonnet 0.6)+ Test(Sonnet 0.6)+ QA + PM(Haiku × 2 = 0.8)= 5.4 GB,8 個 agent。完全在 11GB 預算內,還有 5.6GB 的 headroom。

    所以「3 個 agent」的舊規則其實是錯的。正確的說法是「全 Opus 時 ≤ 3 個;混合模型時可以到 8+ 個,只要總和在預算內」。這一步修完,我理論上可以解鎖原本想要的專職團隊設計。

    但這還不夠:我一直在錯的抽象層解決問題

    我以為修到這裡就結束了。結果真正的問題還在更上面一層。

    記憶體預算制解決了「能不能開 8 個 agent」的技術問題,但沒解決「我該開哪 8 個角色」的設計問題。我仍然在用「頭銜思維」想 Agent Team — 有一個固定的角色清單(架構師、後端、前端、QA…),看看預算能放幾個,從清單挑幾個進來。

    這個思維的錯誤在哪?它把「角色」當成頭銜 / headcount,而角色的本質其實是視角 / perspective — 一種「怎麼思考這件事」的觀點。兩個是完全不同的東西:

    思維 頭銜 Headcount 視角 Perspective
    單位 angular-dev、backend-dev 「使用者會怎麼用」「PM 會怎麼想」
    數量 固定清單 每個 plan 動態決定
    「只有 Python」專案 1 個 Python dev 就好 仍需使用者、PM、測試視角
    小任務(3 行) 給最便宜的 dev 可能需要最資深的運維視角

    三個永遠不能省的視角

    這是最實用的一條發現。不管你的專案是什麼技術、什麼規模、幾個人做,這三個視角永遠必須存在:

    • 使用者視角 — 誰會用這個東西?他們的心智模型是什麼?介面怎麼設計才順?
    • PM 視角 — 這件事符合大方向嗎?是現在最該做的嗎?會影響別的優先級嗎?
    • 測試者視角 — 什麼會壞?邊界 case 在哪?漏掉什麼?失敗模式是什麼?

    這三個視角可以 fold 到其他 agent 的 prompt 裡(例如讓 backend-dev 在設計階段戴 PM 帽子),但絕對不能默默省略。省略它們的結果是:你會產出「技術上正確、實用上無用」的東西 — API 能跑但沒人想用、feature 完成但 PM 說這不是重點、code 過 review 但上線就爆 edge case。

    「只有 Python」不是跳過使用者視角的理由。「只有你一個人」不是跳過 PM 視角的理由。「只是改一行」不是跳過測試者視角的理由。這三個視角的思考必須發生,差別只在「由誰承擔」,不在「要不要做」。

    視角打分模型:風險 × 範圍

    光知道「要列視角」還不夠,還要知道哪些重要、哪些次要。我用的打分模型很簡單:Score = Risk × Scope,兩個維度都是 0-3,總分 0-9。

    • 風險:這個視角漏掉的話多慘?(0 = 無所謂,3 = 災難)
    • 範圍:這個視角涉及多少產出物?(0 = 無,3 = 全部)

    分數算出來後按以下規則分組到 agent:

    • Score ≥ 6:獨立 agent,很可能要用 Opus 或高階 Sonnet
    • Score 3-5:獨立 agent,或跟另一個視角配對(Sonnet)
    • Score 1-2:fold 到其他 agent 的 prompt,明確列入該 agent 的優先順序
    • Score 0:在計畫裡 acknowledge 就好,不花 agent 資源

    最後檢查記憶體預算是否超過 11GB,超過就把分數最低的兩個合併。

    反直覺發現:小任務需要更資深的視角

    這是這整套方法論裡最違反直覺、也最容易忽略的一條。實作越簡單的任務,反而越需要資深視角

    原因很微妙:大任務的風險分散在許多實作細節裡,實作者視角得分最高(因為「寫錯」的機率大)。但小任務的實作幾乎免費 — 風險已經轉移到別的地方了:「這東西該不該存在」(架構師視角)、「會不會在奇怪情境下爆」(測試者視角)、「運維怎麼用這個 endpoint 做決策」(運維視角)

    具體例子:幫一個 Docker 服務加 /health endpoint,回 {"status": "ok"}

    • 實作者視角:風險 1 × 範圍 1 = 1 分(3 行誰都會寫)
    • 運維視角:風險 3 × 範圍 2 = 6 分(這個 endpoint 決定服務要不要被 monitoring 重啟)
    • 架構師視角:風險 3 × 範圍 1 = 3 分(「健康」的定義是最大問題)
    • 測試者視角:風險 2 × 範圍 1 = 2 分
    • 使用者視角:風險 2 × 範圍 1 = 2 分

    結論:這個任務要派一個 Sonnet agent,但 prompt 的優先順序是運維 > 架構 > 測試 > 使用者 > 實作。它不是「後端 dev 寫 3 行」,是「一個有運維腦袋的人剛好以 3 行 code 為產出」。

    這個例子回答了我長久的困惑:「小任務該加一個專職 agent,還是從 tester 出發、還是從 dev 出發?」答案是 — 從得分最高的視角出發,不從頭銜出發。小任務通常是資深視角得分高,所以該派的不是 junior implementer,是帶著資深視角的人。

    三個完整案例

    案例 1:純 Python script(200 行,讀 CSV 做統計)

    視角枚舉:Python 實作者、使用者、PM、測試者、架構師

    打分:實作者 6、測試者 6(資料邊界 case 是真風險)、使用者 2、PM 1、架構師 1

    分組:2 個 Sonnet(~1.2 GB)

    • Agent 1:實作 + 架構 + PM 視角(優先:實作)
    • Agent 2:測試 + 使用者視角(優先:測試 — 資料邊界主導)

    2 個 agent,但 5 個視角都被想過了。要的是「每個視角都有思考發生」,不是「每個視角都有一個人」。

    案例 2:OMS 訂單退款功能(跨前後端/Kafka/SQL/admin)

    視角枚舉:架構師、安全、後端、前端、Kafka、SQL、Admin UI、測試、使用者、PM、合規

    打分:每個都 ≥ 4 分,因為範圍廣且風險高(金流)

    分組:9 個 agent(~6.6 GB,預算內)

    • Architect(Opus)、Security(Opus)
    • Backend-dev、Frontend-dev、Kafka、SQL、Admin-UI、Tester(全部 Sonnet)
    • 使用者 + PM + 合規視角 fold 到一個 Haiku

    這就是大型專案值得完整專職團隊的時候。注意:即使 9 個 agent,使用者和 PM 視角還是被 fold 到一個 Haiku,不是各自獨立。視角沒被省,但不一定要佔獨立 slot

    案例 3:3 行 /health endpoint(已在前面詳細分析)

    分組:1 個 Sonnet,所有 5 個視角都 fold,優先順序運維 > 架構 > 測試 > 使用者 > 實作。

    計畫修訂時必須重算

    一個容易忽略的點:staffing 是 live document,不是一次性決定。當計畫執行到一半發現:

    • 新需求浮現(「喔原來這個也要接 Kafka」)→ 新視角需要打分,決定獨立或 fold
    • 某視角發現其實很關鍵(原本 fold 的安全視角,發現有 PII 暴露)→ 升級為獨立 agent
    • 某視角發現其實不必要(決定不做 admin UI)→ 該 agent 退場或重新分配

    每次 plan 修訂都該觸發 staffing 重算。如果 AGENTS.md 跟當前 plan 不同步,應該先更新 AGENTS.md 再繼續 dispatch。

    壞規則會自我強化 — template anchoring 的陷阱

    最後我想分享一個元層級的教訓,是這整件事最讓我不舒服的部分。

    我一開始以為只需要改 runtime 規則(把「3 個上限」改成「預算制」)就好了。但我後來發現:壞規則會透過 template 自我強化。我的 AGENTS.md template 原本的例子只有 2 個角色(implementer + reviewer),這個「低錨」讓我每次初始化新專案時,都自動往「3-5 個角色」的方向填,幾乎從不主動設計 7-10 個。

    這形成一個 feedback loop:

    • 創傷 → 記憶體規則收緊到「3 個」
    • Template 的例子反映記憶體規則 → 錨在低數量
    • 我用 template 寫新 AGENTS.md → 產出低角色的設計
    • 低角色的設計變成「標準」→ 我的記憶認為「就是這樣」
    • 循環加強

    光改規則不夠,template 也得改。我最後把 template 改成強迫跑完整個視角打分流程的版本:頂部要求先讀「視角驅動 staffing」的 brain,中間有「Perspective Inventory」區塊要你填每個視角的 Risk × Scope 分數,再下面是「Memory Budget」計算區塊。沒跑完流程根本填不完 template。

    這個 template 和配套的方法論 brain 都放在公開的領域腦 repo裡,歡迎 clone 參考。

    跟 VCS / 版本控制的關係

    如果你讀過我之前那篇 Jujutsu vs Git,可能會問:視角驅動 staffing 跟 jj 有什麼關係?

    答案是:沒有關係,而且這個順序很重要。視角驅動 staffing 是計畫期的事 — 你在寫 plan、決定 Agent Team 編制時發生。VCS(git / jj / worktree / workspace)是執行期的事 — Agent Team 已經成立、開始跑、產出變動需要合併的時候才登場。不能倒過來。

    我之前一度把這兩件事混在一起寫(在「VCS + AI 工作流」的 brain 裡討論 Agent Team 編制),後來意識到這是錯的。Staffing 決定「要做什麼」,VCS 決定「做的產出怎麼合併」。兩者相關但獨立,混在一起會讓人以為「切到 jj 就能多開 agent」— 不是的,能多開 agent 是記憶體預算 + 視角分析的事,跟 VCS 無關。

    結論:為什麼這個順序重要

    我從「Agent Team 最多 3 個」走到「視角驅動編制」的這段路程,對我來說最重要的一個 meta-lesson 是:你的限制規則可能不是來自設計,而是來自創傷。一次系統炸掉的記憶,會變成一條 Iron Rule,然後透過 template anchoring 滲透到你所有未來的設計決策。

    修復這種規則不能只修表面。你得:

    1. 檢視規則的原始情境 — 它是在什麼條件下產生的?那些條件還成立嗎?
    2. 把規則轉成可計算的東西 — 不是「最多 3 個」,是「11GB 預算」;不是「寫 3 行 API」,是「運維視角 6 分」
    3. 上升到更對的抽象層 — 從「數量」到「預算」是一次升級,從「頭銜」到「視角」是又一次升級
    4. 同時改 template — 不然新專案會繼承舊錨,規則永遠修不乾淨

    這個流程不只適用 Agent Team 編制,適用任何你「歸納」出來的工程規則。下次你發現自己下意識地拒絕某個選項(「不能這樣做,會爆」),請誠實問自己:這個規則是設計來的,還是創傷來的?如果是後者,它可能正在偷偷殺死你的系統設計,而你根本沒意識到。

  • 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 的差距