【文章翻譯】Quick, reliable calculations with A2UI's Client-Side Functions

【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】

原文:https://flutter.dev/blog/a2ui-client-side-functions

使用 A2UI 的客戶端函數實現快速、可靠的計算

了解客戶端函數如何讓代理直接將本地操作委託給在使用者設備上執行的 Dart 程式碼。

學習如何使用客戶端函數
學習如何使用客戶端函數

生成式使用者介面 (GenUI) 正在改變我處理 Flutter UI 開發的方式。GenUI 不再為每個場景硬編碼固定畫面,而是讓 AI 代理在執行時動態生成和調整使用者介面元件。使用 Agent-to-User Interface (A2UI) JSON 訊息和 genui 套件,Flutter 應用程式可以即時渲染動態的 AI 驅動卡片和介面。

然而,當我開始建構真實世界的代理應用程式時,我發現自己正在尋找減少延遲並減少模型出錯機會的方法。當使用大型語言模型 (LLM) 進行建構時,很容易依賴模型來處理所有事情。但要求 LLM 承擔它不一定設計用於執行的任務(例如算術)會引入延遲並增加出錯的機會。

這就是 A2UI 的 **客戶端** 函數的用武之地。

在這篇文章中,我將介紹客戶端函數如何讓代理直接將本地操作委託給在使用者設備上執行的 Dart 程式碼,從而減少往返次數並在您的 Flutter GenUI 應用程式中提供更可預測的結果。


客戶端函數解決了什麼問題

為了純粹的數學運算,來回向 LLM 發送原始提示效率低下,增加了往返延遲並消耗了額外的令牌。相反,客戶端函數讓代理在設備上本地計算諸如成分成本、稅費總計或單位轉換之類的值。

客戶端函數透過創建清晰的分工來解決這個問題:

  1. **Flutter 客戶端** 在 GenUI 目錄中宣告可用的客戶端函數,告知代理可以呼叫哪些本地操作、它們期望哪些參數以及它們返回什麼格式。
  2. **LLM 代理** 決定何時以及何處顯示組件,並發出一個 A2UI 表達式,呼叫客戶端函數並提供必要的參數(例如項目 ID 和數量)。
  3. **Flutter 客戶端** 在本地評估表達式並在 Dart 程式碼中同步執行數學運算,立即在螢幕上渲染清晰、格式化的結果。

透過將計算卸載到客戶端設備,您的應用程式避免了不必要的網路開銷,實現了一致的格式化,並降低了總體令牌使用量。

dart
sequenceDiagram 
 participant LLM as Gemini Agent 
 participant Client as Flutter App (genui) 
 participant Function as CalculateCost (Dart) 
 LLM->>Client: A2UI Payload with calculateCost(black_beans, 3) 
 Client->>Function: executeSync(args) 
 Function->>Client: "$2.97" 
 Client->>Client: Render Text widget ($2.97) 

讓我們看看我如何在名為 **Commis** 的範例應用程式中實作這種模式,Commis 是一個為商業廚房和餐飲團隊建置的智慧助理。


CalculateCostFunction 類別的解剖

在準備即將到來的餐飲活動時,廚師可能會詢問菜單中使用的食材。為了可靠地計算和顯示食材成本,而無需等待伺服器往返,我創建了一個名為 CalculateCostFunction 的客戶端函數。

在 `genui` 套件中,同步客戶端函數擴展了 `SynchronousClientFunction`。這是 `client_functions.dart` 的完整實作:

dart
/// 一個客戶端函數,使用本地 Dart 邏輯直接在設備上計算食材成本。 
class CalculateCostFunction extends SynchronousClientFunction { 
 const CalculateCostFunction(); 
 
 // 1. LLM 在 A2UI 負載中引用的識別碼。 
 @override 
 String get name => 'calculateCost'; 
 
 // 2. 提供給 LLM 的清晰描述,以便它知道何時以及為何使用該函數。 
 @override 
 String get description => 
 'Calculates the cost for a certain quantity of an ingredient. ' 
 'Returns a formatted dollar string (for example, \$4.50).'; 
 
 // 3. 綁定的預期回傳類型。 
 @override 
 ClientFunctionReturnType get returnType => ClientFunctionReturnType.string; 
 
 // 4. 定義所需輸入參數的 JSON Schema。 
 @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'], 
 ); 
 
 // 5. 客戶端上的同步 Dart 執行邏輯 
 @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)}'; 
 } 
} 

讓我們分解這個類別的關鍵部分:

  1. `name`:AI 代理在 A2UI 負載中生成函數呼叫時使用的唯一識別碼(`calculateCost`)。
  2. `description`:發送給 LLM 的簡潔說明,讓它知道何時以及為何呼叫此函數,以及預期什麼輸出格式。
  3. `returnType`:指定函數返回的資料類型(在此範例中為字串)。
  4. `argumentSchema`:使用 `json_schema_builder` 建立,此 Schema 會明確告知 LLM 需要哪些參數(`ingredient_id` 和 `quantity`)。
  5. `executeSync`:當 UI 渲染時,在使用者設備上執行的核心 Dart 方法。它會解析傳入的參數,呼叫我的本地 `CostService`,並返回格式化的美元字串。

如果您剛剛注意到這種模式與用於 UI 元件的目錄條目的模式非常相似,那麼您是正確的!兩者都為代理提供了推理時使用的元數據,並搭配了執行有用操作的 Dart 邏輯:要嘛建立 Widget,要嘛在此範例中計算一個值。


目錄註冊和系統提示整合

為了讓代理了解 `calculateCost`,我在應用程式的 GenUI `Catalog` 中註冊了它。

在實例化 `Catalog` 時,我將 `CalculateCostFunction()` 傳遞到 `functions` 列表以及我的 UI 元件中:

dart
// lib/ui/catalog/catalog.dart 
 
final commisCatalog = Catalog( 
 [ 
 cateringJobItem, 
 recipeLineCatalogItem, 
 ingredientLineCatalogItem, 
 navigationCardCatalogItem, 
 simpleCardCatalogItem, 
 ], 
 functions: [ 
 CalculateCostFunction(), // 在這裡! 
 ], 
 catalogId: 'commis_catalog', 
); 

當初始化對話會話時,`genui` 的 `PromptBuilder` 會檢查目錄並自動提取所有客戶端函數宣告,將它們的名稱、描述和 Schema 整合到提供給 Gemini 的系統提示中。

有了這些,當廚師詢問食譜價格時,Gemini 不會嘗試猜測或計算美元總額。相反,它會發出一個 A2UI 訊息,其中包含對 `calculateCost` 的呼叫(在此範例中,用於三罐豆子的價格):

json
{ 
 "id": "cost_val", 
 "component": "Text", 
 "text": { 
 "call": "calculateCost", 
 "args": { 
 "ingredient_id": "black_beans", 
 "quantity": 3 
 }, 
 "returnType": "string" 
 }, 
 "variant": "h2" 
} 

請注意,A2UI 訊息中 `call` 屬性的值與目錄中註冊的函數名稱 (`calculateCost`) 相符。當 `SurfaceController` 收到此訊息時,它會在設備上使用 `executeSync` 本地評估 `calculateCost`,並立即在螢幕上渲染準確的美元字串(例如 `$2.97`)。


使用 Flutter 熱重載實現快速開發人員迭代

由於客戶端函數不會被後端微服務或雲端函數部署鎖定,因此使用它們仍然感覺像普通的 Dart。

如果我想更新貨幣格式(例如,添加批量折扣邏輯或從 `$4.50` 切換到 USD `4.50`),我只需在 Dart 中編輯 `executeSync`,保存檔案,然後觀察熱重載用新結果更新我的應用程式。


摘要與後續步驟

準備好在您的應用程式中嘗試 GenUI 和客戶端函數了嗎?

祝您建構愉快!

更多 Flutter 內容