بناء روبوت محادثة ذكي باستخدام OpenAI Assistants API و Next.js

يختلف OpenAI Assistants API اختلافاً جوهرياً عن Chat Completions API. بدلاً من إدارة سجل المحادثات بنفسك، يمنحك Assistants API محادثات مستمرة (threads)، وأدوات مدمجة مثل البحث في الملفات ومفسر الأكواد، وتنفيذ قائم على التشغيل (runs) يتعامل مع كل شيء تلقائياً.
في هذا الدليل، ستبني روبوت محادثة متكامل المزايا مع Next.js يستفيد من هذه الإمكانيات — من بث الردود المباشر إلى تحميل المستندات التي يمكن للمساعد البحث فيها.
لماذا Assistants API؟ يتطلب Chat Completions API إدارة حالة المحادثة يدوياً وبناء خطوط أنابيب RAG وحلقات تنفيذ الأدوات. يتولى Assistants API كل هذا تلقائياً — المحادثات تُحفظ آلياً، والملفات تُفهرس للبحث، والأكواد تُنفذ في بيئة معزولة.
ما ستتعلمه
بنهاية هذا الدليل، ستتمكن من:
- إنشاء وتكوين مساعد OpenAI بتعليمات مخصصة
- إدارة محادثات مستمرة عبر threads
- بث ردود المساعد في الوقت الفعلي
- تفعيل البحث في الملفات ليتمكن المساعد من الإجابة من المستندات المرفوعة
- تفعيل مفسر الأكواد ليتمكن المساعد من كتابة وتنفيذ أكواد Python
- بناء واجهة دردشة احترافية بـ Next.js و React
- معالجة الأخطاء وتطبيق أفضل ممارسات الإنتاج
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت (
node --version) - معرفة بـ TypeScript (الأنماط العامة، async/await)
- مفتاح OpenAI API مع صلاحية Assistants API (احصل عليه من platform.openai.com)
- أساسيات Next.js (App Router و Server Actions)
- محرر أكواد — يُنصح بـ VS Code
ما ستبنيه
روبوت محادثة ذكي متكامل يدعم:
- محادثات مستمرة — يمكن للمستخدمين العودة للمحادثات السابقة
- بث الردود — تظهر الكلمات تباعاً أثناء توليد المساعد للرد
- أسئلة وأجوبة من المستندات — ارفع ملفات PDF واطرح أسئلة عن محتواها
- تنفيذ الأكواد — يستطيع المساعد كتابة وتشغيل أكواد Python وإرجاع النتائج والرسوم البيانية
- محادثات متعددة — إدارة سياقات محادثة منفصلة
الخطوة 1: إعداد المشروع
أنشئ مشروع Next.js جديد وثبّت التبعيات المطلوبة:
npx create-next-app@latest openai-chatbot --typescript --tailwind --app --src-dir
cd openai-chatbotثبّت OpenAI SDK:
npm install openaiأنشئ ملف البيئة:
# .env.local
OPENAI_API_KEY=sk-proj-your-api-key-here
OPENAI_ASSISTANT_ID= # سنملؤه في الخطوة 3أضف أنماط متغيرات البيئة في src/env.d.ts:
declare namespace NodeJS {
interface ProcessEnv {
OPENAI_API_KEY: string;
OPENAI_ASSISTANT_ID: string;
}
}الخطوة 2: فهم بنية Assistants API
قبل كتابة الكود، من المهم فهم المفاهيم الأساسية:
المساعد (Assistant) — كيان ذكاء اصطناعي مُعدّ بتعليمات محددة ونموذج وأدوات مفعّلة. اعتبره شخصية تستمر عبر المحادثات.
المحادثة (Thread) — جلسة محادثة. تخزن المحادثات سجل الرسائل الكامل وهي مستمرة — تبقى حتى بعد إعادة تشغيل الخادم ويمكن استئنافها في أي وقت.
الرسالة (Message) — رسالة واحدة من المستخدم أو المساعد ضمن محادثة. يمكن أن تتضمن نصاً وصوراً ومرفقات.
التشغيل (Run) — تنفيذ المساعد على محادثة. عند إنشاء تشغيل، يقرأ المساعد المحادثة ويقرر ما إذا كان سيستدعي أدوات ويولّد رداً.
خطوة التشغيل (Run Step) — إجراء دقيق ضمن التشغيل (استدعاء أداة، إنشاء رسالة). مفيد لتصحيح الأخطاء وعرض التقدم.
التدفق يبدو هكذا:
إنشاء المساعد → إنشاء محادثة → إضافة رسالة → إنشاء تشغيل → بث الرد
↑ |
└──────────────────────────┘
(تستمر المحادثة)
الخطوة 3: إنشاء المساعد
يمكنك إنشاء مساعد عبر الـ API أو لوحة تحكم OpenAI. لنفعل ذلك برمجياً ليكون التكوين في الكود.
أنشئ src/lib/openai.ts:
import OpenAI from "openai";
export const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});أنشئ سكريبت الإعداد في scripts/create-assistant.ts:
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
async function createAssistant() {
const assistant = await openai.beta.assistants.create({
name: "Noqta Assistant",
instructions: `You are a helpful, knowledgeable assistant. Follow these rules:
- Be concise but thorough
- Use code examples when explaining technical concepts
- When using file search results, cite the source document
- When asked to analyze data, use code interpreter to create visualizations
- Always respond in the same language the user writes in`,
model: "gpt-4o",
tools: [
{ type: "file_search" },
{ type: "code_interpreter" },
],
});
console.log("Assistant created:", assistant.id);
console.log("Add this to your .env.local:");
console.log(`OPENAI_ASSISTANT_ID=${assistant.id}`);
}
createAssistant();شغّله:
npx tsx scripts/create-assistant.tsانسخ معرّف المساعد في ملف .env.local.
نصيحة: يمكنك أيضاً إنشاء وإدارة المساعدين من OpenAI Playground. تمنحك لوحة التحكم واجهة مرئية لتعديل التعليمات واختبار الأدوات تفاعلياً.
الخطوة 4: بناء واجهة API لإدارة المحادثات
المحادثات هي العمود الفقري لـ Assistants API. أنشئ مسارات API لإدارتها.
أنشئ src/app/api/threads/route.ts:
import { NextResponse } from "next/server";
import { openai } from "@/lib/openai";
export async function POST() {
try {
const thread = await openai.beta.threads.create();
return NextResponse.json({ threadId: thread.id });
} catch (error) {
return NextResponse.json(
{ error: "Failed to create thread" },
{ status: 500 }
);
}
}ينشئ هذا المسار محادثة جديدة. يحصل كل thread على معرّف فريد ستخزنه على العميل لاستئناف المحادثات.
الخطوة 5: تنفيذ بث الردود المباشر
هنا يحدث السحر. يدعم Assistants API البث عبر Server-Sent Events، مما يمنح المستخدمين تأثير الكتابة في الوقت الفعلي.
أنشئ src/app/api/chat/route.ts:
import { openai } from "@/lib/openai";
export async function POST(request: Request) {
const { threadId, message } = await request.json();
if (!threadId || !message) {
return new Response("Missing threadId or message", { status: 400 });
}
await openai.beta.threads.messages.create(threadId, {
role: "user",
content: message,
});
const stream = openai.beta.threads.runs.stream(threadId, {
assistant_id: process.env.OPENAI_ASSISTANT_ID,
});
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
try {
for await (const event of stream) {
if (event.event === "thread.message.delta") {
const delta = event.data.delta;
if (delta.content) {
for (const block of delta.content) {
if (block.type === "text" && block.text?.value) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({
type: "text",
content: block.text.value,
})}\n\n`)
);
}
}
}
}
if (event.event === "thread.run.completed") {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: "done" })}\n\n`)
);
}
if (event.event === "thread.run.failed") {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({
type: "error",
content: "Run failed",
})}\n\n`)
);
}
}
} catch (err) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({
type: "error",
content: "Stream error",
})}\n\n`)
);
} finally {
controller.close();
}
},
});
return new Response(readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}هذا المسار:
- يضيف رسالة المستخدم إلى المحادثة
- ينشئ تشغيلاً متدفقاً ضد المساعد
- يرسل أجزاء النص للعميل كـ Server-Sent Events
- يشير إلى الاكتمال أو الأخطاء
الخطوة 6: تفعيل البحث في الملفات (RAG)
يتيح البحث في الملفات للمساعد الإجابة على الأسئلة من المستندات المرفوعة. يقوم الـ API تلقائياً بتقسيم الملفات وتضمينها وفهرستها — دون الحاجة لقاعدة بيانات متجهات خارجية.
أنشئ src/app/api/files/route.ts:
import { NextResponse } from "next/server";
import { openai } from "@/lib/openai";
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get("file") as File;
const threadId = formData.get("threadId") as string;
if (!file || !threadId) {
return NextResponse.json(
{ error: "Missing file or threadId" },
{ status: 400 }
);
}
const uploadedFile = await openai.files.create({
file,
purpose: "assistants",
});
await openai.beta.threads.messages.create(threadId, {
role: "user",
content: "I've uploaded a document for reference.",
attachments: [
{
file_id: uploadedFile.id,
tools: [{ type: "file_search" }],
},
],
});
return NextResponse.json({
fileId: uploadedFile.id,
fileName: file.name,
});
}عند رفع المستخدم لملف:
- يُرفع الملف إلى مخزن OpenAI
- يُرفق برسالة في المحادثة مع أداة
file_search - يستطيع المساعد الآن البحث في محتويات المستند عند الإجابة
أنواع الملفات المدعومة: PDF و DOCX و TXT و MD و JSON و CSV و HTML وغيرها. يمكن أن يصل حجم كل ملف إلى 512 ميغابايت. يتعامل الـ API تلقائياً مع التقسيم والتضمين — لا تحتاج لإدارة مخزن متجهات يدوياً للمرفقات على مستوى المحادثة.
الخطوة 7: تفعيل مفسر الأكواد
يتيح مفسر الأكواد للمساعد كتابة وتنفيذ أكواد Python في بيئة معزولة. وهذا فعّال لتحليل البيانات والرياضيات وتوليد الرسوم البيانية ومعالجة الملفات.
مفسر الأكواد مفعّل بالفعل في تكوين المساعد من الخطوة 3. لنتعامل الآن مع المخرجات في مسار البث.
حدّث معالج البث في src/app/api/chat/route.ts لمعالجة أحداث مفسر الأكواد:
for await (const event of stream) {
if (event.event === "thread.message.delta") {
const delta = event.data.delta;
if (delta.content) {
for (const block of delta.content) {
if (block.type === "text" && block.text?.value) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({
type: "text",
content: block.text.value,
})}\n\n`)
);
}
if (block.type === "image_file" && block.image_file) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({
type: "image",
fileId: block.image_file.file_id,
})}\n\n`)
);
}
}
}
}
if (event.event === "thread.run.step.delta") {
const stepDelta = event.data.delta;
if (stepDelta.step_details?.type === "tool_calls") {
for (const toolCall of stepDelta.step_details.tool_calls ?? []) {
if (
toolCall.type === "code_interpreter" &&
toolCall.code_interpreter?.input
) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({
type: "code",
content: toolCall.code_interpreter.input,
})}\n\n`)
);
}
}
}
}
if (event.event === "thread.run.completed") {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: "done" })}\n\n`)
);
}
if (event.event === "thread.run.failed") {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({
type: "error",
content: "Run failed",
})}\n\n`)
);
}
}الآن يُصدر البث ثلاثة أنواع من المحتوى:
- text — رسائل المساعد العادية
- code — أكواد Python التي ينفذها مفسر الأكواد
- image — رسوم بيانية أو تصورات مولّدة (تُرجع كمعرّفات ملفات)
لعرض الصور المولّدة من مفسر الأكواد، أضف نقطة نهاية لاسترجاع الملفات:
// src/app/api/files/[fileId]/route.ts
import { NextResponse } from "next/server";
import { openai } from "@/lib/openai";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ fileId: string }> }
) {
const { fileId } = await params;
const response = await openai.files.content(fileId);
const buffer = Buffer.from(await response.arrayBuffer());
return new NextResponse(buffer, {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=3600",
},
});
}الخطوة 8: بناء واجهة الدردشة
لنبنِ الآن الواجهة الأمامية. أنشئ hook مخصص لإدارة اتصال البث:
أنشئ src/hooks/use-assistant-chat.ts:
"use client";
import { useState, useCallback, useRef } from "react";
interface ChatMessage {
id: string;
role: "user" | "assistant";
content: string;
codeBlocks?: string[];
images?: string[];
}
export function useAssistantChat() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [threadId, setThreadId] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
const initThread = useCallback(async () => {
const res = await fetch("/api/threads", { method: "POST" });
const { threadId: id } = await res.json();
setThreadId(id);
return id;
}, []);
const sendMessage = useCallback(
async (content: string) => {
const currentThreadId = threadId ?? (await initThread());
setIsLoading(true);
const userMessage: ChatMessage = {
id: crypto.randomUUID(),
role: "user",
content,
};
const assistantMessage: ChatMessage = {
id: crypto.randomUUID(),
role: "assistant",
content: "",
codeBlocks: [],
images: [],
};
setMessages((prev) => [...prev, userMessage, assistantMessage]);
abortRef.current = new AbortController();
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
threadId: currentThreadId,
message: content,
}),
signal: abortRef.current.signal,
});
const reader = res.body?.getReader();
const decoder = new TextDecoder();
if (!reader) throw new Error("No reader available");
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const data = JSON.parse(line.slice(6));
if (data.type === "text") {
setMessages((prev) => {
const updated = [...prev];
const last = updated[updated.length - 1];
last.content += data.content;
return updated;
});
}
if (data.type === "code") {
setMessages((prev) => {
const updated = [...prev];
const last = updated[updated.length - 1];
last.codeBlocks = [
...(last.codeBlocks ?? []),
data.content,
];
return updated;
});
}
if (data.type === "image") {
setMessages((prev) => {
const updated = [...prev];
const last = updated[updated.length - 1];
last.images = [
...(last.images ?? []),
`/api/files/${data.fileId}`,
];
return updated;
});
}
if (data.type === "done") {
setIsLoading(false);
}
if (data.type === "error") {
setIsLoading(false);
}
}
}
} catch (err) {
if ((err as Error).name !== "AbortError") {
setIsLoading(false);
}
}
},
[threadId, initThread]
);
const stopGeneration = useCallback(() => {
abortRef.current?.abort();
setIsLoading(false);
}, []);
const resetChat = useCallback(() => {
setMessages([]);
setThreadId(null);
setIsLoading(false);
}, []);
return {
messages,
isLoading,
threadId,
sendMessage,
stopGeneration,
resetChat,
};
}الآن ابنِ مكون الدردشة. أنشئ src/components/chat.tsx:
"use client";
import { useState, useRef, useEffect } from "react";
import { useAssistantChat } from "@/hooks/use-assistant-chat";
export function Chat() {
const { messages, isLoading, sendMessage, stopGeneration, resetChat } =
useAssistantChat();
const [input, setInput] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
sendMessage(input.trim());
setInput("");
};
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto">
<header className="flex items-center justify-between p-4 border-b">
<h1 className="text-lg font-semibold">المساعد الذكي</h1>
<button
onClick={resetChat}
className="text-sm text-gray-500 hover:text-gray-700"
>
محادثة جديدة
</button>
</header>
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-gray-400 mt-20">
<p className="text-xl mb-2">ابدأ محادثة</p>
<p className="text-sm">
اطرح أسئلة، ارفع ملفات، أو اطلب تحليل بيانات.
</p>
</div>
)}
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${
msg.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-3 ${
msg.role === "user"
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-900"
}`}
>
<p className="whitespace-pre-wrap">{msg.content}</p>
{msg.codeBlocks?.map((code, i) => (
<pre
key={i}
className="mt-2 p-3 bg-gray-900 text-green-400 rounded-lg text-sm overflow-x-auto"
>
<code>{code}</code>
</pre>
))}
{msg.images?.map((src, i) => (
<img
key={i}
src={src}
alt="رسم بياني مولّد"
className="mt-2 rounded-lg max-w-full"
/>
))}
</div>
</div>
))}
{isLoading && messages[messages.length - 1]?.content === "" && (
<div className="flex justify-start">
<div className="bg-gray-100 rounded-2xl px-4 py-3">
<div className="flex space-x-1">
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:0.1s]" />
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:0.2s]" />
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="اكتب رسالتك..."
className="flex-1 rounded-xl border border-gray-300 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
/>
{isLoading ? (
<button
type="button"
onClick={stopGeneration}
className="px-6 py-3 rounded-xl bg-red-500 text-white font-medium hover:bg-red-600 transition-colors"
>
إيقاف
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="px-6 py-3 rounded-xl bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
إرسال
</button>
)}
</div>
</form>
</div>
);
}أخيراً، أضف الدردشة لصفحتك. حدّث src/app/page.tsx:
import { Chat } from "@/components/chat";
export default function Home() {
return <Chat />;
}الخطوة 9: إضافة رفع الملفات للواجهة
لنضف زر رفع ملفات لواجهة الدردشة ليتمكن المستخدمون من إرفاق مستندات.
أنشئ src/components/file-upload.tsx:
"use client";
import { useRef, useState } from "react";
interface FileUploadProps {
threadId: string | null;
onUploadComplete: (fileName: string) => void;
disabled?: boolean;
}
export function FileUpload({
threadId,
onUploadComplete,
disabled,
}: FileUploadProps) {
const fileRef = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !threadId) return;
setUploading(true);
const formData = new FormData();
formData.append("file", file);
formData.append("threadId", threadId);
try {
const res = await fetch("/api/files", {
method: "POST",
body: formData,
});
if (res.ok) {
const { fileName } = await res.json();
onUploadComplete(fileName);
}
} finally {
setUploading(false);
if (fileRef.current) fileRef.current.value = "";
}
};
return (
<>
<input
ref={fileRef}
type="file"
onChange={handleUpload}
accept=".pdf,.docx,.txt,.md,.csv,.json"
className="hidden"
/>
<button
type="button"
onClick={() => fileRef.current?.click()}
disabled={disabled || uploading || !threadId}
className="p-3 rounded-xl border border-gray-300 hover:bg-gray-50 disabled:opacity-50 transition-colors"
title="رفع ملف"
>
{uploading ? (
<svg
className="w-5 h-5 animate-spin text-gray-500"
viewBox="0 0 24 24"
fill="none"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
) : (
<svg
className="w-5 h-5 text-gray-500"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13"
/>
</svg>
)}
</button>
</>
);
}الخطوة 10: حفظ المحادثات في التخزين المحلي
للسماح للمستخدمين باستئناف المحادثات عبر إعادة تحميل الصفحة، احفظ معرّفات المحادثات في التخزين المحلي.
حدّث src/hooks/use-assistant-chat.ts لإضافة الحفظ:
const STORAGE_KEY = "openai-chat-threads";
interface ThreadInfo {
id: string;
title: string;
createdAt: string;
}
function getStoredThreads(): ThreadInfo[] {
if (typeof window === "undefined") return [];
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
}
function storeThread(thread: ThreadInfo) {
const threads = getStoredThreads();
threads.unshift(thread);
localStorage.setItem(STORAGE_KEY, JSON.stringify(threads.slice(0, 50)));
}أضف مسار API لسجل المحادثة. أنشئ src/app/api/threads/[threadId]/messages/route.ts:
import { NextResponse } from "next/server";
import { openai } from "@/lib/openai";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ threadId: string }> }
) {
const { threadId } = await params;
try {
const messages = await openai.beta.threads.messages.list(threadId, {
order: "asc",
});
return NextResponse.json({ messages: messages.data });
} catch {
return NextResponse.json(
{ error: "Thread not found" },
{ status: 404 }
);
}
}الخطوة 11: اعتبارات الإنتاج
قبل النشر، عالج هذه المخاوف المهمة:
تحديد معدل الطلبات
احمِ مسارات API من سوء الاستخدام:
// src/lib/rate-limit.ts
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
export function rateLimit(
identifier: string,
maxRequests = 20,
windowMs = 60_000
): boolean {
const now = Date.now();
const entry = rateLimitMap.get(identifier);
if (!entry || now > entry.resetTime) {
rateLimitMap.set(identifier, { count: 1, resetTime: now + windowMs });
return true;
}
if (entry.count >= maxRequests) {
return false;
}
entry.count++;
return true;
}إدارة التكاليف
يفرض Assistants API رسوماً على:
- استخدام الرموز — رموز الإدخال والإخراج بسعر النموذج
- تخزين الملفات — 0.20 دولار لكل جيجابايت يومياً
- جلسات مفسر الأكواد — 0.03 دولار لكل جلسة
للتحكم بالتكاليف:
- حدد
max_prompt_tokensوmax_completion_tokensفي التشغيلات - نظّف المحادثات والملفات القديمة دورياً
- راقب الاستخدام عبر لوحة تحكم OpenAI
const stream = openai.beta.threads.runs.stream(threadId, {
assistant_id: process.env.OPENAI_ASSISTANT_ID,
max_prompt_tokens: 50000,
max_completion_tokens: 4096,
});اختبار التنفيذ
شغّل خادم التطوير:
npm run devاختبر هذه السيناريوهات:
- محادثة أساسية — أرسل رسالة وتحقق من عمل البث
- أسئلة متابعة — تأكد أن المساعد يتذكر السياق من وقت سابق في المحادثة
- رفع ملف — ارفع PDF واطرح أسئلة عن محتواه
- مفسر الأكواد — اطلب "أنشئ رسماً بيانياً شريطياً يوضح عدد سكان أكبر 5 دول"
- استعادة الأخطاء — اقطع اتصال الشبكة لفترة وجيزة وتحقق من تعامل الواجهة معه
- محادثة جديدة — انقر "محادثة جديدة" وتحقق من إنشاء محادثة جديدة
استكشاف الأخطاء
خطأ "Assistant not found"
تحقق أن OPENAI_ASSISTANT_ID في .env.local يطابق مساعداً في حساب OpenAI الخاص بك. المساعدون مرتبطون بمؤسسة مفتاح API.
توقف البث في منتصف الرد
عادة يعني أن التشغيل وصل لحد الرموز أو انتهت مهلته. زِد max_prompt_tokens أو تحقق من أوقات تنفيذ الأدوات الطويلة.
البحث في الملفات لا يرجع نتائج
تأكد أن الملف رُفع بـ purpose: "assistants". الملفات المرفوعة لأغراض أخرى لا تُفهرس للبحث. تحقق أيضاً من دعم صيغة الملف.
مفسر الأكواد ينتهي بالمهلة العمليات الحسابية المعقدة قد تتجاوز مهلة التنفيذ. قسّم الطلبات المعقدة لخطوات أصغر أو اطلب من المساعد تبسيط نهجه.
خطأ "Run already active"
يمكن تشغيل run واحد فقط لكل محادثة في وقت واحد. انتظر اكتمال التشغيل الحالي قبل بدء آخر، أو ألغِ التشغيل النشط باستخدام openai.beta.threads.runs.cancel(threadId, runId).
الخطوات التالية
الآن بعد أن أصبح لديك روبوت محادثة يعمل، فكر في هذه التحسينات:
- المصادقة — أضف مصادقة المستخدم مع NextAuth.js أو Clerk لربط المحادثات بكل مستخدم
- مخازن المتجهات — أنشئ مخازن متجهات مشتركة لقواعد معرفة على مستوى المؤسسة
- استدعاء الدوال — أضف دوال مخصصة ليتمكن المساعد من التفاعل مع واجهاتك البرمجية
- عرض Markdown — استخدم
react-markdownمع تلوين بناء الجملة للردود المنسقة - التوافق مع الهواتف — حسّن تخطيط الدردشة للشاشات الصغيرة
- التحليلات — تتبع استخدام الرموز وأوقات الاستجابة ورضا المستخدمين
الخلاصة
لقد بنيت روبوت محادثة متكامل باستخدام OpenAI Assistants API و Next.js. يلغي Assistants API الكثير من الكود المتكرر المرتبط ببناء تطبيقات الذكاء الاصطناعي — إدارة المحادثات وفهرسة المستندات وتنفيذ الأكواد كلها تتم عبر المنصة.
الميزة الرئيسية لهذا النهج هي الاستمرارية. على عكس إكمالات الدردشة عديمة الحالة، تحفظ محادثاتك سجل المحادثة الكامل، وتبقى الملفات المرفقة مفهرسة، ويمكن للمستخدمين المتابعة من حيث توقفوا. هذا يجعل Assistants API مناسباً بشكل خاص لدعم العملاء وقواعد المعرفة الداخلية وأدوات تحليل البيانات.
الكود من هذا الدليل أساس متين. وسّعه بالمصادقة والأدوات المخصصة وواجهة مستخدم مصقولة لإنشاء مساعد ذكي مخصص لحالة استخدامك.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

إنشاء بودكاست من ملف PDF باستخدام Vercel AI SDK و LangChain
تعلم كيفية إنشاء بودكاست من ملف PDF باستخدام Vercel AI SDK و PDFLoader من LangChain و ElevenLabs و Next.js.

ضبط دقيق لـ GPT مع OpenAI و Next.js و Vercel AI SDK
تعلم كيفية ضبط GPT-4o بدقة باستخدام OpenAI و Next.js و Vercel AI SDK لإنشاء Shooketh، روبوت AI مستوحى من شكسبير.

تنفيذ RAG على ملفات PDF باستخدام البحث في الملفات في واجهة برمجة تطبيقات Responses
دليل شامل حول الاستفادة من واجهة برمجة تطبيقات Responses للتوليد المعزز بالاسترجاع (RAG) الفعال على مستندات PDF.