Conversation est une API de haut niveau qui représente une conversation unique avec état avec le LLM. Il s'agit du point d'entrée recommandé pour la plupart des utilisateurs. Il gère en interne un Session et traite les 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 requête Jinja avec 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 :
- Créez un
Engine: initialisez un seulEngineavec le chemin d'accès et la configuration du modèle. Il s'agit d'un objet lourd qui contient les poids du modèle. - Créer un
Conversation: utilisezEnginepour créer un ou plusieurs objetsConversationlégers. - Envoyer un message : utilisez les méthodes de l'objet
Conversationpour 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. Il est recommandé pour la plupart des cas d'utilisation. Elle est identique aux API Gemini Chat.
SendMessage: appel bloquant qui prend en entrée la saisie de l'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 des exemples d'extraits de code :
Contenu textuel 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
CreatePrintMessageCallbackabsl::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 la conversation avec des outils
Pour en savoir plus sur l'utilisation avancée des outils avec l'API Conversation, consultez Utilisation avancée.
Composants dans les conversations
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 de les envoyer à 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 requêtes 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 et des modèles de requêtes s'attendent à ce que Message suive des conventions semblables à celles utilisées dans le contenu de l'API Gemini ou la structure des messages OpenAI.
Message doit contenir role, qui indique 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 les données d'entrée multimodales, content est une liste de part. Encore une fois, part n'est pas une structure de données prédéfinie, mais un type de données de paire clé/valeur ordonnée. Les champs spécifiques dépendent de ce que le modèle et le modèle d'invite 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 les part multimodaux, 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èles Jinja, qui traite les entrées JSON pour générer des requêtes mises en forme.
Le moteur de modèles Jinja est un format largement adopté pour les modèles de requêtes LLM. Voici quelques exemples :
Le format du moteur de modèles Jinja doit correspondre strictement à la structure attendue par le modèle affiné pour les instructions. 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.
[!NOTE] Une légère modification du format d'un prompt 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
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 contexte dont le LLM a besoin pour démarrer l'interaction. Cela permet d'obtenir des fonctionnalités semblables à celles de Gemini API system instruction et Gemini API Tools.
Preface contient les champs suivants :
messagesMessages dans la préface. Les messages ont fourni le contexte initial de la conversation. Par exemple, les messages peuvent correspondre à l'historique des conversations, aux instructions du système d'ingénierie des requêtes, aux exemples few-shot, etc.toolsOutils que le modèle peut utiliser dans la conversation. Le format des outils n'est pas fixe, mais suit généralementGemini API FunctionDeclaration.extra_contextContexte supplémentaire qui permet aux modèles de personnaliser les informations de contexte requises pour démarrer une conversation. Par exemple,enable_thinkingpour les modèles avec mode 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 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 la liste de tous les échanges de Message au cours de la session. Cet historique est essentiel pour le rendu des modèles de requête, car le modèle de requête Jinja nécessite généralement l'intégralité de l'historique des conversations pour générer la requête appropriée 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 affichant le modèle de prompt deux fois : une fois avec l'historique jusqu'au tour précédent, et une fois en incluant le message actuel. En comparant ces deux requêtes rendues, il extrait la nouvelle partie à envoyer à la session.
ConversationConfig
ConversationConfig est utilisé pour initialiser une instance Conversation. Vous pouvez créer cette configuration de plusieurs façons :
- À partir d'un
Engine: cette méthode utilise leSessionConfigpar défaut associé au moteur. - À partir d'une
SessionConfigspécifique : cela permet de contrôler plus précisément les paramètres de session.
Au-delà des paramètres de session, vous pouvez personnaliser davantage le comportement de Conversation dans ConversationConfig. Par exemple :
- Fournir un
Preface. - Écraser la valeur par défaut
PromptTemplate. - Écraser la valeur par défaut
DataProcessorConfig.
Ces remplacements sont particulièrement utiles pour les modèles affinés, qui peuvent nécessiter des configurations ou des modèles de requête différents de ceux du modèle de base dont ils sont issus.
MessageCallback
MessageCallback est la fonction de rappel que les utilisateurs doivent implémenter lorsqu'ils utilisent la méthode SendMessageAsync asynchrone.
La signature du rappel est absl::AnyInvocable<void(absl::StatusOr<Message>)>.
Cette fonction est déclenchée dans les conditions suivantes :
- Lorsqu'un nouveau bloc de
Messageest reçu du modèle. - Si une erreur se produit lors du traitement du message par LiteRT-LM.
- Une fois l'inférence du LLM terminée, le rappel est déclenché avec un
Messagevide (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.
[!IMPORTANT] Le
Messagereç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 SendMessage bloquant est la suivante :
{
"role": "model",
"content": [
"type": "text",
"text": "Hello World!"
]
}
Le rappel dans SendMessageAsync peut être invoqué plusieurs fois, chaque fois avec un extrait 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 de 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 Regex ou des règles de grammaire.
Pour l'activer, définissez EnableConstrainedDecoding(true) dans ConversationConfig et fournissez un ConstraintProviderConfig (par exemple, LlGuidanceConfig pour la compatibilité avec les expressions régulières, JSON et la grammaire). Transmettez ensuite 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 compatibilité avec le schéma JSON et la grammaire Lark, consultez la documentation sur le décodage contraint.
Utilisation de l'outil
L'appel d'outils permet au LLM de demander l'exécution de fonctions côté client. Vous définissez les outils dans le Preface de la conversation, en les identifiant par leur 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.
Étapes clés :
1. Déclarer des outils : définissez les outils (nom, description, paramètres) dans le JSON Preface.
2. Appels de détection : vérifiez model_message["tool_calls"] dans la réponse.
3. Execute : 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.