Multi-turn 與多 tool
一輪不夠用——寫一個會 loop 的 agent loop,加 max_iterations 防無窮自呼,多 tool 一輪可能多個 tool_use 要全部跑完再回。
TL;DR
- 一輪 tool use 通常不夠——要寫一個 while loop 直到
stop_reason != "tool_use" - 多 tool 場景:model 一輪可能回多個 tool_use block,要全部跑完再 batch 回
- 設
max_iterations不然 model 跑歪可能無限自呼,燒錢又當機
一個情境:「177 天後是星期幾?幫我設提醒」
User 給你這句。你的 server 上有三個 tool:
get_current_datetime()→ 拿今天add_duration_to_datetime(start, days)→ 加日期set_reminder(date, text)→ 寫到 reminder DB
要把這個 task 完成,Claude 得做三件事連在一起:
- 先呼叫
get_current_datetime拿今天 - 拿到結果後,呼叫
add_duration_to_datetime加 177 天 - 再呼叫
set_reminder寫進去 - 最後回 user:「已設定 6 月 27 日的提醒」
這要四個 API request。每次 Claude 看到上一輪 tool result 才能決定下一步。寫死「一個 request = 一個 tool call」遠遠不夠。
Agent loop 的骨架
把它變一個 while loop。Pseudo-code:
while True:
response = call_claude(messages, tools)
messages.append(assistant=response.content)
if response.stop_reason != "tool_use":
break # ← 結束,回 final text 給 user
tool_results = run_all_tools(response)
messages.append(user=tool_results)
return messages
每次 loop 三件事:
- 問 Claude 下一步是什麼(call API)
- 把 assistant message 寫回 history(包含 text + tool_use block)
- 是 tool_use 就跑、不是就停
跑 tool 的部分要小心一個細節:一輪可能有多個 tool_use block,要全部跑完再一次回。
跑多 tool:filter content blocks
import json
def run_tool(name, tool_input):
if name == "get_current_datetime":
return get_current_datetime(**tool_input)
elif name == "add_duration_to_datetime":
return add_duration_to_datetime(**tool_input)
elif name == "set_reminder":
return set_reminder(**tool_input)
raise ValueError(f"Unknown tool: {name}")
def run_tools(message):
tool_use_blocks = [b for b in message.content if b.type == "tool_use"]
results = []
for block in tool_use_blocks:
try:
output = run_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(output),
"is_error": False,
})
except Exception as e:
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Error: {e}",
"is_error": True,
})
return results
幾個關鍵:
- 用
block.type == "tool_use"篩出 tool 請求(也可能混 text block,要忽略) - 一個 tool 失敗不要 crash 整個 loop——包 try/except 把 error 餵回 model
- 全部結果放在同一個 user message 裡,不要分多個
包進 run_conversation
def run_conversation(messages, tools, max_iterations=10):
for _ in range(max_iterations):
res = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": res.content})
if res.stop_reason != "tool_use":
return messages # 結束
tool_results = run_tools(res)
messages.append({"role": "user", "content": tool_results})
raise RuntimeError("Hit max_iterations without final answer")
一個 user query 進來呼叫一次 run_conversation,內部跑幾輪 model 不知道,對你的 caller 來說就是「問一次拿一次答案」。這個包裝就是 mini agent。
結束條件三選一
實務上 loop 會在三個地方退出:
| 條件 | 怎麼觸發 | 建議 |
|---|---|---|
stop_reason == "end_turn" | model 覺得任務完成 | 正常退出 |
stop_reason == "stop_sequence" | 撞到你設的 stop_sequences(少見) | 視為正常退出 |
max_iterations | 你的硬上限 | 看成 anomaly,log 起來分析 |
max_iterations 不只是保險,更是你的成本防線。每輪都是一個完整 API call、token 線性累加,一輪掉到無窮迴圈帳單會很有感。
REPL 心智模型
把這個 loop 想成 model 跟 tool 的 REPL——每輪 model 看到上一輪的 tool result 才決定下一步:
LLM 是一邊執行一邊規劃的。它不會預先規劃完一次說完,而是看到上一輪的 result 才決定下一步。所以 schema 寫得清楚比想多步驟順序重要。
Agent loop 跟「真正的 agent」差在哪
到這裡你已經寫了一個會 loop、會用多 tool、會處理 error 的小型 agent。
但「真正的 agents」還會做更多事:planning、memory、subagent、long-horizon execution、自動 compaction。這些變數一拉開,loop 結構會更複雜——這個系列的 agents 章節 會專門講 workflow vs agent 的取捨、Anthropic 的 best practice、什麼時候自己刻什麼時候用內建 agent harness。
這篇的 run_conversation 是 agent 的最小骨架,80% 的 production 場景用這個就夠。
接下來
下一篇換個角度——不是所有 tool 都要自己寫。Anthropic 直營了一些 server-side 跑的 tool,像 web_search、text_editor、code_execution。怎麼用、跟自寫 tool 的差別是什麼,下一篇收尾整個 Tool Use section。

