ИИ-бота для поддержки обычно продают так: подключите источники данных, мы их проиндексируем, и ваш бот будет отвечать на их основе.
Перечитайте это глазами инженера. Подключите источники данных значит «сделайте выгрузку». Мы их проиндексируем значит, что данные ваших клиентов теперь живут ещё и в системе вендора. А «отвечать на их основе» значит отвечать по тому, что успела скопировать последняя синхронизация.
Для документации такая сделка нормальна: статьи меняются раз в неделю, и индекс им подходит. Для живых данных это плохая сделка. Меняются цены. Меняются остатки. Статус заказа меняется, пока клиент печатает вопрос. Индекс — это фотография того, что движется.
Другой вариант
Здесь есть идея гораздо старше, и она не требует никуда переносить данные: дать модели функцию, и пусть модель её вызывает.
Именно так вызов инструментов (tool calling) и работает с самого появления. Необычно здесь то, где выполняется функция. В большинстве реализаций она выполняется на серверах вендора, и вы снова выдаёте им доступы, добавляете их IP в белый список и строите интеграцию.
В Kookee она выполняется в браузере посетителя.
KookeeChat.init({
apiKey: 'YOUR_API_KEY',
tools: [{
name: 'search_orders',
description: "Look up the signed-in customer's orders.",
parameters: { type: 'object', properties: { status: { type: 'string' } } },
handler: ({ status }) =>
fetch('/api/my/orders?status=' + status, { credentials: 'include' })
.then((r) => r.json()),
}],
});
Посмотрите, что такое этот handler. Это fetch к вашему API с credentials: 'include', который выполняется на вашей странице от имени посетителя, который уже вошёл в систему.
Отсюда три следствия, и в них весь смысл:
Вы не строили интеграцию. Никакого обмена API-ключами, белого списка IP, вебхука или OAuth-приложения. Вы вызываете эндпоинт, который ваш фронтенд и так вызывает.
Kookee не хранит ваши данные. До Kookee доходят название инструмента, описание и схема параметров — этого модели хватает, чтобы знать, что инструмент есть и что ему передать. Обработчик выполняется в браузере, его результат проходит через Kookee к модели ради этого ответа и не сохраняется. Вашей таблицы заказов нет в базе Kookee, потому что я её и не просил.
Авторизация уже решена. Самый трудный вопрос в «ИИ, который видит данные клиентов», — какого именно клиента. Здесь такого вопроса нет. Запрос несёт собственную сессионную cookie посетителя, поэтому возвращает его заказы и ничьи больше. За этим следит тот же код авторизации, что защищает остальное ваше приложение.
Как это выглядит для посетителя
Посетитель: где мой последний заказ?
Проверяю ваши заказы…
Ассистент: Ваш заказ #4821 отправлен 21 августа, доставят во вторник. Трек-номер: DHL 0034…
Самое интересное — пауза посередине. Ответ действительно прерывается: модель запрашивает инструмент, генерация останавливается, браузер выполняет вашу функцию, результат возвращается, и ответ продолжается. Посетитель видит один непрерывный ответ со строкой статуса.
Почему я сделал именно так
В первом варианте между двумя половинами ответа история диалога уходила обратно в браузер, и клиент присылал её снова. Без состояния, без памяти на сервере, красиво на доске.
Два ревьюера этот вариант забраковали, и их причины стоит повторить:
Он допускал утечку. Поиск Kookee по статьям справки работает на сервере и возвращает полный текст статей, включая статьи chatbot_only, которых нет в публичном центре помощи. Если бы сообщения хода шли через браузер, эти статьи увидел бы во вкладке Network любой, кто открыл devtools.
Его можно было подделать. Если состояние диалога хранит клиент, клиент может его изменить. Удалите несколько записей, и счётчик раундов сбрасывается — бесконечный цикл вызовов модели за чужой счёт.
Поэтому приостановленный ход живёт на сервере, в Redis, за одноразовым токеном с коротким сроком жизни. Браузер получает только необходимое: какие функции вызвать, с какими аргументами, и непрозрачный токен, чтобы продолжить. Результатов серверных инструментов он не видит и переписать ход не может.
В проверке токена есть своя тонкость, и сначала я в ней ошибся: исходный код удалял токен до того, как проверял, какому проекту он принадлежит. Угадать случайный 24-байтовый токен никто бы не смог, но запрос с чужим проектом всё равно уничтожил бы законный незавершённый ход. Прочитать, проверить, потом удалить.
Ограничения и где они заканчиваются
Что Kookee проверяет сам:
Результаты инструментов оборачиваются как недоверенные данные, а не как инструкции.
Из них вырезаются маркеры цитирования, чтобы результат инструмента не мог выдумать источник.
Обработчик прерывается через 15 секунд, результат обрезается до 4000 символов.
Если инструмент выбрасывает исключение, ошибка уходит модели, а ответ не обрывается.
И одного я намеренно не делаю: перед запуском инструмента нет автоматического подтверждения. Если вы зарегистрируете что-то с побочными эффектами — отмену заказа, отправку письма, — оно выполнится, как только модель решит его вызвать.
Я думал добавить окно подтверждения для каждого инструмента и отказался. Подтверждение перед поиском товаров, который только читает данные, — лишнее трение без всякой пользы, а по JSON-схеме я не могу отличить одно от другого. Вы можете. Поэтому инструменты для чтения регистрируйте свободно, а в каждый обработчик, который что-то записывает, добавьте window.confirm.
Когда это не нужно
Если ответ есть в документации, напишите статью. Инструменты — для данных, которые меняются быстрее, чем вы успеваете публиковать: цены, остатки, статус заказа, состояние аккаунта. Всё стабильное дешевле, быстрее и надёжнее оформить статьёй справки, и ассистент сошлётся на неё ссылкой, которую посетитель сможет открыть.
Правило, которым я пользуюсь: если для нового ответа нужен деплой — это инструмент. Если хватит правки страницы — это статья.
Уже доступно в @kookee/react 1.5.0 и @kookee/sdk 1.5.0, а также в теге script вообще без установки. До десяти инструментов на виджет — больше, чем нужно любому ассистенту поддержки из тех, что я видел.