آزمودنِ یک عامل
لیلا یک جمله از پرامپت سیستمیِ mini را عوض میکند تا جلوی بازنویسیِ کاملِ فایلها را بگیرد، وقتی یک تغییرِ کوچک کافی است. یک کار را امتحان میکند و بهتر جواب میدهد. حالا mini بهتر شده است؟ یا لیلا در یک اجرا شانس آورده و چیزی را که امتحان نکرده خراب کرده است؟
عوض کردنِ یک عامل بدونِ راهی برای اندازه گرفتنش، حدس زدن است. این درس دربارهی دو جور آزمونی است که این حدس را به یک عدد تبدیل میکنند.
لیلا مثلِ هر برنامهی دیگری آزمایشش میکنم: روی چند کار اجرایش میکنم و میبینم درست کار میکند یا نه.
امید کدام بخشِ mini را میخواهی بیازمایی؟
لیلا همهاش را... ولی منظورت را میفهمم. یک بخش کدِ خودِ mini است: حلقه، ابزارها، بررسیِ اجازه. یک بخش هم کاری است که مدل تصمیم میگیرد بکند.
امید این دو یک جور آزمون میخواهند؟
لیلا کد باید هر بار دقیقاً همان کار را بکند. اگر امروز مسیری بیرون از پوشه رد میشود، فردا هم باید رد شود. مدل فرق دارد: میتواند یک کار را به دو شکلِ مختلف انجام دهد.
امید پس برای کد، چه چیزی آزمون را تکرارپذیر میکند؟
لیلا مدل را بیرون بیاوریم. به mini یک مدلِ قلابی بدهیم که همیشه همان حرفها را بزند. همهی تمرینها همین کار را کردند، نه؟ یک مدلِ ساختگی که پاسخهایش را از ورودی میخواند.
امید دقیقاً. و برای بخشِ مدل؟
لیلا مدلِ واقعی را به کار ببریم، ولی هر کار را چند بار اجرا کنیم و بشماریم چند بار درست شد. یک اجرا تقریباً هیچ چیز به من نمیگوید.
امید و چه کسی تصمیم میگیرد که درست شد یا نه؟
لیلا نه خلاصهای که خودِ عامل مینویسد. این یکی را قبلاً یاد گرفتهایم: چیزی که نتیجه را بررسی کند. آزمونهای پوشه، یا اینکه فایل آنجا هست یا نه.
آزمونِ واحد با مدلِ ساختگی
mini فقط از راهِ یک شیء، client، با مدل حرف میزند و فقط client.messages.create را صدا میزند. این شیء را با یک شیءِ ساختگی عوض کنید که پاسخهای از پیش نوشته برمیگرداند، تا بتوانید تکتکِ خطهای کدِ خودِ mini را بیکلید، رایگان، در کسری از ثانیه و هر بار با همان نتیجه بیازمایید.
from types import SimpleNamespace as NS
import mini
def reply(*blocks, stop="tool_use"):
usage = NS(input_tokens=1000, output_tokens=50, cache_read_input_tokens=0, cache_creation_input_tokens=0)
return NS(content=list(blocks), stop_reason=stop, usage=usage)
class FakeClient:
def __init__(self, replies):
self.messages = self
self.replies, self.requests = list(replies), []
def create(self, **request):
self.requests.append({**request, "messages": list(request["messages"])})
return self.replies.pop(0)SimpleNamespace شیءهای کوچکی میسازد با هر ویژگیای که به آن بدهید، و mini هم بیشتر از این لازم ندارد: پاسخی با content و stop_reason و usage، و بلوکهایی با type و id و name و input. مدلِ ساختگی از هر درخواستی که برایش فرستاده شده یک رونوشت نگه میدارد (رونوشتی از فهرستِ پیامها، چون mini مدام به آن اضافه میکند)، تا آزمون بتواند دقیقاً ببیند mini چه چیزی برای مدلِ واقعی میفرستاد.
حالا هر آزمون مثلِ یک داستانِ کوتاه خوانده میشود:
def test_a_path_outside_the_folder_is_refused():
mini.client = FakeClient([
reply(NS(type="tool_use", id="t1", name="read_file", input={"path": "../secrets.txt"})),
reply(NS(type="text", text="I can't read that."), stop="end_turn"),
])
answer = mini.run_agent("read ../secrets.txt", ["read_file"], mini.SYSTEM)
result = mini.client.requests[1]["messages"][-1]["content"][0]
assert result["is_error"] and "outside the project folder" in result["content"]
assert answer == "I can't read that."پاسخِ از پیش نوشتهی اول میخواهد فایلی بیرون از پروژه را بخواند. پاسخِ دوم پاسخِ نهاییِ مدل است. بعد آزمون درخواستِ دوم را بررسی میکند: نتیجهی ابزاری که mini پس فرستاده باید خطا باشد، همراهِ دلیلش. آزمونهایی از همین دست سقفِ نوبتها، ایستادن با max_tokens، هوکها، فشردهسازی و فهرستِ ابزارهای زیرعامل را میآزمایند، بیآنکه یک سنت خرج شود.
eval با مدلِ واقعی
eval (کوتاهشدهی evaluation، یعنی ارزیابی) مجموعهای از کارهاست که هر کدام یک پوشهی آغازین و یک بررسیِ خودکار دارد و با مدلِ واقعی اجرا میشود. برای mini، بررسی فقط فرمانی است که یا موفق میشود یا شکست میخورد:
| کار | پوشهی آغازین | بررسی |
|---|---|---|
| «fix the failing test» | پروژهای با یک تابعِ خراب | python -m pytest با کدِ خروجِ 0 تمام شود |
| «add a README with the run command» | پروژهای بی README | grep -q "python main.py" README.md |
| «rename every .jpeg to .jpg» | پوشهای از عکسها | test -z "$(ls *.jpeg 2>/dev/null)" |
اجرای یک eval خودش یک حلقه است:
import shutil, subprocess, tempfile
from pathlib import Path
mini.check_permission = lambda name, args: None # say yes to everything: only in a throwaway copy
def run_once(task, fixture, check):
folder = Path(tempfile.mkdtemp())
shutil.copytree(fixture, folder, dirs_exist_ok=True)
mini.ROOT = folder
mini.run_agent(task, list(mini.TOOLS), mini.SYSTEM)
return subprocess.run(check, shell=True, cwd=folder).returncode == 0هر اجرا رونوشتِ تازهای از پوشهی آغازین میگیرد، تا تغییرهای یک اجرا به اجرای بعد کمک نکنند یا خرابش نکنند. پرسیدنِ اجازه خاموش شده است، چون کسی آنجا نیست که جواب بدهد. این کار فقط به این دلیل پذیرفتنی است که رونوشت دور ریخته میشود، و حتی آن وقت هم یک فرمانِ پوسته هنوز میتواند به بقیهی کامپیوتر برسد. evalهای جدی هر رونوشت را درونِ یک کانتینر اجرا میکنند.
عددها را بخوانید، نه یک اجرا را
هر کار را چند بار اجرا کنید، پنج بار شروعِ خوبی است، و به سه عدد نگاه کنید:
- نرخِ قبولی (pass rate): چند اجرا قبول شد. ۵ از ۵ محکم است. ۰ از ۵ یعنی چیزی کم است (یک ابزار، یک اجازه، یک دستور). ۳ از ۵ یعنی ناپایدار (flaky).
- هزینه برای هر قبولی: کلِ هزینهی یک کار تقسیم بر تعدادِ قبولیهایش. کارِ ارزانی که در نیمی از اجراها شکست میخورد، ممکن است برای هر کارِ تمامشده گرانتر از کارِ پرهزینهای تمام شود که همیشه درست درمیآید.
- تغییر میانِ نسخهها: همان eval را پیش و پس از تغییرِ پرامپت، یک ابزار یا مدل اجرا کنید. لیلا از همین راه میفهمد جملهی تازهاش کمک کرده یا نه، و آیا چیزِ دیگری را خراب کرده است.
بررسیها هر جا که ممکن است باید خودکار و دقیق باشند: آزمونها، محتوای فایلها، خروجیِ یک فرمان. گاهی نتیجه را نمیشود با برنامه بررسی کرد («این خلاصه درست است؟»). آن وقت از یک فراخوانیِ دیگرِ مدل، با معیارهای نوشتهشده، برای نمره دادن استفاده میکنند. این کار جواب میدهد، ولی نمرهی یک مدل را شاهدی بیشتر بدانید، نه اثبات، و چندتایی را خودتان بررسی کنید.
امتحان کنید
آزمونِ مدلِ ساختگیِ بالا را کنارِ mini با نامِ
test_mini.pyذخیره کنید و باpytestاجرایش کنید (یا خودتان تابع را صدا بزنید). بعد mini را عمداً خراب کنید: درcheck_permissionخطی را کهinsideرا بررسی میکند پاک کنید و آزمون را دوباره اجرا کنید. آزمونی که وقتی چیزِ مورد آزمون را خراب میکنید شکست بخورد، آزمونی است که میشود به آن اعتماد کرد. تمرینِ بعد از این درس گزارشی را مینویسد که یک eval چاپ میکند.
درست به کار بردن
- هارنس را بدونِ مدل بیازمایید. یک کلاینتِ ساختگی که پاسخهای از پیش نوشته را پخش میکند، کدِ خودِ mini را آزمودنی میکند: رایگان، سریع و هر بار یکسان.
- نتیجه را با کد بررسی کنید، نه با خلاصهی عامل. بررسیِ یک eval یک فرمان یا یک مقایسه است، هرگز «عامل گفت کار کرد».
- هر کارِ eval را چند بار اجرا کنید. یک اجرا میتواند شانس باشد. نرخِ قبولی اندازهگیری است. کارهای ناپایدار اولین جاییاند که اثرِ یک تغییر در آنها دیده میشود.
- eval را نگه دارید و بعد از هر تغییر اجرایش کنید. پرامپت، توضیحِ یک ابزار، مدل، میزانِ تلاش: هر کدام میتواند به یک کار کمک کند و به کارِ دیگری آسیب بزند.
واژههای این درس
- مدلِ ساختگی (fake model): جانشینِ مدل در آزمونها، که پاسخهای از پیش نوشته برمیگرداند تا هارنس بیکلید و هر بار به یک شکل آزموده شود
- eval (ارزیابی): مجموعهای از کارها با وضعیتِ آغازین و بررسیِ خودکار، که با مدلِ واقعی اجرا میشود تا اندازه بگیرد عامل چقدر خوب کار میکند
- نرخِ قبولی (pass rate): سهمِ اجراهای یک کار در eval که بررسیشان قبول شد
- ناپایدار (flaky): کاری که در بعضی اجراها قبول میشود و در بعضی رد
ایدهی اصلی
در یک عامل دو چیز را باید آزمود: کدی که باید هر بار یکسان رفتار کند، که با مدلِ ساختگی آزموده میشود، و مدلی که هر بار فرق میکند، که با eval اندازه گرفته میشود. نتیجه را با کد بررسی کنید، هر کار را چند بار اجرا کنید، و آن وقت میتوانید عامل را عوض کنید و بدانید بهتر شده است یا نه.