Этот учебник создает агента управления календарем в пяти концентрических кольцах. Каждое кольцо — это полная, работающая программа, которая добавляет ровно одну концепцию к предыдущему кольцу. К концу вы напишете цикл агента вручную, а затем замените его абстракцией Tool Runner SDK.
Примером инструмента является create_calendar_event. Его схема использует вложенные объекты, массивы и необязательные поля, поэтому вы увидите, как Claude обрабатывает реалистичные формы входных данных, а не просто одну плоскую строку.
Каждое кольцо работает независимо. Скопируйте любое кольцо в новый файл, и оно будет выполняться без кода из предыдущих колец.
Самая маленькая возможная программа, использующая инструменты: один инструмент, одно сообщение пользователя, один вызов инструмента, один результат. Код содержит много комментариев, чтобы вы могли сопоставить каждую строку с жизненным циклом использования инструмента.
Запрос отправляет массив tools вместе с сообщением пользователя. Когда Claude решает вызвать инструмент, ответ возвращается с stop_reason: "tool_use" и блоком содержимого tool_use, содержащим имя инструмента, уникальный id и структурированный input. Ваш код выполняет инструмент, затем отправляет результат обратно в блоке tool_result, чей tool_use_id соответствует id из вызова.
Was this page helpful?
#!/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 с нуля при каждом запросе, ведите текущий список и добавляйте к нему. Каждый ход видит полный предыдущий контекст.
#!/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'Чего ожидать
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 в одном ответе. Ваш цикл должен обработать все из них и отправить все результаты вместе в одном сообщении пользователя. Переберите каждый блок tool_use в response.content, а не только первый.
#!/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'Чего ожидать
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 читает ошибку и может повторить попытку с исправленным входом, попросить уточнение у пользователя или объяснить ограничение.
#!/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'Чего ожидать
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 использует betaZodTool со схемой Zod.
Tool Runner доступен в Python, TypeScript и Ruby SDK. Вкладки Shell и CLI показывают примечание вместо кода; сохраните цикл Ring 4 для скриптов на основе shell.
#!/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.Чего ожидать
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.