К содержимому журнала

AI и модели

Flutter + Genkit Dart 1.0: от обычного чат-бота к полноценному AI-агенту

Как соединить разговорный интерфейс, интерактивные карточки и серверную логику — и сохранить контроль над действиями приложения.

Flutter и Genkit превращают сообщение в интерфейс выбора времени и подтверждения записи.
Содержание

Представьте приложение для записи на услуги. Пользователь открывает его и пишет:

«Перенеси мою запись на пятницу после обеда».

За этой короткой фразой скрывается целая последовательность действий. Нужно понять, о какой записи идёт речь, определить дату, проверить расписание, найти подходящее время и показать стоимость. Затем получить подтверждение и выполнить перенос.

В обычном приложении пользователь проходит эту последовательность самостоятельно: открывает запись, выбирает календарь, просматривает слоты и нажимает кнопку. AI-ассистент может взять на себя поиск и объяснение вариантов, оставив человеку выбор и подтверждение.

Именно такие сценарии становятся интереснее с выходом Genkit Dart 1.0 . Google объявила о стабильном релизе 8 октября 2026 года. Фреймворк позволяет описывать AI-логику, инструменты и типизированные операции на Dart — языке, который Flutter-команда уже использует каждый день. Официальный анонс .

Попробуем разобраться, как из этого собрать небольшого ассистента для записи на услуги и где проходит граница между возможностями модели и ответственностью приложения.

Разговор становится частью рабочего процесса #

Текстовый интерфейс удобен, когда пользователь ещё не знает, какую кнопку искать.

«Хочу время попозже», «найди что-нибудь рядом с обедом», «можно к тому же специалисту на следующей неделе?» — такие запросы естественно выражаются словами. Превращать каждый из них в отдельный экран и набор фильтров довольно трудоёмко.

Модель может интерпретировать запрос и выбрать следующий шаг. Но для полезного результата ей нужны реальные данные и разрешённые способы их получить.

В Genkit эту роль выполняют tools — Dart-функции, доступные модели. Например, инструмент findSlots обращается к системе записи и возвращает свободное время. Модель решает, когда запросить его, а серверный код определяет, какие данные можно прочитать.

Есть и flows : операции с описанными входными и выходными данными. Они объединяют шаги обработки и позволяют запускать, тестировать и наблюдать за ними отдельно от интерфейса. Документация flows .

Получается вполне знакомая разработчику конструкция: функции, контракты данных и бизнес-правила. LLM занимает в ней конкретное место — помогает работать с запросами на естественном языке.

Что принесла версия 1.0 #

Главное значение релиза — стабилизация основного API Genkit Dart.

Фреймворк предоставляет общий интерфейс к Gemini, Claude, OpenAI и OpenAI-совместимым моделям. Плагины позволяют подключать провайдеров, сохраняя общий способ обращения к ним. При этом смена модели всё равно требует проверки качества: разные модели могут по-разному выбирать инструменты и соблюдать формат ответа. Анонс Genkit Dart 1.0 .

Для Flutter-команд особенно полезна возможность разделять типы между клиентом и сервером. Пакет schemantic генерирует классы, сериализацию и схемы из Dart-деклараций. Вместо двух независимо поддерживаемых описаний API появляется общий контракт. Документация schemantic .

Есть важная оговорка: стабильный статус ядра не распространяется автоматически на весь стек.

Высокоуровневые API агентов, включая defineAgent и remoteAgent , остаются экспериментальными. genkit_a2ui тоже имеет экспериментальный статус, а GenUI для Flutter находится в alpha. Анонс Flutter , статус GenUI .

Поэтому экспериментальный интерфейс стоит подключать поверх самостоятельной бизнес-логики. Перенос записи должен оставаться понятной серверной операцией, даже если способ отображения предложенных вариантов изменится.

Один язык — несколько уровней ответственности #

Для нашего ассистента выберем простую архитектуру.

Flutter отображает разговор и интерактивные элементы. Dart-сервер запускает агента и предоставляет ему инструменты. Система записи хранит расписание, цены и бронирования.

Модель получает только необходимые данные через backend. API-ключ провайдера остаётся на сервере.

textTEXT
Flutter
   │
   │ сообщение пользователя
   ▼
Dart-сервер с Genkit
   ├── обращение к LLM
   ├── поиск свободных слотов
   └── подготовка предложения
   │
   ▼
Flutter: выбор и подтверждение
   │
   ▼
Серверная операция переноса

На каждом запросе backend проверяет пользовательский токен. Затем проверяет доступ к конкретной записи.

Это две разные проверки. Пользователь может быть авторизован в приложении, но не иметь права изменить чужое бронирование. Идентификатор владельца берём из проверенного контекста авторизации, а принадлежность записи проверяем в системе хранения.

Самому агенту дадим инструмент чтения расписания. Перенос будет выполняться отдельной операцией после подтверждения.

Вместо длинного ответа — два понятных варианта #

Вернёмся к запросу пользователя.

Допустим, сервер нашёл два времени: 14:30 за 2 500 рублей и 16:00 за 2 800 рублей. Это условные данные для примера.

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

Здесь появляются A2UI и GenUI .

A2UI описывает интерфейс структурированными сообщениями. GenUI отображает его во Flutter, используя заранее подготовленный каталог виджетов. Модель выбирает компоненты и их параметры, а реализацию компонентов контролирует разработчик. Обзор GenUI .

Каталог задаёт словарь интерфейса. Для сервиса записи он может включать карточку слота, информацию об услуге, форму уточнения и сводку стоимости.

Генерируемый интерфейс особенно полезен там, где запрос уже понятен, а подходящее представление ответа зависит от ситуации.

Для списка свободного времени подойдут карточки. Для уточнения условий — форма. Для сравнения услуг — несколько вариантов рядом.

При этом дизайн остаётся частью приложения: размеры, цвета, доступность и поведение виджетов определяет Flutter-код.

Собираем небольшой агентный сценарий #

Начнём с данных, которыми обмениваются сервер и клиент:

booking_models.dartDART
import 'package:schemantic/schemantic.dart';

part 'booking_models.g.dart';

@Schema()
abstract class $FindSlotsInput {
  String get bookingId;
  String get date; // YYYY-MM-DD
}

@Schema()
abstract class $Slot {
  String get slotId;
  String get startsAt;
  String get timeZone;
  int get priceMinor;
  String get currency;
}

@Schema()
abstract class $SlotsOutput {
  List<$Slot> get slots;
}

Время передаём с явным часовым поясом. Деньги храним целым числом в минимальных единицах валюты: для рублей значение 250000 соответствует 2 500 рублям.

Такая схема помогает согласовать формат данных. Проверка доступности времени и корректности цены остаётся задачей сервера.

Следующий шаг — инструмент поиска. Ниже показан основной фрагмент: repo — адаптер системы записи, а requireUser получает пользователя из проверенного контекста запроса.

dartDART
final findSlots = ai.defineTool(
  name: 'findSlots',
  description:
      'Returns available slots and prices for a booking '
      'on a YYYY-MM-DD date.',
  inputSchema: FindSlotsInput.$schema,
  outputSchema: SlotsOutput.$schema,
  fn: (input, ctx) async {
    final result = await repo.findSlots(
      userId: requireUser(ctx.context),
      bookingId: input.bookingId,
      date: input.date,
    );

    return .response(result);
  },
);

Внутри repo.findSlots проверяются права на запись, дата и правила подбора времени. Модели возвращается результат, пригодный для отображения. Вызов инструментов в Genkit .

Затем подключаем агентный слой и A2UI:

dartDART
import 'package:genkit/genkit.dart';
import 'package:genkit/experimental.dart';
import 'package:genkit_a2ui/a2ui.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';

final ai = Genkit(
  plugins: [googleAI(), A2uiPlugin()],
);

// Здесь регистрируется findSlots.

final bookingAgent = ai.defineAgent(
  name: 'bookingAgent',
  model: googleAI.gemini('gemini-flash-latest'),
  tools: [findSlots],
  maxTurns: 4,
  system: '''
Помоги выбрать время переноса записи.

Получай свободные слоты и цены через findSlots.
Уточняй неоднозначные даты.
Показывай варианты через A2UI.

Кнопки выбора отправляют choose_slot
с bookingId и slotId.
''',
  use: [a2ui(validate: 'strict')],
);

Это фрагменты интеграции, а не готовое приложение: для запуска также нужны серверные маршруты, авторизация, адаптер данных и Flutter-экран.

Обратите внимание на набор инструментов: здесь есть поиск, а операции изменения записи нет. Агент может подобрать варианты, но выполнять перенос будет отдельный серверный endpoint.

Для прототипа можно использовать базовый каталог A2UI. В продукте собственные компоненты помогут точнее контролировать отображение и разрешённые действия. Пакет genkit_a2ui .

Как карточки попадают во Flutter #

На клиенте поток ответа агента передаётся в SurfaceController . Он обрабатывает A2UI-сообщения и обновляет поверхности интерфейса.

Для API GenUI 0.10.4 подключение выглядит примерно так:

dartDART
final catalog = ui.BasicCatalogItems.asCatalog().copyWith(
  catalogId: a2ui.basicCatalogId,
);

final controller = ui.SurfaceController(
  catalogs: [catalog],
);

final turn = chat.sendStream(
  text: 'Перенеси мою запись на пятницу после обеда',
);

await for (final chunk in turn.stream) {
  final envelopes = a2ui.a2uiEnvelopesFromParts(
    chunk.raw.modelChunk?.content,
  );

  for (final envelope in envelopes) {
    controller.handleMessage(
      core.A2uiMessage.fromJson(envelope),
    );
  }
}

await turn.response;

В этом фрагменте ui , a2ui и core — префиксы импортов GenUI, клиентских функций genkit_a2ui и a2ui_core . chat создаётся через remoteAgent , подключённый к нашему backend.

Экран отображает полученные поверхности через Surface и обрабатывает действия пользователя через controller.onSubmit . Идентификаторы каталогов клиента и сервера должны совпадать. Руководство по интеграции A2UI .

Но валидный интерфейс ещё не гарантирует достоверность содержимого. Режим strict проверяет структуру сообщений и имена компонентов; цена, URL и другие значения требуют собственных проверок. Границы валидации .

Это особенно важно на следующем шаге — перед изменением записи.

Подтверждаем конкретные условия #

Пользователь нажимает на 14:30. Приложение отправляет идентификатор слота серверу, а сервер готовит предложение переноса.

В нём фиксируются исходная запись, новое время, стоимость и срок действия. Предложение получает собственный proposalId и связывается с пользователем.

После этого Flutter показывает привычный диалог:

Данные для этого экрана приходят из проверенного серверного предложения. Согласие относится к конкретным условиям, которые человек видит перед собой.

После нажатия «Перенести» backend снова проверяет:

  • предложение принадлежит текущему пользователю;
  • срок его действия не истёк;
  • запись ещё находится в ожидаемом состоянии;
  • выбранное время доступно;
  • стоимость соответствует подтверждённой.

Проверка и изменение должны выполняться атомарно. Иначе между ними другой пользователь может занять слот.

Если условия изменились, приложение показывает новое предложение. Повторный запрос с уже выполненным proposalId возвращает сохранённый результат, чтобы сетевой сбой не привёл к повторному выполнению.

Для более длинных агентных процессов Genkit предоставляет interrupts : инструмент возвращает .interrupt(...) , выполнение приостанавливается и затем возобновляется после ответа человека. Документация interrupts .

Но возобновление тоже требует серверной проверки разрешения. Флаг approved: true сам по себе не доказывает согласие на конкретную операцию.

В нашем небольшом сценарии отдельный endpoint подтверждения делает процесс проще: агент подбирает время, пользователь выбирает, сервер выполняет согласованный перенос.

За скорость отвечает вся архитектура #

Каждое обращение к модели добавляет задержку и расход токенов. Поэтому полезно заранее определить, какие задачи действительно требуют LLM.

Понимание фразы «после обеда» и уточнение пожеланий — подходящие задачи для модели. Расчёт стоимости, проверка расписания и перенос записи хорошо выполняются обычными Dart-функциями.

После нажатия на карточку приложение уже располагает структурированными данными. Повторно интерпретировать их моделью обычно не требуется.

Streaming позволяет раньше показать текст или часть интерфейса. Но полное время операции зависит и от модели, и от базы данных, и от количества инструментальных вызовов.

Во время разработки Genkit Developer UI помогает исследовать выполнение по шагам:

bashShell
genkit start -- dart run bin/server.dart

Для серверного мониторинга genkit_otel экспортирует телеметрию OpenTelemetry, включая использование токенов и длительность модельных операций. Документация genkit_otel .

Практически полезно следить за временем до первой карточки, полным временем подбора, количеством вызовов и ошибками генерации интерфейса. Отдельно — за случаями, когда предложение успело устареть до подтверждения.

Эти показатели помогают оценить качество пользовательского сценария целиком.

Когда достаточно обычного интерфейса #

Если запись всегда проходит по одной схеме — услуга, дата, время, подтверждение — статические Flutter-виджеты могут оказаться самым удобным решением.

Разговорный слой становится полезнее, когда пользователь приходит с неполным запросом, сочетает несколько условий или ожидает помощи в выборе. GenUI добавляет ценность, когда результат удобнее представить разными способами.

Поэтому разумно двигаться постепенно: сохранить основной сценарий записи, добавить понимание свободного текста и инструменты чтения. Затем подключать динамический интерфейс там, где он действительно сокращает путь пользователя.

Genkit Dart 1.0 делает Flutter частью общего стека AI-приложений. Команда может описывать интерфейс, серверные операции и контракты данных на Dart, используя привычные подходы к разработке.

Для пользователя ценность этого стека выражается гораздо проще: он формулирует просьбу своими словами, получает подходящие варианты и подтверждает понятное действие. Приложение превращает разговор в результат — с реальными данными и предсказуемым выполнением.

Был ли материал полезен?

0 полезных · 0 неполезных · 0 оценок

Голосование временно недоступно: подключаем безопасную идентификацию читателя.