Generative UI в агентном чате
Пользователь пишет агенту «сделай мне постер». Тот спрашивает: «в каком стиле?». Ответ: «ну, современно, но не слишком, может, как тот фильм Вонг Карвая». Агент парсит это в реальный prompt для генерации — плохо. Пользователь перепечатывает. Итерация медленная, потому что каждый шаг — это free-text вход и free-text выход, а агент постоянно угадывает, что имел в виду пользователь. Idea-card с тремя конкретными превью закрыл бы тот же вопрос одним кликом.
Ниже — паттерн: позволить агенту испускать не просто текст, а типизированные UI-компоненты, которые рендерятся прямо в потоке. Следующее «сообщение» пользователя становится кликом по одному из них, а не предложением.
Это generative UI: ситуативные, испускаемые агентом UI-элементы, перемежающиеся с репликами ассистента. Чат-агент может делать больше, чем испускать текст — он может испускать типизированные компоненты, которые рендерятся как карточки, опросники, previews, формы. Следующее сообщение пользователя становится кликом внутри одного из них, а не свободным текстом, и агент получает структурированное событие вместо абзаца, который он должен парсить.
Когда применять
- В разговоре есть точки ветвления, которые лучше выражены кнопками/формами, чем «введите A, B или C».
- У агента есть частичный результат, который стоит показать сейчас (карточка идеи, черновой промпт, ранняя картинка) — чтобы пользователь мог вмешаться до следующей стадии.
- LLM в runtime не поддерживает нативный tool / function calling, но структурные промежуточные состояния всё равно нужны.
Механизм
- Словарь компонентов. Закрытый набор типизированных UI-элементов:
IdeaList,Questionnaire,PromptPreview,ArtworkCard,PublishForm,DoneCard. У каждого — JSON-форма и рендерер. - JSON-эмулированный function calling. LLM испускает ограждённый JSON-блок (например,
```tool\n{"name":"show_ideas","args":{...}}\n```). Сервер парсит такие блоки из потока и превращает в UI-события. Нативный tool-use API не требуется. - Streamed SSE. Ответ агента — Server-Sent-Events-поток смешанных text-delta и tool-call-объектов. Клиент дописывает текст в текущий bubble до tool-события, затем рендерит компонент и стартует новый bubble.
- Click-as-message. Каждый интерактивный элемент в отрендеренном компоненте отправляет структурированное сообщение в эндпоинт агента (например,
{"action":"pick_idea","idea_id":"…"}). Сервер трактует это как следующий ход пользователя, но со известной схемой, так что LLM не парсит intent из свободного текста. - Перенос состояния. Стор агента (по четырёхуровневой иерархии источников) хранит артефакты, произведённые предыдущими компонентами (выбранная идея, черновой промпт, URL сгенерированной картинки), чтобы последующие tool-вызовы получали их как структурированные аргументы, а не как обрывки истории чата.
Почему JSON-эмуляция, а не нативный tool-use
- Работает с любой LLM, включая модели, проксируемые через third-party-хосты или развёрнутые локально, где нативный tool-use API не выставлен.
- Легче дебажить — сырой поток человекочитаемый.
- Тот же fenced-формат работает для стриминга частичного JSON: text-delta появляются внутри ограждения, парсер терпит незавершённые объекты, пока не придёт закрывающее ограждение.
Разобранный словарь компонентов
IdeaList → PromptQuestionnaire → PromptPreview → ArtworkCard → PublishForm → DoneCard. Агент комментирует поток на естественном языке и испускает один из этих компонентов в каждой точке решения. Пользователь выбирает идею → chosenIdea в сторе обновляется → рендерится questionnaire → инструмент craft_prompt стреляет с приложенным полным four-tier-контекстом.
Разговор читается как чат, но форма данных под ним — структурированная.
Pitfalls
- Scroll-jank на hover-тултипах. Всё, что приякорено к chat-bubble, должно переизмеряться на скролле. Capture-phase scroll-listener'ы, обнуляющие hover-state на каждом wheel-tick, саморазрушаются под auto-scroll. Портально в
document.bodyи трекать через rAF +getBoundingClientRect. - Утечка состояния компонента. Когда один и тот же тип компонента рендерится дважды за сессию (пользователь возвращается и выбирает другую идею), каждому экземпляру нужен стабильный key, привязанный к ID хода агента, а не только к типу компонента.
- Fallback-текст. Каждое tool-событие нуждается в текстовом эквиваленте для screen-reader'ов и для replay'а потока, где UI не отрендерился.
Идёт в паре с
- four-tier-prompt-source-hierarchy — что агент переносит между ходами, управляемыми компонентами.
- template-dispatch-prompts — серверная сторона: какой шаблон промпта использовать для каждого click-message.
- two-stage-architect-pattern — generative UI — естественная поверхность для шага «Креативного директора» (карточки / опции) перед шагом «Технического архитектора» (коммит).