- Агент испускает не только текст, но и типизированные компоненты, которые рендерятся в потоке
- Следующий ход пользователя — это клик со схемой, а не абзац, который нужно парсить
- Закрытый словарь из 6 компонентов; вызовы эмулируются JSON-блоком без нативного tool-use
Задача
Пользователь пишет агенту «сделай постер». Агент спрашивает про стиль. Ответ: «современно, но не слишком, может, как у Вонг Карвая». Агент превращает это в промпт для генерации плохо, пользователь перепечатывает, и так по кругу. Каждый шаг — это свободный текст на входе и на выходе, а агент угадывает, что имелось в виду. Карточка с тремя конкретными превью закрыла бы тот же вопрос одним кликом.
flowchart TB
subgraph before[Свободный текст]
direction LR
Q[Вопрос<br/>про стиль] --> T[Абзац<br/>пользователя]
T --> G[Агент<br/>угадывает]
G --> Q
end
subgraph after[Компонент]
direction LR
K[Карточка<br/>с тремя превью] --> C[Один клик]
end
before ~~~ after
classDef key stroke:#FF3600,stroke-width:2px
class G keyРазговор с генеративным агентом полон точек ветвления (какая идея, какой формат, публиковать или нет) и промежуточных результатов, которые стоит показать до следующей стадии. Текст для этого — плохой носитель.
Приём
Позволить агенту испускать типизированные UI-компоненты, которые рендерятся прямо в потоке диалога: карточки, опросники, превью, формы. Следующее сообщение пользователя становится кликом внутри компонента, а агент получает структурированное событие вместо абзаца.
Механизм из пяти частей.
- Словарь компонентов. Закрытый набор: список идей, опросник, превью промпта, карточка результата, форма публикации, карточка завершения. У каждого JSON-схема и рендерер.
- Эмуляция вызова через JSON. Модель испускает огороженный блок с именем компонента и аргументами; сервер вырезает такие блоки из потока и превращает в UI-события. Нативный tool-use API не нужен.
- Поток событий. Ответ агента — это SSE-поток из текстовых дельт и вызовов компонентов. Клиент дописывает текст в текущий пузырь до события, рендерит компонент, открывает новый пузырь.
- Клик как сообщение. Каждый интерактивный элемент отправляет в эндпоинт агента структурированное событие с известной схемой. Сервер трактует его как следующий ход пользователя, и модели не нужно парсить намерение.
- Перенос состояния. Хранилище агента держит артефакты предыдущих компонентов (выбранная идея, черновой промпт, URL картинки), чтобы следующие вызовы получали их как аргументы, а не как обрывки истории.
sequenceDiagram
participant M as Модель
participant S as Сервер
participant C as Клиент
M->>S: текст и JSON-блок
S->>C: SSE-поток и компонент
C->>S: клик, событие со схемой
S->>S: артефакт в хранилище
S->>M: следующий ход с артефактамиДве стороны одного хода:
блок в потоке от модели:
{"name":"show_ideas","args":{"ideas":[{"id":"a1","title":"…"}]}}
событие клика от клиента:
{"action":"pick_idea","idea_id":"a1"}
Почему эмуляция, а не нативный tool-use: работает с любой моделью, включая проксируемые через сторонние хосты и локальные; сырой поток человекочитаем и легко отлаживается; тот же формат тянет частичный JSON — парсер терпит незакрытый объект до закрывающего ограждения.
Цепочка в продукте: список идей → опросник → превью промпта → карточка результата → форма публикации → завершение. Агент комментирует поток естественным языком и в каждой точке решения испускает один компонент. Пользователь выбрал идею, хранилище обновилось, отрисовался опросник, инструмент сборки промпта получил полный контекст. Разговор читается как чат, форма данных под ним — структурная.
flowchart TB
subgraph pick[Выбор]
direction LR
I[Список идей] --> Q[Опросник] --> P[Превью<br/>промпта]
end
subgraph res[Результат]
direction LR
R[Карточка<br/>результата] --> F[Форма<br/>публикации] --> Z[Завершение]
end
P --> RКогда применять: есть точки ветвления, которые лучше выражены кнопками, чем «введите A, B или C»; есть частичный результат, который стоит показать сейчас; модель в рантайме не поддерживает нативный tool calling, а структурные промежуточные состояния нужны.
Где ломается
- Компонент без текстового эквивалента. Каждому событию нужен текстовый дубль для экранных дикторов и для повтора потока там, где UI не отрисовался.
- Словарь без границ. Открытый набор компонентов превращает агента в генератор разметки, и сервер перестаёт понимать, что рендерить. Словарь закрытый, добавление компонента — это изменение контракта.
- Клик не попадает в состояние. Если структурное событие просто дописывается в чат как текст, модель снова парсит намерение, и выигрыш теряется. Событие должно попадать в хранилище и в аргументы следующего вызова.
Итог
- Компоненты вместо абзацев в точках ветвления; клик вместо свободного текста на входе.
- Эмуляция вызова JSON-блоком делает паттерн независимым от модели и провайдера.
- Закрытый словарь из 6 компонентов и хранилище состояния держат разговор структурным.
© Александр Никулин. Цитирование — с указанием автора и ссылкой · Версия для LLM