تصور کنید یک برنامهنویس میخواهد فرآیند پرداخت محصولش را خودکار کند، اما عامل هوشمند او بهجای دکمهٔ «تأیید»، روی لینک «قوانین» در پایین صفحه کلیک میکند. حتی اگر یک عامل مرورگر از هوش مصنوعی بسیار بالایی برخوردار باشد، گم کردن یک دکمه در حد چند پیکسل همچنان یک شکست در دقت محسوب میشود. این شکست در دقت، دقیقاً همان جایی است که اکثر عاملهای مرورگر در دنیای واقعی سقوط میکنند.
طبق گزارش Smoketest.sh، این تیم برای پر کردن شکاف قابلیت اطمینان، استفاده از توصیفات زبان طبیعی بهعنوان انتخابگرهای (selectors) اصلی المانها را ممنوع کرد. در Smoketest.sh، کاربران یک جریان کاری را در قالب یک جمله ساده توصیف میکنند؛ مثلاً: «وارد حساب شو، یک صندلی پرداختشده اضافه کن و تأیید کن که صورتحساب بهروزرسانی شده است» و سپس یک عامل هوشمند این مسیر را در یک مرورگر واقعی اجرا میکند. عامل صفحه را میخواند، مراحل لازم را تصمیمگیری میکند و Playwright را برای اجرای آنها هدایت میکند.
نسخه اول این سیستم در دمو عالی عمل کرد اما در دومین اجرای خود از هم پاشید. این شکست یک نقص بحرانی را آشکار کرد: اجازه دادن به یک مدل زبانی بزرگ (LLM) برای هدف قرار دادن المانهای صفحه از طریق توصیفات زبان طبیعی، بهطور بنیادی ناپایدار و لرزان (flaky) است.
اکثر عاملهای هوشمند فعلی بهگونهای عمل میکنند که آنچه میخواهند روی آن کلیک کنند را به انگلیسی توصیف میکنند؛ مثلاً «دکمه Sign in». این رویکرد در محیط تولید شکست میخورد زیرا توصیف در هر بار اجرا دوباره تولید میشود و بهندرت اتفاق میافتد که دقیقاً به یک المان واحد اشاره کند. برای مثال، یک صفحه ورود ساده اغلب شامل سه مورد است که با عبارت «Sign in» مطابقت دارند: یک لینک در نوار پیمایش، یک لینک در فوتر و خودِ دکمه اصلی. در چنین مواردی، مکانیاب (locator) به یک لیست تبدیل میشود و Playwright روی اولین مورد کلیک میکند، که ممکن است عامل را به جایی پیشبینی نشده هدایت کند.
مشکلات دیگر شامل تغییرات در متنها (copy changes) است؛ ممکن است متن یک دکمه در یک هفته «Sign In» باشد و هفته بعد به «Log in» تغییر کند و بدین ترتیب توصیفات قبلی منسوخ شوند. علاوه بر این، مدل اغلب توصیفات خود را در هر بار اجرا بازنویسی میکند. عبارت «The Sign in button» ممکن است به «دکمه ورود آبی در بالا سمت راست» تبدیل شود و باعث شود جستوجوهای مبتنی بر نقش و نام (role-and-name lookups) کاملاً با شکست مواجه شوند. اینها باگهای مدل نیستند، بلکه نتیجه استفاده از عبارتهای مبهم و بازتولیدشده انگلیسی بهعنوان انتخابگر در یک صفحه شلوغ هستند.
برای حل این بحران، Smoketest منبع حقیقت (Source of Truth) عامل را از چیدمانهای بصری یا HTML خام به درخت دسترسی (Accessibility Tree) تغییر داد. درخت دسترسی — شبیه به یک نقشهٔ متنی است که برای صفحهخوانهای نابینایان طراحی شده و ساختار واقعی صفحه را بدون پیچیدگیهای بصری نشان میدهد. با استفاده از متد page.ariaSnapshot({ mode: 'ai' }) در Playwright، عامل یک نمایش فشرده و مبتنی بر نقش از صفحه دریافت میکند که در آن هر المان تعاملی با یک شناسهی پایدار مانند [ref=eN] علامتگذاری شده است.
مکانیسمهای هدفگذاری پایدار
به نقل از مستندات فنی این پروژه، مدل اکنون بهجای حدس زدن، از ابزار خاصی به نام getAccessibilityTree استفاده میکند. این ابزار متد page.ariaSnapshot({ mode: 'ai' }) را اجرا کرده تا یک نمایش ساختاریافته از محتوای صفحه برگرداند. مدل اکنون درختی شبیه به این را میبیند:
- heading "Welcome back" [level=1]
- textbox "Email" [ref=e4]
- textbox "Password" [ref=e5]
- button "Sign in" [ref=e6]
- link "Forgot password?" [ref=e7]

حالا عامل میتواند بهطور مشخص به e6 اشاره کند. این شناسه مانند یک قرارداد سختگیرانه بین آنچه مدل درک میکند و آنچه Playwright اجرا میکند، عمل میکند. این رویکرد اسنپشات ساختاریافته، همان متدی است که توسط سرور Playwright MCP مایکروسافت استفاده میشود: اجازه دهید مدل روی ارجاعات دسترسی (accessibility refs) عمل کند، نه روی پیکسلها یا حدسها.
از آنجایی که aria-ref=eN یک موتور مکانیاب درجه اول در Playwright است، عامل دیگر به عبارتهای مبهم متکی نیست. ابزار کلیک بهگونهای برنامهریزی شده است که اولویت را به شناسه (ref) نسبت به توصیف بدهد:
execute: async ({ ref, description }) => {
const refStr = ref?.trim() || null;
const text = description?.trim() || null;
if (!refStr && !text) {
throw new Error('click requires either ref or description');
}
const locator = refStr
? page.locator(`aria-ref=${refStr}`) // stable path
: await resolveLocator(page, text!); // fallback path
await locator.click();
}
استراتژیهای بازیابی و لایههای پشتیبان
البته شناسهها هم همیشه کامل نیستند. برای زمانی که مدل روی المانی عمل میکند که در آخرین اسنپشات نبوده (مانند المانی که در اثر یک تغییر پویا ظاهر شده)، یک «نردبان تعمدی» (deliberate ladder) طراحی شده است. تابع resolveLocator از طریق یک لیست از عبارتهای کاندید، چندین استراتژی تطبیق را به ترتیب امتحان میکند:
- جستوجوی نقش (Role Lookups): اگر یک راهنمای نقش (role hint) در دسترس باشد، متد
page.getByRole(roleHint, { name: phrase, exact: false })را امتحان میکند. - تطبیق برچسب (Label Matching): برای فیلدهای فرم، متد
page.getByLabel(phrase, { exact: false })را امتحان میکند. - جستوجوی Placeholder: برای ورودیها، متد
page.getByPlaceholder(phrase, { exact: false })را بررسی میکند. - جستوجوی متنی (Text Search): آخرین تلاش از طریق
page.getByText(phrase, { exact: false })انجام میشود.
برای اطمینان از اینکه تطبیق معتبر است، هر کاندید باید از یک بررسی isVisible عبور کند؛ این بررسی در واقع یک دستور waitFor({ state: 'visible' }) به مدت ۵ ثانیه است که در یک بلوک try/catch قرار گرفته تا از کلیک روی المانهای مخفی جلوگیری کند. علاوه بر این، سیستم برای جلوگیری از اثرگذاریِ پرحرفی مدل (verbosity)، عبارتهای داخل کوتیشن را از توصیفات استخراج میکند (مثلاً عبارت «روی دکمهای که برچسب "Place order" دارد کلیک کن» به عبارت ساده «Place order» تبدیل میشود) تا کلمات اضافی، تطبیق دقیق را خراب نکنند.
بستن حلقهٔ شکست
ابزارهایی که شناسه (ref) را میپذیرند به تنهایی کافی نیستند، زیرا LLMها بهطور غریزی عاشق توصیف چیزها به زبان انگلیسی هستند. برای مهار این عادت، Smoketest سه قانون سیستمی غیرقابل مذاکره وضع کرده است:
۱. پیش از لمس هر صفحهای که قبلاً دیده نشده، درخت دسترسی را بخوان.
۲. در هر اقدام، شناسه (Ref) را بر توصیف ترجیح بده.
۳. بعد از هر اقدام ناموفق، بهجای بازنویسی توصیف، دوباره درخت دسترسی را برای بهدست آوردن شناسههای تازه بخوان.
قانون سوم حیاتی است. اگر عملیاتی شکست بخورد، غریزه مدل این است که توصیف پیچیدهتر و مفصلتری را امتحان کند. این حرکت اشتباه است زیرا توصیف هرگز مسیر قابل اطمینانی نبود. بازخوانی درخت، شناسههای تازهای را فراهم میکند که با DOM فعلی مطابقت دارند.
وقتی حتی لایههای پشتیبان شکست میخورند، سیستم بهجای بازگرداندن یک خطای خشک مانند «المان یافت نشد» (که باعث سردرگمی و پرتاب مدل میشود)، یک تابع به نام collectClickDiagnostics را فعال میکند. این تابع یک شیء JSON شامل موارد زیر برمیگرداند:
- تعداد نقشها: مثلاً «۰ دکمه مطابقت داشت».
- تعداد برچسبها: مثلاً «۰ برچسب مطابقت داشت».
- تعداد متنها: مثلاً «۳ گره متنی مطابقت داشت».
- نمونه لینکها: لیستی از لینکهای فعلی موجود در صفحه.
- URL فعلی: آدرس دقیق صفحه در لحظه شکست.
این دادههای تشخیصی باعث میشود شکست برای مدل «خواندنی» شود. اگر مدل ببیند textCount: 3 و roleCount: 0 است، متوجه میشود موردی که او «دکمه» میپنداشت، در واقع فقط یک متن ساده بوده و او را ترغیب میکند تا دوباره درخت را بخواند و یک المان تعاملی واقعی را هدف قرار دهد. برای لینکها نیز یک تخصص وجود دارد: اگر مکانیاب شکست بخورد، سیستم href را با تطبیق متن لینک یا aria-label جستوجو میکند تا مستقیماً پیمایش کند و بدین ترتیب مشکلات کلیک روی لایههای پوششی (overlay) یا مداخلات را دور بزند.
موازنههای مهندسی
این معماری هدفگذاری قابل اطمینانی را فراهم میکند، اما این یک عامل قطعی (deterministic) نیست. دو محدودیت اصلی وجود دارد:
ناپایداری اسنپشاتها (Snapshot Volatility): شناسهها فقط برای اسنپشاتی که از آن تولید شدهاند معتبرند. بعد از یک ناوبری یا تغییر در DOM، شناسه e6 ممکن است به هیچجا اشاره نکند یا به یک گره اشتباه اشاره کند. به همین دلیل سیستم با شناسهها به عنوان موارد «مخصوص هر اسنپشات» برخورد میکند و پس از شکستها، اجرای مجدد getAccessibilityTree را اجباری میکند.
هزینه توکنها: اسنپشاتها گران هستند. یک درخت دسترسی برای صفحهای با محتوای زیاد میتواند دهها هزار توکن مصرف کند. این موضوع در تحلیل «آنچه در اجرای Playwright MCP در Claude Code کار میکند و چه چیزی میشکند» به تفصیل شرح داده شده است. در راستای بهینهسازی این هزینهها، روشهایی مانند تبدیل دستورالعملهای متنی به وزنهای رفتاری مورد توجه قرار گرفتهاند تا هزینههای استنتاج در عاملهای هوشمند کاهش یابد. برای مدیریت این هزینه، Smoketest در هر مرحله اسنپشات نمیگیرد، بلکه فقط زمانی این کار را میکند که صفحه جدید باشد یا اقدامی با شکست مواجه شود.
در نهایت، شناسهها تضمین میکنند که وقتی مدل تصمیم میگیرد روی دکمه «Sign in» کلیک کند، دقیقاً روی دکمه درست کلیک کند و نه روی یک لینک در فوتر. البته آنها مانع از این نمیشوند که مدل تصمیم بگیرد کلاً روی چیز اشتباهی کلیک کند؛ آن مسئله نیازمند یک مرحله ارزیابی مجزا است.
توصیههایی برای ساخت عاملهای مرورگر LLM
برای کسانی که در حال ساخت عاملهای مشابه هستند، نکته کلیدی این است که هرگز اجازه ندهید مدل خودش «سلکتور» تولید کند. در عوض، این توالی را دنبال کنید:
- یک اسنپشات ساختاریافته با شناسههای پایدار با استفاده از
page.ariaSnapshot({ mode: 'ai' })فراهم کنید. - تمام ابزارهای عملیاتی را بهگونهای طراحی کنید که ابتدا یک شناسه (ID) بگیرند و توصیف را فقط بهعنوان پشتیبان از طریق
page.locator('aria-ref=eN')بپذیرند. - جریان «ابتدا اسنپشات، سپس عمل» (snapshot-then-act) را در پرامپت سیستمی اجباری کنید.
- در صورت شکست، بهجای اجازه دادن به مدل برای تغییر توصیفات، دوباره اسنپشات بگیرید.
- در صورت عدم یافتن المان، تشخیصهای غنی (rich diagnostics) برگردانید تا مدل بتواند با استفاده از دادهها بازیابی شود.
این گذار باعث شد عامل از وضعیتی که «در دمو پاس میشود» به وضعیتی برسد که «در بار دوم و صدم اجرا نیز پاس میشود».
این روش را روی اپلیکیشن خود امتحان کنید. ما در Smoketest این جریانها (ورود، تسویه حساب، آنبوردینگ، صورتحساب) را بعد از هر استقرار در مرورگرهای واقعی اجرا میکنیم تا دقیقاً به شما بگوییم چه چیزی خراب شده است و نیاز شما را به مالکیت یا نگهداری یک مجموعه تست Playwright برداریم. برای اطلاعات بیشتر به smoketest.sh مراجعه کنید.




گفتگو