Паттерны использования инструментов: надёжные интерфейсы агент-инструмент
Ваш агент вызвал инструмент и получил 40-строчный JSON-блоб — сырой ответ API, вложенные объекты, коды ошибок, зарытые внутри поля status. Модель прочитала его, выбрала правдоподобно выглядящее значение и продолжила. Значение было неверным. Три шага спустя агент уверенно написал отчёт на основе неправильных данных.
Инструмент работал. Интерфейс подвёл.
Использование инструментов — это механизм, который превращает языковую модель в агента. Каждая возможность агента — поиск в базах данных, запись файлов, вызовы API, запросы к сервисам — приходит через интерфейс инструмента. Если интерфейс спроектирован плохо, модель принимает худшие решения, даже когда базовый сервис работает корректно. Это руководство охватывает пять паттернов для создания точных, надёжных и готовых к продакшну интерфейсов инструментов.
Предварительные требования: знакомство с Python и API Claude. О MCP как транспортном уровне для инструментов см. Создание первого MCP-сервера.
Почему важен дизайн интерфейса
Когда агент выбирает и использует инструмент, он принимает два решения:
- Какой инструмент вызвать — определяется
nameиdescriptionинструмента - Какие аргументы передать — определяется
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 jsonfrom dataclasses import dataclass, asdictfrom typing import Any, Optional
@dataclassclass 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
Следующие шаги
- Начните со схемы — напишите
input_schemaдо тела функции - Добавьте обёртку
ToolResultк каждому существующему инструменту - Интегрируйте
safe_tool_callдля укрепления цикла агента - Настройте валидацию результатов для инструментов, вызывающих внешние API
Связанные руководства:
- Создание первого MCP-сервера
- Мультиагентные паттерны
- Системы памяти агентов
- Паттерны восстановления после ошибок агентов
- Отладка и наблюдаемость