تصميم سير عمل الذكاء الاصطناعي اللي فعلاً يشتغل في الإنتاج
دليل عملي لبناء خطوط أنابيب ذكاء اصطناعي قوية. تعلم بنية الخطوط، تسلسل الخطوات، منطق التفرع، معالجة الأخطاء وتسلسل البرومبتات من مهندسين نشروا هذي الأنظمة فعلياً.
ليش أغلب سير عمل الذكاء الاصطناعي يفشل في الإنتاج
هنا شي تعلمناه بالطريقة الصعبة: أنك تخلي الذكاء الاصطناعي يسوي شي مبهر في ديمو سهل. إنك تخليه يسوي نفس الشي بشكل موثوق، آلاف المرات في اليوم، مع بيانات حقيقية وحالات حدية حقيقية؟ هنا أغلب الفرق تصطدم بالحيط.
بنينا سير عمل ذكاء اصطناعي لمعالجة المستندات، أتمتة دعم العملاء، توليد المحتوى وتحليل البيانات. في الطريق، سوينا كل خطأ ممكن تتخيله. هذا الدليل هو اللي كنا نتمنى لو أحد قالنا إياه قبل ما نبدأ.
الفرق بين النموذج الأولي ونظام الإنتاج مش النموذج اللي تستخدمه. هو كل شي حواليه.
خليني أوريك كيف نصمم سير عمل الذكاء الاصطناعي الحين فعلياً، بعد ما تعلمنا هذي الدروس.
تشريح خط أنابيب ذكاء اصطناعي للإنتاج
تخيل سير عمل الذكاء الاصطناعي كسلسلة من مراحل المعالجة، كل واحدة بمهمة محددة. هنا الهيكل الأساسي اللي نستخدمه:
المدخل → التحقق → المعالجة المسبقة → معالجة الذكاء الاصطناعي → المعالجة اللاحقة → التحقق → المخرج
↓ ↓ ↓ ↓ ↓
[معالجة [التحويل [استدعاء LLM [التحليل [فحص
الخطأ] + الإثراء] + الأدوات] + التنسيق] الجودة]
كل مرحلة عندها مدخلات ومخرجات وأنماط فشل واضحة. خلينا نفككها.
المرحلة 1: التحقق من المدخلات والتطبيع
لا تثق بالبيانات الواردة أبداً. أبداً. تعلمنا هذا لما عميل أرسلنا "مستند نصي" اللي كان فعلياً ملف بايناري 50 ميجا. الخط اختنق، الطابور تكدس، وقضينا عطلة نهاية الأسبوع نصلحه.
const validateInput = async (input) => {
const checks = {
exists: input !== null && input !== undefined,
sizeOk: Buffer.byteLength(input, 'utf8') < MAX_INPUT_SIZE,
formatOk: isValidFormat(input),
contentOk: !containsMaliciousPatterns(input)
};
const failures = Object.entries(checks)
.filter(([_, passed]) => !passed)
.map(([check]) => check);
if (failures.length > 0) {
throw new ValidationError(`فشل المدخل: ${failures.join(', ')}`);
}
return normalizeInput(input);
};
شو لازم تتحقق منه:
- حدود حجم الملف (خليها أقل مما تتوقع)
- التحقق من التنسيق (هل هو فعلاً JSON، مش بس مسمى
.json؟) - ترميز الأحرف (مشاكل UTF-8 راح تطاردك)
- أمان المحتوى (لا ترسل محتوى ضار لـ LLM حقك)
المرحلة 2: المعالجة المسبقة والإثراء
المدخلات الخام نادراً تروح مباشرة لـ LLM. عادةً تحتاج تحولها، تضيف سياق أو تقسمها لقطع.
| مهمة المعالجة المسبقة | متى تستخدم | مثال |
|---|---|---|
| التقسيم (Chunking) | مستندات طويلة تتجاوز حدود السياق | تقسيم PDF من 100 صفحة لقطع 2000 توكن |
| الإثراء | سياق إضافي مطلوب | إضافة تاريخ العميل قبل معالجة تذكرة الدعم |
| الاستخراج | بس أجزاء من المدخل مهمة | سحب بس حقل "description" من payload JSON |
| التحويل | تحويل التنسيق مطلوب | تحويل HTML لـ markdown لمعالجة أنظف |
| إزالة التكرار | المحتوى المتكرر يضيع التوكنات | حذف الفقرات المتكررة من محتوى مسحوب |
هنا استراتيجية تقسيم نستخدمها فعلياً:
const chunkDocument = (text, options = {}) => {
const {
maxTokens = 2000,
overlap = 200,
preserveParagraphs = true
} = options;
const chunks = [];
let currentChunk = '';
const paragraphs = text.split(/\n\n+/);
for (const para of paragraphs) {
const combined = currentChunk + '\n\n' + para;
if (estimateTokens(combined) > maxTokens) {
if (currentChunk) {
chunks.push(currentChunk.trim());
// الحفاظ على التداخل لاستمرارية السياق
currentChunk = getLastNTokens(currentChunk, overlap) + '\n\n' + para;
} else {
// فقرة واحدة طويلة جداً، تقسيم إجباري
chunks.push(...forceSplitParagraph(para, maxTokens));
currentChunk = '';
}
} else {
currentChunk = combined;
}
}
if (currentChunk.trim()) {
chunks.push(currentChunk.trim());
}
return chunks;
};
التداخل مهم جداً. بدونه، المعلومات اللي تمتد عبر حدود القطع تضيع. تعلمنا هذا لما ملخص المستندات حقنا كان يفوت نقاط مهمة صادف وقعت بين القطع.
تصميم نواة معالجة الذكاء الاصطناعي
هنا تصير استدعاءات LLM الفعلية. لكن استدعاء واحد نادراً يكفي للمهام المعقدة.
نمط سلسلة البرومبت
بدلاً من برومبت ضخم يحاول يسوي كل شي، قسم المهام المعقدة لخطوات مركزة:
[فهم] → [تخطيط] → [تنفيذ] → [تحقق] → [تنسيق]
مثال: معالجة شكوى عميل
const processComplaint = async (complaint) => {
// الخطوة 1: فهم - استخراج المعلومات الرئيسية
const analysis = await llm.call({
system: `استخرج معلومات منظمة من شكاوى العملاء.
أرجع JSON مع: issue_type, urgency, customer_emotion, key_details`,
user: complaint
});
// الخطوة 2: تخطيط - تحديد استراتيجية الرد
const strategy = await llm.call({
system: `بناءً على تحليل الشكوى هذا، حدد استراتيجية الرد.
اعتبر: خيارات الحل، احتياجات التصعيد، أهلية التعويض`,
user: JSON.stringify(analysis)
});
// الخطوة 3: تنفيذ - توليد الرد
const response = await llm.call({
system: `اكتب رد للعميل باتباع هذي الاستراتيجية.
النبرة: متعاطفة، مهنية. الطول: 2-3 فقرات.`,
user: `الاستراتيجية: ${JSON.stringify(strategy)}\nالشكوى الأصلية: ${complaint}`
});
// الخطوة 4: تحقق - فحص الجودة
const verification = await llm.call({
system: `راجع رد العميل هذا. تحقق من:
- يعالج كل المخاوف؟ - نبرة مناسبة؟ - خطوات تالية قابلة للتنفيذ؟
أرجع: {approved: boolean, issues: string[]}`,
user: response
});
if (!verification.approved) {
return await regenerateWithFeedback(response, verification.issues);
}
return response;
};
كل خطوة أبسط، أكثر قابلية للاختبار وأسهل في التصحيح. لما شي يخطئ، تعرف بالضبط أي مرحلة فشلت.
متى تسلسل vs. متى توازي
مش كل شي لازم يكون متسلسل. هكذا نقرر:
| النمط | استخدم عندما | مثال |
|---|---|---|
| سلسلة متتابعة | كل خطوة تعتمد على مخرج الخطوة السابقة | فهم → تخطيط → تنفيذ |
| تنفيذ متوازي | الخطوات مستقلة | تحليل عدة مستندات بالتزامن |
| Map-Reduce | تحتاج تعالج عناصر ثم تجمعها | تلخيص كل قطعة، ثم دمج الملخصات |
| تفرع شرطي | مسارات مختلفة لمدخلات مختلفة | استعلامات بسيطة vs. تحليل معقد |
مثال التنفيذ المتوازي:
const analyzeDocuments = async (documents) => {
// معالجة كل المستندات بالتوازي
const analyses = await Promise.all(
documents.map(doc => analyzeDocument(doc))
);
// Reduce: دمج في تقرير نهائي
const combinedReport = await llm.call({
system: 'اصنع من هذي التحليلات الفردية تقرير متماسك',
user: analyses.map((a, i) => `المستند ${i + 1}:\n${a}`).join('\n\n---\n\n')
});
return combinedReport;
};
منطق التفرع: التوجيه للمعالج الصحيح
المدخلات من العالم الحقيقي تتنوع بشكل كبير. "رسالة عميل" ممكن تكون شكوى، سؤال، مجاملة أو سبام. كل واحدة تحتاج معالجة مختلفة.
التوجيه المبني على التصنيف
const routeCustomerMessage = async (message) => {
const classification = await llm.call({
system: `صنف رسالة العميل هذي في فئة واحدة بالضبط:
- complaint: عميل يعبر عن عدم رضا
- question: عميل يبحث عن معلومات
- feedback: ملاحظات عامة أو اقتراحات
- urgent: مشاكل أمان، تهديدات قانونية، تصعيد تنفيذي
- spam: رسائل غير مهمة أو آلية
أرجع بس اسم الفئة.`,
user: message
});
const handlers = {
complaint: handleComplaint,
question: handleQuestion,
feedback: handleFeedback,
urgent: escalateToHuman,
spam: markAsSpam
};
const handler = handlers[classification.toLowerCase()] || handleUnknown;
return await handler(message);
};
التوجيه المبني على الثقة
أحياناً الذكاء الاصطناعي مش متأكد. ادمج هذا عدم اليقين في توجيهك:
const routeWithConfidence = async (input) => {
const result = await llm.call({
system: `حلل ووجه هذا الطلب. أرجع JSON:
{
"category": "string",
"confidence": 0.0-1.0,
"reasoning": "ليش هذي الفئة"
}`,
user: input
});
if (result.confidence < 0.7) {
// ثقة منخفضة - خذ مدخل بشري أو استخدم بديل
return await handleLowConfidence(input, result);
}
if (result.confidence < 0.9 && isHighStakes(result.category)) {
// ثقة متوسطة في قرارات مهمة - تحقق
return await verifyThenRoute(input, result);
}
// ثقة عالية - تابع تلقائياً
return await routeToHandler(result.category, input);
};
معالجة الأخطاء: لأن الأشياء راح تتكسر
سير عمل الذكاء الاصطناعي يفشل بطرق البرمجيات التقليدية ما تعرفها. الـ LLM ممكن يرجع JSON غير صالح، يهلوس معلومات، أو ببساطة... ما يتبع التعليمات. خطط لهذا.
تسلسل الأخطاء
class AIWorkflowError extends Error {
constructor(message, stage, recoverable = true) {
super(message);
this.stage = stage;
this.recoverable = recoverable;
}
}
class ValidationError extends AIWorkflowError {
constructor(message) {
super(message, 'validation', true);
}
}
class LLMError extends AIWorkflowError {
constructor(message, type) {
super(message, 'llm_call', type !== 'rate_limit');
this.type = type; // 'timeout', 'rate_limit', 'invalid_response', 'refused'
}
}
class OutputError extends AIWorkflowError {
constructor(message) {
super(message, 'output', true);
}
}
استراتيجيات إعادة المحاولة اللي تشتغل
أخطاء مختلفة تحتاج مناهج إعادة محاولة مختلفة:
| نوع الخطأ | استراتيجية إعادة المحاولة | أقصى محاولات | Backoff |
|---|---|---|---|
| Rate limit | انتظر وأعد المحاولة | 5 | أسي مع عشوائية |
| Timeout | أعد المحاولة فوراً | 3 | خطي |
| رد غير صالح | أعد المحاولة مع ملاحظات | 2 | لا شي |
| النموذج رفض | أعد الصياغة وأعد المحاولة | 2 | لا شي |
| خطأ خادم | انتظر وأعد المحاولة | 3 | أسي |
const withRetry = async (operation, options = {}) => {
const {
maxRetries = 3,
backoffMs = 1000,
backoffMultiplier = 2,
shouldRetry = () => true
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (!shouldRetry(error) || attempt === maxRetries) {
throw error;
}
const delay = backoffMs * Math.pow(backoffMultiplier, attempt);
const jitter = Math.random() * delay * 0.1;
await sleep(delay + jitter);
}
}
throw lastError;
};
// الاستخدام مع استدعاءات LLM
const safeLLMCall = async (params) => {
return await withRetry(
() => llm.call(params),
{
maxRetries: 3,
shouldRetry: (error) => {
if (error.type === 'rate_limit') return true;
if (error.type === 'timeout') return true;
if (error.type === 'server_error') return true;
return false;
}
}
);
};
الإصلاح الذاتي للردود غير الصالحة
لما الـ LLM يرجع خردة، أحياناً تقدر تصلحها:
const parseWithRecovery = async (llmResponse, expectedSchema) => {
// المحاولة الأولى: التحليل المباشر
try {
const parsed = JSON.parse(llmResponse);
if (validateSchema(parsed, expectedSchema)) {
return parsed;
}
} catch (e) {
// تحليل JSON فشل، استمر للاستعادة
}
// محاولة الاستعادة: اطلب من LLM يصلح مخرجه
const fixed = await llm.call({
system: `الرد التالي لازم يكون JSON صالح يطابق هذا المخطط:
${JSON.stringify(expectedSchema)}
صلح الرد ليكون JSON صالح. أرجع بس الـ JSON المصلح.`,
user: llmResponse
});
try {
const parsed = JSON.parse(fixed);
if (validateSchema(parsed, expectedSchema)) {
return parsed;
}
} catch (e) {
// لسا مكسور
}
// البديل الأخير: استخرج اللي تقدر عليه
return extractPartialData(llmResponse, expectedSchema);
};
المراقبة والقابلية للملاحظة
ما تقدر تصلح اللي ما تشوفه. هنا اللي نتتبعه:
المقاييس الرئيسية
| المقياس | ليش مهم | عتبة التنبيه |
|---|---|---|
| زمن الاستجابة (p50, p95, p99) | تجربة المستخدم، خطر انتهاء الوقت | p95 > 10 ثانية |
| معدل النجاح | الصحة العامة | < 95% |
| استخدام التوكنات | التحكم في التكاليف | > 150% من الأساس |
| معدل إعادة المحاولة | عدم استقرار مخفي | > 10% |
| توزيع التصنيفات | كشف الانحراف | تغير كبير من الأساس |
| درجات جودة المخرجات | كشف التدهور | المتوسط < 0.8 |
التسجيل المنظم
كل تشغيل لسير العمل لازم ينتج سجلات قابلة للتتبع:
const runWorkflow = async (input, context) => {
const runId = generateRunId();
const startTime = Date.now();
const log = (stage, data) => {
logger.info({
runId,
stage,
timestamp: Date.now(),
elapsed: Date.now() - startTime,
...data
});
};
try {
log('start', { inputSize: input.length });
const validated = await validate(input);
log('validated', { valid: true });
const processed = await process(validated);
log('processed', {
tokensUsed: processed.usage.total,
model: processed.model
});
const output = await format(processed);
log('complete', {
success: true,
outputSize: output.length,
totalTime: Date.now() - startTime
});
return output;
} catch (error) {
log('error', {
error: error.message,
stage: error.stage,
recoverable: error.recoverable
});
throw error;
}
};
جمع كل شي: مثال كامل
خلينا نبني سير عمل تحليل مستندات يستخدم كل اللي ناقشناه:
class DocumentAnalysisPipeline {
constructor(options = {}) {
this.maxChunkSize = options.maxChunkSize || 3000;
this.concurrency = options.concurrency || 5;
}
async run(document) {
// المرحلة 1: التحقق
const validated = await this.validate(document);
// المرحلة 2: المعالجة المسبقة
const chunks = await this.preprocess(validated);
// المرحلة 3: تحليل متوازي مع التحكم في التزامن
const analyses = await this.analyzeChunks(chunks);
// المرحلة 4: التجميع
const synthesis = await this.synthesize(analyses);
// المرحلة 5: فحص الجودة
const final = await this.qualityCheck(synthesis, document);
return final;
}
async validate(document) {
if (!document || typeof document !== 'string') {
throw new ValidationError('المستند لازم يكون نص غير فارغ');
}
if (document.length > 1000000) {
throw new ValidationError('المستند يتجاوز الحجم الأقصى');
}
return document;
}
async preprocess(document) {
return chunkDocument(document, {
maxTokens: this.maxChunkSize,
overlap: 200
});
}
async analyzeChunks(chunks) {
const results = [];
// المعالجة على دفعات للتحكم في التزامن
for (let i = 0; i < chunks.length; i += this.concurrency) {
const batch = chunks.slice(i, i + this.concurrency);
const batchResults = await Promise.all(
batch.map((chunk, idx) => this.analyzeChunk(chunk, i + idx))
);
results.push(...batchResults);
}
return results;
}
async analyzeChunk(chunk, index) {
return await withRetry(async () => {
const result = await llm.call({
system: `حلل هذا القسم من المستند. استخرج:
- المواضيع والثيمات الرئيسية
- الحقائق والأرقام المهمة
- الاقتباسات أو التصريحات الملحوظة
- الأسئلة أو فجوات المعلومات
أرجع JSON منظم.`,
user: `القسم ${index + 1}:\n\n${chunk}`
});
return parseWithRecovery(result, ANALYSIS_SCHEMA);
});
}
async synthesize(analyses) {
const combined = analyses.map((a, i) =>
`القسم ${i + 1}:\n${JSON.stringify(a, null, 2)}`
).join('\n\n---\n\n');
return await llm.call({
system: `اصنع من تحليلات الأقسام هذي ملخص مستند شامل.
الهيكل:
1. ملخص تنفيذي (2-3 جمل)
2. النتائج الرئيسية (نقاط)
3. تفاصيل مهمة
4. فجوات أو أسئلة
5. توصيات`,
user: combined
});
}
async qualityCheck(synthesis, originalDocument) {
const check = await llm.call({
system: `راجع هذا التحليل للجودة. تحقق من:
- الدقة: هل يعكس المستند الأصلي؟
- الاكتمال: هل النقاط الرئيسية مغطاة؟
- الوضوح: هل هو منظم وواضح؟
أرجع: {score: 0-1, issues: string[], approved: boolean}`,
user: `التحليل:\n${synthesis}\n\nالأصلي (أول 2000 حرف):\n${originalDocument.slice(0, 2000)}`
});
if (!check.approved) {
// سجل للمراجعة لكن لا تفشل
logger.warn({ issues: check.issues, score: check.score });
}
return {
analysis: synthesis,
qualityScore: check.score,
qualityIssues: check.issues
};
}
}
الأخطاء الشائعة وكيف تتجنبها
بعد بناء عشرات من هذي الأنظمة، هنا الأخطاء اللي نشوفها أكثر:
1. بدون حدود timeout
كل استدعاء LLM يحتاج timeout. حددها بقوة.
// سيء: بدون timeout
const result = await llm.call(params);
// جيد: timeout صريح
const result = await Promise.race([
llm.call(params),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 30000)
)
]);
2. تجاهل حدود التوكنات
تتبع الاستخدام وحدد ميزانيات:
const tokenBudget = {
max: 10000,
used: 0,
canSpend(amount) {
return this.used + amount <= this.max;
},
spend(amount) {
this.used += amount;
if (this.used > this.max * 0.8) {
logger.warn('ميزانية التوكنات استهلكت 80%');
}
}
};
3. بدون بديل للمسارات الحرجة
دايماً عندك خطة ب:
const processWithFallback = async (input) => {
try {
return await primaryProcess(input);
} catch (error) {
if (error.recoverable) {
return await simplifiedProcess(input);
}
// مسار حرج - ضعه في الطابور للمعالجة اليدوية
await queueForManualReview(input, error);
return { status: 'queued_for_review' };
}
};
شو الجاي
سير عمل الذكاء الاصطناعي يصير أكثر تطوراً. هنا وين نشوف الأشياء رايحة:
- استخدام أفضل للأدوات: النماذج تتحسن في تقرير متى وكيف تستخدم أدوات خارجية
- سياق أطول: نوافذ سياق أكبر تعني صداع تقسيم أقل
- استدلال أسرع: زمن الاستجابة ينزل، يمكن سير عمل في الوقت الفعلي أكثر تعقيداً
- نماذج متخصصة: النماذج المضبوطة لمهام محددة تتفوق على النماذج العامة
لكن الأساسيات ما تتغير. تحقق من المدخلات، عالج الأخطاء بأناقة، راقب كل شي ودايماً عندك بديل. ابني على هذي المبادئ، وسير عمل الذكاء الاصطناعي حقك راح ينجو من الاتصال بالعالم الحقيقي.
إذا تبني سير عمل ذكاء اصطناعي وتصطدم بالحيطان، غالباً شفنا مشكلتك قبل. تواصل معنا، وخلينا نلاقي الحل سوا.
Topics covered
Ready to implement agentic AI?
Our team specializes in building production-ready AI systems. Let's discuss how we can help you leverage agentic AI for your enterprise.
Start a conversation