Conversation to interfejs API wysokiego poziomu, który reprezentuje pojedynczą rozmowę z modelem LLM z zachowaniem stanu. Jest to zalecany punkt wejścia dla większości użytkowników. Wewnętrznie zarządza Session i wykonuje złożone zadania przetwarzania danych. Obejmują one zachowanie początkowego kontekstu, zarządzanie definicjami narzędzi, wstępne przetwarzanie danych multimodalnych i stosowanie szablonów promptów Jinja z formatowaniem wiadomości opartym na rolach.
Przepływ pracy Conversation API
Typowy cykl życia korzystania z interfejsu Conversation API wygląda tak:
- Utwórz
Engine: zainicjuj pojedynczy elementEngineza pomocą ścieżki modelu i konfiguracji. Jest to obiekt o dużej wadze, który zawiera wagi modelu. - Utwórz
Conversation: użyjEngine, aby utworzyć co najmniej 1 lekki obiektConversation. - Wyślij wiadomość: użyj metod obiektu
Conversation, aby wysyłać wiadomości do modelu LLM i otrzymywać odpowiedzi, co umożliwia interakcję podobną do czatu.
Poniżej znajdziesz najprostszy sposób wysyłania wiadomości i uzyskiwania odpowiedzi od modelu. Jest on zalecany w większości przypadków. Jest on podobny do interfejsów Gemini Chat API.
SendMessage: wywołanie blokujące, które przyjmuje dane wejściowe użytkownika i zwraca pełną odpowiedź modelu.SendMessageAsync: wywołanie nieblokujące, które przesyła strumieniowo odpowiedź modelu token po tokenie za pomocą wywołań zwrotnych.
Oto przykładowy fragment kodu:
Treści zawierające tylko tekst
#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));
Przykład 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();
}
Treści danych multimodalnych
// 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;
Korzystanie z funkcji Rozmowa z narzędziami
Szczegółowe informacje o korzystaniu z narzędzia z interfejsem Conversation API znajdziesz w sekcji Zaawansowane użycie.
Komponenty w Rozmowach
Conversation może być traktowany jako pełnomocnik użytkowników do utrzymywania Session i złożonego przetwarzania danych przed wysłaniem danych do sesji.
Typy wejść/wyjść
Podstawowym formatem wejściowym i wyjściowym interfejsu Conversation API jest Message. Obecnie jest to zaimplementowane jako JsonMessage, czyli alias typu dla ordered_json, elastycznej zagnieżdżonej struktury danych klucz-wartość.
Interfejs API Conversation działa na zasadzie „wiadomość przychodząca – wiadomość wychodząca”, co przypomina typowy czat. Elastyczność Message umożliwia użytkownikom uwzględnianie dowolnych pól w zależności od potrzeb konkretnych szablonów promptów lub modeli LLM, dzięki czemu LiteRT-LM obsługuje szeroką gamę modeli.
Nie ma jednego sztywnego standardu, ale większość szablonów promptów i modeli Message oczekuje, że będą one zgodne z konwencjami podobnymi do tych, które są używane w treściach interfejsu Gemini API lub strukturze wiadomości OpenAI.
Message musi zawierać role, czyli informację o tym, kto wysłał wiadomość. content może być prostym ciągiem tekstowym.
{
"role": "model", // Represent who the message is sent from.
"content": "Hello World!" // Naive text only content.
}
W przypadku danych multimodalnych content to lista part. part nie jest predefiniowaną strukturą danych, ale uporządkowanym typem danych w postaci pary klucz-wartość. Konkretne pola zależą od tego, czego oczekuje szablon prompta i model.
{
"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"
}
]
}
W przypadku trybu multimodalnego part obsługujemy ten format obsługiwany przez
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",
}
Szablon prompta
Aby zachować elastyczność w przypadku modeli wariantowych, PromptTemplate jest implementowany jako cienka otoczka wokół Minji. Minja to implementacja w C++ silnika szablonów Jinja, który przetwarza dane wejściowe w formacie JSON, aby generować sformatowane prompty.
Silnik szablonów Jinja to powszechnie stosowany format szablonów promptów LLM. Oto kilka przykładów:
Format silnika szablonów Jinja musi ściśle odpowiadać strukturze oczekiwanej przez model dostrojony pod kątem instrukcji. Zwykle wersje modeli zawierają standardowy szablon Jinja, aby zapewnić prawidłowe korzystanie z modelu.
Szablon Jinja używany przez model będzie podany w metadanych pliku modelu.
[!NOTE] Niewielka zmiana promptu spowodowana nieprawidłowym formatowaniem może prowadzić do znacznego pogorszenia jakości modelu. Jak podano w artykule Quantifying Language Models' Sensitivity to Spurious Features in Prompt Design or: How I learned to start worrying about prompt formatting
Wstęp
Preface ustawia początkowy kontekst rozmowy. Może on zawierać początkowe wiadomości, definicje narzędzi i inne informacje, których model LLM potrzebuje do rozpoczęcia interakcji. Działa podobnie do funkcji Gemini API system instruction i Gemini API Tools.
Przedmowa zawiera te pola:
messagesWiadomości w przedmowie. Wiadomości te stanowiły początkowe tło rozmowy. Mogą to być na przykład historia rozmowy, instrukcje systemu inżynierii promptów, przykłady few-shot itp.toolsNarzędzia, których model może używać w rozmowie. Format narzędzi ponownie nie jest ustalony, ale w większości przypadków jest zgodny z formatemGemini API FunctionDeclaration.extra_contextDodatkowy kontekst, który zapewnia modelom możliwość dostosowywania wymaganych informacji kontekstowych w celu rozpoczęcia rozmowy. Przykłady:enable_thinkingw przypadku modeli z trybem myślenia, np. Qwen3 lub SmolLM3-3B.
Przykładowy wstęp zawierający początkowe instrukcje systemowe, narzędzia i wyłączający tryb myślenia.
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}
}
});
Historia
Rozmowa zawiera listę wszystkich wymian wiadomości w ramach sesji. Ta historia jest kluczowa w przypadku renderowania szablonu prompta, ponieważ szablon prompta Jinja zwykle wymaga całej historii rozmowy, aby wygenerować prawidłowy prompt dla LLM-u.
Model LiteRT-LM Session jest jednak stanowy, co oznacza, że przetwarza dane wejściowe przyrostowo. Aby to osiągnąć, Conversation generuje niezbędny przyrostowy prompt, renderując szablon promptu 2 razy: raz z historią do poprzedniej tury, a raz z uwzględnieniem bieżącej wiadomości. Porównując te 2 wyrenderowane prompty, wyodrębnia nową część, która ma zostać wysłana do sesji.
ConversationConfig
ConversationConfig służy do inicjowania instancji Conversation. Tę konfigurację możesz utworzyć na kilka sposobów:
- Z
Engine: ta metoda korzysta z domyślnegoSessionConfigpowiązanego z silnikiem. - Z określonego
SessionConfig: umożliwia to bardziej precyzyjną kontrolę nad ustawieniami sesji.
Oprócz ustawień sesji możesz dodatkowo dostosować działanie Conversation w ramach ConversationConfig. Obejmuje to m.in.:
- Podaj
Preface. - Zastępowanie domyślnego
PromptTemplate. - Zastępowanie domyślnego
DataProcessorConfig.
Te zastąpienia są szczególnie przydatne w przypadku dostrojonych modeli, które mogą wymagać innych konfiguracji lub szablonów promptów niż model podstawowy, z którego pochodzą.
MessageCallback
MessageCallback to funkcja wywołania zwrotnego, którą użytkownicy powinni zaimplementować, gdy korzystają z asynchronicznej metody SendMessageAsync.
Sygnatura wywołania zwrotnego to absl::AnyInvocable<void(absl::StatusOr<Message>)>.
Ta funkcja jest aktywowana w tych warunkach:
- Gdy z modelu zostanie odebrany nowy fragment
Message. - Jeśli podczas przetwarzania wiadomości przez LiteRT-LM wystąpi błąd.
- Po zakończeniu wnioskowania przez LLM wywoływane jest wywołanie zwrotne z pustym
Message(np.JsonMessage()), aby zasygnalizować koniec odpowiedzi.
Przykład implementacji znajdziesz w kroku 6 dotyczącym wywołania asynchronicznego.
[!IMPORTANT] Funkcja zwrotna otrzymuje tylko
Message, który zawiera tylko najnowszy fragment danych wyjściowych modelu, a nie całą historię wiadomości.
Jeśli na przykład pełna odpowiedź modelu oczekiwana w przypadku wywołania blokującego SendMessage to:
{
"role": "model",
"content": [
"type": "text",
"text": "Hello World!"
]
}
Wywołanie zwrotne w funkcji SendMessageAsync może być wywoływane wielokrotnie, za każdym razem z kolejnym fragmentem tekstu:
// 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!"
]
}
Jeśli podczas strumienia asynchronicznego potrzebna jest pełna odpowiedź, osoba wdrażająca musi gromadzić te fragmenty. Pełna odpowiedź będzie dostępna jako ostatni wpis w History po zakończeniu wywołania asynchronicznego.
Zaawansowane użycie
Dekodowanie z ograniczeniami
LiteRT-LM obsługuje dekodowanie z ograniczeniami, co pozwala wymuszać określone struktury danych wyjściowych modelu, takie jak schematy JSON, wzorce wyrażeń regularnych lub reguły gramatyczne.
Aby ją włączyć, ustaw EnableConstrainedDecoding(true) w ConversationConfig i podaj ConstraintProviderConfig (np. LlGuidanceConfig w przypadku obsługi wyrażeń regularnych, JSON-a i gramatyki). Następnie przekaż ograniczenia za pomocą OptionalArgs w SendMessage.
Przykład: ograniczenie wyrażenia regularnego
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}
);
Szczegółowe informacje, w tym obsługę schematu JSON i gramatyki Lark, znajdziesz w dokumentacji dotyczącej dekodowania z ograniczeniami.
Korzystanie z narzędzia
Wywoływanie narzędzi umożliwia LLM żądanie wykonania funkcji po stronie klienta. Narzędzia definiujesz w Preface rozmowy, przypisując je do nazw. Gdy model wygeneruje wywołanie narzędzia, przechwyć je, wykonaj odpowiednią funkcję w aplikacji i zwróć wynik do modelu.
Przepływ zadań wysokiego poziomu:
1. Zadeklaruj narzędzia: zdefiniuj narzędzia (nazwę, opis, parametry) w pliku JSON Preface.
2. Wykrywanie połączeń: sprawdź, czy w odpowiedzi jest model_message["tool_calls"].
3. Wykonaj: uruchom logikę aplikacji dla żądanego narzędzia.
4. Odpowiedz: wysłanie do modelu wiadomości z symbolem role: "tool" zawierającej wynik działania narzędzia.
Szczegółowe informacje i pełny przykład pętli czatu znajdziesz w dokumentacji dotyczącej korzystania z narzędzi.