LiteRT-LM Cross-Platform C++ API

Conversation adalah API tingkat tinggi, yang merepresentasikan satu percakapan dengan LLM yang memiliki status dan merupakan titik entri yang direkomendasikan untuk sebagian besar pengguna. Secara internal, class ini mengelola Session dan menangani tugas pemrosesan data yang kompleks. Tugas ini mencakup mempertahankan konteks awal, mengelola definisi alat, memproses 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. Buat Engine: Lakukan inisialisasi satu Engine dengan jalur dan konfigurasi model. Ini adalah objek berat yang memuat bobot model.
  2. Buat Conversation: Gunakan Engine untuk membuat satu atau beberapa objek Conversation ringan.
  3. Kirim Pesan: Gunakan 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. Direkomendasikan untuk sebagian besar kasus penggunaan. API ini mencerminkan Gemini Chat API.

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

Berikut contoh cuplikan kode:

Konten hanya teks

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

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 penerima tugas bagi pengguna untuk mempertahankan Session dan pemrosesan data yang rumit sebelum mengirimkan data ke Sesi.

Jenis I/O

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

API Conversation beroperasi berdasarkan pesan masuk-pesan keluar, meniru pengalaman chat biasa. 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 ketat, sebagian besar template dan model perintah mengharapkan Message mengikuti konvensi yang serupa dengan yang digunakan dalam Konten Gemini API atau Struktur Pesan OpenAI.

Message harus berisi role, yang menunjukkan siapa pengirim pesan. 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 sebelumnya, melainkan 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 diimplementasikan sebagai wrapper tipis di sekitar Minja. Minja adalah penerapan 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 adalah beberapa contohnya:

Format mesin template Jinja harus sesuai dengan struktur yang diharapkan oleh model yang dioptimalkan untuk mengikuti perintah. Biasanya, rilis model mencakup template Jinja standar untuk memastikan penggunaan model yang tepat.

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

[!CATATAN] Perubahan kecil pada perintah karena pemformatan yang salah dapat menyebabkan penurunan kualitas model yang signifikan. Seperti yang dilaporkan dalam Quantifying Language Models' Sensitivity to Spurious Features in Prompt Design or: How I learned to start worrying about prompt formatting

Pengantar

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

Preface berisi kolom berikut

  • messages Pesan dalam kata pengantar. Pesan tersebut memberikan latar belakang awal percakapan. Misalnya, pesan dapat berupa histori percakapan, petunjuk sistem teknik perintah, contoh beberapa tembakan, 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 ekstensibilitas model untuk menyesuaikan informasi konteks yang diperlukan untuk memulai percakapan. Misalnya:

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

Contoh kata 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

Conversation menyimpan daftar semua pertukaran Message dalam sesi. Histori ini sangat penting untuk rendering template perintah, karena template perintah jinja biasanya memerlukan seluruh histori percakapan untuk menghasilkan perintah yang benar bagi LLM.

Namun, Sesi LiteRT-LM bersifat stateful, yang berarti sesi memproses input secara inkremental. Untuk menjembatani kesenjangan ini, Conversation membuat perintah inkremental yang diperlukan dengan merender template perintah dua kali: sekali dengan histori hingga giliran sebelumnya, dan sekali dengan menyertakan pesan saat ini. Dengan membandingkan kedua perintah yang dirender ini, bagian baru akan diekstrak untuk dikirim ke Session.

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 SessionConfig tertentu: 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:

Penggantian ini sangat berguna untuk model yang di-fine-tune, yang mungkin memerlukan konfigurasi atau template perintah yang berbeda dengan model dasar yang menjadi dasarnya.

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 potongan Message baru diterima dari Model.
  • Jika terjadi error selama pemrosesan pesan LiteRT-LM.
  • Setelah inferensi LLM selesai, callback dipicu dengan Message kosong (misalnya, JsonMessage()) untuk menandakan akhir respons.

Lihat Panggilan asinkron Langkah 6 untuk contoh penerapannya.

[!PENTING] Message yang diterima oleh callback hanya berisi potongan output model terbaru, bukan seluruh histori pesan.

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

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

Callback di SendMessageAsync dapat 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!"
  ]
}

Pelaksana 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

Decoding Terkendala

LiteRT-LM mendukung decoding terbatas, sehingga Anda dapat 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 Ekspresi Reguler

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 dalam Preface percakapan, dengan menentukannya berdasarkan nama. Saat model menghasilkan panggilan alat, Anda akan mengambilnya, menjalankan fungsi yang sesuai di aplikasi, dan menampilkan hasilnya ke model.

Alur tingkat tinggi: 1. Mendeklarasikan Alat: Tentukan alat (nama, deskripsi, parameter) dalam JSON Preface. 2. Mendeteksi Panggilan: Periksa model_message["tool_calls"] dalam respons. 3. Jalankan: Jalankan logika aplikasi Anda untuk alat yang diminta. 4. Respons: Kirim pesan dengan role: "tool" yang berisi output alat kembali ke model.

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