Conversation เป็น API ระดับสูงที่แสดงการสนทนาแบบมีสถานะรายการเดียวกับ LLM และเป็นจุดเริ่มต้นที่แนะนำสำหรับผู้ใช้ส่วนใหญ่
โดยจะจัดการ Session ภายในและจัดการงานประมวลผลข้อมูลที่ซับซ้อน
งานเหล่านี้รวมถึงการรักษาบริบทเริ่มต้น การจัดการคำจำกัดความของเครื่องมือ การประมวลผลล่วงหน้าของข้อมูลหลายรูปแบบ และการใช้เทมเพลตพรอมต์ Jinja กับการจัดรูปแบบข้อความตามบทบาท
เวิร์กโฟลว์ของ Conversation API
วงจรการใช้งานทั่วไปของ Conversation API มีดังนี้
- สร้าง
Engine: เริ่มต้นEngineรายการเดียว ด้วยเส้นทางและค่ากำหนดของโมเดล นี่คือออบเจ็กต์ที่มีน้ำหนักมากซึ่ง เก็บน้ำหนักของโมเดล - สร้าง
Conversation: ใช้Engineเพื่อสร้างออบเจ็กต์Conversationขนาดเล็กอย่างน้อย 1 รายการ - ส่งข้อความ: ใช้เมธอดของออบเจ็กต์
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
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();
}
เนื้อหาข้อมูลหลายรูปแบบ
// 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 และการประมวลผลข้อมูลที่ซับซ้อนก่อนส่ง
ข้อมูลไปยัง Session
ประเภท I/O
รูปแบบอินพุตและเอาต์พุตหลักสำหรับ Conversation API คือ
Message ปัจจุบันฟีเจอร์นี้ได้รับการติดตั้งใช้งานเป็น JsonMessage ซึ่งเป็นนามแฝงของประเภทสำหรับ ordered_json ซึ่งเป็นโครงสร้างข้อมูลแบบคีย์-ค่าที่ซ้อนกันแบบยืดหยุ่น
API ของ Conversation จะทำงานแบบข้อความเข้า-ออก
ซึ่งจำลองประสบการณ์การแชททั่วไป ความยืดหยุ่นของ
Message ช่วยให้ผู้ใช้รวมฟิลด์ที่กำหนดเองได้ตามต้องการโดย
เทมเพลตพรอมต์หรือโมเดล LLM ที่เฉพาะเจาะจง ซึ่งช่วยให้ LiteRT-LM รองรับโมเดลได้หลากหลาย
แม้จะไม่มีมาตรฐานที่ตายตัว แต่เทมเพลตพรอมต์และโมเดลส่วนใหญ่
คาดหวังให้ Message เป็นไปตามรูปแบบที่คล้ายกับที่ใช้ในเนื้อหา Gemini API หรือโครงสร้างข้อความของ OpenAI
Message ต้องมี role ซึ่งแสดงถึงผู้ที่ส่งข้อความ
content อาจเป็นสตริงข้อความที่เรียบง่าย
{
"role": "model", // Represent who the message is sent from.
"content": "Hello World!" // Naive text only content.
}
สําหรับอินพุตข้อมูลหลายรูปแบบ content คือรายการของ part อีกครั้งที่ 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",
}
Prompt Template
PromptTemplate ได้รับการติดตั้งใช้งานเป็น Wrapper แบบบางรอบ Minja เพื่อให้โมเดลตัวแปรมีความยืดหยุ่น Minja เป็นการใช้งาน Jinja template engine ใน C++ ซึ่ง ประมวลผลอินพุต JSON เพื่อสร้างพรอมต์ที่จัดรูปแบบ
เครื่องมือเทมเพลต Jinja เป็นรูปแบบที่ใช้กันอย่างแพร่หลายสำหรับเทมเพลตพรอมต์ LLM ลองดูตัวอย่างต่อไปนี้
รูปแบบเครื่องมือเทมเพลต Jinja ควรตรงกับโครงสร้าง ที่โมเดลที่ปรับแต่งตามคำสั่งคาดหวังอย่างเคร่งครัด โดยปกติแล้ว การเผยแพร่โมเดลจะมี เทมเพลต Jinja มาตรฐานเพื่อให้มั่นใจว่ามีการใช้โมเดลอย่างเหมาะสม
ไฟล์โมเดลจะให้ข้อมูลเมตาของเทมเพลต Jinja ที่โมเดลใช้
[!NOTE] การเปลี่ยนแปลงพรอมต์เพียงเล็กน้อยเนื่องจากการจัดรูปแบบไม่ถูกต้องอาจทําให้โมเดลเสื่อมถอยลงอย่างมาก ตามที่รายงานใน Quantifying Language Models' Sensitivity to Spurious Features in Prompt Design or: How I learned to start worrying about prompt formatting
บทนำ
Preface กำหนดบริบทเริ่มต้นสำหรับการสนทนา ซึ่งอาจรวมถึงข้อความเริ่มต้น คำจำกัดความของเครื่องมือ และข้อมูลพื้นฐานอื่นๆ
ที่ LLM ต้องใช้เพื่อเริ่มการโต้ตอบ ซึ่งจะทำให้มีฟังก์ชันการทำงานคล้ายกับ
Gemini API system instruction
และ Gemini API Tools
คำนำประกอบด้วยฟิลด์ต่อไปนี้
messagesข้อความในคำนำ ข้อความดังกล่าวเป็นพื้นฐานเริ่มต้น ของการสนทนา เช่น ข้อความอาจเป็น ประวัติการสนทนา คำสั่งของระบบวิศวกรรมพรอมต์ ตัวอย่างแบบ Few-Shot เป็นต้นtoolsเครื่องมือที่โมเดลใช้ในการสนทนาได้ รูปแบบของเครื่องมือ ยังคงไม่คงที่ แต่ส่วนใหญ่จะใช้รูปแบบGemini API FunctionDeclarationextra_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}
}
});
ประวัติ
การสนทนาจะเก็บรายการข้อความ ทั้งหมดที่แลกเปลี่ยนภายในเซสชัน ประวัติการสนทนานี้มีความสําคัญอย่างยิ่งต่อการแสดงผลเทมเพลตพรอมต์ เนื่องจากโดยปกติแล้วเทมเพลตพรอมต์ Jinja จะต้องมีประวัติการสนทนาทั้งหมดเพื่อสร้างพรอมต์ที่ถูกต้องสําหรับ LLM
อย่างไรก็ตาม Session ของ LiteRT-LM เป็นแบบมีสถานะ ซึ่งหมายความว่าระบบจะประมวลผล อินพุตแบบเพิ่มทีละรายการ การสนทนาจะสร้างข้อความแจ้งเตือนที่จำเป็นเพิ่มเติมโดยการแสดงผลเทมเพลตข้อความแจ้งเตือน 2 ครั้ง ได้แก่ ครั้งหนึ่ง พร้อมประวัติจนถึงรอบก่อนหน้า และอีกครั้งหนึ่งรวมถึงข้อความปัจจุบัน เพื่อลดช่องว่างนี้ การเปรียบเทียบพรอมต์ที่แสดงผล 2 รายการนี้จะดึงส่วนใหม่เพื่อ ส่งไปยังเซสชัน
ConversationConfig
ConversationConfig ใช้เพื่อเริ่มต้นอินสแตนซ์
Conversation คุณสร้างการกำหนดค่านี้ได้ 2 วิธี ดังนี้
- จาก
Engine: วิธีนี้ใช้SessionConfigเริ่มต้นที่เชื่อมโยงกับเครื่องมือ - จาก
SessionConfigที่เฉพาะเจาะจง: วิธีนี้ช่วยให้ควบคุมการตั้งค่าเซสชันได้ละเอียดมากขึ้น
นอกเหนือจากการตั้งค่าเซสชันแล้ว คุณยังปรับแต่งลักษณะการทำงานของ
Conversation ภายใน
ConversationConfig ได้อีกด้วย ซึ่งรวมถึงเนื้อหาต่อไปนี้
- การระบุ
Preface - เขียนทับ
PromptTemplateเริ่มต้น - เขียนทับ
DataProcessorConfigเริ่มต้น
การเขียนทับเหล่านี้มีประโยชน์อย่างยิ่งสำหรับโมเดลที่ปรับแต่งแล้ว ซึ่งอาจ ต้องใช้การกำหนดค่าหรือเทมเพลตพรอมต์ที่แตกต่างจากโมเดลพื้นฐานที่ นำมาใช้
MessageCallback
MessageCallback คือฟังก์ชัน Callback ที่ผู้ใช้ควร
ใช้เมื่อใช้วิธีการ SendMessageAsync แบบไม่พร้อมกัน
ลายเซ็นการเรียกกลับคือ absl::AnyInvocable<void(absl::StatusOr<Message>)>
ฟังก์ชันนี้จะทริกเกอร์ภายใต้เงื่อนไขต่อไปนี้
- เมื่อได้รับ
Messageใหม่จากโมเดล - หากเกิดข้อผิดพลาดระหว่างการประมวลผลข้อความของ LiteRT-LM
- เมื่อการอนุมานของ LLM เสร็จสมบูรณ์ ระบบจะทริกเกอร์การเรียกกลับด้วย
Messageที่ว่างเปล่า (เช่นJsonMessage()) เพื่อส่งสัญญาณสิ้นสุดการตอบกลับ
ดูตัวอย่างการใช้งานได้ที่การเรียกแบบอะซิงโครนัสในขั้นตอนที่ 6
[!IMPORTANT]
Messageที่ได้รับจาก Callback จะมีเฉพาะ เอาต์พุตของโมเดลล่าสุด ไม่ใช่ประวัติข้อความทั้งหมด
เช่น หากการตอบกลับของโมเดลที่สมบูรณ์ซึ่งคาดหวังจากการเรียกใช้การบล็อก
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, รูปแบบ Regex หรือกฎไวยากรณ์
หากต้องการเปิดใช้ ให้ตั้งค่า EnableConstrainedDecoding(true) ใน ConversationConfig และระบุ ConstraintProviderConfig (เช่น LlGuidanceConfig สำหรับการรองรับนิพจน์ทั่วไป/JSON/ไวยากรณ์) จากนั้นส่งข้อจำกัดผ่าน OptionalArgs ใน SendMessage
ตัวอย่าง: ข้อจำกัดนิพจน์ทั่วไป
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" ที่มีเอาต์พุตของเครื่องมือกลับไปยังโมเดล
ดูรายละเอียดทั้งหมดและตัวอย่างลูปแชทที่สมบูรณ์ได้ในเอกสารประกอบการใช้เครื่องมือ