Claude Platform Docs
Messages工具

教學:建構一個使用工具的代理

從單一工具呼叫到可用於正式環境的代理迴圈的引導式逐步教學。

本教學以五個同心環的方式建構一個行事曆管理代理。每一環都是一個完整、可執行的程式,並在前一環的基礎上恰好新增一個概念。到最後,您將親手寫出「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)

預期結果

Output
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)

預期結果

Output
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)

預期結果

Output
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)

預期結果

Output
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)

預期結果

Output
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 訊號。

後續步驟

Schema 規格與最佳實務。

完整的 SDK 抽象層參考。

修正常見的工具使用錯誤。

Was this page helpful?