٣ عمليات إعادة كتابة لـ LangGraph: ما تعلمته من عملية الـ Checkpointing في بيئة الإنتاج
لمدة ثلاثة أسابيع، فقد وكيل التهيئة (onboarding agent) الخاص بنا كل مهمة كان من المفترض أن يحفظها. لم تظهر السجلات (logs) أي شيء. لا أخطاء، ولا تحذيرات. أبلغت أداة الـ checkpointer عن النجاح، ولكن الحالة (state) اختفت عند الاستئناف.
أعدت كتابة خط معالجة (pipeline) LangGraph نفسه ثلاث مرات قبل أن أفهم الحقيقة. لقد قام الإطار (framework) بما طلبته منه تماماً، لكنني كنت أعطيه التعليمات الخاطئة فقط.
إذا كنت تبني وكلاء يعتمدون على الحالة (stateful agents) لبيئة الإنتاج، فلا تفترض أن الإعدادات الافتراضية ستنقذك. إليك ما تعطل وكيف قمت بإصلاحه.
الخلل الأول: فقدان الحالة الصامت
كان الوكيل الخاص بي يتعامل مع تدفق تهيئة مكون من خمس خطوات. استخدمت Postgres لحفظ التقدم حتى يتمكن المستخدمون من الاستئناف لاحقاً. ولكن كل عملية استئناف كانت تبدأ من الخطوة الأولى.
كان السبب هو مخطط الحالة (state schema) الخاص بي. في LangGraph، تعيد كل عقدة (node) تحديثاً يتم دمجه في الحالة. إذا لم تحدد كيفية الدمج، فإن الإجراء الافتراضي هو الاستبدال (overwrite).
اعتقدت أن قائمة الرسائل الخاصة بي ستُضاف إليها الرسائل الجديدة (append). بدلاً من ذلك، كانت كل عقدة جديدة تستبدل السجل بالكامل برسالة واحدة فقط. لقد قامت أداة الـ checkpoint بحفظ البيانات الخاطئة بشكل مثالي.
الحل: استخدم حقول Annotated مع reducers صريحة.
• استخدم عامل add لقوائم الرسائل.
• استخدم دالة دمج (merge function) مخصصة للقواميس (dictionaries).
• استخدم الاستبدال الافتراضي فقط للقيم الفردية مثل "step".
الخلل الثاني: إلغاء التسلسل (Deserialization) والتزامن (Concurrency)
واجهت إعادة الكتابة الثانية مشكلتين جديدتين:
- صفوف تالفة: قمت بتخزين كائنات مخصصة (custom objects) في الحالة. لم يتمكن المُسلسل (serializer) من التعامل معها، مما أدى إلى إنشاء صفوف موجودة ولكنها غير قابلة للاستخدام.
- مفاتيح مكررة: يقوم WhatsApp بإعادة محاولة إرسال الـ webhooks إذا لم تستجب بسرعة. إذا وصلت رسالتان في وقت واحد، سيحاول تشغيلان للرسم البياني (graph runs) الكتابة في نفس الخيط (thread)، مما تسبب في تصادمات في قاعدة البيانات.
الحلول:
• تجريد الكائنات المخصصة. استخدم القواميس البسيطة (plain dicts) وأنواع LangChain القياسية فقط.
• التعامل مع الـ webhooks خارج الرسم البياني. استخدم طابوراً (queue) ومفتاح idempotency للتخلص من المكررات.
• إضافة قفل لقاعدة البيانات (database lock). تأكد من حدوث تشغيل واحد فقط لكل خيط (thread) في المرة الواحدة.
إعادة الكتابة الثالثة: النمط المستقر
ركز الإصدار النهائي على ثلاثة مبادئ:
• Small graphs: I broke one massive graph into three smaller subgraphs. This reduced the blast radius of bugs. • Explicit checkpoints: I stopped checkpointing after every single node. I only checkpoint at meaningful resume points. This cut database writes by 60%. • Idempotent nodes: This is vital. Every node must produce the same result if it runs twice. If a node sees that a task is already done in the state, it should return immediately. This prevents double-charging for expensive model calls.
𝗟𝗲𝘀𝘀𝗼𝗻𝘀 𝗳𝗼𝗿 𝘆𝗼𝘂:
- Read reducer semantics before you write code.
- Do not store custom objects in state.
- Move concurrency control outside the graph.
- Make every node idempotent.
The framework did not fail. My assumptions did.
Optional learning community: https://t.me/GyaanSetuAi