自定義 LLM 提供商
如何與其他 Flutter 功能整合。
連線 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 提供程式接入聊天檢視。AI Toolkit 開箱即用提供了兩個提供程式,它們都實現了將提供程式接入以下內容所需的 LlmProvider 介面:
- Firebase AI Logic 提供程式,它封裝了
firebase_ai包 - Echo 提供程式,作為一個最小化的提供程式示例非常有用
實現
#要構建自己的提供程式,您需要在實現 LlmProvider 介面時考慮以下幾點:
提供全面的配置支援
處理歷史記錄
將訊息和附件轉換為底層 LLM 可識別的格式
呼叫底層 LLM
-
配置:為了在自定義提供程式中支援完全的可配置性,您應該允許使用者建立底層模型並將其作為引數傳入,正如
MyLlmProvider所做的那樣。
class MyLlmProvider extends LlmProvider ... {
@immutable
MyLlmProvider({
required GenerativeModel model,
...
}) : _model = model,
...
final GenerativeModel _model;
...
}
透過這種方式,無論底層模型將來發生什麼變化,配置選項都將提供給您的自定義提供程式的使用者。
- 歷史記錄:歷史記錄是任何提供程式的重要組成部分——提供程式不僅需要允許直接操作歷史記錄,還必須在歷史記錄更改時通知監聽器。此外,為了支援序列化和更改提供程式引數,它還必須支援在構建過程中儲存歷史記錄。
Firebase 提供程式的處理方式如下所示:
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 提供程式在呼叫底層聊天特定方法時所做的那樣),或者手動重新建立(就像 Firebase 提供程式在手動設定歷史記錄時所做的那樣)。
- 訊息和附件
附件必須從 LlmProvider 型別暴露的標準 ChatMessage 類對映到底層 LLM 處理的格式。例如,Firebase 提供程式將 AI Toolkit 中的 ChatMessage 類對映到 Firebase Logic AI SDK 提供的 Content 型別,如下例所示:
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 方法。每個提供程式都需要提供自己的對映邏輯。
- 呼叫 LLM
如何呼叫底層 LLM 來實現 generateStream 和 sendMessageStream 方法取決於它暴露的協議。AI Toolkit 中的 Firebase 提供程式處理配置和歷史記錄,但對 generateStream 和 sendMessageStream 的呼叫最終都會呼叫 Firebase Logic AI SDK 中的 API。
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 提供程式的實現為您自己的自定義提供程式提供了一個很好的起點。如果您想檢視一個剝離了所有到底層 LLM 呼叫的提供程式實現示例,請檢視 Echo 示例應用,它只是將使用者的提示和附件格式化為 Markdown,並作為響應傳送回給使用者。