Это руководство строит агента для управления календарём в пять концентрических колец. Каждое кольцо — это полная, запускаемая программа, которая добавляет ровно одну концепцию к предыдущему кольцу. К концу вы напишете агентный цикл вручную, а затем замените его абстракцией Tool Runner SDK.
Пример инструмента — create_calendar_event. Его схема использует вложенные объекты, массивы и необязательные поля, поэтому вы увидите, как Claude обрабатывает реалистичные формы входных данных, а не одну плоскую строку.
Самая маленькая возможная программа с использованием инструментов: один инструмент, одно сообщение пользователя, один вызов инструмента, один результат. Код подробно прокомментирован, чтобы вы могли сопоставить каждую строку с жизненным циклом использования инструментов.
Запрос отправляет массив 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 должен передать при вызове инструмента.
# Эта схема включает вложенные объекты (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",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with [email protected] and [email protected] 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 должен совпадать с id из блока tool_use выше. Предыдущий
# ответ ассистента включён, чтобы у Claude была полная история.
followup = client.messages.create(
model="claude-opus-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 [email protected] and [email protected] 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': ['[email protected]', '[email protected]']}
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, а содержимое — естественный язык для пользователя.
Кольцо 1 предполагало, что Claude вызовет инструмент ровно один раз. Реальные задачи часто требуют нескольких вызовов: Claude может создать событие, прочитать подтверждение, а затем создать ещё одно. Решение — цикл while, который продолжает запускать инструменты и передавать результаты обратно, пока stop_reason не перестанет быть "tool_use".
Другое изменение — история разговора. Вместо того чтобы перестраивать массив messages с нуля при каждом запросе, ведите текущий список и добавляйте в него. Каждый ход видит полный предыдущий контекст.
# Кольцо 2: агентный цикл.
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. Invite the whole team: [email protected], [email protected], [email protected].",
}
]
response = client.messages.create(
model="claude-opus-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",
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 разбивает задачу. Вашему коду больше не нужно знать это заранее.
Агенты редко имеют только одну возможность. Добавьте второй инструмент, list_calendar_events, чтобы Claude мог проверить существующее расписание перед созданием чего-то нового.
Когда у Claude есть несколько независимых вызовов инструментов, он может вернуть несколько блоков tool_use в одном ответе. Ваш цикл должен обработать их все и отправить обратно все результаты вместе в одном сообщении пользователя. Итерируйте по каждому блоку tool_use в response.content, а не только по первому.
# Кольцо 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 next Monday, then schedule a planning session that avoids any conflicts.",
}
]
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
# Один ответ может содержать несколько блоков tool_use. Обработайте их
# все и верните все результаты вместе в одном сообщении пользователя.
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",
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 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 читает ошибку и может повторить попытку с исправленными входными данными, попросить пользователя уточнить или объяснить ограничение.
# Кольцо 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 an all-hands with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),
}
]
response = client.messages.create(
model="claude-opus-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",
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 видит флаг и текст ошибки и реагирует соответствующим образом. См. Обработка вызовов инструментов для полного справочника по обработке ошибок.
Кольца со 2 по 4 писали один и тот же цикл вручную: вызвать API, проверить stop_reason, запустить инструменты, добавить результаты, повторить. Tool Runner делает это за вас. Определите каждый инструмент как функцию, передайте список в tool_runner и получите финальное сообщение после завершения цикла. Обёртывание ошибок, форматирование результатов и управление разговором обрабатываются внутри.
Каждый SDK предоставляет вспомогательную функцию, которая превращает обычную функцию в запускаемый инструмент и выводит схему входных данных из её сигнатуры; вкладки ниже показывают идиоматическую форму для каждого языка.
# Кольцо 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",
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)Чего ожидать
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.
Спецификация схемы и лучшие практики.
Полный справочник по абстракции SDK.
Исправление распространённых ошибок использования инструментов.
Was this page helpful?