API de C++ multiplataforma de LiteRT-LM

Conversation es una API de alto nivel que representa una sola conversación con estado con el LLM y es el punto de entrada recomendado para la mayoría de los usuarios. Administra internamente un Session y controla tareas complejas de procesamiento de datos. Estas tareas incluyen mantener el contexto inicial, administrar las 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:

  1. Crea un Engine: Inicializa un solo Engine con la ruta de acceso y la configuración del modelo. Es un objeto pesado que contiene los pesos del modelo.
  2. Crea un objeto Conversation: Usa Engine para crear uno o más objetos Conversation ligeros.
  3. Send Message: Utiliza los métodos del objeto Conversation para 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 un mensaje y obtener una respuesta del modelo. Se recomienda para la mayoría de los casos de uso. Es similar a las APIs de Gemini Chat.

  • SendMessage: Es una llamada de bloqueo que toma la entrada del usuario y devuelve 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 incluyen ejemplos de fragmentos de código:

Contenido de solo 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 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();
}

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;

Cómo usar Conversar con herramientas

Consulta Uso avanzado para obtener información detallada sobre el uso de la herramienta con la API de Conversation.

Componentes en la conversación

Conversation se podría considerar como un delegado para que los usuarios mantengan Session y procesen datos complejos antes de enviarlos a Session.

Tipos de E/S

El formato principal de entrada y salida de la API de Conversation es Message. Actualmente, esto se implementa como JsonMessage, que es un alias de tipo para ordered_json, una estructura de datos flexible de clave-valor anidada.

La API de Conversation funciona según el principio de mensaje entrante y mensaje saliente, lo que simula 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 de 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 y los modelos de instrucciones esperan que Message siga 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 quién 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. Nuevamente, 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"
    }
  ]
}

En el caso de 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 de variantes, PromptTemplate se implementa como un wrapper delgado alrededor de Minja. Minja es una implementación en C++ del motor de plantillas Jinja, que procesa la entrada JSON para generar instrucciones con formato.

El motor de plantillas Jinja es un formato ampliamente adoptado para las plantillas de instrucciones de LLM. Estos son algunos ejemplos:

El formato del motor de plantillas Jinja debe coincidir estrictamente con la estructura que espera el modelo ajustado según las instrucciones. Por lo general, las versiones de modelos incluyen la plantilla estándar de Jinja para garantizar el uso adecuado del modelo.

La plantilla de Jinja que usa el modelo se proporcionará en los metadatos del archivo del 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.

Preface contiene los siguientes campos:

  • messages Son los mensajes del prefacio. Los mensajes proporcionaron el contexto inicial de la conversación. Por ejemplo, los mensajes pueden ser el historial de conversación, las instrucciones del sistema de ingeniería de instrucciones, los ejemplos de pocos disparos, etcétera.

  • tools Las herramientas que el modelo puede usar en la conversación. El formato de las herramientas tampoco es fijo, pero, en su mayoría, sigue Gemini API FunctionDeclaration.

  • extra_context Es el contexto adicional que mantiene la extensibilidad de los modelos para personalizar la información de contexto requerida para iniciar una conversación. Por ejemplo:

    • enable_thinking para modelos con modo de pensamiento, p.ej., 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 la plantilla 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 mantiene un estado, lo que significa que procesa las entradas de forma incremental. Para subsanar esta brecha, Conversation genera la instrucción incremental necesaria renderizando la plantilla de instrucción dos veces: una con el historial hasta el turno anterior y otra con el mensaje actual. Al comparar estas dos instrucciones renderizadas, se 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:

  1. Desde un Engine: Este método usa el SessionConfig predeterminado asociado al motor.
  2. Desde un SessionConfig específico: Esto permite un control más detallado sobre la configuración de la sesión.

Más allá de la configuración de la sesión, puedes personalizar aún más el comportamiento de Conversation dentro de ConversationConfig. Esto incluye lo siguiente:

Estas anulaciones son particularmente útiles para los modelos ajustados, que podrían 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 asíncrono SendMessageAsync.

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 de Message del modelo.
  • Si se produce un error durante el procesamiento de mensajes de LiteRT-LM.
  • Cuando se completa la inferencia del LLM, se activa la devolución de llamada con un Message vacío (p.ej., JsonMessage()) para indicar el final de la respuesta.

Consulta la llamada asíncrona del paso 6 para ver un ejemplo de implementación.

[!IMPORTANT] El objeto Message que recibe la devolución de llamada solo contiene el fragmento más reciente del resultado del modelo, no el historial de mensajes completo.

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

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 Regex o reglas gramaticales.

Para habilitarlo, establece EnableConstrainedDecoding(true) en ConversationConfig y proporciona un ConstraintProviderConfig (p.ej., LlGuidanceConfig para la compatibilidad con regex, JSON y gramática). Luego, pasa las restricciones a través de OptionalArgs en SendMessage.

Ejemplo: Restricción de regex

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 todos los detalles, incluido el esquema JSON y la compatibilidad con la gramática de Lark, consulta la documentación de la decodificación restringida.

Uso de herramientas

La llamada a herramientas permite que el LLM solicite la ejecución de funciones del cliente. Defines las herramientas en el Preface de la conversación y las identificas por su nombre. Cuando el modelo genera una llamada a herramienta, la capturas, ejecutas la función correspondiente en tu aplicación y le devuelves el resultado al modelo.

Flujo general: 1. Declare Tools: Define herramientas (nombre, descripción, parámetros) en el JSON de Preface. 2. Llamadas de detección: Verifica model_message["tool_calls"] en la respuesta. 3. Ejecutar: Ejecuta la lógica de tu aplicación para la herramienta solicitada. 4. Responder: Envía un mensaje con role: "tool" que contenga el resultado de la herramienta al modelo.

Para obtener todos los detalles y un ejemplo completo de bucle de chat, consulta la documentación sobre el uso de herramientas.