Перейти к содержанию

Глава 8. Модель выполнения и каталог инструментов

1. Начнем с того же кейса поддержки, но уже на пути записи

Продолжим тот же сценарий из первых глав.

Пользователь пишет:

Я уже третий день жду активации доступа. Проверьте статус и создайте срочный тикет, если заявка застряла.

На первый взгляд задача кажется простой:

  • агент читает сообщение;
  • вызывает инструмент проверки статуса;
  • если заявка действительно застряла, вызывает инструмент создания тикета;
  • возвращает ответ.

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

Потому что теперь вопрос уже не только в том, что модель захотела сделать. Вопрос в другом:

  • какой инструмент ей вообще разрешено вызвать;
  • в какой области арендатора это допустимо;
  • какие аргументы считаются валидными;
  • где отделяются операции чтения и записи;
  • что делать, если внешний сервис завис после побочного эффекта;
  • как потом доказать, был ли тикет создан один раз или дважды.

Именно поэтому вызов инструментов нужно проектировать не как функцию при модели, а как слой выполнения платформы.

2. Агент не должен ходить в инструменты напрямую

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

Вместо этого нужен слой выполнения, который:

  • знает каталог доступных инструментов;
  • валидирует входные аргументы;
  • навешивает проверки политик;
  • отделяет операции чтения и записи;
  • управляет повторами, тайм-аутами и идемпотентностью;
  • пишет события аудита.

Для того же кейса поддержки это означает, что модель не должна сама ходить в API helpdesk или IAM-сервис. Она должна разговаривать только со слоем выполнения.

Сквозной кейс: контроль дубля тикета

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

3. Как один запрос проходит через слой выполнения

Посмотрим на тот же сценарий уже как на путь выполнения.

3.1. Сначала модель предлагает инструмент чтения

Чтобы понять, застряла ли заявка, агенту нужен инструмент проверки статуса. Это путь чтения:

  • он не должен менять внешний мир;
  • ему нужна корректная область арендатора;
  • он должен вернуть понятный структурированный результат.

3.2. Потом система решает, разрешен ли инструмент записи

Если статус говорит, что заявка действительно застряла, следующим шагом может быть create_ticket. Но это уже путь записи:

  • здесь появляется побочный эффект;
  • может потребоваться подтверждение;
  • нужен ключ идемпотентности;
  • нужен более строгий журнал аудита.

3.3. Потом слой выполнения берет на себя неприятную реальность

Именно здесь появляются сценарии, о которых редко думают на демо:

  • helpdesk ответил тайм-аутом после создания тикета;
  • инструмент вернул частичный успех;
  • модель повторила вызов после повтора;
  • внешний сервис вернул неожиданную полезную нагрузку;
  • у рантайма нет уверенности, произошел ли побочный эффект.

Это уже не "вызов инструментов". Это полноценная дисциплина выполнения.

Модель должна разговаривать не с внешним миром напрямую, а со слоем выполнения

flowchart LR
    A["Подсказка + контекст политики"] --> B["Модель"]
    B --> C["Запрос инструмента"]
    C --> D["Слой выполнения"]
    D --> E["Поиск в каталоге"]
    D --> F["Политика / валидация"]
    D --> G["Повтор / тайм-аут / идемпотентность"]
    G --> H["Внешняя система"]
    H --> D
    D --> I["Структурированный результат инструмента"]
    I --> B

4. Каталог инструментов это интерфейс платформы, а не список случайных функций

Если смотреть на каталог как на "папку с вызовами", он быстро превращается в свалку интеграций. Гораздо полезнее считать его публичным интерфейсом слоя выполнения.

Для нашего сценария поддержки в каталоге полезно явно видеть, что именно умеет агент:

  • check_access_request_status
  • get_user_profile
  • create_support_ticket
  • request_human_approval

У хорошего каталога обычно есть:

  • стабильное имя инструмента;
  • описание назначения;
  • схема входных аргументов;
  • класс риска;
  • уровень побочного эффекта;
  • разрешенные вызывающие стороны или возможности;
  • тайм-аут, политика повторов и ожидания идемпотентности.

Это делает слой выполнения обозримым: команда видит не "что-то там может вызвать модель", а конкретный контракт платформы.

4.1. Слишком большой каталог инструментов ухудшает выбор, а не расширяет свободу

Еще одна очень практичная проблема появляется в тот момент, когда каталог становится слишком большим.

Чем больше инструментов одновременно видит модель:

  • тем больше токенов уходит на их описания;
  • тем длиннее становятся похожие друг на друга контракты;
  • тем труднее различать почти одинаковые возможности;
  • тем сильнее размывается внимание на этапе выбора.

Именно поэтому "давайте просто покажем модели все инструменты" обычно плохо заканчивается. Качество выбора падает не потому, что модель "стала глупее", а потому, что набор кандидатов стал слишком шумным.

Хороший практический паттерн здесь обычно называется семантическая фильтрация инструментов:

  • полный реестр продолжает жить в слое платформы;
  • но конкретному запуску показывают только узкий релевантный поднабор;
  • часто это 3–5 инструментов, а не несколько десятков.

Это особенно важно для перекрывающихся возможностей, где различия тонкие: несколько инструментов поиска, несколько адаптеров записи, несколько похожих действий оркестрации.

И еще одно полезное практическое правило: повторы не лечат плохой выбор инструмента, если сама модель изначально видела слишком шумный каталог.

5. Важно различать инструменты чтения и инструменты записи

Это кажется очевидным, но на практике многие системы описывают их почти одинаково. А зря.

Для того же агента поддержки check_access_request_status и create_support_ticket не просто два инструмента. Это два разных класса риска.

read tools обычно:

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

write tools обычно:

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

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

5.1. Еще одна полезная таксономия: data, action, orchestration

В практическом гайде OpenAI есть еще одно полезное упрощение: инструменты удобно делить не только на read и write, но и по их роли в системе.2

  • data tools читают и возвращают контекст: проверка статуса, извлечение, чтение CRM;
  • action tools меняют внешний мир: создать тикет, отправить письмо, обновить запись;
  • orchestration tools помогают самому рантайму: запросить подтверждение, сделать передачу, вызвать планировщик.

Эти две оси хорошо работают вместе:

  • data tools почти всегда ближе к read;
  • action tools почти всегда ближе к write;
  • orchestration tools могут быть и тем, и другим, но у них отдельный эксплуатационный смысл.

Таксономия паттернов рабочих процессов у Anthropic добавляет сюда еще одну полезную дисциплину.1 Каталог должен не только сообщать модели, какие инструменты вообще существуют. Он еще должен делать явным, в каких паттернах оркестрации эти инструменты безопасно участвовать.

Например:

  • data tool может быть безопасен внутри routing, prompt chaining или parallelization;
  • write action tool может быть допустим только после прерывания на подтверждение или внутри жестко ограниченного рабочего процесса;
  • orchestration tool вроде request_human_approval или handoff_to_specialist меняет сам граф выполнения и потому требует более строгих правил трассировки и владения;
  • паттерн orchestrator-workers может требовать явного безопасного для воркера поднабора каталога, а не всей родительской поверхности инструментов.

Именно поэтому зрелый каталог инструментов со временем перестает быть просто списком вызываемых функций. Он становится граничным контрактом между паттернами выполнения и допустимыми побочными эффектами.

6. Контракт инструмента должен быть скучным и строгим

Одна из худших привычек в агентных системах: позволять модели импровизировать формат вызова.

В хорошем дизайне у инструмента есть контракт:

  • четкие обязательные поля;
  • понятные enum и ограничения;
  • нормальные сообщения об ошибках;
  • явный формат ответа;
  • предсказуемое поведение при тайм-ауте или дубликате запроса.

Для нашего кейса поддержки это может выглядеть так:

tools:
  check_access_request_status:
    description: "Read the current status of an access request"
    kind: "read"
    risk: "low"
    timeout_seconds: 10
    input_schema:
      required: ["request_id", "tenant_id"]
      properties:
        request_id: {type: string}
        tenant_id: {type: string}

  create_support_ticket:
    description: "Create a support ticket in the internal helpdesk"
    kind: "write"
    risk: "medium"
    idempotent: true
    timeout_seconds: 15
    input_schema:
      required: ["title", "queue", "requester_id", "tenant_id", "idempotency_key"]
      properties:
        title: {type: string, maxLength: 200}
        queue: {type: string, enum: ["support", "security", "ops"]}
        requester_id: {type: string}
        tenant_id: {type: string}
        idempotency_key: {type: string}
        description: {type: string}

Это выглядит прозаично. И это хорошо. Чем меньше магии в контрактном слое, тем устойчивее инструментальная часть системы.

7. Слой выполнения должен нормализовать ошибки

Еще один частый провал: каждый внешний сервис возвращает свои ошибки в своем стиле, а агенту пробрасывают это почти без обработки.

Для того же кейса поддержки это легко превращается в хаос:

  • IAM-сервис вернул HTTP 500;
  • helpdesk ответил "created": true, но не прислал ticket_id;
  • старый интеграционный адаптер вернул HTML;
  • тайм-аут случился уже после побочного эффекта;
  • нижестоящий API ответил пустым телом.

Слой выполнения должен превращать это в нормальные типы исходов:

  • success
  • retryable_failure
  • validation_failure
  • permission_denied
  • side_effect_unknown

Это резко повышает объяснимость и позволяет агенту принимать более взрослые решения: повторить, запросить подтверждение, эскалировать человеку или безопасно остановиться.

8. Идемпотентность и повторы нельзя додумывать потом

Почти каждая реальная интеграция рано или поздно дает хотя бы один неприятный сценарий:

  • тайм-аут после того, как побочный эффект уже случился;
  • дубль вызова после повтора;
  • частичный успех;
  • состояние гонки между двумя запусками;
  • внешний сервис ответил позже ожидаемого окна.

Если идемпотентность не заложена в модель выполнения, агент очень быстро начинает делать то, что в обычных системах и так больно чинить: повторно создавать тикеты, дублировать письма, несколько раз менять один и тот же объект.

Для кейса поддержки практическое правило простое: любой инструмент записи, который может создавать тикет, обновлять запись или отправлять сообщение, должен иметь явную стратегию идемпотентности до первого эксплуатационного запуска.

9. Практические правила для слоя выполнения

Если нужен короткий набор правил, который реально помогает в эксплуатации, он обычно такой:

  1. У каждого инструмента должны быть владелец, схема, класс риска и жизненный цикл контракта.
  2. Пути чтения и записи нужно различать до первого запуска, а не после первого инцидента.
  3. Все инструменты записи должны иметь стратегию идемпотентности до того, как их увидит реальный трафик.
  4. Все исходы инструментов нужно нормализовать до передачи обратно модели.
  5. Если побочный эффект уже мог произойти, безопаснее остановиться, чем делать слепой повтор.

10. Что команды чаще всего делают неправильно

Слой выполнения почти всегда ломают одинаково:

  • дают модели прямой доступ к внешнему API;
  • смешивают чтение и запись в одну безликую категорию;
  • позволяют инструментам утекать между паттернами оркестрации без явного ответа, где вообще разрешены маршрутизация, параллелизация, прерывания на подтверждение и делегирование воркерам;
  • прячут повторы глубоко в адаптере без журнала аудита и идемпотентности;
  • возвращают модели сырую полезную нагрузку вместо нормализованных результатов;
  • не назначают владельца и политику вывода из эксплуатации для слоя каталога.

11. Простой кодовый шаблон для слоя выполнения

Ниже не эксплуатационный рантайм, а каркас, который показывает правильное разделение ответственности: поиск, валидацию, выполнение и нормализацию результата.

from dataclasses import dataclass


@dataclass
class ToolSpec:
    name: str
    kind: str
    timeout_seconds: int
    idempotent: bool


@dataclass
class ToolResult:
    status: str
    payload: dict


def execute_tool(spec: ToolSpec, args: dict) -> ToolResult:
    if spec.kind not in {"read", "write"}:
        return ToolResult(status="validation_failure", payload={"reason": "unknown tool kind"})

    if spec.kind == "write" and "idempotency_key" not in args:
        return ToolResult(status="validation_failure", payload={"reason": "missing idempotency key"})

    # In production this call would go through policy checks, a gateway, and typed adapters.
    return ToolResult(status="success", payload={"tool": spec.name})

Важно не то, насколько этот пример "богатый". Важно, что инструмент не исполняется напрямую из решения модели.

12. Результаты инструментов тоже нужно проектировать, а не просто возвращать как есть

Если результат инструмента слишком сырой, у модели снова появляется пространство для опасной импровизации.

Хороший результат:

  • краткий;
  • структурированный;
  • не тащит лишний технический шум;
  • содержит машиночитаемый статус;
  • не прячет неопределенность.

Плохой результат:

  • возвращает всю внешнюю полезную нагрузку простыней;
  • смешивает пользовательский текст и системные детали;
  • не различает "ничего не найдено" и "система упала";
  • не говорит, произошел ли побочный эффект.

13. Каталог инструментов должен эволюционировать медленно

Если инструменты меняются каждый день без совместимости и версионирования, агентная система начинает вести себя как клиент на нестабильном частном API.

Поэтому у слоя каталога полезны такие привычки:

  • версионированные контракты;
  • политика вывода из эксплуатации;
  • владелец у каждого инструмента;
  • тесты на схему и форму результата;
  • разбор возможностей перед добавлением новых инструментов записи.

Это скучная платформа, а не романтическая импровизация. Именно поэтому она работает.

14. Быстрый тест зрелости для инструментального слоя

Команде не стоит считать слой выполнения зрелым только потому, что инструменты уже можно вызывать.

Более сильный стандарт такой:

  • каталог явный и у него есть владелец;
  • различие между семантикой чтения, записи и оркестрации видно сразу;
  • идемпотентность спроектирована до того, как на это укажет инцидент;
  • неопределенность не прячется за фальшивым успехом;
  • модель не становится прямой интеграционной поверхностью.

Если одного-двух пунктов не хватает, система еще может работать. Если не хватает большинства, инструментальный слой пока остается только прототипной обвязкой.

15. Что делать сразу после этой главы

Если хочешь быстро проверить свой слой выполнения, пройди по короткому списку:

  1. Есть ли у тебя отдельный каталог инструментов, а не просто набор функций?
  2. Разделены ли инструменты чтения и записи?
  3. Есть ли валидация схемы для аргументов?
  4. Нормализуются ли ошибки внешних систем?
  5. Учтены ли тайм-ауты, повторы и идемпотентность?
  6. Видно ли, произошел побочный эффект или нет?
  7. Есть ли владелец и жизненный цикл контракта у каждого инструмента?

Если на несколько пунктов подряд ответ "нет", агент у тебя уже умеет вызывать инструменты, но модель выполнения пока еще незрелая.

16. Что читать дальше

Следующие естественные темы в этой части: выполнение в песочнице, MCP как контракт интеграции и правила для повторов и границ отката. Именно там становится видно, как тот же агент поддержки не просто вызывает инструменты, а делает это через зрелый слой выполнения.