Tool Calling That Works
Designing tool contracts: JSON Schema, descriptions, errors treated as results, parallel calls, and idempotency.
Lesson content is currently available in Russian.
What you will learn
- →Писать схемы и описания инструментов, по которым модель не промахивается.
- →Возвращать ошибки инструментов как результат, чтобы агент мог восстановиться.
- →Отличать читающие инструменты от пишущих и проектировать идемпотентность.
Контракт инструмента: имя, описание, схема
Инструмент — это API, у которого единственный потребитель — модель. Проектируйте его так же строго, как публичный REST-эндпоинт, только помните: ваша «документация» — это единственное, что модель видит.
Имя должно называть действие в стиле глагол_объект: search_tickets, create_label, get_issue. Модель выбирает инструмент в том числе по имени; do_stuff и helper2 гарантируют промахи. Описание отвечает на три вопроса: что делает, когда использовать, что возвращает. Хорошая эвристика — пишите как для стажёра, который видит систему впервые. Отдельно опишите каждый параметр и сузьте типы схемой: enum вместо свободной строки, minimum/maximum для чисел, required только для действительно обязательного.
{"name": "search_issues",
"description": "Поиск по issues репозитория. Используй ПЕРЕД созданием "
"метки duplicate. Возвращает до 10 совпадений: number, title, state.",
"parameters": {"type": "object", "properties": {
"query": {"type": "string",
"description": "Ключевые слова, 2-6 слов, без номера issue"},
"state": {"type": "string", "enum": ["open", "closed", "all"],
"description": "Фильтр по статусу, по умолчанию open"}},
"required": ["query"]}}
Два антипаттерна. Первый — инструмент-комбайн: один universal_api с параметром action на восемь режимов. Модель путает режимы; восемь узких инструментов работают лучше одного широкого. Второй — утечка внутренних абстракций: инструмент требует internal_tenant_uuid, которого модель знать не может. Всё, что модель не может вывести из диалога или получить от другого инструмента, ваш код должен подставлять сам.
Проверяйте контракт простым тестом: покажите схему и описание коллеге без контекста и попросите угадать, что инструмент вернёт и когда его звать. Если коллега мнётся — модель будет мяться точно так же, только молча и за ваши деньги.
Ошибки — тоже результат
В уроке 2 исключение инструмента убивало агента. Это неправильно по фундаментальной причине: для агента ошибка — это информация. «API вернул 404: репозиторий не найден» — полезнейший результат, из которого модель сделает вывод и сменит стратегию. Правило: инструмент никогда не бросает исключение наружу — он возвращает результат, иногда результат-ошибку.
def safe_call(fn, **args) -> str:
try:
return fn(**args)
except requests.Timeout:
return "ошибка: таймаут запроса, попробуй сузить запрос"
except requests.HTTPError as e:
return f"ошибка HTTP {e.response.status_code}: {e.response.text[:200]}"
except Exception as e:
return f"ошибка инструмента: {type(e).__name__}: {e}"
Качество текста ошибки — это качество восстановления. Сравните: «ошибка» — модель слепа и, скорее всего, повторит тот же вызов; «ошибка: rate limit GitHub, повтори через 60 секунд» — модель переключится на другую подзадачу или честно сообщит о задержке. Формулируйте ошибки с подсказкой следующего шага, как формулировали бы для коллеги.
Две обязательные детали. Таймаут: каждый сетевой инструмент получает жёсткий timeout (обычно 10-30 секунд), иначе один зависший запрос заморозит весь прогон. Усечение: инструмент, вернувший мегабайт JSON, мгновенно раздует историю — обрезайте результат до разумного размера (единицы килобайт) и явно помечайте усечение: «...ещё 4200 строк опущено, уточни запрос». Модель умеет реагировать на такую пометку уточнением — но только если пометка есть.
И не смешивайте уровни ошибок: safe_call ловит инфраструктурные сбои, а осмысленные отказы — «тикет не найден», «недостаточно прав» — инструмент формулирует сам, как обычный результат. Это часть его контракта, а не исключительная ситуация.
Чтение, запись и идемпотентность
Разделите инструменты на два класса и относитесь к ним по-разному. Читающие (search, get, list) безопасны: их можно вызывать повторно, параллельно, по ошибке — мир не изменится. Пишущие (create, update, send, delete) меняют мир, и с ними два специфических риска.
Первый — дубли при повторе. Модель не получила результат (сеть моргнула, прогон перезапустился) и решает повторить вызов create_comment — в issue появляются два одинаковых комментария. Лекарство то же, что в платёжных API, — идемпотентность: пишущий инструмент принимает или вычисляет ключ идемпотентности, и повторный вызов с тем же ключом не создаёт второй объект, а возвращает результат первого. Иногда идемпотентность даётся бесплатно семантикой операции: add_labels с одним и тем же набором меток можно вызывать сколько угодно — состояние issue не изменится со второго раза. Предпочитайте такие «естественно идемпотентные» формулировки операций.
Второй риск — необратимость. delete_branch не отменить, отправленное письмо не вернуть. Такие инструменты либо не даются агенту вовсе, либо проходят через подтверждение человеком — механику разберём в уроке 5. Практическое следствие для проектирования: помечайте каждый инструмент атрибутом readonly / write / irreversible прямо в коде реестра. Эта одна строка метаданных позже станет фундаментом guardrails: по ней автоматика решит, что можно исполнять молча, а что требует одобрения.
И про параллельные вызовы: модель может запросить несколько инструментов за шаг. Для читающих это подарок — исполняйте конкурентно и экономьте латентность. Для пишущих параллельность опасна (порядок не гарантирован), поэтому зрелые циклы исполняют write-вызовы строго последовательно, в порядке из ответа модели.
Practice
Три инструмента для support-агента
Спроектируйте инструменты search_tickets, get_ticket и reply_to_ticket для агента поддержки: полные JSON-схемы с enum и описаниями каждого параметра, обёртка safe_call с таймаутом и усечением длинных результатов, пометки readonly/write, идемпотентность для reply_to_ticket (ключ или естественная). Подключите их к агенту из урока 2 и добейтесь восстановления после подстроенной ошибки: пусть search_tickets один раз вернёт «ошибка: таймаут», и проследите, что модель повторяет или сужает запрос, а не падает.
Deliverable
Код трёх инструментов со схемами и safe_call плюс трейс прогона, где видно восстановление агента после ошибки инструмента.
Checkpoint
Почему инструмент агента не должен бросать исключение, и что он должен делать вместо этого?
- ✓ Объяснено, что исключение убивает прогон, а ошибка-как-результат даёт модели информацию для смены стратегии.
- ✓ Приведён пример информативного текста ошибки с подсказкой следующего шага.
- ✓ Упомянуты таймауты и усечение больших результатов как обязательные свойства инструмента.
Check yourself
Sources