Skip to main content
Contents

自定义 LLM 提供程序

Contents

连接大型语言模型 (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 进行的单个聊天会话的历史记录。随着历史记录的变化,底层聊天要么需要自动保持最新(就像 Dart 的 Gemini AI SDK 在你调用底层特定于聊天的方法时那样),要么需要手动重新创建(就像 Gemini 提供程序在每次手动设置历史记录时那样)。

  1. 消息和附件

必须将附件从 LlmProvider 类型公开的标准 ChatMessage 类映射到底层 LLM 处理的任何内容。例如,Gemini 提供程序将 AI 工具包的 ChatMessage 类映射到 Dart 的 Gemini AI SDK 提供的 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 来实现 generateStreamsendMessageStream 方法取决于它公开的协议。AI 工具包中的 Gemini 提供程序处理配置和历史记录,但对 generateStreamsendMessageStream 的调用最终都会调用 Dart 的 Gemini AI SDK 的 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 以作为其响应发送回用户。