فاز 19 · درس 21

پیاده‌سازی رجیستری ابزارها با اعتبارسنجی اسکیما

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
یک سیستم بایگانی دقیق و کشوهای طبقه‌بندی شده همراه با یک ابزار سنجش شیشه‌ای شفاف برای بررسی ابعاد قطعات ورودی
فهرست مطالب
  1. اهداف یادگیری
  2. چرا رجیستری مقدم بر ابزار است؟
  3. ساختار یک رکورد ابزار چگونه است؟
  4. زیرمجموعه‌ی JSON Schema 2020-12
  5. مسیرهای خطا با JSON Pointer
  6. ثبت و بازنویسی
  7. اعتبارسنج چه هست و چه نیست؟
  8. ساختار
  9. راهنمای خواندن کد
  10. برای مطالعه‌ی بیشتر

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

حل این مشکل در تفکیک ساختار داده از منطق اجرا است. با تعریف یک رجیستری نوع‌دار (Tool Registry)، پیش از آنکه کد ابزار فراخوانی شود، آرگومان‌ها با یک اعتبارسنج سبک و بر مبنای JSON Schema ارزیابی می‌شوند. در صورت بروز مغایرت، مسیر دقیق خطا در قالب JSON Pointer گزارش می‌شود تا مدل بتواند در یک گام ورودی خود را تصحیح کند.

این رویکرد معماری پایه، وابستگی ایجنت به آزمون و خطا را کاهش داده و مرز مشخصی میان مشخصات ورودی، داده‌های ورودی و کدهای اجرایی برقرار می‌کند.

ابزاری که ایجنت نتواند اعتبارسنجی‌اش کند، ابزاری است که ایجنت اصلاً نمی‌تواند فراخوانی کند. رجیستری و اعتبارسنج اسکیما را قبل از ساختن خود ابزارها پیاده‌سازی کنید.

اهداف یادگیری

  • نگه‌داری یک رجیستری نوع‌دار (Typed registry) از «نام ابزار ← اسکیما ← کنترل‌کننده (Handler)» که دیسپچر (Dispatcher) بتواند یک بار آن را بخواند و بعد با خیال راحت به آن اعتماد کند.
  • پیاده‌سازی زیرمجموعه‌ای از استاندارد JSON Schema 2020-12 که کلیدواژه‌های مورد استفاده در 90 درصد فراخوانی‌های واقعی ابزارها را پوشش دهد.
  • بازگرداندن مسیرهای خطای دقیق در قالب JSON Pointer، تا مدل بتواند در یک رفت‌وبرگشت خطا را تصحیح کند.
  • رد کردن ثبت مجدد بدون بازنویسی صریح (Explicit override)؛ چرا که بازنویسی‌های بی‌صدا دقیقاً عامل اصلی ناهمگام‌شدن فهرست ابزارها در محیط عملیاتی (پروداکشن) هستند.
  • خالص (Pure) نگه‌داشتن اعتبارسنج (بدون I/O، بدون وابستگی به زمان، بدون متغیرهای سراسری) تا بتوان آن را روی گزارش‌های بازپخش (Replay logs) دوباره اجرا کرد.
دروازه رجیستری + اسکیمااعتبارسنجی پیش از اجرای هر کنترل‌کننده
REGISTRY + SCHEMA GATEThe registry pins name to schema to handler once, and the dispatcher trusts it afterwards. A bad call never reaches the handler: the schema check bounces it back with a json-pointer path the model can fix in one round trip. The corrected call passes the same pure validator and only then touches code.modeltool registrynameبررسی اسکیماارجاع handlerعدم رونویسی بی‌صداhandler/args/limit: expected integerاسکیما داده است، کنترل‌کننده کد: اعتبارسنج هرگز به I/O دست نمی‌زند
رجیستری نام را تنها یک‌بار به اسکیما و کنترل‌کننده متصل می‌کند و توزیع‌کننده پس از آن به آن اعتماد دارد. یک فراخوانی نامعتبر هرگز به کنترل‌کننده نمی‌رسد: بررسی اسکیما آن را با یک مسیر json-pointer پس می‌زند تا مدل بتواند آن را در یک رفت‌وبرگشت اصلاح کند. فراخوانیِ اصلاح‌شده از همان اعتبارسنجِ خالص عبور کرده و تنها پس از آن کد را لمس می‌کند.

چرا رجیستری مقدم بر ابزار است؟

یک ایجنت برنامه‌نویسی در سال 2026، ابزارهای ثبت‌شده‌ی بیشتری نسبت به گنجایش پنجره‌ی بافتار (Context Window) یک مدل در اختیار دارد. یک بستر آزمایشی (Harness) واقعی و جدی ممکن است 200 ابزار را ثبت کند و در هر نوبت گفت‌وگو (Turn)، بین 10 تا 40 ابزار را در اختیار مدل بگذارد. رجیستری، منبع حقیقت (Source of Truth) برای سه پرسش اساسی است: «چه ابزارهایی وجود دارند؟»، «آرگومان‌های آن‌ها چه ساختاری دارند؟» و «کدام کنترل‌کننده را باید صدا بزنم؟». با مشخص شدن پاسخ این سه پرسش، بقیه‌ی بستر آزمایشی دیگر نیازی به حدس زدن ندارد.

اشتباهی که می‌خواهیم از آن پرهیز کنیم، ارائه‌ی کنترل‌کننده‌ها بدون اسکیما، یا ارائه‌ی اسکیماها بدون اعتبارسنجی است. هر دو بسیار رایج هستند. هر دوی این اشتباهات لایه‌ی بعدی (دیسپچر در درس 23) را به یک بازی حدس‌وگمان تبدیل می‌کنند که تنها نشانه‌ی شکست در آن، ردگیری محاسبات (Stack trace) ناشی از خطای کنترل‌کننده خواهد بود.

ساختار یک رکورد ابزار چگونه است؟

ToolRecord
  name        : str          (unique, lowercase alphanumeric and underscore segments separated by dots, e.g., snake_case.segment.case)
  description : str          (one line, shown to the model)
  schema      : dict         (JSON Schema 2020-12 subset)
  handler     : Callable     (async or sync, returns Any)
  idempotent  : bool         (dispatcher uses this for retry decisions)
  timeout_ms  : int          (override per-tool dispatcher default)

اسکیما تنها فیلدی است که اعتبارسنج به آن دست می‌زند. کنترل‌کننده برای اعتبارسنج کاملاً مات و دست‌نخورده است. ما عمداً آن‌ها را از هم جدا کرده‌ایم. اسکیما داده است و کنترل‌کننده کد. ترکیب این دو وسوسه‌تان می‌کند که منطق اعتبارسنجی را داخل کنترل‌کننده بگذارید؛ یعنی همان باگی که جلوی آن را می‌گیریم.

زیرمجموعه‌ی JSON Schema 2020-12

مستندات کامل مشخصات نسخه‌ی 2020-12 یک مقاله‌ی بلندبالاست. ما فقط به 8 کلیدواژه نیاز داریم.

type           string / number / integer / boolean / object / array / null
properties     map of property name -> schema
required       list of property names
enum           list of allowed primitive values
minLength      integer, applies to strings
maxLength      integer, applies to strings
pattern        ECMA-262-compatible regex, applies to strings
items          schema applied to every array element

همین مقدار برای پوشش دادن نیازهای واقعی یک API ابزار کاملاً کافی است. کلیدواژه‌هایی که حذف کرده‌ایم (مانند oneOf ،anyOf ،allOf ،$ref و عبارات شرطی) اگرچه در اسکیماهای پروداکشن معتبرند، اما اعتبارسنج را به یک پیمایش‌گر درخت با حلقه‌ها و چرخه‌های تکرارشونده تبدیل می‌کنند. ما داریم یک رجیستری می‌سازیم، نه یک موتور JSON Schema.

مسیرهای خطا با JSON Pointer

وقتی اعتبارسنجی شکست می‌خورد، اعتبارسنج فهرستی از خطاها را برمی‌گرداند. هر خطا شامل یک مسیر بر پایه‌ی JSON Pointer به درون ورودی است. یک اشاره‌گر، دنباله‌ای از نام ویژگی‌ها و اندیس‌های آرایه است که با اسلش شروع می‌شوند.

{"a": {"b": [1, 2, "x"]}}
                    ^
                    /a/b/2

مدل‌ها مسیرهای خطا را بسیار بهتر از جملات متنی می‌فهمند. اگر اسکیما مقدار args.user.email را الزامی بداند و مدل به‌جای آن یک عدد صحیح بفرستد، خروجی خطا باید /user/email همراه با expected_type: string باشد. مدل این خطا را در فراخوانی بعدی، بدون نیاز به توضیحات متنی اضافی اصلاح می‌کند.

ثبت و بازنویسی

متد register(name, schema, handler, **opts) به‌صورت پیش‌فرض از ثبت مجدد ابزار جلوگیری می‌کند. فراخواننده برای جایگزینی حتماً باید مقدار override=True را ارسال کند. این کار نوعی بهداشت عملیاتی است. اینکه دو بخش از کد بی‌صدا ابزاری با نام یکسان ثبت کنند، از آن دسته باگ‌هایی است که پیدا کردنش در پروداکشن یک هفته زمان می‌برد.

رجیستری سه متد خواندن در اختیارتان می‌گذارد: get(name) رکورد را برمی‌گرداند یا خطا می‌دهد؛ validate(name, args) مقدار Ok یا فهرستی از خطاها را ارائه می‌کند؛ و names() اسامی ابزارها را به ترتیب زمان ثبت بازمی‌گرداند.

اعتبارسنج چه هست و چه نیست؟

این اعتبارسنج یک پیمایش بازگشتی و تک‌مرحله‌ای روی درخت اسکیما است. خالص است؛ کنترل‌کننده‌ها را فراخوانی نمی‌کند؛ تبدیل نوع خودکار (Coercion) انجام نمی‌دهد (رشته‌ی "42" از آزمون اسکیمای عددی قبول نمی‌شود) و داده‌ها را هم بی‌صدا کوتاه نمی‌کند.

این اعتبارسنج یک مرز امنیتی نیست. یک کنترل‌کننده‌ی مخرب، حتی پس از تأیید در اعتبارسنجی هم می‌تواند رفتار نامناسبی داشته باشد. دیسپچر در درس 23 لایه‌های مهلت زمانی (Timeout) و ساندباکس (Sandbox) را اضافه خواهد کرد؛ کار رجیستری فقط بررسی ساختار است.

ساختار

ثبت name، schema، handlerاعتبارسنجی آرگومان‌هاکد شماToolRegistrynameschemahandlertimeoutOk یا فهرست خطاها

راهنمای خواندن کد

فایل code/main.py کلاس‌های ToolRegistry ،ToolRecord ،ValidationError و هشت تابع اعتبارسنجی را تعریف می‌کند. اعتبارسنج بر اساس schema["type"] تصمیم‌گیری می‌کند (یا اسکیمای دارای enum را به‌عنوان بررسی enum بدون نوع پردازش می‌کند). هر اعتبارسنج نوع، یا یک لیست خالی بازمی‌گرداند یا لیستی از ValidationError. پیمایش‌گر سطح بالا هنگام پایین رفتن در درخت، خطاها را جمع‌آوری کرده و بخش‌های مسیر را به ابتدای آن‌ها می‌افزاید.

فایل code/tests/test_registry.py فرآیند ثبت، بازنویسی، اعتبارسنجی موفق، خطاهای اعتبارسنجی همراه با مسیر، و تک‌تک کلیدواژه‌های این زیرمجموعه را پوشش می‌دهد.

برای مطالعه‌ی بیشتر

دو امکانی که پس از پایان این درس به‌کارتان می‌آید، حل $ref بر اساس یک بلوک تعاریف محلی و استفاده از additionalProperties: false برای تعریف ساختار سخت‌گیرانه است. هر دو کوچک هستند و هر دو با بزرگ‌تر شدن فهرست ابزارها به بیش از 50 مورد، معمولاً اضافه می‌شوند. ما آن‌ها را از این درس کنار گذاشتیم تا خواندن این فایل طولانی نشود.

در درس بعدی (درس 22)، بستر انتقال (Transport) از نوع JSON-RPC stdio را می‌سازیم که این رجیستری را در اختیار کلاینت مدل می‌گذارد. در درس پس از آن (درس 23)، هر دوی آن‌ها را داخل یک دیسپچر مجهز به مهلت زمانی و تلاش مجدد بسته‌بندی خواهیم کرد.

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسچرا اسکیمای یک ابزار جدا از کنترل‌کننده (handler) آن ثبت می‌شود؟
  2. حین درسکدام کلمات کلیدی عمداً خارج از زیرمجموعه‌ای هستند که این درس پیاده‌سازی می‌کند؟
  3. حین درسچرا ()register به‌طور پیش‌فرض موارد تکراری را رد می‌کند؟
  4. حین درسهنگامی که یک اسکیما نوع integer را مشخص کرده اما مقدار True را دریافت می‌کند، اعتبارسنج چه می‌کند؟
  5. پس از درسوقتی شیء ورودی {'user': {'email': 0}} است و اسکیما نیاز دارد که user.email یک رشته باشد، مسیر خطا چیست؟
  6. پس از درسچرا رجیستری یک مرز امنیتی محسوب نمی‌شود؟

سؤالات متداول

چرا رجیستری ابزار باید پیش از کنترل‌کننده‌ها (Handlers) پیاده‌سازی شود؟

رجیستری مرجع واحدی برای نام، اسکیما و کنترل‌کننده هر ابزار است و مانع اجرای کدهایی با ورودی نامعتبر یا ثبت تکراری و تصادفی ابزارها می‌شود.

چرا در خطاهای اعتبارسنجی از JSON Pointer استفاده می‌شود؟

مدل‌های زبانی مسیرهای دقیق مانند user/email/ را بسیار بهتر از توضیحات متنی بلند تحلیل کرده و در یک دور تعامل نوع داده را تصحیح می‌کنند.

آیا اعتبارسنجی اسکیما یک مرز امنیتی کامل است؟

خیر؛ اعتبارسنج فقط تطابق ساختار ورودی را می‌سنجد. مسائل امنیتی نظیر ساندباکس و مهلت زمانی باید در لایه دیسپچر پیاده شوند.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.