跳到主內容

功能整合

如何與其他 Flutter 功能整合。

除了 LlmChatView 自動提供的功能外,還有許多整合點允許您的應用與其它功能無縫融合,從而提供額外的功能。

  • 歡迎訊息:向用戶顯示初始問候語。
  • 建議提示詞:為使用者提供預定義的提示詞以引導互動。
  • 系統指令:為 LLM 提供特定輸入以影響其響應。
  • 停用附件和音訊輸入:移除聊天 UI 中可選的部分。
  • 管理取消或錯誤行為:更改使用者取消操作或 LLM 錯誤的處理行為。
  • 管理歷史記錄:每個 LLM 提供程式都允許管理聊天曆史記錄,這對於清除記錄、動態更改記錄以及在會話間儲存記錄非常有用。
  • 聊天序列化/反序列化:在應用會話之間儲存和檢索對話。
  • 自定義響應小部件:引入專門的 UI 元件來呈現 LLM 響應。
  • 自定義樣式:定義獨特的視覺樣式,使聊天外觀與整個應用保持一致。
  • 無 UI 聊天:直接與 LLM 提供程式互動,而不影響使用者當前的聊天會話。
  • 自定義 LLM 提供程式:構建您自己的 LLM 提供程式,以將聊天功能與您自己的模型後端整合。
  • 重定向提示詞:除錯、記錄或重定向發往提供程式的各種訊息,以跟蹤問題或動態路由提示詞。

歡迎訊息

#

聊天檢視允許您提供自定義歡迎訊息,為使用者設定上下文。

Example welcome
message

您可以透過設定 welcomeMessage 引數在初始化 LlmChatView 時提供歡迎訊息。

dart
class ChatPage extends StatelessWidget {
 const ChatPage({super.key});

 @override
 Widget build(BuildContext context) => Scaffold(
       appBar: AppBar(title: const Text(App.title)),
       body: LlmChatView(
         welcomeMessage: 'Hello and welcome to the Flutter AI Toolkit!',
         provider: FirebaseProvider(
          model: FirebaseAI.geminiAI().generativeModel(
             model: 'gemini-2.5-flash',
           ),
         ),
       ),
     );
}

要檢視設定歡迎訊息的完整示例,請檢視 歡迎示例

建議提示詞

#

您可以提供一組建議提示詞,讓使用者瞭解該聊天會話已針對哪些內容進行了最佳化。

Example suggested
prompts

建議僅在沒有現有聊天曆史記錄時顯示。點選其中一個建議會立即將其作為請求傳送給底層 LLM。要設定建議列表,請在構造 LlmChatView 時使用 suggestions 引數。

dart
class ChatPage extends StatelessWidget {
 const ChatPage({super.key});

 @override
 Widget build(BuildContext context) => Scaffold(
       appBar: AppBar(title: const Text(App.title)),
       body: LlmChatView(
         suggestions: [
           'I\'m a Star Wars fan. What should I wear for Halloween?',
           'I\'m allergic to peanuts. What candy should I avoid at Halloween?',
           'What\'s the difference between a pumpkin and a squash?',
         ],
         provider: FirebaseProvider(
          model: FirebaseAI.geminiAI().generativeModel(
             model: 'gemini-2.5-flash',
           ),
         ),
       ),
     );
}

要檢視為使用者設定建議的完整示例,請檢視 建議示例

LLM 指令

#

為了根據您的應用需求最佳化 LLM 的響應,您需要為其提供指令。例如,食譜示例應用 使用 GenerativeModel 類的 systemInstructions 引數來調整 LLM,使其專注於根據使用者的指示提供食譜。

dart
class _HomePageState extends State<HomePage> {
  ...
  // create a new provider with the given history and the current settings
  LlmProvider _createProvider([List<ChatMessage>? history]) => FirebaseProvider(
      history: history,
        ...,
        model: FirebaseAI.geminiAI().generativeModel(
          model: 'gemini-2.5-flash',
          ...,
          systemInstruction: Content.system('''
You are a helpful assistant that generates recipes based on the ingredients and
instructions provided as well as my food preferences, which are as follows:
${Settings.foodPreferences.isEmpty ? 'I don\'t have any food preferences' : Settings.foodPreferences}

You should keep things casual and friendly. You may generate multiple recipes in a single response, but only if asked. ...
''',
          ),
        ),
      );
  ...
}

設定系統指令對於每個提供程式都是唯一的;FirebaseProvider 允許您透過 systemInstruction 引數提供這些指令。

請注意,在這種情況下,我們將使用者偏好作為建立傳遞給 LlmChatView 建構函式的 LLM 提供程式的一部分引入。每次使用者更改偏好時,我們都會在建立過程中設定指令。食譜應用允許使用者使用 Scaffold 上的抽屜來更改他們的飲食偏好。

Example of refining
prompt

每當使用者更改飲食偏好時,食譜應用都會建立一個新模型來使用這些新偏好。

dart
class _HomePageState extends State<HomePage> {
  ...
  void _onSettingsSave() => setState(() {
        // move the history over from the old provider to the new one
        final history = _provider.history.toList();
        _provider = _createProvider(history);
      });
}

函式呼叫

#

為了使 LLM 能夠代表使用者執行操作,您可以提供一組 LLM 可以呼叫的工具(函式)。FirebaseProvider 開箱即用地支援函式呼叫。它處理傳送使用者提示、從 LLM 接收函式呼叫請求、執行函式並將結果傳送回 LLM 的迴圈,直到生成最終的文字響應。

要使用函式呼叫,您需要定義工具並將其傳遞給 FirebaseProvider。有關詳細資訊,請檢視 函式呼叫示例

停用附件和音訊輸入

#

如果您想停用附件(+ 按鈕)或音訊輸入(麥克風按鈕),可以透過 LlmChatView 建構函式的 enableAttachmentsenableVoiceNotes 引數來實現。

dart
class ChatPage extends StatelessWidget {
  const ChatPage({super.key});

  @override
  Widget build(BuildContext context) {
    // ...

    return Scaffold(
      appBar: AppBar(title: const Text('Restricted Chat')),
      body: LlmChatView(
        // ...
        enableAttachments: false,
        enableVoiceNotes: false,
      ),
    );
  }
}

這兩個標誌的預設值均為 true

自定義語音轉文字

#

預設情況下,AI Toolkit 使用傳遞給 LlmChatViewLlmProvider 來提供語音轉文字實現。如果您想提供自己的實現(例如使用特定於裝置的各種服務),可以透過實現 SpeechToText 介面並將其傳遞給 LlmChatView 建構函式來實現。

dart
LlmChatView(
  // ...
  speechToText: MyCustomSpeechToText(),
)

有關詳細資訊,請檢視 自定義 STT 示例

管理取消或錯誤行為

#

預設情況下,當用戶取消 LLM 請求時,LLM 的響應會追加字串 "CANCEL",並會彈出一個訊息提示使用者已取消請求。同樣,如果發生 LLM 錯誤(例如網路連線中斷),LLM 的響應會追加字串 "ERROR",並會彈出一個包含錯誤詳細資訊的警告對話方塊。

您可以使用 LlmChatViewcancelMessageerrorMessageonCancelCallbackonErrorCallback 引數覆蓋取消和錯誤行為。例如,以下程式碼替換了預設的取消處理行為。

dart
class ChatPage extends StatelessWidget {
  // ...

  void _onCancel(BuildContext context) {
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(const SnackBar(content: Text('Chat cancelled')));
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text(App.title)),
    body: LlmChatView(
      // ...
      onCancelCallback: _onCancel,
      cancelMessage: 'Request cancelled',
    ),
  );
}

您可以覆蓋這些引數中的任何一個或全部,對於您未覆蓋的任何內容,LlmChatView 將使用其預設值。

管理歷史記錄

#

定義所有可插入聊天檢視的 LLM 提供程式的標準介面 包括獲取和設定提供程式歷史記錄的功能。

dart
abstract class LlmProvider implements Listenable {
  Stream<String> generateStream(
    String prompt, {
    Iterable<Attachment> attachments,
  });

  Stream<String> sendMessageStream(
    String prompt, {
    Iterable<Attachment> attachments,
  });

  Iterable<ChatMessage> get history;
  set history(Iterable<ChatMessage> history);
}

當提供程式的歷史記錄更改時,它會呼叫 Listenable 基類公開的 notifyListener 方法。這意味著您可以手動使用 addremove 方法進行訂閱/取消訂閱,或使用它來構造 ListenableBuilder 類的例項。

generateStream 方法在不影響歷史記錄的情況下呼叫底層 LLM。呼叫 sendMessageStream 方法會透過在響應完成後向提供程式的歷史記錄新增兩條新訊息(一條用於使用者訊息,一條用於 LLM 響應)來更改歷史記錄。聊天檢視在處理使用者聊天提示時使用 sendMessageStream,在處理使用者語音輸入時使用 generateStream

要檢視或設定歷史記錄,您可以訪問 history 屬性。

dart
void _clearHistory() => _provider.history = [];

在重新建立提供程式同時保持歷史記錄時,訪問提供程式歷史記錄的能力也很有用。

dart
class _HomePageState extends State<HomePage> {
  ...
  void _onSettingsSave() => setState(() {
        // move the history over from the old provider to the new one
        final history = _provider.history.toList();
        _provider = _createProvider(history);
      });
}

_createProvider 方法使用來自上一個提供程式的歷史記錄以及新的使用者偏好建立一個新提供程式。這對使用者來說是無縫的;他們可以繼續聊天,但現在 LLM 會在考慮其新飲食偏好的情況下給出響應。例如:

dart
class _HomePageState extends State<HomePage> {
  ...
  // create a new provider with the given history and the current settings
  LlmProvider _createProvider([List<ChatMessage>? history]) =>
    FirebaseProvider(
      history: history,
      ...
    );
  ...
}

要檢視歷史記錄的實際效果,請檢視 食譜示例應用歷史記錄示例應用

聊天序列化/反序列化

#

要在應用會話之間儲存和恢復聊天曆史記錄,需要能夠序列化和反序列化每個使用者提示(包括附件)以及每個 LLM 響應。這兩種訊息(使用者提示和 LLM 響應)都在 ChatMessage 類中公開。序列化可以透過使用每個 ChatMessage 例項的 toJson 方法來完成。

dart
Future<void> _saveHistory() async {
  // get the latest history
  final history = _provider.history.toList();

  // write the new messages
  for (var i = 0; i != history.length; ++i) {
    // skip if the file already exists
    final file = await _messageFile(i);
    if (file.existsSync()) continue;

    // write the new message to disk
    final map = history[i].toJson();
    final json = JsonEncoder.withIndent('  ').convert(map);
    await file.writeAsString(json);
  }
}

同樣,要進行反序列化,請使用 ChatMessage 類的靜態 fromJson 方法。

dart
Future<void> _loadHistory() async {
  // read the history from disk
  final history = <ChatMessage>[];
  for (var i = 0;; ++i) {
    final file = await _messageFile(i);
    if (!file.existsSync()) break;

    final map = jsonDecode(await file.readAsString());
    history.add(ChatMessage.fromJson(map));
  }

  // set the history on the controller
  _provider.history = history;
}

為了確保序列化時的高效週轉,我們建議只寫入每條使用者訊息一次。否則,使用者必須等待您的應用每次寫入所有訊息,而在處理二進位制附件時,這可能需要很長時間。

要檢視此功能的實際效果,請檢視 歷史記錄示例應用

自定義響應小部件

#

預設情況下,聊天檢視顯示的 LLM 響應是格式化的 Markdown。但是,在某些情況下,您希望建立一個自定義小部件來顯示特定於您的應用且與其整合的 LLM 響應。例如,當用戶在 食譜示例應用 中請求食譜時,LLM 響應被用於建立一個特定於顯示食譜的小部件(就像應用的其他部分一樣),並提供一個新增按鈕,以便使用者可以將食譜新增到他們的資料庫中。

Add recipe button

這是透過設定 LlmChatView 建構函式的 responseBuilder 引數來完成的。

dart
LlmChatView(
  provider: _provider,
  welcomeMessage: _welcomeMessage,
  responseBuilder: (context, response) => RecipeResponseView(
    response,
  ),
),

在這個特定示例中,RecipeResponseView 小部件是使用 LLM 提供程式的響應文字構建的,並使用該文字來實現其 build 方法。

dart
class RecipeResponseView extends StatelessWidget {
  const RecipeResponseView(this.response, {super.key});
  final String response;

  @override
  Widget build(BuildContext context) {
    final children = <Widget>[];
    String? finalText;

    // created with the response from the LLM as the response streams in, so
    // many not be a complete response yet
    try {
      final map = jsonDecode(response);
      final recipesWithText = map['recipes'] as List<dynamic>;
      finalText = map['text'] as String?;

      for (final recipeWithText in recipesWithText) {
        // extract the text before the recipe
        final text = recipeWithText['text'] as String?;
        if (text != null && text.isNotEmpty) {
          children.add(MarkdownBody(data: text));
        }

        // extract the recipe
        final json = recipeWithText['recipe'] as Map<String, dynamic>;
        final recipe = Recipe.fromJson(json);
        children.add(const Gap(16));
        children.add(Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(recipe.title, style: Theme.of(context).textTheme.titleLarge),
            Text(recipe.description),
            RecipeContentView(recipe: recipe),
          ],
        ));

        // add a button to add the recipe to the list
        children.add(const Gap(16));
        children.add(OutlinedButton(
          onPressed: () => RecipeRepository.addNewRecipe(recipe),
          child: const Text('Add Recipe'),
        ));
        children.add(const Gap(16));
      }
    } catch (e) {
      debugPrint('Error parsing response: $e');
    }

    ...

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: children,
    );
  }
}

此程式碼解析文字以從 LLM 中提取介紹性文字和食譜,將它們與新增食譜按鈕捆綁在一起,以取代 Markdown 顯示。

請注意,我們正在將 LLM 響應解析為 JSON。通常會將提供程式設定為 JSON 模式並提供模式以限制其響應格式,以確保我們獲得可以解析的內容。每個提供程式都以自己的方式公開此功能,但 FirebaseProvider 類透過 GenerationConfig 物件啟用此功能,食譜示例使用如下:

dart
class _HomePageState extends State<HomePage> {
  ...

  // create a new provider with the given history and the current settings
  LlmProvider _createProvider([List<ChatMessage>? history]) => FirebaseProvider(
        ...
        model: FirebaseAI.geminiAI().generativeModel(
          ...
          generationConfig: GenerationConfig(
            responseMimeType: 'application/json',
            responseSchema: Schema(...),
          systemInstruction: Content.system('''
...
Generate each response in JSON format
with the following schema, including one or more "text" and "recipe" pairs as
well as any trailing text commentary you care to provide:

{
  "recipes": [
    {
      "text": "Any commentary you care to provide about the recipe.",
      "recipe":
      {
        "title": "Recipe Title",
        "description": "Recipe Description",
        "ingredients": ["Ingredient 1", "Ingredient 2", "Ingredient 3"],
        "instructions": ["Instruction 1", "Instruction 2", "Instruction 3"]
      }
    }
  ],
  "text": "any final commentary you care to provide",
}
''',
          ),
        ),
      );
  ...
}

此程式碼透過將 responseMimeType 引數設定為 'application/json' 並將 responseSchema 引數設定為定義您準備解析的 JSON 結構的 Schema 類例項來初始化 GenerationConfig 物件。此外,最好也要求使用 JSON,並在系統指令中提供該 JSON 模式的描述,我們在這裡就是這樣做的。

要檢視此功能的實際效果,請檢視 食譜示例應用

自定義樣式

#

聊天檢視開箱即用,為背景、文字欄位、按鈕、圖示、建議等提供了一組預設樣式。您可以透過使用 style 引數傳遞給 LlmChatView 建構函式來設定自己的樣式,從而完全自定義這些外觀。

dart
LlmChatView(
  provider: FirebaseProvider(...),
  style: LlmChatViewStyle(...),
),

例如,自定義樣式示例應用 使用此功能實現了萬聖節主題的應用。

Halloween-themed demo app

有關 LlmChatViewStyle 類中可用樣式的完整列表,請檢視 參考文件。您還可以使用 LlmChatViewStyle 類的 voiceNoteRecorderStyle 引數來自定義語音錄音機的外觀,這在 樣式示例 中有演示。

要檢視自定義樣式的實際效果,除了 自定義樣式示例樣式示例 外,還可以檢視 深色模式示例演示應用

無 UI 聊天

#

您不必使用聊天檢視來訪問底層提供程式的功能。除了可以簡單地透過其提供的任何專有介面呼叫它之外,您還可以將其與 LlmProvider 介面 一起使用。

作為一個示例,食譜示例應用在編輯食譜的頁面上提供了一個“魔法”按鈕。該按鈕的目的是使用您當前的飲食偏好更新資料庫中的現有食譜。按下該按鈕可以預覽建議的更改,並決定是否要應用它們。

User decides whether to update recipe in
database

“編輯食譜”頁面沒有使用應用聊天部分使用的同一個提供程式(這會將虛假的使用者訊息和 LLM 響應插入到使用者的聊天曆史記錄中),而是建立了自己的提供程式並直接使用它。

dart
class _EditRecipePageState extends State<EditRecipePage> {
  ...
  final _provider = FirebaseProvider(...);
  ...
  Future<void> _onMagic() async {
    final stream = _provider.sendMessageStream(
      'Generate a modified version of this recipe based on my food preferences: '
      '${_ingredientsController.text}\n\n${_instructionsController.text}',
    );
    var response = await stream.join();
    final json = jsonDecode(response);

    try {
      final modifications = json['modifications'];
      final recipe = Recipe.fromJson(json['recipe']);

      if (!context.mounted) return;
      final accept = await showDialog<bool>(
        context: context,
        builder: (context) => AlertDialog(
          title: Text(recipe.title),
          content: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text('Modifications:'),
              const Gap(16),
              Text(_wrapText(modifications)),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => context.pop(true),
              child: const Text('Accept'),
            ),
            TextButton(
              onPressed: () => context.pop(false),
              child: const Text('Reject'),
            ),
          ],
        ),
      );
      ...
    } catch (ex) {
      ...
      }
    }
  }
}

sendMessageStream 的呼叫會在提供程式的歷史記錄中建立條目,但由於它未與聊天檢視關聯,因此不會顯示它們。如果方便的話,您也可以透過呼叫 generateStream 來完成同樣的事情,這允許您在不影響聊天曆史記錄的情況下重用現有的提供程式。

要檢視此功能的實際效果,請檢視食譜示例的 編輯食譜頁面

重定向提示詞

#

如果您想除錯、記錄或操作聊天檢視與底層提供程式之間的連線,可以透過實現 LlmStreamGenerator 函式來完成。然後,您將該函式透過 messageSender 引數傳遞給 LlmChatView

dart
class ChatPage extends StatelessWidget {
  final _provider = FirebaseProvider(...);

  @override
  Widget build(BuildContext context) => Scaffold(
      appBar: AppBar(title: const Text(App.title)),
      body: LlmChatView(
        provider: _provider,
        messageSender: _logMessage,
      ),
    );

  Stream<String> _logMessage(
    String prompt, {
    required Iterable<Attachment> attachments,
  }) async* {
    // log the message and attachments
    debugPrint('# Sending Message');
    debugPrint('## Prompt\n$prompt');
    debugPrint('## Attachments\n${attachments.map((a) => a.toString())}');

    // forward the message on to the provider
    final response = _provider.sendMessageStream(
      prompt,
      attachments: attachments,
    );

    // log the response
    final text = await response.join();
    debugPrint('## Response\n$text');

    // return it
    yield text;
  }
}

此示例記錄了使用者提示和 LLM 響應的來回互動。當提供一個函式作為 messageSender 時,您有責任呼叫底層提供程式。如果您不這樣做,它就不會收到訊息。此功能允許您執行高階操作,例如動態路由到提供程式或檢索增強生成 (RAG)。

要檢視此功能的實際效果,請檢視 日誌記錄示例應用