Conversation ist eine API auf hoher Ebene, die eine einzelne, zustandsbehaftete Unterhaltung mit dem LLM darstellt. Sie ist der empfohlene Einstiegspunkt für die meisten Nutzer. Intern wird eine Session verwaltet und es werden
komplexe Aufgaben zur Datenverarbeitung ausgeführt. Zu diesen Aufgaben gehören das Beibehalten des ursprünglichen Kontexts, das Verwalten von Tool-Definitionen, die Vorverarbeitung multimodaler Daten und die Anwendung von Jinja-Promptvorlagen mit rollenbasierter Nachrichtenformatierung.
Conversation API-Workflow
Der typische Lebenszyklus für die Verwendung der Conversation API sieht so aus:
Engineerstellen: Initialisieren Sie eine einzelneEnginemit dem Modellpfad und der Konfiguration. Dies ist ein schwergewichtiges Objekt, das die Modellgewichte enthält.Conversationerstellen: Verwenden Sie dieEngine, um ein oder mehrere leichtgewichtigeConversation-Objekte zu erstellen.- Nachricht senden: Verwenden Sie die Methoden des
Conversation-Objekts, um Nachrichten an das LLM zu senden und Antworten zu erhalten. So wird eine chatähnliche Interaktion ermöglicht.
Im Folgenden wird die einfachste Möglichkeit beschrieben, eine Nachricht zu senden und eine Modellantwort zu erhalten. Sie wird für die meisten Anwendungsfälle empfohlen. Sie spiegelt die Gemini Chat APIs wider.
SendMessage: Ein blockierender Aufruf, der die Nutzereingabe verwendet und die vollständige Modellantwort zurückgibt.SendMessageAsync: Ein nicht blockierender Aufruf, der die Antwort des Modells Token für Token über Callbacks zurückgibt.
Hier sind Beispiele für Code-Snippets:
Nur Textinhalte
#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));
Beispiel für 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();
}
Multimodale Dateninhalte
// 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;
Unterhaltung mit Tools verwenden
Weitere Informationen zur Tool-Nutzung mit Conversation API finden Sie unter Erweiterte Verwendung.
Komponenten in der Unterhaltung
Conversation kann als Delegat für Nutzer betrachtet werden, um
Session und komplizierte Datenverarbeitung zu verwalten, bevor die
Daten an die Sitzung gesendet werden.
E/A-Typen
Das wichtigste Eingabe- und Ausgabeformat für die Conversation API ist
Message. Derzeit wird dies als
JsonMessage implementiert, ein Typalias für
ordered_json, eine flexible verschachtelte Schlüssel/Wert-Datenstruktur.
Die Conversation API funktioniert nach dem Prinzip „Nachricht rein, Nachricht raus“
und ahmt so eine typische Chat-Erfahrung nach. Die Flexibilität von
Message ermöglicht es Nutzern, beliebige Felder einzufügen, die für
bestimmte Promptvorlagen oder LLM-Modelle erforderlich sind. So kann LiteRT-LM eine Vielzahl von
Modellen unterstützen.
Es gibt zwar keinen einheitlichen Standard, aber die meisten Promptvorlagen und Modelle
erwarten Message, dass Konventionen folgen, die denen in der
Gemini API Content oder der
OpenAI Message-Struktur ähneln.
Message muss role enthalten, die angibt, von wem die Nachricht gesendet wurde. content kann so einfach wie ein Textstring sein.
{
"role": "model", // Represent who the message is sent from.
"content": "Hello World!" // Naive text only content.
}
Bei multimodaler Dateneingabe ist content eine Liste von part. Auch hier ist part keine
vordefinierte Datenstruktur, sondern ein
geordneter Schlüssel/Wert-Datentyp. Die spezifischen Felder hängen davon ab, was die Promptvorlage und das Modell erwarten.
{
"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"
}
]
}
Für multimodale part unterstützen wir das folgende Format, das von
data_utils.h verarbeitet wird.
{
"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",
}
Eingabeaufforderungsvorlage
Um die Flexibilität für verschiedene Modelle zu gewährleisten, wird PromptTemplate als dünner Wrapper um Minja implementiert. Minja ist eine C++-Implementierung der Jinja-Vorlagen-Engine, die JSON-Eingaben verarbeitet, um formatierte Prompts zu generieren.
Die Jinja-Vorlagen-Engine ist ein weit verbreitetes Format für LLM-Prompt vorlagen. Hier einige Beispiele:
Das Format der Jinja-Vorlagen-Engine muss genau der Struktur entsprechen, die vom auf Anweisungen abgestimmten Modell erwartet wird. In der Regel enthalten Modellversionen die Standard-Jinja-Vorlage, um eine korrekte Modellnutzung zu gewährleisten.
Die vom Modell verwendete Jinja-Vorlage wird in den Metadaten der Modelldatei angegeben.
Hinweis:Eine geringfügige Änderung des Prompts aufgrund einer falschen Formatierung kann zu einer erheblichen Verschlechterung des Modells führen. Siehe Quantifying Language Models' Sensitivity to Spurious Features in Prompt Design oder: How I learned to start worrying about prompt formatting
Vorwort
Preface legt den anfänglichen Kontext für die Unterhaltung fest. Es kann anfängliche Nachrichten, Tool-Definitionen und alle anderen Hintergrundinformationen enthalten, die das LLM benötigt, um die Interaktion zu starten. So wird eine ähnliche Funktionalität wie bei
der
Gemini API system instruction
und Gemini API Tools erreicht.
`Preface` enthält die folgenden Felder:
messages: Die Nachrichten im Vorwort. Die Nachrichten liefern den anfänglichen Hintergrund für die Unterhaltung. Beispiele für Nachrichten sind der Unterhaltungsverlauf, Systemanweisungen für das Prompt-Engineering, Beispiele für wenige Aufnahmen usw.tools: Die Tools, die das Modell in der Unterhaltung verwenden kann. Das Format der Tools ist nicht festgelegt, folgt aber meistensGemini API FunctionDeclaration.extra_context: Der zusätzliche Kontext, der die Erweiterbarkeit für Modelle beibehält, um die erforderlichen Kontextinformationen für den Beginn einer Unterhaltung anzupassen. Beispiele:enable_thinkingfür Modelle mit Denkmodus, z.B. Qwen3 oder SmolLM3-3B.
Beispiel für ein Vorwort, um anfängliche Systemanweisungen und Tools bereitzustellen und den Denkmodus zu deaktivieren.
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}
}
});
Verlauf
In der Unterhaltung wird eine Liste aller Nachrichtenaustausche innerhalb der Sitzung geführt. Dieser Verlauf ist entscheidend für das Rendern von Promptvorlagen, da die Jinja-Promptvorlage in der Regel den gesamten Unterhaltungsverlauf benötigt, um den korrekten Prompt für das LLM zu generieren.
Die LiteRT-LM Sitzung ist jedoch zustandsbehaftet, d. h., sie verarbeitet Eingaben inkrementell. Um diese Lücke zu schließen, generiert die Unterhaltung den erforderlichen inkrementellen Prompt, indem die Promptvorlage zweimal gerendert wird: einmal mit dem Verlauf bis zum vorherigen Zug und einmal mit der aktuellen Nachricht. Durch den Vergleich dieser beiden gerenderten Prompts wird der neue Teil extrahiert, der an die Sitzung gesendet werden soll.
ConversationConfig
ConversationConfig wird verwendet, um eine
Conversation-Instanz zu initialisieren. Sie können diese Konfiguration auf verschiedene Arten erstellen:
- Aus einer
Engine: Bei dieser Methode wird die Standard-SessionConfigverwendet, die mit der Engine verknüpft ist. - Aus einer bestimmten
SessionConfig: So haben Sie eine genauere Kontrolle über die Sitzungseinstellungen.
Neben den Sitzungseinstellungen können Sie das
Conversation Verhalten in der
ConversationConfig weiter anpassen. Dazu gehören:
- Bereitstellen eines
Preface. - Überschreiben der Standard-
PromptTemplate - Überschreiben der Standard-
DataProcessorConfig
Diese Überschreibungen sind besonders nützlich für fein abgestimmte Modelle, die möglicherweise andere Konfigurationen oder Promptvorlagen als das Basismodell benötigen, von dem sie abgeleitet wurden.
MessageCallback
MessageCallback ist die Callback-Funktion, die Nutzer implementieren sollten, wenn sie die asynchrone Methode SendMessageAsync verwenden.
Die Callback-Signatur ist absl::AnyInvocable<void(absl::StatusOr<Message>)>.
Diese Funktion wird unter den folgenden Bedingungen ausgelöst:
- Wenn ein neuer Chunk der
Messagevom Modell empfangen wird. - Wenn bei der Nachrichtenverarbeitung von LiteRT-LM ein Fehler auftritt.
- Nach Abschluss der Inferenz des LLM wird der Callback mit einer
leeren
Message(z.B.JsonMessage()) ausgelöst, um das Ende der Antwort zu signalisieren.
Ein Beispiel für die Implementierung finden Sie unter Schritt 6: Asynchroner Aufruf.
Hinweis: Die
Message
von der Callback-Funktion empfangene Nachricht enthält nur den letzten Chunk der Modellausgabe,
nicht den gesamten Nachrichtenverlauf.
Wenn die vollständige Modellantwort, die von einem blockierenden
SendMessage-Aufruf erwartet wird, beispielsweise so aussieht:
{
"role": "model",
"content": [
"type": "text",
"text": "Hello World!"
]
}
Der Callback in SendMessageAsync kann mehrmals aufgerufen werden, jeweils mit einem nachfolgenden Teil des Texts:
// 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!"
]
}
Der Implementierer ist dafür verantwortlich, diese Chunks zu sammeln, wenn die vollständige Antwort während des asynchronen Streams benötigt wird. Alternativ ist die vollständige
Antwort als letzter Eintrag in der History verfügbar, sobald
der asynchrone Aufruf abgeschlossen ist.
Erweiterte Verwendung {#advanced-usage}
Eingeschränkte Decodierung
LiteRT-LM unterstützt die eingeschränkte Decodierung, mit der Sie bestimmte Strukturen für die Ausgabe des Modells erzwingen können, z. B. JSON-Schemas, reguläre Ausdrücke oder Grammatikregeln.
Setzen Sie dazu EnableConstrainedDecoding(true) in ConversationConfig und geben Sie eine ConstraintProviderConfig an (z.B. LlGuidanceConfig für die Unterstützung von regulären Ausdrücken, JSON und Grammatik). Übergeben Sie dann Einschränkungen über OptionalArgs in SendMessage.
Beispiel: Regulärer Ausdruck
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}
);
Ausführliche Informationen, einschließlich der Unterstützung für JSON-Schemas und Lark-Grammatik, finden Sie in der Dokumentation zur eingeschränkten Decodierung.
Tool-Nutzung
Mit dem Tool-Aufruf kann das LLM die Ausführung von clientseitigen Funktionen anfordern. Sie definieren Tools im Preface der Unterhaltung und geben ihnen einen Namen. Wenn das Modell einen Tool-Aufruf ausgibt, erfassen Sie ihn, führen die entsprechende Funktion in Ihrer Anwendung aus und geben das Ergebnis an das Modell zurück.
Ablauf auf hoher Ebene :
- Tools deklarieren:Definieren Sie Tools (Name, Beschreibung, Parameter) im
Preface-JSON. - Aufrufe erkennen: Prüfen Sie
model_message["tool_calls"]in der Antwort. - Ausführen:Führen Sie die Anwendungslogik für das angeforderte Tool aus.
- Antworten: Senden Sie eine Nachricht mit
role: "tool"zurück an das Modell, die die Ausgabe des Tools enthält.
Ausführliche Informationen und ein vollständiges Beispiel für eine Chat-Schleife finden Sie in der Dokumentation zur Tool-Nutzung.