Knaph

آزمودن و تحویل33 از 377 دقیقه مطالعه

آزمودنِ یک عامل

لیلا یک جمله از پرامپت سیستمیِ 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)
Python

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."
Python

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

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

eval با مدلِ واقعی

eval (کوتاه‌شده‌ی evaluation، یعنی ارزیابی) مجموعه‌ای از کارهاست که هر کدام یک پوشه‌ی آغازین و یک بررسیِ خودکار دارد و با مدلِ واقعی اجرا می‌شود. برای mini، بررسی فقط فرمانی است که یا موفق می‌شود یا شکست می‌خورد:

کارپوشه‌ی آغازینبررسی
«fix the failing test»پروژه‌ای با یک تابعِ خرابpython -m pytest با کدِ خروجِ 0 تمام شود
«add a README with the run command»پروژه‌ای بی READMEgrep -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
Python

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

عددها را بخوانید، نه یک اجرا را

هر کار را چند بار اجرا کنید، پنج بار شروعِ خوبی است، و به سه عدد نگاه کنید:

  • نرخِ قبولی (pass rate): چند اجرا قبول شد. ۵ از ۵ محکم است. ۰ از ۵ یعنی چیزی کم است (یک ابزار، یک اجازه، یک دستور). ۳ از ۵ یعنی ناپایدار (flaky).
  • هزینه برای هر قبولی: کلِ هزینه‌ی یک کار تقسیم بر تعدادِ قبولی‌هایش. کارِ ارزانی که در نیمی از اجراها شکست می‌خورد، ممکن است برای هر کارِ تمام‌شده گران‌تر از کارِ پرهزینه‌ای تمام شود که همیشه درست درمی‌آید.
  • تغییر میانِ نسخه‌ها: همان eval را پیش و پس از تغییرِ پرامپت، یک ابزار یا مدل اجرا کنید. لیلا از همین راه می‌فهمد جمله‌ی تازه‌اش کمک کرده یا نه، و آیا چیزِ دیگری را خراب کرده است.

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

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

امتحان کنید

آزمونِ مدلِ ساختگیِ بالا را کنارِ mini با نامِ test_mini.py ذخیره کنید و با pytest اجرایش کنید (یا خودتان تابع را صدا بزنید). بعد mini را عمداً خراب کنید: در check_permission خطی را که inside را بررسی می‌کند پاک کنید و آزمون را دوباره اجرا کنید. آزمونی که وقتی چیزِ مورد آزمون را خراب می‌کنید شکست بخورد، آزمونی است که می‌شود به آن اعتماد کرد. تمرینِ بعد از این درس گزارشی را می‌نویسد که یک eval چاپ می‌کند.

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

  1. هارنس را بدونِ مدل بیازمایید. یک کلاینتِ ساختگی که پاسخ‌های از پیش نوشته را پخش می‌کند، کدِ خودِ mini را آزمودنی می‌کند: رایگان، سریع و هر بار یکسان.
  2. نتیجه را با کد بررسی کنید، نه با خلاصه‌ی عامل. بررسیِ یک eval یک فرمان یا یک مقایسه است، هرگز «عامل گفت کار کرد».
  3. هر کارِ eval را چند بار اجرا کنید. یک اجرا می‌تواند شانس باشد. نرخِ قبولی اندازه‌گیری است. کارهای ناپایدار اولین جایی‌اند که اثرِ یک تغییر در آن‌ها دیده می‌شود.
  4. eval را نگه دارید و بعد از هر تغییر اجرایش کنید. پرامپت، توضیحِ یک ابزار، مدل، میزانِ تلاش: هر کدام می‌تواند به یک کار کمک کند و به کارِ دیگری آسیب بزند.

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

  • مدلِ ساختگی (fake model): جانشینِ مدل در آزمون‌ها، که پاسخ‌های از پیش نوشته برمی‌گرداند تا هارنس بی‌کلید و هر بار به یک شکل آزموده شود
  • eval (ارزیابی): مجموعه‌ای از کارها با وضعیتِ آغازین و بررسیِ خودکار، که با مدلِ واقعی اجرا می‌شود تا اندازه بگیرد عامل چقدر خوب کار می‌کند
  • نرخِ قبولی (pass rate): سهمِ اجراهای یک کار در eval که بررسی‌شان قبول شد
  • ناپایدار (flaky): کاری که در بعضی اجراها قبول می‌شود و در بعضی رد

ایده‌ی اصلی

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

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

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