Назад в блог
AI Agents

Tool calling в AI-агентах: как модель вызывает реальные функции

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

Схема, права, idempotency, read-back и маршруты отказа

Авторский TOOL-GATE-8: учебный контракт, а не клиентские метрики
tool calling AI agents, архитектура вызова функций и контроль инструментов
ТемаScoped tool execution
ФокусTOOL-GATE-8
СтатусPUBLISHED / 2026-09-17
Схема вызова инструмента через проверку схемы и прав к исполнению и проверенному результату
Схема вызова инструмента через проверку схемы и прав к исполнению и проверенному результату
TERMINAL_PREVIEW.LOG
$ call --contract TOOL-GATE-8
> propose: tool / typed arguments
> gate: identity / tenant / policy
> execute: bounded adapter / idempotency
> verify: read-back / receipt / route
Разбор

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

Разберём технический путь от запроса до проверенного результата. Пример ниже синтетический: он показывает проектное решение, не измеренную производительность клиентской системы. Если вы ещё выбираете, нужен ли агент, начните с дерева решений. Широкие вопросы внедрения относятся к AI-специалисту в Армении.

Проблема и требования

Представим запрос: «Найди актуальную карточку поставщика и подготовь изменение адреса». Агенту нужны чтение записи, проверка источника изменения и черновик. Запись в CRM — отдельная операция с разрешением человека. Ответ модели «адрес обновлён» ничего не доказывает: требуется подтверждение системы назначения и повторное чтение. Для задачи задают владельца, tenant, идентификатор объекта, лимит времени, допустимые инструменты и критерий завершения.

Функции полезны там, где нужен внешний факт или действие. Обычный текстовый ответ не должен маскировать отсутствие вызова. И наоборот, не давайте модельному выбору функцию записи только потому, что она доступна в API. Каталог инструментов должен содержать минимальные разрешённые операции для данной стадии.

Архитектура: предложение, шлюз, исполнение, квитанция

Поток начинается с аутентифицированного запроса. Оркестратор формирует контекст с задачей и ограниченным набором схем. Модель выдаёт предложение вызова. Шлюз принимает его как недоверенный ввод: проверяет название, строгую схему, размер, tenant и права пользователя, а также политику для типа действия. Только после этого адаптер вызывает внешний сервис. Результат нормализуется, секреты удаляются, а оркестратор сохраняет квитанцию и передаёт модели только нужный фрагмент.

УзелКонтрактЧто проверять
Модельимя инструмента и JSON аргументыСхема, допустимые значения, лимит шагов
Policy gatewayличность, tenant, действие, объектПрава независимо от модели
Адаптертаймаут, idempotency key, код результатаПовтор, отмена, неизвестный исход
Хранилище состоянияrequest ID, версия, маршрут, квитанцияВосстановление после сбоя
Проверка результатаread-back из системы назначенияДействительно ли действие принято

Эта схема не обещает безопасную автономию сама по себе. Она делает место отказа видимым. Для интеграции процесса смотрите AI-автоматизацию, для управления форматом ответа — prompt engineering.

Авторский пример: TOOL-GATE-8

Минимальный пример использует supplier.lookup как read-only вызов и supplier.draft_change как создание черновика. Запись supplier.commit_change не предлагается модели до одобрения. Все идентификаторы и значения здесь учебные; код показывает контракт, а не готовый SDK.

ts
type ProposedCall = { name: string; args: unknown; requestId: string };
async function execute(call: ProposedCall, ctx: Context) {
  const tool = registry.get(call.name);
  if (!tool) return deny("unknown_tool");
  const args = tool.schema.parse(call.args);
  if (!policy.allows(ctx.user, ctx.tenant, call.name, args)) return deny("forbidden");
  if (tool.risk === "write" && !ctx.approvalFor(call.requestId)) return hold("approval_required");
  const key = `${ctx.tenant}:${call.requestId}:${call.name}`;
  const result = await withTimeout(tool.run(args, { key, tenant: ctx.tenant }), tool.timeoutMs);
  const checked = tool.risk === "write" ? await tool.readBack(args) : result;
  await receipts.save({ key, tool: call.name, route: "verified", resultRef: checked.ref });
  return redact(checked);
}

В рабочей системе ошибка парсинга возвращает структурированный отказ, а не второй вызов с более широкими правами. Idempotency key нужен, чтобы повтор сетевого запроса не создал второе изменение. При таймауте результат может быть неизвестным: сначала запросите статус по тому же ключу, затем решайте о повторе. Журнал хранит ссылки и reason codes, не полные секретные payload.

Ключевые компоненты и границы

Схема инструмента. Имя, назначение, поля, допустимые значения и тип результата должны быть версионированы. Проверка на сервере обязательна даже при строгом ответе модели. Пустая строка, неверный тип, неожиданный tenant и слишком большой payload отклоняются до адаптера.

Права и подтверждение. Пользовательская сессия определяет область данных. Инструкции в документе или ответе инструмента не меняют права. Чтение, создание черновика и окончательная запись — разные capability. Не передавайте сервисный ключ модели и не полагайтесь на текст «пожалуйста, не записывай» как на контроль.

Состояние. Сохраните request ID, выбранный инструмент, версию схемы, idempotency key, причину отказа и терминальный маршрут. Для длинной задачи нужен механизм возобновления, который не повторит уже выполненную запись.

Результат. Нормализуйте ошибки внешнего API. Различайте denied, invalid, timeout, unknown_outcome, completed и verified. После записи прочитайте объект из целевой системы или получите эквивалентное подтверждение. Успех HTTP-вызова сам по себе не доказывает бизнес-результат.

Failure modes: где агент ошибается

  1. Несуществующий инструмент. Модель предлагает имя, которого нет. Возвращайте явный отказ и ограничивайте число повторных предложений.
  2. Неверные аргументы. Схема принимает только объявленные поля; объект или tenant из контекста сервера нельзя подменить текстом.
  3. Prompt injection в результате чтения. Текст поставщика может содержать команды. Рассматривайте его как данные, а не новую инструкцию или разрешение.
  4. Таймаут после записи. Состояние неизвестно. Поиск по idempotency key и read-back предшествуют повтору.
  5. Устаревшая запись. При изменении версии между чтением и записью требуется конфликт и новая проверка, а не тихое перезаписывание.
  6. Ложное завершение. Модель сообщает об успехе без квитанции. UI должен показывать проверенный статус из приложения.
  7. Цепочка расходов. Лимиты шагов, времени и стоимости останавливают цикл независимо от желания модели продолжать.

Тестирование и production-чек

Сначала соберите fixture-набор: корректный вызов, неизвестное имя, неверный тип, чужой tenant, недостаток прав, injected текст результата, таймаут до и после выполнения, дублированный request ID, конфликт версии и невозможность read-back. Для каждой фикстуры укажите ожидаемый маршрут и запрещённый побочный эффект. Проверяйте адаптер и policy gateway отдельно от модели, затем весь цикл с записью квитанции.

Перед релизом зафиксируйте версии схем и политик, владельца инструмента, технический бюджет, процедуру остановки и восстановления, правила redaction и срок хранения журнала. Прогоните staging с тестовыми правами. Пилотируйте только ограниченное множество задач; расширяйте доступ после проверки ошибок и ручной приёмки. Технический аудит может выявить, нужна ли вообще агентная оркестрация: иногда достаточно детерминированного workflow.

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

CODE_BLOCK.TXT
require(schema.valid && policy.allows(user, tenant, tool));
if (tool.isWrite && !approval.valid) return hold;
result = await executeOnce(idempotencyKey);
if (result.unknown) return reconcileBeforeRetry;
return verifyByReadBack(result);