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 או במבנה ההודעות של 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",
}
תבנית הנחיה
כדי לשמור על גמישות במודלים של וריאנטים, PromptTemplate מיושם כעטיפה דקה סביב Minja. Minja היא הטמעה של C++ של מנוע התבניות Jinja, שמבצע עיבוד של קלט 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ההודעות בהקדמה. ההודעות סיפקו את הרקע הראשוני לשיחה. לדוגמה, ההודעות יכולות להיות היסטוריית השיחות, הוראות מערכת להנדסת הנחיות, דוגמאות של למידה עם מעט דוגמאות וכו'.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 Session הוא מודל עם שמירת מצב, כלומר הוא מעבד את הקלט באופן מצטבר. כדי לגשר על הפער הזה, 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: קריאה אסינכרונית.
[!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, דפוסי 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. Detect Calls: בודקים אם התשובה היא model_message["tool_calls"].
3. ביצוע: הפעלת הלוגיקה של האפליקציה עבור הכלי המבוקש.
4. תגובה: שליחת הודעה עם role: "tool" שמכילה את הפלט של הכלי בחזרה למודל.
פרטים מלאים ודוגמה מלאה של לולאת צ'אט זמינים במסמכי התיעוד בנושא שימוש בכלים.