СИСТЕМА КОМПЬЮТЕРНОГО ДОСТУПА

Паттерны использования инструментов: надёжные интерфейсы агент-инструмент


Ваш агент вызвал инструмент и получил 40-строчный JSON-блоб — сырой ответ API, вложенные объекты, коды ошибок, зарытые внутри поля status. Модель прочитала его, выбрала правдоподобно выглядящее значение и продолжила. Значение было неверным. Три шага спустя агент уверенно написал отчёт на основе неправильных данных.

Инструмент работал. Интерфейс подвёл.

Использование инструментов — это механизм, который превращает языковую модель в агента. Каждая возможность агента — поиск в базах данных, запись файлов, вызовы API, запросы к сервисам — приходит через интерфейс инструмента. Если интерфейс спроектирован плохо, модель принимает худшие решения, даже когда базовый сервис работает корректно. Это руководство охватывает пять паттернов для создания точных, надёжных и готовых к продакшну интерфейсов инструментов.

Предварительные требования: знакомство с Python и API Claude. О MCP как транспортном уровне для инструментов см. Создание первого MCP-сервера.


Почему важен дизайн интерфейса

Когда агент выбирает и использует инструмент, он принимает два решения:

  1. Какой инструмент вызвать — определяется name и description инструмента
  2. Какие аргументы передать — определяется input_schema инструмента

Размытые описания приводят к неверному выбору инструмента. Слабые схемы позволяют модели передавать некорректные входные данные. Неструктурированные результаты заставляют модель угадывать. Большинство ошибок агентов находится не в рассуждениях — они живут на границе инструмента.


Паттерн 1: Проектирование схемы в первую очередь

Напишите JSON-схему прежде, чем писать реализацию. Строгая схема ограничивает поведение модели на этапе ввода — до того, как что-либо выполняется.

import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "search_products",
"description": (
"Поиск по каталогу продуктов по ключевым словам. "
"Возвращает список совпадающих продуктов с ID, именами и ценами. "
"Используйте, когда пользователь хочет найти или просмотреть продукты."
),
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Ключевые слова для поиска"
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "food", "home", "all"],
"description": "Категория продукта для фильтрации. Используйте 'all', если не указано."
},
"max_results": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"description": "Количество возвращаемых результатов. По умолчанию: 5"
}
},
"required": ["query", "category"]
}
}
]
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "Найди электронику дешевле 5000 рублей"}]
)

Правила схемы, сокращающие ошибки:

  • Используйте enum для полей с фиксированным набором допустимых значений
  • Устанавливайте minimum/maximum для числовых полей
  • Помечайте поля как required только когда инструмент действительно не может работать без них
  • Пишите описания с точки зрения модели: “Используйте, когда…”

Когда применять: При каждом определении инструмента.


Паттерн 2: Структурированные результаты инструмента

Возвращайте типизированные, машиночитаемые результаты. Никогда не возвращайте сырые ответы API.

import json
from dataclasses import dataclass, asdict
from typing import Any, Optional
@dataclass
class ToolResult:
success: bool
data: Optional[Any] = None
error: Optional[str] = None
def to_content(self) -> str:
return json.dumps(asdict(self), ensure_ascii=False)
def search_products(
query: str,
category: str,
max_results: int = 5,
) -> ToolResult:
try:
raw_results = _query_database(query, category, limit=max_results)
products = [
{"id": r["product_id"], "name": r["title"], "price": r["price_usd"]}
for r in raw_results
]
return ToolResult(success=True, data={"products": products, "count": len(products)})
except ConnectionError as e:
return ToolResult(success=False, error=f"База данных недоступна: {e}")
except Exception as e:
return ToolResult(success=False, error=f"Поиск не удался: {type(e).__name__}: {e}")
def _query_database(query, category, limit):
return []

Постоянный конверт {success, data, error} означает, что модель всегда знает, где искать. Для элегантной обработки сбоев см. Паттерны восстановления после ошибок агентов.


Паттерн 3: Параллельные вызовы инструментов

Claude может запросить несколько инструментов в одном ответе. Обрабатывайте их параллельно, а не последовательно.

from concurrent.futures import ThreadPoolExecutor, as_completed
TOOL_REGISTRY = {
"search_products": search_products,
}
def dispatch_tool(name: str, inputs: dict) -> ToolResult:
handler = TOOL_REGISTRY.get(name)
if not handler:
return ToolResult(success=False, error=f"Неизвестный инструмент: {name}")
return handler(**inputs)
def process_tool_calls(response: anthropic.types.Message) -> list[dict]:
tool_uses = [
block for block in response.content
if block.type == "tool_use"
]
if not tool_uses:
return []
def execute(tool_use):
result = dispatch_tool(tool_use.name, tool_use.input)
return {
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": result.to_content(),
}
with ThreadPoolExecutor(max_workers=len(tool_uses)) as executor:
futures = {executor.submit(execute, tu): tu for tu in tool_uses}
results = []
for future in as_completed(futures):
results.append(future.result())
return results
def run_agent(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
return ""
if response.stop_reason == "tool_use":
tool_results = process_tool_calls(response)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
else:
break
return ""

Для оркестрации нескольких агентов, каждый из которых вызывает инструменты, см. Мультиагентные паттерны.


Паттерн 4: Безопасная обёртка вызова инструмента

Никогда не позволяйте исключениям инструментов добираться до цикла агента необработанными.

import signal
def timeout_handler(signum, frame):
raise TimeoutError("Превышено время выполнения инструмента")
def safe_tool_call(
name: str,
inputs: dict,
timeout_seconds: int = 30,
) -> ToolResult:
"""
Выполняет инструмент с таймаутом, перехватывая все исключения.
Всегда возвращает ToolResult — никогда не выбрасывает исключение.
"""
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(timeout_seconds)
try:
return dispatch_tool(name, inputs)
except TimeoutError:
return ToolResult(
success=False,
error=f"Инструмент '{name}' превысил таймаут {timeout_seconds}с"
)
except Exception as e:
return ToolResult(
success=False,
error=f"Инструмент '{name}' вызвал {type(e).__name__}: {e}"
)
finally:
signal.alarm(0)

Когда инструмент возвращает success: false, модель может решить, повторить попытку, попробовать альтернативу или сообщить об ошибке. Для более широких стратегий повтора см. Паттерны восстановления после ошибок.


Паттерн 5: Валидация и усечение результатов

Валидируйте результаты инструментов перед возвратом их модели.

MAX_TOOL_RESULT_CHARS = 8000
def validate_result(result: ToolResult, expected_keys: list[str]) -> ToolResult:
if not result.success or not isinstance(result.data, dict):
return result
missing = [k for k in expected_keys if k not in result.data]
if missing:
return ToolResult(
success=False,
error=f"В ответе инструмента отсутствуют ожидаемые поля: {missing}"
)
return result
def truncate_result(result: ToolResult) -> ToolResult:
content = result.to_content()
if len(content) <= MAX_TOOL_RESULT_CHARS:
return result
truncated_data = {
"truncated": True,
"chars_omitted": len(content) - MAX_TOOL_RESULT_CHARS,
"content": content[:MAX_TOOL_RESULT_CHARS],
}
return ToolResult(
success=result.success,
data=truncated_data,
error="Результат усечён — слишком велик для контекстного окна",
)
def safe_tool_call_validated(
name: str,
inputs: dict,
expected_keys: list[str] | None = None,
) -> ToolResult:
result = safe_tool_call(name, inputs)
if expected_keys:
result = validate_result(result, expected_keys)
result = truncate_result(result)
return result

Для наблюдения и отладки этих сбоев в продакшне см. Отладка и наблюдаемость.


Распространённые ошибки

Ошибка 1: Возврат сырых ответов API

Модель получает вложенный объект с 30 полями, большинство из которых нерелевантны. Она выбирает неправильное.

Решение: Форматируйте ответ перед возвратом. Возвращайте только то, что нужно модели для следующего решения.

Ошибка 2: Инструменты с побочными эффектами без подтверждения

Инструмент, отправляющий письмо, удаляющий запись или снимающий деньги, не должен выполняться молча.

Решение: Для необратимых действий используйте двойной паттерн: plan_email возвращает предпросмотр, send_email фактически отправляет.

Ошибка 3: Пересечение ответственностей инструментов

Два инструмента, делающих похожие вещи, заставляют модель угадывать.

Решение: У каждого инструмента должна быть уникальная, не пересекающаяся цель.

Ошибка 4: Отсутствие таймаута для внешних инструментов

Медленный вызов сторонего API бесконечно блокирует весь цикл агента.

Решение: Всегда устанавливайте таймаут (Паттерн 4).


Чеклист для продакшна

  • Каждый инструмент имеет описание с точки зрения модели (“Используйте, когда…”)
  • Поля enum для всех входных данных с фиксированными значениями
  • Каждый инструмент возвращает {success, data, error} — никаких сырых ответов
  • Вызовы инструментов обёрнуты в safe_tool_call
  • Параллельное выполнение для мультиинструментальных ответов
  • Таймаут установлен для каждого внешнего инструмента
  • Усечение результатов для нагрузок переменного размера
  • Валидация для инструментов, вызывающих внешние API

Следующие шаги

  1. Начните со схемы — напишите input_schema до тела функции
  2. Добавьте обёртку ToolResult к каждому существующему инструменту
  3. Интегрируйте safe_tool_call для укрепления цикла агента
  4. Настройте валидацию результатов для инструментов, вызывающих внешние API

Связанные руководства: