Applied AI School
v0 · 規劃中
Anthropic

處理 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_result block 在新的 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來源內容
textmodel 生成的給 user 看的文字(model 在 tool use 前常先說「我來查一下」)
tool_usemodel 生成的model 想呼叫的 tool 名 + input
thinkingmodel 生成的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_turnmodel 講完了把 final 的 text 顯示給 user
tool_usemodel 想呼叫 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_errortool 執行失敗設 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 防止無窮自呼。