API C++ multiplataforma LiteRT-LM

Conversation é uma API de alto nível que representa uma única conversa com estado com o LLM e é o ponto de entrada recomendado para a maioria dos usuários. Ele gerencia internamente um Session e processa tarefas complexas de processamento de dados. Essas tarefas incluem manter o contexto inicial, gerenciar definições de ferramentas, pré-processar dados multimodais e aplicar modelos de comandos Jinja com formatação de mensagens baseada em função.

Fluxo de trabalho da Conversation API

O ciclo de vida típico para usar a API Conversation é:

  1. Crie um Engine: inicialize um único Engine com o caminho e a configuração do modelo. Esse é um objeto pesado que contém os pesos do modelo.
  2. Crie um Conversation: use o Engine para criar um ou mais objetos Conversation leves.
  3. Enviar mensagem: use os métodos do objeto Conversation para enviar mensagens ao LLM e receber respostas, permitindo uma interação semelhante a um chat.

Confira abaixo a maneira mais simples de enviar mensagens e receber respostas do modelo. É recomendado para a maioria dos casos de uso. Ela espelha as APIs do Gemini Chat.

  • SendMessage: uma chamada de bloqueio que recebe a entrada do usuário e retorna a resposta completa do modelo.
  • SendMessageAsync: uma chamada não bloqueadora que transmite a resposta do modelo token por token usando callbacks.

Confira um exemplo de snippet de código:

Conteúdo somente 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));

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

Conteúdo de dados multimodais

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

Usar a conversa com ferramentas

Consulte Uso avançado para saber mais sobre o uso da ferramenta com a API Conversation.

Componentes na conversa

O Conversation pode ser considerado um delegado para os usuários manterem Session e o processamento de dados complicado antes de enviar os dados para a sessão.

Tipos de E/S

O formato principal de entrada e saída da API Conversation é Message. No momento, isso é implementado como JsonMessage, que é um alias de tipo para ordered_json, uma estrutura de dados aninhada flexível de chave-valor.

A API Conversation opera com base em uma mensagem de entrada e uma de saída, imitando uma experiência de chat típica. A flexibilidade do Message permite que os usuários incluam campos arbitrários conforme necessário por modelos de solicitação ou modelos de LLM específicos, permitindo que o LiteRT-LM ofereça suporte a uma ampla variedade de modelos.

Embora não haja um padrão único e rígido, a maioria dos modelos e modelos de comandos espera que Message siga convenções semelhantes às usadas no conteúdo da API Gemini ou na estrutura de mensagens da OpenAI.

Message precisa conter role, representando quem enviou a mensagem. content pode ser tão simples quanto uma string de texto.

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

Para entrada de dados multimodais, content é uma lista de part. Novamente, part não é uma estrutura de dados predefinida, mas um tipo de dados de par chave-valor ordenado. Os campos específicos dependem do que o modelo de comando e o modelo esperam.

{
  "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, oferecemos suporte ao seguinte formato processado por 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",
}

Modelo de comando

Para manter a flexibilidade dos modelos variantes, o PromptTemplate é implementado como um wrapper simples em torno do Minja. O Minja é uma implementação em C++ do mecanismo de modelo Jinja, que processa entradas JSON para gerar comandos formatados.

O mecanismo de modelos Jinja é um formato amplamente adotado para modelos de comandos de LLM. Veja alguns exemplos:

O formato do mecanismo de modelo Jinja precisa corresponder estritamente à estrutura esperada pelo modelo ajustado com instruções. Normalmente, as versões de modelo incluem o modelo Jinja padrão para garantir o uso adequado do modelo.

O modelo Jinja usado pelo modelo será fornecido pelos metadados do arquivo do modelo.

[!NOTE] Uma mudança sutil no comando devido à formatação incorreta pode levar a uma degradação significativa do modelo. Conforme relatado em Quantifying Language Models' Sensitivity to Spurious Features in Prompt Design or: How I learned to start worrying about prompt formatting

Prefácio

Preface define o contexto inicial da conversa. Ela pode incluir mensagens iniciais, definições de ferramentas e outras informações básicas que o LLM precisa para iniciar a interação. Isso alcança uma funcionalidade semelhante a Gemini API system instruction e Gemini API Tools

Prefácio contém os seguintes campos:

  • messages As mensagens no prefácio. As mensagens forneceram o contexto inicial da conversa. Por exemplo, as mensagens podem ser o histórico da conversa, instruções do sistema de engenharia de comandos, exemplos de poucos disparos etc.

  • tools As ferramentas que o modelo pode usar na conversa. O formato das ferramentas não é fixo, mas segue principalmente Gemini API FunctionDeclaration.

  • extra_context O contexto extra que mantém a extensibilidade para que os modelos personalizem as informações de contexto necessárias para iniciar uma conversa. Por exemplo,

    • enable_thinking para modelos com modo de pensamento, por exemplo, Qwen3 ou SmolLM3-3B.

Exemplo de prefácio para fornecer instruções iniciais do sistema, ferramentas e desativar o modo de pensamento.

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

Histórico

A Conversation mantém uma lista de todas as trocas de Message na sessão. Esse histórico é crucial para a renderização do modelo de comando, já que o modelo de comando Jinja geralmente exige todo o histórico de conversas para gerar o comando correto para o LLM.

No entanto, a Session do LiteRT-LM tem estado, ou seja, processa entradas de forma incremental. Para diminuir essa lacuna, a Conversa gera o comando incremental necessário renderizando o modelo de comando duas vezes: uma com o histórico até a interação anterior e outra incluindo a mensagem atual. Ao comparar esses dois comandos renderizados, ele extrai a nova parte a ser enviada para a Sessão.

ConversationConfig

O ConversationConfig é usado para inicializar uma instância de Conversation. É possível criar essa configuração de duas maneiras:

  1. De um Engine:esse método usa o SessionConfig padrão associado ao mecanismo.
  2. De um SessionConfig específico:isso permite um controle mais detalhado das configurações de sessão.

Além das configurações de sessão, é possível personalizar ainda mais o comportamento do Conversation no ConversationConfig. Isso inclui:

Essas substituições são especialmente úteis para modelos ajustados, que podem exigir configurações ou modelos de comandos diferentes do modelo de base de que foram derivados.

MessageCallback

MessageCallback é a função de callback que os usuários precisam implementar ao usar o método assíncrono SendMessageAsync.

A assinatura do callback é absl::AnyInvocable<void(absl::StatusOr<Message>)>. Essa função é acionada nas seguintes condições:

  • Quando um novo bloco do Message é recebido do modelo.
  • Se ocorrer um erro durante o processamento de mensagens do LiteRT-LM.
  • Após a conclusão da inferência do LLM, o callback é acionado com um Message vazio (por exemplo, JsonMessage()) para sinalizar o fim da resposta.

Consulte a chamada assíncrona da etapa 6 para conferir um exemplo de implementação.

[!IMPORTANT] O Message recebido pelo callback contém apenas o último trecho da saída do modelo, não todo o histórico de mensagens.

Por exemplo, se a resposta completa do modelo esperada de uma chamada SendMessage de bloqueio for:

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

O callback em SendMessageAsync pode ser invocado várias vezes, cada vez com uma parte subsequente do 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!"
  ]
}

O implementador é responsável por acumular esses pedaços se a resposta completa for necessária durante o fluxo assíncrono. Como alternativa, a resposta completa vai estar disponível como a última entrada no History quando a chamada assíncrona for concluída.

Uso avançado

Decodificação restrita

O LiteRT-LM oferece suporte à decodificação restrita, permitindo que você aplique estruturas específicas à saída do modelo, como esquemas JSON, padrões de regex ou regras gramaticais.

Para ativar, defina EnableConstrainedDecoding(true) em ConversationConfig e forneça um ConstraintProviderConfig (por exemplo, LlGuidanceConfig para suporte a regex/JSON/gramática). Em seguida, transmita restrições usando OptionalArgs em SendMessage.

Exemplo: restrição 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 mais detalhes, incluindo suporte a esquema JSON e gramática Lark, consulte a documentação sobre decodificação restrita.

Uso de ferramentas

Com a chamada de função, o LLM pode solicitar a execução de funções do lado do cliente. Você define ferramentas no Preface da conversa, identificando-as por nome. Quando o modelo gera uma chamada de ferramenta, você a captura, executa a função correspondente no aplicativo e retorna o resultado ao modelo.

Fluxo de alto nível:1. Declarar ferramentas:defina ferramentas (nome, descrição, parâmetros) no JSON Preface. 2. Detect Calls:verifique model_message["tool_calls"] na resposta. 3. Executar:execute a lógica do aplicativo para a ferramenta solicitada. 4. Responder:envie uma mensagem com role: "tool" contendo a saída da ferramenta de volta para o modelo.

Para mais detalhes e um exemplo completo de loop de conversa, consulte a documentação sobre o uso de ferramentas.