الكتابات/tutorial/2026/07
Tutorial10 يوليو 2026·30 دقيقة

بناء تطبيق دردشة ذكاء اصطناعي متعدد النماذج مع Together AI SDK و Next.js 15

تعلم كيفية بناء تطبيق دردشة ذكاء اصطناعي متدفق جاهز للإنتاج باستخدام TypeScript SDK من Together AI وNext.js 15. يغطي هذا الدرس إعداد SDK ومسارات API المتدفقة وتحديثات واجهة المستخدم في الوقت الفعلي مع التبديل بين أكثر من 200 نموذج مفتوح المصدر.

المتطلبات الأساسية

قبل البدء في هذا الدرس، تأكد من توفر ما يلي:

  • Node.js 20 أو أحدث
  • معرفة أساسية بـ React وApp Router في Next.js
  • حساب Together AI مع مفتاح API
  • إلمام بـ TypeScript
  • محرر أكواد (يُوصى بـ VS Code)

ما الذي ستبنيه

أغلقت Together AI مؤخراً جولة تمويل Series C بقيمة 800 مليون دولار بقيادة أرامكو فنتشرز، مما رسّخ مكانتها بوصفها المنصة الرائدة للاستدلال بنماذج الذكاء الاصطناعي مفتوحة المصدر. مع وصول إلى أكثر من 200 نموذج — بما فيها Llama 3.3 وMistral وQwen 2.5 وDeepSeek وFalcon — تقدم Together AI واحدة من أكثر واجهات برمجة النماذج المتعددة مرونةً اليوم.

في هذا الدرس، ستبني تطبيق دردشة ذكاء اصطناعي متدفق يشمل:

  • بث الرموز في الوقت الفعلي باستخدام Server-Sent Events (SSE)
  • التبديل بين نماذج LLM مفتوحة المصدر الشهيرة
  • واجهة دردشة أنيقة ومتجاوبة مع Next.js 15 App Router
  • تكامل آمن من حيث النوع مع TypeScript SDK الرسمي من Together AI
  • معالجة أخطاء وحالات تحميل جاهزة للإنتاج

الخطوة 1: إنشاء مشروع Next.js 15 جديد

ابدأ بإنشاء مشروع Next.js 15 جديد:

npx create-next-app@latest together-chat --typescript --tailwind --app --src-dir
cd together-chat

عند المطالبة، اختر:

  • TypeScript: نعم
  • ESLint: نعم
  • Tailwind CSS: نعم
  • App Router: نعم

الخطوة 2: تثبيت TypeScript SDK من Together AI

ثبّت الحزمة الرسمية:

npm install together-ai

تُعدّ حزمة together-ai هي SDK الرسمي الذي يوفر أنواع TypeScript الكاملة ودعم البث وإعادة المحاولة التلقائية.

الخطوة 3: تهيئة متغيرات البيئة

أنشئ ملف .env.local في جذر مشروعك:

TOGETHER_API_KEY=your_api_key_here

احصل على مفتاح API من لوحة تحكم Together AI. تمنح المنصة رصيداً مجانياً بقيمة 25 دولاراً للحسابات الجديدة، وهو أكثر من كافٍ لإتمام هذا الدرس.

أضف .env.local إلى .gitignore لتجنب تسريب المفاتيح:

echo ".env.local" >> .gitignore

الخطوة 4: إنشاء عميل Together AI

أنشئ وحدة عميل مشتركة في lib/together.ts:

import Together from 'together-ai';
 
export const together = new Together({
  apiKey: process.env.TOGETHER_API_KEY,
});
 
export const AVAILABLE_MODELS = [
  {
    id: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
    name: 'Llama 3.3 70B',
    provider: 'Meta',
  },
  {
    id: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
    name: 'Mixtral 8x7B',
    provider: 'Mistral AI',
  },
  {
    id: 'Qwen/Qwen2.5-72B-Instruct-Turbo',
    name: 'Qwen 2.5 72B',
    provider: 'Alibaba',
  },
  {
    id: 'deepseek-ai/DeepSeek-V3',
    name: 'DeepSeek V3',
    provider: 'DeepSeek',
  },
];

تُصدّر هذه الوحدة عميل Together المُهيَّأ وقائمة منتقاة بأشهر النماذج مفتوحة المصدر على المنصة.

الخطوة 5: بناء مسار API المتدفق

أنشئ مسار دردشة API في app/api/chat/route.ts:

import { together } from '@/lib/together';
import { NextRequest } from 'next/server';
 
export const runtime = 'nodejs';
export const maxDuration = 60;
 
export async function POST(req: NextRequest) {
  const { messages, model } = await req.json();
 
  if (!messages || !Array.isArray(messages)) {
    return new Response(JSON.stringify({ error: 'Invalid messages format' }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });
  }
 
  const selectedModel = model || 'meta-llama/Llama-3.3-70B-Instruct-Turbo';
 
  const systemMessage = {
    role: 'system' as const,
    content: 'أنت مساعد ذكاء اصطناعي مفيد. كن موجزاً ودقيقاً في إجاباتك.',
  };
 
  const stream = await together.chat.completions.create({
    messages: [systemMessage, ...messages],
    model: selectedModel,
    stream: true,
    max_tokens: 2048,
    temperature: 0.7,
  });
 
  const encoder = new TextEncoder();
 
  const readable = new ReadableStream({
    async start(controller) {
      try {
        for await (const chunk of stream) {
          const content = chunk.choices[0]?.delta?.content || '';
          if (content) {
            controller.enqueue(encoder.encode(content));
          }
        }
      } catch (err) {
        controller.error(err);
      } finally {
        controller.close();
      }
    },
  });
 
  return new Response(readable, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'Transfer-Encoding': 'chunked',
      'X-Content-Type-Options': 'nosniff',
    },
  });
}

يقوم معالج المسار بما يلي:

  1. يستقبل messages (سجل المحادثة) وmodel (النموذج المختار) من جسم الطلب
  2. يُضيف رسالة نظام لتوجيه سلوك المساعد
  3. يستدعي together.chat.completions.create مع stream: true لتفعيل بث الرموز
  4. يُكرّر على التدفق غير المتزامن ويدفع كل قطعة نصية إلى ReadableStream
  5. يُعيد التدفق كاستجابة HTTP نصية

يضمن إعداد maxDuration = 60 بقاء الدالة الحاسوبية نشطة طوال مدة الردود الطويلة.

الخطوة 6: إنشاء أنواع الرسائل

أضف ملف أنواع مشترك في types/chat.ts:

export type Role = 'user' | 'assistant' | 'system';
 
export interface Message {
  id: string;
  role: Role;
  content: string;
}

الخطوة 7: بناء مكوّن الدردشة

أنشئ مكوّن الدردشة الرئيسي في components/Chat.tsx:

'use client';
 
import { useState, useRef, useEffect } from 'react';
import { AVAILABLE_MODELS } from '@/lib/together';
import { Message } from '@/types/chat';
 
export default function Chat() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [selectedModel, setSelectedModel] = useState(AVAILABLE_MODELS[0].id);
  const bottomRef = useRef<HTMLDivElement>(null);
 
  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);
 
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!input.trim() || isLoading) return;
 
    const userMessage: Message = {
      id: crypto.randomUUID(),
      role: 'user',
      content: input.trim(),
    };
 
    const updatedMessages = [...messages, userMessage];
    setMessages(updatedMessages);
    setInput('');
    setIsLoading(true);
 
    const assistantId = crypto.randomUUID();
    setMessages(prev => [
      ...prev,
      { id: assistantId, role: 'assistant', content: '' },
    ]);
 
    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          messages: updatedMessages.map(m => ({
            role: m.role,
            content: m.content,
          })),
          model: selectedModel,
        }),
      });
 
      if (!res.ok) throw new Error('Failed to fetch response');
      if (!res.body) throw new Error('No response body');
 
      const reader = res.body.getReader();
      const decoder = new TextDecoder();
 
      while (true) {
        const result = await reader.read();
        if (result.done) break;
        const text = decoder.decode(result.value, { stream: true });
        setMessages(prev =>
          prev.map(m =>
            m.id === assistantId
              ? { ...m, content: m.content + text }
              : m
          )
        );
      }
    } catch (err) {
      setMessages(prev =>
        prev.map(m =>
          m.id === assistantId
            ? { ...m, content: 'خطأ: تعذّر الحصول على رد. يرجى المحاولة مجدداً.' }
            : m
        )
      );
    } finally {
      setIsLoading(false);
    }
  }
 
  const modelInfo = AVAILABLE_MODELS.find(m => m.id === selectedModel);
 
  return (
    <div className="flex flex-col h-screen max-w-3xl mx-auto px-4" dir="rtl">
      <div className="flex items-center justify-between py-4 border-b">
        <h1 className="text-xl font-semibold">Together AI Chat</h1>
        <select
          value={selectedModel}
          onChange={e => setSelectedModel(e.target.value)}
          className="text-sm border rounded-lg px-3 py-1.5 bg-white dark:bg-zinc-900"
        >
          {AVAILABLE_MODELS.map(m => (
            <option key={m.id} value={m.id}>
              {m.name} ({m.provider})
            </option>
          ))}
        </select>
      </div>
 
      <div className="flex-1 overflow-y-auto py-4 space-y-4">
        {messages.length === 0 && (
          <div className="text-center text-zinc-400 mt-20">
            <p className="text-lg font-medium">ابدأ محادثة</p>
            <p className="text-sm mt-1">
              باستخدام {modelInfo?.name} من {modelInfo?.provider}
            </p>
          </div>
        )}
        {messages.map(message => (
          <div
            key={message.id}
            className={`flex ${message.role === 'user' ? 'justify-start' : 'justify-end'}`}
          >
            <div
              className={`max-w-[80%] rounded-2xl px-4 py-2.5 text-sm ${
                message.role === 'user'
                  ? 'bg-blue-600 text-white'
                  : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100'
              }`}
            >
              {message.content || (
                <span className="animate-pulse">جارٍ التفكير...</span>
              )}
            </div>
          </div>
        ))}
        <div ref={bottomRef} />
      </div>
 
      <form onSubmit={handleSubmit} className="py-4 border-t">
        <div className="flex gap-2 flex-row-reverse">
          <input
            value={input}
            onChange={e => setInput(e.target.value)}
            placeholder="اكتب رسالتك..."
            className="flex-1 border rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-zinc-900 text-right"
            disabled={isLoading}
          />
          <button
            type="submit"
            disabled={isLoading || !input.trim()}
            className="px-4 py-2.5 bg-blue-600 text-white rounded-xl text-sm font-medium disabled:opacity-50 hover:bg-blue-700 transition-colors"
          >
            إرسال
          </button>
        </div>
      </form>
    </div>
  );
}

الأنماط الرئيسية في هذا المكوّن:

  • واجهة مستخدم متفائلة — تظهر رسالة المستخدم فوراً قبل أن يرد API
  • البث المباشر — يقرأ ReadableStreamDefaultReader القطع فور وصولها ويُضيفها إلى رسالة المساعد في الوقت الفعلي
  • تبديل النماذج — تتيح القائمة المنسدلة التبديل بين Llama وMixtral وQwen وDeepSeek في أي وقت
  • التمرير التلقائي — تنتقل نافذة الدردشة إلى أحدث رسالة مع وصول كل رمز جديد

الخطوة 8: ربط الصفحة

حدّث app/page.tsx لعرض الدردشة:

import Chat from '@/components/Chat';
 
export default function Home() {
  return <Chat />;
}

الخطوة 9: اختبار التطبيق

شغّل خادم التطوير:

npm run dev

افتح http://localhost:3000 في متصفحك. يجب أن ترى:

  1. واجهة دردشة مع محدد النموذج في الرأسية
  2. حالة فارغة تدعوك لبدء المحادثة
  3. بث النصوص في الوقت الفعلي رمزاً رمزاً مع رد الذكاء الاصطناعي

اختبر هذه السيناريوهات للتحقق من صحة كل شيء:

  • أرسل سؤالاً بسيطاً وتحقق من عمل البث في الوقت الفعلي
  • بدّل النماذج بين الرسائل
  • اطلب مقطع برمجي طويلاً لاختبار سلوك المخزن المؤقت أثناء البث المستمر

الخطوة 10: تتبع الاستخدام (اختياري)

يتضمن تدفق الاستجابة من Together AI بيانات استخدام الرموز في القطعة الأخيرة. استخدمها للمراقبة والتحكم في التكاليف:

let usage: { total_tokens: number; prompt_tokens: number; completion_tokens: number } | null = null;
 
for await (const chunk of stream) {
  const content = chunk.choices[0]?.delta?.content || '';
  if (content) {
    controller.enqueue(encoder.encode(content));
  }
  if (chunk.usage) {
    usage = chunk.usage;
  }
}
 
if (usage) {
  console.log(
    `الرموز — التوجيه: ${usage.prompt_tokens}, ` +
    `الإكمال: ${usage.completion_tokens}, ` +
    `المجموع: ${usage.total_tokens}`
  );
}

استخدم هذه البيانات لتسجيل تكاليف كل طلب، أو تطبيق حدود رموز لكل مستخدم، أو تغذية لوحة مراقبة.

الخطوة 11: النشر على Vercel

انشر التطبيق دون أي تهيئة إضافية:

npx vercel

أضف متغير البيئة في لوحة تحكم Vercel ضمن Settings → Environment Variables، أو عبر CLI:

vercel env add TOGETHER_API_KEY

البنية التحتية العالمية للاستدلال من Together AI محسّنة لزمن استجابة منخفض، مما يجعلها مناسبة تماماً لمنصات Vercel Serverless والـ Edge.

استكشاف الأخطاء وإصلاحها

يتوقف البث في منتصف الرد
زد قيمة maxDuration في معالج المسار. اضبط export const maxDuration = 60 لتمديد مهلة الدالة الحاسوبية بعد الافتراضي (10 ثوانٍ).

خطأ TOGETHER_API_KEY is not defined
تأكد من وجود .env.local في جذر المشروع، ثم أعد تشغيل خادم التطوير. يقرأ Next.js متغيرات البيئة عند بدء التشغيل فقط.

البث لا يعمل في بيئة الإنتاج
تحقق من استخدامك runtime = 'nodejs' في معالج المسار. وقت تشغيل Edge يفرض قيوداً على التدفقات طويلة الأمد من حزم SDK خارجية.

الخطوات القادمة

الآن بعد أن يعمل تطبيق الدردشة المتدفق، فكّر في هذه الإضافات:

  • استمرارية المحادثة — احفظ سجل الدردشة في Turso أو Supabase بين الجلسات
  • استدعاء الأدوات — استخدم دعم استدعاء الدوال في Together AI للبحث على الإنترنت أو تنفيذ الأكواد
  • نظام RAG — ادمج تضمينات Together AI مع LanceDB للحصول على إجابات سياقية من مستنداتك الخاصة
  • الضبط الدقيق — تدعم Together AI ضبط LoRA على مجموعات بيانات مخصصة عبر نفس SDK
  • توليد الصور — استكشف نماذج FLUX المستندة إلى Together AI إلى جانب إكمالات الدردشة

الخلاصة

لقد بنيت تطبيق دردشة ذكاء اصطناعي متعدد النماذج ومتدفقاً جاهزاً للإنتاج باستخدام TypeScript SDK من Together AI وNext.js 15. تجعل واجهة API المتوافقة مع OpenAI من السهل التبديل بين أكثر من 200 نموذج مفتوح المصدر، ويتعامل SDK الرسمي تلقائياً مع البث وسلامة الأنواع وإعادة المحاولة.

مع التمويل الضخم بقيمة 800 مليون دولار من أرامكو فنتشرز، تتهيأ Together AI لتوسيع كبير في كتالوج نماذجها وبنيتها التحتية العالمية — مما يجعل هذا الوقت مثالياً لبناء منتجات الذكاء الاصطناعي على هذه المنصة.