الكتابات/tutorial/2026/06
Tutorial5 يونيو 2026·30 دقيقة

بناء وكلاء ذكاء اصطناعي بحالة دائمة باستخدام Cloudflare Agents SDK وDurable Objects

تعلّم كيفية بناء وكلاء ذكاء اصطناعي دائمين ومحتفظين بحالتهم على Cloudflare باستخدام Agents SDK وDurable Objects. يغطي هذا الدرس حالة الوكيل، والتخزين عبر SQLite، والمزامنة الحية مع React عبر useAgent، واستدعاء نماذج الذكاء الاصطناعي، والمهام المجدولة، والاستدعاء من الخادم — كل ذلك منشور على الحافة.

تعاني معظم أُطر عمل وكلاء الذكاء الاصطناعي من مشكلة في الذاكرة. فبمجرد انتهاء الطلب، ينسى الوكيل كل شيء — محادثته، وقائمة مهامه، وعمله غير المكتمل. ولجعله دائمًا، تضيف قاعدة بيانات، وطابورًا، وخادم WebSocket، ومجدول مهام، ثم تقضي أيامًا في ربطها معًا.

تسلك حزمة Cloudflare Agents SDK مسارًا مختلفًا. فكل وكيل هو Durable Object: نسخة حوسبة صغيرة أحادية الخيط لها قاعدة بيانات SQLite مدمجة خاصة بها تعيش على الحافة، قريبة من مستخدميك. تُحفظ الحالة تلقائيًا بين الطلبات. ويمكن للنسخة نفسها أن تُبقي اتصال WebSocket مفتوحًا، وتشغّل مهامًا مجدولة، وتستدعي نماذج الذكاء الاصطناعي، وتبث التحديثات إلى واجهة React — دون أن تُجهّز خادمًا واحدًا.

في هذا الدرس ستبني وكيل مساعد بحثي دائمًا. يحصل كل مستخدم على نسخة وكيل خاصة به تتذكر ملاحظاته المحفوظة، وتجيب عن الأسئلة بنموذج ذكاء اصطناعي، وتسجّل كل تفاعل في SQLite، وتزامن حالتها حيًّا مع واجهة React، وتشغّل ملخصًا يوميًا مجدولًا. بنهاية الدرس ستفهم نموذج الوكيل ذي الحالة بالكامل وكيفية نشره على شبكة Cloudflare.

المتطلبات المسبقة

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

  • Node.js 20+ مثبّتًا
  • حساب Cloudflare مجاني — سجّل في dash.cloudflare.com
  • معرفة أساسية بـ TypeScript وReact
  • إلمام بفكرة الدوال عديمة الخادم (لا تحتاج إلى خبرة سابقة بـ Workers)
  • محرر أكواد (يُنصح بـ VS Code)

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

وكيل مساعد بحثي يتضمّن:

  • نسخ وكيل لكل مستخدم — كل نسخة مُسمّاة هي Durable Object معزول
  • حالة دائمة تنجو من إعادة التشغيل، تُزامَن تلقائيًا مع العملاء
  • تسجيل عبر SQLite لكل سؤال وجواب باستخدام قاعدة البيانات المدمجة this.sql
  • استدعاء نماذج الذكاء الاصطناعي عبر Workers AI (قابل للتبديل بـ OpenAI أو أي مزوّد)
  • لوحة تحكم React حية موصولة عبر خطّاف useAgent
  • ملخص يومي مجدول يعمل وفق تعبير cron
  • استدعاء من الخادم (RPC) لتشغيل الوكلاء من مسارات API عادية

لنبدأ.

الخطوة 1: إنشاء هيكل المشروع

أسرع طريقة للبدء هي القالب الرسمي، لكن لفهم كل جزء متحرك سنبني انطلاقًا من مشروع Workers نظيف.

npm create cloudflare@latest research-agent -- --type hello-world --ts
cd research-agent

اختر لا عند سؤالك "هل تريد النشر؟". الآن ثبّت حزمة Agents SDK:

npm install agents

تمنحك حزمة agents الصنف الأساسي Agent، ومساعدات التوجيه، وعميل React. هذه التبعية الوحيدة هي كل ما يحتاجه وقت التشغيل.

الخطوة 2: فهم البنية

قبل كتابة الكود، يفيد فهم النموذج الذي يجعل هذا مختلفًا عن دالة عديمة الخادم عادية.

  • نسخة واحدة لكل اسم. عندما تخاطب وكيلًا باسم — مثل user-42 — توجّه Cloudflare كل طلب لهذا الاسم إلى نفس نسخة Durable Object، في أي مكان في العالم. تلك النسخة هي وكيلك.
  • الحالة دائمة. لكل نسخة تخزين خاص مدعوم بـ SQLite. كل ما تكتبه عبر this.setState() أو this.sql يبقى موجودًا في الطلب التالي، أو اليوم التالي، أو الإصدار التالي.
  • أحادية الخيط. ضمن النسخة الواحدة، تُعالَج الطلبات واحدًا تلو الآخر. تحصل على اتساق قوي مجانًا — دون تسابق على حالة وكيلك.
  • تستيقظ وتسبت. عند الخمول، تسبت النسخة لتوفير الموارد وتستيقظ فورًا عند وصول طلب جديد أو مهمة مجدولة.

هذا ما يجعل الوكيل يبدو كائنًا طويل العمر ذا حالة بدلًا من دالة عديمة الحالة.

الخطوة 3: تعريف أول وكيل لك

أنشئ الملف src/index.ts وعرّف وكيلًا يحمل حالة. يرث كل وكيل من الصنف الأساسي Agent مع وسيطَي نوع: روابط البيئة وشكل حالته.

import { Agent, routeAgentRequest } from "agents";
 
// شكل ملاحظة محفوظة واحدة
type Note = {
  id: string;
  text: string;
  createdAt: number;
};
 
// الحالة الدائمة للوكيل
type ResearchState = {
  notes: Note[];
  questionsAsked: number;
};
 
export class ResearchAgent extends Agent<Env, ResearchState> {
  // تعمل مرة واحدة عندما لم تُضبط الحالة من قبل
  initialState: ResearchState = {
    notes: [],
    questionsAsked: 0
  };
 
  // دالة RPC يمكن للعملاء والخادم استدعاؤها
  addNote(text: string) {
    const note: Note = {
      id: crypto.randomUUID(),
      text,
      createdAt: Date.now()
    };
    // setState تحفظ وتبث إلى العملاء المتصلين
    this.setState({
      ...this.state,
      notes: [...this.state.notes, note]
    });
    return note;
  }
 
  deleteNote(id: string) {
    this.setState({
      ...this.state,
      notes: this.state.notes.filter((n) => n.id !== id)
    });
  }
}

لاحظ أمرين. أولًا، initialState تعمل مرة واحدة فقط لكل نسخة — بعد ذلك تكون this.state آخر ما حفظته. ثانيًا، تقوم setState() بمهمتين معًا: تحفظ الحالة الجديدة في التخزين وتدفعها إلى كل عميل متصل في الوقت الفعلي. لن تكتب أي كود اشتراك إطلاقًا.

الخطوة 4: ضبط Wrangler

تحتاج Cloudflare إلى معرفة أن ResearchAgent هو Durable Object. افتح wrangler.jsonc وأضف الروابط، وهجرة، ورابط Workers AI الذي سنستخدمه لاحقًا.

{
  "name": "research-agent",
  "main": "src/index.ts",
  "compatibility_date": "2026-06-01",
  "compatibility_flags": ["nodejs_compat"],
  "durable_objects": {
    "bindings": [
      { "name": "ResearchAgent", "class_name": "ResearchAgent" }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["ResearchAgent"]
    }
  ],
  "ai": {
    "binding": "AI"
  }
}

كتلة migrations هي ما ينساه المبتدئون. يجب إدراج كل صنف وكيل تحت new_sqlite_classes، وإلا ستصطدم بخطأ No such Durable Object class عند التشغيل. ويمنح رابط ai الوكيل وصولًا إلى نماذج Workers AI.

الخطوة 5: إضافة نقطة دخول الـ Worker

يحتاج الـ Worker إلى تصدير افتراضي مع معالج fetch. تفحص مساعدة routeAgentRequest الرابط الوارد وتوجّهه إلى نسخة الوكيل الصحيحة تلقائيًا. أضف هذا إلى أسفل src/index.ts:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // توجّه ‎/agents/:agent/:name‎ إلى النسخة المطابقة
    const response = await routeAgentRequest(request, env);
    return response ?? new Response("Not found", { status: 404 });
  }
} satisfies ExportedHandler<Env>;

بهذا، تُرسَل الطلبات إلى /agents/research-agent/user-42 نحو نسخة ResearchAgent المسماة user-42. التسمية تلقائية: يصبح الصنف ResearchAgent هو المسار research-agent بصيغة kebab-case.

شغّل خادم التطوير للتأكد من أن كل شيء موصول:

npx wrangler dev

ينبغي أن ترى خادمًا محليًا يبدأ دون أخطاء هجرة. اتركه يعمل.

الخطوة 6: تخزين بيانات منظّمة عبر SQLite

تُعدّ setState مثالية لحالة عمل الوكيل، لكن للتاريخ المُلحَق فقط — كل سؤال طُرح — تريد جدولًا حقيقيًا. لكل نسخة وكيل قاعدة بيانات SQLite خاصة بها مكشوفة عبر القالب الموسوم this.sql. أضف خطّاف دورة حياة ودالة تسجيل إلى الوكيل:

export class ResearchAgent extends Agent<Env, ResearchState> {
  initialState: ResearchState = { notes: [], questionsAsked: 0 };
 
  // onStart تعمل عند إقلاع النسخة أو استيقاظها من السبات
  async onStart() {
    this.sql`
      CREATE TABLE IF NOT EXISTS history (
        id TEXT PRIMARY KEY,
        question TEXT NOT NULL,
        answer TEXT NOT NULL,
        created_at INTEGER NOT NULL
      )
    `;
  }
 
  private logInteraction(question: string, answer: string) {
    this.sql`
      INSERT INTO history (id, question, answer, created_at)
      VALUES (
        ${crypto.randomUUID()},
        ${question},
        ${answer},
        ${Date.now()}
      )
    `;
  }
 
  getHistory() {
    return this.sql`
      SELECT * FROM history
      ORDER BY created_at DESC
      LIMIT 50
    `;
  }
}

يُدرج قالب this.sql القيم بأمان كمعاملات مربوطة، فلا خطر من حقن SQL. ولأن الجدول يعيش داخل Durable Object، تكون الاستعلامات محلية وسريعة — دون قفزة شبكية إلى قاعدة بيانات بعيدة.

نصيحة: استخدم setState للحالة التفاعلية الصغيرة التي تعرضها الواجهة مباشرة، واستخدم this.sql لمجموعات البيانات الأكبر أو المُلحَقة فقط التي تستعلم عنها عند الطلب. المزج بين الاثنين هو النمط المعتاد.

الخطوة 7: استدعاء نموذج ذكاء اصطناعي داخل الوكيل

اجعل الوكيل الآن يقوم بالبحث فعليًا. أضف دالة ask تستدعي نموذج Workers AI، وتحدّث العدّاد في الحالة، وتسجّل التبادل في SQLite.

async ask(question: string): Promise<string> {
  // استدعاء نموذج Workers AI عبر رابط env
  const result = await this.env.AI.run("@cf/meta/llama-3.3-70b-instruct", {
    messages: [
      {
        role: "system",
        content: "You are a concise research assistant. Answer in 3 sentences."
      },
      { role: "user", content: question }
    ]
  });
 
  const answer = result.response ?? "No answer generated.";
 
  // تحديث الحالة التفاعلية — يرى العملاء العدّاد الجديد فورًا
  this.setState({
    ...this.state,
    questionsAsked: this.state.questionsAsked + 1
  });
 
  // تخزين التبادل الكامل في جدول تاريخ الوكيل
  this.logInteraction(question, answer);
 
  return answer;
}

ولأن هذا يعمل داخل الوكيل، يتشارك استدعاء النموذج وتحديث الحالة والكتابة في SQLite نفس النسخة المتسقة. وإن فضّلت OpenAI أو Anthropic أو مزوّدًا آخر، فبدّل استدعاء this.env.AI.run بحزمة ذلك المزوّد — وتبقى بقية الدالة كما هي.

الخطوة 8: بناء لوحة تحكم React حية

هنا يؤتي النموذج ثماره. يفتح الخطّاف useAgent من agents/react اتصال WebSocket بنسخة وكيل مسمّاة ويُبقي state متزامنة تلقائيًا. وعندما يستدعي الوكيل setState، يُعاد رسم مكوّنك — دون استطلاع ودون جلب يدوي.

أنشئ مكوّن React (يمكن أن يعيش في تطبيق واجهة منفصل أو في أصل ثابت لـ Workers):

import { useAgent } from "agents/react";
import { useState } from "react";
 
type Note = { id: string; text: string; createdAt: number };
type ResearchState = { notes: Note[]; questionsAsked: number };
 
export function ResearchDashboard({ userId }: { userId: string }) {
  const [draft, setDraft] = useState("");
  const [question, setQuestion] = useState("");
  const [answer, setAnswer] = useState("");
 
  // الاتصال بنسخة الوكيل الخاصة بهذا المستخدم
  const agent = useAgent<ResearchState>({
    agent: "research-agent",
    name: userId
  });
 
  async function handleAsk() {
    // استدعاء دالة RPC للوكيل عبر الاتصال المفتوح
    const reply = await agent.stub.ask(question);
    setAnswer(reply);
    setQuestion("");
  }
 
  return (
    <div>
      <h2>Questions asked: {agent.state?.questionsAsked ?? 0}</h2>
 
      <input
        value={draft}
        onChange={(e) => setDraft(e.target.value)}
        placeholder="Save a note"
      />
      <button onClick={() => { agent.stub.addNote(draft); setDraft(""); }}>
        Add note
      </button>
 
      <ul>
        {(agent.state?.notes ?? []).map((n) => (
          <li key={n.id}>
            {n.text}
            <button onClick={() => agent.stub.deleteNote(n.id)}>x</button>
          </li>
        ))}
      </ul>
 
      <input
        value={question}
        onChange={(e) => setQuestion(e.target.value)}
        placeholder="Ask a research question"
      />
      <button onClick={handleAsk}>Ask</button>
      {answer && <p>{answer}</p>}
    </div>
  );
}

سيرى عميلان متصلان بنفس userId ملاحظات أحدهما الآخر تظهر فورًا، لأن setState تبثّ إلى كل اتصال. والكائن agent.stub وكيل مكتوب الأنواع: استدعاء agent.stub.ask(...) يشغّل الدالة ask على الخادم ويُعيد نتيجتها.

الخطوة 9: جدولة المهام المتكررة

ميزة بارزة: يمكن للوكلاء جدولة عملهم المستقبلي بأنفسهم. لا حاجة إلى خدمة cron خارجية — يعيش المجدول داخل النسخة. استخدم this.schedule(when, methodName, payload). يقبل الوسيط when تأخيرًا بالثواني، أو كائن Date، أو تعبير cron.

async onStart() {
  this.sql`CREATE TABLE IF NOT EXISTS history (
    id TEXT PRIMARY KEY, question TEXT, answer TEXT, created_at INTEGER
  )`;
 
  // تشغيل ملخص كل يوم في الساعة 08:00 (يُجدوَل مرة واحدة لكل نسخة)
  await this.schedule("0 8 * * *", "dailyDigest", {});
}
 
async dailyDigest() {
  const rows = this.getHistory();
  const count = this.state.questionsAsked;
  console.log(`Daily digest: ${count} questions, ${rows.length} logged.`);
  // هنا يمكنك إرسال ملخص بالبريد، أو دفع إشعار،
  // أو استدعاء نموذج الذكاء الاصطناعي لتلخيص أبحاث اليوم.
}

عند حلول الوقت المجدول، توقظ Cloudflare النسخة الساكنة وتستدعي dailyDigest — حتى لو لم يكن أي مستخدم متصلًا. هذا يجعل العمل في الخلفية والتذكيرات والمتابعات أمرًا بسيطًا. يمكنك أيضًا جدولة مهام لمرة واحدة عبر await this.schedule(30, "methodName", payload) لتعمل بعد 30 ثانية.

الخطوة 10: تشغيل الوكلاء من الخادم عبر RPC

أحيانًا تحتاج إلى التفاعل مع وكيل من مسار API عادي — خطّاف ويب، أو Worker مجدول، أو خدمة أخرى. استخدم getAgentByName للحصول على وكيل مكتوب الأنواع لأي نسخة واستدعاء دوالها مباشرة.

import { getAgentByName } from "agents";
 
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
 
    // مسار API عادي يتحدث إلى وكيل من جهة الخادم
    if (url.pathname === "/api/ask") {
      const { userId, question } = await request.json<{
        userId: string;
        question: string;
      }>();
 
      const agent = await getAgentByName(env.ResearchAgent, userId);
      const answer = await agent.ask(question);
      return Response.json({ answer });
    }
 
    const response = await routeAgentRequest(request, env);
    return response ?? new Response("Not found", { status: 404 });
  }
} satisfies ExportedHandler<Env>;

تُعيد getAgentByName(env.ResearchAgent, userId) النسخة نفسها التي تتصل بها لوحة React. استدعِ ask من الخادم وتنعكس النتيجة — عدّاد الأسئلة الجديد — فورًا في واجهة المستخدم المفتوحة، لأن الحالة مشتركة عبر كل نقطة دخول.

اختبار تنفيذك

شغّل خادم التطوير ومارِس على الوكيل:

npx wrangler dev

ثم أرسل طلبًا إلى مسار الخادم:

curl -X POST http://localhost:8787/api/ask \
  -H "Content-Type: application/json" \
  -d '{"userId":"user-42","question":"What is edge computing?"}'

ينبغي أن تستعيد إجابة بصيغة JSON. شغّلها مجددًا وسيزداد عدّاد questionsAsked للوكيل — دليل على أن الحالة استمرت بين طلبين مستقلين لنفس النسخة.

النشر على Cloudflare

النشر أمر واحد. يحزم Wrangler الـ Worker الخاص بك، ويُجهّز Durable Objects، ويدفع كل شيء إلى شبكة Cloudflare العالمية:

npx wrangler deploy

يعمل وكيلك الآن في مئات المواقع حول العالم، إذ تتجسّد كل نسخة قرب المستخدم الذي يخاطبها. لا خوادم لإدارتها ولا قاعدة بيانات لتجهيزها — يسافر تخزين SQLite مع كل Durable Object.

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

خطأ No such Durable Object class. صنف وكيلك مفقود من قائمة new_sqlite_classes في wrangler.jsonc. أضفه تحت مدخلة migrations وأعد التشغيل.

الحالة undefined عند أول رسم. تكون agent.state بقيمة undefined حتى يتصل WebSocket وتصل أول مزامنة. اقرأها دائمًا بحذر، مثل agent.state?.notes ?? [].

المهمة المجدولة لا تعمل أبدًا. تعمل تعابير cron بتوقيت UTC. تحقق من التعبير وتأكد أن نسخة الوكيل أُنشئت مرة واحدة على الأقل — تحدث الجدولة داخل onStart، التي تعمل فقط عند إقلاع النسخة.

رابط AI غير معرّف. تأكد من وجود كتلة رابط ai في wrangler.jsonc وأنك أعدت النشر بعد إضافتها.

الخطوات التالية

  • أضِف مراجعة بشرية ضمن الحلقة عبر التوقّف داخل استدعاء أداة والاستئناف عند تأكيد المستخدم.
  • ابثّ ردود الذكاء الاصطناعي رمزًا برمز إلى الواجهة بدلًا من إعادة سلسلة واحدة.
  • استكشف طبقة وكيل المحادثة في الحزمة لإدارة محادثة كاملة مع تاريخ الرسائل.
  • ادمج الوكلاء مع Cloudflare Workflows لخطوط معالجة دائمة متعددة الخطوات.
  • اقرأ دليلنا المرافق حول التنفيذ الآمن للأكواد لوكلاء الذكاء الاصطناعي عندما يحتاج وكيلك إلى تشغيل كود غير موثوق.

الخاتمة

تطوي حزمة Cloudflare Agents SDK المكدّس المعتاد — قاعدة بيانات، طابور، خادم WebSocket، مجدول — في بدائية واحدة: كائن ذو حالة، دائم، مقيم على الحافة. كتبت صنف Agent واحدًا فحصلت على حالة دائمة، ومزامنة React حية، وتسجيل SQLite، واستدعاءات نماذج ذكاء اصطناعي، وعمل مجدول، واستدعاء من الخادم، ثم نشرته بأمر واحد.

التحوّل الذهني هو معاملة كل وكيل ككائن طويل العمر يُخاطَب بالاسم بدلًا من دالة عديمة الحالة. وحين تستوعب ذلك، يصبح بناء تجارب دائمة، تعدّدية المستخدمين، مدعومة بالذكاء الاصطناعي أبسط بكثير — وكل ذلك يعمل على الحافة، على بُعد أجزاء من الألف من الثانية من مستخدميك.