LiteRT-LM Cross-Platform C++ API

Conversation هي واجهة برمجة تطبيقات عالية المستوى تمثّل محادثة واحدة ذات حالة مع نموذج اللغة الكبير، وهي نقطة الدخول المقترَحة لمعظم المستخدمين. وتدير هذه الطبقة داخليًا Session وتتولّى مهام معالجة البيانات المعقّدة. تشمل هذه المهام الحفاظ على السياق الأولي وإدارة تعريفات الأدوات والمعالجة المسبقة للبيانات المتعدّدة الوسائط وتطبيق نماذج طلبات Jinja مع تنسيق الرسائل المستند إلى الأدوار.

سير عمل Conversation API

في ما يلي دورة الحياة النموذجية لاستخدام Conversation API:

  1. إنشاء Engine: ابدأ بإنشاء Engine واحد باستخدام مسار النموذج والإعدادات. هذا عنصر كبير الحجم يحتوي على أوزان النموذج.
  2. إنشاء Conversation: استخدِم Engine لإنشاء عنصر واحد أو أكثر من عناصر Conversation الخفيفة.
  3. إرسال رسالة: استخدِم طرق عنصر Conversation لإرسال رسائل إلى النموذج اللغوي الكبير وتلقّي الردود، ما يتيح التفاعل بشكل يشبه المحادثة.

في ما يلي أبسط طريقة لإرسال رسالة وتلقّي ردّ من النموذج. ويُنصح باستخدامها في معظم حالات الاستخدام. وهي تتطابق مع واجهات برمجة التطبيقات في Gemini Chat.

  • 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 ومعالجة البيانات المعقّدة قبل إرسال البيانات إلى "الجلسة".

أنواع الإدخال/الإخراج

إنّ تنسيق الإدخال والإخراج الأساسي لواجهة Conversation API هو Message. في الوقت الحالي، يتم تنفيذ ذلك على النحو التالي: JsonMessage، وهو اسم مستعار للنوع ordered_json، وهو بنية بيانات مرنة ومتداخلة للمفتاح/القيمة.

تعمل واجهة برمجة التطبيقات Conversation على أساس رسالة واردة ورسالة صادرة، ما يحاكي تجربة محادثة نموذجية. تتيح مرونة Message للمستخدمين تضمين حقول عشوائية حسب الحاجة في نماذج الطلبات أو نماذج LLM المحدّدة، ما يتيح لـ LiteRT-LM إمكانية دعم مجموعة متنوعة من النماذج.

على الرغم من عدم توفّر معيار واحد صارم، تتوقّع معظم نماذج وقوالب الطلبات أن يلتزم Message بالاتفاقيات المشابهة لتلك المستخدَمة في محتوى Gemini API أو بنية رسائل OpenAI.

يجب أن يحتوي 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 الذي يستخدمه النموذج من خلال البيانات الوصفية لملف النموذج.

ملاحظة: يمكن أن يؤدي تغيير طفيف في الطلب بسبب التنسيق غير الصحيح إلى تدهور كبير في أداء النموذج. كما ورد في 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.

يحتوي المقدمة على الحقول التالية

  • 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 يتطلّب عادةً سجلّ المحادثة بأكمله لإنشاء الطلب الصحيح للنموذج اللغوي الكبير.

ومع ذلك، فإنّ الجلسة في LiteRT-LM هي جلسة ذات حالة، ما يعني أنّها تعالج المدخلات بشكل تراكمي. لسدّ هذه الفجوة، تنشئ ميزة المحادثة الإشعار التزايدي اللازم من خلال عرض نموذج الإشعار مرتين: مرة مع السجلّ حتى الجولة السابقة، ومرة أخرى مع تضمين الرسالة الحالية. من خلال مقارنة هذين الطلبين المعروضين، يستخرج الجزء الجديد ليتم إرساله إلى الجلسة.

ConversationConfig

يُستخدَم ConversationConfig لتهيئة مثيل Conversation. يمكنك إنشاء هذا الإعداد بطريقتَين:

  1. من Engine: تستخدم هذه الطريقة SessionConfig التلقائي المرتبط بمحرّك البحث.
  2. من SessionConfig معيّن: يتيح ذلك تحكّمًا أكثر دقة في إعدادات الجلسة.

بالإضافة إلى إعدادات الجلسة، يمكنك تخصيص سلوك Conversation بشكل أكبر ضمن ConversationConfig. يشمل ذلك ما يلي:

تكون عمليات الكتابة هذه مفيدة بشكل خاص للنماذج المضبوطة بدقة، والتي قد تتطلب إعدادات أو نماذج طلبات مختلفة عن النموذج الأساسي الذي تم اشتقاقها منه.

MessageCallback

MessageCallback هي دالة رد الاتصال التي يجب أن ينفّذها المستخدمون عند استخدام طريقة SendMessageAsync غير المتزامنة.

توقيع معاودة الاتصال هو absl::AnyInvocable<void(absl::StatusOr<Message>)>. يتم تشغيل هذه الدالة في الحالات التالية:

  • عند تلقّي جزء جديد من Message من النموذج
  • في حال حدوث خطأ أثناء معالجة الرسالة في LiteRT-LM
  • عند اكتمال استنتاج النموذج اللغوي الكبير، يتم تشغيل وظيفة الرجوع مع Message فارغ (مثل JsonMessage()) للإشارة إلى نهاية الرد.

يمكنك الرجوع إلى الخطوة 6: طلب غير متزامن للاطّلاع على مثال للتنفيذ.

ملاحظة: لا تتضمّن البيانات 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 بعد اكتمال المكالمة غير المتزامنة.

الاستخدام المتقدّم {#advanced-usage}

فك الترميز المقيّد

تتيح LiteRT-LM فك الترميز المقيد، ما يسمح لك بفرض بنى محددة على ناتج النموذج، مثل مخططات JSON أو أنماط Regex أو قواعد نحوية.

لتفعيلها، اضبط EnableConstrainedDecoding(true) في ConversationConfig وقدِّم ConstraintProviderConfig (مثل LlGuidanceConfig لتوفير دعم التعبيرات العادية/JSON/النحو). بعد ذلك، مرِّر القيود من خلال OptionalArgs في SendMessage.

مثال: قيود التعبير العادي

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.

استخدام الأدوات

تتيح ميزة "استدعاء الأدوات" للنموذج اللغوي الكبير طلب تنفيذ وظائف من جهة العميل. يمكنك تحديد الأدوات في Preface ضمن المحادثة، مع إدخالها حسب الاسم. عندما يخرج النموذج استدعاء أداة، يمكنك تسجيله وتنفيذ الدالة المقابلة في تطبيقك وإرجاع النتيجة إلى النموذج.

الخطوات العامة:

  1. تعريف الأدوات: حدِّد الأدوات (الاسم والوصف والمعلَمات) في ملف Preface JSON.
  2. رصد المكالمات: ابحث عن model_message["tool_calls"] في الردّ.
  3. التنفيذ: شغِّل منطق تطبيقك للأداة المطلوبة.
  4. الردّ: لإرسال رسالة تتضمّن role: "tool" تحتوي على ناتج الأداة إلى النموذج.

للاطّلاع على التفاصيل الكاملة ومثال كامل على حلقة الدردشة، يُرجى الرجوع إلى مستندات استخدام الأدوات.