API C++ LiteRT-LM multiplate-forme

Conversation est une API de haut niveau qui représente une conversation unique et avec état avec le LLM. Il s'agit du point d'entrée recommandé pour la plupart des utilisateurs. Elle gère en interne une Session et traite des tâches complexes de traitement des données. Ces tâches incluent la gestion du contexte initial, la gestion des définitions d'outils, le prétraitement des données multimodales et l'application de modèles de prompt Jinja avec une mise en forme des messages basée sur les rôles.

Workflow de l'API Conversation

Voici le cycle de vie typique de l'utilisation de l'API Conversation :

  1. Créer un Engine : initialisez un seul Engine avec le chemin et la configuration du modèle. Il s'agit d'un objet lourd qui contient les poids du modèle.
  2. Créer un Conversation : utilisez Engine pour créer un ou plusieurs objets Conversation légers.
  3. Envoyer un message : utilisez les méthodes de l'objet Conversation pour envoyer des messages au LLM et recevoir des réponses, ce qui permet une interaction de type chat.

Vous trouverez ci-dessous la méthode la plus simple pour envoyer un message et obtenir une réponse du modèle. Elle est recommandée pour la plupart des cas d'utilisation. Elle reflète les API Gemini Chat.

  • SendMessage: appel bloquant qui prend l'entrée utilisateur et renvoie la réponse complète du modèle.
  • SendMessageAsync: appel non bloquant qui diffuse la réponse du modèle jeton par jeton via des rappels.

Voici un exemple d'extrait de code :

Contenu texte uniquement

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

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

Contenu de données multimodales

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

Utiliser Conversation avec des outils

Pour en savoir plus sur l'utilisation des outils avec l'API Conversation, consultez Utilisation avancée.

Composants de Conversation

Conversation peut être considéré comme un délégué permettant aux utilisateurs de gérer Session et le traitement complexe des données avant d'envoyer les données à Session.

Types d'E/S

Le format d'entrée et de sortie principal de l'API Conversation est Message. Actuellement, il est implémenté en tant que JsonMessage, qui est un alias de type pour ordered_json, une structure de données clé/valeur imbriquée flexible.

L'API Conversation fonctionne sur la base d'un message entrant et d'un message sortant, ce qui imite une expérience de chat typique. La flexibilité de Message permet aux utilisateurs d'inclure des champs arbitraires selon les besoins de modèles de prompt ou de modèles LLM spécifiques, ce qui permet à LiteRT-LM de prendre en charge une grande variété de modèles.

Bien qu'il n'existe pas de norme unique et rigide, la plupart des modèles de prompt et des modèles s'attendent à ce que Message suive des conventions semblables à celles utilisées dans le contenu de l'API Gemini ou la structure de message OpenAI.

Message doit contenir role, qui représente l'expéditeur du message. content peut être aussi simple qu'une chaîne de texte.

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

Pour l'entrée de données multimodales, content est une liste de part. Là encore, part n'est pas une structure de données prédéfinie, mais un type de données de paire clé/valeur ordonné. Les champs spécifiques dépendent de ce que le modèle de prompt et le modèle attendent.

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

Pour le multimodale part, nous acceptons le format suivant géré par 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",
}

Modèle d'invite.

Pour maintenir la flexibilité des modèles de variantes, PromptTemplate est implémenté en tant que wrapper fin autour de Minja. Minja est une implémentation C++ du moteur de modèle Jinja, qui traite l'entrée JSON pour générer des prompts mis en forme.

Le moteur de modèle Jinja est un format largement adopté pour les modèles de prompt LLM. Voici quelques exemples :

Le format du moteur de modèle Jinja doit correspondre exactement à la structure attendue par le modèle ajusté par instruction. En règle générale, les versions de modèle incluent le modèle Jinja standard pour garantir une utilisation appropriée du modèle.

Le modèle Jinja utilisé par le modèle sera fourni par les métadonnées du fichier de modèle.

Remarque : Une modification subtile du prompt en raison d'une mise en forme incorrecte peut entraîner une dégradation importante du modèle. Comme indiqué dans Quantifying Language Models' Sensitivity to Spurious Features in Prompt Design or: How I learned to start worrying about prompt formatting (Quantification de la sensibilité des modèles de langage aux caractéristiques parasites dans la conception des prompts ou : comment j'ai appris à m'inquiéter de la mise en forme des prompts).

Préface

Preface définit le contexte initial de la conversation. Il peut inclure des messages initiaux, des définitions d'outils et toute autre information de base dont le LLM a besoin pour démarrer l'interaction. Cela permet d'obtenir des fonctionnalités semblables à celles de the Gemini API system instruction and Gemini API Tools

La préface contient les champs suivants :

  • messages : messages de la préface. Les messages ont fourni le contexte initial de la conversation. Par exemple, les messages peuvent être l'historique des conversations, les instructions système d'ingénierie des prompts, des exemples de quelques prises de vue, etc.

  • tools : outils que le modèle peut utiliser dans la conversation. Le format des outils n'est pas fixe, mais suit principalement Gemini API FunctionDeclaration.

  • extra_context : contexte supplémentaire qui permet aux modèles de personnaliser les informations de contexte requises pour démarrer une conversation. Par exemple,

    • enable_thinking pour les modèles avec mode de réflexion, par exemple Qwen3 ou SmolLM3-3B.

Exemple de préface pour fournir des instructions système initiales, des outils et désactiver le mode de réflexion.

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

Historique

Conversation conserve une liste de tous les échanges de messages au sein de la session. Cet historique est essentiel pour le rendu du modèle de prompt, car le modèle de prompt Jinja nécessite généralement l'intégralité de l'historique des conversations pour générer le prompt correct pour le LLM.

Toutefois, la session LiteRT-LM est avec état, ce qui signifie qu'elle traite les entrées de manière incrémentielle. Pour combler cette lacune, Conversation génère le prompt incrémentiel nécessaire en effectuant le rendu du modèle de prompt deux fois : une fois avec l'historique jusqu'au tour précédent et une fois avec le message actuel. En comparant ces deux prompts rendus, il extrait la nouvelle partie à envoyer à la session.

ConversationConfig

ConversationConfig permet d'initialiser une instance Conversation. Vous pouvez créer cette configuration de plusieurs manières :

  1. À partir d'un Engine: cette méthode utilise le SessionConfig par défaut associé au moteur.
  2. À partir d'un SessionConfig: cela permet un contrôle plus précis des paramètres de session.

Au-delà des paramètres de session, vous pouvez personnaliser davantage le Conversation comportement dans le ConversationConfig. Par exemple :

Ces remplacements sont particulièrement utiles pour les modèles ajustés, qui peuvent nécessiter des configurations ou des modèles de prompt différents de ceux du modèle de base à partir duquel ils ont été dérivés.

MessageCallback

MessageCallback est la fonction de rappel que les utilisateurs doivent implémenter lorsqu'ils utilisent la méthode SendMessageAsync asynchrone.

La signature de rappel est absl::AnyInvocable<void(absl::StatusOr<Message>)>. Cette fonction est déclenchée dans les conditions suivantes :

  • Lorsqu'un nouveau bloc du Message est reçu du modèle.
  • Si une erreur se produit lors du traitement des messages de LiteRT-LM.
  • Une fois l'inférence du LLM terminée, le rappel est déclenché avec un vide Message (par exemple, JsonMessage()) pour signaler la fin de la réponse.

Pour obtenir un exemple d'implémentation, consultez l'appel asynchrone de l'étape 6.

Remarque : Le Message reçu par le rappel ne contient que le dernier bloc de la sortie du modèle, et non l’intégralité de l’historique des messages.

Par exemple, si la réponse complète du modèle attendue d'un appel bloquant SendMessage est la suivante :

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

Le rappel dans SendMessageAsync peut être appelé plusieurs fois, chaque fois avec un élément de texte suivant :

// 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!"
  ]
}

L'implémenteur est responsable de l'accumulation de ces blocs si la réponse complète est nécessaire pendant le flux asynchrone. Sinon, la réponse complète sera disponible en tant que dernière entrée dans le History une fois l'appel asynchrone terminé.

Utilisation avancée

Décodage contraint

LiteRT-LM est compatible avec le décodage contraint, ce qui vous permet d'appliquer des structures spécifiques à la sortie du modèle, telles que des schémas JSON, des modèles d'expressions régulières ou des règles de grammaire.

Pour l'activer, définissez EnableConstrainedDecoding(true) dans ConversationConfig et fournissez un ConstraintProviderConfig (par exemple, LlGuidanceConfig pour la prise en charge des expressions régulières/JSON/grammaire). Ensuite, transmettez les contraintes via OptionalArgs dans SendMessage.

Exemple : contrainte d'expression régulière

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

Pour en savoir plus, y compris sur la prise en charge du schéma JSON et de la grammaire Lark, consultez la documentation sur le décodage contraint.

Utilisation de l'outil

L'appel d'outil permet au LLM de demander l'exécution de fonctions côté client. Vous définissez des outils dans la Preface de la conversation, en les associant par nom. Lorsque le modèle génère un appel d'outil, vous le capturez, exécutez la fonction correspondante dans votre application et renvoyez le résultat au modèle.

Flux de haut niveau :

  1. Déclarer des outils : définissez des outils (nom, description, paramètres) dans le JSON Preface.
  2. Détecter les appels : vérifiez model_message["tool_calls"] dans la réponse.
  3. Exécuter : exécutez la logique de votre application pour l'outil demandé.
  4. Répondre : renvoyez un message avec role: "tool" contenant la sortie de l'outil au modèle.

Pour en savoir plus et obtenir un exemple complet de boucle de chat, consultez la documentation sur l'utilisation des outils.