教程:构建使用工具的 agent
从单个工具调用到生产就绪的 agentic 循环的引导式演练。
本教程通过五个同心环构建一个日历管理 agent。每个环都是一个完整的、可运行的程序,恰好向前一个环添加一个概念。到最后,你将手动编写 agentic 循环,然后用工具运行器 SDK 抽象替换它。
示例工具是 create_calendar_event。它的 schema 使用嵌套对象、数组和可选字段,因此你将看到 Claude 如何处理真实的输入形状,而不是单个扁平字符串。
每个环都可以独立运行。将任何环复制到一个新文件中,它将在没有早期环代码的情况下执行。
环 1:单个工具,单个回合
最小的可能工具使用程序:一个工具,一条用户消息,一次工具调用,一个结果。代码有大量注释,以便你可以将每一行映射到工具使用生命周期。
请求在用户消息旁边发送一个 tools 数组。当 Claude 决定调用工具时,响应返回 stop_reason: "tool_use" 和一个 tool_use 内容块,其中包含工具名称、唯一的 id 和结构化的 input。你的代码执行工具,然后在 tool_result 块中发送回结果,其 tool_use_id 与调用中的 id 匹配。
#!/bin/bash
# Ring 1: Single tool, single turn.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
# Define one tool as a JSON fragment. The input_schema is a JSON Schema
# object describing the arguments Claude should pass when it calls this
# tool. This schema includes nested objects (recurrence), arrays
# (attendees), and optional fields, which is closer to real-world tools
# than a flat string argument.
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"]
}
}
]'
USER_MSG="Schedule a 30-minute sync with alice@example.com and bob@example.com next Monday at 10am."
# Send the user's request along with the tool definition. Claude decides
# whether to call the tool based on the request and the tool description.
RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$(jq -n \
--argjson tools "$TOOLS" \
--arg msg "$USER_MSG" \
'{
model: "claude-opus-4-6",
max_tokens: 1024,
tools: $tools,
tool_choice: {type: "auto", disable_parallel_tool_use: true},
messages: [{role: "user", content: $msg}]
}')")
# When Claude calls a tool, the response has stop_reason "tool_use"
# and the content array contains a tool_use block alongside any text.
echo "stop_reason: $(echo "$RESPONSE" | jq -r '.stop_reason')"
# Find the tool_use block. A response may contain text blocks before the
# tool_use block, so filter by type rather than assuming position.
TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")')
TOOL_USE_ID=$(echo "$TOOL_USE" | jq -r '.id')
echo "Tool: $(echo "$TOOL_USE" | jq -r '.name')"
echo "Input: $(echo "$TOOL_USE" | jq -c '.input')"
# Execute the tool. In a real system this would call your calendar API.
# Here the result is hardcoded to keep the example self-contained.
RESULT='{"event_id": "evt_123", "status": "created"}'
# Send the result back. The tool_result block goes in a user message and
# its tool_use_id must match the id from the tool_use block above. The
# assistant's previous response is included so Claude has the full history.
ASSISTANT_CONTENT=$(echo "$RESPONSE" | jq '.content')
FOLLOWUP=$(curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$(jq -n \
--argjson tools "$TOOLS" \
--arg msg "$USER_MSG" \
--argjson assistant "$ASSISTANT_CONTENT" \
--arg tool_use_id "$TOOL_USE_ID" \
--arg result "$RESULT" \
'{
model: "claude-opus-4-6",
max_tokens: 1024,
tools: $tools,
tool_choice: {type: "auto", disable_parallel_tool_use: true},
messages: [
{role: "user", content: $msg},
{role: "assistant", content: $assistant},
{role: "user", content: [
{type: "tool_result", tool_use_id: $tool_use_id, content: $result}
]}
]
}')")
# With the tool result in hand, Claude produces a final natural-language
# answer and stop_reason becomes "end_turn".
echo "stop_reason: $(echo "$FOLLOWUP" | jq -r '.stop_reason')"
echo "$FOLLOWUP" | jq -r '.content[] | select(.type == "text") | .text'
#!/usr/bin/env bash
# Ring 1: Single tool, single turn.
# Uses jq for cross-turn message-array state — building an agentic loop in shell
# requires JSON manipulation beyond ant's single-call --transform scope.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
set -euo pipefail
USER_MSG="Schedule a 30-minute sync with alice@example.com and bob@example.com next Monday at 10am."
MESSAGES=$(jq -n --arg msg "$USER_MSG" '[{role: "user", content: $msg}]')
# Define one tool. The input_schema is a JSON Schema object describing
# the arguments Claude should pass when it calls this tool. This schema
# includes nested objects (recurrence), arrays (attendees), and optional
# fields, which is closer to real-world tools than a flat string argument.
call_api() {
# ant reads the request body as YAML on stdin: no auth headers, no
# hand-built JSON envelope. The static keys (model, tools, tool_choice)
# live in a quoted heredoc; the growing messages array is appended as
# JSON, which YAML accepts as flow syntax.
{
cat <<'YAML'
model: claude-opus-4-6
max_tokens: 1024
tool_choice: {type: auto, disable_parallel_tool_use: true}
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]
YAML
printf 'messages: %s\n' "$MESSAGES"
} | ant messages create --format json
}
# Send the user's request along with the tool definition. Claude decides
# whether to call the tool based on the request and the tool description.
RESPONSE=$(call_api)
# When Claude calls a tool, the response has stop_reason "tool_use"
# and the content array contains a tool_use block alongside any text.
echo "stop_reason: $(jq -r '.stop_reason' <<<"$RESPONSE")"
# Find the tool_use block. A response may contain text blocks before the
# tool_use block, so filter by type rather than assuming position.
TOOL_USE=$(jq '.content[] | select(.type == "tool_use")' <<<"$RESPONSE")
TOOL_USE_ID=$(jq -r '.id' <<<"$TOOL_USE")
echo "Tool: $(jq -r '.name' <<<"$TOOL_USE")"
echo "Input: $(jq -c '.input' <<<"$TOOL_USE")"
# Execute the tool. In a real system this would call your calendar API.
# Here the result is hardcoded to keep the example self-contained.
RESULT='{"event_id": "evt_123", "status": "created"}'
# Send the result back. The tool_result block goes in a user message and
# its tool_use_id must match the id from the tool_use block above. The
# assistant's previous response is included so Claude has the full history.
MESSAGES=$(jq \
--argjson assistant "$(jq '.content' <<<"$RESPONSE")" \
--arg tool_use_id "$TOOL_USE_ID" \
--arg result "$RESULT" \
'. + [
{role: "assistant", content: $assistant},
{role: "user", content: [
{type: "tool_result", tool_use_id: $tool_use_id, content: $result}
]}
]' <<<"$MESSAGES")
FOLLOWUP=$(call_api)
# With the tool result in hand, Claude produces a final natural-language
# answer and stop_reason becomes "end_turn".
echo "stop_reason: $(jq -r '.stop_reason' <<<"$FOLLOWUP")"
jq -r '.content[] | select(.type == "text") | .text' <<<"$FOLLOWUP"
# Ring 1: Single tool, single turn.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
import json
import anthropic
# Create a client. It reads ANTHROPIC_API_KEY from the environment.
client = anthropic.Anthropic()
# Define one tool. The input_schema is a JSON Schema object describing
# the arguments Claude should pass when it calls this tool. This schema
# includes nested objects (recurrence), arrays (attendees), and optional
# fields, which is closer to real-world tools than a flat string argument.
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"],
},
}
]
# Send the user's request along with the tool definition. Claude decides
# whether to call the tool based on the request and the tool description.
response = client.messages.create(
model="claude-opus-4-6",
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 next Monday at 10am.",
}
],
)
# When Claude calls a tool, the response has stop_reason "tool_use"
# and the content array contains a tool_use block alongside any text.
print(f"stop_reason: {response.stop_reason}")
# Find the tool_use block. A response may contain text blocks before the
# tool_use block, so scan the content array rather than assuming position.
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}")
# Execute the tool. In a real system this would call your calendar API.
# Here the result is hardcoded to keep the example self-contained.
result = {"event_id": "evt_123", "status": "created"}
# Send the result back. The tool_result block goes in a user message and
# its tool_use_id must match the id from the tool_use block above. The
# assistant's previous response is included so Claude has the full history.
followup = client.messages.create(
model="claude-opus-4-6",
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 next Monday at 10am.",
},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
}
],
},
],
)
# With the tool result in hand, Claude produces a final natural-language
# answer and stop_reason becomes "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)
// Ring 1: Single tool, single turn.
// Source for <CodeSource> in build-a-tool-using-agent.mdx.
import Anthropic from "@anthropic-ai/sdk";
// Create a client. It reads ANTHROPIC_API_KEY from the environment.
const client = new Anthropic();
// Define one tool. The input_schema is a JSON Schema object describing
// the arguments Claude should pass when it calls this tool. This schema
// includes nested objects (recurrence), arrays (attendees), and optional
// fields, which is closer to real-world tools than a flat string argument.
const tools: Anthropic.Tool[] = [
{
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"],
},
},
];
// Send the user's request along with the tool definition. Claude decides
// whether to call the tool based on the request and the tool description.
const response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
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 next Monday at 10am.",
},
],
});
// When Claude calls a tool, the response has stop_reason "tool_use"
// and the content array contains a tool_use block alongside any text.
console.log(`stop_reason: ${response.stop_reason}`);
// Find the tool_use block. A response may contain text blocks before the
// tool_use block, so scan the content array rather than assuming position.
const toolUse = response.content.find(
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use",
)!;
console.log(`Tool: ${toolUse.name}`);
console.log(`Input: ${JSON.stringify(toolUse.input)}`);
// Execute the tool. In a real system this would call your calendar API.
// Here the result is hardcoded to keep the example self-contained.
const result = { event_id: "evt_123", status: "created" };
// Send the result back. The tool_result block goes in a user message and
// its tool_use_id must match the id from the tool_use block above. The
// assistant's previous response is included so Claude has the full history.
const followup = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
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 next Monday at 10am.",
},
{ role: "assistant", content: response.content },
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: toolUse.id,
content: JSON.stringify(result),
},
],
},
],
});
// With the tool result in hand, Claude produces a final natural-language
// answer and stop_reason becomes "end_turn".
console.log(`stop_reason: ${followup.stop_reason}`);
for (const block of followup.content) {
if (block.type === "text") {
console.log(block.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 next Monday at 10am.
第一个 stop_reason 是 tool_use,因为 Claude 正在等待日历结果。你发送结果后,第二个 stop_reason 是 end_turn,内容是面向用户的自然语言。
环 2:Agentic 循环
环 1 假设 Claude 只会调用工具一次。实际任务通常需要多次调用:Claude 可能会创建一个事件,读取确认,然后创建另一个。解决方案是一个 while 循环,持续运行工具并反馈结果,直到 stop_reason 不再是 "tool_use"。
另一个变化是对话历史。与其在每个请求中从头重建 messages 数组,不如维护一个运行列表并不断追加。每个回合都能看到完整的先前上下文。
#!/bin/bash
# Ring 2: The agentic loop.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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"]
}
}
]'
run_tool() {
local name="$1"
local input="$2"
if [ "$name" = "create_calendar_event" ]; then
local title=$(echo "$input" | jq -r '.title')
jq -n --arg title "$title" '{event_id: "evt_123", status: "created", title: $title}'
else
echo "{\"error\": \"Unknown tool: $name\"}"
fi
}
# Keep the full conversation history in a JSON array so each turn sees prior context.
MESSAGES='[{"role": "user", "content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com."}]'
call_api() {
curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \
'{model: "claude-opus-4-6", max_tokens: 1024, tools: $tools, tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: $messages}')"
}
RESPONSE=$(call_api)
# Loop until Claude stops asking for tools. Each iteration runs the requested
# tool, appends the result to history, and asks Claude to continue.
while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")')
TOOL_NAME=$(echo "$TOOL_USE" | jq -r '.name')
TOOL_INPUT=$(echo "$TOOL_USE" | jq -c '.input')
TOOL_USE_ID=$(echo "$TOOL_USE" | jq -r '.id')
RESULT=$(run_tool "$TOOL_NAME" "$TOOL_INPUT")
ASSISTANT_CONTENT=$(echo "$RESPONSE" | jq '.content')
MESSAGES=$(echo "$MESSAGES" | jq \
--argjson assistant "$ASSISTANT_CONTENT" \
--arg tool_use_id "$TOOL_USE_ID" \
--arg result "$RESULT" \
'. + [
{role: "assistant", content: $assistant},
{role: "user", content: [{type: "tool_result", tool_use_id: $tool_use_id, content: $result}]}
]')
RESPONSE=$(call_api)
done
echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'
# Ring 2: The agentic loop.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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}"}
# Keep the full conversation history in a list so each turn sees prior context.
messages = [
{
"role": "user",
"content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",
}
]
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
# Loop until Claude stops asking for tools. Each iteration runs the requested
# tool, appends the result to history, and asks Claude to continue.
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-4-6",
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)
// Ring 2: The agentic loop.
// Source for <CodeSource> in build-a-tool-using-agent.mdx.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
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"],
},
},
];
function runTool(name: string, input: Record<string, unknown>) {
if (name === "create_calendar_event") {
return { event_id: "evt_123", status: "created", title: input.title };
}
return { error: `Unknown tool: ${name}` };
}
// Keep the full conversation history so each turn sees prior context.
const messages: Anthropic.MessageParam[] = [
{
role: "user",
content:
"Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",
},
];
let response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
tools,
tool_choice: { type: "auto", disable_parallel_tool_use: true },
messages,
});
// Loop until Claude stops asking for tools. Each iteration runs the requested
// tool, appends the result to history, and asks Claude to continue.
while (response.stop_reason === "tool_use") {
const toolUse = response.content.find(
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use",
)!;
const result = runTool(toolUse.name, toolUse.input as Record<string, unknown>);
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: toolUse.id,
content: JSON.stringify(result),
},
],
});
response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
tools,
tool_choice: { type: "auto", disable_parallel_tool_use: true },
messages,
});
}
for (const block of response.content) {
if (block.type === "text") {
console.log(block.text);
}
}
预期输出
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.
循环可能运行一次或多次,取决于 Claude 如何分解任务。你的代码不再需要提前知道。
环 3:多个工具,并行调用
Agent 很少只有一种能力。添加第二个工具 list_calendar_events,这样 Claude 就可以在创建新内容之前检查现有日程。
当 Claude 有多个独立的工具调用时,它可能会在单个响应中返回多个 tool_use 块。你的循环需要处理所有这些块,并在一条用户消息中一起发送回所有结果。遍历 response.content 中的每个 tool_use 块,而不仅仅是第一个。
# Ring 3: Multiple tools, parallel calls.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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 next Monday, then schedule a planning session that avoids any conflicts.",
}
]
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
# A single response can contain multiple tool_use blocks. Process all of
# them and return all results together in one user message.
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-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)
// Ring 3: Multiple tools, parallel calls.
// Source for <CodeSource> in build-a-tool-using-agent.mdx.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
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"],
},
},
];
function runTool(name: string, input: Record<string, unknown>) {
if (name === "create_calendar_event") {
return { event_id: "evt_123", status: "created", title: input.title };
}
if (name === "list_calendar_events") {
return {
events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }],
};
}
return { error: `Unknown tool: ${name}` };
}
const messages: Anthropic.MessageParam[] = [
{
role: "user",
content:
"Check what I have next Monday, then schedule a planning session that avoids any conflicts.",
},
];
let response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
tools,
messages,
});
while (response.stop_reason === "tool_use") {
// A single response can contain multiple tool_use blocks. Process all of
// them and return all results together in one user message.
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const result = runTool(block.name, block.input as Record<string, unknown>);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result),
});
}
}
messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });
response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
tools,
messages,
});
}
for (const block of response.content) {
if (block.type === "text") {
console.log(block.text);
}
}
预期输出
I checked your calendar for next Monday 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: Error handling.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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 an all-hands with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),
}
]
response = client.messages.create(
model="claude-opus-4-6",
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:
# Signal failure so Claude can retry or ask for clarification.
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-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)
// Ring 4: Error handling.
// Source for <CodeSource> in build-a-tool-using-agent.mdx.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
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"],
},
},
];
function runTool(name: string, input: Record<string, unknown>) {
if (name === "create_calendar_event") {
const attendees = input.attendees as string[] | undefined;
if (attendees && attendees.length > 10) {
throw new Error("Too many attendees (max 10)");
}
return { event_id: "evt_123", status: "created", title: input.title };
}
if (name === "list_calendar_events") {
return {
events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }],
};
}
throw new Error(`Unknown tool: ${name}`);
}
const emails = Array.from({ length: 15 }, (_, i) => `user${i}@example.com`);
const messages: Anthropic.MessageParam[] = [
{
role: "user",
content: `Schedule an all-hands with everyone: ${emails.join(", ")}`,
},
];
let response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
tools,
messages,
});
while (response.stop_reason === "tool_use") {
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
try {
const result = runTool(block.name, block.input as Record<string, unknown>);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result),
});
} catch (err) {
// Signal failure so Claude can retry or ask for clarification.
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: String(err),
is_error: true,
});
}
}
}
messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });
response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
tools,
messages,
});
}
for (const block of response.content) {
if (block.type === "text") {
console.log(block.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:工具运行器 SDK 抽象
环 2 到 4 手动编写了相同的循环:调用 API,检查 stop_reason,运行工具,追加结果,重复。工具运行器为你完成这些工作。将每个工具定义为函数,将列表传递给 tool_runner,并在循环完成后检索最终消息。错误包装、结果格式化和对话管理在内部处理。
Python SDK 使用 @beta_tool 装饰器从类型提示和文档字符串推断 schema。TypeScript SDK 使用 betaZodTool 配合 Zod schema。
工具运行器在 Python、TypeScript 和 Ruby SDK 中可用。cURL 和 CLI 选项卡显示注释而不是代码;对于 curl 或 CLI 脚本,请保留环 4 的循环。
# Ring 5: The Tool Runner SDK abstraction.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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-4-6",
max_tokens=1024,
tools=[create_calendar_event, list_calendar_events],
messages=[
{
"role": "user",
"content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",
}
],
).until_done()
for block in final_message.content:
if block.type == "text":
print(block.text)
// Ring 5: The Tool Runner SDK abstraction.
// Source for <CodeSource> in build-a-tool-using-agent.mdx.
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic();
const createCalendarEvent = betaZodTool({
name: "create_calendar_event",
description:
"Create a calendar event with attendees and optional recurrence.",
inputSchema: z.object({
title: z.string(),
start: z.string().datetime(),
end: z.string().datetime(),
attendees: z.array(z.string().email()).optional(),
recurrence: z
.object({
frequency: z.enum(["daily", "weekly", "monthly"]),
count: z.number().int().min(1),
})
.optional(),
}),
run: async (input) => {
if (input.attendees && input.attendees.length > 10) {
throw new Error("Too many attendees (max 10)");
}
return JSON.stringify({
event_id: "evt_123",
status: "created",
title: input.title,
});
},
});
const listCalendarEvents = betaZodTool({
name: "list_calendar_events",
description: "List all calendar events on a given date.",
inputSchema: z.object({
date: z.string().date(),
}),
run: async () => {
return JSON.stringify({
events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }],
});
},
});
const finalMessage = await client.beta.messages.toolRunner({
model: "claude-opus-4-6",
max_tokens: 1024,
tools: [createCalendarEvent, listCalendarEvents],
messages: [
{
role: "user",
content:
"Check what I have next Monday, then schedule a planning session that avoids any conflicts.",
},
],
});
for (const block of finalMessage.content) {
if (block.type === "text") {
console.log(block.text);
}
}
预期输出
I checked your calendar for next Monday 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 与实现放在一起。
你构建了什么
你从一个硬编码的工具调用开始,最终得到了一个处理多个工具、并行调用和错误的生产级 agent,然后将所有这些折叠到工具运行器中。在此过程中,你看到了工具使用协议的每个部分:tool_use 块、tool_result 块、tool_use_id 匹配、stop_reason 检查和 is_error 信号。