• Агент испускает не только текст, но и типизированные компоненты, которые рендерятся в потоке
  • Следующий ход пользователя — это клик со схемой, а не абзац, который нужно парсить
  • Закрытый словарь из 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-компоненты, которые рендерятся прямо в потоке диалога: карточки, опросники, превью, формы. Следующее сообщение пользователя становится кликом внутри компонента, а агент получает структурированное событие вместо абзаца.

Механизм из пяти частей.

  1. Словарь компонентов. Закрытый набор: список идей, опросник, превью промпта, карточка результата, форма публикации, карточка завершения. У каждого JSON-схема и рендерер.
  2. Эмуляция вызова через JSON. Модель испускает огороженный блок с именем компонента и аргументами; сервер вырезает такие блоки из потока и превращает в UI-события. Нативный tool-use API не нужен.
  3. Поток событий. Ответ агента — это SSE-поток из текстовых дельт и вызовов компонентов. Клиент дописывает текст в текущий пузырь до события, рендерит компонент, открывает новый пузырь.
  4. Клик как сообщение. Каждый интерактивный элемент отправляет в эндпоинт агента структурированное событие с известной схемой. Сервер трактует его как следующий ход пользователя, и модели не нужно парсить намерение.
  5. Перенос состояния. Хранилище агента держит артефакты предыдущих компонентов (выбранная идея, черновой промпт, 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