LiteRT-LM クロスプラットフォーム C++ API

Conversation は、LLM との単一のステートフルな会話を表す高レベルの API であり、ほとんどのユーザーにおすすめのエントリ ポイントです。内部で Session を管理し、複雑なデータ処理タスクを処理します。これらのタスクには、初期コンテキストの維持、ツール定義の管理、マルチモーダル データの前処理、ロールベースのメッセージ形式を使用した Jinja プロンプト テンプレートの適用が含まれます。

Conversation API ワークフロー

Conversation API を使用する一般的なライフサイクルは次のとおりです。

  1. Engine を作成する: モデルパスと構成を使用して、単一の Engine を初期化します。これは、モデルの重みを保持するヘビーウェイト オブジェクトです。
  2. Conversation を作成する: Engine を使用して、1 つ以上の軽量 Conversation オブジェクトを作成します。
  3. メッセージを送信する: Conversation オブジェクトのメソッドを使用して LLM にメッセージを送信し、レスポンスを受信します。これにより、チャットのようなインタラクションが効果的に実現します。

以下は、メッセージを送信してモデルのレスポンスを取得する最も簡単な方法です。ほとんどのユースケースで推奨されます。Gemini Chat API をミラーリングします。

  • SendMessage: ユーザー入力を受け取り、完全なモデル レスポンスを返すブロッキング呼び出し。
  • SendMessageAsync: コールバックを介してモデルのレスポンスをトークンごとにストリーミングする非ブロッキング呼び出し。

コード スニペットの例を次に示します。

テキストのみのコンテンツ

#include "runtime/engine/engine.h"

// ...

// 1. Define model assets and engine settings.
auto model_assets = ModelAssets::Create(model_path);
CHECK_OK(model_assets);

auto engine_settings = EngineSettings::CreateDefault(
    model_assets,
    /*backend=*/litert::lm::Backend::CPU);

// 2. Create the main Engine object.
absl::StatusOr<std::unique_ptr<Engine>> engine = Engine::CreateEngine(engine_settings);
CHECK_OK(engine);

// 3. Create a Conversation
auto conversation_config = ConversationConfig::CreateDefault(**engine);
CHECK_OK(conversation_config)
absl::StatusOr<std::unique_ptr<Conversation>> conversation = Conversation::Create(**engine, *conversation_config);
CHECK_OK(conversation);

// 4. Send message to the LLM with blocking call.
absl::StatusOr<Message> model_message = (*conversation)->SendMessage(
    JsonMessage{
        {"role", "user"},
        {"content", "What is the tallest building in the world?"}
    });
CHECK_OK(model_message);

// 5. Print the model message.
std::cout << *model_message << std::endl;

// 6. Send message to the LLM with asynchronous call
// where CreatePrintMessageCallback is a users implemented callback that would
// process the message once a chunk of message output is received.
std::stringstream captured_output;
(*conversation)->SendMessageAsync(
    JsonMessage{
        {"role", "user"},
        {"content", "What is the tallest building in the world?"}
    },
    CreatePrintMessageCallback(std::stringstream& captured_output)
);
// Wait until asynchronous finish or timeout.
*engine->WaitUntilDone(absl::Seconds(10));

CreatePrintMessageCallback

absl::AnyInvocable<void(absl::StatusOr<Message>)> CreatePrintMessageCallback(
    std::stringstream& captured_output) {
  return [&captured_output](absl::StatusOr<Message> message) {
    if (!message.ok()) {
      std::cout << message.status().message() << std::endl;
      return;
    }
    if (auto json_message = std::get_if<JsonMessage>(&(*message))) {
      if (json_message->is_null()) {
        std::cout << std::endl << std::flush;
        return;
      }
      ABSL_CHECK_OK(PrintJsonMessage(*json_message, captured_output,
                                     /*streaming=*/true));
    }
  };
}

absl::Status PrintJsonMessage(const JsonMessage& message,
                              std::stringstream& captured_output,
                              bool streaming = false) {
  if (message["content"].is_array()) {
    for (const auto& content : message["content"]) {
      if (content["type"] == "text") {
        captured_output << content["text"].get<std::string>();
        std::cout << content["text"].get<std::string>();
      }
    }
    if (!streaming) {
      captured_output << std::endl << std::flush;
      std::cout << std::endl << std::flush;
    } else {
      captured_output << std::flush;
      std::cout << std::flush;
    }
  } else if (message["content"]["text"].is_string()) {
    if (!streaming) {
      captured_output << message["content"]["text"].get<std::string>()
                      << std::endl
                      << std::flush;
      std::cout << message["content"]["text"].get<std::string>() << std::endl
                << std::flush;
    } else {
      captured_output << message["content"]["text"].get<std::string>()
                      << std::flush;
      std::cout << message["content"]["text"].get<std::string>() << std::flush;
    }
  } else {
    return absl::InvalidArgumentError("Invalid message: " + message.dump());
  }
  return absl::OkStatus();
}

マルチモーダル データ コンテンツ

// To use multimodality, the engine must be created with vision and audio
// backend depending on the multimodality to be used
auto engine_settings = EngineSettings::CreateDefault(
    model_assets,
    /*backend=*/litert::lm::Backend::CPU,
    /*vision_backend*/litert::lm::Backend::GPU,
    /*audio_backend*/litert::lm::Backend::CPU,
);

// The same steps to create Engine and Conversation as above...

// Send message to the LLM with image data.
absl::StatusOr<Message> model_message = (*conversation)->SendMessage(
    JsonMessage{
        {"role", "user"},
        {"content", { // Now content must be an array.
          {
            {"type", "text"}, {"text", "Describe the following image: "}
          },
          {
            {"type", "image"}, {"path", "/file/path/to/image.jpg"}
          }
        }},
    });
CHECK_OK(model_message);

// Print the model message.
std::cout << *model_message << std::endl;

// Send message to the LLM with audio data.
model_message = (*conversation)->SendMessage(
    JsonMessage{
        {"role", "user"},
        {"content", { // Now content must be an array.
          {
            {"type", "text"}, {"text", "Transcribe the audio: "}
          },
          {
            {"type", "audio"}, {"path", "/file/path/to/audio.wav"}
          }
        }},
    });
CHECK_OK(model_message);

// Print the model message.
std::cout << *model_message << std::endl;

// The content can include multiple image or audio data.
model_message = (*conversation)->SendMessage(
    JsonMessage{
        {"role", "user"},
        {"content", { // Now content must be an array.
          {
            {"type", "text"}, {"text", "First briefly describe the two images "}
          },
          {
            {"type", "image"}, {"path", "/file/path/to/image1.jpg"}
          },
          {
            {"type", "text"}, {"text", "and "}
          },
          {
            {"type", "image"}, {"path", "/file/path/to/image2.jpg"}
          },
          {
            {"type", "text"}, {"text", " then transcribe the content in the audio"}
          },
          {
            {"type", "audio"}, {"path", "/file/path/to/audio.wav"}
          }
        }},
    });
CHECK_OK(model_message);

// Print the model message.
std::cout << *model_message << std::endl;

ツールを使用した会話

Conversation API を使用したツールの詳細については、高度な使用方法を参照してください。

会話のコンポーネント

Conversation は、ユーザーが Session を維持し、データを Session に送信する前に複雑なデータ処理を行うためのデリゲートと見なすことができます。

I/O の種類

Conversation API のコアの入出力形式は Message です。現在、これは JsonMessage として実装されています。これは、柔軟なネストされた Key-Value データ構造である ordered_json の型エイリアスです。

Conversation API はメッセージイン メッセージアウト ベースで動作し、一般的なチャット エクスペリエンスを模倣します。Message の柔軟性により、ユーザーは特定のプロンプト テンプレートや LLM モデルで必要に応じて任意のフィールドを含めることができます。これにより、LiteRT-LM は幅広いモデルをサポートできます。

厳密な単一の標準はありませんが、ほとんどのプロンプト テンプレートとモデルでは、MessageGemini API コンテンツまたは OpenAI メッセージ構造で使用されているものと同様の規則に従うことが想定されています。

Message には、メッセージの送信元を表す role が含まれている必要があります。content はテキスト文字列のように単純なものでも構いません。

{
  "role": "model", // Represent who the message is sent from.
  "content": "Hello World!" // Naive text only content.
}

マルチモーダル データ入力の場合、contentpart のリストです。ここでも part は事前定義されたデータ構造ではなく、順序付き Key-Value ペアのデータ型です。具体的なフィールドは、プロンプト テンプレートとモデルが想定している内容によって異なります。

{
  "role": "user",
  "content": [  // Multimodal content.
    // Now the content is composed of parts
    {
      "type": "text",
      "text": "Describe the image in details: "
    },
    {
      "type": "image",
      "path": "/path/to/image.jpg"
    }
  ]
}

マルチモーダル part の場合、data_utils.h で処理される次の形式がサポートされています。

{
  "type": "text",
  "text": "this is a text"
}

{
  "type": "image",
  "path": "/path/to/image.jpg"
}

{
  "type": "image",
  "blob": "base64 encoded image bytes as string",
}

{
  "type": "audio",
  "path": "/path/to/audio.wav"
}

{
  "type": "audio",
  "blob": "base64 encoded audio bytes as string",
}

プロンプト テンプレート

バリアント モデルの柔軟性を維持するため、PromptTemplateMinja のシン ラッパーとして実装されています。Minja は Jinja テンプレート エンジンの C++ 実装です。JSON 入力を処理して、フォーマットされたプロンプトを生成します。

Jinja テンプレート エンジンは、LLM プロンプト テンプレートで広く採用されている形式です。いくつか例を挙げましょう。

Jinja テンプレート エンジンの形式は、指示チューニング モデルが想定する構造と厳密に一致している必要があります。通常、モデルのリリースには、モデルの適切な使用を保証するための標準の Jinja テンプレートが含まれています。

モデルで使用される Jinja テンプレートは、モデルファイルのメタデータによって提供されます。

[!NOTE] 形式が正しくないためにプロンプトがわずかに変更されると、モデルのパフォーマンスが大幅に低下する可能性があります。Quantifying Language Models' Sensitivity to Spurious Features in Prompt Design or: How I learned to start worrying about prompt formatting で報告されているように

序文

Preface は、会話の初期コンテキストを設定します。これには、初期メッセージ、ツール定義、LLM がインタラクションを開始するために必要なその他の背景情報を含めることができます。これにより、Gemini API system instructionGemini API Tools に似た機能が実現します。

Preface には次のフィールドが含まれています

  • messages 序文のメッセージ。メッセージは会話の最初の背景を提供しました。たとえば、メッセージは会話履歴、プロンプト エンジニアリング システムの指示、フューショットの例などです。

  • tools モデルが会話で使用できるツール。ツールの形式は固定されていませんが、ほとんどの場合 Gemini API FunctionDeclaration に従います。

  • extra_context 会話を開始するために必要なコンテキスト情報をモデルがカスタマイズできるように、拡張性を維持する追加のコンテキスト。たとえば、

    • 思考モードのモデル(Qwen3SmolLM3-3B など)の場合は enable_thinking

初期のシステム指示、ツールを提供し、思考モードを無効にするための序文の例。

Preface preface = JsonPreface({
  .messages = {
      {"role", "system"},
      {"content", {"You are a model that can do function calling."}}
    },
  .tools = {
    {
      {"name", "get_weather"},
      {"description", "Returns the weather for a given location."},
      {"parameters", {
        {"type", "object"},
        {"properties", {
          {"location", {
            {"type", "string"},
            {"description", "The location to get the weather for."}
          }}
        }},
        {"required", {"location"}}
      }}
    },
    {
      {"name", "get_stock_price"},
      {"description", "Returns the stock price for a given stock symbol."},
      {"parameters", {
        {"type", "object"},
        {"properties", {
          {"stock_symbol", {
            {"type", "string"},
            {"description", "The stock symbol to get the price for."}
          }}
        }},
        {"required", {"stock_symbol"}}
      }}
    }
  },
  .extra_context = {
    {"enable_thinking": false}
  }
});

履歴

Conversation は、セッション内のすべての Message のやり取りのリストを保持します。この履歴は、プロンプト テンプレートのレンダリングに不可欠です。通常、jinja プロンプト テンプレートでは、LLM の正しいプロンプトを生成するために会話履歴全体が必要になるためです。

ただし、LiteRT-LM セッションはステートフルであり、入力を増分的に処理します。このギャップを埋めるため、会話では、プロンプト テンプレートを 2 回レンダリングして、必要な増分プロンプトを生成します。1 回目は前のターンの履歴を使用し、2 回目は現在のメッセージを含めます。レンダリングされた 2 つのプロンプトを比較して、セッションに送信する新しい部分を抽出します。

ConversationConfig

ConversationConfig は、Conversation インスタンスの初期化に使用されます。この構成は、次の 2 つの方法で作成できます。

  1. Engine から: このメソッドは、エンジンに関連付けられたデフォルトの SessionConfig を使用します。
  2. 特定の SessionConfig から: これにより、セッション設定をより細かく制御できます。

セッション設定以外にも、ConversationConfig 内の Conversation の動作をさらにカスタマイズできます。これには以下が該当します。

これらのオーバーライドは、ファインチューニングされたモデルで特に便利です。ファインチューニングされたモデルでは、派生元のベースモデルとは異なる構成やプロンプト テンプレートが必要になる場合があります。

MessageCallback

MessageCallback は、非同期の SendMessageAsync メソッドを使用する際にユーザーが実装する必要があるコールバック関数です。

コールバックのシグネチャは absl::AnyInvocable<void(absl::StatusOr<Message>)> です。この関数は、次の条件でトリガーされます。

  • Model から新しい Message のチャンクを受信したとき。
  • LiteRT-LM のメッセージ処理中にエラーが発生した場合。
  • LLM の推論が完了すると、空の MessageJsonMessage())を送信して、レスポンスの終了を通知します。

実装例については、ステップ 6 の非同期呼び出しを参照してください。

[!IMPORTANT] コールバックで受信される Message には、メッセージ履歴全体ではなく、モデルの出力の最新のチャンクのみが含まれます。

たとえば、ブロッキング SendMessage 呼び出しから想定される完全なモデル レスポンスが次のようになっているとします。

{
  "role": "model",
  "content": [
    "type": "text",
    "text": "Hello World!"
  ]
}

SendMessageAsync のコールバックは複数回呼び出される可能性があり、そのたびにテキストの次の部分が渡されます。

// 1st Message
{
  "role": "model",
  "content": [
    "type": "text",
    "text": "He"
  ]
}

// 2nd Message
{
  "role": "model",
  "content": [
    "type": "text",
    "text": "llo"
  ]
}

// 3rd Message
{
  "role": "model",
  "content": [
    "type": "text",
    "text": " Wo"
  ]
}

// 4th Message
{
  "role": "model",
  "content": [
    "type": "text",
    "text": "rl"
  ]
}

// 5th Message
{
  "role": "model",
  "content": [
    "type": "text",
    "text": "d!"
  ]
}

非同期ストリーム中に完全なレスポンスが必要な場合、実装者はこれらのチャンクを蓄積する責任を負います。また、非同期呼び出しが完了すると、完全なレスポンスが History の最後のエントリとして利用可能になります。

高度な使用方法

制約付きデコード

LiteRT-LM は制約付きデコードをサポートしており、JSON スキーマ、正規表現パターン、文法規則など、モデルの出力に特定の構造を適用できます。

有効にするには、ConversationConfigEnableConstrainedDecoding(true) を設定し、ConstraintProviderConfigLlGuidanceConfig(正規表現、JSON、文法をサポート)。次に、SendMessageOptionalArgs を介して制約を渡します。

例: Regex 制約

LlGuidanceConstraintArg constraint_arg;
constraint_arg.constraint_type = LlgConstraintType::kRegex;
constraint_arg.constraint_string = "a+b+"; // Force output to match this regex

auto response = conversation->SendMessage(
    user_message,
    {.decoding_constraint = constraint_arg}
);

JSON スキーマと Lark 文法のサポートを含む詳細については、制約付きデコードのドキュメントをご覧ください。

ツールの使用

ツール呼び出しにより、LLM はクライアントサイド関数の実行をリクエストできます。ツールは会話の Preface で定義し、名前でキー設定します。モデルがツール呼び出しを出力したら、それをキャプチャし、アプリケーションで対応する関数を実行して、結果をモデルに返します。

フローの概要: 1. ツールを宣言する: Preface JSON でツール(名前、説明、パラメータ)を定義します。2. 検出呼び出し: レスポンスで model_message["tool_calls"] を確認します。3. 実行: リクエストされたツールのアプリケーション ロジックを実行します。4. Respond: ツールの出力を含む role: "tool" を含むメッセージをモデルに返送します。

詳細と完全なチャットループの例については、ツールの使用に関するドキュメントをご覧ください。