LiteRT-LM Cross-Platform C++ API

Conversation adalah API tingkat tinggi, yang mewakili satu percakapan stateful dengan LLM dan merupakan titik entri yang direkomendasikan untuk sebagian besar pengguna. API ini secara internal mengelola Session dan menangani tugas pemrosesan data yang kompleks. Tugas ini mencakup mempertahankan konteks awal, mengelola definisi alat, melakukan pra-pemrosesan data multimodal, dan menerapkan template perintah Jinja dengan format pesan berbasis peran.

Alur Kerja Conversation API

Siklus proses umum untuk menggunakan Conversation API adalah:

  1. Membuat Engine: Menginisialisasi satu Engine dengan jalur dan konfigurasi model. Ini adalah objek berat yang menyimpan bobot model.
  2. Membuat Conversation: Menggunakan Engine untuk membuat satu atau beberapa objek Conversation ringan.
  3. Mengirim Pesan: Menggunakan metode objek Conversation untuk mengirim pesan ke LLM dan menerima respons, sehingga memungkinkan interaksi seperti chat.

Berikut adalah cara paling sederhana untuk mengirim pesan dan mendapatkan respons model. Cara ini direkomendasikan untuk sebagian besar kasus penggunaan. Cara ini mencerminkan Gemini Chat API.

  • SendMessage: Panggilan pemblokiran yang mengambil input pengguna dan menampilkan respons model lengkap.
  • SendMessageAsync: Panggilan non-pemblokiran yang menstreaming respons model kembali token demi token melalui callback.

Berikut adalah contoh cuplikan kode:

Konten teks saja

#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));

Contoh 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();
}

🔴 Baru: Prediksi Multi-Token (MTP)

Prediksi Multi-Token (MTP) adalah pengoptimalan performa yang mempercepat kecepatan dekode secara signifikan. MTP direkomendasikan secara universal untuk semua tugas di backend GPU.

Untuk menggunakan MTP, Anda harus mengaktifkan dekode spekulatif di setelan lanjutan konfigurasi mesin.

// 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::GPU);
CHECK_OK(engine_settings);

// 2. Enable MTP via speculative decoding in advanced settings.
litert::lm::AdvancedSettings advanced_settings;
advanced_settings.enable_speculative_decoding = true;
engine_settings->GetMutableMainExecutorSettings().SetAdvancedSettings(
    advanced_settings);

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

// The same steps to create Conversation and send messages as above...

Konten data multimodal

// 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;

Menggunakan Percakapan dengan Alat

Lihat Penggunaan Lanjutan untuk Penggunaan Alat mendetail dengan Conversation API

Komponen dalam Percakapan

Conversation dapat dianggap sebagai delegasi bagi pengguna untuk mempertahankan Session dan pemrosesan data yang rumit sebelum mengirim data ke Sesi.

Jenis I/O

Format input dan output inti untuk Conversation API adalah Message. Saat ini, format ini diimplementasikan sebagai JsonMessage, yang merupakan alias jenis untuk ordered_json, struktur data key-value pair bertingkat yang fleksibel.

The Conversation API beroperasi berdasarkan pesan masuk-pesan keluar basis, yang meniru pengalaman chat yang umum. Fleksibilitas Message memungkinkan pengguna menyertakan kolom arbitrer sesuai kebutuhan template perintah atau model LLM tertentu, sehingga LiteRT-LM dapat mendukung berbagai model.

Meskipun tidak ada satu standar yang kaku, sebagian besar template dan model perintah mengharapkan Message mengikuti konvensi yang mirip dengan yang digunakan dalam Konten Gemini API atau struktur Pesan OpenAI.

Message harus berisi role, yang menunjukkan dari siapa pesan dikirim. content bisa sesederhana string teks.

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

Untuk input data multimodal, content adalah daftar part. Sekali lagi, part bukanlah struktur data yang telah ditentukan, tetapi jenis data key-value pair yang diurutkan. Kolom tertentu bergantung pada apa yang diharapkan oleh template perintah dan model.

{
  "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"
    }
  ]
}

Untuk part multimodal, kami mendukung format berikut yang ditangani oleh 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",
}

Template Perintah

Untuk mempertahankan fleksibilitas model varian, PromptTemplate di implementasikan sebagai wrapper tipis di sekitar Minja. Minja adalah implementasi C++ dari mesin template Jinja, yang memproses input JSON untuk menghasilkan perintah yang diformat.

Mesin template Jinja adalah format yang banyak digunakan untuk template perintah LLM. Berikut beberapa contohnya:

Format mesin template Jinja harus sesuai dengan struktur yang diharapkan oleh model yang disesuaikan dengan petunjuk. Biasanya, rilis model menyertakan template Jinja standar untuk memastikan penggunaan model yang tepat.

Template Jinja yang digunakan oleh model akan disediakan oleh metadata file model.

Catatan: Perubahan kecil dalam perintah karena format yang salah dapat menyebabkan penurunan kualitas model yang signifikan. Seperti yang dilaporkan dalam Mengukur Sensitivitas Model Bahasa terhadap Fitur Palsu dalam Desain Perintah atau: Cara Saya Belajar untuk Mulai Mengkhawatirkan Format Perintah

Pengantar

Preface menetapkan konteks awal untuk percakapan. Konteks ini dapat mencakup pesan awal, definisi alat, dan informasi latar belakang lainnya yang diperlukan LLM untuk memulai interaksi. Hal ini mencapai fungsi yang mirip dengan the Gemini API system instruction dan Gemini API Tools

Pengantar berisi kolom berikut

  • messages Pesan dalam pengantar. Pesan tersebut memberikan latar belakang awal untuk percakapan. Misalnya, pesan dapat berupa histori percakapan, petunjuk sistem rekayasa perintah, contoh beberapa kali, dll.

  • tools Alat yang dapat digunakan model dalam percakapan. Format alat tidak tetap, tetapi sebagian besar mengikuti Gemini API FunctionDeclaration.

  • extra_context Konteks tambahan yang mempertahankan kemampuan model untuk menyesuaikan informasi konteks yang diperlukan untuk memulai percakapan. Misalnya,

    • enable_thinking untuk model dengan mode berpikir, misalnya Qwen3 atau SmolLM3-3B.

Contoh pengantar untuk memberikan petunjuk sistem awal, alat, dan menonaktifkan mode berpikir.

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}
  }
});

Histori

Percakapan mempertahankan daftar semua pertukaran Pesan dalam sesi. Histori ini sangat penting untuk rendering template perintah, karena template perintah jinja biasanya memerlukan seluruh histori percakapan untuk menghasilkan perintah yang benar untuk LLM.

Namun, Sesi LiteRT-LM bersifat stateful, yang berarti sesi ini memproses input secara bertahap. Untuk menjembatani kesenjangan ini, Percakapan menghasilkan perintah inkremental yang diperlukan dengan merender template perintah dua kali: sekali dengan histori hingga giliran sebelumnya, dan sekali termasuk pesan saat ini. Dengan membandingkan kedua perintah yang dirender ini, perintah tersebut akan mengekstrak bagian baru yang akan dikirim ke Sesi.

ConversationConfig

ConversationConfig digunakan untuk menginisialisasi instance Conversation. Anda dapat membuat konfigurasi ini dengan beberapa cara:

  1. Dari Engine: Metode ini menggunakan SessionConfig default yang terkait dengan mesin.
  2. Dari SessionConfigtertentu: Hal ini memungkinkan kontrol yang lebih terperinci atas setelan sesi.

Selain setelan sesi, Anda dapat menyesuaikan lebih lanjut perilaku Conversation dalam ConversationConfig. Hal ini mencakup:

Penimpaan ini sangat berguna untuk model yang disesuaikan, yang mungkin memerlukan konfigurasi atau template perintah yang berbeda dari model dasar yang digunakan.

MessageCallback

MessageCallback adalah fungsi callback yang harus diterapkan pengguna saat menggunakan metode SendMessageAsync asinkron.

Tanda tangan callback adalah absl::AnyInvocable<void(absl::StatusOr<Message>)>. Fungsi ini dipicu dalam kondisi berikut:

  • Saat bagian baru Message diterima dari Model.
  • Jika terjadi error selama pemrosesan pesan LiteRT-LM.
  • Setelah penyelesaian inferensi LLM, callback akan dipicu dengan kosong Message (misalnya, JsonMessage()) untuk menandakan akhir respons.

Lihat panggilan asinkron Langkah 6 untuk contoh implementasi.

Catatan: Message yang diterima oleh callback hanya berisi bagian terbaru dari output model, bukan seluruh histori pesan.

Misalnya, jika respons model lengkap yang diharapkan dari panggilan pemblokiran SendMessage adalah:

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

Callback di SendMessageAsync mungkin dipanggil beberapa kali, setiap kali dengan bagian teks berikutnya:

// 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!"
  ]
}

Implementer bertanggung jawab untuk mengumpulkan bagian-bagian ini jika respons lengkap diperlukan selama streaming asinkron. Atau, respons lengkap akan tersedia sebagai entri terakhir di History setelah panggilan asinkron selesai.

Penggunaan Lanjutan

Dekode Terbatas

LiteRT-LM mendukung dekode terbatas, yang memungkinkan Anda menerapkan struktur tertentu pada output model, seperti skema JSON, pola Regex, atau aturan tata bahasa.

Untuk mengaktifkannya, tetapkan EnableConstrainedDecoding(true) di ConversationConfig dan berikan ConstraintProviderConfig (misalnya, LlGuidanceConfig untuk dukungan regex/JSON/tata bahasa). Kemudian, teruskan batasan melalui OptionalArgs di SendMessage.

Contoh: Batasan 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}
);

Untuk mengetahui detail selengkapnya, termasuk dukungan Skema JSON dan Tata Bahasa Lark, lihat dokumentasi Dekode Terbatas.

Penggunaan Alat

Panggilan alat memungkinkan LLM meminta eksekusi fungsi sisi klien. Anda menentukan alat di Preface percakapan, dengan memberi kunci berdasarkan nama. Saat model menampilkan panggilan alat, Anda akan mengambilnya, menjalankan fungsi yang sesuai di aplikasi, dan menampilkan hasilnya ke model.

Alur tingkat tinggi:

  1. Mendeklarasikan Alat: Menentukan alat (nama, deskripsi, parameter) dalam JSON Preface.
  2. Mendeteksi Panggilan: Memeriksa model_message["tool_calls"] dalam respons.
  3. Menjalankan: Menjalankan logika aplikasi untuk alat yang diminta.
  4. Merespons: Mengirim pesan dengan role: "tool" yang berisi output alat kembali ke model.

Untuk mengetahui detail selengkapnya dan contoh loop chat lengkap, lihat dokumentasi Penggunaan Alat.