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 é:
- Crie um
Engine: inicialize um únicoEnginecom o caminho e a configuração do modelo. Esse é um objeto pesado que contém os pesos do modelo. - Crie um
Conversation: use oEnginepara criar um ou mais objetosConversationleves. - Enviar mensagem: use os métodos do objeto
Conversationpara 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
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();
}
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:
messagesAs 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.toolsAs ferramentas que o modelo pode usar na conversa. O formato das ferramentas não é fixo, mas segue principalmenteGemini API FunctionDeclaration.extra_contextO 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_thinkingpara 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:
- De um
Engine:esse método usa oSessionConfigpadrão associado ao mecanismo. - De um
SessionConfigespecí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:
- Fornecer um
Preface. - Substituindo o
PromptTemplatepadrão. - Substituindo o
DataProcessorConfigpadrão.
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
Messagevazio (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
Messagerecebido 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.