跳到主要內容

Qwen3.8-27B 本機 Agent 驗收教學:Reasoning Effort、Tool Call 與 Runtime 6 關協定

最後更新: ·
Qwen3.8-27B 本機 Agent 相容性驗收教學首圖

你把 Qwen3.8-27B 裝進本機、聊天也答得像模像樣,接上 Agent 後卻可能遇到另一個世界:reasoning_effort 看似送出了,實際套用哪一檔不清楚;第一輪工具呼叫成功,第二輪一重播歷史就壞掉。這也是 Qwen3.8-27B 本機 Agent 真正該先驗收的地方。

這篇不重寫下載與安裝教學,而是交付一套可反覆執行的相容性驗收協定:固定模型、Prompt、工具 schema 與評分規則,再由讀者分別跑 llama.cpp、vLLM/OpenAI-compatible endpoint,填回關閉思考、low、medium、xhigh 四種模式的結果。讀完後,你會知道如何產生「哪個組合真的能完成兩輪 Agent 迴圈」的驗收表,而不是把未跑過的組合寫成成績。

先說清楚驗證範圍:截至 2026 年 8 月 18 日,本次 AlphaLab 執行環境是 Apple M4、16 GiB 統一記憶體,沒有安裝或執行 llama.cpp、vLLM,也沒有載入 Qwen3.8-27B 權重。AlphaLab 實際跑過的只有「不載入權重、直接重播官方 chat template」的 JSON-string history 測試;下文的 24-run 矩陣與 16/24/48 GiB 配置都是給讀者重跑的 protocol/候選起點,不是 AlphaLab 的跨 runtime benchmark 結果。

如果你還在決定量化檔與顯存,先看 AlphaLab 的Qwen3.8-27B 模型解讀本機 LLM 顯存指南GGUF 量化指南;本文從「模型已經能啟動」的下一步開始。

先說結論

  • 先驗協定,再測速度。能聊天不代表能正確輸出 tool call、保留 call ID、接回 tool result。
  • off 不是第四種 reasoning_effort。Qwen 官方模型卡列出的 effort 是 low、medium、xhigh;關閉思考要另外設定 enable_thinking=false
  • 同一個「OpenAI-compatible」標籤不保證同一種行為。把原始 request、response、版本與模型 revision 一起存下來,runtime 更新後才有東西可以重播。

Qwen3.8-27B 本機 Agent 的一句話定位

先記住這個式子:

本機 Agent 可用性 = 模型答對 × Runtime 傳對 × Harness 接對

任何一項是 0,整個迴圈就是 0。

模型決定「要不要呼叫工具」;Runtime 把模型輸出轉成 API 的 tool_calls;Harness(負責保存訊息、執行工具、把結果送回模型的控制層)則完成下一輪。排行榜通常只量第一項,本文的相容性套件專門把後兩項照亮。想先理解這三層差異,可接著讀AI Agent Harness 是什麼Harness 四模式實測

Qwen3.8-27B 本機 Agent 可用性三層圖,依序為模型答對、Runtime 傳對、Harness 接對
不要把 Agent 失敗全部怪給模型;先定位是模型、Runtime 還是 Harness。圖/AlphaLab

為什麼先做相容性實驗,而不是先追 tokens/s?

Agent 的失敗常是「格式正確但語意錯」或「第一輪成功、第二輪才爆」。例如 server 回傳一個外觀看似正常的 function.arguments,client 卻把 JSON 字串當成物件;又或者 UI 顯示 xhigh,後端實際沒有把該值送進 chat template。這些錯不一定會變成 HTTP 500,速度曲線也看不出來。

因此正確順序是:protocol correctness → task correctness → latency/throughput。先要求每次執行都通過同一套斷言,再比較延遲與 token。這和把模型排行榜轉成私有 Eval的原則相同:總榜只幫你選候選者,固定任務才負責驗收。

步驟一:把版本與啟動參數釘死

痛點是「昨天能跑、今天不能跑」卻無法重現。解法叫做版本帳本:每一筆結果都綁定 runtime 版本、模型 revision/GGUF 檔案雜湊、量化、GPU、context、KV cache 類型與完整啟動參數。先執行:

llama-server --version
python -c "import vllm; print(vllm.__version__)"
shasum -a 256 /path/to/Qwen3.8-27B-*.gguf

以下以 32K context 建立第一條 baseline,並把目前預設已開啟的 --jinja明寫出來,避免未來版本或既有啟動腳本改變 template 路徑;先不要同時加入 MTP、超長 context 或激進 KV 量化,否則出錯時無法知道是哪個變因:

llama-server \
  --jinja \
  -hf ggml-org/Qwen3.8-27B-GGUF:Q4_K_M \
  --no-mmproj \
  --reasoning-format deepseek \
  -c 32768 \
  --host 127.0.0.1 \
  --port 8001

llama.cpp server 文件(commit 169e4a7)把 Jinja 設為預設,--jinja在此用來固定實驗意圖;文件也建議從 /props檢查載入的 template 是否具備工具能力。換句話說,第一個健康檢查不是問模型「你好嗎」,而是先確認 server 真正載入哪個 template。

vLLM baseline 則依 Qwen 官方 repository指定 Qwen reasoning parser 與 tool parser。下面採官方 FP8 checkpoint、只載語言模型,並把 262K 範例縮到 32K;recipe 的 vram_minimum_gb: 38只是容量 heuristic,不是實測峰值,而且 FP8 還要符合支援的硬體與 kernel。16/24 GiB 路線先跑上一段文字版 llama.cpp/GGUF:

vllm serve Qwen/Qwen3.8-27B-FP8 \
  --host 127.0.0.1 \
  --port 8002 \
  --tensor-parallel-size 1 \
  --max-model-len 32768 \
  --language-model-only \
  --kv-cache-dtype fp8 \
  --reasoning-parser qwen3 \
  --enable-auto-tool-choice \
  --tool-call-parser qwen3_coder

vLLM Qwen3.8-27B recipe(commit 62a146b)列出 min_vllm_version: 0.17.0並安裝 transformers>=5.8.0;0.17 是架構下限,不等於所有 Qwen3.8 功能都已在該 release 驗證,較舊版本也只是未驗證而非已證實不相容。請 pin 官方 qwen38 image/exact build 並記錄 digest。上面兩個 baseline 都綁在 127.0.0.1;要開放到其他機器時,先加入 API key、反向代理或等效存取控制。兩個 endpoint 再用同一個 GET /v1/models、同一個 system prompt、同一組 generation 參數。請把「server 能回 200」與「Agent 測試通過」分成兩欄;前者只是啟動成功。

步驟二:正確拆開 off、low、medium、xhigh

Qwen3.8-27B 官方模型卡目前列出的 reasoning_effortlowmediumxhigh,其中 xhigh 為預設;「off」要用獨立的 enable_thinking=false。因此不要把四個字串塞進同一欄,而要建立兩個維度:

  • Thinking 開關:falsetrue
  • Effort:只有 thinking 開啟時才測 low、medium、xhigh。
  • Budget:若 runtime 另有硬上限,把它當第三個變因;不要拿 token budget 冒充 effort。

原始 HTTP payload 的最小差異可以保存成四個 patch。關閉思考:

{
  "chat_template_kwargs": {
    "enable_thinking": false
  }
}

開啟思考並選 low/medium/xhigh:

{
  "chat_template_kwargs": {
    "enable_thinking": true,
    "reasoning_effort": "low"
  }
}

如果你用 OpenAI SDK,chat_template_kwargs通常要放進 extra_body;若直接送 HTTP,它就是 request body 的欄位。上面把 effort 放在 template kwargs,是為了讓 Qwen 的 xhigh 跨過 vLLM 0.17 的舊版頂層 enum;vLLM 0.27.1 已接受頂層 xhigh,但這要列成另一組 pinned-version transport test。llama.cpp 與 vLLM 的接受欄位不必然同步,所以 adapter 應把「測試模式」翻譯成各 runtime 的 payload,同時把實際送出的 JSON 落盤,不能只記 UI 顯示值。

llama.cpp 這條 baseline 保持預設的 --reasoning auto,再加跑一組 transport test:off 送頂層 "reasoning_effort":"none",其餘三格送 low/medium/xhigh。若你在 server 啟動時硬設 --reasoning onoff,要把它列成另一個變因;截至 2026 年 8 月 18 日,啟動時保存的 enable_thinking與 request override 仍可能互相覆寫,不能把兩種啟動方式的結果混在一起。

每一格不能只留一個 PASS。至少分成五欄:client validation(SDK 是否接受欄位)、server acceptance(HTTP/schema 是否接受)、template applied(有效 rendered prompt 或 server trace 是否真的套用)、behavior(固定任務的成功率/延遲)、accounting(原始 usage 欄位)。HTTP 200、request 裡看得到欄位,或輸出變長,都不能單獨證明 effort 已套用;runtime 沒有暴露 rendered prompt/effective kwargs 時,template-applied 欄應寫「未驗證」,不能硬填 PASS。

在模型 revision 1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0(template SHA-256 c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041)裡,xhigh 會加入較完整的推理指示,low 會加入簡短指示,medium 不額外加入指示;enable_thinking=false才是關閉思考。這是 chat-template prompt steering,不是 reasoning token 的硬配額。

要證明「template applied」,不要從回答長短猜。llama.cpp 用 /apply-template檢查 rendered prompt;vLLM 0.27.1 可用 /v1/chat/completions/render後接 /detokenize,其他 qwen38 build 要先確認是否暴露相同 route,不能從 0.17 架構下限推定。斷言 xhigh/low 的固定指示字串存在、medium 兩者皆無、off 產生空的 <think></think> prefill;在 pinned build 上若 high/minimal/max 沒被 Qwen template 拒絕,也應標成契約漂移。

Qwen 目前預設 preserve_thinking=true。它決定舊 reasoning trace 是否留在多輪歷史,會直接改變 context 成長;請在 24 格 baseline 固定為 true,再用同一段兩輪 fixture 加跑 true/false 的 render test,不要把這個變因混進 effort 成績。

最小矩陣是 2 個 runtime × 4 個模式 × 每格 3 次 = 24 次。每次記錄首 token 時間、總延遲、輸出 token、runtime 回傳的 reasoning usage 原始物件、總 context、tool-call 合格與否、第二輪合格與否、最終任務合格與否。不同 runtime 若沒有回傳同名 reasoning token 欄位,保留原始 response,該格標成不可直接比較,不要補猜一個數字。

Qwen3.8-27B 本機 Agent 六關相容性測試流程,從載入、請求、工具呼叫到第二輪與任務驗收
每一格都過六關,才有資格進入速度比較。圖/AlphaLab

步驟三:用一個固定 Tool Call 測完兩輪

痛點是測試太「聰明」:Prompt 每次不同、工具回傳也不同,最後無法判斷是模型退步還是資料變了。解法是封閉 fixture。工具只接受唯一城市,Prompt 也明確要求先呼叫工具:

{
  "type": "function",
  "function": {
    "name": "lookup_weather",
    "description": "讀取固定測試天氣",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {"type": "string", "enum": ["台北"]}
      },
      "required": ["city"],
      "additionalProperties": false
    }
  }
}

固定 user prompt 是「請查台北天氣。一定要先呼叫 lookup_weather,不可猜。」第一輪收到 response 後,不要急著執行工具,先做六個斷言:

  1. HTTP 200,且 response 能依 OpenAI chat completion 結構解析。
  2. tool_calls只有一筆,名稱精確等於 lookup_weather
  3. function.arguments在 wire 上是字串,而且能通過 json.loads()
  4. 解析後精確等於 {"city":"台北"},沒有額外欄位。
  5. 第一輪的 tool_calls[0].id 是非空字串;下一輪 role 為 tool 的 message 再把同一個值填入 tool_call_id
  6. 第一輪 content 不夾帶未解析的 <tool_call>標記。

通過後再回傳固定結果 {"city":"台北","temperature_c":31,"condition":"午後雷陣雨"}。第二輪必須沿用第一輪 assistant message 與 call ID,再追加 role 為 tool 的結果。最終回答要同時包含「31」與「午後雷陣雨」,而且不能再呼叫一次工具。這一輪才證明 Harness 的歷史重播接得起來。

下面是一格測試的最小可執行骨架;它會把兩輪原始 request/response 印到 stdout。FIXTURE_RESULTS只回傳 allowlist 內的固定資料,不會把模型產生的 arguments 當成指令執行:

import json, os, requests

BASE_URL = os.getenv("BASE_URL", "http://127.0.0.1:8001/v1")
MODEL = os.environ["MODEL"]
MODE = os.getenv("MODE", "off")
RUNTIME = os.getenv("RUNTIME", "llama_cpp")
RUNTIME_MODE_PATCHES = {
  "vllm": {
    "off":{"chat_template_kwargs":{"enable_thinking":False}},
    "low":{"chat_template_kwargs":{"enable_thinking":True,"reasoning_effort":"low"}},
    "medium":{"chat_template_kwargs":{"enable_thinking":True,"reasoning_effort":"medium"}},
    "xhigh":{"chat_template_kwargs":{"enable_thinking":True,"reasoning_effort":"xhigh"}},
  },
  "llama_cpp": {
    "off":{"reasoning_effort":"none"},
    "low":{"reasoning_effort":"low"},
    "medium":{"reasoning_effort":"medium"},
    "xhigh":{"reasoning_effort":"xhigh"},
  },
}
assert RUNTIME in RUNTIME_MODE_PATCHES
assert MODE in RUNTIME_MODE_PATCHES[RUNTIME]
TOOL = {"type":"function","function":{
  "name":"lookup_weather","description":"讀取固定測試天氣",
  "strict":True,
  "parameters":{"type":"object","properties":{
    "city":{"type":"string","enum":["台北"]}},
    "required":["city"],"additionalProperties":False}}}
FIXTURE_RESULTS = {"lookup_weather": {
  "city":"台北","temperature_c":31,"condition":"午後雷陣雨"}}

headers = {"Content-Type":"application/json"}
def complete(messages, tool_choice):
    body = {"model":MODEL,"messages":messages,"tools":[TOOL],
            "tool_choice":tool_choice,"parallel_tool_calls":False,
            "temperature":0,"max_tokens":512}
    body.update(RUNTIME_MODE_PATCHES[RUNTIME][MODE])
    print("RAW REQUEST", json.dumps(body, ensure_ascii=False))
    r = requests.post(f"{BASE_URL}/chat/completions",
                      headers=headers, json=body, timeout=180)
    print("RAW RESPONSE", r.text)
    r.raise_for_status()
    return r.json()

messages = [{"role":"user","content":
             "請查台北天氣。一定要先呼叫 lookup_weather,不可猜。"}]
first = complete(messages, "required")
assistant = first["choices"][0]["message"]
assert first["choices"][0].get("finish_reason") == "tool_calls"
assert "<tool_call>" not in (assistant.get("content") or "")
calls = assistant.get("tool_calls") or []
assert len(calls) == 1
call = calls[0]
assert isinstance(call.get("id"), str) and call["id"]
assert call["function"]["name"] == "lookup_weather"
raw_args = call["function"]["arguments"]
assert isinstance(raw_args, str)
args = json.loads(raw_args)
assert args == {"city":"台北"}
result = FIXTURE_RESULTS[call["function"]["name"]]

tool_message = {"role":"tool","tool_call_id":call["id"],
                "content":json.dumps(result, ensure_ascii=False)}
second = complete(messages + [assistant, tool_message], "none")
final = second["choices"][0]["message"]
assert not final.get("tool_calls")
assert "<tool_call>" not in (final.get("content") or "")
assert "31" in (final.get("content") or "")
assert "午後雷陣雨" in (final.get("content") or "")
print("PASS: two-round tool-call fixture")

把程式存成 fixture.py後,測 llama.cpp 時先執行 export BASE_URL=http://127.0.0.1:8001/v1,再執行 export MODEL="$(curl -s "$BASE_URL/models" | jq -r '.data[0].id')",最後以 RUNTIME=llama_cpp MODE=off python fixture.py跑一格。測 vLLM 時把 endpoint 改成 export BASE_URL=http://127.0.0.1:8002/v1、重新取得 MODEL,並用 RUNTIME=vllm MODE=off python fixture.py。把 mode 換成 low、medium、xhigh,並對每個 runtime 各重跑 3 次、保存 stdout。這一版第一輪用 required、第二輪用 none,專門隔離 transport/parser/history;通過後再另跑 auto + strict,才量模型是否自行選對工具。若 endpoint 需要 Bearer token,另在 headers 加入環境變數,不要把密鑰寫入 fixture。

JSON 字串為什麼會讓 tool history 壞掉?

OpenAI-compatible response 的 function.arguments通常是 JSON 字串;但 template 內部若直接對它做 key/value 迭代,就會把「wire 格式」與「template 需要的物件」混為一談。AlphaLab 在 2026 年 8 月 18 日對 Qwen3.8-27B revision 1d4bf0f 的 chat template做了不載入權重的最小重播:把 arguments 保持為 "{\"city\":\"Paris\"}",Jinja 在迭代 tool_call.arguments|items時拋出 TypeError。這與目前仍開啟的 Qwen issue #1894重現路徑一致。

修法不是永久改寫 OpenAI wire history,而是在「直接呼叫 chat template」的邊界建立副本,僅把已驗證的 arguments 解成物件:

from copy import deepcopy
import json

def template_safe_history(messages):
    safe = deepcopy(messages)
    for message in safe:
        if message.get("role") != "assistant":
            continue
        for call in message.get("tool_calls", []):
            args = call["function"]["arguments"]
            if isinstance(args, str):
                parsed = json.loads(args or "{}")
                if not isinstance(parsed, dict):
                    raise TypeError("function.arguments must decode to an object")
                call["function"]["arguments"] = parsed
    return safe

這段只示範格式正規化;production 還要補 schema 驗證、長度上限、工具 allowlist、timeout、重試與 idempotency。若 runtime 已在 server 邊界修正,就不應在 client 再轉一次。例如 llama.cpp 2026 年 8 月 18 日的 mastervLLM 0.27.1都已在 server 邊界解析完整、有效的 JSON-string arguments;這不等於容忍截斷或格式錯誤的 JSON。把原始 fixture 同時送進 direct-template 與 server 路徑,哪條通過就保留哪條。

步驟四:把六關寫成 regression assertions

一份結果不是回歸測試;能在更新後自動指出哪一關退步,才是。建議每筆 run 依序檢查:

  1. 載入關:模型、tokenizer、chat template 與 tool parser 都成功載入。
  2. 控制關:分開記錄 client validation、server acceptance、template applied、behavior 與 accounting;未知欄位或未暴露 effective kwargs 不能靜默當成通過。
  3. 第一輪協定關:工具名稱、arguments 字串、call ID 與 finish reason 符合預期。
  4. 工具執行關:arguments 經 JSON Schema 驗證後才送進 mock tool。
  5. 歷史關:完整 assistant tool call 與對應 tool result 能被第二輪接受。
  6. 任務關:最後答案使用工具結果,沒有重複呼叫,也沒有裸露 parser 標記。

CSV 至少保留 runtime_versionmodel_revisionquantcontextkv_typemodeseedlatency_msusage_raw、三個 pass/fail 與原始 response 的 SHA-256。reasoning 文字欄也要做 adapter:目前 vLLM Chat 輸出 message.reasoning,llama.cpp 的 deepseek format 輸出 message.reasoning_content;保留兩個原始欄位,再映射成自己的統一欄,不能把缺欄當成沒有推理。若你已有 trace 系統,讓每次 tool call、tool result 與第二輪共用同一個 trace ID;做法可參考Agent Observability 實作

回歸門檻可以非常直白:任何既有通過格變成失敗就擋住 runtime 升級;品質全數通過後,總延遲增加超過你自行設定的容忍值才警告。不要把「更快但 tool call 壞掉」算成性能提升。

本文主套件走 Chat Completions。若你的 Harness 只支援 Responses API,要另做一條完整歷史重播 smoke test,第二輪再次送入 assistant function call 與 tool output,並設 store:false;不要假設 previous_response_id可跨本機 runtime 使用。Responses 與 Chat 是兩份 protocol 契約,不能用一邊通過替另一邊背書。

16/24/48 GiB:先選 artifact,再實測能否載入

這不是顯存實測表。圖中的 16/24/48 GiB 指單一加速器的可用 VRAM 級距,不等於 macOS 統一記憶體或整機 system RAM;共同假設只有文字工具、batch 1、concurrency 1、不載入 vision mmproj、不啟用 MTP。因為 AlphaLab 沒有跑這三組硬體,GPU offload、KV cache 類型、峰值 VRAM/system RAM 與可用 context 都必須留成「待量測」,不能由檔案大小推算 PASS。

Qwen3.8-27B 的 16、24、48 GiB artifact 候選卡,所有 runtime fit 與 context 結果標為待量測
檔案容量只用來選第一個候選;offload、KV、峰值記憶體與可用 context 必須由讀者實跑填回。圖/AlphaLab

截至 2026 年 8 月 18 日,精確 artifact 快照是:Unsloth revision f1bfb127c64f7072bdd2cad55f258b9c8b2910feQwen3.8-27B-Q3_K_M.gguf 為 12.87 GiB;ggml-org revision 0669b98607d47046c7c2b3f801011d54a08cfccfQwen3.8-27B-Q4_K_M.gguf 為 17.67 GiB、Qwen3.8-27B-Q8_0.gguf 為 26.63 GiB、BF16 GGUF 為 50.11 GiB;官方 FP8 revision 017b9c7af6b5689d5dd426a76e0bc077eb5ca20a 的 safetensors 合計約 28.75 GiB。這些都只是下載 artifact 的容量地板,不含 runtime overhead。

  • 16 GiB 候選:Q3_K_M 12.87 GiB;把 16K context 當第一個待測目標,逐筆記錄 offload、KV 類型與峰值記憶體,未跑前不得標成可用。
  • 24 GiB 候選:Q4_K_M 17.67 GiB;把 32K context 當待測目標,而不是「較適合」的已驗證 baseline。
  • 48 GiB 候選:Q8_0 26.63 GiB 或官方 FP8 約 28.75 GiB;兩者是不同格式/runtime 路徑,不做跨 backend 速度推論。FP8 還要由硬體與 kernel 支援,recipe 的 38 GB heuristic 也不是峰值保證。BF16 GGUF 單一檔已達 50.11 GiB,未計 overhead,不能列為 48 GiB 全 GPU 起點。

硬體升級的決策也要回到 anchor primitive:量化讓模型裝得下,只改善「模型能不能載入」;工具協定與歷史重播是否正確,仍要重新驗收。

Qwen3.8-27B 本機 Agent 最常踩的 5 個坑

坑一:把 off 當成 reasoning_effort 值

修法:測試資料模型分成 enable_thinkingreasoning_effort;off 寫成前者為 false,另外三格才填 effort。這會避免 UI 名稱把兩種控制混成一個下拉選單。

坑二:把 effort 與 reasoning budget 當同一件事

修法:effort 是當前 chat template 的 prompt steering;budget 則必須寫出 runtime 的具體欄位與官方語義。像 max_tokensmax_completion_tokens通常限制整段 completion,不能在沒有文件時稱為「只限制 reasoning 的 budget」。固定一個變因再測另一個,並把 server acceptance、template applied、behavior 與 usage 分欄記錄。

坑三:只測第一輪 tool call

修法:fixture 必須真的追加 tool result,再要求模型產生 final answer。第一輪驗 parser,第二輪才驗 history serializer 與 Harness。

坑四:SDK 幫你轉型,raw trace 卻沒留下

修法:測試 harness 同時保存 HTTP body 與 SDK 解析後物件。若兩者不同,你才能知道 JSON 字串是在 server、SDK 還是自己的 adapter 被轉成 mapping。

坑五:用極端 KV 量化追 context,卻沒重跑工具品質

修法:每次改 KV 類型或 context,都重跑相同 24 格。llama.cpp function-calling 文件(commit 169e4a7)明確提醒,極端 KV cache 量化可能傷害 tool calling;context 數字變大不等於 Agent 可用 context 變大。

2026 年 8 月 18 日的 Runtime 狀態怎麼讀?

這篇能直接確認的不是「某 runtime 永遠有 bug」,而是兩個可重跑的當前訊號。第一,官方 Qwen3.8-27B chat template 在 revision 1d4bf0f 對 JSON-string tool history 的 direct-template 最小重播會失敗,issue #1894 仍為 open;這不代表已做 server-boundary 正規化的 llama.cpp master 或 vLLM 0.27.1 也會失敗。第二,llama.cpp 的 PR #27221正在處理 server 啟動時 --reasoning on/off與 request-level override 的優先順序;截至 2026 年 8 月 18 日,它是 open、非 draft、尚未合併。

這兩項都可能很快改變,所以它們應該變成 regression fixture,而不是變成永久結論。每次更新 runtime 或 model template:先跑 fixture、保存 commit/revision、比較通過格,再決定是否移除 adapter。只看 issue 標籤或 PR 標題,不能取代自己的 endpoint 回傳。

同樣地,Simon Willison 的單機實測展示 xhigh 與關閉思考可能帶來很大的時間/輸出差距,但那是一個 prompt、特定機器與特定 build 的觀察。它很適合當「為何要測」的線索,不適合填進你自己的性能欄位。

怎麼選:llama.cpp 還是 vLLM?

  • 選 llama.cpp 起步:你的主要限制是 16/24GB 消費級 GPU、需要 GGUF 量化,願意明確控制 Jinja template 與 KV cache。
  • 選 vLLM 起步:你的 checkpoint 與硬體能容納服務層,且需求是並發、OpenAI-compatible server、reasoning parser 與自動 tool choice。
  • 兩者都測:你準備把本機模型接進長期維護的 Agent 產品。相同 fixture 能把「模型本身」與「runtime 轉譯」分開。

最實用的決策不是先押一個勝者,而是選一個 primary、一個 canary:primary 接日常工作,canary 在 runtime 更新時跑 24 格。只要 canary 連續通過,再交換角色。這比看 release note 猜相容性可靠得多。

Qwen3.8-27B 本機 Agent FAQ

1. Qwen3.8-27B 能塞進 16GB 顯卡嗎?

不能只憑檔案大小下結論。Q3_K_M artifact 為 12.87 GiB,但 16 GiB 顯卡仍要容納 KV、buffer 與 context;本文未在該硬體實跑,短 context 只能列為待測候選,不要把社群的 73K 單機紀錄當保證。

2. off、low、medium、xhigh 是四個 reasoning_effort 嗎?

不是。low、medium、xhigh 是模型卡列出的 effort;off 是 enable_thinking=false。測試結果也應分欄保存。

3. reasoning_effort 越高,Agent 一定越好嗎?

不一定。高 effort 可能改善複雜決策,也會增加 context、延遲與超限風險;真正指標是完成固定任務所需的總時間、重試數與成功率。

4. OpenAI-compatible client 可以讓兩個 runtime 行為一致嗎?

不能靠名稱保證。API 外形可以相似,reasoning 欄位、parser、finish reason 與歷史序列化仍可能不同;這正是 raw HTTP fixture 要測的內容。

5. 為何一定要檢查 arguments 是字串?

因為 wire contract 與 template 內部型別不同。先確認 server 回傳,再在唯一邊界做解析;若任由每層偷偷轉型,第二輪錯誤很難定位。

6. 只跑一次 fixture 夠嗎?

不夠。每格至少重複三次並保存 seed;這不是統計學上的最終 benchmark,而是先抓出偶發 parser/generation 失敗。

7. 模型更新後只需要重測新 checkpoint 嗎?

不夠。chat template、runtime、tool parser、client SDK 與 Harness 都可能改變;版本帳本中的任何一項變動,都應觸發相同 fixture。

8. 這套測試可以直接進 production 嗎?

它是最小驗收,不是完整防線。上線前還要加入惡意 arguments、超長輸入、平行 tool calls、timeout、重試、權限、idempotency 與真實任務資料。

給新手的 5 個重點

  1. 先固定版本、模型檔、context 與 Prompt,再改一個變因。
  2. 把 thinking 開關、effort 與 token budget 分成三欄。
  3. 第一輪驗 tool call,第二輪驗 history 與最終答案。
  4. 保留原始 HTTP,不讓 SDK 的自動轉型蓋掉證據。
  5. 六關全過後才比較 tokens/s、延遲與顯存。

接著閱讀

左右滑動查看更多推薦

最後回到那個式子:模型答對 × Runtime 傳對 × Harness 接對。你可以先用固定天氣工具依本文 protocol 跑完 24 格,把每次 request、response 與版本存下來;等六關全綠,再調長 context、換 KV 量化、加 MTP 或追 tokens/s。這樣得到的才不是一張漂亮跑分,而是一個你敢交給 Agent 使用的本機模型入口。

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

每週最多兩封,收到週報精選與關鍵 Alpha Signal。

我們不會 spam,隨時可退訂。