連接大型語言模型 (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 提供者,插入聊天視圖中。 AI Toolkit 預設提供三種 LLM 提供者, 這三者都實作了 LlmProvider 介面, 因此可作為下列用途:

實作方式

#

若要建立自訂的 LLM 提供者,您需要實作 LlmProvider 介面,並注意以下幾點:

  1. 提供完整的組態支援

  2. 處理歷史紀錄

  3. 將訊息與附件轉換給底層 LLM

  4. 呼叫底層 LLM

  5. 組態 為了讓您的自訂提供者支援完整的組態功能, 您應該允許使用者建立底層模型, 並將其作為參數傳入,就像 Gemini provider 所做的那樣:

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

  final GenerativeModel _model;
  ...
}

如此一來,無論未來底層模型有任何變動,所有的設定選項都會對你自訂的 provider 使用者開放。

  1. 歷史紀錄
    歷史紀錄(History)是任何 provider 的重要部分——provider 不僅需要允許直接操作歷史紀錄,還必須在變動時通知監聽者。此外,為了支援序列化以及變更 provider 參數,它還必須支援在建構過程中儲存歷史紀錄。

Gemini provider 的處理方式如下所示:

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 方法需求
  • 可以在建構子參數中傳入初始歷史紀錄(history)
  • 當有新的使用者提示/大型語言模型 (LLM) 回應配對時,會通知監聽器
  • 當歷史紀錄被手動變更時,會通知監聽器
  • 當歷史紀錄變更時,會使用新的歷史紀錄建立新的聊天

基本上,自訂的 provider 會管理與底層大型語言模型 (LLM) 單一聊天會話的歷史紀錄。 隨著歷史紀錄的變化,底層聊天需要自動保持最新 (就像當你呼叫底層聊天專用方法時,Dart 的 Gemini AI SDK 會自動同步) 或是需要手動重新建立 (就像當 Gemini provider 手動設定歷史紀錄時所做的那樣)。

  1. 訊息與附件

附件必須從 LlmProvider 類型所公開的標準 ChatMessage 類別, 對應到底層大型語言模型 (LLM) 能處理的格式。 例如,Gemini provider 會將 AI Toolkit 的 ChatMessage 類別 對應到 Gemini AI SDK for Dart 所提供的 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),
        ],
      );
}

_contentFrom 方法會在每當需要將使用者提示詞傳送至底層 LLM 時被呼叫。
每個提供者都需要為其自身實作對應的映射。

  1. 呼叫 LLM

你如何呼叫底層 LLM 來實作 generateStreamsendMessageStream 方法,取決於其所公開的協定。
AI Toolkit 中的 Gemini 提供者會處理組態與歷史記錄,但對 generateStreamsendMessageStream 的呼叫,最終都會轉為呼叫 Gemini AI SDK for Dart 的 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 providerVertex provider 的實作幾乎完全相同,非常適合作為你自訂 provider 的起點。如果你想參考一個將所有對底層大型語言模型 (LLM) 呼叫都移除的 provider 實作範例,可以查看 Echo example app。該範例僅將使用者的 prompt 和附件格式化為 Markdown,然後作為回應傳回給使用者。