连接 LLM 和 `LlmChatView` 的协议在 `LlmProvider` 接口中定义。

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);
}

LLM 可以位于云端或本地,可以托管在 Google Cloud Platform 或其他云提供商上,也可以是专有 LLM 或开源 LLM。任何可用于实现此接口的 LLM 或类 LLM 端点都可以作为 LLM 提供程序插入到聊天视图中。AI 工具包开箱即用地提供了三个提供程序,所有这些提供程序都实现了将提供程序插入以下内容所需的 `LlmProvider` 接口

实现

#

要构建您自己的提供程序,您需要实现 `LlmProvider` 接口,并牢记以下几点

  1. 提供完整的配置支持

  2. 处理历史记录

  3. 将消息和附件转换为底层 LLM

  4. 调用底层 LLM

  5. 配置 为支持自定义提供程序的完整可配置性,您应该允许用户创建底层模型并将其作为参数传入,就像 Gemini 提供程序所做的那样

Dart
class GeminiProvider extends LlmProvider ... {
  @immutable
  GeminiProvider({
    required GenerativeModel model,
    ...
  })  : _model = model,
        ...

  final GenerativeModel _model;
  ...
}

通过这种方式,无论底层模型未来发生何种变化,您的自定义提供程序的用户都将可以使用所有配置选项。

  1. 历史记录 历史记录是任何提供程序的重要组成部分——提供程序不仅需要允许直接操作历史记录,还必须在其更改时通知侦听器。此外,为支持序列化和更改提供程序参数,它还必须支持将历史记录作为构建过程的一部分进行保存。

Gemini 提供程序按如下方式处理

Dart
class GeminiProvider extends LlmProvider with ChangeNotifier {
  @immutable
  GeminiProvider({
    required GenerativeModel model,
    Iterable<ChatMessage>? history,
    ...
  })  : _model = model,
        _history = history?.toList() ?? [],
        ... { ... }

  final GenerativeModel _model;
  final List<ChatMessage> _history;
  ...

  @override
  Stream<String> sendMessageStream(
    String prompt, {
    Iterable<Attachment> attachments = const [],
  }) async* {
    final userMessage = ChatMessage.user(prompt, attachments);
    final llmMessage = ChatMessage.llm();
    _history.addAll([userMessage, llmMessage]);

    final response = _generateStream(
      prompt: prompt,
      attachments: attachments,
      contentStreamGenerator: _chat!.sendMessageStream,
    );

    yield* response.map((chunk) {
      llmMessage.append(chunk);
      return chunk;
    });

    notifyListeners();
  }

  @override
  Iterable<ChatMessage> get history => _history;

  @override
  set history(Iterable<ChatMessage> history) {
    _history.clear();
    _history.addAll(history);
    _chat = _startChat(history);
    notifyListeners();
  }

  ...
}

您会注意到此代码中的几点

  • 使用 `ChangeNotifier` 来实现 `LlmProvider` 接口中的 `Listenable` 方法要求
  • 能够将初始历史记录作为构造函数参数传入
  • 在新用户提示/LLM 响应对出现时通知侦听器
  • 当历史记录手动更改时通知侦听器
  • 当历史记录更改时,使用新历史记录创建新聊天

本质上,自定义提供程序管理与底层 LLM 的单个聊天会话的历史记录。随着历史记录的更改,底层聊天要么需要自动保持最新(就像您调用底层特定于聊天的方法时 Gemini AI SDK for Dart 所做的那样),要么需要手动重新创建(就像 Gemini 提供程序在手动设置历史记录时所做的那样)。

  1. 消息和附件

附件必须从 `LlmProvider` 类型公开的标准 `ChatMessage` 类映射到由底层 LLM 处理的任何内容。例如,Gemini 提供程序将 AI 工具包中的 `ChatMessage` 类映射到 Gemini AI SDK for Dart 提供的 `Content` 类型,如以下示例所示

Dart
import 'package:google_generative_ai/google_generative_ai.dart';
...

class GeminiProvider extends LlmProvider with ChangeNotifier {
  ...
  static Part _partFrom(Attachment attachment) => switch (attachment) {
        (final FileAttachment a) => DataPart(a.mimeType, a.bytes),
        (final LinkAttachment a) => FilePart(a.url),
      };

  static Content _contentFrom(ChatMessage message) => Content(
        message.origin.isUser ? 'user' : 'model',
        [
          TextPart(message.text ?? ''),
          ...message.attachments.map(_partFrom),
        ],
      );
}

每当用户提示需要发送到底层 LLM 时,都会调用 `_contentFrom` 方法。每个提供程序都需要提供自己的映射。

  1. 调用 LLM

如何调用底层 LLM 来实现 `generateStream` 和 `sendMessageStream` 方法取决于它公开的协议。AI 工具包中的 Gemini 提供程序处理配置和历史记录,但对 `generateStream` 和 `sendMessageStream` 的每次调用最终都会调用 Gemini AI SDK for Dart 中的 API

Dart
class GeminiProvider extends LlmProvider with ChangeNotifier {
  ...

  @override
  Stream<String> generateStream(
    String prompt, {
    Iterable<Attachment> attachments = const [],
  }) =>
      _generateStream(
        prompt: prompt,
        attachments: attachments,
        contentStreamGenerator: (c) => _model.generateContentStream([c]),
      );

  @override
  Stream<String> sendMessageStream(
    String prompt, {
    Iterable<Attachment> attachments = const [],
  }) async* {
    final userMessage = ChatMessage.user(prompt, attachments);
    final llmMessage = ChatMessage.llm();
    _history.addAll([userMessage, llmMessage]);

    final response = _generateStream(
      prompt: prompt,
      attachments: attachments,
      contentStreamGenerator: _chat!.sendMessageStream,
    );

    yield* response.map((chunk) {
      llmMessage.append(chunk);
      return chunk;
    });

    notifyListeners();
  }

  Stream<String> _generateStream({
    required String prompt,
    required Iterable<Attachment> attachments,
    required Stream<GenerateContentResponse> Function(Content)
        contentStreamGenerator,
  }) async* {
    final content = Content('user', [
      TextPart(prompt),
      ...attachments.map(_partFrom),
    ]);

    final response = contentStreamGenerator(content);
    yield* response
        .map((chunk) => chunk.text)
        .where((text) => text != null)
        .cast<String>();
  }

  @override
  Iterable<ChatMessage> get history => _history;

  @override
  set history(Iterable<ChatMessage> history) {
    _history.clear();
    _history.addAll(history);
    _chat = _startChat(history);
    notifyListeners();
  }
}

示例

#

Gemini 提供程序Vertex 提供程序的实现几乎相同,为您的自定义提供程序提供了一个很好的起点。如果您想查看一个剥离了所有对底层 LLM 调用的提供程序示例实现,请查看 Echo 示例应用,它只是将用户的提示和附件格式化为 Markdown,然后将其作为响应发送回给用户。