API C++ multiplataforma LiteRT-LM

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

Fluxo de trabalho da API Conversation

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

  1. Criar 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. Criar 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, ativando uma interação semelhante a um chat.

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

  • SendMessage: uma chamada de bloqueio que recebe a entrada do usuário e retorna a resposta completa do modelo.
  • SendMessageAsync: uma chamada sem bloqueio que transmite a resposta do modelo token por token por meio de callbacks.

Confira um exemplo de snippet de código:

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

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 de ferramentas com a API Conversation.

Componentes na conversa

Conversation pode ser considerado um delegado para que os usuários mantenham 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. Atualmente, ele é implementado como JsonMessage, que é um alias de tipo para ordered_json, uma estrutura de dados de chave-valor aninhada flexível.

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

Embora não haja um padrão rígido único, a maioria dos modelos de comandos e modelos 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 de quem a mensagem é enviada. 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 de 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 fino em torno do Minja. O Minja é uma implementação em C++ do mecanismo de modelo Jinja, que processa a entrada JSON para gerar comandos formatados.

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

O formato do mecanismo de modelo Jinja precisa corresponder estritamente à estrutura esperada pelo modelo ajustado por instrução. Normalmente, as versões do 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 modelo.

Observação: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 ou: How I learned to start worrying about prompt formatting (em inglês).

Prefácio

Preface define o contexto inicial da conversa. Ele pode incluir mensagens iniciais, definições de ferramentas e outras informações de plano de fundo que o LLM precisa para iniciar a interação. Isso alcança funcionalidades semelhantes a o Gemini API system instruction e Gemini API Tools

O prefácio contém os seguintes campos

  • messages: as mensagens no prefácio. As mensagens fornecem o plano de fundo inicial da conversa. Por exemplo, as mensagens podem ser o histórico de conversas, 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 também 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 conversa mantém uma lista de todas as trocas de mensagens na sessão. Esse histórico é essencial para a renderização do modelo de comando, já que o modelo de comando Jinja normalmente exige todo o histórico de conversas para gerar o comando correto para o LLM.

No entanto, a sessão do LiteRT-LM é com estado, o que significa que ela processa as entradas de forma incremental. Para preencher essa lacuna, a conversa gera o comando incremental necessário renderizando o modelo de comando duas vezes: uma com o histórico até a rodada 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

ConversationConfig é usado para inicializar uma instância de Conversation. É possível criar essa configuração de algumas 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 refinado sobre as configurações da sessão.

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

Essas substituições são particularmente úteis para modelos ajustados, que podem exigir configurações ou modelos de comandos diferentes do modelo 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 da 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 vazio Message (por exemplo, JsonMessage()) para sinalizar o fim da resposta.

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

Observação: A Message recebida pelo callback contém apenas o bloco mais recente 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 de bloqueio SendMessage 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 blocos se a resposta completa for necessária durante o fluxo assíncrono. Como alternativa, a resposta completa estará disponível como a última entrada no History quando a chamada assíncrona for concluída.

Uso avançado {#advanced-usage}

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 ativá-lo, 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 esquemas JSON e gramática Lark, consulte a documentação de decodificação restrita.

Uso de ferramentas

A chamada de ferramentas permite que o LLM solicite a execução de funções do lado do cliente. Você define ferramentas no Preface da conversa, identificando-as pelo 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. Detectar chamadas: 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 ao modelo.

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