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. API này 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ì bối 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 lời nhắc Jinja với định dạng thông báo dựa trên vai trò.

Quy trình làm việc của Conversation API

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

  1. Tạo 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 nặng chứa trọng số 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, 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 sử dụng cách này cho hầu hết các trường hợp sử dụng. Cách này phản ánh Gemini Chat API.

  • SendMessage: Một lệnh gọi chặn nhận hoạt động đầ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 phản hồi của mô hình theo từng mã thông báo thông qua lệnh gọi lại.

Dưới đâ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 Conversation với Công cụ

Tham khảo phần 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 Conversation

Conversation có thể được coi là một đại diện để 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.

Loại I/O

Định dạng đầu vào và đầu ra cốt lõi cho Conversation API là Message. Hiện tại, định dạng 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 ghép linh hoạt.

API Conversation hoạt động dựa trên cơ sở tin nhắn vào-tin nhắn ra, 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 vào các trường tuỳ ý theo yêu cầu của các mẫu lời nhắc hoặc mô hình LLM cụ thể, cho phép LiteRT-LM hỗ trợ nhiều mô hình.

Mặc dù không có một tiêu chuẩn cứng nhắc duy nhất, nhưng hầu hết các mẫu và mô hình lời nhắc mong đợi Message tuân theo các quy ước tương tự như các quy ước được sử dụng trong Nội dung Gemini API hoặc cấu trúc Tin nhắn OpenAI.

Message phải chứa role, đại diện cho 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à một danh sách part. Một lần nữa part không phải là cấu trúc dữ liệu được xác định trước mà là một loại dữ liệu cặp khoá-giá trị được sắp xếp. Các trường cụ thể phụ thuộc vào những gì mẫu lời nhắc 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 đa phương thức part, 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",
}

Prompt Template

Để 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 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 lời nhắc được định dạng.

Công cụ mẫu Jinja là một định dạng được áp dụng rộng rãi cho các mẫu lời nhắc 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 điều chỉnh theo hướng dẫn mong đợi. Thông thường, các bản phát hành mô hình 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ẽ được cung cấp bởi siêu dữ liệu tệp mô hình.

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ể của 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 tính năng giả mạo trong Thiết kế lời nhắc hoặc: Cách tôi học được cách bắt đầu lo lắng về định dạng lời nhắc

Lời nói đầu

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

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

  • messages Các tin nhắn trong lời nói đầu. Các tin nhắn cung cấp thông tin nền ban đầu cho cuộc trò chuyện. Ví dụ: các tin nhắn có thể là nhật ký cuộc trò chuyện, hướng dẫn hệ thống thiết kế câu lệnh, ví dụ về few-shot, 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ụ 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 cần thiết nhằm bắt đầu cuộc trò chuyện. Ví dụ:

    • enable_thinking cho các mô hình có chế độ suy nghĩ, chẳng hạn như Qwen3 hoặc SmolLM3-3B.

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

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 trao đổi Tin nhắn trong phiên. Nhật ký này rất quan trọng đối với việc hiển thị mẫu lời nhắc, vì mẫu lời nhắc jinja thường yêu cầu toàn bộ nhật ký cuộc trò chuyện để tạo lời nhắc 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ý dữ liệu đầu vào theo từng bước. Để thu hẹp khoảng cách này, Conversation tạo lời nhắc gia tăng cần thiết bằng cách hiển thị mẫu lời nhắc 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 tin nhắn hiện tại. Bằng cách so sánh hai lời nhắc được hiển thị 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 Conversation thực thể. Bạn có thể tạo cấu hình này theo một vài 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ể: Điều này cho phép kiểm soát chi tiết hơn đối với các chế độ cài đặt phiên.

Ngoài các 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 ghi đè này đặc biệt hữu ích cho 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 lời nhắc khác với mô hình cơ sở mà chúng được lấy từ đó.

MessageCallback

MessageCallback là hàm callback 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ý gọi lại là absl::AnyInvocable<void(absl::StatusOr<Message>)>. Hàm này được kích hoạt trong các điều kiện 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ý tin nhắn của LiteRT-LM.
  • Sau khi hoàn tất quá trình suy luận của LLM, lệnh gọi lại sẽ được kích hoạt bằng một trống Message (ví dụ: JsonMessage()) để báo hiệu kết thúc phản hồi.

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

Lưu ý: Message mà lệnh gọi lại nhận được chỉ chứa đoạn mới nhất của đầu ra 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ừ lệnh gọi chặn SendMessage 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 đoạn này nếu cần phản hồi hoàn chỉnh 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 nhập cuối cùng trong History sau khi hoàn tất lệnh gọi không đồng bộ.

Sử dụng nâng cao {#advanced-usage}

Giải mã bị ràng buộc

LiteRT-LM hỗ trợ giải mã bị 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ư lược đồ JSON, mẫu biểu thức chính quy 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 ConstraintProviderConfig (ví dụ: LlGuidanceConfig để hỗ trợ biểu thức chính quy/JSON/ngữ pháp). Sau đó, hãy truyền các ràng buộc thông qua OptionalArgs trong SendMessage.

Ví dụ: Ràng buộc biểu thức chính quy

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 chi tiết đầy đủ, bao gồm cả tính năng hỗ trợ Lược đồ JSON và Ngữ pháp Lark, hãy xem tài liệu Giải mã bị ràng buộc.

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 khoá cho các công cụ đó theo tên. Khi mô hình xuấ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 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ác công cụ (tên, nội dung mô tả, tham số) trong JSON Preface.
  2. Phát hiện lệnh gọi: Kiểm tra model_message["tool_calls"] trong phản hồi.
  3. Thực thi: Chạy logic ứng dụng cho công cụ được yêu cầu.
  4. Phản hồi: Gửi một tin nhắn có role: "tool" chứa đầu ra của công cụ trở lại mô hình.

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