مهارت های قابل ارزیابی برای عامل های برنامه نویس در تجارت الکترونیک: مطالعه ی موردی وب هوک های تکراری و تحویل دوباره

13 آبان 1405 - خواندن 12 دقیقه - 37 بازدید

مقدمه

عامل های برنامه نویس (coding agents) امروز بخش زیادی از کد فروشگاه های اینترنتی را می نویسند یا تغییر می دهند: افزونه ی ووکامرس، اتصال درگاه پرداخت، پردازش وب هوک سفارش. این عامل ها دانش عمومی خوبی دارند، اما دانش «تجربی» یک مهندس ارشد تجارت الکترونیک را ندارند؛ چیزهایی مثل اینکه چرا نباید روی شناسه ی تلاش تحویل dedupe کرد یا چرا امضای وب هوک باید روی بایت های خام بررسی شود.

یکی از راه های انتقال این دانش، «مهارت» (skill) است: پوشه ای با یک فایل SKILL.md که نام، توضیح و دستورالعمل دارد و عامل وقتی کار با توضیح آن مطابقت داشت، آن را بارگذاری می کند. من مجموعه ای متن باز از این مهارت ها به نام commerce-agent-skills ساخته ام. در این یادداشت دو موضوع را توضیح می دهم: اینکه چطور می شود مهارت را مثل کد قابل آزمون کرد، و یک مطالعه ی موردی فنی درباره ی رایج ترین خطای پنهان در پردازش وب هوک، یعنی تحویل دوباره پس از timeout.

مهارت چیست و با «قاعده» چه فرقی دارد؟

این مجموعه از قالب Agent Skills پیروی می کند: یک پوشه، یک فایل SKILL.md با فیلدهای name و description، و در صورت نیاز فایل های مرجع و اسکریپت های کوچک. عامل ابتدا فقط نام و توضیح را می خواند و تصمیم می گیرد مهارت به کار مربوط است یا نه؛ بدنه ی مهارت و فایل های مرجع فقط وقتی لازم شد بارگذاری می شوند. این طراحی باعث می شود پنجره ی متنی عامل با دانش نامربوط پر نشود.

کنار هر مهارت یک «قاعده» (rule) هم هست: یک فایل Markdown کوتاه که می توان آن را همیشه در متن عامل نگه داشت. تفاوت را این طور خلاصه می کنم: مهارت برای یک کار است، قاعده برای همه ی کارهای یک حوزه.

نسخه ی 0.3.3 مجموعه ۲۹ مهارت دارد؛ از توسعه ی افزونه ی ووکامرس و امنیت افزونه ی وردپرس تا پرداخت Stripe، درگاه های ایرانی مانند زرین پال، موجودی چندانباره، مرجوعی و بازپرداخت، قیمت گذاری B2B، بومی سازی فارسی و راست به چپ، دسترس پذیری و کارایی ویترین، و طراحی ابزارهای MCP برای فروشگاه. (طراحی ابزارهای نوشتن امن در یک سرور MCP را در یادداشت «الگوی پیش نمایش، تایید و ثبت در یک سرور MCP برای ووکامرس» شرح داده ام.)

اصل محوری: با مهارت مثل کد رفتار کنید

متنی که قرار است رفتار یک عامل را شکل دهد، اگر آزمون نداشته باشد، به مرور خراب می شود: توضیحش با محتوایش ناهماهنگ می شود، اصطلاحی از قلم می افتد یا یک تغییر ظاهرا بی خطر نکته ای کلیدی را حذف می کند. برای جلوگیری از این، سه لایه ی کنترل در نظر گرفته ام.

۱. وظیفه های ارزیابی و نمونه ی حل شده

هر مهارت یک فایل evals/tasks.json دارد. هر وظیفه یک صورت مسئله و دو فهرست بررسی است:

  • must_include: رشته هایی که پاسخ باید داشته باشد، معمولا نام API یا فیلد.
  • must_not_include: رشته هایی که پاسخ نباید داشته باشد، معمولا نشانه ی یک اشتباه رایج.

برای هر وظیفه یک نمونه ی حل شده هم هست که همه ی بررسی ها را پاس می کند. در آخرین نسخه ی شاخه ی اصلی مخزن، مجموع وظیفه های ارزیابی ۶۲ مورد است.

۲. اعتبارسنج سخت گیر

اعتبارسنج نسخه ها، بخش های لازم، سرفصل های قاعده، پوشش دسته ها و سبک نگارش را بررسی می کند. مهم ترین بررسی این است: اگر رشته ای در must_include باشد که در خود مهارت، قاعده یا مرجع هایش آموزش داده نشده، اعتبارسنج خطا می دهد. یعنی نمی شود از عامل انتظار چیزی را داشت که مهارت به او یاد نداده است.

۳. اجراکننده ی ارزیابی

اسکریپت run-evals پاسخ هایی را که از یک عامل جمع کرده اید امتیاز می دهد. امتیاز هر وظیفه نسبت بررسی های پاس شده به کل بررسی هاست و وظیفه فقط وقتی پاس است که همه ی بررسی ها پاس شوند. فایل پاسخ گم شده صفر می گیرد.

node scripts/run-evals.mjs
node scripts/run-evals.mjs --answers ./answers --min-score 0.8

فرمان اول نمونه های حل شده را بررسی می کند. فرمان دوم پاسخ ها را از مسیر answers/<skill>/<task-id>.md می خواند و اگر امتیاز زیر آستانه باشد با کد خروج ۱ پایان می یابد، پس می توان آن را در CI گذاشت.

یک نکته ی صادقانه: این بررسی ها از نوع تطبیق زیررشته اند. پاس شدن آن ها درستی پاسخ را فراتر از رشته هایی که نام برده شده ثابت نمی کند. اجراکننده هم خودش وظیفه ها را به عامل زنده نمی فرستد؛ پاسخ هایی را که شما جمع کرده اید امتیاز می دهد.

مطالعه ی موردی: وب هوک تکراری پس از timeout

دو حالت هم زمان و شکست نیمه کاره که در ادامه آمده اند، از بازخورد خوانندگان به مجموعه اضافه شدند؛ این تجربه را در نوشته ای درباره ی بازخورد خوانندگان توضیح داده ام.

صورت مسئله

فرض کنید فروشگاه شما وب هوک «سفارش پرداخت شد» را دریافت می کند. گیرنده رویداد را ذخیره می کند، سفارش را به روز می کند، دستور ارسال (fulfillment) را می سازد و ایمیل تایید می فرستد. اما فرستنده پیش از رسیدن پاسخ 2xx دچار timeout می شود و همان رویداد را دوباره می فرستد؛ اغلب با یک شناسه ی تحویل جدید.

اگر گیرنده خودتوان (idempotent) نباشد، نتیجه دو بار ارسال، دو ایمیل یا دو بار کم شدن موجودی است. این رایج ترین نوع تکرار در عمل است و بی صدا رخ می دهد.

چرا راه حل های ساده کار نمی کنند؟

dedupe روی شناسه ی تحویل. در ووکامرس، هدر X-WC-Webhook-Delivery-ID برای هر تلاش یک شناسه ی لاگ تازه است. پس تحویل دوباره با این کلید «جدید» دیده می شود. به همین دلیل در وظیفه ی ارزیابی این رشته در فهرست must_not_include آمده است.

پاسخ 200 پیش از ذخیره. اگر فرایند بعد از پاسخ و پیش از نوشتن در پایگاه داده از کار بیفتد، رویداد برای همیشه گم می شود.

انجام همه ی کارها پیش از پاسخ. گیرنده ی کندی که رویداد را اعمال می کند و ایمیل را می فرستد و بعد پاسخ می دهد، خودش timeout را دعوت می کند.

الگوی درست: صندوق ورودی پایدار

رویه ای که مهارت order-and-webhook-reliability آموزش می دهد چنین است:

  1. بدنه ی خام را پیش از هر تجزیه ی JSON بخوانید.
  2. امضا را با مقایسه ی زمان ثابت بررسی کنید و درخواست بدون امضا یا با امضای نامعتبر را پیش از نگاه کردن به نوع رویداد رد کنید.
  3. رویداد را در یک صندوق ورودی پایدار (durable inbox) ذخیره کنید که روی یک کلید پایدار یکتا باشد.
  4. فقط پس از موفقیت ذخیره پاسخ 2xx بدهید.
  5. تغییر را فقط اگر این رویداد از نسخه ی ذخیره شده جدیدتر است اعمال کنید.
  6. اگر رویداد ناقص یا خارج از ترتیب بود، سفارش را از منبع حقیقت بخوانید و همان را بنویسید.

کلید یکتای عملی: شناسه ی رویداد ارائه دهنده، اگر در تلاش های دوباره ثابت می ماند؛ در غیر این صورت هش موضوع، شناسه ی منبع و نسخه ی منبع (در ووکامرس date_modified_gmt). هر ردیف صندوق وضعیتی دارد: stored، applied یا ignored.

نکته ی ظریف: اثرهای جانبی

کلید یکتا به تنهایی کافی نیست. تحویل دوباره باید ردیف را در وضعیت applied پیدا کند، 2xx برگرداند و هیچ اثر جانبی اجرا نکند. این فقط وقتی برقرار است که هر اثر جانبی (ایمیل، موجودی، ارسال) در همان تراکنشی ثبت شود که ردیف را applied می کند، یا خودش روی شناسه ی رویداد کلید خورده باشد. شبه کد زیر فقط یک نمونه ی مفهومی برای توضیح ایده است و کد خود مخزن نیست:

BEGIN
row = inbox.insert_or_get(event_key)
if row.status == "applied": COMMIT; return 2xx
apply_order_change(event)
outbox.add("send_email", key=event_key)
outbox.add("create_fulfillment", key=event_key)
row.status = "applied"
COMMIT
return 2xx

یک کارگر جداگانه صف خروجی (outbox) را پردازش می کند و چون هر ورودی روی شناسه ی رویداد کلید خورده، تکرار آن هم بی اثر است.

دو حالت دشوارتر: تحویل هم زمان و شکست نیمه کاره

تحویل هم زمان. گاهی همان رویداد در یک لحظه به دو سرور یا دو کارگر می رسد. اگر گیرنده اول صندوق را بخواند، چیزی پیدا نکند و بعد درج و اعمال کند (الگوی «بررسی، سپس درج»)، هر دو درخواست از بررسی رد می شوند و سفارش دو بار اعمال می شود. راه درست این است که رویداد با یک درج اتمی روی کلید یکتا «تصاحب» شود، مثلا INSERT ... ON CONFLICT DO NOTHING، و فقط درخواستی که ردیف را واقعا ساخته تغییر را اعمال کند. درخواست دیگر 2xx می دهد و هیچ کاری انجام نمی دهد. قفل کردن کل جدول لازم نیست؛ کلید یکتا تصمیم می گیرد.

شکست نیمه کاره. تصاحب نباید یک شکست را پنهان کند. اگر تحویل اول ردیف صندوق را درج کند و پیش از commit شدن تغییر سفارش از کار بیفتد، تحویل دوباره ای که فقط وجود ردیف را ببیند و 2xx بدهد، رویداد را برای همیشه گم می کند و سفارش هرگز «پرداخت شده» نمی شود. راه حل این است که تغییر در همان تراکنشی اعمال شود که ردیف را applied می کند، تا خرابی هر دو را rollback کند؛ ردیفی که پس از timeout هنوز در وضعیت pending مانده، قابل تلاش دوباره است و تحویل بعدی آن را دوباره تصاحب و کار را کامل می کند. پاک کردن ردیف صندوق راه حل نیست. افزون بر این، برای هر تحویل یک ورودی ممیزی با کلید شناسه ی رویداد ثبت می شود که می گوید آن تحویل چه کرد؛ مثلا اول «failed: rolled back before apply» و بعد «applied». نتیجه: تاریخچه ی سفارش دقیقا یک تغییر وضعیت و هر دو تلاش را نشان می دهد.

وظیفه های ارزیابی متناظر

این سناریو در نسخه ی 0.3.2 به شکل وظیفه ی redelivery-after-timeout اضافه شد. صورت مسئله همین داستان است و پاسخ باید چهار مفهوم applied، 2xx، side effect و timeout را داشته باشد و نباید به X-WC-Webhook-Delivery-ID تکیه کند. همان نسخه توضیح این حالت را به خود مهارت، فهرست بررسی و «دام ها»یش و یک خط متناظر به قاعده اضافه کرد؛ چون اعتبارسنج اجازه نمی دهد وظیفه چیزی بخواهد که مهارت یاد نداده.

پس از آن دو وظیفه ی دیگر هم به همین مهارت اضافه شد. وظیفه ی concurrent-duplicate-delivery از پاسخ می خواهد مفاهیم unique key، ON CONFLICT، concurrent و 2xx را داشته باشد و نه به X-WC-Webhook-Delivery-ID تکیه کند و نه «قفل کل جدول» را پیشنهاد دهد. وظیفه ی redelivery-after-partial-failure مفاهیم pending، same transaction، audit، exactly one و rolled back را می خواهد و پاک کردن ردیف صندوق را رد می کند. به این ترتیب این مهارت اکنون پنج وظیفه ی ارزیابی دارد.

جزئیات خاص ووکامرس

  • امضای ووکامرس به شکل base64(hmac_sha256(secret, raw_body)) است. تجزیه ی JSON و امضای دوباره ی JSON.stringify هرگز مطابقت نمی کند، چون فرستنده بایت های اصلی را امضا کرده است.
  • ووکامرس پس از بیش از پنج شکست پیاپی وب هوک را غیرفعال می کند (فیلتر woocommerce_max_webhook_delivery_failures با مقدار پیش فرض ۵).
  • پینگ فعال سازی رویداد سفارش نیست؛ 2xx بدهید و سفارشی نسازید.
  • اگر پایگاه داده در دسترس نیست، 503 برگردانید تا فرستنده دوباره بفرستد؛ کلید یکتا تکرار را امن می کند.

نصب و استفاده

npx @sadrmanesh/commerce-agent-skills init
npx @sadrmanesh/commerce-agent-skills add order-and-webhook-reliability stripe-payments

فرمان اول همه ی مهارت ها را در .agents/skills/ و همه ی قاعده ها را در .agents/rules/ کپی می کند. فرمان دوم فقط مهارت های نام برده را نصب می کند.

محدودیت ها

  • ارزیابی ها مبتنی بر زیررشته اند؛ پاسخی می تواند رشته ها را داشته باشد و همچنان اشتباه باشد. خواندن پاسخ های ناموفق و نمونه ای از پاسخ های موفق لازم است.
  • نمونه های حل شده امتیاز یک عامل زنده نیستند.
  • رویه ی صندوق ورودی فرض می کند پایگاه داده تراکنش یا قید یکتایی دارد.

جمع بندی

دانش تجربی تجارت الکترونیک را می توان به شکلی بسته بندی کرد که عامل های برنامه نویس از آن استفاده کنند، اما این بسته بندی باید مثل کد آزمون داشته باشد. مطالعه ی موردی وب هوک تکراری نشان می دهد چرا: درس اصلی، یعنی «dedupe روی نسخه ی منبع، نه شناسه ی تحویل، و اثرهای جانبی داخل همان تراکنش»، در یک جمله جا می شود، اما اگر از متن مهارت حذف شود، عامل با اطمینان کامل کدی می نویسد که مشتری را دو بار ایمیل می کند.

منابع و لینک ها

نوشته های مرتبط