你把模型載入成功,請它查訂單,畫面卻停在一段工具指令。這篇 Nanbeige4.3-33B Agent 教學,帶你把「模型說要做什麼」接成「程式真的執行、結果送回下一輪」的循環。
本文寫給第一次接觸工具呼叫的讀者。先用假客戶、假訂單理解三個工具回合,再看推理歷史與 XML 格式如何交接;已有 Python 基礎,可以複製下方練習程式。還沒準備 GPU,也能完成不載入模型的 Mock 練習。
截至 2026 年 10 月 27 日,本文對照官方 checkpoint 與固定 revision c1093c501cc6460427c0ac634db7223d0450c0ee。AlphaLab 執行了官方 Jinja 模板的合成訊息渲染及假工具循環,未載入 33B 權重、未測模型速度或任務成功率。後面的 GPU 接線與 A/B 是讀者的驗收方法,並非新增跑分。
先說結論:Agent 接得上=格式對+歷史在+結果回得去
把模型當成會填工作單的人,把 Harness 當成收單、派工、簽收的櫃檯。模型產生工具名稱與參數;執行層檢查並執行;工具結果再寫回訊息歷史。少了其中一段,看起來像 Agent,工作卻沒有交完。
- 先選 XML 與對應解析器,把第一個小回合接通,再考慮 JSON 相容路徑。
enable_thinking控制這一輪的推理提示;preserve_thinking控制較早歷史的推理如何進入下一輪,兩者分開記。- 三次工具執行後,還需要一次模型請求整理答案;本練習共四次模型回應,別把「三工具回合」誤讀為三次請求。
若還不熟悉這個櫃檯,可先看 Agent Harness 白話解說。本篇專注 Nanbeige 的模板與交接;模型定位及發布方分數,另見 Nanbeige 教師模型解析。
一、Nanbeige4.3-33B Agent 教學:先固定五個零件
- 「哪份模型」:使用
Nanbeige/Nanbeige4.3-33B,記錄 revision、dtype 與量化檔名,避免把 Base 或社群量化版混成同一測試。 - 「工作單樣式」:Chat template(聊天模板)把角色、工具規格與歷史訊息排成模型能讀的文字;
tools會啟用工具樣式。 - 「翻譯員」:Parser(解析器)把生成的工具指令拆成程式可讀的
tool_calls。 - 「派工員」:Harness 只執行允許的函式,核對參數、順序、回合數與逾時。
- 「工作簿」:Messages 保留 assistant 的工具要求及對應 tool 結果,再送出下一次請求。
依 官方模型卡,本文選用 vLLM 服務路徑:推理解析器為 qwen3,工具解析器為 qwen3_coder。這是該模型卡的接線方式;不要把 Nanbeige4.2 的專屬分支與解析器名稱直接搬來。
容量先看「權重+KV cache+執行空間」。約 330 億參數乘 BF16 的 2 bytes,權重粗估約 66 GB(十進位);這是紙上估算,還沒加入快取。本文主機僅 16 GB 記憶體,因此停在模板與假工具驗證,未下載完整權重。
依 官方 config 的 52 層、8 個 KV heads、head dimension 128,單請求、每個 K/V 元素 2 bytes 的完整快取,估算為 2 × 52 × 8 × 128 × 2 = 212,992 bytes/token,也就是 208 KiB/token。4,096 tokens 約占 0.8125 GiB。它不是實測峰值,也不包含執行器的分塊與額外配置。
上下文像工作桌,權重像整排工具櫃:縮桌面會省快取,工具櫃仍然要放得下。先用 4,096 context、單請求,並預留輸出;量化權重也要另記 KV dtype,別把兩種量化當成同一個開關。更多容量拆法可看 本機 LLM 記憶體指南。
二、兩個 Thinking 開關:現在寫草稿,還是把舊草稿帶回來?
enable_thinking=true 時,模板在待生成的 assistant 開頭放入 <think>;設成 false,則預填空的思考區塊。它控制這輪的提示格式,並不是「保留先前推理」的同義詞。像是問店員這次要不要先寫草稿,與抽屜裡要放哪幾張舊草稿,是兩件事。
官方建議多輪工具與程式 Agent 使用 preserve_thinking=true。但讀 固定版本的聊天模板,還有一個影響測法的細節:明確設 false 時,會清空最後一則真正 user 訊息之前的 assistant 推理;目前這條工具鏈裡的推理仍會留下。模板把工具結果包進的 user 格式,不等於一則新的真正使用者提問。
AlphaLab 用合成字串檢查這個邊界:先放舊使用者問題、帶 OLD_SENTINEL_TEST_ONLY 的 assistant,再加入新的使用者問題;目前工具鏈則放 CURRENT_SENTINEL_1。在 XML 與 JSON 兩種渲染下,false 都移除了舊標記、保留目前標記;true 則兩者保留。這證明此 revision 的模板分支行為,不證明模型會更聰明。

因此,只把同一個問題跑三個工具回合,再比較 true/false,輸入可能沒有你以為的差異。先儲存實際送出的 messages 與模板渲染文字,確認舊推理真的一組存在、一組消失,才值得測結果。開關也無法補回客戶端已丟掉的推理欄位。
在 API 層另要核對欄位名稱。vLLM 推理輸出文件 的現行範例使用 reasoning;本 checkpoint 模板讀取 reasoning_content。下方練習保留原始 response,再做欄位映射;若你的版本已回傳後者,就沿用它。先看真實 JSON,別只看聊天介面。
三、XML 與 JSON:工作單格式必須配對解析器
官方建議 tool_call_format="xml",JSON 則作為相容選項。這是發布方的建議,本文沒有測得 XML 比 JSON 高出多少成功率。這裡的 XML 是模型的特定指令語法,例如 <function=find_orders>,不要直接拿通用 XML 文件解析器當成現成工具解析器。
<tool_call>
<function=find_orders>
<parameter=customer>
A
</parameter>
</function>
</tool_call>
這是依模板寫出的示意工作單,不是模型生成紀錄。JSON 模式則把內容改為 {"name":"find_orders","arguments":{"customer":"A"}},外面仍有 <tool_call> 包裝。改模板後也要核對 runtime 的解析設定;本文後續固定 XML,不把同一解析器對兩種格式都能成功當成既定事實。
API 的外層 request 本身仍然用 JSON 傳送。換句話說,「內部模型輸出 XML」與「HTTP API 用 JSON」,可以同時成立。參考 vLLM 工具呼叫文件,Harness 應讀取解析後的 tool_calls,核對函式名、JSON 參數與 call id;純文字提到工具名稱,不能當成執行收據。
四、Nanbeige4.3-33B Agent 教學:三工具回合,先用假資料跑通
把任務縮成一張可人工核對的小帳:假客戶 A 有訂單 A1、A2,金額為 1,200、1,800 cents。整數 cents 是最小貨幣單位,練習中避免小數加總誤差。三個工具依序查 ID、取明細、做加法,最後答案是 3,000 cents;這是設計好的測試答案,不是能力成績。
- 第一回合:
find_orders({"customer":"A"})→{"ids":["A1","A2"]}。把 assistant 要求與 tool 結果一起留在 messages。 - 第二回合:
order_details({"ids":["A1","A2"]})→ A1 為 1,200、A2 為 1,800 cents。核對 ID 沒掉、順序與任務契約一致。 - 第三回合:
sum_cents({"values":[1200,1800]})→{"total_cents":3000}。再送第四次模型請求,才整理成最後回答。
以下存成 agent_loop.py。它刻意只接受這一組假資料、一輪一個工具、最多三次工具與四次請求;收到截斷或不合契約的參數就停止。這是方便新手讀懂交接的窄範圍練習,沒有加入串流、通用 schema 驗證、並行工具或 API 身分驗證;需要擴充時,先看 vLLM API 文件,逐項補上後再驗收。
import argparse, json, time
from pathlib import Path
from urllib.request import Request, urlopen
TOOLS = [
('find_orders', '查詢假客戶的訂單', {'customer': {'type': 'string'}}),
('order_details', '讀取假訂單明細', {'ids': {'type': 'array', 'items': {'type': 'string'}}}),
('sum_cents', '加總整數 cents', {'values': {'type': 'array', 'items': {'type': 'integer'}}}),
]
TOOLS = [{'type': 'function', 'function': {'name': n, 'description': d,
'parameters': {'type': 'object', 'properties': p,
'required': list(p), 'additionalProperties': False}}}
for n, d, p in TOOLS]
def execute(name, args):
if name == 'find_orders' and args == {'customer': 'A'}:
return {'ids': ['A1', 'A2']}
if name == 'order_details' and args == {'ids': ['A1', 'A2']}:
return {'rows': [{'id': 'A1', 'cents': 1200}, {'id': 'A2', 'cents': 1800}]}
if name == 'sum_cents' and set(args) == {'values'}:
values = args['values']
if type(values) is list and values == [1200, 1800] and all(type(x) is int for x in values):
return {'total_cents': sum(values)}
raise ValueError('工具或參數不在這個練習的允許範圍')
def mock_message(i):
calls = [('find_orders', {'customer': 'A'}),
('order_details', {'ids': ['A1', 'A2']}),
('sum_cents', {'values': [1200, 1800]})]
if i == 3:
return {'role': 'assistant', 'content': '合計 3000 cents。'}
n, a = calls[i]
return {'role': 'assistant', 'content': None, 'reasoning_content': 'MOCK_ONLY',
'tool_calls': [{'id': 'call_' + str(i), 'type': 'function',
'function': {'name': n, 'arguments': json.dumps(a)}}]}
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--mock', action='store_true')
ap.add_argument('--preserve', choices=['true', 'false'], default='true')
ap.add_argument('--out', default='trace.json')
opt = ap.parse_args()
messages = [{'role': 'user', 'content': '依序查假客戶 A 的訂單、取得明細、加總 cents。完成後只答:合計 3000 cents。'}]
records, completed = [], False
start = time.monotonic()
for i in range(4):
if time.monotonic() - start > 300:
raise TimeoutError('超過本練習總時間限制')
body = {'model': 'Nanbeige/Nanbeige4.3-33B', 'messages': messages,
'tools': TOOLS, 'tool_choice': 'auto', 'stream': False,
'temperature': 1.0, 'top_p': 0.95, 'max_tokens': 1024,
'chat_template_kwargs': {'enable_thinking': True,
'preserve_thinking': opt.preserve == 'true', 'tool_call_format': 'xml'}}
# Deep-copy the sent request before appending the next messages.
record = {'request': json.loads(json.dumps(body)), 'mock': opt.mock}
if opt.mock:
response = {'choices': [{'message': mock_message(i), 'finish_reason': 'stop' if i == 3 else 'tool_calls'}]}
else:
req = Request('http://127.0.0.1:8000/v1/chat/completions',
data=json.dumps(body).encode(), headers={'Content-Type': 'application/json'})
with urlopen(req, timeout=60) as r:
response = json.load(r)
record['response'] = response
records.append(record)
Path(opt.out).write_text(json.dumps(records, ensure_ascii=False, indent=2))
choice = response['choices'][0]
if choice.get('finish_reason') == 'length':
raise RuntimeError('輸出截斷,先查預算,不執行半截工具')
msg = dict(choice['message'])
if msg.get('reasoning') is not None and not msg.get('reasoning_content'):
msg['reasoning_content'] = msg['reasoning']
calls = msg.get('tool_calls') or []
if not calls:
if i != 3 or msg.get('content') != '合計 3000 cents。':
raise RuntimeError('未通過本練習的精確完成條件')
print(msg['content']); completed = True; break
if len(calls) != 1 or i == 3:
raise RuntimeError('本練習每輪只允許一個工具,最多三次')
msg = {k: msg[k] for k in ['role', 'content', 'reasoning_content', 'tool_calls'] if k in msg}
messages.append(msg)
call = calls[0]
if call['function']['name'] != ['find_orders', 'order_details', 'sum_cents'][i]:
raise RuntimeError('工具順序不符合練習契約')
args = json.loads(call['function']['arguments'])
result = execute(call['function']['name'], args)
messages.append({'role': 'tool', 'tool_call_id': call['id'],
'content': json.dumps(result, ensure_ascii=False)})
if not completed:
raise RuntimeError('迴圈已達上限')
if __name__ == '__main__':
main()
先在自己的練習資料夾執行 python3 agent_loop.py --mock --out mock-trace.json。這個模式不連模型,使用預先寫好的 assistant 工作單。AlphaLab 已跑通這條假工具循環,得到 合計 3000 cents。,並保存四次 request/response。它檢查收單與回單接線,沒有測到模型的工具選擇能力。
打開 trace,依序找出每個 tool_calls[].id 與下一筆 tool_call_id 是否相同;第二次 request 應已包含第一次 assistant 與 tool 訊息。Transformers 工具使用說明 也把「模型要求呼叫」「應用程式執行」「結果加回對話」拆成不同步驟。這份工作簿才是排錯起點。
五、接真 Runtime,再做有意義的 A/B
已有足夠 NVIDIA GPU 記憶體與 vLLM 環境的讀者,可依模型卡的四路 tensor parallel(把權重分配到四張卡)路徑開始。下例加上本機綁定與短 context;不是單卡或一般筆電的通用啟動命令。先執行 vllm --version 及 vllm serve --help,記下版本並確認參數存在,模型下載與容量都要預先準備。
vllm serve Nanbeige/Nanbeige4.3-33B \
--revision c1093c501cc6460427c0ac634db7223d0450c0ee \
--host 127.0.0.1 --port 8000 \
--tensor-parallel-size 4 --gpu-memory-utilization 0.8 \
--max-model-len 4096 --max-num-seqs 1 \
--enable-auto-tool-choice \
--reasoning-parser qwen3 --tool-call-parser qwen3_coder
等服務啟動後,在另一個終端執行 python3 agent_loop.py --preserve true --out live-trace.json。程式會對本機服務發出真正推論請求;第一次先看它是否收到結構化 tool_calls,再查下一輪歷史。練習要求最後回覆精確相等,措辭不同也會判失敗,這是刻意收窄的輸出契約,不是通用答案品質評分器。
本文把每次輸出上限設為 1,024 tokens,作為小額接線預算;它比官方 Agent 範例預算保守,可能在推理還沒結束時截斷。收到 finish_reason=length 就記為預算不足、停止工具,不把半截文字修成有效工作單。要增加預算時,輸入、歷史與預留輸出合計仍須放得進 4,096 context。
真正比較 preserve 設定時,別只改命令列重跑單一 user 問題。先在 messages 放入一個已完成的先前 user/assistant 回合,再接這次查帳問題;確認兩份模板文字的舊推理確實不同。固定 checkpoint、量化、工具、題目、取樣、context 與輸出預算,才只切換 preserve。範例的三工具順序與已知答案適合驗接線;要測模型能力,另備未提示答案的多組題目。
- 完成率:分母包含逾時、截斷與格式失敗;分子要求工具收據和最終內容都通過。
- Token 與時間:記錄每次 API 的 usage(若該版本提供)、整件任務的總等待時間、首輪與後輪分開;Mock 不填模型 token 或速度。
- 格式與停止:分別記無 tool_calls、參數不符、順序錯、id 遺漏、逾時與超過回合上限。這些錯誤先修 Harness,再談模型輸贏。
推理保留可能增加歷史長度;KV cache 的空間與速度取捨,可對照 官方快取文件。評分欄位則可延伸 本機 LLM 測量評分卡。本文不預填 true 一定提高多少品質,也不把發布方榜單當作量化部署後的保證。
六、常見問題:八個先講答案的判讀
關掉 enable_thinking,就等於刪掉舊推理?
不是。前者改這輪的生成提示,舊回合如何序列化由 preserve 與訊息內容一起決定。
preserve=false 會清掉每次工具呼叫前的推理?
不會一概清掉。本文固定模板只對最後真正 user 訊息之前的 assistant 推理套用清空分支;目前工具鏈仍保留。
JSON 模式就是把 HTTP request 換成另一種格式?
不是。HTTP API 仍可用 JSON;改的是模板要求模型生成的工具工作單。
XML 工作單印出來就算成功?
不算。要取得結構化 tool_calls、通過參數檢查、執行假工具,並把結果送回下一輪。
三工具回合就是三次模型請求?
本練習不是。三次工具要求加一次最終整理,共四次模型回應。
Mock 通過,能宣稱模型工具能力通過嗎?
不能。Mock 的工作單由程式預先提供,只驗證應用程式的收發與停止流程;模型接線要另跑 live-trace。
把 context 開到 512K 就比較好?
不一定。那是官方宣告的長度上限,快取、權重與實際工作預算仍要一起計算;先從短 context 排錯。
可以把這組工具改成付款或真實資料庫?
先保持假資料。這份程式只允許固定函式及參數;真系統要另處理權限、資料範圍、重試去重與執行紀錄,不能把模型輸出直接交給任意命令。
給新手的三個重點
- 先看工作簿:實際 request 比開關名稱更可靠;每次要求與結果都要成對留下。
- 分開驗兩條線:模板測試確認送出的字,Mock 確認程式派工;真推論才能檢查模型行為。
- 每次只改一件事:先 XML 接通、再測跨 user 的推理保留,最後才擴 context、量化或並行。
接著閱讀
左右滑動查看更多推薦
下一步:先讓一張工作單找到它的回單
今天先把 Mock 跑通,打開第二次 request,親眼找出第一次 assistant 要求與 tool 回單。記住「格式對+歷史在+結果回得去」,就能逐段縮小問題,再接上已準備好的模型服務。想把這個小櫃檯擴成日常工作流,可接著逛 AI 專區,或到 AlphaLab 課程繼續練習;先把一件小事做成可追查的循環。






