Это руководство описывает создание агента для управления календарём в виде пяти концентрических колец. Каждое кольцо — это полноценная, запускаемая программа, которая добавляет ровно одну концепцию к предыдущему кольцу. К концу вы напишете агентный цикл вручную, а затем замените его абстракцией 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-4-8",
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] next Monday 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-4-8",
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] 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),
}
],
},
],
)
# Получив результат инструмента, 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 next Monday 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-4-8",
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-4-8",
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-4-8",
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-4-8",
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-4-8",
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-4-8",
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 и получите итоговое сообщение после завершения цикла. Оборачивание ошибок, форматирование результатов и управление диалогом обрабатываются внутри.
Python SDK использует декоратор @beta_tool для вывода схемы из аннотаций типов и строки документации. TypeScript SDK использует betaZodTool со схемой Zod. Остальные SDK следуют тому же шаблону со своими собственными вспомогательными средствами: BetaRunnableTool в C# и PHP, типизированные классы инструментов в Java и Ruby и toolrunner.NewBetaToolFromJSONSchema в Go.
Tool Runner доступен во всех семи SDK: Python, TypeScript, C#, Go, Java, PHP и Ruby. Полную справку см. в разделе Tool Runner. На вкладках cURL и CLI вместо кода показано примечание; для скриптов на основе curl или CLI используйте цикл из Кольца 4.
# Кольцо 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-4-8",
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?