Was this page helpful?
本教程通過五個同心環構建一個日曆管理代理。每個環都是一個完整的、可運行的程序,它在前一個環的基礎上恰好添加一個概念。最後,你將手動編寫代理循環,然後用 Tool Runner SDK 抽象替換它。
示例工具是 create_calendar_event。其架構使用嵌套對象、數組和可選字段,因此你將看到 Claude 如何處理現實的輸入形狀,而不是單個平面字符串。
每個環都獨立運行。將任何環複製到新文件中,它將執行而無需來自早期環的代碼。
最小的工具使用程序:一個工具、一條用戶消息、一次工具調用、一個結果。代碼有大量註釋,因此你可以將每一行映射到工具使用生命週期。
請求在用戶消息旁邊發送一個 tools 數組。當 Claude 決定調用工具時,響應返回 stop_reason: "tool_use" 和一個包含工具名稱、唯一 id 和結構化 input 的 tool_use 內容塊。你的代碼執行工具,然後在 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 [email protected] and [email protected] 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'預期結果
stop_reason: tool_use
Tool: create_calendar_event
Input: {'title': 'Sync', 'start': '2026-03-30T10:00:00', 'end': '2026-03-30T10:30:00', 'attendees': ['[email protected]', '[email protected]']}
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,內容是給用戶的自然語言。
環 1 假設 Claude 將恰好調用一次工具。實際任務通常需要多次調用:Claude 可能創建一個事件、讀取確認,然後創建另一個。解決方案是一個 while 循環,它持續運行工具並反饋結果,直到 stop_reason 不再是 "tool_use"。
另一個變化是對話歷史。與其在每次請求時從頭重建 messages 數組,不如保持一個運行列表並追加到它。每次轉換都會看到完整的先前上下文。
預期結果
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.循環可能運行一次或多次,取決於 Claude 如何分解任務。你的代碼不再需要提前知道。
代理很少只有一個功能。添加第二個工具 list_calendar_events,以便 Claude 在創建新內容之前可以檢查現有日程。
當 Claude 有多個獨立的工具調用要進行時,它可能在單個響應中返回多個 tool_use 塊。你的循環需要處理所有這些塊,並在一條用戶消息中一起發送所有結果。遍歷 response.content 中的每個 tool_use 塊,而不僅僅是第一個。
預期結果
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.有關並發執行和排序保證的更多信息,請參閱並行工具使用。
工具會失敗。日曆 API 可能拒絕有太多參與者的事件,或日期可能格式不正確。當工具引發錯誤時,使用 is_error: true 發送錯誤消息,而不是崩潰。Claude 讀取錯誤,可以使用更正的輸入重試、要求用戶澄清或解釋限制。
預期結果
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 看到標誌和錯誤文本,並相應地響應。有關完整的錯誤處理參考,請參閱處理工具調用。
環 2 到 4 手動編寫了相同的循環:調用 API、檢查 stop_reason、運行工具、追加結果、重複。Tool Runner 為你做這一切。將每個工具定義為函數,將列表傳遞給 tool_runner,並在循環完成後檢索最終消息。錯誤包裝、結果格式化和對話管理在內部處理。
Python SDK 使用 @beta_tool 裝飾器從類型提示和文檔字符串推斷架構。TypeScript SDK 使用帶有 Zod 架構的 betaZodTool。
Tool Runner 在 Python、TypeScript 和 Ruby SDK 中可用。Shell 和 CLI 選項卡顯示注釋而不是代碼;為基於 shell 的腳本保留環 4 循環。
預期結果
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 相同。區別在於代碼:大約一半的行數、沒有手動循環,架構與實現並存。
你從單個硬編碼的工具調用開始,以處理多個工具、並行調用和錯誤的生產形狀代理結束,然後將所有這些折疊到 Tool Runner 中。在此過程中,你看到了工具使用協議的每一部分:tool_use 塊、tool_result 塊、tool_use_id 匹配、stop_reason 檢查和 is_error 信號。
#!/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: [email protected], [email protected], [email protected]."}]'
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'#!/bin/bash
# Ring 3: Multiple tools, parallel calls.
# 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"]
}
},
{
"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"]
}
}
]'
run_tool() {
case "$1" in
create_calendar_event)
jq -n --arg title "$(echo "$2" | jq -r '.title')" '{event_id: "evt_123", status: "created", title: $title}' ;;
list_calendar_events)
echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;
*)
echo "{\"error\": \"Unknown tool: $1\"}" ;;
esac
}
MESSAGES='[{"role": "user", "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts."}]'
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, messages: $messages}')"
}
RESPONSE=$(call_api)
while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
# A single response can contain multiple tool_use blocks. Process all of
# them and return all results together in one user message.
TOOL_RESULTS='[]'
while read -r block; do
NAME=$(echo "$block" | jq -r '.name')
INPUT=$(echo "$block" | jq -c '.input')
ID=$(echo "$block" | jq -r '.id')
RESULT=$(run_tool "$NAME" "$INPUT")
TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$RESULT" \
'. + [{type: "tool_result", tool_use_id: $id, content: $result}]')
done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")')
MESSAGES=$(echo "$MESSAGES" | jq \
--argjson assistant "$(echo "$RESPONSE" | jq '.content')" \
--argjson results "$TOOL_RESULTS" \
'. + [{role: "assistant", content: $assistant}, {role: "user", content: $results}]')
RESPONSE=$(call_api)
done
echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'#!/bin/bash
# Ring 4: Error handling.
# 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"]
}
},
{
"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"]
}
}
]'
run_tool() {
case "$1" in
create_calendar_event)
local count=$(echo "$2" | jq '.attendees | length // 0')
if [ "$count" -gt 10 ]; then
echo "ERROR: Too many attendees (max 10)"
return 1
fi
jq -n --arg title "$(echo "$2" | jq -r '.title')" '{event_id: "evt_123", status: "created", title: $title}' ;;
list_calendar_events)
echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;
*)
echo "ERROR: Unknown tool: $1"
return 1 ;;
esac
}
EMAILS=$(seq 0 14 | sed 's/.*/user&@example.com/' | paste -sd, -)
MESSAGES="[{\"role\": \"user\", \"content\": \"Schedule an all-hands with everyone: $EMAILS\"}]"
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, messages: $messages}')"
}
RESPONSE=$(call_api)
while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
TOOL_RESULTS='[]'
while read -r block; do
NAME=$(echo "$block" | jq -r '.name')
INPUT=$(echo "$block" | jq -c '.input')
ID=$(echo "$block" | jq -r '.id')
if OUTPUT=$(run_tool "$NAME" "$INPUT"); then
TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \
'. + [{type: "tool_result", tool_use_id: $id, content: $result}]')
else
# Signal failure so Claude can retry or ask for clarification.
TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \
'. + [{type: "tool_result", tool_use_id: $id, content: $result, is_error: true}]')
fi
done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")')
MESSAGES=$(echo "$MESSAGES" | jq \
--argjson assistant "$(echo "$RESPONSE" | jq '.content')" \
--argjson results "$TOOL_RESULTS" \
'. + [{role: "assistant", content: $assistant}, {role: "user", content: $results}]')
RESPONSE=$(call_api)
done
echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'#!/bin/bash
# Ring 5: The Tool Runner SDK abstraction.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
# The Tool Runner SDK abstraction is available in the Python, TypeScript,
# and Ruby SDKs. There is no equivalent for raw curl requests. Switch to
# the Python or TypeScript tab to see Ring 5, or keep the Ring 4 loop as
# your shell implementation.