إنّ طلبات النماذج التوليدية هي جوهر ميزات الذكاء الاصطناعي في تطبيقك، ولكن من النادر أن تتمكّن من معالجة إدخال المستخدم وإرساله إلى النموذج وعرض مخرجات النموذج مجددًا على المستخدم. عادةً ما تكون هناك خطوات ما قبل المعالجة وما بعد المعالجة يجب أن تكون مصحوبة باستدعاء النموذج. على سبيل المثال:
- استرداد المعلومات السياقية لإرسالها مع طلب النموذج
- استرداد سجلّ جلسة المستخدم الحالية، على سبيل المثال في أحد تطبيقات المحادثة
- استخدام نموذج واحد لإعادة تنسيق إدخال المستخدم بطريقة مناسبة لتمرير البيانات إلى نموذج آخر
- تقييم "أمان" مخرجات النموذج قبل عرضها على المستخدم
- دمج نتائج عدة نماذج
يجب أن تعمل كل خطوة من خطوات سير العمل هذا معًا لكي تنجح أي مهمة مرتبطة بالذكاء الاصطناعي.
في Genkit، يمكنك تمثيل هذا المنطق المرتبط بدقة باستخدام بنية تُعرف باسم المسار. يتم كتابة مسارات البيانات تمامًا مثل الدوالّ، باستخدام رمز TypeScript عادي، ولكن تضيف إمكانات إضافية تهدف إلى تسهيل تطوير ميزات الذكاء الاصطناعي:
- أمان النوع: مخطّطات الإدخال والإخراج المحدّدة باستخدام Zod، والتي توفّر عمليات التحقّق من النوع أثناء التشغيل والعمليات الثابتة
- الدمج مع واجهة مستخدم المطوّر: يمكنك تصحيح أخطاء عمليات التدفّق بشكل مستقل عن رمز تطبيقك باستخدام واجهة مستخدم المطوّر. في واجهة مستخدم المطوّر، يمكنك تنفيذ عمليات التنقّل وعرض عمليات التتبّع لكل خطوة من المسار.
- النشر المبسّط: يمكنك نشر عمليات المعالجة مباشرةً كنقاط نهاية لواجهة برمجة التطبيقات للويب باستخدام Cloud Functions لمنصة Firebase أو أي منصة يمكنها استضافة تطبيق ويب.
على عكس الميزات المشابهة في إطارات العمل الأخرى، تكون مسارات Genkit خفيفة و غير مزعجة، ولا تفرض على تطبيقك الامتثال لأي تجريدات محدّدة. يتم كتابة كل منطق المسار باستخدام TypeScript العادي، ولا يلزم أن يكون الرمز البرمجي داخل المسار مدركًا للمسار.
تحديد مسارات الإحالات الناجحة والاتّصال بها
في أبسط أشكاله، لا يفعل المسار سوى لف دالة. يلضم المثال التالي
دالة تستدعي generate()
:
export const menuSuggestionFlow = ai.defineFlow(
{
name: 'menuSuggestionFlow',
},
async (restaurantTheme) => {
const { text } = await ai.generate({
model: gemini15Flash,
prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
});
return text;
}
);
من خلال تضمين طلبات generate()
بهذه الطريقة، يمكنك إضافة بعض الوظائف:
يتيح لك ذلك تنفيذ العملية من خلال واجهة برمجة التطبيقات Genkit CLI ومن خلال واجهة مستخدِم المطوّر،
وهو شرط لاستخدام العديد من ميزات Genkit، بما في ذلك عمليات النشر و
إمكانية المراقبة (تتناول الأقسام اللاحقة هذه المواضيع).
مخطّطات الإدخال والإخراج
من أهم مزايا مسارات Genkit مقارنةً بالاتّصال المباشر بواجهة برمجة تطبيقات
للنماذج هي أمان النوع لكل من الإدخالات والمخرجات. عند تحديد مسارات الإحالات الناجحة،
يمكنك تحديد مخططات لها باستخدام Zod، بالطريقة نفسها التي تحدّد بها
مخطّط الإخراج لمكالمة generate()
، ولكن على عكس generate()
، يمكنك
تحديد مخطّط إدخال أيضًا.
في ما يلي تحسين للمثال الأخير الذي يحدّد عملية تتّخذ سلسلة كإدخال وتُخرج عنصرًا:
const MenuItemSchema = z.object({
dishname: z.string(),
description: z.string(),
});
export const menuSuggestionFlowWithSchema = ai.defineFlow(
{
name: 'menuSuggestionFlow',
inputSchema: z.string(),
outputSchema: MenuItemSchema,
},
async (restaurantTheme) => {
const { output } = await ai.generate({
model: gemini15Flash,
prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
output: { schema: MenuItemSchema },
});
if (output == null) {
throw new Error("Response doesn't satisfy schema.");
}
return output;
}
);
يُرجى العلم أنّه ليس من الضروري أن يتطابق مخطّط مسار مع
مخطّط طلبات generate()
ضمن المسار (في الواقع، قد لا يحتوي المسار على
طلبات generate()
). في ما يلي اختلاف على المثال الذي يُرسِل ملف تخطيط إلى generate()
، ولكنه يستخدم الإخراج المنظَّم لتنسيق سلسلة بسيطة يعرضها المسار.
export const menuSuggestionFlowMarkdown = ai.defineFlow(
{
name: 'menuSuggestionFlow',
inputSchema: z.string(),
outputSchema: z.string(),
},
async (restaurantTheme) => {
const { output } = await ai.generate({
model: gemini15Flash,
prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
output: { schema: MenuItemSchema },
});
if (output == null) {
throw new Error("Response doesn't satisfy schema.");
}
return `**${output.dishname}**: ${output.description}`;
}
);
مسارات الاتصال
بعد تعريف مسار، يمكنك استدعاؤه من رمز Node.js:
const { text } = await menuSuggestionFlow('bistro');
يجب أن تكون الوسيطة للمسار متوافقة مع مخطّط الإدخال، إذا كنت قد حدّدت مخطّطًا.
إذا حدّدت مخطّط إخراج، سيتوافق ردّ سير العمل معه. على سبيل المثال، إذا ضبطت مخطّط الإخراج على MenuItemSchema
، سيتضمّن ناتج العملية خصائصه:
const { dishname, description } =
await menuSuggestionFlowWithSchema('bistro');
مصادر البيانات للبث
تتيح "العمليات المتعدّدة" البث باستخدام واجهة مشابهة لواجهة البث
في generate()
. يكون البث مفيدًا عندما يُنشئ مسارك كمية كبيرة من
النتائج، لأنّه يمكنك عرض النتائج للمستخدم أثناء إنشائها، مما يؤدي إلى تحسين الاستجابة التي يلاحظها المستخدم لتطبيقك. على سبيل المثال، غالباً ما تبث واجهات LLM المستندة إلى المحادثات ردودها على المستخدم أثناء تشكلها.
في ما يلي مثال على عملية تتيح البث:
export const menuSuggestionStreamingFlow = ai.defineStreamingFlow(
{
name: 'menuSuggestionFlow',
inputSchema: z.string(),
streamSchema: z.string(),
outputSchema: z.object({ theme: z.string(), menuItem: z.string() }),
},
async (restaurantTheme, streamingCallback) => {
const response = await ai.generateStream({
model: gemini15Flash,
prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
});
if (streamingCallback) {
for await (const chunk of response.stream) {
// Here, you could process the chunk in some way before sending it to
// the output stream via streamingCallback(). In this example, we output
// the text of the chunk, unmodified.
streamingCallback(chunk.text);
}
}
return {
theme: restaurantTheme,
menuItem: (await response.response).text,
};
}
);
- يحدِّد الخيار
streamSchema
نوع القيم التي تبثّها عملية التدفق. ولا يلزم أن يكون هذا النوع هو نفسه النوع الوارد فيoutputSchema
، وهو نوع الإخراج الكامل للمسار. streamingCallback
هي دالة ردّ اتصال تستخدِم مَعلمة واحدة من نوعstreamSchema
. عندما تصبح البيانات متاحة في تدفقك، أرسِل البيانات إلى بث الإخراج من خلال استدعاء هذه الدالة. يُرجى العلم أنّه لا يتم تعريفstreamingCallback
إلا إذا طلب المُتصل بالمسار إخراج البث، لذا عليك التأكّد من أنّه تم تحديده قبل استدعائه.
في المثال أعلاه، يتم ربط القيم التي يتم بثّها من خلال العملية مباشرةً ب
القيم التي يتم بثّها من خلال طلب generate()
داخل العملية. على الرغم من أنّه
يحدث ذلك غالبًا، ليس عليك اتّباع هذا الأسلوب: يمكنك إخراج القيم إلى البث باستخدام
دالة الاستدعاء بقدر ما يناسب سير عملك.
عمليات بث المكالمات
يمكن أيضًا استدعاء عمليات تدفق البث، ولكنها تُرجع على الفور عنصر استجابة بدلاً من وعد:
const response = menuSuggestionStreamingFlow('Danube');
يحتوي عنصر الاستجابة على خاصيّة stream، والتي يمكنك استخدامها للتكرار على الإخراج المباشر للمسار أثناء إنشائه:
for await (const chunk of response.stream) {
console.log('chunk', chunk);
}
يمكنك أيضًا الحصول على الإخراج الكامل للمسار، كما يمكنك من خلال مسار غير مخصّص للبث:
const output = await response.output;
يُرجى العلم أنّ مخرجات البث لأحد عمليات المعالجة قد لا تكون من النوع نفسه المُستخدَم في المخرجات الكاملة، إذ تتوافق مخرجات البث مع streamSchema
، في حين تتوافق المخرجات الكاملة مع outputSchema
.
تشغيل عمليات التنقّل من سطر الأوامر
يمكنك تشغيل مسارات الإحالات الناجحة من سطر الأوامر باستخدام أداة Genkit CLI:
genkit flow:run menuSuggestionFlow '"French"'
بالنسبة إلى عمليات البث، يمكنك طباعة ناتج البث في وحدة التحكّم عن طريق إضافة علامة -s
:
genkit flow:run menuSuggestionFlow '"French"' -s
من المفيد تشغيل عملية من سطر الأوامر لاختبار عملية أو لتشغيل عمليات تؤدي المهام المطلوبة بشكل عشوائي، على سبيل المثال، لتشغيل عملية تنقل مستندًا إلى قاعدة بيانات المتجهات.
تصحيح الأخطاء في المسارات
من مزايا تضمين منطق الذكاء الاصطناعي في مسار معيّن أنّه يمكنك اختبار المسار وتصحيح الأخطاء فيه بشكل مستقل عن تطبيقك باستخدام واجهة مستخدم المطوّر في Genkit.
لبدء واجهة مستخدِم المطوّر، نفِّذ الأوامر التالية من دليل مشروعك:
genkit start -- tsx --watch src/your-code.ts
من علامة التبويب تشغيل في واجهة مستخدم المطوّر، يمكنك تنفيذ أيّ من عمليات التنقّل المحدّدة في مشروعك:
بعد تشغيل مسار، يمكنك فحص تتبع لطلب تنفيذ المسار من خلال النقر على عرض التتبُّع أو الانتقال إلى علامة التبويب فحص.
في "عارض التتبّع"، يمكنك الاطّلاع على تفاصيل عن تنفيذ العملية بأكملها، بالإضافة إلى تفاصيل عن كل خطوة فردية ضمن العملية. على سبيل المثال، نأخذ في الاعتبار العملية التالية التي تحتوي على عدة طلبات لإنشاء محتوى:
const PrixFixeMenuSchema = z.object({
starter: z.string(),
soup: z.string(),
main: z.string(),
dessert: z.string(),
});
export const complexMenuSuggestionFlow = ai.defineFlow(
{
name: 'complexMenuSuggestionFlow',
inputSchema: z.string(),
outputSchema: PrixFixeMenuSchema,
},
async (theme: string): Promise<z.infer<typeof PrixFixeMenuSchema>> => {
const chat = ai.chat({ model: gemini15Flash });
await chat.send('What makes a good prix fixe menu?');
await chat.send(
'What are some ingredients, seasonings, and cooking techniques that ' +
`would work for a ${theme} themed menu?`
);
const { output } = await chat.send({
prompt:
`Based on our discussion, invent a prix fixe menu for a ${theme} ` +
'themed restaurant.',
output: {
schema: PrixFixeMenuSchema,
},
});
if (!output) {
throw new Error('No data generated.');
}
return output;
}
);
عند تنفيذ هذه العملية، يعرض لك "عارض التتبّع" تفاصيل عن كل طلب إنشاء، بما في ذلك مخرجاته:
خطوات المسار
في المثال الأخير، رأيت أنّ كلّ طلب generate()
ظهر كخطوة
منفصلة في "عارض التتبّع". تظهر كلّ الإجراءات الأساسية في Genkit كخطوات
منفصلة لأحد المسارات:
generate()
Chat.send()
embed()
index()
retrieve()
إذا أردت تضمين رمز غير الرمز أعلاه في عمليات التتبّع، يمكنك إجراء ذلك
من خلال لفّ الرمز في طلب run()
. يمكنك إجراء ذلك للاستدعاءات الموجهة إلى مكتبات تابعة لجهات خارجية لا تتوافق مع Genkit، أو لأي قسم مهم من الرمز البرمجي.
على سبيل المثال، إليك عملية تتضمّن خطوتَين: تسترجع الخطوة الأولى قائمة باستخدام
طريقة غير محدّدة، وتشمل الخطوة الثانية القائمة كسياق لمكالمة
generate()
.
export const menuQuestionFlow = ai.defineFlow(
{
name: 'menuQuestionFlow',
inputSchema: z.string(),
outputSchema: z.string(),
},
async (input: string): Promise<string> => {
const menu = await run('retrieve-daily-menu', async (): Promise<string> => {
// Retrieve today's menu. (This could be a database access or simply
// fetching the menu from your website.)
// ...
return menu;
});
const { text } = await ai.generate({
model: gemini15Flash,
system: "Help the user answer questions about today's menu.",
prompt: input,
docs: [{ content: [{ text: menu }] }],
});
return text;
}
);
بما أنّ خطوة الاسترجاع مضمّنة في طلب run()
، يتم تضمينها كخطوة
في أداة عرض التتبّع:
نشر المسارات
يمكنك نشر مسارات الإحالات الناجحة مباشرةً كنقاط نهاية لواجهة برمجة تطبيقات الويب، وتكون جاهزة للاتّصال بها من عملاء تطبيقك. تتم مناقشة عملية النشر بالتفصيل في عدة صفحات أخرى، ولكنّ هذا القسم يقدّم نظرة عامة موجزة على خيارات النشر.
وظائف السحابة الإلكترونية لبرنامج Firebase
لنشر مسارات الإحالات الناجحة باستخدام وظائف السحابة الإلكترونية لبرنامج Firebase، استخدِم المكوّن الإضافي firebase
. في
تعريفات مسارات الإحالات الناجحة، استبدِل defineFlow
بـ onFlow
وأدرِج
authPolicy
.
import { firebaseAuth } from '@genkit-ai/firebase/auth';
import { onFlow } from '@genkit-ai/firebase/functions';
export const menuSuggestion = onFlow(
ai,
{
name: 'menuSuggestionFlow',
authPolicy: firebaseAuth((user) => {
if (!user.email_verified) {
throw new Error('Verified email required to run flow');
}
}),
},
async (restaurantTheme) => {
// ...
}
);
لمزيد من المعلومات، يُرجى الاطّلاع على الصفحات التالية:
Express.js
لنشر عمليات التنفيذ باستخدام أيّ منصة استضافة Node.js، مثل Cloud Run، حدِّد
عمليات التنفيذ باستخدام defineFlow()
ثمّ استخدِم startFlowServer()
:
export const menuSuggestionFlow = ai.defineFlow(
{
name: 'menuSuggestionFlow',
},
async (restaurantTheme) => {
// ...
}
);
ai.startFlowServer({
flows: [menuSuggestionFlow],
});
سيعرض startFlowServer
تلقائيًا جميع مسارات الإحالات الناجحة المحدّدة في قاعدة بياناتك كنقاط نهاية لبروتوكول HTTP (مثل http://localhost:3400/menuSuggestionFlow
). يمكنك استدعاء مسار إحالة ناجحة باستخدام طلب POST على النحو التالي:
curl -X POST "http://localhost:3400/menuSuggestionFlow" \
-H "Content-Type: application/json" -d '{"data": "banana"}'
يمكنك تخصيص خادم عمليات التدفق، عند الحاجة، لعرض قائمة معيّنة من عمليات التدفق، كما هو موضّح أدناه. يمكنك أيضًا تحديد منفذ مخصّص (ستستخدمه متغيّر البيئة PORT في حال ضبطه) أو تحديد إعدادات سياسة مشاركة الموارد المتعددة المصادر (CORS).
export const flowA = ai.defineFlow({ name: 'flowA' }, async (subject) => {
// ...
});
export const flowB = ai.defineFlow({ name: 'flowB' }, async (subject) => {
// ...
});
ai.startFlowServer({
flows: [flowB],
port: 4567,
cors: {
origin: '*',
},
});
للحصول على معلومات عن عمليات النشر على منصات معيّنة، اطّلِع على مقالتَي النشر باستخدام Cloud Run وعمليات النشر على أي منصة Node.js.