教學:建構一個使用工具的代理
從單一工具呼叫到可用於正式環境的代理迴圈的引導式逐步教學。
本教學以五個同心環的方式建構一個行事曆管理代理。每一環都是一個完整、可執行的程式,並在前一環的基礎上恰好新增一個概念。到最後,您將親手寫出「agentic loop」(代理迴圈),然後再以 Tool Runner SDK 抽象層取代它。
範例工具是 create_calendar_event。它的 schema 使用了巢狀物件、陣列與選用欄位,因此您將看到 Claude 如何處理貼近實務的輸入結構,而非單一的扁平字串。
第 1 環:單一工具、單一回合
最小可行的工具使用程式:一個工具、一則使用者訊息、一次工具呼叫、一個結果。程式碼附有大量註解,讓您能將每一行對應到工具使用生命週期。
請求會在使用者訊息旁一併送出 tools 陣列。當 Claude 判斷需要進行工具呼叫時,回應會帶著 stop_reason: "tool_use" 以及一個 tool_use 內容區塊返回,其中包含工具名稱、唯一的 id,以及結構化的 input。您的程式碼執行該工具,然後在 tool_result 區塊中將結果送回,其 tool_use_id 與呼叫中的 id 相符。
# 第 1 環:單一工具,單一回合。
import json
import anthropic
# 建立一個用戶端。它會從環境變數讀取 ANTHROPIC_API_KEY。
client = anthropic.Anthropic()
# 定義一個工具。input_schema 是一個 JSON Schema 物件,用於描述
# Claude 呼叫此工具時應傳遞的引數。此 schema
# 包含巢狀物件(recurrence)、陣列(attendees)以及選用
# 欄位,這比扁平的字串引數更接近真實世界的工具。
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
}
]
# 將使用者的請求連同工具定義一起傳送。Claude 會根據
# 請求內容與工具描述來決定是否呼叫該工具。
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",
}
],
)
# 當 Claude 呼叫工具時,回應的 stop_reason 為 "tool_use",
# 且 content 陣列中會包含一個 tool_use 區塊以及任何文字內容。
print(f"stop_reason: {response.stop_reason}")
# 尋找 tool_use 區塊。回應中可能在 tool_use 區塊之前包含文字
# 區塊,因此請掃描 content 陣列,而非假設其位置。
tool_use = next(block for block in response.content if block.type == "tool_use")
print(f"Tool: {tool_use.name}")
print(f"Input: {tool_use.input}")
# 執行該工具。在真實系統中,這會呼叫您的行事曆 API。
# 此處結果為硬編碼,以保持範例的獨立完整性。
result = {"event_id": "evt_123", "status": "created"}
# 將結果回傳。tool_result 區塊需放在 user 訊息中,且其
# tool_use_id 必須與上方 tool_use 區塊的 id 相符。同時
# 納入助理先前的回應,讓 Claude 擁有完整的對話歷史。
followup = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",
},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
}
],
},
],
)
# 取得工具結果後,Claude 會產生最終的自然語言
# 回答,且 stop_reason 變為 "end_turn"。
print(f"stop_reason: {followup.stop_reason}")
final_text = next(block for block in followup.content if block.type == "text")
print(final_text.text)預期結果
stop_reason: tool_use
Tool: create_calendar_event
Input: {'title': 'Sync', 'start': '2026-03-30T10:00:00', 'end': '2026-03-30T10:30:00', 'attendees': ['alice@example.com', 'bob@example.com']}
stop_reason: end_turn
I've scheduled your 30-minute sync with Alice and Bob for Monday, March 30 at 10am.第一個 stop_reason 是 tool_use,因為 Claude 正在等待行事曆的結果。在您送出結果之後,第二個 stop_reason 是 end_turn,而內容則是給使用者的自然語言。
第 2 環:代理迴圈
第 1 環假設 Claude 只會呼叫工具恰好一次。實際任務往往需要多次呼叫:Claude 可能會建立一個活動、讀取確認訊息,然後再建立另一個。解決方法是一個 while 迴圈,持續執行工具並將結果回饋,直到 stop_reason 不再是 "tool_use" 為止。
另一項變更是對話歷史。與其在每次請求時從頭重建 messages 陣列,不如維護一份持續累積的清單並向其附加內容。每一回合都能看到完整的先前上下文。
# Ring 2:代理迴圈(agentic loop)。
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
}
]
def run_tool(name, tool_input):
if name == "create_calendar_event":
return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
return {"error": f"Unknown tool: {name}"}
# 將完整對話歷史保存在清單中,讓每一輪都能看到先前的上下文。
messages = [
{
"role": "user",
"content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks, starting Monday, March 30, 2026. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",
}
]
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
# 持續迴圈直到 Claude 不再要求使用工具。每次迭代會執行所要求的
# 工具,將結果附加到歷史中,並請 Claude 繼續。
while response.stop_reason == "tool_use":
tool_use = next(block for block in response.content if block.type == "tool_use")
result = run_tool(tool_use.name, tool_use.input)
messages.append({"role": "assistant", "content": response.content})
messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
}
],
}
)
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)預期結果
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.迴圈可能執行一次或多次,取決於 Claude 如何拆解任務。您的程式碼不再需要事先知道。
第 3 環:多個工具、平行呼叫
代理很少只有一項能力。新增第二個工具 list_calendar_events,讓 Claude 能在建立新活動之前先檢查現有的行程。
當 Claude 有多個彼此獨立的工具呼叫要進行時,它可能會在單一回應中回傳數個 tool_use 區塊。您的迴圈需要處理所有這些區塊,並在一則使用者訊息中將所有結果一併送回。請遍歷 response.content 中的每一個 tool_use 區塊,而不只是第一個。
# Ring 3:多個工具,平行呼叫。
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
},
{
"name": "list_calendar_events",
"description": "List all calendar events on a given date.",
"input_schema": {
"type": "object",
"properties": {
"date": {"type": "string", "format": "date"},
},
"required": ["date"],
},
},
]
def run_tool(name, tool_input):
if name == "create_calendar_event":
return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
if name == "list_calendar_events":
return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}
return {"error": f"Unknown tool: {name}"}
messages = [
{
"role": "user",
"content": "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
}
]
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
# 單一回應可能包含多個 tool_use 區塊。請處理所有
# 區塊,並在同一則 user 訊息中一併傳回所有結果。
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = run_tool(block.name, block.input)
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
}
)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)預期結果
I checked your calendar for Monday, March 30 and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.關於並行執行與順序保證的更多資訊,請參閱平行工具使用。
第 4 環:錯誤處理
工具會失敗。行事曆 API 可能會拒絕與會者過多的活動,或者日期格式可能有誤。當工具拋出錯誤時,請以 is_error: true 將錯誤訊息送回,而不是讓程式當掉。Claude 會讀取錯誤,並可以用修正後的輸入重試、向使用者要求釐清,或說明該限制。
# Ring 4:錯誤處理。
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
},
{
"name": "list_calendar_events",
"description": "List all calendar events on a given date.",
"input_schema": {
"type": "object",
"properties": {
"date": {"type": "string", "format": "date"},
},
"required": ["date"],
},
},
]
def run_tool(name, tool_input):
if name == "create_calendar_event":
if "attendees" in tool_input and len(tool_input["attendees"]) > 10:
raise ValueError("Too many attendees (max 10)")
return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
if name == "list_calendar_events":
return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}
raise ValueError(f"Unknown tool: {name}")
messages = [
{
"role": "user",
"content": "Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),
}
]
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
try:
result = run_tool(block.name, block.input)
tool_results.append(
{"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}
)
except Exception as exc:
# 回報失敗,讓 Claude 可以重試或要求釐清。
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(exc),
"is_error": True,
}
)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)預期結果
I tried to schedule the all-hands but the calendar only allows 10 attendees per event. I can split this into two sessions, or you can let me know which 10 people to prioritize.is_error 旗標是與成功結果之間唯一的差異。Claude 會看到該旗標與錯誤文字,並據此回應。完整的錯誤處理參考請參閱處理工具呼叫。
第 5 環:Tool Runner SDK 抽象層
第 2 環到第 4 環都是親手撰寫相同的迴圈:呼叫 API、檢查 stop_reason、執行工具、附加結果、重複。Tool Runner 會替您完成這些工作。將每個工具定義為函式,把清單傳給 client.beta.messages.tool_runner(),並在迴圈完成後取得最終訊息。錯誤包裝、結果格式化和對話管理都會在內部處理。
每個 SDK 都提供一個輔助工具,可將一般函式轉換為可執行的工具,並從其簽章推導出輸入 schema;下方的分頁顯示了各語言的慣用寫法。
# Ring 5:Tool Runner SDK 抽象層。
import json
import anthropic
from anthropic import beta_tool
client = anthropic.Anthropic()
@beta_tool
def create_calendar_event(
title: str,
start: str,
end: str,
attendees: list[str] | None = None,
recurrence: dict | None = None,
) -> str:
"""Create a calendar event with attendees and optional recurrence.
Args:
title: Event title.
start: Start time in ISO 8601 format.
end: End time in ISO 8601 format.
attendees: Email addresses to invite.
recurrence: Dict with 'frequency' (daily, weekly, monthly) and 'count'.
"""
if attendees and len(attendees) > 10:
raise ValueError("Too many attendees (max 10)")
return json.dumps({"event_id": "evt_123", "status": "created", "title": title})
@beta_tool
def list_calendar_events(date: str) -> str:
"""List all calendar events on a given date.
Args:
date: Date in YYYY-MM-DD format.
"""
return json.dumps({"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]})
final_message = client.beta.messages.tool_runner(
model="claude-opus-5-5",
max_tokens=1024,
tools=[create_calendar_event, list_calendar_events],
messages=[
{
"role": "user",
"content": "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
}
],
).until_done()
for block in final_message.content:
if block.type == "text":
print(block.text)預期結果
I checked your calendar for Monday, March 30 and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.輸出與第 3 環完全相同。差異在於程式碼:行數大約減半、沒有手動迴圈,而且 schema 與實作放在一起。
您建構了什麼
您從單一寫死的工具呼叫開始,最終完成了一個具備正式環境樣貌的代理,能處理多個工具、平行呼叫與錯誤,然後將這一切收斂到 Tool Runner 中。過程中您看到了工具使用協定的每一個部分:tool_use 區塊、tool_result 區塊、tool_use_id 比對、stop_reason 檢查,以及 is_error 訊號。
後續步驟
Was this page helpful?