LiteRT-LM Cross-Platform C++ API

Conversation एक हाई-लेवल एपीआई है. यह एलएलएम के साथ एक ही बातचीत को दिखाता है. साथ ही, यह ज़्यादातर उपयोगकर्ताओं के लिए सुझाया गया एंट्री पॉइंट है. यह अंदरूनी तौर पर Session को मैनेज करता है. साथ ही, डेटा को प्रोसेस करने से जुड़े मुश्किल टास्क को हैंडल करता है. इन टास्क में, शुरुआती कॉन्टेक्स्ट को बनाए रखना, टूल की परिभाषाओं को मैनेज करना, मल्टीमॉडल डेटा को प्रीप्रोसेस करना, और भूमिका के आधार पर मैसेज फ़ॉर्मैटिंग के साथ Jinja प्रॉम्प्ट टेंप्लेट लागू करना शामिल है.

Conversation API वर्कफ़्लो

Conversation API का इस्तेमाल करने का सामान्य लाइफ़साइकल यह है:

  1. Engine बनाएं: मॉडल पाथ और कॉन्फ़िगरेशन के साथ एक Engine शुरू करें. यह एक हैवीवेट ऑब्जेक्ट है, जिसमें मॉडल के वज़न की जानकारी होती है.
  2. Conversation बनाना: एक या उससे ज़्यादा हल्के-फुल्के Conversation ऑब्जेक्ट बनाने के लिए, Engine का इस्तेमाल करें.
  3. मैसेज भेजें: एलएलएम को मैसेज भेजने और जवाब पाने के लिए, Conversation ऑब्जेक्ट के तरीकों का इस्तेमाल करें. इससे चैट जैसा इंटरैक्शन किया जा सकेगा.

यहां मैसेज भेजने और मॉडल से जवाब पाने का सबसे आसान तरीका बताया गया है. ज़्यादातर मामलों में इसका इस्तेमाल करने का सुझाव दिया जाता है. यह 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 और जटिल डेटा प्रोसेसिंग को बनाए रखने में मदद मिलती है.

I/O टाइप

Conversation API के लिए, मुख्य इनपुट और आउटपुट फ़ॉर्मैट Message है. फ़िलहाल, इसे JsonMessage के तौर पर लागू किया गया है. यह ordered_json के लिए टाइप एलियास है. यह नेस्ट की गई की-वैल्यू वाली एक फ़्लेक्सिबल डेटा स्ट्रक्चर है.

Conversation API, मैसेज-इन-मैसेज-आउट के आधार पर काम करता है. यह चैट के सामान्य अनुभव की तरह काम करता है. Message की सुविधा की वजह से, उपयोगकर्ता अपनी ज़रूरत के हिसाब से फ़ील्ड शामिल कर सकते हैं. ऐसा खास प्रॉम्प्ट टेंप्लेट या एलएलएम मॉडल के लिए किया जा सकता है. इससे LiteRT-LM, कई तरह के मॉडल के साथ काम कर पाता है.

हालांकि, कोई एक तय स्टैंडर्ड नहीं है, लेकिन ज़्यादातर प्रॉम्प्ट टेंप्लेट और मॉडल, Message को Gemini API Content या OpenAI Message structure में इस्तेमाल किए गए स्टैंडर्ड के मुताबिक फ़ॉर्मैट करने की उम्मीद करते हैं.

Message में role शामिल होना चाहिए. इससे पता चलता है कि मैसेज किसने भेजा है. content एक टेक्स्ट स्ट्रिंग जितनी आसान हो सकती है.

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

मल्टीमोडल डेटा इनपुट के लिए, content, part की एक सूची है. part पहले से तय किया गया डेटा स्ट्रक्चर नहीं है. हालांकि, यह क्रम से लगाया गया कुंजी-वैल्यू पेयर डेटा टाइप है. फ़ील्ड, इस बात पर निर्भर करते हैं कि प्रॉम्प्ट टेंप्लेट और मॉडल को किस तरह के डेटा की ज़रूरत है.

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

प्रॉम्प्ट टेंप्लेट

वैरिएंट मॉडल के लिए फ़्लेक्सिबिलिटी बनाए रखने के लिए, PromptTemplate को Minja के चारों ओर एक थिन रैपर के तौर पर लागू किया जाता है. Minja, Jinja टेंप्लेट इंजन का C++ वर्शन है. यह फ़ॉर्मैट किए गए प्रॉम्प्ट जनरेट करने के लिए, JSON इनपुट को प्रोसेस करता है.

Jinja टेंप्लेट इंजन, एलएलएम प्रॉम्प्ट टेंप्लेट के लिए सबसे ज़्यादा इस्तेमाल किया जाने वाला फ़ॉर्मैट है. यहां कुछ उदाहरण दिए गए हैं:

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 से बातचीत का शुरुआती कॉन्टेक्स्ट सेट होता है. इसमें शुरुआती मैसेज, टूल की परिभाषाएं, और कोई भी ऐसी बैकग्राउंड जानकारी शामिल हो सकती है जिसकी ज़रूरत एलएलएम को इंटरैक्शन शुरू करने के लिए होती है. इससे Gemini API system instruction और Gemini API Tools जैसी सुविधाएं मिलती हैं

Preface में ये फ़ील्ड शामिल होते हैं

  • messages पेज नंबर 12 पर मौजूद मैसेज. मैसेज में बातचीत की शुरुआती जानकारी दी गई थी. उदाहरण के लिए, मैसेज में बातचीत का इतिहास, प्रॉम्प्ट इंजीनियरिंग सिस्टम के निर्देश, कुछ उदाहरण वगैरह शामिल हो सकते हैं.

  • tools बातचीत के दौरान मॉडल जिन टूल का इस्तेमाल कर सकता है. टूल का फ़ॉर्मैट फिर से तय नहीं किया गया है, लेकिन ज़्यादातर Gemini API FunctionDeclaration के मुताबिक होता है.

  • extra_context अतिरिक्त कॉन्टेक्स्ट, जो मॉडल को बातचीत शुरू करने के लिए ज़रूरी कॉन्टेक्स्ट की जानकारी को पसंद के मुताबिक बनाने की सुविधा देता है. उदाहरण के लिए,

    • enable_thinking सूझ-बूझ वाले मॉडल के लिए, जैसे कि Qwen3 या SmolLM3-3B.

सिस्टम को शुरुआती निर्देश देने, टूल उपलब्ध कराने, और सोचने के मोड को बंद करने के लिए, प्रस्तावना का उदाहरण.

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

इतिहास

बातचीत, सेशन के दौरान हुए सभी मैसेज के आदान-प्रदान की सूची बनाए रखता है. यह इतिहास, प्रॉम्प्ट टेंप्लेट रेंडरिंग के लिए ज़रूरी है. ऐसा इसलिए, क्योंकि जिंजा प्रॉम्प्ट टेंप्लेट को आम तौर पर एलएलएम के लिए सही प्रॉम्प्ट जनरेट करने के लिए, बातचीत के पूरे इतिहास की ज़रूरत होती है.

हालांकि, LiteRT-LM का सेशन स्टेटफ़ुल होता है. इसका मतलब है कि यह इनपुट को धीरे-धीरे प्रोसेस करता है. इस अंतर को कम करने के लिए, बातचीत में ज़रूरी इंक्रीमेंटल प्रॉम्प्ट जनरेट होता है. इसके लिए, प्रॉम्प्ट टेंप्लेट को दो बार रेंडर किया जाता है: पहली बार पिछले टर्न तक के इतिहास के साथ और दूसरी बार मौजूदा मैसेज के साथ. रेंडर किए गए इन दोनों प्रॉम्प्ट की तुलना करके, यह सेशन में भेजे जाने वाले नए हिस्से को निकालता है.

ConversationConfig

ConversationConfig का इस्तेमाल, Conversation इंस्टेंस को शुरू करने के लिए किया जाता है. इस कॉन्फ़िगरेशन को कुछ तरीकों से बनाया जा सकता है:

  1. Engine से: इस तरीके में, इंजन से जुड़े डिफ़ॉल्ट SessionConfig का इस्तेमाल किया जाता है.
  2. किसी खास SessionConfig से: इससे सेशन की सेटिंग पर ज़्यादा कंट्रोल मिलता है.

सेशन की सेटिंग के अलावा, ConversationConfig में जाकर Conversation के व्यवहार को अपनी पसंद के मुताबिक बनाया जा सकता है. इसमें इस तरह का कॉन्टेंट शामिल है:

  • Preface उपलब्ध कराना.
  • डिफ़ॉल्ट PromptTemplate को ओवरराइट करना.
  • डिफ़ॉल्ट DataProcessorConfig को ओवरराइट करना.

ये ओवरराइट, फ़ाइन-ट्यून किए गए मॉडल के लिए खास तौर पर फ़ायदेमंद होते हैं. इन्हें उस बेस मॉडल के मुकाबले अलग कॉन्फ़िगरेशन या प्रॉम्प्ट टेंप्लेट की ज़रूरत हो सकती है जिससे इन्हें बनाया गया था.

MessageCallback

MessageCallback एक कॉलबैक फ़ंक्शन है. उपयोगकर्ताओं को इसे तब लागू करना चाहिए, जब वे असिंक्रोनस SendMessageAsync तरीके का इस्तेमाल करते हैं.

कॉलबैक का सिग्नेचर absl::AnyInvocable<void(absl::StatusOr<Message>)> है. यह फ़ंक्शन इन स्थितियों में ट्रिगर होता है:

  • जब मॉडल से Message का कोई नया हिस्सा मिलता है.
  • अगर LiteRT-LM के मैसेज प्रोसेस करने के दौरान कोई गड़बड़ी होती है.
  • एलएलएम के अनुमान लगाने की प्रोसेस पूरी होने पर, कॉलबैक को ट्रिगर किया जाता है.इसमें एक खाली Message होता है. उदाहरण के लिए, JsonMessage()) का इस्तेमाल किया जाता है.

उदाहरण के लिए, छठा चरण: एसिंक्रोनस कॉल देखें.

[!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 स्कीमा, रेगुलर एक्सप्रेशन पैटर्न या व्याकरण के नियम.

इसे चालू करने के लिए, ConversationConfig में EnableConstrainedDecoding(true) सेट करें और ConstraintProviderConfig दें. उदाहरण के लिए, LlGuidanceConfig के लिए). इसके बाद, SendMessage में OptionalArgs के ज़रिए शर्तें पास करें.

उदाहरण: रेगुलर एक्सप्रेशन की शर्त

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 Grammar के साथ काम करने की सुविधा के बारे में पूरी जानकारी के लिए, Constrained Decoding से जुड़ा दस्तावेज़ देखें.

टूल का इस्तेमाल

टूल कॉलिंग की सुविधा की मदद से, एलएलएम क्लाइंट-साइड फ़ंक्शन को लागू करने का अनुरोध कर सकता है. बातचीत के Preface में टूल तय किए जाते हैं. इन्हें नाम के हिसाब से तय किया जाता है. जब मॉडल, टूल कॉल का आउटपुट देता है, तब उसे कैप्चर किया जाता है. इसके बाद, अपने ऐप्लिकेशन में उससे जुड़ा फ़ंक्शन लागू किया जाता है और मॉडल को नतीजा वापस भेजा जाता है.

हाई-लेवल फ़्लो: 1. टूल के बारे में जानकारी देना: Preface JSON में टूल (नाम, ब्यौरा, पैरामीटर) के बारे में जानकारी दें. 2. कॉल का पता लगाना: जवाब में model_message["tool_calls"] देखें. 3. Execute: अनुरोध किए गए टूल के लिए, ऐप्लिकेशन लॉजिक चलाएं. 4. जवाब दें: टूल के आउटपुट के साथ role: "tool" वाला मैसेज, मॉडल को वापस भेजें.

पूरी जानकारी और चैट लूप के उदाहरण के लिए, टूल इस्तेमाल करने से जुड़ा दस्तावेज़ देखें.