處理 tool_use response
response 不是純文字——是 content blocks 陣列。怎麼走 blocks、怎麼包 tool_result、怎麼對 tool_use_id。
TL;DR
- 開了 tool use 之後,response 不是
content[0].text——是一個 block 陣列,可能混text/tool_use/thinking - 接續對話時,要把 assistant 的整段 content 塞回 messages、再附上
tool_resultblock 在新的 user message 裡 stop_reason: "tool_use"是「我要 tool」的信號;tool_use_id對不上 API 直接 422
一個情境:把 .text 當第一個 block
之前用 Claude 寫 chat,習慣 res.content[0].text 拿回應。第一次接 tool use 你照這樣寫:
res = client.messages.create(model=..., tools=tools, messages=messages)
print(res.content[0].text) # ❌ 有時會 AttributeError
時不時噴 AttributeError: 'ToolUseBlock' object has no attribute 'text'。因為當 Claude 決定要用 tool 的時候,第一個 block 可能直接是 tool_use,根本沒 text。
開了 tool use,content list 結構就變了。它從「一定只有一個 text block」變成「可能有多個各式各樣的 block」。
Content block 有哪些
type | 來源 | 內容 |
|---|---|---|
text | model 生成的 | 給 user 看的文字(model 在 tool use 前常先說「我來查一下」) |
tool_use | model 生成的 | model 想呼叫的 tool 名 + input |
thinking | model 生成的 | extended thinking 的推理過程(要 opt in) |
tool_result | 你 server 寫的 | 跑完 tool 的結果(放在 user message 裡) |
image / document | 你 server 寫的 | 多模態輸入(user message) |
「block」基本上就是「一段帶 type 的內容」。Multi-block message 是 tool use 後的常態。
一個 tool_use response 長這樣
{
"id": "msg_01XF...",
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{
"type": "text",
"text": "我來查一下舊金山現在的時間。"
},
{
"type": "tool_use",
"id": "toolu_01A0...",
"name": "get_current_datetime",
"input": { "date_format": "%H:%M:%S" }
}
]
}
要存 conversation history,整段 content 都要塞回去,不要只挑 text 或只挑 tool_use:
messages.append({"role": "assistant", "content": res.content})
把這個 list 當不可變的封包看待。少塞 block 後續 Claude 會不認得自己上一輪寫了什麼。
Stop reason 對照
stop_reason 是 model「為什麼停下來」的 signal。檢查它比掃 content list 找 tool_use 簡單得多:
stop_reason | 意思 | 你要做什麼 |
|---|---|---|
end_turn | model 講完了 | 把 final 的 text 顯示給 user |
tool_use | model 想呼叫 tool | 跑 tool、塞 tool_result、再 call 一次 API |
max_tokens | 撞到 max_tokens 上限 | 提高上限重 call、或當作截斷處理 |
stop_sequence | 撞到你設的 stop_sequences | 通常是有意的,正常結束 |
pause_turn | 長 task 中段(少見) | 帶整段 messages 重 call 繼續 |
判斷 tool use 用:
if res.stop_reason == "tool_use":
...
比走 content list 找 block.type == "tool_use" 乾淨。
把 tool 跑完再包 tool_result
model 已經告訴你它要哪個 tool、input 是什麼。你 server 真的去跑:
tool_use_block = next(b for b in res.content if b.type == "tool_use")
# 從 input dict 解開呼叫實際 function
result = get_current_datetime(**tool_use_block.input) # "15:04:22"
然後包成 tool_result,放在 user message 裡:
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use_block.id, # 必須對應
"content": str(result), # 一律字串化
"is_error": False, # 預設 False
}],
})
幾個欄位的細節:
| 欄位 | 必填 | 重點 |
|---|---|---|
type | ✅ | 一定是 "tool_result" |
tool_use_id | ✅ | 對應 tool_use.id,對不上 API 直接 422 報錯 |
content | ✅ | 字串或 list of blocks(要回 image 也行) |
is_error | ✗ | tool 執行失敗設 True,model 會看到並可能改參數重試 |
完整一輪:Python in、Python out
把上面湊起來:
import anthropic
from datetime import datetime
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
def get_current_datetime(date_format="%H:%M:%S"):
return datetime.now().strftime(date_format)
tools = [{
"name": "get_current_datetime",
"description": "Returns the current time in HH:MM:SS format.",
"input_schema": {
"type": "object",
"properties": {
"date_format": {"type": "string"}
},
"required": [],
},
}]
messages = [{"role": "user", "content": "現在幾點?"}]
# Round 1: model 決定要 tool
res = client.messages.create(
model=MODEL, max_tokens=512, tools=tools, messages=messages
)
assert res.stop_reason == "tool_use"
messages.append({"role": "assistant", "content": res.content})
# 跑 tool + 包結果
for block in res.content:
if block.type == "tool_use":
result = get_current_datetime(**block.input)
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
}],
})
# Round 2: model 看 tool result 給最終回答
final = client.messages.create(
model=MODEL, max_tokens=512, tools=tools, messages=messages
)
print(final.content[0].text) # "現在是 15:04:22。"
注意 round 2 也要帶 tools=tools——model 需要 schema 解讀對話歷史裡的 tool block,不帶會報錯。
抽 text 的小 helper
只想拿給 user 看的文字?篩 text block:
def text_from_message(msg):
return "\n".join(b.text for b in msg.content if b.type == "text")
multi-block message 的 final response 可能還是只有 text block,這個 helper 通用。
Tool 失敗的時候
不要丟 exception 出去——把 error 包進 tool_result 餵回 model,它會看 error message 試著改參數重來:
try:
result = run_tool(block.name, block.input)
tr = {"type": "tool_result", "tool_use_id": block.id,
"content": str(result), "is_error": False}
except Exception as e:
tr = {"type": "tool_result", "tool_use_id": block.id,
"content": f"Error: {e}", "is_error": True}
messages.append({"role": "user", "content": [tr]})
這個小細節讓 Claude 變得自我修復:傳錯參數會看 error 自己 retry,不需要你寫 retry logic。
接下來
到這邊你會處理「一輪 tool use」。但現實常常一個 user query 要好幾輪 tool(先查時間、再算日期、再寫到 reminder)。下一篇把這個流程包成 agent loop——一個 while loop 直到 stop_reason != "tool_use"、再加上 max_iterations 防止無窮自呼。

