Skip to main content

功能集成

除了LlmChatView自动提供的功能外,许多集成点允许您的应用与其他功能无缝融合,以提供附加功能:

  • 欢迎消息: 向用户显示初始问候。
  • 建议提示: 为用户提供预定义提示以指导交互。
  • 系统指令: 为LLM提供特定输入以影响其响应。
  • 管理历史记录: 每个LLM提供程序都允许管理聊天历史记录,这对于清除历史记录、动态更改历史记录以及在会话之间存储历史记录非常有用。
  • 聊天序列化/反序列化: 在应用程序会话之间存储和检索对话。
  • 自定义响应小部件: 引入专门的UI组件来呈现LLM响应。
  • 自定义样式: 定义独特的视觉样式,使聊天外观与整个应用程序相匹配。
  • 无UI聊天: 直接与LLM提供程序交互,而不会影响用户的当前聊天会话。
  • 自定义LLM提供程序: 构建您自己的LLM提供程序,以将聊天与您自己的模型后端集成。
  • 重定向提示: 调试、记录或重定向意图发送给提供程序的消息,以追踪问题或动态路由提示。

欢迎消息

#

聊天视图允许您提供自定义欢迎消息来为用户设置上下文:

欢迎消息示例

您可以通过设置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: '您好,欢迎使用Flutter AI工具包!',
         provider: GeminiProvider(
           model: GenerativeModel(
             model: 'gemini-1.5-flash',
             apiKey: geminiApiKey,
           ),
         ),
       ),
     );
}

要查看设置欢迎消息的完整示例,请查看欢迎示例

建议提示

#

您可以提供一组建议提示,让用户了解聊天会话针对哪些方面进行了优化:

建议提示示例

只有在没有现有聊天历史记录时才会显示建议。单击一个建议会将文本复制到用户的提示编辑区域。要设置建议列表,请使用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: [
           '我是一个星球大战迷。万圣节我应该穿什么?',
           '我对花生过敏。万圣节我应该避免哪些糖果?',
           '南瓜和南瓜的区别是什么?',
         ],
         provider: GeminiProvider(
           model: GenerativeModel(
             model: 'gemini-1.5-flash',
             apiKey: geminiApiKey,
           ),
         ),
       ),
     );
}

要查看为用户设置建议的完整示例,请查看建议示例

LLM 指令

#

为了根据应用程序的需求优化LLM的响应,您需要向其提供指令。例如,食谱示例应用程序 使用GenerativeModel类的systemInstructions参数来调整LLM,使其专注于根据用户的指令提供食谱:

dart
class _HomePageState extends State<HomePage> {
  ...
  // 使用给定的历史记录和当前设置创建一个新的提供程序
  LlmProvider _createProvider([List<ChatMessage>? history]) => GeminiProvider(
      history: history,
        ...,
        model: GenerativeModel(
          model: 'gemini-1.5-flash',
          apiKey: geminiApiKey,
          ...,
          systemInstruction: Content.system('''
你是一个有帮助的助手,根据提供的食材和说明以及我的食物偏好生成食谱,我的食物偏好如下:
${Settings.foodPreferences.isEmpty ? '我没有食物偏好' : Settings.foodPreferences}

你应该保持随意和友好的态度。你可以在一个回复中生成多个食谱,但只有在被要求时才这样做。...
''',
          ),
        ),
      );
  ...
}

设置系统指令对于每个提供程序都是唯一的;GeminiProviderVertexProvider都允许您通过systemInstruction参数提供它们。

请注意,在这种情况下,我们将用户偏好作为传递给LlmChatView构造函数的LLM提供程序创建的一部分引入。每次用户更改其偏好设置时,我们都会在创建过程中设置指令。食谱应用程序允许用户使用脚手架上的抽屉更改其食物偏好:

细化提示的示例

每当用户更改其食物偏好时,食谱应用程序都会创建一个新的模型来使用新的偏好设置:

dart
class _HomePageState extends State<HomePage> {
  ...
  void _onSettingsSave() => setState(() {
        // 将历史记录从旧提供程序移动到新提供程序
        final history = _provider.history.toList();
        _provider = _createProvider(history);
      });
}

管理历史记录

#

可以插入聊天视图的定义所有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(() {
        // 将历史记录从旧提供程序移动到新提供程序
        final history = _provider.history.toList();
        _provider = _createProvider(history);
      });
}

_createProvider方法使用来自先前提供程序的历史记录 新的用户偏好设置创建一个新的提供程序。对于用户来说,这是无缝的;他们可以继续聊天,但现在LLM会根据他们的新食物偏好给出回复。例如:

dart
class _HomePageState extends State<HomePage> {
  ...
  // 使用给定的历史记录和当前设置创建一个新的提供程序
  LlmProvider _createProvider([List<ChatMessage>? history]) =>
    GeminiProvider(
      history: history,
      ...
    );
  ...
}

要查看实际操作中的历史记录,请查看食谱示例应用程序历史记录示例应用程序

聊天序列化/反序列化

#

要在应用程序会话之间保存和恢复聊天历史记录,需要能够序列化和反序列化每个用户提示(包括附件)和每个LLM响应。两种类型的消息(用户提示和LLM响应)都在ChatMessage类中公开。可以使用每个ChatMessage实例的toJson方法来完成序列化。

dart
Future<void> _saveHistory() async {
  // 获取最新的历史记录
  final history = _provider.history.toList();

  // 写入新消息
  for (var i = 0; i != history.length; ++i) {
    // 如果文件已存在则跳过
    final file = await _messageFile(i);
    if (file.existsSync()) continue;

    // 将新消息写入磁盘
    final map = history[i].toJson();
    final json = JsonEncoder.withIndent('  ').convert(map);
    await file.writeAsString(json);
  }
}

同样,要反序列化,请使用ChatMessage类的静态fromJson方法:

dart
Future<void> _loadHistory() async {
  // 从磁盘读取历史记录
  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));
  }

  // 在控制器上设置历史记录
  _provider.history = history;
}

为了确保在序列化时快速周转,我们建议只写入每个用户消息一次。否则,用户必须等待您的应用程序每次都写入每条消息,并且面对二进制附件时,这可能需要一段时间。

要查看实际操作,请查看历史记录示例应用程序

自定义响应小部件

#

默认情况下,聊天视图显示的LLM响应是格式化的Markdown。但是,在某些情况下,您可能想要创建一个自定义小部件来显示特定于您的应用程序并与之集成的LLM响应。例如,当用户在食谱示例应用程序中请求食谱时,LLM响应用于创建一个特定于显示食谱的小部件,就像应用程序的其余部分一样,并提供一个 添加 按钮,以防用户想将食谱添加到其数据库中:

添加食谱按钮

这是通过设置LlmChatView构造函数的responseBuilder参数来实现的:

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

在这个特定示例中,RecipeReponseView小部件是用LLM提供程序的响应文本构造的 这段代码解析文本以从LLM中提取介绍性文本和食谱,并将它们与“添加食谱”按钮捆绑在一起,以代替Markdown显示。

请注意,我们正在将LLM响应解析为JSON。通常的做法是将提供程序设置为JSON模式,并提供一个模式来限制其响应的格式,以确保我们得到可以解析的内容。每个提供程序都以其自身的方式公开此功能,但GeminiProviderVertexProvider类都使用GenerationConfig对象启用此功能,食谱示例如下所示:

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

  // 使用给定的历史记录和当前设置创建一个新的提供程序
  LlmProvider _createProvider([List<ChatMessage>? history]) => GeminiProvider(
        ...
        model: GenerativeModel(
          ...
          generationConfig: GenerationConfig(
            responseMimeType: 'application/json',
            responseSchema: Schema(...),
          systemInstruction: Content.system('''
...
以JSON格式生成每个响应,使用以下模式,包括一个或多个“text”和“recipe”对,以及您想提供的任何尾随文本注释:

{
  "recipes": [
    {
      "text": "您想提供的关于食谱的任何评论。",
      "recipe":
      {
        "title": "食谱标题",
        "description": "食谱描述",
        "ingredients": ["食材1", "食材2", "食材3"],
        "instructions": ["步骤1", "步骤2", "步骤3"]
      }
    }
  ],
  "text": "您想提供的任何最终评论",
}
''',
          ),
        ),
      );
  ...
}

此代码通过将responseMimeType参数设置为'application/json'并将responseSchema参数设置为定义要解析的JSON结构的Schema类的实例来初始化GenerationConfig对象。此外,最好也请求JSON,并在系统指令中提供该JSON模式的描述,我们在这里已经这样做了。

要查看实际操作,请查看食谱示例应用程序

自定义样式

#

聊天视图自带一组默认样式,用于背景、文本字段、按钮、图标、建议等等。您可以通过使用style参数设置自己的样式来完全自定义这些样式,该参数用于LlmChatView构造函数:

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

例如,自定义样式示例应用程序使用此功能实现具有万圣节主题的应用程序:

万圣节主题演示应用程序

有关LlmChatViewStyle类中可用样式的完整列表,请查看参考文档。要查看实际操作中的自定义样式,除了自定义样式示例外,还可以查看暗模式示例演示应用程序

无UI聊天

#

您不必使用聊天视图即可访问底层提供程序的功能。除了可以使用其提供的任何专有接口进行简单调用外,您还可以使用LlmProvider接口

例如,食谱示例应用程序在编辑食谱的页面上提供了一个“魔法”按钮。此按钮的目的是使用您当前的食物偏好来更新数据库中现有的食谱。按下按钮后,您可以预览推荐的更改,并决定是否要应用它们:

用户决定是否更新数据库中的食谱

“编辑食谱”页面不使用应用程序聊天部分使用的相同提供程序(这会将虚假的用户消息和LLM响应插入到用户的聊天历史记录中),而是创建它自己的提供程序并直接使用它:

dart
class _EditRecipePageState extends State<EditRecipePage> {
  ...
  final _provider = GeminiProvider(...);
  ...
  Future<void> _onMagic() async {
    final stream = _provider.sendMessageStream(
      '根据我的食物偏好生成此食谱的修改版本:'
      '${_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('修改:'),
              const Gap(16),
              Text(_wrapText(modifications)),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => context.pop(true),
              child: const Text('接受'),
            ),
            TextButton(
              onPressed: () => context.pop(false),
              child: const Text('拒绝'),
            ),
          ],
        ),
      );
      ...
    } catch (ex) {
      ...
      }
    }
  }
}

sendMessageStream的调用会在提供程序的历史记录中创建条目,但由于它与聊天视图无关,因此不会显示它们。如果方便的话,您也可以通过调用generateStream来完成同样的事情,这允许您重用现有提供程序而不会影响聊天历史记录。

要查看实际操作,请查看食谱示例的编辑食谱页面

重定向提示

#

如果您想调试、记录或操作聊天视图和底层提供程序之间的连接,您可以使用LlmStreamGenerator函数的实现。然后,您可以将该函数通过messageSender参数传递给LlmChatView

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

  @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* {
    // 记录消息和附件
    debugPrint('# 发送消息');
    debugPrint('## 提示\n$prompt');
    debugPrint('## 附件\n${attachments.map((a) => a.toString())}');

    // 将消息转发到提供程序
    final response = _provider.sendMessageStream(
      prompt,
      attachments: attachments,
    );

    // 记录响应
    final text = await response.join();
    debugPrint('## 响应\n$text');

    // 返回它
    yield text;
  }
}

此示例在用户来回发送时记录用户提示和LLM响应。当提供函数作为messageSender时,您有责任调用底层提供程序。如果您不这样做,它将不会收到消息。此功能允许您执行高级操作,例如动态路由到提供程序或检索增强生成 (RAG)。

要查看实际操作,请查看日志记录示例应用程序