跳到主內容

自定義 LLM 提供商

如何與其他 Flutter 功能整合。

連線 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 提供程式接入聊天檢視。AI Toolkit 開箱即用提供了兩個提供程式,它們都實現了將提供程式接入以下內容所需的 LlmProvider 介面:

實現

#

要構建自己的提供程式,您需要在實現 LlmProvider 介面時考慮以下幾點:

  1. 提供全面的配置支援

  2. 處理歷史記錄

  3. 將訊息和附件轉換為底層 LLM 可識別的格式

  4. 呼叫底層 LLM

  5. 配置:為了在自定義提供程式中支援完全的可配置性,您應該允許使用者建立底層模型並將其作為引數傳入,正如 MyLlmProvider 所做的那樣。

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

  final GenerativeModel _model;
  ...
}

透過這種方式,無論底層模型將來發生什麼變化,配置選項都將提供給您的自定義提供程式的使用者。

  1. 歷史記錄:歷史記錄是任何提供程式的重要組成部分——提供程式不僅需要允許直接操作歷史記錄,還必須在歷史記錄更改時通知監聽器。此外,為了支援序列化和更改提供程式引數,它還必須支援在構建過程中儲存歷史記錄。

Firebase 提供程式的處理方式如下所示:

dart
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 提供程式在手動設定歷史記錄時所做的那樣)。

  1. 訊息和附件

附件必須從 LlmProvider 型別暴露的標準 ChatMessage 類對映到底層 LLM 處理的格式。例如,Firebase 提供程式將 AI Toolkit 中的 ChatMessage 類對映到 Firebase Logic AI SDK 提供的 Content 型別,如下例所示:

dart
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 方法。每個提供程式都需要提供自己的對映邏輯。

  1. 呼叫 LLM

如何呼叫底層 LLM 來實現 generateStreamsendMessageStream 方法取決於它暴露的協議。AI Toolkit 中的 Firebase 提供程式處理配置和歷史記錄,但對 generateStreamsendMessageStream 的呼叫最終都會呼叫 Firebase Logic AI SDK 中的 API。

dart
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,並作為響應傳送回給使用者。