В Flutter с GenUI агент может собирать экраны во время работы приложения. Но арифметику лучше не передавать ему: это тратит токены, требует ответа по сети и делает результат менее предсказуемым. Клиентские функции A2UI позволяют зарегистрировать Dart-функцию, ее параметры и тип результата. Агент вызывает ее из сообщения, а приложение выполняет код на устройстве.
Что решают клиентские функции:
Отправлять простые математические запросы в LLM неэффективно. Это добавляет задержку на сеть и расходует лишние токены. Клиентские функции позволяют агенту вычислять значения локально на устройстве: стоимость ингредиентов, налоги, конвертацию единиц.
Разделение труда выглядит так. Flutter-клиент объявляет доступные функции в каталоге GenUI: сообщает агенту, какие локальные операции можно вызвать, какие параметры они ожидают и что возвращают. Агент решает, когда и где показать компонент, и отправляет A2UI-выражение с вызовом функции и нужными аргументами. Клиент выполняет выражение локально и сразу отображает результат.
Это избавляет от лишних сетевых запросов, обеспечивает единообразное форматирование и снижает расход токенов.
Как устроена клиентская функция:
Синхронные клиентские функции наследуются от SynchronousClientFunction. Вот пример функции для расчета стоимости ингредиента:
class CalculateCostFunction extends SynchronousClientFunction {
const CalculateCostFunction();
@override
String get name => 'calculateCost';
@override
String get description =>
'Calculates the cost for a certain quantity of an ingredient. '
'Returns a formatted dollar string (for example, \$4.50).';
@override
ClientFunctionReturnType get returnType => ClientFunctionReturnType.string;
@override
Schema get argumentSchema => S.object(
properties: {
'ingredient_id': S.string(description: 'The ID of the ingredient.'),
'quantity': S.number(description: 'The quantity of the ingredient.'),
},
required: ['ingredient_id', 'quantity'],
);
@override
Object? executeSync(JsonMap args, ExecutionContext context) {
final ingredientId = args['ingredient_id'].toString();
final quantity = num.tryParse(args['quantity'].toString())?.toDouble();
if (quantity == null || quantity < 1) {
return '\$0.00';
}
final cost = CostService().fetchPrice(ingredientId, quantity);
return '\$${cost.toStringAsFixed(2)}';
}
}
Ключевые моменты:
🔹name: уникальный идентификатор, который агент использует при вызове функции в A2UI-сообщениях.
🔹description: краткое объяснение для LLM, чтобы он знал, когда и зачем вызывать функцию и какой формат результата ожидать.
🔹returnType: тип возвращаемого значения. В данном случае строка.
🔹argumentSchema: схема, которая сообщает LLM, какие параметры обязательны: идентификатор ингредиента и количество.
🔹executeSync: метод, который выполняется на устройстве. Он парсит аргументы, вызывает локальный сервис и возвращает отформатированную строку.
Этот паттерн похож на тот, что используется для элементов каталога UI-компонентов. Оба предоставляют агенту метаданные для рассуждений и Dart-логику, которая делает что-то полезное: создает виджеты или вычисляет значение.
Регистрация в каталоге:
Чтобы агент знал о функции, она регистрируется в каталоге GenUI:
final commisCatalog = Catalog(
[
cateringJobItem,
recipeLineCatalogItem,
ingredientLineCatalogItem,
navigationCardCatalogItem,
simpleCardCatalogItem,
],
functions: [
CalculateCostFunction(),
],
catalogId: 'commis_catalog',
);
При инициализации сессии PromptBuilder из genui инспектирует каталог и автоматически извлекает объявления клиентских функций, добавляя их имена, описания и схемы в системный промпт для Gemini.
🔗 Читать подробнее
💡 Вывод:
Клиентские функции A2UI позволяют вынести вычисления из ИИ-агента в Dart-код. Это снижает задержку, экономит токены и делает результат предсказуемым.
Подписаться на канал:
➡️ Flutter & Dart | Мобильный трудоголик