跳转至正文

自定义 LLM 提供商

如何与其他 Flutter 功能集成。

连接 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 提供商接入聊天视图。 AI 工具包自带两个提供商,均实现接入所需的 LlmProvider 接口:

实现

#

构建自有提供商时,实现 LlmProvider 接口需注意:

  1. 提供完整配置支持

  2. 处理历史记录

  3. 将消息与附件转换为底层 LLM 格式

  4. 调用底层 LLM

  5. 配置要在自定义提供商中支持完整可配置性,应让用户创建底层模型并作为参数传入,如 MyLlmProvider

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

  final GenerativeModel _model;
  ...
}

这样无论底层模型未来如何变化,自定义提供商的用户仍可使用全部配置项。

  1. 历史记录

    历史记录是任何提供商的重要部分——不仅需支持直接操作历史,还须在变更时通知监听者。此外,为支持序列化与更改提供商参数,构造过程中还须支持保存历史。

Firebase provider 的处理方式如下:

dart
class MyLlmProvider extends LlmProvider with ChangeNotifier {
  @immutable
  MyLlmProvider({
    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 的历史。历史变更时,底层聊天需自动保持同步(如 Firebase provider 在调用聊天专用方法时),或手动重建(如 Firebase provider 在手动设置历史时)。

  1. 消息与附件

附件必须从 LlmProvider 暴露的标准 ChatMessage 类映射到底层 LLM 所处理的类型。例如,Firebase provider 将 AI 工具包的 ChatMessage 映射为 Firebase Logic AI SDK 的 Content 类型,如下所示:

dart
import 'package:firebase_ai/firebase_ai.dart';
...

class MyLlmProvider 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

如何实现 generateStreamsendMessageStream 取决于底层 LLM 暴露的协议。 AI 工具包中的 Firebase provider 处理配置与历史,但对 generateStreamsendMessageStream 的调用最终都会落到 Firebase Logic AI SDK 的 API:

dart
class MyLlmProvider 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();
  }
}

示例

#

Firebase provider 实现是构建自定义提供商的良好起点。若想看去掉所有底层 LLM 调用的示例实现,请参阅 Echo 示例应用:它将用户提示词与附件格式化为 Markdown 作为回复返回。