ما هو AI SDK 5؟
يُعدّ Vercel AI SDK 5 أهم تحديث يشهده هذا المكتبة منذ إطلاقها. مع تجاوز 30 مليون تنزيل أسبوعي مجمّع عبر حزمة ai الأساسية وحزم المزودين، يمثّل هذا الإصدار المعيار الرئيسي لبناء المنتجات المدعومة بالذكاء الاصطناعي بـ TypeScript. يُقدّم الإصدار الخامس إعادة تصميم معماري جوهرية تفصل بين متطلبات واجهة المستخدم ومتطلبات النموذج، مما يجعل بناء تطبيقات الذكاء الاصطناعي متكاملة الطرفين أكثر سهولةً وقابلية للتصحيح والصيانة.
أبرز التغييرات في AI SDK 5:
- UIMessage مقابل ModelMessage — نوعان متمايزان للرسائل بمسؤوليات مختلفة
- useChat القائم على النقل — يستبدل إدارة HTTP الداخلية بطبقة نقل صريحة وقابلة للتبديل
- بث SSE — تحل أحداث Server-Sent Events محل بروتوكول البث الثنائي المخصص، مما يتيح تصحيح الأخطاء عبر أدوات المتصفح الأصلية
- التحكم في الحلقة الوكيلة — يمنحك
stopWhenوprepareStepتحكمًا دقيقًا في استدعاءات الأدوات متعددة الخطوات - رسائل مخصصة آمنة الأنواع — استنتاج أنواع TypeScript الكاملة من تعريفات أدواتك وسكيماتك
- توليد الكلام — الأولوية الأولى لـ
generateSpeechلتحويل النص إلى صوت
المتطلبات الأساسية
قبل البدء، تأكد من توفر ما يلي:
- تثبيت Node.js 18 أو أعلى
- مشروع Next.js 15 منشأ بـ
npx create-next-app@latest my-ai-app --typescript --app - مفتاح OpenAI API مخزّن باسم
OPENAI_API_KEYفي.env.local - إلمام أساسي بـ React وTypeScript والبرمجة غير المتزامنة
ما الذي ستبنيه؟
بنهاية هذا الدرس ستمتلك تطبيق محادثة ذكاء اصطناعي متكامل الطرفين يقوم بما يلي:
- بث الردود في الوقت الفعلي من نموذج OpenAI
- استخدام بنية UIMessage/ModelMessage بشكل صحيح
- استدعاء أداة مخصصة ومعالجة نتيجتها ضمن التدفق
- التحكم في الحلقة الوكيلة بـ
stopWhenوprepareStep - تعريف شكل رسالة مخصص آمن الأنواع بالكامل
- تحويل أي نص إلى كلام بـ
generateSpeech
يعمل المشروع المكتمل مع أي مزود متوافق مع AI SDK 5 — Anthropic وGoogle Gemini وMistral وأكثر من 20 مزودًا آخر — بتغيير سطرين فقط من الكود.
الخطوة 1: تثبيت AI SDK 5
في جذر مشروع Next.js الخاص بك، ثبّت الحزمة الأساسية ومزود OpenAI:
npm install ai @ai-sdk/openaiتحقق من الإصدار المثبّت:
npm list aiيجب أن ترى ai@5.x.x في المخرجات. إذا كنت تُرقّي من AI SDK 4، راجع دليل الترحيل الرسمي — تغيّر كلٌّ من useChat API وشكل الرسالة بشكل ملحوظ.
أضف مفتاح API الخاص بك إلى .env.local:
OPENAI_API_KEY=sk-...الخطوة 2: فهم UIMessage مقابل ModelMessage
هذا هو المفهوم الأهم في AI SDK 5. قبل الإصدار الخامس، كان نوع Message الواحد يخدم غرضين — احتواء حالة واجهة المستخدم المُعرضة في المتصفح، والحمولة الخام المُرسلة إلى نموذج اللغة. أدى ذلك التصميم إلى تعقيد في التسلسل وجعل استمرارية المحادثة عُرضة للأخطاء.
يفصل AI SDK 5 هذين الجانبين بوضوح إلى نوعين متمايزين:
| النوع | مكان وجوده | محتوياته |
|---|---|---|
UIMessage | حدّ العميل والخادم | الحالة الكاملة للرسالة: أجزاء النص، نتائج الأدوات، البيانات الوصفية |
ModelMessage | الخادم فقط — يُرسل إلى نموذج اللغة | حمولة مُبسَّطة محسّنة لاستهلاك النموذج |
تدفق البيانات في كل طلب:
- يُرسل العميل
UIMessage[]إلى مسار API الخاص بك - يستدعي الخادم
convertToModelMessages(messages)لإنتاجModelMessage[] - تنتقل
ModelMessage[]إلى نموذج اللغة عبرstreamText - يعود التدفق كـ
UIMessageStreamResponse - يُحدّث
useChatعلى العميل حالةUIMessageالمحلية من التدفق
يجعل هذا الفصل الاستمرارية سهلة. يوفر معاودة الاتصال onFinish رسائل UIMessage[] جاهزة للتخزين دون الحاجة إلى أي تحويل يدوي.
الخطوة 3: بناء مسار الخادم
أنشئ مسار API في app/api/chat/route.ts:
import { openai } from '@ai-sdk/openai';
import {
convertToModelMessages,
streamText,
UIMessage,
tool,
stepCountIs,
} from 'ai';
import { z } from 'zod';
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
system: 'أنت مساعد مفيد. استخدم الأدوات عند الاقتضاء.',
messages: convertToModelMessages(messages),
stopWhen: stepCountIs(5),
tools: {
getWeather: tool({
description: 'احصل على الطقس الحالي لمدينة ما',
inputSchema: z.object({
city: z.string().describe('اسم المدينة'),
}),
execute: async ({ city }) => {
// استبدل هذا باستدعاء API طقس حقيقي في الإنتاج
return { city, temperature: 22, condition: 'مشمس' };
},
}),
},
onFinish: async ({ messages: finalMessages }) => {
// احفظ المحادثة هنا — finalMessages هو UIMessage[]
console.log('اكتملت المحادثة:', finalMessages.length, 'رسائل');
},
});
return result.toUIMessageStreamResponse();
}القرارات الرئيسية في هذا المسار:
convertToModelMessages(messages)يُترجمUIMessage[]الواردة إلىModelMessage[]قبل استدعاء نموذج اللغةstopWhen: stepCountIs(5)يعمل كحد أقصى للسلامة — تتوقف الحلقة الوكيلة بعد 5 خطوات كحد أقصىtoUIMessageStreamResponse()يُغلّف التدفق كـ SSE يفهمهuseChatبشكل أصلي- معاودة الاتصال
onFinishهي المكان المعياري لاستمرارية المحادثات
الخطوة 4: بناء واجهة المحادثة للعميل
أنشئ الصفحة في app/chat/page.tsx:
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export default function ChatPage() {
const [input, setInput] = useState('');
const { messages, sendMessage, status } = useChat();
return (
<div className="flex flex-col max-w-2xl mx-auto h-screen p-4">
<div className="flex-1 overflow-y-auto space-y-4 py-4">
{messages.map(message => (
<div key={message.id} className="space-y-1">
<strong className="capitalize text-sm text-gray-500">
{message.role === 'user' ? 'أنت' : 'المساعد'}
</strong>
<div>
{message.parts.map((part, i) => {
if (part.type === 'text') {
return <p key={i} className="text-gray-800">{part.text}</p>;
}
if (part.type === 'tool-getWeather') {
return (
<div key={i} className="bg-blue-50 border border-blue-200 rounded p-3 text-sm">
<span className="font-medium">الطقس في {part.result.city}:</span>{' '}
{part.result.temperature}°م، {part.result.condition}
</div>
);
}
return null;
})}
</div>
</div>
))}
{status === 'streaming' && (
<div className="text-gray-400 text-sm italic">جارٍ التفكير...</div>
)}
</div>
<form
onSubmit={e => {
e.preventDefault();
sendMessage({ text: input });
setInput('');
}}
className="flex gap-2 pt-4 border-t"
>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="اسأل عن الطقس في أي مدينة..."
className="flex-1 border rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={status === 'streaming'}
/>
<button
type="submit"
disabled={status === 'streaming'}
className="px-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50 hover:bg-blue-700"
>
إرسال
</button>
</form>
</div>
);
}الفارق الجوهري عن AI SDK 4 هو طريقة الوصول إلى محتوى الرسائل. كل UIMessage الآن يحتوي على مصفوفة parts حيث لكل عنصر محدد type صريح:
- محتوى النص:
{ type: 'text', text: '...' } - نتائج الأدوات:
{ type: 'tool-getWeather', result: { city, temperature, condition } }
أنواع أجزاء الأدوات تحمل الصيغة tool-<toolName> لمنع تعارض الأسماء بين الأدوات.
الخطوة 5: التحكم في الحلقة الوكيلة
تمنحك الأدوات الأولية stopWhen وprepareStep تحكمًا دقيقًا في سلوك الوكيل متعدد الخطوات.
شروط التوقف
stopWhen يقبل شرطًا واحدًا أو مصفوفة من الشروط. تتوقف الحلقة عند تحقق أي شرط:
import { stepCountIs, toolCalls, textIncludes } from 'ai';
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
stopWhen: [
stepCountIs(10), // حد أقصى صارم: التوقف بعد 10 خطوات
toolCalls('submitFinalAnswer'), // التوقف عند إشارة النموذج بالانتهاء
textIncludes('TASK_COMPLETE'), // التوقف على سلسلة محددة في المخرجات
],
tools: { /* ... */ },
});مرجع شامل لشروط التوقف المضمّنة:
| الشرط | يُفعَّل عند |
|---|---|
stepCountIs(n) | بلوغ عدد الخطوات n |
toolCalls(name) | استدعاء أداة بعينها |
toolResults(name) | تلقّي نتيجة من أداة بعينها |
textIncludes(str) | احتواء نص المخرجات على السلسلة |
textDoesNotInclude(str) | عدم احتواء نص المخرجات على السلسلة بعد الآن |
custom(() => boolean) | إعادة المحدد المخصص للقيمة true |
إعداد كل خطوة بـ prepareStep
تعمل prepareStep قبل كل خطوة وتتيح تعديل النموذج والأدوات المتاحة أو سجل الرسائل لكل خطوة على حدة:
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
prepareStep: async ({ stepNumber, messages }) => {
// أجبر الخطوة الأولى على استدعاء أداة الطقس فورًا
if (stepNumber === 0) {
return {
toolChoice: { type: 'tool', toolName: 'getWeather' },
activeTools: ['getWeather'],
};
}
// اختصر السجل للوكلاء طويلي الأمد للتحكم في تكاليف الرموز
if (messages.length > 20) {
return { messages: messages.slice(-10) };
}
// إعادة كائن فارغ يستخدم الإعدادات الافتراضية لهذه الخطوة
return {};
},
tools: { /* ... */ },
});نمط شائع هو استخدام prepareStep لاستبدال نموذج أرخص في خطوات استدعاء الأدوات الوسيطة والعودة إلى نموذج أكثر قدرة فقط في خطوة التوليف الأخيرة.
الخطوة 6: تعريف UIMessage مخصص آمن الأنواع
يتيح لك AI SDK 5 تعريف الشكل الدقيق لـ TypeScript لرسائلك باستخدام Zod وInferUITools. أنشئ lib/ai-types.ts:
import { InferUITools, ToolSet, UIMessage, tool } from 'ai';
import { z } from 'zod';
// 1. البيانات الوصفية المُلحقة بكل رسالة
const metadataSchema = z.object({
model: z.string(),
latencyMs: z.number().optional(),
tokensUsed: z.number().optional(),
});
export type MessageMetadata = z.infer<typeof metadataSchema>;
// 2. أجزاء البيانات المخصصة المُرسَلة في منتصف الرد
const dataPartSchema = z.object({
weatherCard: z.object({
city: z.string(),
temperature: z.number(),
condition: z.string(),
}),
});
export type MessageDataPart = z.infer<typeof dataPartSchema>;
// 3. تعريفات الأدوات المشتركة — استوردها في مسار API الخاص بك أيضًا
export const appTools: ToolSet = {
getWeather: tool({
description: 'احصل على الطقس الحالي لمدينة ما',
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => ({
city,
temperature: 22,
condition: 'مشمس',
}),
}),
};
// 4. استنتاج أنواع نتائج الأدوات تلقائيًا
type AppTools = InferUITools<typeof appTools>;
// 5. رسالتك المُحدَّدة الأنواع بالكامل — استخدمها في كل مكان
export type AppUIMessage = UIMessage<MessageMetadata, MessageDataPart, AppTools>;استبدل AppUIMessage بـ UIMessage في كل أجزاء كودك:
// مسار API — جسم الطلب محدد الأنواع
const { messages }: { messages: AppUIMessage[] } = await req.json();
// مكوّن العميل — Hook محدد الأنواع
const { messages } = useChat<AppUIMessage>();
// الآن يعرف TypeScript:
// message.metadata.model → string
// message.metadata.latencyMs → number | undefined
// part.type === 'tool-getWeather' → part.result.city سلسلة نصيةيُلغي هذا فئة كاملة من أخطاء وقت التشغيل حيث تتباعد أشكال نتائج الأدوات بين الخادم ومنطق عرض العميل.
الخطوة 7: إضافة توليد الكلام
يُرقّي AI SDK 5 توليد الكلام إلى واجهة برمجة أولى. أضف مسارًا مخصصًا في app/api/speech/route.ts:
import { generateSpeech } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { text } = await req.json();
const { audio } = await generateSpeech({
model: openai.speech('tts-1-hd'),
text,
voice: 'nova',
});
return new Response(audio.uint8Array, {
headers: {
'Content-Type': 'audio/mpeg',
'Cache-Control': 'no-store',
},
});
}استدعِه من العميل للتحدث بأي رسالة من المساعد:
async function speakMessage(text: string) {
const response = await fetch('/api/speech', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
new Audio(audioUrl).play();
}الأصوات المتاحة لـ OpenAI TTS: alloy وecho وfable وonyx وnova وshimmer. يوفر نموذج tts-1-hd جودة صوت أعلى مع زمن استجابة أعلى قليلًا مقارنة بـ tts-1.
اختبار التطبيق
شغّل خادم التطوير:
npm run devافتح http://localhost:3000/chat وأرسل الرسالة: "ما هو الطقس في تونس؟"
يجب أن تُلاحظ:
- نص المساعد يتدفق كلمة بكلمة
- استدعاء أداة
getWeatherوالنتيجة تظهر في الواجهة - نص المتابعة من المساعد يُلخّص بيانات الطقس
افتح أدوات مطوري المتصفح، واختر Network وصفّه بـ EventStream، وافحص طلب /api/chat. ستشاهد أحداث SSE كنص مقروء — تحسين جوهري مقارنة ببروتوكول البث الثنائي السابق.
استكشاف الأخطاء وإصلاحها
"messages is not iterable" — مسار API الخاص بك يتلقى جسمًا فارغًا. تأكد من أن العميل يُرسل Content-Type: application/json وأن شكل الجسم هو { messages: [...] }.
نتيجة الأداة غائبة من واجهة المستخدم — يجب أن يتطابق part.type بدقة مع tool-<toolName>. إذا كانت أداتك مسماة get_weather بشرطة سفلية، فإن نوع الجزء هو tool-get_weather.
التدفق ينتهي بعد خطوة واحدة — النموذج لا يُولّد استدعاءات للأدوات. تحقق من أن موجه system الخاص بك يُشجّع استخدام الأدوات وأن رسالة المستخدم ذات صلة بوصف الأداة.
أخطاء TypeScript على أجزاء الرسائل — استورد UIMessage وInferUITools من 'ai' (الحزمة الأساسية)، وليس من '@ai-sdk/react'. يجب أن تأتي الأنواع الأساسية من الحزمة الجذرية.
الخطوات التالية
- أضف استمرارية المحادثة: احفظ
UIMessage[]منonFinishفي قاعدة بيانات Postgres أو SQLite باستخدام Drizzle ORM - استكشف فئة
Agentالخفيفة لأنابيب الوكلاء القائمة على العقد غير المتدفقة - أضف مراقبة نموذج اللغة باستخدام Langfuse لتتبع كل استدعاء أداة وكل خطوة وعدد الرموز
- استبدل OpenAI بـ Anthropic Claude بتغيير
@ai-sdk/openaiإلى@ai-sdk/anthropic— بقية الكود لا يتغير
الخلاصة
يجعل Vercel AI SDK 5 بناء تطبيقات الذكاء الاصطناعي الإنتاجية بـ TypeScript أكثر متانة بشكل ملحوظ. يُلغي فصل UIMessage/ModelMessage فئة كاملة من أخطاء التسلسل، ويجعل بث SSE التصحيح طبيعيًا بأدوات المتصفح القياسية، وتمنحك الأدوات الأولية للحلقة الوكيلة — stopWhen وprepareStep — التحكم الذي تحتاجه دون كتابة منطق الحلقة الخاص بك. مع أكثر من 30 مليون تنزيل أسبوعي ودعم لأكثر من 25 مزودًا، يُمثّل AI SDK 5 الأساس الأكثر عملية لتطوير الذكاء الاصطناعي متكامل الطرفين في منظومة JavaScript اليوم.