Кроссплатформенный C++ API LiteRT-LM

Conversation — это высокоуровневый API, представляющий собой единый диалог с LLM, сохраняющий состояние, и является рекомендуемой точкой входа для большинства пользователей. Он внутренне управляет Session и обрабатывает сложные задачи обработки данных. Эти задачи включают в себя поддержание исходного контекста, управление определениями инструментов, предварительную обработку мультимодальных данных и применение шаблонов подсказок Jinja с форматированием сообщений на основе ролей.

Рабочий процесс API диалога

Типичный жизненный цикл использования API для диалогов выглядит следующим образом:

  1. Создание Engine : инициализируйте один Engine указав путь к модели и ее конфигурацию. Это ресурсоемкий объект, содержащий веса модели.
  2. Создание Conversation : Используйте Engine для создания одного или нескольких легковесных объектов Conversation .
  3. Отправка сообщения : Используйте методы объекта Conversation для отправки сообщений в LLM и получения ответов, что позволяет эффективно организовать взаимодействие, подобное чату.

Ниже представлен самый простой способ отправить сообщение и получить ответ от модели. Он рекомендуется для большинства случаев использования. Он аналогичен API чата Gemini .

  • 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 , гибкой вложенной структуры данных типа «ключ-значение».

API Conversation работает по принципу "сообщение входящее - сообщение исходящее", имитируя типичный чат. Гибкость Message позволяет пользователям включать произвольные поля по мере необходимости в соответствии с конкретными шаблонами запросов или моделями LLM, что позволяет LiteRT-LM поддерживать широкий спектр моделей.

Хотя единого жесткого стандарта не существует, большинство шаблонов и моделей подсказок ожидают, Message будет следовать соглашениям, аналогичным тем, которые используются в Gemini API Content или структуре Message 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 — это широко распространенный формат для шаблонов заданий к магистерским диссертациям. Вот несколько примеров:

Формат шаблонизатора Jinja должен строго соответствовать структуре, ожидаемой от модели, оптимизированной для конкретных инструкций. Как правило, в релизы моделей включается стандартный шаблон Jinja для обеспечения корректного использования модели.

Шаблон Jinja, используемый моделью, будет предоставлен метаданными файла модели.

[!NOTE] Незначительное изменение в подсказке из-за неправильного форматирования может привести к существенному ухудшению качества модели. Как сообщается в статье «Количественная оценка чувствительности языковых моделей к ложным признакам в дизайне подсказок, или: Как я научился беспокоиться о форматировании подсказок».

Предисловие

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 генерирует необходимую инкрементальную подсказку, дважды отображая шаблон подсказки: один раз с историей до предыдущего хода, и один раз включая текущее сообщение. Сравнивая эти две отрендеренные подсказки, он извлекает новую часть, которая будет отправлена ​​в сессию .

ConversationConfig

ConversationConfig используется для инициализации экземпляра Conversation . Создать эту конфигурацию можно несколькими способами:

  1. Из Engine : Этот метод использует SessionConfig по умолчанию, связанную с движком.
  2. В рамках конкретного параметра SessionConfig : это позволяет более точно контролировать настройки сессии.

Помимо настроек сессии, вы можете дополнительно настроить поведение Conversation в файле ConversationConfig . Это включает в себя:

Эти перезаписи особенно полезны для точно настроенных моделей, которые могут требовать иных конфигураций или шаблонов подсказок, чем базовая модель, на основе которой они были созданы.

MessageCallback

MessageCallback — это функция обратного вызова, которую пользователи должны реализовать при использовании асинхронного метода SendMessageAsync .

Сигнатура функции обратного вызова absl::AnyInvocable<void(absl::StatusOr<Message>)> . Эта функция запускается при следующих условиях:

  • Когда от модели поступает новый фрагмент Message .
  • Если во время обработки сообщений LiteRT-LM возникает ошибка.
  • После завершения вывода LLM, вызывается функция обратного вызова с пустым 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 после завершения асинхронного вызова.

Расширенные возможности использования

Декодирование с ограничениями

LiteRT-LM поддерживает декодирование с ограничениями, позволяя применять к выходным данным модели определенные структуры, такие как JSON-схемы, шаблоны регулярных выражений или правила грамматики.

Чтобы включить эту функцию, установите 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, см. в документации по декодированию с ограничениями .

Использование инструментов

Вызов инструментов позволяет LLM запрашивать выполнение функций на стороне клиента. Вы определяете инструменты в Preface » диалога, указывая их по имени. Когда модель выдает вызов инструмента, вы перехватываете его, выполняете соответствующую функцию в своем приложении и возвращаете результат модели.

Общая схема работы: 1. Объявление инструментов: Определите инструменты (имя, описание, параметры) в JSON- Preface . 2. Обнаружение вызовов: Проверьте model_message["tool_calls"] в ответе. 3. Выполнение: Запустите логику вашего приложения для запрошенного инструмента. 4. Ответ: Отправьте сообщение с role: "tool" , содержащее выходные данные инструмента, обратно в модель.

Подробную информацию и полный пример цикла чата см. в документации по использованию инструмента .