← Query Engine

Диалог

query-engine · workzone

Запрос пользователя — не одиночный вопрос, а сообщение в диалоге, и диалогом владеет Query Engine: помнит историю, переносит контекст между сообщениями, держит сессию. Но сам ход ведёт chat-модель пользователя — у неё в руках инструмент поиска по базе знаний, и она решает на каждом сообщении: ответить из себя или обратиться к хранилищу. Все surface'ы — Web App, Slack, Telegram, Mattermost, MCP, расширение — лишь тонкие окна в одну модель диалога; состояние живёт здесь, в своих таблицах (conversations · messages), а не на клиенте.

Диалогом владеет Query Engine

Диалог stateful: сессия группирует шаги одного разговора, история накапливается сообщение за сообщением. Где-то это состояние должно жить — и живёт оно у Query Engine, единственного модуля, который видит и запрос, и собранный ответ. Дом данных — собственные таблицы: сессии и сообщения с их ответами.

Где начинается беседа

Беседа — устойчивый тред, а не эфемерная сессия входа: живёт, пока её не удалят, и не истекает по простою (тайм-аут — про вход в систему, не про разговор). Границу между «новой» беседой и «продолжением» задаёт не отдельная команда, а наличие или отсутствие ссылки на беседу (conversation_id) в запросе: реплика со ссылкой дописывает существующую, реплика без неё открывает новую.

Беседа создаётся лениво — на первом сообщении: пустых тредов не заводим, строка conversations рождается, когда отправлена первая реплика, и её идентификатор возвращается клиенту для следующих ходов. Кнопка «Новый чат» ничего не открывает на сервере — она лишь сбрасывает активную ссылку на клиенте, а новую беседу родит уже первое отправленное сообщение.

Отсюда — единый рычаг на все каналы: «новый чат», «clear», «resume» не отдельные операции API, а выражения одного и того же «какую ссылку шлёт клиент». Что считать новой беседой, решает surface:

  • Web App — кнопка «Новый чат» и список бесед; clear = сброс активной ссылки.
  • Slack — нативный тред: новый тред или DM — новая беседа.
  • Telegram — DM без тредов: команда /new сбрасывает активную ссылку, новую беседу родит следующее сообщение (перехват на стороне поверхности, в историю не идёт).
  • Mattermost — правило Slack: новый тред или DM — новая беседа.
  • Browser Extension — эфемерно, в рамках страницы или вкладки.

Управление беседами — список, переименование, удаление, срок хранения — живёт в оболочке Web App и проектируется вместе с ней; модель данных к этому готова (каскад уходит за conversations). Здесь — лишь правило, где проходит граница одного разговора.

Модель решает: ответить или искать

Не каждое сообщение — запрос к базе. «Спасибо», «переформулируй абзац», «что такое OKR вообще» — обычный разговор, которому хранилище не нужно. А «какой дедлайн проекта Аякс» — вопрос к корпоративным знаниям. Поэтому ход ведёт chat-модель: в руках у неё набор инструментов — search_knowledge (когда база непуста) и, если администратор включил, web_search — и она решает на каждом сообщении: ответить из себя или обратиться к ним. Разговор → отвечает без поиска и без цитат. Вопрос к знаниям → зовёт инструменты — при необходимости несколько разом, одним раундом — и строит ответ на том, что они вернули.

chat-модель ведёт ход AI в руках 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-инструкции, история диалога, текущее сообщение и найденный контекст — и надо ещё оставить место под сам ответ. Поэтому переполнение — не «слишком длинная история», а конкуренция за единый бюджет: два потребителя растут независимо, а режутся под один потолок.

Контекстное окно · один ход
System prompt платформенный prompt (безопасность + организация) + grounding-инструкции + описание search_knowledge фикс
История диалога прошлые реплики · усекает Query Engine (token-window) растёт по ходам ▲
Текущее сообщение реплика хода всегда целиком
Найденный контекст top-K фрагментов из KS · усекает augment под остаток большой за ход ▲
Резерв под ответ место под генерацию — держится свободным защищён

Усечение окна — не потеря памяти. Дословная переписка живёт в messages, снимки retrieval — в retrieval_trace, а факты ответа перевыбираются свежим retrieval из Knowledge Store на каждом grounding-ходе. Окно — рабочий буфер одного вызова, не хранилище: выпавшее из него остаётся в базе. Оттого историю здесь можно усекать смелее, чем в чистом чат-боте, где она — единственная память.

Общий потолок, усечение по токенам. История и найденный контекст делят один бюджет окна, а не режутся каждый по своему лимиту: augment укладывает фрагменты в остаток, что оставила усечённая история, удерживая резерв под ответ. История усекается с конца по токенам — ранние ходы уходят первыми, текущая реплика всегда целиком. Связную формулировку follow-up это не ломает: пока субъект в окне, модель строит самостоятельный запрос как прежде.

v2running summary старых ходов: когда даже усечённая история перестаёт влезать, ранние ходы сворачиваются отдельным вызовом в краткое резюме, и в окно идёт «резюме + последнее дословное окно». Надстройка над token-window, не замена: добавляет глубину памяти ценой лишнего вызова и риска растерять низкочастотную, но важную деталь при повторных сжатиях.

Один диалог на все surface'ы

Точек входа в диалог несколько — 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. Это не действие над диалогом, а сигнал качества: понравился ответ или промахнулся. Оценка ложится рядом с самим сообщением в хранилище диалога — копим её на будущее, для оценки качества выдачи и настройки ранжирования.