← Your First AI Agent: From Zero to Autonomy
03 / 07 · 45 min

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

Инструмент получил от API ошибку 429 (rate limit). Как правильно?

01Инструмент получил от API ошибку 429 (rate limit). Как правильно?

Почему восемь узких инструментов обычно лучше одного universal_api с параметром action?

02Почему восемь узких инструментов обычно лучше одного universal_api с параметром action?

Что такое идемпотентность пишущего инструмента?

03Что такое идемпотентность пишущего инструмента?

Инструмент вернул 2 МБ JSON. Что должен сделать ваш код перед записью в историю?

04Инструмент вернул 2 МБ JSON. Что должен сделать ваш код перед записью в историю?

Модель запросила параллельно search_issues и delete_branch. Как исполнять?

05Модель запросила параллельно search_issues и delete_branch. Как исполнять?

Sources

Tool Calling That Works · Your First AI Agent: From Zero to Autonomy · VORCL