LiteRT-LM Cross-Platform C++ API

Conversation یک API سطح بالا است که یک مکالمه واحد و دارای وضعیت با LLM را نشان می‌دهد و نقطه ورود پیشنهادی برای اکثر کاربران است. این API به صورت داخلی یک Session مدیریت می‌کند و وظایف پیچیده پردازش داده‌ها را انجام می‌دهد. این وظایف شامل حفظ زمینه اولیه، مدیریت تعاریف ابزار، پیش‌پردازش داده‌های چندوجهی و اعمال قالب‌های اعلان Jinja با قالب‌بندی پیام مبتنی بر نقش است.

گردش کار API مکالمه

چرخه حیات معمول برای استفاده از API مکالمه به صورت زیر است:

  1. ایجاد یک Engine : یک Engine واحد را با مسیر و پیکربندی مدل مقداردهی اولیه کنید. این یک شیء سنگین است که وزن‌های مدل را در خود نگه می‌دارد.
  2. ایجاد Conversation : از Engine برای ایجاد یک یا چند شیء Conversation سبک استفاده کنید.
  3. ارسال پیام : از متدهای شیء Conversation برای ارسال پیام به LLM و دریافت پاسخ‌ها استفاده کنید، که عملاً تعاملی شبیه چت را امکان‌پذیر می‌سازد.

در زیر ساده‌ترین روش برای ارسال پیام و دریافت پاسخ مدل آمده است. این روش برای اکثر موارد استفاده توصیه می‌شود. این روش از APIهای چت Gemini تقلید می‌کند.

  • SendMessage : یک فراخوانی مسدودکننده که ورودی کاربر را دریافت کرده و پاسخ کامل مدل را برمی‌گرداند.
  • SendMessageAsync : یک فراخوانی غیر مسدودکننده که پاسخ مدل را توکن به توکن از طریق فراخوانی‌های برگشتی (callbacks) ارسال می‌کند.

در اینجا قطعه کد نمونه آمده است:

محتوای فقط متنی

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

استفاده از مکالمه با ابزارها

برای جزئیات بیشتر در مورد نحوه‌ی استفاده از ابزار با API مکالمه، لطفاً به بخش «کاربرد پیشرفته» مراجعه کنید.

اجزا در مکالمه

Conversation می‌توان به عنوان نماینده‌ای برای کاربران در نظر گرفت تا Session و پردازش داده‌های پیچیده را قبل از ارسال داده‌ها به Session حفظ کنند.

انواع ورودی/خروجی

قالب ورودی و خروجی اصلی برای API مکالمه، Message است. در حال حاضر، این قالب به صورت JsonMessage پیاده‌سازی شده است که یک نام مستعار برای ordered_json ، یک ساختار داده کلید-مقدار تو در تو و انعطاف‌پذیر، است.

رابط برنامه‌نویسی کاربردی Conversation API) بر اساس ارسال پیام (message-in-message-out) عمل می‌کند و یک تجربه چت معمولی را شبیه‌سازی می‌کند. انعطاف‌پذیری Message به کاربران این امکان را می‌دهد که فیلدهای دلخواه را در صورت نیاز توسط قالب‌های اعلان خاص یا مدل‌های LLM وارد کنند و LiteRT-LM را قادر می‌سازد تا از طیف گسترده‌ای از مدل‌ها پشتیبانی کند.

اگرچه هیچ استاندارد واحد و دقیقی وجود ندارد، اما اکثر الگوها و مدل‌های اعلان انتظار دارند که Message از قراردادهایی مشابه قراردادهای مورد استفاده در Gemini API Content یا ساختار OpenAI Message پیروی کند.

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 یک پیاده‌سازی C++ از موتور قالب Jinja است که ورودی JSON را برای تولید اعلان‌های قالب‌بندی شده پردازش می‌کند.

موتور قالب Jinja یک قالب پرکاربرد برای قالب‌های آماده LLM است. در اینجا چند نمونه آورده شده است:

قالب موتور قالب Jinja باید کاملاً با ساختار مورد انتظار مدل تنظیم‌شده توسط دستورالعمل مطابقت داشته باشد. معمولاً نسخه‌های مدل شامل قالب استاندارد Jinja هستند تا از استفاده صحیح از مدل اطمینان حاصل شود.

قالب Jinja که توسط مدل استفاده می‌شود، توسط فراداده‌های فایل مدل ارائه می‌شود.

[!نکته] یک تغییر نامحسوس در اعلان به دلیل قالب‌بندی نادرست می‌تواند منجر به تخریب قابل توجه مدل شود. همانطور که در «کمی‌سازی حساسیت مدل‌های زبانی به ویژگی‌های جعلی در طراحی اعلان» یا «چگونه یاد گرفتم که نگران قالب‌بندی اعلان باشم» گزارش شده است.

پیشگفتار

Preface زمینه اولیه مکالمه را تعیین می‌کند. این می‌تواند شامل پیام‌های اولیه، تعاریف ابزار و هرگونه اطلاعات پیش‌زمینه دیگری باشد که LLM برای شروع تعامل به آن نیاز دارد. این امر عملکردی مشابه Gemini API system instruction و Gemini API Tools را ارائه می‌دهد.

مقدمه شامل فیلدهای زیر است

  • messages پیام‌های موجود در مقدمه. پیام‌ها زمینه اولیه مکالمه را فراهم می‌کردند. برای مثال، پیام‌ها می‌توانند شامل تاریخچه مکالمه، دستورالعمل‌های سیستم مهندسی سریع، مثال‌های کوتاه و غیره باشند.

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

تاریخچه

مکالمه فهرستی از تمام پیام‌های رد و بدل شده در طول جلسه را نگهداری می‌کند. این تاریخچه برای رندر کردن قالب اعلان بسیار مهم است، زیرا قالب اعلان jinja معمولاً برای تولید اعلان صحیح برای LLM به کل تاریخچه مکالمه نیاز دارد.

با این حال، جلسه LiteRT-LM حالت‌مند است، به این معنی که ورودی‌ها را به صورت افزایشی پردازش می‌کند. برای پر کردن این شکاف، Conversation با دو بار رندر کردن الگوی اعلان، اعلان افزایشی لازم را تولید می‌کند: یک بار با تاریخچه تا نوبت قبلی و یک بار شامل پیام فعلی. با مقایسه این دو اعلان رندر شده، بخش جدیدی را که باید به Session ارسال شود، استخراج می‌کند.

پیکربندی مکالمه

ConversationConfig برای مقداردهی اولیه یک نمونه Conversation استفاده می‌شود. می‌توانید این پیکربندی را به چند روش ایجاد کنید:

  1. از یک Engine : این روش از SessionConfig پیش‌فرض مرتبط با موتور استفاده می‌کند.
  2. از یک SessionConfig خاص: این امکان کنترل دقیق‌تر بر تنظیمات جلسه را فراهم می‌کند.

فراتر از تنظیمات جلسه، می‌توانید رفتار Conversation را در ConversationConfig بیشتر سفارشی کنید. این شامل موارد زیر است:

این بازنویسی‌ها به ویژه برای مدل‌های تنظیم‌شده دقیق مفید هستند، که ممکن است به پیکربندی‌ها یا قالب‌های اعلان متفاوتی نسبت به مدل پایه‌ای که از آن مشتق شده‌اند، نیاز داشته باشند.

پیامتماس برگشتی

MessageCallback تابع فراخوانی است که کاربران باید هنگام استفاده از متد SendMessageAsync ناهمزمان پیاده‌سازی کنند.

امضای تابع فراخوانی absl::AnyInvocable<void(absl::StatusOr<Message>)> است. این تابع تحت شرایط زیر فعال می‌شود:

  • وقتی بخش جدیدی از Message از مدل دریافت می‌شود.
  • اگر در طول پردازش پیام LiteRT-LM خطایی رخ دهد.
  • پس از اتمام استنتاج LLM، تابع فراخوانی با یک Message خالی (مثلاً JsonMessage() ) آغاز می‌شود تا پایان پاسخ را نشان دهد.

برای یک نمونه پیاده‌سازی، به فراخوانی ناهمزمان مرحله 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، الگوهای Regex یا قوانین دستور زبان را بر روی خروجی مدل اعمال کنید.

برای فعال کردن آن، EnableConstrainedDecoding(true) را در ConversationConfig تنظیم کنید و یک ConstraintProviderConfig (مثلاً LlGuidanceConfig برای پشتیبانی از regex/JSON/grammar) ارائه دهید. سپس، محدودیت‌ها را از طریق OptionalArgs در SendMessage ارسال کنید.

مثال: محدودیت 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 Schema و Lark Grammar، به مستندات Constrained Decoding مراجعه کنید.

استفاده از ابزار

فراخوانی ابزار به LLM اجازه می‌دهد تا اجرای توابع سمت کلاینت را درخواست کند. شما ابزارها را در Preface مکالمه تعریف می‌کنید و آنها را با نام مشخص می‌کنید. وقتی مدل یک فراخوانی ابزار را خروجی می‌دهد، شما آن را ضبط می‌کنید، تابع مربوطه را در برنامه خود اجرا می‌کنید و نتیجه را به مدل برمی‌گردانید.

جریان سطح بالا: ۱. تعریف ابزارها: تعریف ابزارها (نام، توضیحات، پارامترها) در JSON Preface . ۲. تشخیص فراخوانی‌ها: بررسی model_message["tool_calls"] در پاسخ. ۳. اجرا: اجرای منطق برنامه برای ابزار درخواستی. ۴. پاسخ: ارسال پیامی با role: "tool" حاوی خروجی ابزار به مدل.

برای جزئیات کامل و یک مثال کامل از حلقه چت، به مستندات Tool Use مراجعه کنید.