LiteRT-LM 교차 플랫폼 C++ API

Conversation은 LLM과의 단일 상태 저장 대화를 나타내는 상위 수준 API이며 대부분의 사용자에게 권장되는 진입점입니다. 내부적으로 Session를 관리하고 복잡한 데이터 처리 작업을 처리합니다. 이러한 작업에는 초기 컨텍스트 유지, 도구 정의 관리, 멀티모달 데이터 사전 처리, 역할 기반 메시지 형식을 사용하여 Jinja 프롬프트 템플릿 적용이 포함됩니다.

Conversation API 워크플로

Conversation API 사용의 일반적인 수명 주기는 다음과 같습니다.

  1. Engine 만들기: 모델 경로와 구성으로 단일 Engine를 초기화합니다. 모델 가중치를 보유하는 헤비급 객체입니다.
  2. Conversation 만들기: Engine를 사용하여 하나 이상의 경량 Conversation 객체를 만듭니다.
  3. 메시지 보내기: Conversation 객체의 메서드를 활용하여 LLM에 메시지를 보내고 응답을 받아 채팅과 유사한 상호작용을 효과적으로 지원합니다.

아래는 메시지를 보내고 모델 응답을 받는 가장 간단한 방법입니다. 대부분의 사용 사례에 권장됩니다. Gemini Chat API를 미러링합니다.

  • SendMessage: 사용자 입력을 가져와 완전한 모델 응답을 반환하는 차단 호출입니다.
  • SendMessageAsync: 콜백을 통해 모델의 응답을 토큰별로 다시 스트리밍하는 비차단 호출입니다.

다음은 코드 스니펫의 예입니다.

텍스트 전용 콘텐츠

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

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

멀티모달 데이터 콘텐츠

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

도구를 사용한 대화

Conversation API를 사용한 자세한 도구 사용법은 고급 사용법을 참고하세요.

대화의 구성요소

Conversation은 사용자가 세션에 데이터를 보내기 전에 Session 및 복잡한 데이터 처리를 유지하는 대리인으로 간주될 수 있습니다.

I/O 유형

Conversation API의 핵심 입력 및 출력 형식은 Message입니다. 현재 이는 유연한 중첩 키-값 데이터 구조인 ordered_json의 유형 별칭인 JsonMessage로 구현됩니다.

Conversation API는 일반적인 채팅 환경을 모방하여 메시지 입력-메시지 출력 방식으로 작동합니다. Message의 유연성을 통해 사용자는 특정 프롬프트 템플릿이나 LLM 모델에 필요한 임의의 필드를 포함할 수 있으므로 LiteRT-LM이 다양한 모델을 지원할 수 있습니다.

단일한 엄격한 표준은 없지만 대부분의 프롬프트 템플릿과 모델은 MessageGemini API 콘텐츠 또는 OpenAI 메시지 구조에서 사용되는 것과 유사한 규칙을 따를 것으로 예상합니다.

Message에는 메시지를 보낸 사람을 나타내는 role이 포함되어야 합니다. content는 텍스트 문자열만큼 간단할 수 있습니다.

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

멀티모달 데이터 입력의 경우 contentpart 목록입니다. part는 미리 정의된 데이터 구조가 아니라 정렬된 키-값 쌍 데이터 유형입니다. 구체적인 필드는 프롬프트 템플릿과 모델이 기대하는 바에 따라 다릅니다.

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

멀티모달 part의 경우 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",
}

프롬프트 템플릿

변형 모델의 유연성을 유지하기 위해 PromptTemplateMinja를 래핑하는 얇은 래퍼로 구현됩니다. Minja는 JSON 입력을 처리하여 형식이 지정된 프롬프트를 생성하는 Jinja 템플릿 엔진의 C++ 구현입니다.

Jinja 템플릿 엔진은 LLM 프롬프트 템플릿에 널리 사용되는 형식입니다. 예를 들면 다음과 같습니다.

Jinja 템플릿 엔진 형식은 명령어로 조정된 모델에서 예상하는 구조와 엄격하게 일치해야 합니다. 일반적으로 모델 출시에는 적절한 모델 사용을 보장하기 위해 표준 Jinja 템플릿이 포함됩니다.

모델에서 사용하는 Jinja 템플릿은 모델 파일 메타데이터에 의해 제공됩니다.

[!NOTE] 형식이 잘못되어 프롬프트가 미묘하게 변경되면 모델 성능이 크게 저하될 수 있습니다. 프롬프트 설계에서 가짜 기능에 대한 언어 모델의 민감도 정량화 또는 프롬프트 형식에 대해 걱정하기 시작한 방법에 보고된 바와 같이

머리말

Preface은 대화의 초기 컨텍스트를 설정합니다. 여기에는 초기 메시지, 도구 정의, LLM이 상호작용을 시작하는 데 필요한 기타 배경 정보가 포함될 수 있습니다. 이렇게 하면 Gemini API system instructionGemini API Tools과 유사한 기능을 구현할 수 있습니다.

서문에는 다음 필드가 포함됩니다.

  • messages 서문의 메시지 메일은 대화의 초기 배경을 제공했습니다. 예를 들어 메시지는 대화 기록, 프롬프트 엔지니어링 시스템 안내, 퓨샷 예시 등이 될 수 있습니다.

  • tools 모델이 대화에서 사용할 수 있는 도구입니다. 도구 형식은 다시 고정되지 않지만 대부분 Gemini API FunctionDeclaration을 따릅니다.

  • extra_context 모델이 대화를 시작하는 데 필요한 컨텍스트 정보를 맞춤설정할 수 있도록 지원하는 추가 컨텍스트입니다. 예를 들면 다음과 같습니다.

    • enable_thinking: 사고 모드가 있는 모델(예: Qwen3 또는 SmolLM3-3B)

초기 시스템 요청 사항, 도구를 제공하고 사고 모드를 사용 중지하는 서문의 예

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

기록

Conversation은 세션 내의 모든 Message 교환 목록을 유지합니다. 이 기록은 프롬프트 템플릿 렌더링에 매우 중요합니다. 일반적으로 jinja 프롬프트 템플릿은 LLM에 적합한 프롬프트를 생성하기 위해 전체 대화 기록이 필요하기 때문입니다.

하지만 LiteRT-LM 세션은 상태 저장 방식이므로 입력을 증분 방식으로 처리합니다. 이 격차를 해소하기 위해 대화는 프롬프트 템플릿을 두 번 렌더링하여 필요한 증분 프롬프트를 생성합니다. 한 번은 이전 턴까지의 기록으로, 한 번은 현재 메시지를 포함하여 렌더링합니다. 렌더링된 이 두 프롬프트를 비교하여 세션으로 전송할 새 부분을 추출합니다.

ConversationConfig

ConversationConfigConversation 인스턴스를 초기화하는 데 사용됩니다. 이 구성은 다음과 같은 방법으로 만들 수 있습니다.

  1. Engine에서: 이 메서드는 엔진과 연결된 기본 SessionConfig를 사용합니다.
  2. 특정 SessionConfig에서: 이렇게 하면 세션 설정을 더 세부적으로 제어할 수 있습니다.

세션 설정 외에도 ConversationConfig 내에서 Conversation 동작을 추가로 맞춤설정할 수 있습니다. 여기에는 다음이 포함됩니다.

이러한 덮어쓰기는 파인 튜닝된 모델에 특히 유용합니다. 파인 튜닝된 모델은 파생된 기본 모델과 다른 구성이나 프롬프트 템플릿이 필요할 수 있기 때문입니다.

MessageCallback

MessageCallback는 사용자가 비동기 SendMessageAsync 메서드를 사용할 때 구현해야 하는 콜백 함수입니다.

콜백 서명은 absl::AnyInvocable<void(absl::StatusOr<Message>)>입니다. 이 함수는 다음 조건에서 트리거됩니다.

  • 모델에서 Message의 새 청크가 수신될 때
  • LiteRT-LM의 메시지 처리 중에 오류가 발생한 경우
  • LLM의 추론이 완료되면 빈 Message (예: JsonMessage())를 사용하여 대답의 끝을 알립니다.

구현 예시는 6단계 비동기 호출을 참고하세요.

[!IMPORTANT] 콜백에서 수신한 Message에는 전체 메시지 기록이 아닌 모델 출력의 최신 청크만 포함됩니다.

예를 들어 차단 SendMessage 호출에서 예상되는 전체 모델 응답이 다음과 같은 경우:

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

SendMessageAsync의 콜백은 여러 번 호출될 수 있으며, 각 호출마다 후속 텍스트가 전달됩니다.

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

비동기 스트림 중에 전체 응답이 필요한 경우 구현자는 이러한 청크를 누적해야 합니다. 또는 비동기 호출이 완료되면 전체 응답이 History의 마지막 항목으로 제공됩니다.

고급 사용

제약 조건이 있는 디코딩

LiteRT-LM은 제한된 디코딩을 지원하므로 JSON 스키마, 정규식 패턴, 문법 규칙과 같은 특정 구조를 모델의 출력에 적용할 수 있습니다.

사용 설정하려면 ConversationConfig에서 EnableConstrainedDecoding(true)를 설정하고 ConstraintProviderConfig (예: LlGuidanceConfig(정규식/JSON/문법 지원) 그런 다음 SendMessageOptionalArgs을 통해 제약 조건을 전달합니다.

예: 정규식 제약 조건

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

JSON 스키마 및 Lark 문법 지원을 비롯한 자세한 내용은 제약 조건이 있는 디코딩 문서를 참고하세요.

도구 사용

도구 호출을 사용하면 LLM이 클라이언트 측 함수의 실행을 요청할 수 있습니다. 대화의 Preface에서 도구를 정의하고 이름으로 키를 지정합니다. 모델이 도구 호출을 출력하면 이를 캡처하고 애플리케이션에서 해당 함수를 실행한 후 결과를 모델에 반환합니다.

개략적인 흐름: 1. 도구 선언: Preface JSON에서 도구 (이름, 설명, 파라미터)를 정의합니다. 2. 통화 감지: 대답에서 model_message["tool_calls"]를 확인합니다. 3. 실행: 요청된 도구의 애플리케이션 로직을 실행합니다. 4. 대답: 도구의 출력이 포함된 role: "tool"로 모델에 다시 메시지를 보냅니다.

자세한 내용과 전체 채팅 루프 예시는 도구 사용 문서를 참고하세요.