Зачем управлять выбором: когда auto не подходит
Представьте: вы строите пайплайн сбора данных. Агент должен обязательно вызвать fetch_data, прежде чем анализировать. Но в режиме auto модель иногда считает, что знает ответ без вызова инструмента, и отвечает напрямую. Пайплайн ломается.
Или другая задача: нужно извлечь структурированные данные из неструктурированного текста — имя, email, телефон. Можно попросить Claude ответить JSON, но тогда придётся парсить и валидировать текст. Есть способ лучше: использовать tool_choice в режиме tool, чтобы модель заполнила JSON Schema напрямую. Функцию при этом вызывать не нужно — аргументы блока tool_use и есть структурированный вывод.
Параметр tool_choice даёт три режима управления этим поведением:
Важно: tool_choice влияет на то, будет ли вызван инструмент, но не на то, какие аргументы модель ему передаст. Аргументы всегда определяет модель на основе контекста разговора.
auto: модель решает сама
Режим по умолчанию. Модель взвешивает, нужен ли инструмент для ответа на текущий запрос. Если вопрос требует актуальных данных — вызовет инструмент. Если может ответить по своим знаниям — ответит напрямую без вызова.
Это правильное поведение для универсальных агентов, где одни вопросы требуют данных (погода, цены, статусы), а другие — нет (объяснения, советы, код).
Как это выглядит в протоколе
[tool_use: get_weather({ city: "Москва" })]
[text: "asyncio — это библиотека..."]
Признак того, что модель вызвала инструмент: response.stop_reason == "tool_use". Признак прямого ответа: stop_reason == "end_turn". Проверяйте это значение в цикле агента, чтобы знать, нужно ли обрабатывать tool_use блоки.
import anthropic
client = anthropic.Anthropic()
tools = [{
"name": "get_weather",
"description": "Получает текущую погоду в городе",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Название города"}
},
"required": ["city"],
},
}]
# auto — значение по умолчанию, можно не указывать tool_choice вообще
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto"}, # ← можно не писать: auto — дефолт
messages=[{"role": "user", "content": "Какая погода в Москве?"}],
)
# stop_reason показывает решение модели:
# "tool_use" → вызвала инструмент, нужно обработать tool_use блоки
# "end_turn" → ответила напрямую, в content будет TextBlock
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
print(f"Вызван: {block.name}") # get_weather
print(f"Аргументы: {block.input}") # {"city": "Москва"}
else:
for block in response.content:
if hasattr(block, "text"):
print(block.text) # прямой текстовый ответ
Используйте auto для большинства агентов — это естественное поведение, которое не заставляет модель вызывать инструменты там, где они не нужны. Переходите к другим режимам, только когда появляются конкретные требования: гарантированный вызов или конкретный инструмент.
any: инструмент обязателен
Режим any заставляет модель вызвать хотя бы один из переданных инструментов. Прямой текстовый ответ без вызова инструмента — невозможен. stop_reason всегда будет "tool_use".
Это полезно когда нужно гарантировать запуск инструмента независимо от того, считает ли модель, что знает ответ сама. Типичные сценарии: пайплайны сбора данных, нормализация и обогащение записей, принудительная маршрутизация задачи к конкретной подсистеме.
Протокол: модель вызывает, даже если «знает» ответ
← в режиме auto модель ответила бы напрямую (знает ответ)
[tool_use: search_web({ query: "Python programming language" })]
tools = [
{
"name": "fetch_product_data",
"description": "Получает актуальные данные о товаре из базы",
"input_schema": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
"fields": {
"type": "array",
"items": {"type": "string"},
"description": "Список полей для получения"
},
},
"required": ["product_id"],
},
},
{
"name": "log_request",
"description": "Логирует запрос для аудита",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"},
"intent": {"type": "string"},
},
"required": ["query"],
},
},
]
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "any"}, # ← модель ОБЯЗАНА вызвать инструмент
messages=[{
"role": "user",
"content": "Покажи товар SKU-42"
}],
)
# stop_reason здесь всегда "tool_use" — гарантировано параметром
assert response.stop_reason == "tool_use"
for block in response.content:
if block.type == "tool_use":
print(f"Модель выбрала: {block.name}")
print(f"Аргументы: {block.input}")
Если передать несколько инструментов в режиме any, модель сама решит, какой из них вызвать. Если нужен конкретный — используйте режим tool. Если нужно несколько конкретных шагов — запускайте несколько запросов, переключая режим между шагами.
tool: конкретный инструмент
Самый строгий режим: модель обязана вызвать конкретный инструмент, указанный в name. Она всё ещё самостоятельно определяет аргументы на основе контекста, но выбора инструмента у неё нет.
Это полезно для двух совершенно разных задач: принудительного первого шага в пайплайне и — самое нестандартное применение — структурированной экстракции данных без реального вызова функции.
Базовое использование: принудительный вызов
tools = [
{
"name": "search_web",
"description": "Ищет актуальную информацию в интернете",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Поисковый запрос"},
"max_results": {"type": "integer", "default": 5},
},
"required": ["query"],
},
},
{
"name": "read_file",
"description": "Читает файл с диска",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"],
},
},
]
# Модель ОБЯЗАНА вызвать search_web — даже если read_file может быть полезнее
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "search_web"}, # ← конкретный
messages=[{"role": "user", "content": "Найди последние новости о Python 3.14"}],
)
for block in response.content:
if block.type == "tool_use":
# Гарантированно будет search_web, никогда read_file
assert block.name == "search_web"
print(block.input) # {"query": "Python 3.14 новости", "max_results": 5}
Паттерн: структурированная экстракция без вызова функции
Это самый мощный и неочевидный способ использования режима tool. Идея: описать нужную структуру данных как «инструмент», принудить модель «вызвать» его, а потом прочитать аргументы этого вызова как структурированный вывод. Функцию при этом вызывать не нужно — block.input и есть результат.
Обычный подход: просить JSON в тексте
user: "Верни JSON: {name, email, phone}"
assistant: "```json\n{...}\n```" ← нужно парсить, может прийти неверный JSON
Tool choice подход: модель заполняет схему
tool_choice: {"type": "tool", "name": "extract_contact"}
assistant: tool_use { name: "extract_contact", input: {name:..., email:..., phone:...} }
↑ block.input — уже валидный dict, соответствующий JSON Schema
"""
Структурированная экстракция через tool_choice.
Функция save_contact НЕ вызывается — мы только читаем её аргументы.
"""
# Описываем нужную структуру данных как "инструмент"
extract_tool = {
"name": "save_contact",
"description": "Сохраняет контактные данные, извлечённые из текста",
"input_schema": {
"type": "object",
"properties": {
"full_name": {"type": "string", "description": "Полное имя"},
"email": {"type": "string", "description": "Email адрес"},
"phone": {"type": "string", "description": "Номер телефона"},
"company": {"type": "string", "description": "Компания или организация"},
"position": {"type": "string", "description": "Должность"},
},
"required": ["full_name"],
},
}
def extract_contact(text: str) -> dict:
"""Извлекает контактные данные из произвольного текста."""
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=512,
tools=[extract_tool],
tool_choice={"type": "tool", "name": "save_contact"}, # ← обязан заполнить схему
messages=[{
"role": "user",
"content": f"Извлеки контактные данные из текста:\n\n{text}",
}],
)
for block in response.content:
if block.type == "tool_use" and block.name == "save_contact":
# block.input — уже структурированный dict, прошедший через JSON Schema
# Функцию НЕ вызываем, аргументы и есть результат экстракции
return block.input
return {}
# Использование
text = """
Привет, меня зовут Анна Михайлова, я директор по развитию в TechStart.
Напишите на anna.mikhailova@techstart.ru или звоните: +7 (495) 123-45-67.
"""
contact = extract_contact(text)
print(contact)
# {
# "full_name": "Анна Михайлова",
# "email": "anna.mikhailova@techstart.ru",
# "phone": "+7 (495) 123-45-67",
# "company": "TechStart",
# "position": "директор по развитию"
# }
При экстракции через tool_choice модель не может вернуть невалидный JSON или добавить лишний текст вокруг структуры. Аргументы tool_use всегда являются валидным JSON, соответствующим указанной схеме. Вы получаете гарантированно типизированный словарь без парсинга.
Пакетная экстракция с Pydantic-валидацией
from pydantic import BaseModel
from typing import Optional
class Contact(BaseModel):
full_name: str
email: Optional[str] = None
phone: Optional[str] = None
company: Optional[str] = None
position: Optional[str] = None
def extract_contacts_batch(texts: list[str]) -> list[Contact]:
"""Извлекает контакты из списка текстов."""
results = []
for text in texts:
raw = extract_contact(text) # возвращает dict из предыдущего примера
try:
results.append(Contact(**raw))
except Exception as e:
# Pydantic проверит типы и заполнит дефолты
results.append(Contact(full_name="Unknown"))
return results
Контроль параллельности: disable_parallel_tool_use
Когда Claude в режиме any или auto видит задачу, которую можно разбить на несколько независимых вызовов, он может вернуть несколько tool_use блоков в одном ответе (parallel tool calling). Иногда это нежелательно.
Флаг disable_parallel_tool_use: true ограничивает ответ одним инструментом за раз. Используйте его когда:
- инструменты имеют побочные эффекты и не могут работать одновременно (запись в БД, отправка сообщений)
- порядок вызовов важен — второй инструмент использует результат первого
- вы хотите давать промежуточный фидбек пользователю после каждого шага
tools = [create_order_tool, send_email_tool, update_inventory_tool]
# Без флага — модель может попытаться вызвать несколько одновременно
# Это опасно: create_order и send_email могут выполниться до update_inventory
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=2048,
tools=tools,
tool_choice={
"type": "any",
"disable_parallel_tool_use": True, # ← строго один за раз
},
messages=messages,
)
# Теперь в каждом ответе будет ровно один tool_use блок.
# Агент выполнит шаги последовательно:
# 1. create_order → получили order_id
# 2. update_inventory(order_id=...)
# 3. send_email(order_id=...)
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
assert len(tool_use_blocks) == 1 # гарантировано
Флаг работает со всеми тремя режимами: auto, any и tool. Для режима tool он избыточен (конкретный инструмент всегда один), но не сломает код.
disable_parallel_tool_use увеличивает количество round-trip к API: вместо одного запроса с тремя параллельными вызовами вы делаете три отдельных запроса. Это медленнее, но безопаснее для операций с побочными эффектами.
Паттерны применения
Паттерн: принудительный первый шаг пайплайна
Агент-исследователь должен сначала обязательно найти актуальные данные, а потом анализировать их. В режиме auto модель иногда отвечает по своим знаниям, пропуская поиск. Решение: первый запрос с tool_choice: tool, последующие — с auto.
async def research_agent(query: str) -> str:
"""
Агент-исследователь с гарантированным первым поиском.
Шаг 1: принудительный search_web (tool_choice: tool)
Шаг 2+: свободный режим auto
"""
messages = [{"role": "user", "content": query}]
# ── Шаг 1: гарантированный поиск ──────────────────
response = await client.messages.create(
model="claude-opus-4-6",
max_tokens=512,
tools=tools,
tool_choice={"type": "tool", "name": "search_web"}, # ← обязательно
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
# Обрабатываем результаты поиска
search_results = await handle_all_tool_calls(response.content)
messages.append({"role": "user", "content": search_results})
# ── Шаги 2+: свободный анализ ─────────────────────
for _ in range(10): # max_iterations
response = await client.messages.create(
model="claude-opus-4-6",
max_tokens=4096,
tools=tools,
tool_choice={"type": "auto"}, # ← теперь модель решает сама
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
tool_results = await handle_all_tool_calls(response.content)
messages.append({"role": "user", "content": tool_results})
return "Достигнут лимит итераций"
Паттерн: маршрутизация задач через any
Часто первым шагом агент должен классифицировать запрос и направить его к нужному инструменту. Вместо отдельного вызова классификатора можно использовать any с набором «роутер-инструментов»: модель выберет подходящий и заполнит его аргументы.
# «Маршрутизирующие» инструменты — описания говорят когда их использовать
router_tools = [
{
"name": "handle_billing_query",
"description": "Используй, если вопрос о счёте, оплате, подписке или возврате",
"input_schema": {
"type": "object",
"properties": {
"query_type": {
"type": "string",
"enum": ["invoice", "payment", "subscription", "refund"],
},
"user_message": {"type": "string"},
},
"required": ["query_type", "user_message"],
},
},
{
"name": "handle_technical_query",
"description": "Используй, если вопрос технический: баги, интеграции, API",
"input_schema": {
"type": "object",
"properties": {
"severity": {"type": "string", "enum": ["critical", "high", "medium", "low"]},
"user_message": {"type": "string"},
},
"required": ["severity", "user_message"],
},
},
{
"name": "handle_general_query",
"description": "Используй для всех остальных вопросов",
"input_schema": {
"type": "object",
"properties": {"user_message": {"type": "string"}},
"required": ["user_message"],
},
},
]
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=256,
tools=router_tools,
tool_choice={"type": "any"}, # ← обязан выбрать маршрут
messages=[{"role": "user", "content": user_message}],
)
for block in response.content:
if block.type == "tool_use":
route = block.name # "handle_billing_query" и т.д.
args = block.input # уже заполненные поля
await dispatch(route, args) # направляем в нужный обработчик
Типичные ошибки
{"type": "tool", "name": "extract_contact"} смысл в том, что функция не вызывается. Разработчики по инерции пишут result = extract_contact(**block.input) — и функция выполняется там, где не должна.block.input. Никаких вызовов функции.{"type": "any"} агенту с вопросом «объясни что такое Python», модель вызовет какой-нибудь инструмент — потому что обязана. Это лишние токены, время и деньги.any — для задач, которые по определению требуют вызова (сбор данных, формы, маршрутизация). Для общения — auto.{"type": "tool", "name": "search_web"} гарантирует что будет вызван search_web, но не гарантирует конкретный query. Аргументы определяет модель. Если модель передала невалидные аргументы — используйте Pydantic-валидацию из предыдущего урока.tool.{"type": "tool", "name": "search_web"} инструмент search_web должен быть в массиве tools. Если его нет — API вернёт ошибку. Частая опечатка: несоответствие имени в tool_choice.name и в описании инструмента.tool_choice.name точно совпадает с tools[i].name.any stop_reason гарантированно "tool_use". Но если перейти обратно на auto в следующем запросе — stop_reason может стать "end_turn". Код, который не проверяет stop_reason, сломается.response.stop_reason перед тем как искать tool_use блоки.Шпаргалка
Модель решает: вызывать ли инструмент
- Универсальные агенты
- Чат-боты с инструментами
- Смешанные задачи (часть требует инструментов, часть нет)
- По умолчанию — если не знаешь что выбрать
Гарантированный вызов хотя бы одного
- Пайплайны сбора данных
- Обогащение записей (каждая строка через инструмент)
- Маршрутизация/классификация запроса
- Тестирование — проверить что инструмент вызывается
Гарантированный конкретный инструмент
- Структурированная экстракция данных
- Принудительный первый шаг пайплайна
- Контроль конкретного этапа workflow
- Замена парсинга JSON из текста
Параметр disable_parallel_tool_use: добавьте "disable_parallel_tool_use": true в любой режим, если инструменты имеют побочные эффекты или порядок выполнения важен.
Паттерн структурированной экстракции:
- Опишите нужную структуру данных как инструмент с JSON Schema
- Передайте
tool_choice={"type": "tool", "name": "..."} - Прочитайте
block.input— это и есть результат, функцию не вызывайте - Опционально — провалидируйте через Pydantic
Переключение режимов в пайплайне: первый запрос — tool (гарантированный сбор данных), следующие — auto (анализ и ответ).
Практическое задание
Задача 1. Напишите функцию extract_invoice(text: str) -> dict, которая принимает произвольный текст счёта-фактуры и извлекает: номер счёта, дату, сумму, название поставщика, название покупателя. Используйте паттерн структурированной экстракции через tool_choice. Добавьте Pydantic-модель для валидации результата. Проверьте на трёх текстах с разным форматом.
Задача 2. Реализуйте маршрутизатор запросов поддержки: три инструмента (handle_refund, handle_technical, handle_general), режим any. Напишите тест, который подаёт 10 разных сообщений и проверяет, что каждое направлено в ожидаемый маршрут. Добавьте логирование какой инструмент был вызван и почему (попросите модель добавить поле reason в схему).
Задача 3 (продвинутая). Реализуйте агента с принудительным первым шагом: (а) первый запрос с tool_choice: tool, name: search_web всегда ищет актуальные данные; (б) следующие запросы в режиме auto, но если агент снова хочет искать — это разрешено; (в) добавьте счётчик вызовов инструментов и выводите его в конце. Проверьте что агент не отвечает из своих данных на вопросы требующие актуальной информации.