跳到主内容

功能集成

如何与其他 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。要设置建议列表,请使用 suggestions 参数构造 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(
         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 提供程序的一部分引入。每次用户更改其偏好时,我们都会在创建过程的每个步骤中设置指令。食谱应用程序允许用户使用支架上的抽屉更改他们的食物偏好

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。请查看 函数调用示例 以获取详细信息。

禁用附件和音频输入

#

如果您想禁用附件(+ 按钮)或音频输入(麦克风按钮),可以使用 enableAttachmentsenableVoiceNotes 参数传递给 LlmChatView 构造函数

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 工具包使用 LlmProvider 传递给 LlmChatView 以提供语音转文本实现。如果您想提供自己的实现,例如使用设备特定的服务,可以通过实现 SpeechToText 接口并将其传递给 LlmChatView 构造函数来执行此操作

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

请查看 自定义 STT 示例 以获取详细信息。

管理取消或错误行为

#

默认情况下,当用户取消 LLM 请求时,LLM 的响应将附加字符串“CANCEL”,并且会弹出一个消息,告知用户已取消请求。同样,如果发生 LLM 错误,例如网络连接中断,LLM 的响应将附加字符串“ERROR”,并且会弹出一个警报对话框,其中包含错误的详细信息。

您可以使用 cancelMessageerrorMessageonCancelCallbackonErrorCallback 参数的 LlmChatView 来覆盖取消和错误行为。例如,以下代码替换了默认的取消处理行为

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,
  ),
),

在此示例中,构建了 RecipeReponseView 小部件,并使用 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)。

要查看此功能实际效果,请查看 日志示例应用