標籤: BitNet

  • 本地 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)才能真正發揮。