自定义 LLM 提供商
连接 LLM 和 `LlmChatView` 的协议在 `LlmProvider` 接口中定义。
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` 接口
- Gemini 提供程序,它封装了 `google_generative_ai` 包
- Vertex 提供程序,它封装了 `firebase_vertexai` 包
- Echo 提供程序,它可作为极简提供程序示例
实现
#要构建您自己的提供程序,您需要实现 `LlmProvider` 接口,并牢记以下几点
提供完整的配置支持
处理历史记录
将消息和附件转换为底层 LLM
调用底层 LLM
配置 为支持自定义提供程序的完整可配置性,您应该允许用户创建底层模型并将其作为参数传入,就像 Gemini 提供程序所做的那样
class GeminiProvider extends LlmProvider ... {
@immutable
GeminiProvider({
required GenerativeModel model,
...
}) : _model = model,
...
final GenerativeModel _model;
...
}
通过这种方式,无论底层模型未来发生何种变化,您的自定义提供程序的用户都将可以使用所有配置选项。
- 历史记录 历史记录是任何提供程序的重要组成部分——提供程序不仅需要允许直接操作历史记录,还必须在其更改时通知侦听器。此外,为支持序列化和更改提供程序参数,它还必须支持将历史记录作为构建过程的一部分进行保存。
Gemini 提供程序按如下方式处理
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 提供程序在手动设置历史记录时所做的那样)。
- 消息和附件
附件必须从 `LlmProvider` 类型公开的标准 `ChatMessage` 类映射到由底层 LLM 处理的任何内容。例如,Gemini 提供程序将 AI 工具包中的 `ChatMessage` 类映射到 Gemini AI SDK for Dart 提供的 `Content` 类型,如以下示例所示
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` 方法。每个提供程序都需要提供自己的映射。
- 调用 LLM
如何调用底层 LLM 来实现 `generateStream` 和 `sendMessageStream` 方法取决于它公开的协议。AI 工具包中的 Gemini 提供程序处理配置和历史记录,但对 `generateStream` 和 `sendMessageStream` 的每次调用最终都会调用 Gemini AI SDK for Dart 中的 API
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,然后将其作为响应发送回给用户。