Запрос пользователя — не одиночный вопрос, а сообщение в диалоге, и диалогом владеет Query Engine: помнит историю, переносит контекст между сообщениями, держит сессию. Но сам ход ведёт chat-модель пользователя — у неё в руках инструмент поиска по базе знаний, и она решает на каждом сообщении: ответить из себя или обратиться к хранилищу. Все surface'ы — Web App, Slack, Telegram, Mattermost, MCP, расширение — лишь тонкие окна в одну модель диалога; состояние живёт здесь, в своих таблицах (conversations · messages), а не на клиенте.
Диалог stateful: сессия группирует шаги одного разговора, история накапливается сообщение за сообщением. Где-то это состояние должно жить — и живёт оно у Query Engine, единственного модуля, который видит и запрос, и собранный ответ. Дом данных — собственные таблицы: сессии и сообщения с их ответами.
Беседа — устойчивый тред, а не эфемерная сессия входа:
живёт, пока её не удалят, и не истекает по простою (тайм-аут — про вход
в систему, не про разговор). Границу между «новой» беседой и
«продолжением» задаёт не отдельная команда, а наличие или
отсутствие ссылки на беседу (conversation_id) в
запросе: реплика со ссылкой дописывает существующую, реплика без неё
открывает новую.
Беседа создаётся лениво — на первом сообщении: пустых тредов не заводим, строка conversations рождается, когда отправлена первая реплика, и её идентификатор возвращается клиенту для следующих ходов. Кнопка «Новый чат» ничего не открывает на сервере — она лишь сбрасывает активную ссылку на клиенте, а новую беседу родит уже первое отправленное сообщение.
Отсюда — единый рычаг на все каналы: «новый чат», «clear», «resume» не отдельные операции API, а выражения одного и того же «какую ссылку шлёт клиент». Что считать новой беседой, решает surface:
/new
сбрасывает активную ссылку, новую беседу родит следующее сообщение
(перехват на стороне поверхности, в историю не идёт).
Управление беседами — список, переименование, удаление, срок хранения —
живёт в оболочке Web App и проектируется вместе с ней; модель данных к
этому готова (каскад уходит за conversations). Здесь — лишь
правило, где проходит граница одного разговора.
Не каждое сообщение — запрос к базе. «Спасибо», «переформулируй абзац»,
«что такое OKR вообще» — обычный разговор, которому хранилище не нужно.
А «какой дедлайн проекта Аякс» — вопрос к корпоративным знаниям.
Поэтому ход ведёт chat-модель: в руках у неё набор
инструментов — search_knowledge (когда
база непуста) и, если администратор включил,
web_search — и она
решает на каждом сообщении: ответить из себя или обратиться к ним.
Разговор → отвечает без поиска и без цитат. Вопрос к знаниям → зовёт
инструменты — при необходимости несколько разом, одним
раундом — и строит ответ на том, что они вернули.
search_knowledge · web_search
search_knowledge ± web_search)
ответ строго из найденного ·
с цитатой на источник
Смещение в сторону поиска. На грани «болтовня это или
запрос» модель склоняется искать: пустой поиск дёшев, а выдуманный
корпоративный факт дорог — рушит доверие. Это — когда есть что искать:
при пустой базе search_knowledge модели не подаётся вовсе
(производно от
is_empty), смещение неприменимо, и модель отвечает
из общих знаний как обычный ассистент. Что именно значит grounding
в каждом режиме и как звучит честное «не нашёл» — держит
grounding. Какие
инструменты вообще даны чату — решает администратор:
web_search по умолчанию выключен, и выключенный инструмент
модели не подаётся
(каталог инструментов).
Где один раунд инструментов кончается и начинается полный агентный
цикл — граница с Agent Engine.
Внутри сессии сообщения связаны: «а что у него по срокам?» имеет смысл
только на фоне предыдущего. Поэтому, решив искать, модель формулирует
самостоятельный запрос — понятный без истории — и
передаёт его аргументом в search_knowledge. Отдельного
шага переписывания нет: модель и так держит историю диалога перед
глазами, так что подставляет опущенный субъект сама, тем же ходом, что
решает позвать поиск. Без этого «а ещё?» ушло бы в retrieval как «ещё»
— вектор поймал бы реплику, а не тему разговора.
Что происходит при вызове инструмента — embed, поиск под правами, упаковка, grounding-генерация — держит маршрут поиска. Сколько прошлых ходов доедет до модели в длинном разговоре — вопрос бюджета окна.
Окно chat-модели не безразмерно: всё, что она видит за один ход, делит общий потолок токенов. На grounding-ходе в него разом ложатся system-инструкции, история диалога, текущее сообщение и найденный контекст — и надо ещё оставить место под сам ответ. Поэтому переполнение — не «слишком длинная история», а конкуренция за единый бюджет: два потребителя растут независимо, а режутся под один потолок.
search_knowledge
фикс
Усечение окна — не потеря памяти. Дословная переписка живёт в messages, снимки retrieval — в retrieval_trace, а факты ответа перевыбираются свежим retrieval из Knowledge Store на каждом grounding-ходе. Окно — рабочий буфер одного вызова, не хранилище: выпавшее из него остаётся в базе. Оттого историю здесь можно усекать смелее, чем в чистом чат-боте, где она — единственная память.
v2 — running summary старых ходов: когда даже усечённая история перестаёт влезать, ранние ходы сворачиваются отдельным вызовом в краткое резюме, и в окно идёт «резюме + последнее дословное окно». Надстройка над token-window, не замена: добавляет глубину памяти ценой лишнего вызова и риска растерять низкочастотную, но важную деталь при повторных сжатиях.
Точек входа в диалог несколько — Web App Slack Telegram Mattermost Browser Extension — и все они тонкие клиенты: рисуют сообщения и стримят ответ, но логики диалога не несут. Они делят одну модель диалога через единый внутренний API Query Engine. Сессии, история, перенос контекста, решение о поиске — всё на стороне Query Engine; surface лишь открывает сессию, шлёт реплику и показывает ответ.
Так разные каналы не расходятся в поведении: правило диалога реализовано один раз, а не по копии в каждом клиенте. Новый surface подключается тем же API и получает ту же память разговора даром — в том числе кастомная поверхность заказчика через публичный контракт. → Public API
MCP сюда не входит. Это поверхность иного рода — не диалоговый клиент, а поставщик инструмента поиска внешней модели: держит не беседу, а tool-call, и возвращает находки, а не готовый ответ. → MCP
Ответ отдаётся синхронно, потоком: токены grounding-ответа текут в surface по мере генерации, без постановки в очередь задач.
Slack — исключение. Мессенджер не держит соединение и ждёт ответ за 3 секунды: стрим собирается на сервере и возвращается постом в тред — приём подтверждается сразу, обработка идёт отложенно через очередь. → Slack
У каждого ответа — лёгкая оценка: thumbs up / down. Это не действие над диалогом, а сигнал качества: понравился ответ или промахнулся. Оценка ложится рядом с самим сообщением в хранилище диалога — копим её на будущее, для оценки качества выдачи и настройки ранжирования.