Knaph

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

هوک‌ها در عاملِ خودتان

mini حالا پیش از نوشتنِ فایل می‌پرسد. اما لیلا دو قاعده‌ی دیگر هم دارد. در پوشه‌اش فایلی به نامِ .env هست که رمزهای شرکت در آن است، و mini هرگز نباید آن را بخواند، حتی با اجازه‌ی خودِ لیلا ساعتِ دوِ بامداد. و دستوری که ۵۰٬۰۰۰ خط چاپ می‌کند نباید همه‌اش را در زمینه بریزد. هیچ‌کدام از این دو چیزی نیست که از آدم پرسیده شود. قاعده‌هایی‌اند که باید هر بار، بی‌استثنا، برقرار باشند.


لیلا می‌توانم هر دو را به check_permission اضافه کنم. یک if دیگر برای .env، و خروجیِ طولانی... نه، آن یکی بعد از اجرای ابزار پیش می‌آید. check_permission خیلی زود است.

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

لیلا بیشتر از دو تا. یکی می‌خواهد هر دستور را ثبت کند، یکی رمزها را از خروجی پاک کند، یکی هر فایلی را که mini می‌نویسد مرتب کند.

امید دوست داری برای هر کدام run_tool را عوض کنی؟

لیلا نه چندان. ترجیح می‌دهم برای هر لحظه یک فهرست داشته باشم و تابعی به آن اضافه کنم.

امید سیستمِ هوک دقیقاً همین است. Claude Code فهرست‌هایش را از یک فایلِ تنظیمات می‌خواند و هوک‌هایش معمولاً دستورهای شِل‌اند. در mini، هوک یک تابعِ پایتون است و فهرست هم یک فهرستِ پایتون.


دو فهرست از تابع‌ها

هوک در mini تابعی ساده است که run_tool در لحظه‌ای ثابت از هر فراخوانیِ ابزار صدا می‌زند: همان ایده‌ی هوک‌های عامل، این بار در کدِ خودتان. دو لحظه داریم، پس دو فهرست:

def no_secrets(name, args):  # a before-hook: blocks, every time
    if ".env" in args.get("path", ""):
        return "mini never reads .env files"
 
 
def cut_long_output(name, args, output):  # an after-hook: changes the result
    if len(output) > MAX_OUTPUT:
        return output[:MAX_OUTPUT] + f"\n[... {len(output) - MAX_OUTPUT} more characters cut]"
    return output
 
 
BEFORE_TOOL = [no_secrets]
AFTER_TOOL = [cut_long_output]
Python

این دو نوع کارهای متفاوتی دارند و شکلشان هم همین را نشان می‌دهد:

  • هوکِ پیش از اجرا (before-hook) نامِ ابزار و ورودی‌هایش را می‌گیرد. برای ادامه‌ی فراخوانی None برمی‌گرداند و برای جلوگیری از آن یک دلیل، درست مثل check_permission.
  • هوکِ پس از اجرا (after-hook) نام، ورودی‌ها و خروجیِ ابزار را می‌گیرد و خروجی‌ای را برمی‌گرداند که باید به کار برود: همان متن، یا نسخه‌ای تغییرکرده.

no_secrets نه پرسشی دارد و نه استثنایی. نکته همین است: با قاعده‌ای که در هوک است نمی‌شود چانه زد، نه مدل می‌تواند و نه آدمی که عجله دارد. و cut_long_output همان بریدنِ درسِ «ابزارهایی که به فایل‌ها دست می‌زنند» است که به تنها جایی رفته که یک‌جا روی همه‌ی ابزارها اثر می‌کند.

اجرای هوک‌ها

این run_tool است با هر دو فهرست. از اینجا به بعد mini همین نسخه را دارد:

def run_tool(block, allowed):
    name, args = block.name, block.input
    reason = None if name in allowed else f"no tool called {name}"
    for hook in BEFORE_TOOL:
        reason = reason or hook(name, args)
    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}
    try:
        output = str(TOOLS[name][0](**args))
    except Exception as error:
        return {"type": "tool_result", "tool_use_id": block.id, "content": f"Error: {error}", "is_error": True}
    for hook in AFTER_TOOL:
        output = hook(name, args, output)
    return {"type": "tool_result", "tool_use_id": block.id, "content": output}
Python

به این‌ها دقت کنید:

  • reason or hook(...) هوک را فقط تا وقتی صدا می‌زند که هنوز چیزی جلوی فراخوانی را نگرفته باشد. پس اولین هوکی که جلو را بگیرد برنده است، و مدل دلیلِ همان را می‌خواند.
  • هوک‌های پیش از اجرا پیش از پرسیدنِ اجازه اجرا می‌شوند. اگر no_secrets جلوی خواندنِ .env را بگیرد، اصلاً از لیلا چیزی پرسیده نمی‌شود. قاعده‌ای که نمی‌شود نادیده‌اش گرفت نباید بپرسد.
  • هوک‌های پس از اجرا به ترتیب اجرا می‌شوند، هر کدام روی خروجیِ قبلی. ترتیب مهم است: هوکی که رمزها را پاک می‌کند باید پیش از هوکی بیاید که متن را کوتاه می‌کند، وگرنه ممکن است رمزی درست سرِ جای بریدن بیفتد.
  • وقتی ابزار شکست بخورد، هوک‌های پس از اجرا اجرا نمی‌شوند. خطا مستقیم به مدل برمی‌گردد.

یک فراخوانیِ ابزار در run_tool در mini: اول هوک‌های پیش از اجرا (no_secrets)، بعد بررسیِ اجازه، بعد خودِ ابزار، بعد هوک‌های پس از اجرا (cut_long_output)، و نتیجه‌ی ابزار بیرون می‌آید. اگر در یکی از دو مرحله‌ی اول جلوی درخواست گرفته شود، به جایش نتیجه‌ی Blocked برمی‌گردد.

یک بررسی پیش از تمام کردن

Claude Code در لحظه‌های بیشتری از این دو لحظه هوک دارد، و یکی از آن‌ها خیلی به کار می‌آید: بررسی‌ای در لحظه‌ای که عامل می‌خواهد بگوید کارش تمام شد. در mini، «می‌خواهد بگوید تمام شد» همان خطی از حلقه است که stop_reason در آن "end_turn" است. بررسی‌ای در آنجا می‌تواند آزمون‌ها را اجرا کند و اگر رد شدند، مدل را به جای تمام کردن دوباره سرِ کار بفرستد:

if response.stop_reason == "end_turn":
    tests = run_command("python -m pytest -q")
    if not tests.startswith("exit code 0") and checks_left > 0:
        checks_left -= 1
        messages.append({"role": "user", "content": "The tests fail, so the task isn't done yet:\n" + tests})
        continue
    return "".join(b.text for b in response.content if b.type == "text")
Python

این در mini.py نیست، چون همه‌ی پروژه‌ها آزمون ندارند. تغییری است که برای پروژه‌ای که دارد می‌توانید بدهید. دو جزئیات مهم است. شکست به‌صورتِ یک پیامِ عادیِ user برمی‌گردد، چون پیامِ آخر پاسخِ نهاییِ مدل بود و نقش‌ها باید نوبتی بیایند. و checks_left (مثلاً ۳، که پیش از حلقه تنظیم می‌شود) نمی‌گذارد این دو تا ابد سرِ آزمونی که مدل نمی‌تواند درستش کند دور بزنند.

پایانِ حلقه با یک بررسی پیش از تمام کردن. مدل پاسخِ نهایی را می‌نویسد و mini آزمون‌ها را اجرا می‌کند. اگر قبول شوند، پاسخ نشان داده می‌شود و حلقه تمام می‌شود. اگر رد شوند، خروجیِ آزمون به‌صورتِ پیامِ user به مدل برمی‌گردد و حلقه ادامه پیدا می‌کند.

امتحان کنید

یک هوکِ پیش از اجرای سوم به mini اضافه کنید که جلوی هر write_file را بگیرد که مسیرش به .py ختم می‌شود و در محتوایش TODO هست. (بدنه‌اش سه خط است.) بعد از mini بخواهید «write a Python file with a TODO in it» و نتیجه‌ی Blocked را که می‌گیرد بخوانید. کلید ندارید؟ هوک را به هر حال بنویسید و خودتان صدایش بزنید: print(no_todo("write_file", {"path": "a.py", "content": "# TODO"})).

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

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

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

  • هوکِ پیش از اجرا (before-hook) - تابعی که هارنس پیش از هر فراخوانیِ ابزار اجرا می‌کند؛ می‌تواند با یک دلیل جلوی فراخوانی را بگیرد
  • هوکِ پس از اجرا (after-hook) - تابعی که هارنس پس از هر فراخوانیِ موفقِ ابزار اجرا می‌کند؛ خروجی را می‌گیرد و خروجی‌ای را که باید به کار برود برمی‌گرداند
  • بررسیِ پیش از تمام کردن (stop check) - کدی که وقتی عامل می‌خواهد کار را تمام کند اجرا می‌شود و می‌تواند آن را دوباره سرِ کار بفرستد، مثلاً وقتی آزمون‌ها رد می‌شوند

ایده‌ی اصلی

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

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

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