連線 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 以作為響應傳送回用戶。