Руководство: создание агента, использующего инструменты
Пошаговое руководство от одного вызова инструмента до готового к продакшену агентного цикла.
В этом руководстве агент для управления календарём строится в пять концентрических колец. Каждое кольцо — это законченная, запускаемая программа, которая добавляет ровно одну концепцию к предыдущему кольцу. К концу вы напишете «agentic loop» (агентный цикл) вручную, а затем замените его абстракцией SDK Tool Runner.
Пример инструмента — create_calendar_event. Его схема использует вложенные объекты, массивы и необязательные поля, поэтому вы увидите, как Claude работает с реалистичными формами входных данных, а не с одной плоской строкой.
Кольцо 1: один инструмент, один ход
Наименьшая возможная программа с использованием инструментов: один инструмент, одно сообщение пользователя, один вызов инструмента, один результат. Код подробно прокомментирован, чтобы вы могли сопоставить каждую строку с жизненным циклом использования инструментов.
Запрос отправляет массив 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-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 alice@example.com and bob@example.com 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-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 alice@example.com and bob@example.com 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': ['alice@example.com', 'bob@example.com']}
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, а содержимое представляет собой текст на естественном языке для пользователя.
Кольцо 2: агентный цикл
Кольцо 1 предполагало, что Claude вызовет инструмент ровно один раз. Реальные задачи часто требуют нескольких вызовов: Claude может создать событие, прочитать подтверждение, а затем создать ещё одно. Решение — цикл while, который продолжает выполнять инструменты и возвращать результаты, пока stop_reason не перестанет быть "tool_use".
Другое изменение — история разговора. Вместо того чтобы заново собирать массив messages при каждом запросе, ведите непрерывный список и добавляйте в него. Каждый ход видит полный предшествующий контекст.
# Кольцо 2: agentic loop (агентный цикл).
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, starting Monday, March 30, 2026. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",
}
]
response = client.messages.create(
model="claude-opus-5-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-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 разбивает задачу. Вашему коду больше не нужно знать это заранее.
Кольцо 3: несколько инструментов, параллельные вызовы
У агентов редко бывает только одна возможность. Добавьте второй инструмент, 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 on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
}
]
response = client.messages.create(
model="claude-opus-5-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-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 Monday, March 30 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 читает ошибку и может повторить попытку с исправленными входными данными, попросить пользователя уточнить или объяснить ограничение.
# Кольцо 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 a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),
}
]
response = client.messages.create(
model="claude-opus-5-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-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 видит флаг и текст ошибки и отвечает соответствующим образом. Полный справочник по обработке ошибок см. в разделе Обработка вызовов инструментов.
Кольцо 5: абстракция SDK Tool Runner
В кольцах 2–4 один и тот же цикл писался вручную: вызвать API, проверить stop_reason, запустить инструменты, добавить результаты, повторить. Tool Runner делает это за вас. Определите каждый инструмент как функцию, передайте список в client.beta.messages.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-5",
max_tokens=1024,
tools=[create_calendar_event, list_calendar_events],
messages=[
{
"role": "user",
"content": "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
}
],
).until_done()
for block in final_message.content:
if block.type == "text":
print(block.text)Чего ожидать
I checked your calendar for Monday, March 30 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?