Conversation הוא API ברמה גבוהה שמייצג שיחה יחידה עם LLM, עם שמירת מצב, והוא נקודת הכניסה המומלצת לרוב המשתמשים. הוא מנהל באופן פנימי Session ומבצע משימות מורכבות של עיבוד נתונים. המשימות האלה כוללות שמירה על ההקשר הראשוני, ניהול הגדרות של כלים, עיבוד מוקדם של נתונים מרובי-אופנים והחלה של תבניות הנחיות Jinja עם עיצוב הודעות מבוסס-תפקידים.
Conversation API Workflow
מחזור החיים האופייני של שימוש ב-Conversation API הוא:
- יצירת
Engine: הפעלה שלEngineיחיד עם נתיב המודל וההגדרה. זהו אובייקט כבד שמכיל את משקלי המודל. - יצירת
Conversation: משתמשים בEngineכדי ליצור אובייקטConversationקל משקל אחד או יותר. - Send Message: שימוש בשיטות של אובייקט
Conversationכדי לשלוח הודעות ל-LLM ולקבל תשובות, וכך ליצור אינטראקציה שדומה לצ'אט.
בהמשך מוסבר איך לשלוח הודעה ולקבל תשובה מהמודל. מומלץ להשתמש בו ברוב המקרים. היא משקפת את ממשקי Gemini Chat API.
-
SendMessage: קריאה חוסמת שמקבלת קלט של משתמשים ומחזירה את התגובה המלאה של המודל. -
SendMessageAsync: קריאה לא חוסמת שמעבירה את התשובה של המודל בחזרה באמצעות callback, טוקן אחרי טוקן.
קטע קוד לדוגמה:
תוכן טקסט בלבד
#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 ומבצע עיבוד נתונים מורכב לפני שליחת הנתונים לסשן.
סוגי קלט/פלט
פורמט הקלט והפלט העיקרי של Conversation API הוא Message. בשלב הזה, ההטמעה מתבצעת באמצעות JsonMessage, שהוא כינוי לסוג ordered_json, מבנה נתונים גמיש של צמדי מפתח-ערך מקוננים.
ה-API של Conversation פועל על בסיס הודעה נכנסת והודעה יוצאת, בדומה לחוויית צ'אט רגילה. הגמישות של Message מאפשרת למשתמשים לכלול שדות שרירותיים לפי הצורך בתבניות ספציפיות של הנחיות או במודלים של LLM, וכך מאפשרת ל-LiteRT-LM לתמוך במגוון רחב של מודלים.
אין תקן קשיח אחד, אבל רוב תבניות ההנחיות והמודלים מצפים ש-Message יפעלו לפי מוסכמות דומות לאלה שמשמשות ב-Gemini API Content או ב-OpenAI Message structure.
השדה 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",
}
תבנית הנחיה
כדי לשמור על גמישות במודלים של וריאנטים, PromptTemplate מיושם כעטיפה דקה סביב Minja. Minja היא הטמעה של C++ של מנוע התבניות Jinja, שמטפל בקלט JSON כדי ליצור הנחיות מעוצבות.
מנוע התבניות Jinja הוא פורמט נפוץ לתבניות של הנחיות ל-LLM. הנה כמה דוגמאות:
הפורמט של מנוע התבניות Jinja צריך להתאים בדיוק למבנה שהמודל שעבר כוונון לפי הוראות מצפה לו. בדרך כלל, מהדורות של מודלים כוללות את תבנית Jinja הרגילה כדי להבטיח שימוש נכון במודל.
תבנית Jinja שבה נעשה שימוש במודל תסופק על ידי מטא-נתונים של קובץ המודל.
הערה: שינוי קל בהנחיה בגלל עיצוב לא נכון עלול לגרום לירידה משמעותית באיכות המודל. כפי שדווח במאמר 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
החלונית Preface מכילה את השדות הבאים
messagesההודעות בהקדמה. ההודעות סיפקו את הרקע הראשוני לשיחה. לדוגמה, ההודעות יכולות להיות היסטוריית השיחות, הוראות מערכת להנדסת הנחיות, דוגמאות של למידה עם מעט דוגמאות וכו'.toolsהכלים שהמודל יכול להשתמש בהם בשיחה. הפורמט של 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 שומרת רשימה של כל ההודעות שהוחלפו במהלך הסשן. ההיסטוריה הזו חיונית לעיבוד של תבנית ההנחיה, כי בדרך כלל תבנית ההנחיה של jinja דורשת את כל היסטוריית השיחות כדי ליצור את ההנחיה הנכונה עבור ה-LLM.
עם זאת, מודל הסשן של LiteRT-LM הוא מודל עם שמירת מצב, כלומר הוא מעבד את הקלט באופן מצטבר. כדי לגשר על הפער הזה, Conversation יוצרת את ההנחיה המצטברת הנדרשת על ידי רינדור תבנית ההנחיה פעמיים: פעם אחת עם ההיסטוריה עד לתור הקודם, ופעם אחת כולל ההודעה הנוכחית. על ידי השוואה בין שתי ההנחיות שעברו עיבוד, הוא מחלץ את החלק החדש כדי לשלוח אותו אל הסשן.
ConversationConfig
הפונקציה ConversationConfig משמשת לאתחול של מופע Conversation. יש כמה דרכים ליצור את ההגדרה הזו:
- מ-
Engine: בשיטה הזו נעשה שימוש ב-SessionConfigשמוגדר כברירת מחדל ומשויך למנוע. - מ
SessionConfigספציפי: האפשרות הזו מאפשרת שליטה מפורטת יותר בהגדרות של הביקור.
בנוסף להגדרות הסשן, אפשר להתאים אישית את ההתנהגות של Conversation בתוך ConversationConfig. למשל:
- הוספת
Preface. - החלפת ברירת המחדל
PromptTemplate. - החלפת ברירת המחדל
DataProcessorConfig.
השינויים האלה שימושיים במיוחד במודלים שעברו כוונון עדין, כי יכול להיות שהם ידרשו תצורות שונות או תבניות הנחיה שונות מאלה של מודל הבסיס שממנו הם נגזרו.
MessageCallback
MessageCallback היא פונקציית הקריאה החוזרת שהמשתמשים צריכים להטמיע כשהם משתמשים בשיטת SendMessageAsync אסינכרונית.
החתימה של הקריאה החוזרת היא absl::AnyInvocable<void(absl::StatusOr<Message>)>.
הפונקציה הזו מופעלת בתנאים הבאים:
- כשמתקבל נתח חדש של
Messageמהמודל. - אם מתרחשת שגיאה במהלך עיבוד ההודעה של LiteRT-LM.
- אחרי שההסקה של ה-LLM מסתיימת, מופעלת קריאה חוזרת עם
Messageריק (לדוגמה,JsonMessage()) כדי לסמן את סוף התגובה.
דוגמה להטמעה מופיעה בשלב 6: קריאה אסינכרונית.
הערה: הפונקציה
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 אחרי שהקריאה האסינכרונית תסתיים.
שימוש מתקדם {#advanced-usage}
פענוח מוגבל
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 של השיחה, ומקצים להם שמות. כשהמודל מוציא קריאה לכלי, אתם לוכדים אותה, מפעילים את הפונקציה המתאימה באפליקציה ומחזירים את התוצאה למודל.
תרשים זרימה ברמה גבוהה:
- הצהרה על כלים: מגדירים כלים (שם, תיאור, פרמטרים) בקובץ ה-
PrefaceJSON. - Detect Calls: בודקים אם התשובה היא
model_message["tool_calls"]. - ביצוע: הפעלת הלוגיקה של האפליקציה עבור הכלי המבוקש.
- תגובה: שליחת הודעה עם
role: "tool"שמכילה את הפלט של הכלי בחזרה למודל.
פרטים מלאים ודוגמה מלאה של לולאת צ'אט זמינים במסמכי התיעוד בנושא שימוש בכלים.