DEV Community

Cover image for وكلاء الذكاء الاصطناعي: استمرارية النتائج لمنع الفوترة المزدوجة عند إعادة المحاولة
Yusuf Khalidd
Yusuf Khalidd

Posted on Originally published at apidog.com

وكلاء الذكاء الاصطناعي: استمرارية النتائج لمنع الفوترة المزدوجة عند إعادة المحاولة

مبدأ الثبات (Idempotency) في أدوات وكلاء الذكاء الاصطناعي

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

جرّب Apidog اليوم

هذه إحدى طرق الفشل التي تميّز الوكلاء عن عملاء API التقليديين. يرى الإنسان مؤشر تحميل وينتظر، بينما يرى الوكيل صمتًا في حلقة إعادة المحاولة، فيرسل الطلب مرة أخرى—وأحيانًا ثلاث أو أربع مرات بسرعة تفوق أي مستخدم بشري.

كل سياسة إعادة محاولة تضيفها لتحسين موثوقية الوكيل قد تزيد أيضًا احتمال تكرار عمليات الكتابة. الحل هو مبدأ الثبات (idempotency): يجب أن ينتج الطلب المتكرر النتيجة نفسها التي ينتجها الطلب المنفرد.

يغطي هذا الدليل:

  • دلالة مبدأ الثبات على مستوى HTTP.
  • إنشاء مفاتيح يستطيع الوكيل إعادة استخدامها.
  • ما يجب على الخادم تخزينه ومعالجته.
  • اختبار التكرار قبل أن يُحاسب عميل حقيقي مرتين.

إذا لم تقرأ مقال لماذا تتعطل وكلاء الذكاء الاصطناعي في بيئة الإنتاج، فعمليات الكتابة المكررة هي أحد أنماط الفشل الأساسية التي يشرحها.

يظهر Apidog في جزء الاختبار. أما مبدأ الثبات نفسه فيجب بناؤه داخل API وطبقة أدوات الوكيل، ثم اختبار إرسال الطلب نفسه مرتين وإثبات أن الاستدعاء الثاني لم يغيّر شيئًا.

لماذا يكسر الوكلاء مبدأ الثبات أكثر من البشر؟

هناك ثلاثة أسباب رئيسية:

1. كثرة عمليات إعادة المحاولة

تعيد أطر عمل الوكلاء المحاولة بقوة افتراضيًا، لأن أعطال الشبكة العابرة شائعة. يستعرض دليل استعادة الأخطاء لوكلاء الذكاء الاصطناعي استراتيجيات التراجع وقواطع الدائرة، لكن كل إعادة محاولة تزيد عدد مرات وصول الطلب إلى خادمك.

2. غموض انتهاء المهلة

عند انتهاء مهلة الطلب، لا يعرف العميل ما إذا كان الخادم قد نفّذ العملية. قد يعني رمز 504 أن عملية الكتابة لم تحدث، أو أنها حدثت لكن الاستجابة فُقدت.

يتحقق البشر عادةً قبل إعادة المحاولة. أما الوكيل فقد لا يفعل ذلك، لأن التحقق أولًا يتطلب استدعاء أداة إضافية وقرارًا من النموذج.

3. إعادة تشغيل المهمة كاملة

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

إذًا، المشكلة ليست أن الوكلاء يرسلون طلبات خاطئة؛ بل أنهم قد يرسلون الطلب الصحيح أكثر من مرة.

ماذا يضمن مبدأ الثبات فعليًا؟

تكون العملية ثابتة عندما يكون لتنفيذها عدة مرات التأثير نفسه لتنفيذها مرة واحدة.

تُعرَّف GET وPUT وDELETE بأنها ثابتة في RFC 9110، بينما POST ليست ثابتة. لهذا تُستخدم POST كثيرًا في العمليات الحساسة مثل إنشاء طلب، إرسال رسالة، أو بدء تحويل.

الثبات ليس الأمان

الطريقة الآمنة لا تغيّر حالة الخادم. أما الطريقة الثابتة فقد تكون مدمّرة:

  • DELETE ثابتة، لكنها تحذف المورد.
  • استدعاؤها خمس مرات يترك المورد محذوفًا، كما لو استُدعيت مرة واحدة.

لذلك يجب تقييم خاصيتي الأمان والثبات بشكل منفصل. راجع أيضًا مقال مفاتيح API للوكلاء بأقل الامتيازات.

الثبات لا يعني تطابق الاستجابة

قد يعيد الاستدعاء الثاني الاستجابة المخزنة نفسها، أو يعيد رمز حالة مختلفًا. المهم هو ألا تتغير حالة الخادم:

  • خصم واحد.
  • طلب واحد.
  • رسالة بريد إلكتروني واحدة.

مفاتيح الثبات: جعل طلبات POST آمنة للتكرار

النمط القياسي هو أن ينشئ العميل مفتاحًا ويرسله مع الطلب. يخزن الخادم المفتاح والنتيجة، ثم يعيد النتيجة المخزنة لأي طلب لاحق يحمل المفتاح نفسه بدلًا من تنفيذ العملية مرة أخرى.

استخدمت Stripe رأس Idempotency-Key على نطاق واسع، وتقدم وثائق Stripe حول الطلبات الثابتة شرحًا واضحًا للدلالة. كما توجد مسودة IETF لتوحيد حقل رأس Idempotency-Key.

مثال:

POST /v1/payments HTTP/1.1
Host: api.yourservice.com
[REDACTED CREDENTIAL] [REDACTED]...
Idempotency-Key: 9f2b7c14-6d3a-4b18-9d55-1e2a7c0b4f31
Content-Type: application/json

{
  "amount": 4900,
  "currency": "usd",
  "customer_id": "cus_8812",
  "description": "Pro plan, August"
}
Enter fullscreen mode Exit fullscreen mode

المفتاح هنا UUID. لا يحمل معنى خاصًا للخادم سوى: «هذه العملية المنطقية نفسها». يخزن الخادم المفتاح مع بصمة لجسم الطلب والاستجابة الناتجة.

إنشاء مفتاح يعيد الوكيل استخدامه

الخطأ الأكثر شيوعًا هو إنشاء UUID جديد داخل غلاف الأداة عند كل استدعاء. عندها يتغير المفتاح في كل إعادة محاولة، ولا يحقق مبدأ الثبات أي فائدة.

يجب ربط المفتاح بالعملية المنطقية، لا بمحاولة HTTP:

import uuid

class PaymentTool:
    def __init__(self, client):
        self.client = client
        self._keys = {}

    def charge(self, task_id, step_id, amount, customer_id):
        # One key per (task, step). Retries of the same step reuse it.
        op = f"{task_id}:{step_id}"
        if op not in self._keys:
            self._keys[op] = str(uuid.uuid4())

        return self.client.post(
            "/v1/payments",
            headers={"Idempotency-Key": self._keys[op]},
            json={"amount": amount, "customer_id": customer_id},
        )
Enter fullscreen mode Exit fullscreen mode

ينشئ هذا المثال مفتاحًا واحدًا لكل زوج من task_id وstep_id، ثم يعيد استخدامه في كل إعادة محاولة للخطوة نفسها.

يمكنك أيضًا استخدام مفتاح حتمي (deterministic)، وهو أفضل عند إعادة تشغيل العمليات لأنه لا يعتمد على قاموس داخل الذاكرة:

import hashlib

def idempotency_key(task_id: str, step_id: str, payload: dict) -> str:
    raw = f"{task_id}|{step_id}|{sorted(payload.items())}"
    return hashlib.sha256(raw.encode()).hexdigest()[:32]
Enter fullscreen mode Exit fullscreen mode

اشتق المفتاح من تشغيل المهمة والخطوة، وليس من طابع زمني أو قيمة عشوائية تُنشأ في كل محاولة.

إذا أعاد الوكيل تشغيل المهمة كاملة وكان يقصد فعلًا إنشاء خصم جديد، فيجب أن يتغير معرف المهمة، وبالتالي يتغير المفتاح. هذا هو السلوك المطلوب.

ما يجب على الخادم فعله

لا يكفي البحث عن الرأس فقط. يجب أن ينفذ الخادم الخطوات التالية:

  1. المطالبة بالمفتاح قبل تنفيذ العمل: أدخل المفتاح في جدول ذي قيد فريد. إذا فشل الإدخال، فهناك محاولة أخرى تملكه.
  2. رفض اختلاف الحمولة: إذا كان المفتاح موجودًا لكن بصمة الطلب مختلفة، أعد 422. استخدام المفتاح نفسه مع جسم مختلف خطأ من العميل.
  3. منع التنافس: إذا كانت المحاولة الأولى ما تزال قيد التنفيذ، أعد 409 حتى يتراجع العميل بدلًا من تنفيذ العملية بالتوازي.
  4. تخزين النتيجة: بعد انتهاء العمل، خزّن رمز الحالة وجسم الاستجابة مقابل المفتاح، ثم أعدهما لكل استدعاء لاحق.

مثال لجدول PostgreSQL:

CREATE TABLE idempotency_records (
  key             TEXT PRIMARY KEY,
  request_hash    TEXT NOT NULL,
  state           TEXT NOT NULL,      -- in_progress | completed
  response_status INT,
  response_body   JSONB,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  expires_at      TIMESTAMPTZ NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

حدد مدة انتهاء صلاحية للمفاتيح. تغطي مدة 24 ساعة معظم نوافذ إعادة المحاولة الواقعية، بينما يؤدي الاحتفاظ بالمفاتيح إلى الأبد إلى تضخم الجدول. تنتهي صلاحية مفاتيح Stripe بعد 24 ساعة، وهي قيمة افتراضية معقولة.

اختبار أن الاستدعاء الثاني لا يغيّر شيئًا

بناء مبدأ الثبات نصف العمل؛ وإثبات فعاليته هو النصف الآخر. قد تبدو الاستجابة سليمة حتى عند تنفيذ العملية مرتين، إذ يمكن لعمليتي خصم ناجحتين أن تعيدا 200.

الاختبار الأساسي:

  1. أرسل الطلب بمفتاح ثابت.
  2. خزّن الاستجابة الأولى.
  3. أرسل الطلب نفسه مرة أخرى بالمفتاح نفسه.
  4. تحقق من حالة الخادم، لا من الاستجابة فقط.

تحقق من الآتي:

  • يتطابق جسم الاستجابة الثاني مع الأول، بما في ذلك resource ID.
  • يعيد طلب GET لاحق سجلًا واحدًا لا سجلين.
  • تحرك الرصيد أو العداد مرة واحدة فقط.

اختبار مبدأ الثبات في API

في Apidog، يمكنك حفظ ذلك كسيناريو اختبار:

  1. أرسل POST بمفتاح Idempotency-Key ثابت.
  2. كرر الطلب بالمفتاح نفسه.
  3. استعلم عن المورد وتأكد من عدد السجلات.
  4. خزّن معرف الاستجابة الأولى في متغير، وتأكد من أن الثانية تعيد القيمة نفسها.

بما أن السيناريو محفوظ، يمكن تشغيله في CI عند كل تغيير في مسار الدفع. لمزيد من الأنماط، راجع دليل اختبار عقود API.

غطِّ أيضًا حالتين تكشفان أخطاء حقيقية:

  • المفتاح نفسه مع جسم طلب مختلف: يجب أن تكون النتيجة 422، لا نجاحًا صامتًا.
  • التكرارات المتزامنة: أرسل الطلبين في الوقت نفسه، وتأكد من تنفيذ العملية مرة واحدة فقط. يكشف ذلك غياب القيد الفريد الذي قد لا يظهر في اختبار تسلسلي.

إذا كانت واجهة الدفع غير جاهزة، استخدم محاكاة (mock) مدركة لمبدأ الثبات، بما في ذلك إرجاع 422 عند اختلاف الحمولة. يشرح مقال لماذا يجب على الوكلاء استخدام المحاكاة بدلًا من بيئة الإنتاج هذا النهج بمزيد من التفصيل.

عندما لا يمكنك إضافة مفتاح

إذا كانت API خارج سيطرتك ولا تدعم مفاتيح الثبات، استخدم الخيارات التالية حسب الأولوية:

1. اجعل العملية ثابتة بطبيعتها

استخدم PUT إلى مسار يختاره العميل:

PUT /orders/{client_order_id}
Enter fullscreen mode Exit fullscreen mode

إذا كنت تتحكم في تصميم API، فغالبًا يكون هذا أفضل من إضافة رأس إلى POST، ولا يحتاج إلى جدول إضافي.

2. تحقق قبل الكتابة

استعلم عن سجل موجود يحمل المفتاح الطبيعي قبل إنشاء سجل جديد. هذا أضعف، لأن السباق بين التحقق والكتابة قد ينتج سجلين، لكنه يزيل بعض حالات انتهاء المهلة الشائعة.

3. أزل التكرار في الطرف المستقبِل

بالنسبة للرسائل والأحداث، أرفق معرفًا ثابتًا واجعل المستهلك يتجاهل التكرارات. هذا نمط شائع في الأنظمة المعتمدة على الأحداث، ويتوافق مع إرشادات تصميم webhooks موثوقة.

4. أضف بوابة موافقة

للعمليات غير القابلة للعكس التي لا يمكن جعلها ثابتة، ضع مراجعة بشرية قبل التنفيذ. هذا هو نمط بوابة الموافقة الذي يشرحه مقال حواجز حماية وكلاء الذكاء الاصطناعي.

اربط كل عملية تشغيل بما نفذته

يمنع مبدأ الثبات التكرار، لكنه لا يجيب عن سؤال: أي محاولة أنشأت السجل؟

احتفظ بهوية التشغيل مع كل عملية. عندما يكون الوكيل خدمة تملكها، سجّل:

  • task_id
  • step_id
  • مفتاح الثبات
  • نتيجة كل محاولة

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

قائمة مراجعة قبل النشر

  • كل أداة غير ثابتة يمكن للوكيل استدعاؤها تتطلب idempotency key.
  • يرفض غلاف الأداة الإرسال عند غياب المفتاح.
  • تُشتق المفاتيح من المهمة والخطوة، لا من المحاولة.
  • يطالب الخادم بالمفتاح قبل تنفيذ العمل.
  • يعيد المفتاح نفسه مع حمولة مختلفة خطأً، لا استجابة مخزنة.
  • تُعالج التكرارات المتزامنة بقيد قاعدة بيانات، لا بتوقيت التطبيق.
  • يثبت اختبار محفوظ أن الاستدعاء الثاني لا يغير شيئًا.
  • يعمل الاختبار في CI.
  • تنتهي صلاحية المفاتيح ويُنظَّف الجدول دوريًا.

عند تطبيق هذه القائمة، تصبح سياسة إعادة المحاولة أكثر قوة من دون أن تجعل الوكيل خطيرًا. هذه هي الفائدة الأساسية لمبدأ الثبات: مرونة أعلى، وتأثيرات مكررة أقل.

أسئلة شائعة

هل أحتاج إلى مفاتيح ثبات لأدوات القراءة فقط؟

لا. طلبات GET ثابتة وآمنة، لذا فإن إعادة محاولتها تستهلك بعض زمن الاستجابة فقط. استخدم المفاتيح مع العمليات التي تنشئ أو تفرض رسومًا أو ترسل أو تغيّر الحالة.

أين يجب إنشاء المفتاح: في الوكيل أم في غلاف الأداة؟

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

ما رمز الحالة الذي يجب أن يعيده الطلب المتكرر؟

أعد الحالة المخزنة من الاستدعاء الأصلي. إذا أعاد طلب POST الأول 201، فيجب أن يعيد الطلب الثاني 201 مع جسم الاستجابة نفسه.

قد تضيف بعض APIs رأسًا مثل Idempotent-Replay: true لتمييز الاستجابة المعادة، وهو مفيد للتصحيح ولا يضر العملاء الذين يتجاهلونه.

كم من الوقت يجب الاحتفاظ بالمفاتيح؟

تغطي 24 ساعة معظم نوافذ إعادة المحاولة. الاحتفاظ بها فترة أطول يزيد حجم الجدول غالبًا من دون فائدة كبيرة. بعد انتهاء المدة، تعامل مع الطلب كعملية جديدة.

هل تحل مفاتيح الثبات محل المعاملات؟

لا. تمنع مفاتيح الثبات الطلبات المكررة من إنتاج تأثيرات مكررة، بينما تجعل المعاملات الطلب الواحد ذريًا. تحتاج إلى الاثنين، ويُفضّل كتابة المطالبة بالمفتاح في المعاملة نفسها التي تنفذ العمل، متى سمحت قاعدة البيانات بذلك.

كيف أختبر ذلك دون مزود دفع حقيقي؟

وجّه الوكيل إلى mock يطبق دلالات المفتاح، بما في ذلك 422 عند اختلاف الحمولة. وإذا أردت تنفيذ المحاكاة واختبار إعادة المحاولة في المشروع نفسه، نزّل Apidog.

إعادة محاولة طلب ثابت

Top comments (0)