API C++ multipiattaforma LiteRT-LM

Conversation è un'API di alto livello che rappresenta una singola conversazione stateful con l'LLM ed è il punto di accesso consigliato per la maggior parte degli utenti. Gestisce internamente un Session e gestisce attività complesse di elaborazione dei dati. Queste attività includono il mantenimento del contesto iniziale, la gestione delle definizioni degli strumenti, l'elaborazione preliminare dei dati multimodali e l'applicazione di modelli di prompt Jinja con formattazione dei messaggi basata sui ruoli.

Workflow API Conversation

Il ciclo di vita tipico per l'utilizzo dell'API Conversation è:

  1. Crea un Engine: inizializza un singolo Engine con il percorso e la configurazione del modello. Si tratta di un oggetto pesante che contiene i pesi del modello.
  2. Crea un Conversation: utilizza Engine per creare uno o più oggetti Conversation leggeri.
  3. Send Message (Invia messaggio): utilizza i metodi dell'oggetto Conversation per inviare messaggi all'LLM e ricevere risposte, consentendo di fatto un'interazione simile a una chat.

Di seguito è riportato il modo più semplice per inviare un messaggio e ottenere una risposta del modello. È consigliato per la maggior parte dei casi d'uso. Rispecchia le API Gemini Chat.

  • SendMessage: una chiamata bloccante che accetta l'input utente e restituisce la risposta completa del modello.
  • SendMessageAsync: una chiamata non bloccante che trasmette in streaming la risposta del modello token per token tramite i callback.

Ecco un esempio di snippet di codice:

Contenuti solo testo

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

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

Contenuti dei dati multimodali

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

Utilizzare Conversazione con Strumenti

Per informazioni dettagliate sull'utilizzo dello strumento con l'API Conversation, consulta Utilizzo avanzato.

Componenti in Conversazione

Conversation potrebbe essere considerato un delegato per gli utenti per mantenere Session ed eseguire un trattamento dei dati complesso prima di inviarli a Session.

Tipi di I/O

Il formato di input e output principale per l'API Conversation è Message. Attualmente, questa operazione viene implementata come JsonMessage, che è un alias di tipo per ordered_json, una struttura di dati flessibile di coppie chiave-valore nidificate.

L'API Conversation funziona in base al principio di input/output dei messaggi, simulando una tipica esperienza di chat. La flessibilità di Message consente agli utenti di includere campi arbitrari in base alle esigenze di modelli di prompt o modelli LLM specifici, consentendo a LiteRT-LM di supportare un'ampia varietà di modelli.

Sebbene non esista un unico standard rigido, la maggior parte dei modelli e dei modelli di prompt si aspetta che Message segua convenzioni simili a quelle utilizzate nella Gemini API Content o nella struttura dei messaggi di OpenAI.

Message deve contenere role, che rappresenta il mittente del messaggio. content può essere semplice come una stringa di testo.

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

Per l'input di dati multimodali, content è un elenco di part. Anche in questo caso part non è una struttura di dati predefinita, ma un tipo di dati di coppia chiave-valore ordinata. I campi specifici dipendono da ciò che si aspettano il modello e il modello di prompt.

{
  "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"
    }
  ]
}

Per part multimodale, supportiamo il seguente formato gestito da 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",
}

Prompt Template

Per mantenere la flessibilità dei modelli di varianti, PromptTemplate viene implementato come wrapper leggero intorno a Minja. Minja è un'implementazione C++ del motore di modelli Jinja, che elabora l'input JSON per generare prompt formattati.

Il motore di modelli Jinja è un formato ampiamente adottato per i modelli di prompt LLM. Ecco alcuni esempi:

Il formato del motore di modelli Jinja deve corrispondere rigorosamente alla struttura prevista dal modello ottimizzato per le istruzioni. In genere, le release del modello includono il modello Jinja standard per garantire un utilizzo corretto del modello.

Il modello Jinja utilizzato dal modello verrà fornito dai metadati del file del modello.

[!NOTE] Una leggera modifica del prompt dovuta a una formattazione errata può comportare un significativo degrado del modello. Come riportato in Quantifying Language Models' Sensitivity to Spurious Features in Prompt Design or: How I learned to start worrying about prompt formatting

Prefazione

Preface imposta il contesto iniziale della conversazione. Possono includere messaggi iniziali, definizioni di strumenti e qualsiasi altra informazione di base di cui l'LLM ha bisogno per avviare l'interazione. In questo modo si ottiene una funzionalità simile a Gemini API system instruction e Gemini API Tools

Prefazione contiene i seguenti campi

  • messages I messaggi nella prefazione. I messaggi hanno fornito il contesto iniziale della conversazione. Ad esempio, i messaggi possono essere la cronologia della conversazione, le istruzioni del sistema di prompt engineering, esempi few-shot e così via.

  • tools Gli strumenti che il modello può utilizzare nella conversazione. Il formato degli strumenti non è di nuovo fisso, ma segue per lo più Gemini API FunctionDeclaration.

  • extra_context Il contesto aggiuntivo che mantiene l'estensibilità dei modelli per personalizzare le informazioni di contesto richieste per iniziare una conversazione. Ad esempio:

    • enable_thinking per i modelli con modalità di ragionamento, ad es. Qwen3 o SmolLM3-3B.

Prefazione di esempio per fornire istruzioni di sistema iniziali, strumenti e disattivare la modalità di pensiero.

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

Cronologia

Conversazione mantiene un elenco di tutti gli scambi di messaggi all'interno della sessione. Questa cronologia è fondamentale per il rendering del modello di prompt, in quanto il modello di prompt Jinja in genere richiede l'intera cronologia della conversazione per generare il prompt corretto per l'LLM.

Tuttavia, la sessione LiteRT-LM è stateful, il che significa che elabora gli input in modo incrementale. Per colmare questa lacuna, Conversation genera il prompt incrementale necessario eseguendo il rendering del modello di prompt due volte: una volta con la cronologia fino al turno precedente e una volta includendo il messaggio attuale. Confrontando questi due prompt sottoposti a rendering, estrae la nuova parte da inviare alla sessione.

ConversationConfig

ConversationConfig viene utilizzato per inizializzare un'istanza di Conversation. Puoi creare questa configurazione in due modi:

  1. Da un Engine: questo metodo utilizza il SessionConfig predefinito associato al motore.
  2. Da un SessionConfig specifico:consente un controllo più preciso delle impostazioni della sessione.

Oltre alle impostazioni della sessione, puoi personalizzare ulteriormente il comportamento di Conversation all'interno di ConversationConfig. È incluso quanto segue:

Queste sovrascritture sono particolarmente utili per i modelli ottimizzati, che potrebbero richiedere configurazioni o modelli di prompt diversi rispetto al modello di base da cui sono derivati.

MessageCallback

MessageCallback è la funzione di callback che gli utenti devono implementare quando utilizzano il metodo asincrono SendMessageAsync.

La firma del callback è absl::AnyInvocable<void(absl::StatusOr<Message>)>. Questa funzione viene attivata nelle seguenti condizioni:

  • Quando viene ricevuto un nuovo blocco di Message dal modello.
  • Se si verifica un errore durante l'elaborazione dei messaggi di LiteRT-LM.
  • Al termine dell'inferenza del LLM, viene attivato il callback con un Message vuoto (ad es. JsonMessage()) per segnalare la fine della risposta.

Per un esempio di implementazione, consulta la chiamata asincrona del passaggio 6.

[!IMPORTANT] Il Message ricevuto dal callback contiene solo l'ultimo blocco dell'output del modello, non l'intera cronologia dei messaggi.

Ad esempio, se la risposta completa del modello prevista da una chiamata di blocco SendMessage sarebbe:

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

Il callback in SendMessageAsync potrebbe essere richiamato più volte, ogni volta con una parte successiva del testo:

// 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!"
  ]
}

L'implementatore è responsabile dell'accumulo di questi blocchi se è necessaria la risposta completa durante lo stream asincrono. In alternativa, la risposta completa sarà disponibile come ultima voce nel History una volta completata la chiamata asincrona.

Utilizzo avanzato

Decodifica vincolata

LiteRT-LM supporta la decodifica vincolata, che consente di applicare strutture specifiche all'output del modello, come schemi JSON, pattern Regex o regole grammaticali.

Per abilitarlo, imposta EnableConstrainedDecoding(true) in ConversationConfig e fornisci un ConstraintProviderConfig (ad es. LlGuidanceConfig per il supporto di espressioni regolari/JSON/grammatica). Poi, passa i vincoli tramite OptionalArgs in SendMessage.

Esempio: vincolo 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}
);

Per tutti i dettagli, inclusi il supporto di JSON Schema e Lark Grammar, consulta la documentazione sul decodifica vincolata.

Utilizzo degli strumenti

La chiamata di strumenti consente all'LLM di richiedere l'esecuzione di funzioni lato client. Definisci gli strumenti nel Preface della conversazione, digitandoli per nome. Quando il modello restituisce una chiamata allo strumento, la acquisisci, esegui la funzione corrispondente nella tua applicazione e restituisci il risultato al modello.

Flusso di alto livello: 1. Dichiarare gli strumenti:definisci gli strumenti (nome, descrizione, parametri) nel file JSON Preface. 2. Detect Calls: controlla model_message["tool_calls"] nella risposta. 3. Esegui:esegui la logica dell'applicazione per lo strumento richiesto. 4. Rispondi:invia un messaggio con role: "tool" contenente l'output dello strumento al modello.

Per tutti i dettagli e un esempio completo di ciclo di chat, consulta la documentazione sull'utilizzo degli strumenti.