WIIN.AGENCYвнедряем ИИ
в продажи

Как ИИ-ассистент выбирает действие и работает с инструментами

В нашем ассистенте LLM интерпретирует запрос и возвращает структурированное решение, а серверный код проверяет контекст, права и выполняет выбранный инструмент. Разбираем весь production-флоу на реальном сбое с пересланной Trello-карточкой.

ИИ-ассистент выбирает действие в два этапа. Сначала языковая модель интерпретирует диалог и возвращает строгое JSON-решение с ответом, названием инструмента и аргументами. Затем серверный код проверяет это решение, права пользователя и доступные интеграции, после чего либо задаёт вопрос, либо выполняет конкретное действие.

Это принципиальная граница. Модель может решить, что пользователь хочет создать задачу, найти клиента или назначить встречу. Но сама она не пишет в Trello, CRM или календарь. Запись делает обычный код через API, с проверкой данных и подтверждением там, где оно нужно.

Ниже разбираем текущий production-флоу Telegram-ассистента wiin.agency. Без архитектурных облаков из пяти прямоугольников, где между «пользователем» и «магией ИИ» почему-то всегда нарисована одна стрелка.

Короткая схема работы

Один пользовательский запрос проходит такой путь:

  1. Telegram присылает update с текстом, голосом, вложением, пересылкой или ответом на сообщение.
  2. Детерминированный маршрутизатор распознаёт специальные сценарии, которые не требуют догадки модели.
  3. Сервер определяет пользователя, арендатора системы, роль и доступные интеграции.
  4. Redis-блокировка не даёт двум сообщениям одного пользователя обрабатываться одновременно.
  5. Ассистент достаёт короткую историю диалога и ожидаемые подтверждения.
  6. Сервер собирает живой контекст: задачи Trello, участников доски, колонки, часовой пояс и каталог разрешённых MCP-инструментов.
  7. LLM возвращает JSON: что сказать, какой action выбрать и с какими аргументами.
  8. Код разбирает и валидирует решение.
  9. Для действия с внешним эффектом ассистент получает подтверждение, если оно предусмотрено сценарием.
  10. Исполнитель вызывает Trello, Google Calendar, CRM, базу знаний или другой сервис.
  11. Ответ отправляется в Telegram, а вызов модели и технический результат попадают в логи.

На практике часть запросов заканчивается раньше седьмого шага. Это хорошо. Если карточку Trello можно прочитать напрямую по URL, звать LLM для угадывания содержимого карточки дороже и менее надёжно, чем сделать один GET-запрос к Trello API.

Кто именно определяет action

Внутри ассистента есть метод принятия решения. Он получает историю разговора, данные пользователя, текущие задачи и список инструментов. Затем отправляет модели системную инструкцию и диалог.

Модель должна вернуть объект примерно такого вида:

{
  "say": "Я понял так: ставлю задачу на Дмитрия без срока. Ок?",
  "tool": null,
  "args": {
    "title": "Проверить форму регистрации",
    "assignee": "Дмитрий",
    "dueIso": null
  },
  "done": false,
  "newTopic": false,
  "buttons": [
    { "label": "✅ Да", "value": "да" },
    { "label": "✖️ Нет", "value": "нет" }
  ]
}

На первом ходе tool равен null: ассистент только показывает своё понимание. После ответа «да» модель возвращает tool: "create_task", а сервер создаёт карточку.

Для других запросов возможны actions update_task, comment_task, complete_task, list_my_tasks, create_event, schedule_team_meeting, add_knowledge, content_idea и универсальный mcp. Если инструмент не нужен, модель отвечает с tool: null.

Получается три разных слоя ответственности:

Слой Что делает Чего не должен делать
LLM Понимает намерение, выбирает action, заполняет аргументы, формулирует ответ Напрямую менять данные во внешней системе
Оркестратор Собирает контекст, проверяет JSON, управляет подтверждением и памятью Додумывать смысл сложной свободной фразы набором хрупких if
Исполнитель инструмента Вызывает API Trello, CRM, календаря или базы знаний Самостоятельно менять намерение пользователя

Именно LLM чаще всего «додумывает». Код ограничивает пространство этого додумывания схемой, контекстом и доступным набором действий. Если ограничений мало, модель может уверенно выбрать не тот action. Красивый JSON от этого не становится правильным.

Что происходит до вызова модели

Не каждый входящий текст сразу уходит в LLM. Сначала работает программный маршрутизатор. Он обрабатывает команды, нажатия кнопок, ожидаемые ответы, встречи и известные форматы ссылок.

Например, после предложения подтвердить внешнее действие состояние pending хранится на сервере. Следующее «да» обрабатывается как подтверждение именно этого действия. Пользователь не обязан заново описывать задачу или публикацию.

Пересланные сообщения складываются в очередь и могут обрабатываться по порядку. Telegram передаёт происхождение пересланного сообщения в поле forward_origin, а исходное сообщение при ответе доступно через reply_to_message. Это позволяет отличать слова пользователя от приложенного контекста. Формат этих полей описан в официальной документации Telegram Bot API.

Вложения тоже считаются данными, а не инструкциями. Текст, распознанный на изображении или в PDF, добавляется в контекст с отдельной меткой. Команды внутри такого файла не должны исполняться. Это защита от косвенной prompt injection, когда вредная инструкция попадает не в сообщение пользователя, а в документ или веб-страницу. OWASP относит prompt injection к ключевым рискам приложений с LLM и отдельно описывает непрямые инъекции через внешние источники в LLM01: Prompt Injection.

Есть и совсем прямые маршруты. Если пользователь сначала спрашивает «о чём эта задача», а следующим сообщением пересылает Trello-ссылку, сервер теперь читает карточку через API и возвращает название, описание, срок и статус. Модель в этом пути не участвует.

Почему сообщения одного пользователя блокируются

Telegram может доставить два сообщения почти подряд. Первое ещё обрабатывается, а второе уже пришло. Без синхронизации обе обработки увидят одинаковую старую историю и примут решения независимо.

Отсюда появляются странные эффекты:

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

Поэтому на пару tenant + chat + user ставится Redis-lock. Текущая реализация ждёт освобождения до 30 секунд, а срок жизни блокировки составляет 45 секунд. Блокировка создаётся командой SET с параметрами NX и PX, а снимается только владельцем по уникальному токену через Lua-скрипт. Такой паттерн совпадает с базовой схемой безопасной блокировки из документации Redis.

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

Как устроена память диалога

История хранится в Redis отдельно для каждого пользователя и чата. В модель попадают последние 16 реплик. Жёсткий TTL истории равен 30 минутам, а пауза дольше 10 минут считается началом нового разговора.

Кроме обычных реплик, состояние может содержать:

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

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

Если пользователь явно меняет тему, модель возвращает newTopic: true. Оркестратор оставляет только последнюю реплику и не переносит старый контекст дальше.

Как собирается живой контекст

Перед решением сервер параллельно загружает рабочий контекст и настройки персоны ассистента.

Для Trello в контекст входят:

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

Для диалога добавляются имя собеседника, его роль, часовой пояс и текущее локальное время. Фраза «поставь на меня завтра» после этого превращается в конкретного участника доски и ISO-дату, а не в философский вопрос о природе слова «завтра».

Владельцу аккаунта дополнительно показывается каталог доступных MCP-инструментов с их схемами. Обычный менеджер получает более узкий набор возможностей. MCP описывает инструменты как вызываемые функции с именем, описанием и inputSchema; сам протокол рекомендует валидировать входы, контролировать доступ, ограничивать частоту вызовов и показывать чувствительные операции пользователю. Эти требования есть в официальной спецификации MCP Tools.

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

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

Какая модель принимает решение

Для диалогового шага используется стратегия BALANCED. Сейчас production-цепочка такая:

  1. Fireworks gpt-oss-20b.
  2. Fireworks gpt-oss-120b.
  3. Mistral Medium.
  4. OpenAI.
  5. xAI.

Фактический состав цепочки зависит от настроенных ключей. Если провайдер недоступен, его нет в цепочке. Первый вариант выбран как дешёвый и достаточно быстрый для классификации намерения и заполнения JSON. Более тяжёлая модель и другие провайдеры нужны как fallback.

Fireworks поддерживает JSON mode и вывод по JSON Schema. В документации провайдера это называется Structured Outputs: формат ответа ограничивается схемой, чтобы результат можно было разобрать программно.

В нашем текущем вызове модель получает требование вернуть строго JSON. Затем парсер приводит значения к разрешённому набору actions. Если ответ не разбирается, запускается отдельная попытка repair с той же схемой. Если и она не удалась, ассистент ничего не выполняет и просит сформулировать запрос ещё раз.

Важно не смешивать два типа fallback:

  • provider fallback срабатывает, когда API модели вернул ошибку, таймаут или недоступен;
  • JSON repair срабатывает, когда модель ответила, но нарушила контракт формата.

Ни один из них сам по себе не исправляет логическую ошибку. Валидное create_task может быть совершенно неуместным. Именно это произошло в кейсе с Trello-ссылкой.

Как работает fallback провайдеров

Оркестратор идёт по цепочке моделей по порядку. Для первой модели разрешён один повтор через две секунды, но только при временной ошибке: таймауте, rate limit или ответе сервера класса 5xx.

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

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

Это отличает рабочий production-контур от демонстрации, где в коде написано model: "best-ai-ever" и считается, что вопрос надёжности закрыт.

Подтверждение и выполнение действия

Подтверждение зависит от типа действия.

Для создания Trello-задачи используется двухходовый диалог:

  1. Модель формулирует, как поняла название, исполнителя и срок. Action пока не вызывается.
  2. Пользователь отвечает «да», «ок» или нажимает кнопку.
  3. На следующем ходе модель возвращает create_task с аргументами.
  4. Сервер ещё раз сопоставляет исполнителя и колонку с живой доской.
  5. Trello API создаёт карточку.
  6. Пользователь получает ссылку на созданную задачу.

Для универсальных MCP-инструментов подтверждение задаётся серверными метаданными риска. Публичное, внешнее или необратимое действие сохраняется как pending. Сервер показывает понятное описание и ждёт согласия. После «да» выполняются сохранённые имя инструмента и аргументы, а не новая догадка модели.

У MCP есть хороший принцип: подтверждение должно быть частью приложения, а не надеждой на послушание модели. Спецификация прямо рекомендует human-in-the-loop для операций инструментов и подтверждение чувствительных действий пользователем.

Схожая граница полезна при любой интеграции ИИ с CRM. LLM решает, что сделать. Коннектор решает, можно ли это сделать и как именно записать данные.

Разбор сбоя с пересланной Trello-карточкой

Пользователь написал: «о чем эта задача». Ассистент ответил, что не знает, о какой задаче идёт речь. Затем пользователь переслал ссылку на карточку Trello.

Вместо чтения карточки ассистент предложил создать новую задачу с названием Review Trello task <URL> и показал кнопки подтверждения.

По production-логам оба решения приняла Fireworks-модель accounts/fireworks/models/gpt-oss-20b. Первый вызов занял около 1,5 секунды, второй около 7,4 секунды. Fallback на другую модель не происходил.

Почему так случилось:

  1. Первый вопрос попал в историю без самой ссылки.
  2. Пересланная ссылка пришла отдельным сообщением через очередь пересылок.
  3. У модели был action create_task и подробная инструкция по работе с задачами.
  4. Не было прямого инструмента «прочитать эту карточку по URL» до этапа LLM.
  5. Модель связала слово «задача» и Trello-ссылку с самым близким доступным action.

Это не проблема «старой» или «глупой» модели в чистом виде. Более крупная модель могла угадать намерение лучше, но архитектурная неоднозначность осталась бы. Когда факт можно получить из API, заставлять модель угадывать этот факт по URL неправильно.

Исправление состоит из двух частей.

Первая часть детерминированная. Если предыдущая реплика похожа на запрос чтения карточки, а в следующей пересылке есть URL вида trello.com/c/..., сервер извлекает short link и вызывает GET /1/cards/{id}. Официальный Trello API поддерживает получение карточки по GET /cards/{id}, что описано в Cards API. Ответ строится из реальных полей name, desc, due, closed и url.

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

Главное исправление здесь первое. Промпт помогает, но прямой маршрут гарантирует поведение для известного сценария.

Что логируется и как найти виновника

Каждый вызов LLM записывается в журнал с полями:

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

Сервисные логи отдельно показывают время ожидания turn lock, полную длительность хода и факт fallback. По связке времени, пользователя и операции можно восстановить путь запроса.

Так мы и установили, что ошибочный action выбрала именно gpt-oss-20b, а не Mistral, OpenAI или xAI. Название первой модели в конфиге ещё ничего не доказывает. Доказывает запись о фактическом вызове.

Для публичных и внешних действий MCP-вызовы также проходят через контроль scope и аудит. Ключ API связан с конкретным пользователем, а его scopes дополнительно ограничивают операции уровня read, write:safe и write:full.

Где остаются риски

Даже после исправления ассистент не становится формальной системой доказательств. Основные риски остаются понятными.

Неверная классификация намерения

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

Правильный action с неправильными аргументами

Модель может выбрать update_task, но перепутать карточку или исполнителя. Поэтому сервер сопоставляет значения с живым списком и просит уточнение при нескольких совпадениях.

Инструкция внутри внешних данных

Пересылка, документ и распознанное изображение могут содержать текст, похожий на команду. Такие данные помечаются как evidence-only. Разрешение на действие должно исходить из явной реплики пользователя.

Устаревший контекст

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

Слишком широкие права инструмента

Если ключ умеет удалять всё, ошибка классификации становится дороже. Поэтому права разделяются по пользователям и scopes, а опасные операции требуют подтверждения.

Вопросы защиты данных при внедрении вынесены в отдельный материал «ИИ-агенты и 152-ФЗ». Здесь важен инженерный вывод: безопасность агента складывается из кода, прав, аудита и интерфейса подтверждения. Один системный промпт эту работу не заменяет.

Как мы решаем, что оставить модели, а что вынести в код

Практическое правило простое.

Оставляем LLM задачи, где нужен смысл:

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

Выносим в код задачи, где известен точный алгоритм:

  • извлечь Trello short link из URL;
  • получить карточку по API;
  • проверить роль и scope;
  • не допустить параллельную обработку одного диалога;
  • сохранить и проверить pending confirmation;
  • сопоставить имя с реальным участником доски;
  • вызвать внешний API;
  • записать аудит и метрики.

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

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

Что получает пользователь на выходе

Хороший ответ ассистента содержит только то, что нужно для текущего шага.

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

После завершения темы история очищается или естественно истекает. Пользователь не видит JSON, provider chain и внутренние инструменты. Но при разборе инцидента команда может восстановить каждый технический шаг.

Именно так полезно оценивать ИИ-ассистента: не по тому, насколько человечески он пишет «Принял!», а по тому, можно ли объяснить и проверить, почему он выбрал действие, кто дал разрешение и что в итоге изменилось во внешней системе.

FAQ

Кто выбирает action в ассистенте?

В большинстве свободных запросов action выбирает LLM. Она возвращает структурированное JSON-решение. Для известных сценариев, например чтения пересланной Trello-карточки после вопроса о ней, action определяет детерминированный маршрутизатор без участия модели.

Может ли модель сама создать задачу в Trello?

Нет. Модель только возвращает create_task и аргументы. Карточку создаёт серверный исполнитель через Trello API после предусмотренного подтверждения и проверки подключения пользователя.

Какая модель сейчас обрабатывает диалог?

Первой в стратегии BALANCED вызывается Fireworks gpt-oss-20b. Затем доступны Fireworks gpt-oss-120b, Mistral Medium, OpenAI и xAI. Фактическая модель каждого ответа записывается в журнал вызовов.

Зачем нужен JSON, если модель всё равно может ошибиться?

JSON ограничивает форму ответа и позволяет безопасно маршрутизировать разрешённые actions. Он защищает от сломанного формата, но не гарантирует правильность намерения. Логику нужно дополнительно ограничивать контекстом, правами, подтверждениями и прямыми программными маршрутами.

Почему не отправлять каждый запрос в самую большую модель?

Большая модель дороже и медленнее, но всё равно может ошибиться в неоднозначной архитектуре. Частые точные сценарии надёжнее реализовать кодом, а для остальных запросов использовать недорогую модель с fallback на более сильные.

Что происходит, если Fireworks недоступен?

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

Как ассистент защищается от команд внутри документа или пересылки?

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

Можно ли понять постфактум, почему ассистент ошибся?

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