LiteRT-LM Cross-Platform C++ API

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:

  1. Utwórz Engine: zainicjuj pojedynczy element Engine za pomocą ścieżki modelu i konfiguracji. Jest to obiekt o dużej wadze, który zawiera wagi modelu.
  2. Utwórz Conversation: użyj Engine, aby utworzyć co najmniej 1 lekki obiekt Conversation.
  3. 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

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

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:

  • messages Wiadomoś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.

  • tools Narzę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 formatem Gemini API FunctionDeclaration.

  • extra_context Dodatkowy kontekst, który zapewnia modelom możliwość dostosowywania wymaganych informacji kontekstowych w celu rozpoczęcia rozmowy. Przykłady:

    • enable_thinking w 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:

  1. Engine: ta metoda korzysta z domyślnego SessionConfig powiązanego z silnikiem.
  2. 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.:

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)ConversationConfig i podaj ConstraintProviderConfig (np. LlGuidanceConfig w przypadku obsługi wyrażeń regularnych, JSON-a i gramatyki). Następnie przekaż ograniczenia za pomocą OptionalArgsSendMessage.

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.