كل فريق يبني تطبيقات مدعومة بالذكاء الاصطناعي يصطدم في نهاية المطاف بنفس العقبة: التطبيق يعمل في بيئة التطوير، لكن في الإنتاج تحدث أخطاء لا يمكن تفسيرها. مستخدم يُبلّغ عن إجابة سيئة، أو تقفز التكاليف فجأة بين ليلة وضحاها، أو يرتفع وقت الاستجابة لأنواع معينة من المدخلات — ولا أحد يعرف ما الذي تغيّر.
أدوات المراقبة التقليدية تقيس البنية التحتية — المعالج، الذاكرة، مدة الطلب. تخبرك أن الطلب استغرق 900ms، لكنها لا تخبرك لماذا أعطى النموذج إجابة رديئة، أو أي إصدار من الـ Prompt هو المسؤول، أو كيف تتطور تكاليف التوكن لكل شريحة من المستخدمين.
Langfuse هو المنصة الرائدة مفتوحة المصدر لهندسة نماذج اللغة، وقد بُنيت تحديدًا لسد هذه الفجوة. إنها تمنح تطبيق الذكاء الاصطناعي الخاص بك نفس مستوى الرؤية الذي تمنحه Datadog لبنيتك التحتية: شجرة تتبع كاملة، إدارة Prompts مع نسخ متعددة واختبار A/B، حلقات تغذية راجعة من المستخدمين، خطوط تقييم تلقائية، ولوحة تحكم لتحليل التكاليف — كل ذلك في حزمة قابلة للاستضافة الذاتية.
في هذا الدرس ستبني شات بوت باستخدام Next.js وتُضيف إليه مراقبة كاملة من البداية إلى النهاية باستخدام Langfuse.
المتطلبات الأساسية
قبل البدء، تأكد من توفر ما يلي:
- Node.js 20+ مثبّت (تحقق بـ
node -v) - مشروع Next.js 15 (أو أنشئ واحدًا بـ
npx create-next-app@latest) - مفتاح OpenAI API (أو أي مزوّد نماذج آخر)
- حساب مجاني على Langfuse في cloud.langfuse.com — أو Docker للاستضافة الذاتية
- معرفة جيدة بـ TypeScript وبـ Next.js App Router
ما الذي ستبنيه
بنهاية هذا الدرس سيتمتع الشات بوت بما يلي:
- تتبع كامل لاستدعاءات الذكاء الاصطناعي — كل طلب مُسجَّل في Langfuse مع المدخلات والمخرجات والتوكن
- نطاقات متداخلة — تتبع كل عملية فرعية (استرداد، إعادة ترتيب، توليد) بشكل منفصل
- سياق المستخدم والجلسة — تجميع التتبعات حسب معرّف المستخدم والجلسة
- ملاحظات المستخدمين — واجهة إعجاب/عدم إعجاب مرتبطة مباشرة بنتائج Langfuse
- إدارة الـ Prompts — جلب الـ Prompts من لوحة Langfuse بدلاً من ترميزها في الكود
- تقييم تلقائي — تقييم باستخدام نموذج لغة كحكم يعمل بشكل غير متزامن بعد كل إجابة
لماذا مراقبة نماذج اللغة ضرورية في 2026
الانتقال من النموذج الأولي إلى الإنتاج يكشف عن فئة من الأخطاء تفوت أدوات المراقبة العادية تمامًا:
- انتكاسات الـ Prompt — تغيير في الصياغة يبدو بسيطًا يُدهور جودة الإجابات لأنماط استعلام محددة
- سوء استخدام نافذة السياق — يُقطع المحادثة بصمت حين تتجاوز سجل الرسائل حدًّا معينًا
- مجموعات الهلوسة — مجموعة من المدخلات تستدعي باستمرار إجابات خاطئة بثقة
- شذوذات تكلفة التوكن — استعلام واحد ينفق توكنًا أكثر بـ 50 مرة من المتوقع
- تفاوت وقت الاستجابة — وقت الاستجابة عند النسبة المئوية التاسعة والتسعين أعلى بـ 10 أضعاف من المتوسط
بدون تتبع لن تستطيع الاستجابة إلا بعد شكاوى المستخدمين. مع Langfuse لديك رؤية كاملة لاكتشاف هذه المشكلات قبل أن تؤثر على الاحتفاظ بالمستخدمين.
الخطوة 1: تثبيت المكتبات
ابدأ من جذر مشروع Next.js:
npm install langfuse openaiأضف متغيرات البيئة في ملف .env.local:
# OpenAI
OPENAI_API_KEY=sk-...
# Langfuse — من إعدادات مشروعك في cloud.langfuse.com
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.comللاستضافة الذاتية، استبدل LANGFUSE_BASE_URL بعنوان خادمك (مثلاً https://langfuse.yourcompany.com).
الخطوة 2: تهيئة عميل Langfuse
أنشئ نسخة مفردة (singleton) من العميل لتجنب فتح اتصالات متعددة في كل طلب:
// lib/langfuse.ts
import { Langfuse } from "langfuse";
export const langfuse = new Langfuse({
secretKey: process.env.LANGFUSE_SECRET_KEY!,
publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
baseUrl: process.env.LANGFUSE_BASE_URL ?? "https://cloud.langfuse.com",
flushAt: 1, // أرسل فورًا في بيئة serverless
flushInterval: 0, // أوقف إرسال الدفعات المعتمد على المؤقت
});التفريغ في بيئة serverless أمر بالغ الأهمية. في بيئة serverless مثل Vercel أو AWS Lambda، تُوقَف العملية فور إرسال الرد — قبل أن يعمل مؤقت الدُّفعات. حدّد flushAt: 1 واستدعِ دائمًا await langfuse.flushAsync() في نهاية كل معالج لضمان عدم ضياع أي أحداث.
الخطوة 3: تتبع أول استدعاء لنموذج اللغة
أنشئ مسار API بسيطًا يُغلّف استدعاء OpenAI داخل تتبع Langfuse. التتبع يمثل عملية منطقية واحدة من منظور المستخدم (مثل دورة محادثة واحدة). داخل التتبع تنشئ توليدًا لتسجيل استدعاء النموذج مع نموذجه ومدخلاته ومخرجاته واستخدام التوكن.
// app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { langfuse } from "@/lib/langfuse";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function POST(req: NextRequest) {
const { messages, userId, sessionId } = await req.json();
// 1. ابدأ تتبعًا لهذه الدورة
const trace = langfuse.trace({
name: "chat-turn",
userId: userId ?? "anonymous",
sessionId: sessionId ?? "default",
input: { messages },
tags: ["chat", process.env.NODE_ENV ?? "development"],
});
// 2. سجّل استدعاء النموذج كـ generation داخل التتبع
const generation = trace.generation({
name: "openai-gpt4o",
model: "gpt-4o",
modelParameters: {
temperature: 0.7,
maxTokens: 1024,
},
input: messages,
});
try {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
max_tokens: 1024,
temperature: 0.7,
});
const content = response.choices[0].message.content ?? "";
// 3. أغلق التوليد مع المخرجات واستخدام التوكن
generation.end({
output: content,
usage: {
promptTokens: response.usage?.prompt_tokens,
completionTokens: response.usage?.completion_tokens,
totalTokens: response.usage?.total_tokens,
},
});
trace.update({ output: { content } });
// 4. فرّغ قبل إنهاء الدالة
await langfuse.flushAsync();
return NextResponse.json({
content,
traceId: trace.id,
});
} catch (error) {
generation.end({
level: "ERROR",
statusMessage: String(error),
});
await langfuse.flushAsync();
throw error;
}
}بعد تنفيذ أول طلب، افتح لوحة Langfuse وانتقل إلى Traces. ستجد التتبع مع المدخلات والمخرجات الكاملة، اسم النموذج، عدد التوكن، وقت الاستجابة، والتكلفة التقديرية.
الخطوة 4: نطاقات متداخلة للتطبيقات المعقدة
التطبيقات الحقيقية تجري أكثر من استدعاء واحد لنموذج اللغة في كل طلب. خط أنابيب RAG، مثلاً، يسترد الوثائق ويُعيد ترتيبها ثم يبني نافذة السياق قبل توليد الإجابة. يتيح لك Langfuse تداخل النطاقات (spans) داخل التتبع لجعل هذا مرئيًا.
// app/api/rag-chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { langfuse } from "@/lib/langfuse";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function retrieveDocuments(query: string) {
return [
{ id: "doc-1", content: "مقطع ذو صلة بالموضوع..." },
{ id: "doc-2", content: "مقطع آخر ذو صلة..." },
];
}
export async function POST(req: NextRequest) {
const { query, userId, sessionId } = await req.json();
const trace = langfuse.trace({
name: "rag-chat",
userId,
sessionId,
input: { query },
});
// النطاق 1: استرداد الوثائق
const retrievalSpan = trace.span({
name: "vector-retrieval",
input: { query },
});
const documents = await retrieveDocuments(query);
retrievalSpan.end({
output: { documentCount: documents.length, documentIds: documents.map((d) => d.id) },
});
// النطاق 2: بناء السياق
const contextSpan = trace.span({ name: "context-construction" });
const context = documents.map((d) => d.content).join("\n\n");
const messages: OpenAI.ChatCompletionMessageParam[] = [
{ role: "system", content: "أجب باستخدام السياق المقدم فقط." },
{ role: "user", content: `السياق:\n${context}\n\nالسؤال: ${query}` },
];
contextSpan.end({ output: { tokenEstimate: context.length / 4 } });
// النطاق 3: توليد الإجابة
const generation = trace.generation({
name: "answer-generation",
model: "gpt-4o",
input: messages,
});
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
});
const answer = response.choices[0].message.content ?? "";
generation.end({
output: answer,
usage: {
promptTokens: response.usage?.prompt_tokens,
completionTokens: response.usage?.completion_tokens,
totalTokens: response.usage?.total_tokens,
},
});
trace.update({ output: { answer } });
await langfuse.flushAsync();
return NextResponse.json({ answer, traceId: trace.id });
}في لوحة Langfuse يظهر التتبع الآن كشجرة: التتبع الجذر مع ثلاثة أبناء (نطاق الاسترداد، نطاق السياق، التوليد). يمكنك رؤية أين يُقضى الوقت بالضبط وكيف تُسهم كل خطوة في الإجابة النهائية.
الخطوة 5: جمع ملاحظات المستخدمين
ربط إعجاب/عدم إعجاب المستخدمين بتتبعاتك يُغلق الحلقة بين تجربة المستخدم وأداء النموذج. يُسمّي Langfuse هذه النتائج (scores) — يمكن أن تأتي من المستخدمين أو من تقييمات تلقائية أو مراجعين بشريين.
أولاً، اكشف نقطة نهاية للملاحظات:
// app/api/feedback/route.ts
import { NextRequest, NextResponse } from "next/server";
import { langfuse } from "@/lib/langfuse";
export async function POST(req: NextRequest) {
const { traceId, value, comment } = await req.json();
// value: 1 للإعجاب، 0 لعدم الإعجاب
langfuse.score({
traceId,
name: "user-feedback",
value,
comment: comment ?? undefined,
dataType: "BOOLEAN",
});
await langfuse.flushAsync();
return NextResponse.json({ ok: true });
}ثم اربط واجهة المستخدم:
// components/FeedbackButtons.tsx
"use client";
import { useState } from "react";
interface FeedbackButtonsProps {
traceId: string;
}
export function FeedbackButtons({ traceId }: FeedbackButtonsProps) {
const [submitted, setSubmitted] = useState<boolean | null>(null);
const sendFeedback = async (value: 0 | 1) => {
setSubmitted(value === 1);
await fetch("/api/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ traceId, value }),
});
};
if (submitted !== null) {
return <p className="text-sm text-gray-500">شكرًا على ملاحظتك!</p>;
}
return (
<div className="flex gap-2 mt-2">
<button
onClick={() => sendFeedback(1)}
className="px-3 py-1 text-sm border rounded hover:bg-green-50"
>
👍 مفيد
</button>
<button
onClick={() => sendFeedback(0)}
className="px-3 py-1 text-sm border rounded hover:bg-red-50"
>
👎 غير مفيد
</button>
</div>
);
}مرّر traceId الذي تُعيده واجهة API إلى هذا المكوّن بعد كل رد. تظهر النتائج فورًا في Langfuse حيث يمكنك تصفية التتبعات حسب النتيجة، ومعرفة الجلسات التي تحتوي على أكبر قدر من التغذية الراجعة السلبية، وربط انخفاضات الجودة بإصدارات Prompt أو تغييرات النموذج.
الخطوة 6: إدارة الـ Prompts من لوحة التحكم
ترميز الـ Prompts في الكود المصدري يعني أن كل تحسين يتطلب نشرًا جديدًا. إدارة الـ Prompts في Langfuse تتيح لك إصدار الـ Prompts واختبارها A/B من لوحة التحكم — وجلب الإصدار النشط في وقت التشغيل دون نشر.
إنشاء Prompt في Langfuse
- في لوحة Langfuse انتقل إلى Prompts → New Prompt
- سمّه
chat-system-prompt - الصق الـ Prompt مستخدمًا صيغة المتغيرات بين قوسين:
أنت مساعد مفيد لشركة {{company_name}}. - انشره بتسمية
production
جلب الـ Prompt واستخدامه
// lib/prompts.ts
import { langfuse } from "@/lib/langfuse";
export async function getSystemPrompt(companyName: string): Promise<string> {
const prompt = await langfuse.getPrompt("chat-system-prompt", undefined, {
label: "production",
cacheTtlSeconds: 60,
});
return prompt.compile({ company_name: companyName });
}اربط الـ Prompt بتوليد التتبع:
const promptObj = await langfuse.getPrompt("chat-system-prompt", undefined, {
label: "production",
cacheTtlSeconds: 60,
});
const systemPrompt = promptObj.compile({ company_name: "نقطة" });
const generation = trace.generation({
name: "openai-gpt4o",
model: "gpt-4o",
input: [
{ role: "system", content: systemPrompt },
...messages,
],
prompt: promptObj,
});تثبيت إصدار الـ Prompt. بشكل افتراضي تجلب getPrompt التسمية production. لإصدار محدد (مثلاً في التهيئة)، مرّر رقم الإصدار كمعامل ثانٍ: langfuse.getPrompt("chat-system-prompt", 3). استخدم التسميات (production، staging، canary) لإدارة طرح التحديثات دون لمس الكود.
الخطوة 7: التقييم التلقائي
ملاحظات المستخدمين ذات قيمة كبيرة لكنها نادرة. التقييم التلقائي يتيح لك تقييم 100% من تتبعاتك باستخدام فحوصات قواعد بسيطة أو نموذج لغة كحكم.
التقييم القائم على القواعد
// lib/evaluators.ts
import { langfuse } from "@/lib/langfuse";
export async function evaluateResponse(
traceId: string,
generationId: string,
output: string,
expectedKeywords: string[]
): Promise<void> {
// النتيجة 1: فحص طول الرد
const lengthScore = output.split(" ").length > 20 ? 1 : 0;
langfuse.score({
traceId,
observationId: generationId,
name: "response-length-ok",
value: lengthScore,
dataType: "BOOLEAN",
});
// النتيجة 2: تغطية الكلمات المفتاحية
const covered = expectedKeywords.filter((kw) =>
output.toLowerCase().includes(kw.toLowerCase())
).length;
const coverageScore = expectedKeywords.length > 0
? covered / expectedKeywords.length
: 1;
langfuse.score({
traceId,
observationId: generationId,
name: "keyword-coverage",
value: coverageScore,
dataType: "NUMERIC",
comment: `${covered}/${expectedKeywords.length} كلمات مفتاحية موجودة`,
});
await langfuse.flushAsync();
}نموذج لغة كحكم
// lib/llm-judge.ts
import OpenAI from "openai";
import { langfuse } from "@/lib/langfuse";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function llmJudge(
traceId: string,
question: string,
answer: string
): Promise<void> {
const judgeTrace = langfuse.trace({ name: "llm-judge", tags: ["eval"] });
const judgeGeneration = judgeTrace.generation({
name: "judge-call",
model: "gpt-4o-mini",
input: [
{
role: "user",
content: `قيّم الإجابة التالية على مقياس من 0 إلى 1 من حيث الدقة والفائدة. أعد كائن JSON فقط: {"score": 0.0, "reason": "..."}.
السؤال: ${question}
الإجابة: ${answer}`,
},
],
});
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "user",
content: `قيّم الإجابة التالية على مقياس من 0 إلى 1 من حيث الدقة والفائدة. أعد كائن JSON فقط بمفتاحَي "score" و"reason".
السؤال: ${question}
الإجابة: ${answer}`,
},
],
response_format: { type: "json_object" },
});
const raw = response.choices[0].message.content ?? '{"score":0,"reason":"خطأ في التحليل"}';
const parsed = JSON.parse(raw) as { score: number; reason: string };
judgeGeneration.end({ output: parsed });
langfuse.score({
traceId,
name: "llm-judge-quality",
value: parsed.score,
dataType: "NUMERIC",
comment: parsed.reason,
});
await langfuse.flushAsync();
}استدعِ llmJudge بشكل غير متزامن (عبر مهمة خلفية مثل Hatchet أو Trigger.dev) بعد كل دورة محادثة حتى لا يُضيف أي تأخر إلى مسار الاستجابة الرئيسي.
الخطوة 8: قائمة التحقق للإنتاج
أخذ عينات من التتبعات ذات الحجم الكبير
إذا كنت تعالج آلاف الطلبات في الدقيقة، فقد يكون تتبع 100% مكلفًا. أضف بوابة أخذ عينات بسيطة:
export function shouldTrace(sampleRate = 0.1): boolean {
return Math.random() < sampleRate;
}الاستضافة الذاتية لـ Langfuse
Langfuse مرخص بـ MIT وتشحن بـ Docker Compose للاستضافة الذاتية. مثالي لمتطلبات إقامة البيانات في الأسواق العربية والأوروبية:
git clone https://github.com/langfuse/langfuse.git
cd langfuse
cp .env.example .env
# عدّل .env: حدد NEXTAUTH_SECRET وDATABASE_URL وSALT
docker compose up -dعزل البيئات
استخدم المشاريع في Langfuse لفصل بيانات التطوير والاختبار والإنتاج. كل مشروع له زوج مفاتيح خاص به.
لا تُسجّل بيانات المستخدمين الحساسة. تتبعات Langfuse مُخزَّنة وقد تكون مرئية لفريقك. قبل تمرير الرسائل إلى trace.input() أو generation.input()، احذف أو غيّر بيانات PII (الأسماء، البريد الإلكتروني، أرقام الهوية) التي لا ينبغي أن تظهر في أدوات المراقبة.
استكشاف الأخطاء وإصلاحها
التتبعات لا تظهر في لوحة التحكم
- تأكد من صحة
LANGFUSE_SECRET_KEYوLANGFUSE_PUBLIC_KEY(يبدآن بـsk-lf-وpk-lf-) - تأكد من استدعاء
await langfuse.flushAsync()قبل إعادة الدالة - تحقق من أن
LANGFUSE_BASE_URLيتطابق مع URL السحابة أو الاستضافة الذاتية
عدد التوكن يظهر صفرًا
- Langfuse يقرأ عدد التوكن من الكائن
usageالذي تمرره لـgeneration.end(). تأكد من تمريرresponse.usage.prompt_tokensوresponse.usage.completion_tokensوresponse.usage.total_tokensمن رد OpenAI.
getPrompt تُعيد خطأ 404
- يجب أن يكون الـ Prompt موجودًا في Langfuse وأن يكون له إصدار واحد على الأقل بالتسمية المطلوبة (
productionافتراضيًا).
ارتفاع وقت الاستجابة بسبب التتبع
- استدعاءات SDK في Langfuse غير متزامنة — تُضيف الأحداث لقائمة انتظار وتُرسلها بشكل غير متزامن. العمل الوحيد المتزامن هو
getPrompt(مخزَّن مؤقتًا بعد الاستدعاء الأول). إذا رأيت ارتفاعًا في وقت الاستجابة، فعّل خيارcacheTtlSecondsفيgetPrompt.
الخطوات التالية
مع تجهيز خط أنابيب المراقبة، إليك توسعات طبيعية:
- RAG الذكي مع Next.js — ادمج تتبعات Langfuse مع وكيل استرداد متعدد الخطوات
- Claude Agent SDK لـ TypeScript — أضف تتبعات Langfuse لسير عمل الوكلاء المدعومين بـ Claude
- سير عمل n8n متعدد الوكلاء — نسّق وكلاء ذكاء اصطناعي متعددة وتتبع كل منها
- مجموعات بيانات Langfuse — احتفظ بالأمثلة المتتبعة في مجموعات بيانات للتقييم الانحدار
- التقييم الفوري — اضبط Langfuse لتشغيل حكم LLM تلقائيًا على كل تتبع جديد عبر Webhooks
الخلاصة
أصبح الآن لديك طبقة مراقبة احترافية على تطبيق Next.js الذكي. كل استدعاء لنموذج اللغة متتبَّع مع السياق الكامل، يمكن للمستخدمين الإشارة إلى الجودة عبر أزرار الملاحظات، الـ Prompts ذات إصدارات ويمكن إدارتها دون نشر، والمقيّمون التلقائيون يُقيّمون جودة الإجابات باستمرار.
القدرة على رؤية ما يفعله الذكاء الاصطناعي — ليس فقط إذا كان قد نجح أم فشل، بل كيف ولماذا — هي ما يُفرّق بين النماذج الأولية والمنتجات الحقيقية. Langfuse تمنحك هذه الرؤية بمرونة مفتوحة المصدر، سواء أدرتها على سحابتهم أو استضفتها بنفسك.
ابنِ ذكاءً اصطناعيًا يمكن مراقبته، لا صناديق سوداء.