自定義 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 的單個聊天會話的歷史記錄。隨著歷史記錄的變化,底層聊天需要自動保持最新(就像 Dart 的 Gemini AI SDK 在您呼叫底層聊天特定方法時所做的那樣)或手動重新建立(就像 Gemini 提供商在歷史記錄手動設定時所做的那樣)。
- 訊息和附件
附件必須從 LlmProvider 型別公開的標準 ChatMessage 類對映到底層 LLM 處理的任何內容。例如,Gemini 提供商將 AI 工具包中的 ChatMessage 類對映到 Dart 的 Gemini AI SDK 提供的 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 的呼叫最終都會呼叫 Dart 的 Gemini AI SDK 中的 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 以作為響應傳送回用戶。