API C++ đa nền tảng LiteRT-LM

Conversation là một API cấp cao, đại diện cho một cuộc trò chuyện duy nhất, có trạng thái với LLM và là điểm truy cập được đề xuất cho hầu hết người dùng. Nó quản lý nội bộ một Session và xử lý các tác vụ xử lý dữ liệu phức tạp. Các tác vụ này bao gồm duy trì ngữ cảnh ban đầu, quản lý định nghĩa công cụ, tiền xử lý dữ liệu đa phương thức và áp dụng các mẫu câu lệnh Jinja với định dạng thông báo dựa trên vai trò.

Quy trình API Conversation

Vòng đời điển hình để sử dụng Conversation API là:

  1. Tạo một Engine: Khởi chạy một Engine duy nhất bằng đường dẫn và cấu hình mô hình. Đây là một đối tượng có trọng lượng lớn, lưu giữ trọng số của mô hình.
  2. Tạo Conversation: Sử dụng Engine để tạo một hoặc nhiều đối tượng Conversation đơn giản.
  3. Gửi tin nhắn: Sử dụng các phương thức của đối tượng Conversation để gửi tin nhắn đến LLM và nhận phản hồi, nhờ đó cho phép tương tác giống như trò chuyện một cách hiệu quả.

Dưới đây là cách đơn giản nhất để gửi tin nhắn và nhận phản hồi của mô hình. Bạn nên dùng phương thức này trong hầu hết các trường hợp sử dụng. Nền tảng này phản ánh Gemini Chat API.

  • SendMessage: Một lệnh gọi chặn nhận dữ liệu đầu vào của người dùng và trả về phản hồi hoàn chỉnh của mô hình.
  • SendMessageAsync: Một lệnh gọi không chặn, truyền trực tuyến mã thông báo phản hồi của mô hình theo từng mã thông báo thông qua các lệnh gọi lại.

Sau đây là ví dụ về đoạn mã:

Nội dung chỉ có văn bản

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

Ví dụ 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();
}

Nội dung dữ liệu đa phương thức

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

Sử dụng tính năng Trò chuyện với các công cụ

Vui lòng tham khảo phần Cách sử dụng nâng cao để biết thông tin chi tiết về Cách sử dụng công cụ với Conversation API

Các thành phần trong cuộc trò chuyện

Conversation có thể được coi là một đại biểu cho người dùng để duy trì Session và xử lý dữ liệu phức tạp trước khi gửi dữ liệu đến Phiên.

Các loại I/O

Định dạng đầu vào và đầu ra chính cho Conversation API là Message. Hiện tại, điều này được triển khai dưới dạng JsonMessage, là một bí danh loại cho ordered_json, một cấu trúc dữ liệu khoá-giá trị lồng nhau linh hoạt.

API Conversation hoạt động dựa trên cơ chế gửi tin nhắn và nhận tin nhắn, mô phỏng trải nghiệm trò chuyện thông thường. Tính linh hoạt của Message cho phép người dùng đưa các trường tuỳ ý vào khi cần theo các mẫu câu lệnh hoặc mô hình LLM cụ thể, cho phép LiteRT-LM hỗ trợ nhiều loại mô hình.

Mặc dù không có một tiêu chuẩn duy nhất và cố định, nhưng hầu hết các mẫu và mô hình câu lệnh đều mong đợi Message tuân theo các quy ước tương tự như quy ước được dùng trong Nội dung của Gemini API hoặc Cấu trúc thông báo của OpenAI.

Message phải chứa role, cho biết người gửi tin nhắn. content có thể đơn giản như một chuỗi văn bản.

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

Đối với dữ liệu đầu vào đa phương thức, content là danh sách part. Một lần nữa, part không phải là một cấu trúc dữ liệu được xác định trước mà là một kiểu dữ liệu cặp khoá-giá trị có thứ tự. Các trường cụ thể phụ thuộc vào những gì mà mẫu câu lệnh và mô hình mong đợi.

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

Đối với part đa phương thức, chúng tôi hỗ trợ định dạng sau do data_utils.h xử lý

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

Mẫu câu lệnh

Để duy trì tính linh hoạt cho các mô hình biến thể, PromptTemplate được triển khai dưới dạng một trình bao bọc mỏng xung quanh Minja. Minja là một cách triển khai C++ của công cụ mẫu Jinja, xử lý dữ liệu đầu vào JSON để tạo ra các câu lệnh được định dạng.

Công cụ mẫu Jinja là một định dạng được sử dụng rộng rãi cho các mẫu câu lệnh LLM. Dưới đây là một số ví dụ:

Định dạng công cụ mẫu Jinja phải hoàn toàn khớp với cấu trúc mà mô hình được tinh chỉnh theo hướng dẫn dự kiến. Thông thường, các bản phát hành mô hình sẽ bao gồm mẫu Jinja tiêu chuẩn để đảm bảo sử dụng mô hình đúng cách.

Mẫu Jinja mà mô hình sử dụng sẽ do siêu dữ liệu tệp mô hình cung cấp.

[LƯU Ý] Một thay đổi nhỏ trong câu lệnh do định dạng không chính xác có thể dẫn đến sự suy giảm đáng kể về mô hình. Như đã báo cáo trong Định lượng độ nhạy của mô hình ngôn ngữ đối với các đặc điểm giả tạo trong thiết kế câu lệnh hoặc: Cách tôi học được cách bắt đầu lo lắng về định dạng câu lệnh

Lời nói đầu

Preface đặt bối cảnh ban đầu cho cuộc trò chuyện. Nội dung này có thể bao gồm các thông báo ban đầu, định nghĩa công cụ và mọi thông tin cơ bản khác mà LLM cần để bắt đầu tương tác. Điều này giúp đạt được chức năng tương tự như Gemini API system instructionGemini API Tools

Lời nói đầu chứa các trường sau

  • messages Các thông báo trong phần lời nói đầu. Các tin nhắn này cung cấp bối cảnh ban đầu cho cuộc trò chuyện. Ví dụ: các tin nhắn có thể là nhật ký trò chuyện, hướng dẫn hệ thống kỹ thuật tạo câu lệnh, ví dụ minh hoạ vài lần, v.v.

  • tools Các công cụ mà mô hình có thể sử dụng trong cuộc trò chuyện. Định dạng của các công cụ này cũng không cố định, nhưng hầu hết đều tuân theo Gemini API FunctionDeclaration.

  • extra_context Bối cảnh bổ sung giúp duy trì khả năng mở rộng cho các mô hình để tuỳ chỉnh thông tin bối cảnh bắt buộc nhằm bắt đầu một cuộc trò chuyện. Ví dụ:

    • enable_thinking cho các mô hình có chế độ tư duy, ví dụ: Qwen3 hoặc SmolLM3-3B.

Ví dụ về lời nói đầu để cung cấp hướng dẫn ban đầu cho hệ thống, các công cụ và tắt chế độ tư duy.

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

Cập nhật trước đây

Conversation duy trì danh sách tất cả các lượt trao đổi Message trong phiên. Nhật ký này rất quan trọng đối với việc hiển thị mẫu câu lệnh, vì mẫu câu lệnh jinja thường yêu cầu toàn bộ nhật ký trò chuyện để tạo câu lệnh chính xác cho LLM.

Tuy nhiên, Phiên LiteRT-LM có trạng thái, nghĩa là phiên này xử lý các đầu vào theo gia số. Để khắc phục khoảng trống này, Conversation sẽ tạo câu lệnh gia tăng cần thiết bằng cách hiển thị mẫu câu lệnh hai lần: một lần với nhật ký cho đến lượt trước và một lần bao gồm cả thông báo hiện tại. Bằng cách so sánh hai câu lệnh được kết xuất này, hệ thống sẽ trích xuất phần mới để gửi đến Phiên.

ConversationConfig

ConversationConfig được dùng để khởi chạy một thực thể Conversation. Bạn có thể tạo cấu hình này theo một số cách:

  1. Từ Engine: Phương thức này sử dụng SessionConfig mặc định được liên kết với công cụ.
  2. Từ một SessionConfig cụ thể: Lựa chọn này giúp bạn kiểm soát chế độ cài đặt phiên một cách chi tiết hơn.

Ngoài chế độ cài đặt phiên, bạn có thể tuỳ chỉnh thêm hành vi Conversation trong ConversationConfig. Nội dung như vậy bao gồm:

Các thao tác ghi đè này đặc biệt hữu ích đối với các mô hình được tinh chỉnh, có thể yêu cầu các cấu hình hoặc mẫu câu lệnh khác với mô hình cơ sở mà chúng được lấy từ đó.

MessageCallback

MessageCallback là hàm gọi lại mà người dùng nên triển khai khi sử dụng phương thức SendMessageAsync không đồng bộ.

Chữ ký của lệnh gọi lại là absl::AnyInvocable<void(absl::StatusOr<Message>)>. Hàm này được kích hoạt trong các trường hợp sau:

  • Khi một đoạn Message mới được nhận từ Mô hình.
  • Nếu xảy ra lỗi trong quá trình xử lý thông báo của LiteRT-LM.
  • Sau khi quá trình suy luận của LLM hoàn tất, lệnh gọi lại sẽ được kích hoạt bằng một Message trống (ví dụ: JsonMessage()) để báo hiệu phần cuối của phản hồi.

Hãy tham khảo Lệnh gọi không đồng bộ ở Bước 6 để biết ví dụ về cách triển khai.

[!IMPORTANT] Message mà lệnh gọi lại nhận được chỉ chứa đoạn đầu ra mới nhất của mô hình, chứ không phải toàn bộ nhật ký tin nhắn.

Ví dụ: nếu phản hồi hoàn chỉnh của mô hình dự kiến từ một lệnh gọi SendMessage chặn sẽ là:

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

Lệnh gọi lại trong SendMessageAsync có thể được gọi nhiều lần, mỗi lần với một đoạn văn bản tiếp theo:

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

Người triển khai chịu trách nhiệm tích luỹ các khối này nếu cần phản hồi đầy đủ trong luồng không đồng bộ. Ngoài ra, phản hồi đầy đủ sẽ có sẵn dưới dạng mục cuối cùng trong History sau khi lệnh gọi không đồng bộ hoàn tất.

Cách sử dụng nâng cao

Giải mã bị hạn chế

LiteRT-LM hỗ trợ hoạt động giải mã có ràng buộc, cho phép bạn thực thi các cấu trúc cụ thể trên đầu ra của mô hình, chẳng hạn như giản đồ JSON, mẫu Regex hoặc quy tắc ngữ pháp.

Để bật tính năng này, hãy đặt EnableConstrainedDecoding(true) trong ConversationConfig và cung cấp một ConstraintProviderConfig (ví dụ: LlGuidanceConfig để hỗ trợ biểu thức chính quy/JSON/ngữ pháp). Sau đó, hãy truyền các quy tắc ràng buộc thông qua OptionalArgs trong SendMessage.

Ví dụ: Regex Constraint

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

Để biết thông tin đầy đủ, bao gồm cả hỗ trợ Cú pháp Lark và Sơ đồ JSON, hãy xem tài liệu về Giải mã bị hạn chế.

Sử dụng công cụ

Tính năng gọi công cụ cho phép LLM yêu cầu thực thi các hàm phía máy khách. Bạn xác định các công cụ trong Preface của cuộc trò chuyện, đặt tên cho các công cụ đó. Khi mô hình xuất một lệnh gọi công cụ, bạn sẽ nắm bắt lệnh gọi đó, thực thi hàm tương ứng trong ứng dụng của mình và trả về kết quả cho mô hình.

Luồng cấp cao: 1. Khai báo công cụ: Xác định công cụ (tên, nội dung mô tả, tham số) trong JSON Preface. 2. Phát hiện cuộc gọi: Kiểm tra model_message["tool_calls"] trong câu trả lời. 3. Thực thi: Chạy logic ứng dụng cho công cụ được yêu cầu. 4. Trả lời: Gửi một thông báo có role: "tool" chứa đầu ra của công cụ trở lại mô hình.

Để biết thông tin đầy đủ và ví dụ hoàn chỉnh về vòng lặp trò chuyện, hãy xem tài liệu về việc sử dụng công cụ.