Conversation es una API de alto nivel que representa una conversación única y con estado con el LLM, y es el punto de entrada recomendado para la mayoría de los usuarios. Administra internamente una Session y controla
tareas complejas de procesamiento de datos. Estas tareas incluyen mantener el contexto inicial, administrar definiciones de herramientas, preprocesar datos multimodales y aplicar plantillas de instrucciones de Jinja con formato de mensajes basado en roles.
Flujo de trabajo de la API de Conversation
El ciclo de vida típico para usar la API de Conversation es el siguiente:
- Crea un
Engine: Inicializa un soloEnginecon la ruta de acceso y la configuración del modelo. Este es un objeto pesado que contiene los pesos del modelo. - Crea una
Conversation: Usa elEnginepara crear uno o más objetosConversationligeros. - Envía un mensaje: Usa los métodos del objeto
Conversationpara enviar mensajes al LLM y recibir respuestas, lo que permite una interacción similar a un chat.
A continuación, se muestra la forma más sencilla de enviar mensajes y obtener respuestas del modelo. Se recomienda para la mayoría de los casos de uso. Refleja las APIs de Gemini Chat.
SendMessage: Es una llamada de bloqueo que toma la entrada del usuario y muestra la respuesta completa del modelo.SendMessageAsync: Es una llamada sin bloqueo que transmite la respuesta del modelo token por token a través de devoluciones de llamada.
A continuación, se muestra un ejemplo de fragmento de código:
Contenido solo de texto
#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));
Ejemplo de 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();
}
Contenido de datos 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;
Usa Conversation con herramientas
Consulta Uso avanzado para obtener información detallada sobre el uso de herramientas con la API de Conversation.
Componentes en Conversation
Conversation se puede considerar como un delegado para que los usuarios
mantengan Session y el procesamiento de datos complicado antes de enviar los
datos a Session.
Tipos de E/S
El formato principal de entrada y salida para la API de Conversation es
Message. Actualmente, se implementa como
JsonMessage, que es un alias de tipo para
ordered_json, una estructura de datos flexible de pares clave-valor anidados.
La API Conversation opera según el principio de entrada y salida de mensajes, lo que imita una experiencia de chat típica. La flexibilidad de
Message permite a los usuarios incluir campos arbitrarios según lo requieran las
plantillas de instrucciones o los modelos LLM específicos, lo que permite que LiteRT-LM admita una amplia
variedad de modelos.
Si bien no existe un estándar único y rígido, la mayoría de las plantillas de instrucciones y los modelos
esperan Message que sigan convenciones similares a las que se usan en el
contenido de la API de Gemini o la
estructura de mensajes de OpenAI.
Message debe contener role, que representa desde quién se envía el mensaje. content puede ser tan simple como una cadena de texto.
{
"role": "model", // Represent who the message is sent from.
"content": "Hello World!" // Naive text only content.
}
Para la entrada de datos multimodales, content es una lista de part. De nuevo, part no es una
estructura de datos predefinida, sino un tipo de datos de pares clave-valor ordenados. Los campos específicos dependen de lo que esperan la plantilla de instrucciones y el modelo.
{
"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"
}
]
}
Para part multimodal, admitimos el siguiente formato que controla
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",
}
Plantilla de instrucciones
Para mantener la flexibilidad de los modelos variantes, PromptTemplate se implementa como un wrapper delgado alrededor de Minja. Minja es una implementación en C++ del motor de plantillas de Jinja, que procesa la entrada JSON para generar instrucciones con formato.
El motor de plantillas de Jinja es un formato ampliamente adoptado para las plantillas de instrucciones de LLM. Estos son algunos ejemplos:
El formato del motor de plantillas de Jinja debe coincidir estrictamente con la estructura que espera el modelo ajustado por instrucciones. Por lo general, las versiones del modelo incluyen la plantilla de Jinja estándar para garantizar el uso adecuado del modelo.
Los metadatos del archivo del modelo proporcionarán la plantilla de Jinja que usa el modelo.
Nota: Un cambio sutil en la instrucción debido a un formato incorrecto puede provocar una degradación significativa del modelo. Según se informó en Quantifying Language Models' Sensitivity to Spurious Features in Prompt Design or: How I learned to start worrying about prompt formatting
Prefacio
Preface establece el contexto inicial de la conversación. Puede incluir mensajes iniciales, definiciones de herramientas y cualquier otra información de contexto que el LLM necesite para iniciar la interacción. Esto logra una funcionalidad similar a
la de
Gemini API system instruction
y Gemini API Tools.
El prefacio contiene los siguientes campos:
messagesLos mensajes del prefacio. Los mensajes proporcionaron el contexto inicial de la conversación. Por ejemplo, los mensajes pueden ser el historial de conversaciones, las instrucciones del sistema de ingeniería de instrucciones, ejemplos de pocos disparos, etcétera.toolsLas herramientas que el modelo puede usar en la conversación. El formato de las herramientas tampoco es fijo, pero, en su mayoría, sigueGemini API FunctionDeclaration.extra_contextEl contexto adicional que mantiene la extensibilidad para que los modelos personalicen la información de contexto requerida para iniciar una conversación. Por ejemplo,enable_thinkingpara modelos con modo de pensamiento, por ejemplo, Qwen3 o SmolLM3-3B.
Ejemplo de prefacio para proporcionar instrucciones iniciales del sistema, herramientas y desactivar el modo de pensamiento.
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}
}
});
Historial
Conversation mantiene una lista de todos los intercambios de Message dentro de la sesión. Este historial es fundamental para la renderización de plantillas de instrucciones, ya que la plantilla de instrucciones de Jinja suele requerir todo el historial de conversaciones para generar la instrucción correcta para el LLM.
Sin embargo, la sesión de LiteRT-LM tiene estado, lo que significa que procesa las entradas de forma incremental. Para cerrar esta brecha, Conversation genera la instrucción incremental necesaria renderizando la plantilla de instrucciones dos veces: una con el historial hasta el turno anterior y otra que incluye el mensaje actual. Cuando compara estas dos instrucciones renderizadas, extrae la parte nueva que se enviará a la sesión.
ConversationConfig
ConversationConfig se usa para inicializar una instancia de
Conversation. Puedes crear esta configuración de dos maneras:
- Desde un
Engine: Este método usa elSessionConfigpredeterminado asociado con el motor. - Desde un
SessionConfigespecífico: Esto permite un control más detallado sobre la configuración de la sesión.
Además de la configuración de la sesión, puedes personalizar aún más el
Conversation comportamiento dentro de la
ConversationConfig. Esto incluye lo siguiente:
- Proporcionar un
Preface. - Reemplazar el
PromptTemplatepredeterminado. - Reemplazar el
DataProcessorConfigpredeterminado.
Estos reemplazos son particularmente útiles para los modelos ajustados, que pueden requerir diferentes configuraciones o plantillas de instrucciones que el modelo base del que se derivaron.
MessageCallback
MessageCallback es la función de devolución de llamada que los usuarios deben
implementar cuando usan el método SendMessageAsync
asíncrono.
La firma de devolución de llamada es absl::AnyInvocable<void(absl::StatusOr<Message>)>.
Esta función se activa en las siguientes condiciones:
- Cuando se recibe un nuevo fragmento del
Messagedel modelo. - Si se produce un error durante el procesamiento de mensajes de LiteRT-LM.
- Una vez que se completa la inferencia del LLM, se activa la devolución de llamada con un
vacío
Message(p.ej.,JsonMessage()) para indicar el final de la respuesta.
Consulta la llamada asíncrona del paso 6 para ver una implementación de ejemplo.
Nota: El
Message
que recibe la devolución de llamada contiene solo el fragmento más reciente del resultado del modelo,
no todo el historial de mensajes.
Por ejemplo, si la respuesta completa del modelo que se espera de una llamada de bloqueo
SendMessage sería la siguiente:
{
"role": "model",
"content": [
"type": "text",
"text": "Hello World!"
]
}
Es posible que se invoque la devolución de llamada en SendMessageAsync varias
veces, cada vez con una parte posterior del texto:
// 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!"
]
}
El implementador es responsable de acumular estos fragmentos si se necesita la respuesta completa durante la transmisión asíncrona. Como alternativa, la respuesta completa
estará disponible como la última entrada en History una vez
que se complete la llamada asíncrona.
Uso avanzado {#advanced-usage}
Decodificación restringida
LiteRT-LM admite la decodificación restringida, lo que te permite aplicar estructuras específicas en el resultado del modelo, como esquemas JSON, patrones de expresiones regulares o reglas de gramática.
Para habilitarlo, establece EnableConstrainedDecoding(true) en ConversationConfig y proporciona un ConstraintProviderConfig (p.ej., LlGuidanceConfig para la compatibilidad con expresiones regulares, JSON o gramática). Luego, pasa las restricciones a través de OptionalArgs en SendMessage.
Ejemplo: Restricción de expresiones regulares
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}
);
Para obtener detalles completos, incluida la compatibilidad con el esquema JSON y la gramática de Lark, consulta la documentación de decodificación restringida.
Uso de herramientas
La llamada a herramientas permite que el LLM solicite la ejecución de funciones del cliente. Defines herramientas en el Preface de la conversación y las claves por nombre. Cuando el modelo genera una llamada a herramienta, la capturas, ejecutas la función correspondiente en tu aplicación y muestras el resultado al modelo.
Flujo de alto nivel:
- Declara herramientas: Define herramientas (nombre, descripción, parámetros) en el JSON
Preface. - Detecta llamadas: Verifica
model_message["tool_calls"]en la respuesta. - Ejecuta: Ejecuta la lógica de tu aplicación para la herramienta solicitada.
- Responde: Envía un mensaje con
role: "tool"que contenga el resultado de la herramienta al modelo.
Para obtener detalles completos y un ejemplo completo de bucle de chat, consulta la documentación de uso de herramientas.