۳ بازنویسی LangGraph: آنچه چک‌پوینتینگ در محیط عملیاتی به من آموخت

به مدت سه هفته، عامل (agent) فرآیند ورود (onboarding) ما تمام وظایفی را که قرار بود ذخیره کند، از دست می‌داد. لاگ‌ها چیزی نشان نمی‌دادند. نه خطایی، نه هشداری. چک‌پوینتر (checkpointer) گزارش موفقیت می‌داد، اما با از سرگیری (resume)، وضعیت (state) ناپدید می‌شد.

قبل از اینکه حقیقت را بفهمم، همان خط لوله (pipeline) LangGraph را سه بار بازنویسی کردم. فریم‌ورک دقیقاً همان کاری را انجام می‌داد که من به آن گفته بودم. من فقط دستورات اشتباهی به آن می‌دادم.

اگر در حال ساخت عامل‌های دارای وضعیت (stateful agents) برای محیط عملیاتی هستید، تصور نکنید تنظیمات پیش‌فرض شما را نجات خواهند داد. در اینجا آنچه خراب شد و نحوه اصلاح آن را آورده‌ام.

اولین خرابی: از دست رفتن بی‌صدای وضعیت (Silent State Loss)

عامل من یک جریان ورود پنج مرحله‌ای را مدیریت می‌کرد. من از Postgres برای ذخیره پیشرفت استفاده کردم تا کاربران بتوانند بعداً کار را از ادامه شروع کنند. اما هر بار که فرآیند از سر گرفته می‌شد، از مرحله اول شروع می‌شد.

علت، طرح‌واره (schema) وضعیت من بود. در LangGraph، هر گره (node) یک به‌روزرسانی را برمی‌گرداند که در وضعیت ادغام (merge) می‌شود. اگر مشخص نکنید که چگونه ادغام انجام شود، حالت پیش‌فرض، بازنویسی (overwrite) است.

فکر می‌کردم لیست پیام‌های من به انتهای لیست اضافه (append) می‌شود. اما در عوض، هر گره جدید تمام تاریخچه را فقط با یک پیام جایگزین می‌کرد. چک‌پوینت، داده‌های اشتباه را به شکلی بی‌نقص ذخیره می‌کرد.

راه حل: استفاده از فیلدهای Annotated با reducerهای صریح.

• استفاده از عملگر add برای لیست پیام‌ها. • استفاده از یک تابع merge سفارشی برای دیکشنری‌ها. • استفاده از overwrite پیش‌فرض فقط برای مقادیر تک‌گانه مانند "step".

دومین خرابی: سریال‌سازی مجدد (Deserialization) و همزمانی (Concurrency)

بازنویسی دوم با دو مشکل جدید روبرو شد:

۱. ردیف‌های فاسد: من اشیاء سفارشی (custom objects) را در وضعیت ذخیره می‌کردم. سریالایزر نمی‌توانست آن‌ها را مدیریت کند. این کار باعث ایجاد ردیف‌هایی می‌شد که وجود داشتند اما غیرقابل استفاده بودند. ۲. کلیدهای تکراری: اگر سریع پاسخ ندهید، WhatsApp در فراخوانی webhookها تلاش مجدد می‌کند. اگر دو پیام همزمان برسند، دو اجرای گراف سعی می‌کنند در یک thread بنویسند. این امر باعث برخورد (collision) در پایگاه داده می‌شد.

راه حل‌ها: • حذف اشیاء سفارشی. فقط از dictهای ساده و انواع استاندارد LangChain استفاده کنید. • مدیریت webhookها خارج از گراف. از یک صف (queue) و یک کلید یکسان‌ساز (idempotency key) برای حذف موارد تکراری استفاده کنید. • افزودن قفل پایگاه داده (database lock). اطمینان حاصل کنید که در هر لحظه فقط یک اجرا برای هر thread انجام می‌شود.

سومین بازنویسی: الگوی پایدار

نسخه نهایی بر سه اصل تمرکز داشت:

• گراف‌های کوچک: من یک گراف بسیار بزرگ را به سه زیرگراف کوچک‌تر تقسیم کردم. این کار دامنه اثر باگ‌ها را کاهش داد. • چک‌پوینت‌های صریح: من چک‌پوینت‌گیری بعد از هر گره را متوقف کردم. فقط در نقاط بازگشت معنادار چک‌پوینت می‌گیرم. این کار حجم نوشتن در پایگاه داده را ۶۰٪ کاهش داد. • گره‌های Idempotent: این موضوع حیاتی است. هر گره باید در صورت اجرای دو‌باره، نتیجه یکسانی تولید کند. اگر یک گره تشخیص دهد که وظیفه‌ای در وضعیت (state) قبلاً انجام شده است، باید بلافاصله بازگردد. این کار از هزینه‌ی مضاعف برای فراخوانی‌های گران‌قیمت مدل جلوگیری می‌کند.

𝗟𝗲𝘀𝘀𝗼𝗻𝘀 𝗳𝗼𝗿 𝘆𝗼𝘂:

  • پیش از نوشتن کد، معناشناسی (semantics) reducer را مطالعه کنید.
  • اشیاء سفارشی (custom objects) را در state ذخیره نکنید.
  • کنترل همزمانی (concurrency control) را به خارج از گراف منتقل کنید.
  • هر گره را Idempotent بسازید.

فریم‌ورک شکست نخورد؛ بلکه فرضیات من اشتباه بودند.

منبع: https://dev.to/elenarevicheva/three-langgraph-rewrites-what-production-checkpointing-actually-taught-me-ok9

انجمن یادگیری اختیاری: https://t.me/GyaanSetuAi