Knaph

ایمن نگه داشتنِ عامل18 از 376 دقیقه مطالعه

پیش از هر کار، پرسیدن

در پایانِ بخشِ قبل، mini می‌تواند فایل بخواند، فایل بنویسد و هر دستورِ شِلی را در پوشه‌اش اجرا کند. هر چه مدل بخواهد، همان لحظه انجام می‌دهد. برای آزمایش روی پوشه‌ای پر از فایل‌های بی‌ارزش این اشکالی نداشت. اما لیلا می‌خواهد mini را سراغِ گزارش‌های واقعی‌اش بفرستد، و پیش از آن mini باید کاری را بکند که همه‌ی عامل‌هایی که لیلا با آن‌ها کار کرده می‌کنند: بایستد و بپرسد.


لیلا ساده‌ترین راه این است که در هر ابزار یک بررسی بگذارم. write_file بپرسد «مطمئنی؟»، run_command هم بپرسد.

امید برای چهار ابزار جواب می‌دهد. وقتی ابزارِ پنجم را اضافه کنیم چه؟ یا ابزارِ ششمی که کسِ دیگری می‌نویسد؟

لیلا هر کدام بررسیِ خودش را لازم دارد، و دیر یا زود یکی یادش می‌رود.

امید پس یک بررسی را کجا می‌شود گذاشت که هیچ فراخوانیِ ابزاری نتواند دورش بزند؟

لیلا در run_tool. هر فراخوانی‌ای که از مدل می‌آید، پیش از رسیدن به هر ابزاری از آن می‌گذرد.

امید درست است. Claude Code هم اجازه‌هایش را همان‌جا بررسی می‌کند: تنها دری که بینِ خواسته‌ی مدل و کامپیوترِ تو هست. حالا وقتی بررسی رد می‌کند، چه باید بگوید؟

لیلا هیچ؟ فقط ابزار را اجرا نمی‌کند.

امید پس به درخواستِ بعدی فکر کن. مدل یک ابزار خواسته و جوابی نگرفته. چه می‌بیند؟

لیلا یک فراخوانیِ ابزار که هیچ چیز بعدش نیامده. یا گیر می‌کند، یا دوباره همان کار را امتحان می‌کند.

امید تازه API اصلاً چنین درخواستی را نمی‌پذیرد: هر بلوکِ tool_use باید با یک tool_result با همان شناسه جواب بگیرد. پس رد کردن هم یک نتیجه است. فقط می‌گوید «جلویش گرفته شد» و چرا.


یک تابع تصمیم می‌گیرد

بررسیِ اجازه (permission check) تابعی است که هارنس پیش از اجرای هر فراخوانیِ ابزار صدا می‌زند تا بگوید آن فراخوانی اجازه‌ی اجرا دارد یا نه. نامش در mini check_permission است. نامِ ابزار و ورودی‌هایش را می‌گیرد و اگر فراخوانی می‌تواند اجرا شود None برمی‌گرداند، وگرنه دلیلی کوتاه:

ALLOW = {"read_file", "list_files", "load_skill", "delegate"}  # never ask
DENY_COMMANDS = ["rm -rf", "git push", "curl ", "sudo "]
 
 
def check_permission(name, args):
    """None if the call may run, else the reason it may not."""
    if "path" in args and inside(args["path"]) is None:
        return f"{args['path']} is outside the project folder"
    if name == "run_command" and any(bad in args["command"] for bad in DENY_COMMANDS):
        return "that command is not allowed in mini"
    if name in ALLOW:
        return None
    answer = input(f"\nmini wants to {name} {json.dumps(args)[:200]}. Allow? [y/N] ")
    return None if answer.strip().lower() == "y" else "the user said no"
Python

آن را از بالا بخوانید، چون ترتیبِ خطها همان طراحی است:

  1. آنچه هرگز نباید رخ دهد رد می‌شود. مسیری بیرون از پوشه‌ی پروژه (inside همان تابعِ درسِ «ابزارهایی که به فایل‌ها دست می‌زنند» است) و دستورهای فهرستِ ممنوع، پیش از هر چیزِ دیگری رد می‌شوند. هیچ قاعده‌ی اجازه‌ای و هیچ «بله»ای از آدمی خسته نمی‌تواند از این خطها بگذرد.
  2. آنچه همیشه بی‌خطر است اجازه می‌گیرد. خواندن و فهرست کردنِ فایل‌ها چیزی را تغییر نمی‌دهد، پس هیچ‌وقت نمی‌پرسد. (load_skill و delegate در یکی از بخش‌های بعد می‌آیند؛ آن‌ها هم فقط می‌خوانند.)
  3. درباره‌ی بقیه پرسیده می‌شود. نوشتنِ فایل یا اجرای دستور منتظرِ آدم می‌ماند. هر جوابی جز y یعنی نه، پس اگر به اشتباه Enter بزنید اتفاقی نمی‌افتد.

این همان شکلی است که در اجازه‌های عامل دیدید: اول ممنوع، بعد مجاز، بعد پرسیدن. اینجا همان را در دوازده خطِ خوانا دارید.

یک فراخوانیِ ابزار از بررسیِ اجازه‌ی mini می‌گذرد: اول «درونِ پوشه است؟»، بعد «دستورِ ممنوع است؟»، بعد «همیشه مجاز است؟» و آخر «از آدم بپرس». هر کدام از این‌ها می‌تواند درخواست را به نتیجه‌ی Blocked تبدیل کند که به مدل برمی‌گردد. اگر درخواست از همه بگذرد، ابزار اجرا می‌شود.

وصل کردنِ آن به run_tool

بررسی درست پیش از اجرای ابزار، در run_tool قرار می‌گیرد:

def run_tool(block, allowed):
    name, args = block.name, block.input
    reason = None if name in allowed else f"no tool called {name}"
    reason = reason or check_permission(name, args)
    if reason:
        return {"type": "tool_result", "tool_use_id": block.id,
                "content": f"Blocked: {reason}", "is_error": True}
    ...  # run the tool, as before
Python

به سه نکته دقت کنید:

  • رد کردن هم یک tool_result است، با همان tool_use_id فراخوانی‌ای که به آن جواب می‌دهد. گفت‌وگو معتبر می‌ماند و مدل می‌فهمد چه شد.
  • is_error برابر True است. این به مدل می‌گوید این فراخوانی کار نکرد، درست مثل ابزاری که خطا داده، تا راهِ دیگری انتخاب کند و طوری ادامه ندهد که انگار فایل نوشته شده است.
  • دلیل برای مدل نوشته شده است. «the user said no» باعث می‌شود مدل بایستد و بپرسد لیلا چه می‌خواهد؛ «outside the project folder» باعث می‌شود مسیری درونِ پوشه را امتحان کند. یک «نه»ی خالی مدل را سرگردان می‌گذارد.

پیام‌ها پس از یک رد. پیامِ assistant یک بلوکِ tool_use با id برابر t3 دارد که می‌خواهد README.md را بنویسد. پیامِ user بعدی یک tool_result برای t3 دارد، با is_error برابر true و متنِ «Blocked: the user said no». بعد مدل پاسخ می‌دهد و می‌پرسد لیلا به جایش چه می‌خواهد.

کاری که فهرستِ ممنوع نمی‌تواند بکند

فهرستِ ممنوع خیال را راحت می‌کند، اما فقط متن را مقایسه می‌کند. rm -rf old/ رد می‌شود، اما rm -r -f old/ نه، و find old -delete هم نه، و نه یک خط پایتون که همان فایل‌ها را پاک کند. لازم نیست مدل نیتِ بدی داشته باشد تا یکی از این‌ها را بنویسد: فقط می‌خواهد کار را تمام کند، و وقتی راهِ اول بسته است راهِ دیگری برمی‌دارد.

پس هر قاعده را برای کاری به کار ببرید که در آن خوب است:

  • فهرستِ مجاز محکم است. «خواندن هیچ‌وقت نمی‌پرسد، بقیه همه می‌پرسند» را نمی‌شود دور زد، چون هر چه در آن نامی ندارد به پرسش می‌رسد.
  • فهرستِ ممنوع یک سرعت‌گیر است. اشتباهِ رایج را می‌گیرد و دلیلی روشن به مدل می‌دهد. جلوی کسی را که راهِ دیگری پیدا کند نمی‌گیرد.
  • مرزهای واقعی از بیرونِ برنامه می‌آیند. برای اینکه مطمئن شوید یک دستور به چیزِ دیگری دست نمی‌زند، mini را درونِ یک سندباکس (sandbox) اجرا کنید: یک کانتینر یا یک حسابِ کاربریِ جدا که فقط به یک پوشه دسترسی دارد و به شبکه نه. آن وقت حتی دستوری که از همه‌ی قاعده‌ها بگذرد هم فقط به همان چیزی می‌رسد که آنجاست.

امتحان کنید

کلیدِ API را تنظیم کنید، mini را در یک پوشه‌ی تمرینی اجرا کنید و از آن بخواهید «create a file called hello.txt saying hi». وقتی پرسید، n بزنید. بعد بخوانید که پس از آن چه می‌گوید: ردِ شما را به‌صورتِ نتیجه‌ی ابزار دیده است. دوباره اجرایش کنید و این بار y بزنید. کلید ندارید؟ دو پیامِ تصویرِ دوم را بخوانید و با کلماتِ خودتان بنویسید که مدل بعد از هر کدام چه می‌داند.

درست به کار بردن

  1. بررسی را در یک جا بگذارید. همه‌ی فراخوانی‌ها باید پیش از اجرای هر ابزاری از یک تابع بگذرند، تا هیچ ابزارِ تازه‌ای بی‌بررسی وارد نشود.
  2. ردهای قطعی را اول بگذارید. قاعده‌هایی که هرگز نباید نادیده گرفته شوند، بالای فهرستِ مجاز و پرسش می‌آیند تا هیچ چیزِ بعدی نتواند آن‌ها را بی‌اثر کند.
  3. به هر فراخوانیِ ابزار جواب بدهید. فراخوانیِ ردشده یک tool_result با شناسه‌ی خودش و is_error می‌گیرد، وگرنه درخواستِ بعدی شکست می‌خورد و مدل سرگردان می‌ماند.
  4. به فهرستِ مجاز بیشتر از فهرستِ ممنوع اعتماد کنید. آنچه را بی‌خطر است فهرست کنید و درباره‌ی بقیه بپرسید؛ برای آنچه باید ناممکن باشد، سندباکس به کار ببرید.

واژه‌های این درس

  • بررسیِ اجازه (permission check) - تابعی که هارنس پیش از هر فراخوانیِ ابزار صدا می‌زند و فراخوانی را اجرا می‌کند، از آدم می‌پرسد، یا با یک دلیل ردش می‌کند
  • فهرستِ ممنوع (deny list) - الگوهایی که همیشه رد می‌شوند؛ نوشتنش آسان است، اما با دستوری که جورِ دیگری نوشته شده به‌آسانی دور زده می‌شود
  • is_error - بخشی از tool_result که به مدل می‌گوید فراخوانی‌اش شکست خورده یا جلویش گرفته شده، پس باید راهِ دیگری امتحان کند

ایده‌ی اصلی

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

دانش خود را بسنجید

ورود برای سنجش دانش خود از این درس و پیگیری پیشرفتتان.