شیدهوش
فصل ۴ · درس ۱۳ از ۱۳

۱۳. پروژه (۲): خط لوله و داشبورد مقایسهٔ مدل‌ها

دیتاست و بررسی‌ها آماده‌اند. حالا خط لوله‌ای می‌سازیم که چند مدل را روی دیتاست طلایی می‌سنجد، با شرط‌های سخت فیلترشان می‌کند، یکی را انتخاب می‌کند، داشبوردی برای دیدن جزئیات می‌سازد و هر اجرا را با همهٔ تنظیماتش ثبت می‌کند.

زمان مطالعه: ۵۱ دقیقهمرجع در کتاب: پروژهٔ طراحی‌شده برای این دوره، بر پایهٔ درس‌های ۴، ۵، ۶ و ۱۱ همین فصل

چه می‌سازیم؟

فایلتازه یا تغییر؟کارش
zehnyar/pipeline.pyتازهاجرای یک مدل روی دیتاست، جمع‌بندی، شرط‌های سخت، دفترچهٔ آزمایش
zehnyar/dashboard.pyتازهساختن داشبورد HTML
projects/ch04_pipeline/run_compare.pyتازهمقایسهٔ مدل‌ها و انتخاب — اسکریپت اجرایی این درس
reports/dashboard.html و runs/experiments.jsonlخودکار ساخته می‌شوندخروجی‌های خط لوله؛ لازم نیست خودتان بسازیدشان

سه «مدل» داریم که همه را از فصل‌های قبل می‌شناسید: mock (دانش کلیدواژه‌ای فصل ۱)، local (مدل زبانی خام فصل ۲) و ollama (یک مدل واقعی روی کامپیوتر خودتان، اختیاری). همه پشت همان لایهٔ provider فصل ۱‌اند — برای همین خط لوله برای هر سه یک کد دارد.

قدم ۱ — خط لوله: pipeline.py

قلب این درس. چهار تابع اصلی دارد: اجرای یک مدل روی کل دیتاست، جمع‌بندی نتیجه‌ها، مقایسه با شرط‌های سخت، و ثبت در دفترچه. در پوشهٔ zehnyar بسازیدش:

zehnyar/pipeline.py
"""خط لولهٔ ارزیابی ذهن‌یار — فصل ۴.

یک مدل را روی کل دیتاست طلایی اجرا می‌کند، هر پاسخ را با بررسی‌های
checks.py می‌سنجد، نتیجه‌ها را برش‌به‌برش جمع می‌زند، با شرط‌های سخت
مقایسه می‌کند، و هر اجرا را با همهٔ تنظیماتش در دفترچهٔ آزمایش‌ها
ثبت می‌کند تا بعداً قابل‌مقایسه و قابل‌تکرار باشد.
"""
import json
import pathlib
from datetime import datetime

from . import checks, config
from .core.assistant import Zehnyar
from .evalset import SLICES, bootstrap, fingerprint

ROOT = pathlib.Path(__file__).resolve().parent.parent
LOG = ROOT / "runs" / "experiments.jsonl"
SEED = 0                     # بذر ثابت: مدل‌های احتمالی هر بار همان پاسخ را بدهند

LABELS = {"accuracy": "دقت", "hallucination": "توهم", "out_of_scope": "خارج از دامنه",
          "instructions": "پیروی از دستور", "p90_ms": "تأخیر p90"}


def run_model(provider_name: str, items: list[dict]) -> dict | None:
    """همهٔ سؤال‌ها را از یک مدل می‌پرسد؛ None یعنی این مدل در دسترس نیست."""
    bot = Zehnyar(provider_name)
    if bot.provider.name != provider_name:
        return None              # get_provider به mock برگشته؛ این دیگر همان مدل نیست
    bot.ask("سلام", seed=SEED)   # گرم کردن: اولین فراخوانی کندتر است و نباید حساب شود
    rows = []
    for it in items:
        r = bot.ask(it["question"], seed=SEED)
        rows.append({
            "id": it["id"], "slice": it["slice"], "question": it["question"],
            "answer": r.text,
            "outcome": checks.outcome(it, r.text),
            "instructions_ok": all(checks.instruction_checks(r.text).values()),
            "latency_ms": r.latency_ms,
            "out_tokens": r.completion_tokens,
            "cost_usd": r.cost_usd,
        })
    return {"provider": bot.provider.name, "model": bot.provider.model,
            "rows": rows, "summary": summarize(rows)}


def rate(rows: list[dict], *good: str) -> float:
    """چه کسری از این ردیف‌ها یکی از نتیجه‌های good را گرفته‌اند؟"""
    return sum(r["outcome"] in good for r in rows) / len(rows) if rows else 0.0


def summarize(rows: list[dict]) -> dict:
    inside = [r for r in rows if r["slice"] != checks.OUT_OF_SCOPE]
    outside = [r for r in rows if r["slice"] == checks.OUT_OF_SCOPE]
    latencies = sorted(r["latency_ms"] for r in rows)
    return {
        "accuracy": rate(inside, "درست"),
        "accuracy_ci": bootstrap([rate([r], "درست") for r in inside]),
        "hallucination": rate(inside, "توهم"),
        "out_of_scope": rate(outside, "رد درست"),
        "instructions": sum(r["instructions_ok"] for r in rows) / len(rows),
        "slices": {s: rate([r for r in rows if r["slice"] == s], "درست", "رد درست")
                   for s in SLICES},
        "p50_ms": latencies[len(latencies) // 2],
        "p90_ms": latencies[round(0.9 * (len(latencies) - 1))],
        "out_tokens": sum(r["out_tokens"] for r in rows) / len(rows),
        "cost_usd": sum(r["cost_usd"] for r in rows),
    }


def failed_criteria(summary: dict, criteria: dict) -> list[str]:
    """شرط‌های سختی که این مدل رد شده (فهرست خالی یعنی همه را دارد)."""
    failed = []
    for metric, (op, limit) in criteria.items():
        value = summary[metric]
        ok = value >= limit if op == ">=" else value <= limit
        if not ok:
            failed.append(metric)
    return failed


def log_experiment(result: dict, items: list[dict]) -> None:
    """هر چیزی که اگر عوض شود نتیجه عوض می‌شود، کنار نتیجه ثبت می‌شود."""
    entry = {
        "time": datetime.now().isoformat(timespec="seconds"),
        "dataset": fingerprint(items),
        "provider": result["provider"],
        "model": result["model"],
        "sampling": {**config.SAMPLING, "seed": SEED},
        "system_prompt": config.SYSTEM_PROMPT,
        "checks": {"max_words": checks.MAX_WORDS, "min_persian": checks.MIN_PERSIAN},
        "summary": result["summary"],
    }
    LOG.parent.mkdir(exist_ok=True)
    with LOG.open("a", encoding="utf-8") as f:
        f.write(json.dumps(entry, ensure_ascii=False) + "\n")
این تکه از کدیعنی چه
SEED = 0بذر ثابت برای همهٔ مدل‌ها؛ در دفترچه هم ثبت می‌شود
LABELSاسم فارسی هر سنجه، برای چاپ و داشبورد
bot = Zehnyar(provider_name)ذهن‌یار را با ارائه‌دهندهٔ خواسته‌شده بساز — همان کلاس فصل ۱
if bot.provider.name != provider_name: return Noneیادتان هست وقتی Ollama در دسترس نباشد، کارخانهٔ فصل ۱ بی‌سروصدا به mock برمی‌گردد؟ این‌جا آن «تنزل آرام» خطرناک است: نتیجهٔ mock به اسم ollama ثبت می‌شد! پس اگر مدل همان نبود، کنارش می‌گذاریم
bot.ask("سلام", seed=SEED)گرم کردن: اولین فراخوانی هر مدل کندتر است (بار شدن توکنایزر یا مدل). این یکی را حساب نمی‌کنیم تا تأخیر واقعی را بسنجیم
rows.append({...})برای هر سؤال یک ردیف: پاسخ، نتیجه، رعایت دستورها، تأخیر، توکن و هزینه — همان سنجه‌هایی که از فصل ۱ در Completion جمع می‌کردیم «تا در فصل ۴ به کارمان بیایند». حالا آمدند
all(checks.instruction_checks(r.text).values())پاسخ فقط وقتی «دستورها را رعایت کرده» که هر چهار بررسی بله باشد
def rate(rows, *good)کسر ردیف‌هایی که نتیجه‌شان یکی از good است. ستاره یعنی «هر تعداد آرگومان»: rate(rows, "درست", "رد درست")
sum(r["outcome"] in good for r in rows)in برای هر ردیف True یا False می‌دهد؛ sum آن‌ها را مثل ۱ و ۰ جمع می‌زند
"accuracy_ci": bootstrap(...)بازهٔ بوت‌استرپ دقت — همان تابع درس قبل
"slices": {s: ... for s in SLICES}برش‌به‌برش: برای هر برش، کسر پاسخ‌های قابل‌قبول (برای خارج از دامنه یعنی «رد درست»)
latencies[round(0.9 * (len(latencies) - 1))]صدک ۹۰ تأخیر: تأخیرها را مرتب کن و عضوی را بردار که ۹۰٪ بقیه از آن سریع‌ترند (درس ۵)
def failed_criteria(summary, criteria)هر شرط یک جهت دارد (>= یا <=) و یک حد. این تابع فهرست شرط‌هایی را می‌دهد که مدل رد شده
def log_experiment(...)دفترچهٔ آزمایش‌ها (درس ۱۱): هر چیزی که اگر عوض شود نتیجه عوض می‌شود — اثر انگشت دیتاست، مدل، تنظیمات نمونه‌برداری، پرامپت سیستمی، تنظیمات بررسی‌ها — کنار نتیجه
LOG.open("a", ...)حالت a یعنی اضافه کن (append): هر اجرا یک خط به آخر فایل اضافه می‌کند و تاریخچه پاک نمی‌شود
json.dumps(entry, ...) + "\n"هر آزمایش یک خط JSON. به این قالب JSON Lines می‌گویند — پسوندش .jsonl

قدم ۲ — داشبورد: dashboard.py

خروجی خط لوله فقط چند عدد در ترمینال نیست؛ یک صفحهٔ وب می‌سازیم که شرط‌ها را رنگی، برش‌ها را میله‌ای، و پاسخ تک‌تک مدل‌ها به تک‌تک سؤال‌ها را کنار هم نشان می‌دهد. هیچ کتابخانه‌ای لازم نیست: صفحهٔ وب فقط یک متن بزرگ است. در پوشهٔ zehnyar بسازیدش:

zehnyar/dashboard.py
"""داشبورد مقایسهٔ مدل‌ها — یک فایل HTML که با مرورگر باز می‌شود (فصل ۴).

هیچ کتابخانه‌ای لازم ندارد: صفحه را مثل یک رشتهٔ متنی بزرگ می‌سازیم.
html.escape نمی‌گذارد متن پاسخ‌ها (مثلاً یک < یا >) صفحه را به هم بریزد.
"""
import html

from .evalset import SLICES
from .pipeline import LABELS

STYLE = """
body { font-family: Tahoma, sans-serif; direction: rtl; background: #f6f7f9; color: #1d2330;
       max-width: 1100px; margin: 24px auto; padding: 0 16px; line-height: 1.7; }
table { border-collapse: collapse; width: 100%; background: #fff; margin: 12px 0 28px; }
th, td { border: 1px solid #dde1e8; padding: 8px 10px; text-align: right; vertical-align: top; }
small { color: #6b7385; }
.ok { background: #e5f5ec; } .bad { background: #fbe9e9; }
.win { background: #1d4ed8; color: #fff; border-radius: 6px; padding: 0 8px; font-size: .8em; }
.bar { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
.bar span { width: 70px; } .bar b { width: 48px; }
.bar i { display: block; height: 14px; background: #1d4ed8; border-radius: 4px; }
.tag { font-size: .8em; padding: 0 6px; border-radius: 6px; color: #fff; }
"""
COLORS = {"درست": "#15803d", "رد درست": "#15803d", "رد صادقانه": "#b45309",
          "توهم": "#b91c1c", "پاسخ بی‌جا": "#b91c1c"}


def fmt(metric: str, value: float) -> str:
    return f"{value / 1000:.2f} ثانیه" if metric.endswith("_ms") else f"{value:.0%}"


def write_dashboard(path, results: list[dict], criteria: dict, winner: dict | None,
                    dataset: str) -> None:
    e = html.escape

    # ۱. جدول شرط‌های سخت: سبز یعنی شرط برقرار است، قرمز یعنی نه
    head = "".join(f"<th>{LABELS[m]}<br><small>{'دست‌کم' if op == '>=' else 'حداکثر'} "
                   f"{fmt(m, lim)}</small></th>" for m, (op, lim) in criteria.items())
    table = ""
    for r in results:
        badge = " <span class='win'>انتخاب</span>" if r is winner else ""
        cells = "".join(f"<td class='{'bad' if m in r['failed'] else 'ok'}'>"
                        f"{fmt(m, r['summary'][m])}</td>" for m in criteria)
        table += f"<tr><th>{e(r['provider'])} / {e(r['model'])}{badge}</th>{cells}</tr>"

    # ۲. نمودار میله‌ای هر برش
    bars = ""
    for s in SLICES:
        bars += f"<h3>{e(s)}</h3>"
        for r in results:
            v = r["summary"]["slices"][s]
            bars += (f"<div class='bar'><span>{e(r['provider'])}</span>"
                     f"<i style='width:{v * 400:.0f}px'></i><b>{v:.0%}</b></div>")

    # ۳. تک‌تک سؤال‌ها، کنار هم
    detail = ""
    for i, row in enumerate(results[0]["rows"]):
        cells = ""
        for r in results:
            x = r["rows"][i]
            cells += (f"<td><span class='tag' style='background:{COLORS[x['outcome']]}'>"
                      f"{x['outcome']}</span> {e(x['answer'][:160])}</td>")
        detail += f"<tr><td>{row['id']}</td><td>{e(row['question'])}<br><small>{e(row['slice'])}</small></td>{cells}</tr>"
    models = "".join(f"<th>{e(r['provider'])}</th>" for r in results)

    choice = f"انتخاب: {e(winner['provider'])} / {e(winner['model'])}" if winner \
        else "هیچ مدلی همهٔ شرط‌های سخت را ندارد"
    page = (f"<!doctype html><html lang='fa'><head><meta charset='utf-8'>"
            f"<title>داشبورد ارزیابی ذهن‌یار</title><style>{STYLE}</style></head><body>"
            f"<h1>داشبورد ارزیابی ذهن‌یار</h1>"
            f"<p>دیتاست طلایی: <code>{dataset}</code> · {len(results[0]['rows'])} سؤال · <b>{choice}</b></p>"
            f"<h2>شرط‌های سخت</h2><table><tr><th>مدل</th>{head}</tr>{table}</table>"
            f"<h2>برش‌به‌برش</h2>{bars}"
            f"<h2>تک‌تک سؤال‌ها</h2><table><tr><th></th><th>سؤال</th>{models}</tr>{detail}</table>"
            f"</body></html>")
    path.parent.mkdir(exist_ok=True)
    path.write_text(page, encoding="utf-8")
این تکه از کدیعنی چه
STYLE = """..."""ظاهر صفحه به زبان CSS: فونت، جهت راست‌به‌چپ (direction: rtl)، رنگ خانه‌های سبز (.ok) و قرمز (.bad) و میله‌ها
COLORSرنگ برچسب هر نتیجه: سبز برای درست، نارنجی برای رد صادقانه، قرمز برای توهم و پاسخ بی‌جا
def fmt(metric, value)اگر اسم سنجه با _ms تمام شود، میلی‌ثانیه را بر ۱۰۰۰ تقسیم کن و به ثانیه با دو رقم اعشار بنویس؛ وگرنه به شکل درصد
'دست‌کم' if op == '>=' else 'حداکثر'در سرستون‌ها جهت شرط را با واژه می‌نویسیم، نه با >=: در صفحهٔ راست‌به‌چپ، مرورگر علامت‌های > و < را آینه‌ای نشان می‌دهد و «بزرگ‌تر» شبیه «کوچک‌تر» دیده می‌شود
e = html.escapeاسم کوتاه برای تابعی که < و > و & را بی‌خطر می‌کند. اگر پاسخ مدلی <b> داشت، صفحه را به هم نمی‌ریزد
r is winneris یعنی «دقیقاً همان شیء». برچسب «انتخاب» فقط کنار مدل برنده می‌آید
'bad' if m in r['failed'] else 'ok'اگر مدل این شرط را رد شده، خانه قرمز؛ وگرنه سبز
style='width:{v * 400:.0f}px'پهنای هر میله متناسب با نمره: ۱۰۰٪ = ۴۰۰ پیکسل
results[0]["rows"] و r["rows"][i]همهٔ مدل‌ها سؤال‌ها را به یک ترتیب جواب داده‌اند، پس ردیف i هر مدل مال همان سؤال است
x['answer'][:160]فقط ۱۶۰ نویسهٔ اول هر پاسخ، تا جدول خوانا بماند
page = (f"..." f"...")چند رشتهٔ f پشت هم داخل پرانتز، پایتون خودش به هم می‌چسباندشان — یک صفحهٔ کامل HTML
path.write_text(page, encoding="utf-8")صفحه را در فایل بنویس. utf-8 برای اینکه فارسی درست ذخیره شود

قدم ۳ — مقایسه و انتخاب: run_compare.py

حالا اسکریپتی که همه را به هم وصل می‌کند. شرط‌های سخت ذهن‌یار — مثل جدول ۴-۳ کتاب در درس ۵ — بالای همین فایل آمده‌اند، چون تصمیم این آزمایش‌اند. در پوشهٔ projects/ch04_pipeline بسازیدش:

projects/ch04_pipeline/run_compare.py
"""پروژهٔ فصل ۴ (بخش ۲) — خط لوله و داشبورد مقایسهٔ مدل‌ها.

اجرا:
    .venv\\Scripts\\python.exe -m projects.ch04_pipeline.run_compare
"""
import pathlib

from zehnyar import _console
from zehnyar.dashboard import write_dashboard
from zehnyar.evalset import SLICES, fingerprint, load_golden
from zehnyar.pipeline import LABELS, failed_criteria, log_experiment, run_model

_console.setup()   # تا کنسول ویندوز فارسی را درست چاپ کند

ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
GOLDEN = ROOT / "evals" / "golden.json"
REPORT = ROOT / "reports" / "dashboard.html"

CANDIDATES = ["mock", "local", "ollama"]

# شرط‌های سخت، مثل جدول ۴-۳ کتاب: مدلی که حتی یکی را نداشته باشد کنار می‌رود
CRITERIA = {
    "accuracy":      (">=", 0.40),   # دست‌کم ۴۰٪ سؤال‌های داخل دامنه درست
    "hallucination": ("<=", 0.20),   # حداکثر ۲۰٪ پاسخ غلطِ مطمئن
    "out_of_scope":  (">=", 0.50),   # دست‌کم نیمی از سؤال‌های بی‌ربط را رد کند
    "instructions":  (">=", 0.80),   # دست‌کم ۸۰٪ پاسخ‌ها فارسی، کوتاه و بی‌عبارت ممنوع
    "p90_ms":        ("<=", 5000),   # ۹۰٪ پاسخ‌ها زیر ۵ ثانیه
}


def main() -> None:
    items = load_golden(GOLDEN)
    print("=" * 56)
    print(f" مقایسهٔ مدل‌ها  |  {len(items)} سؤال  |  دیتاست {fingerprint(items)}")
    print("=" * 56)

    results = []
    for name in CANDIDATES:
        r = run_model(name, items)
        if r is None:
            print(f"\n▶ {name}: در دسترس نیست — از مقایسه کنار گذاشته شد")
            continue
        s = r["summary"]
        r["failed"] = failed_criteria(s, CRITERIA)
        results.append(r)
        log_experiment(r, items)

        low, high = s["accuracy_ci"]
        print(f"\n▶ {r['provider']} / {r['model']}")
        print(f"  دقت {s['accuracy']:.0%} (بازهٔ ۹۰٪: {low:.0%} تا {high:.0%})  |  "
              f"توهم {s['hallucination']:.0%}  |  خارج از دامنه {s['out_of_scope']:.0%}  |  "
              f"دستورها {s['instructions']:.0%}")
        print("  برش‌ها: " + " · ".join(f"{sl} {s['slices'][sl]:.0%}" for sl in SLICES))
        print("  شرط‌های سخت: " + ("همه ✓" if not r["failed"] else
              "رد شد در " + "، ".join(LABELS[m] for m in r["failed"])))
        print(f"  تأخیر p90: {s['p90_ms']:.1f} ms")

    passed = [r for r in results if not r["failed"]]
    winner = max(passed, key=lambda r: r["summary"]["accuracy"]) if passed else None
    print("\n" + "-" * 56)
    if winner:
        print(f"انتخاب: {winner['provider']} — بهترین دقت میان مدل‌هایی که همهٔ شرط‌ها را دارند")
    else:
        print("انتخاب: هیچ مدلی همهٔ شرط‌های سخت را ندارد")

    write_dashboard(REPORT, results, CRITERIA, winner, fingerprint(items))
    print("داشبورد: reports/dashboard.html")
    print("دفترچهٔ آزمایش‌ها: runs/experiments.jsonl")


if __name__ == "__main__":
    main()
این تکه از کدیعنی چه
CANDIDATES = ["mock", "local", "ollama"]مدل‌هایی که مقایسه می‌شوند — به ترتیب
CRITERIA = {...}هر شرط: اسم سنجه ← (جهت، حد). مثلاً ("<=", 0.20) برای توهم یعنی «حداکثر ۲۰٪». این عددها انتخاب ماست و باید از نیاز محصول بیایند — آستانهٔ سودمندی فصل ۱
r = run_model(name, items)مدل را روی کل دیتاست اجرا کن؛ اگر در دسترس نبود None برمی‌گردد و با continue می‌رویم سراغ بعدی
r["failed"] = failed_criteria(s, CRITERIA)شرط‌های ردشدهٔ این مدل را کنار نتیجه‌اش نگه دار — داشبورد هم از همین استفاده می‌کند
log_experiment(r, items)هر مدل، یک خط در دفترچه
low, high = s["accuracy_ci"]بوت‌استرپ دو عدد برمی‌گرداند؛ این‌جا هر دو را با هم در دو متغیر می‌ریزیم
passed = [r for r in results if not r["failed"]]قدم فیلتر گردش کار درس ۶: فقط مدل‌هایی که هیچ شرطی را رد نشده‌اند
max(passed, key=lambda r: r["summary"]["accuracy"])از میان باقی‌مانده‌ها، باکیفیت‌ترین. اگر هیچ‌کس نماند، None

دستور اجرا:

PowerShell
.venv\Scripts\python.exe -m projects.ch04_pipeline.run_compare

اگر Ollama ندارید، این را می‌بینید — ollama کنار گذاشته می‌شود و دو مدل دیگر مقایسه می‌شوند. این خروجی واقعی همین کد است؛ فقط عدد تأخیر روی کامپیوتر شما کمی فرق می‌کند:

خروجی مورد انتظار
========================================================
 مقایسهٔ مدل‌ها  |  20 سؤال  |  دیتاست 3fc6bde6fdcf
========================================================

▶ mock / zehnyar-mock-v1
  دقت 50% (بازهٔ ۹۰٪: 31% تا 69%)  |  توهم 6%  |  خارج از دامنه 75%  |  دستورها 100%
  برش‌ها: تعریف 100% · غلط تایپی 25% · سؤال سخت 0% · خارج از دامنه 75%
  شرط‌های سخت: همه ✓
  تأخیر p90: 50.7 ms

▶ local / zehnyar-ngram-v1
  دقت 0% (بازهٔ ۹۰٪: 0% تا 0%)  |  توهم 100%  |  خارج از دامنه 0%  |  دستورها 45%
  برش‌ها: تعریف 0% · غلط تایپی 0% · سؤال سخت 0% · خارج از دامنه 0%
  شرط‌های سخت: رد شد در دقت، توهم، خارج از دامنه، پیروی از دستور
  تأخیر p90: 216.1 ms
[هشدار] Ollama در دسترس نیست — به mock برگشتم.

▶ ollama: در دسترس نیست — از مقایسه کنار گذاشته شد

--------------------------------------------------------
انتخاب: mock — بهترین دقت میان مدل‌هایی که همهٔ شرط‌ها را دارند
داشبورد: reports/dashboard.html
دفترچهٔ آزمایش‌ها: runs/experiments.jsonl

قدم ۴ — داشبورد و دفترچه را ببینید

داشبورد را با مرورگر باز کنید. این دستور فایل را با برنامهٔ پیش‌فرض ویندوز (مرورگرتان) باز می‌کند:

PowerShell
start reports\dashboard.html

سه بخش می‌بینید: جدول شرط‌های سخت با خانه‌های سبز و قرمز (و برچسب «انتخاب» کنار برنده)، نمودار میله‌ای هر برش، و جدول تک‌تک سؤال‌ها که پاسخ هر مدل را با برچسب رنگی نتیجه‌اش نشان می‌دهد. این جدول آخر مهم‌ترین بخش است: عدد را با چشم وارسی کنید. یک پاسخ «توهم» واقعاً غلط است؟ یک «درست» واقعاً درست است؟ این همان «ارزیابیِ ارزیابی» درس ۱۱ است.

دفترچهٔ آزمایش‌ها را هم ببینید:

PowerShell
Get-Content runs\experiments.jsonl

هر خط یک آزمایش است: زمان، اثر انگشت دیتاست، مدل، تنظیمات نمونه‌برداری، پرامپت سیستمی، تنظیمات بررسی‌ها و همهٔ سنجه‌ها. اسکریپت را دوباره اجرا کنید: خط‌های تازه اضافه می‌شوند و قبلی‌ها می‌مانند. اگر چیزی را عوض کنید — مثلاً MAX_WORDS — از روی همین خط‌ها می‌فهمید کدام عدد با کدام تنظیمات آمده.

قدم ۵ (اختیاری) — یک مدل واقعی در مسابقه

اگر در فصل ۳ Ollama و مدل qwen2.5:7b را نصب کرده‌اید، در همان پاورشل بزنید $env:OLLAMA_MODEL = "qwen2.5:7b" و دستور قدم ۳ را دوباره اجرا کنید. مدل واقعی کند است: روی یک کامپیوتر خانگی بدون کارت گرافیک قوی، هر پاسخ ده‌ها ثانیه طول می‌کشد و کل اجرا شاید ربع ساعت.

این خروجی واقعی همین اسکریپت با qwen2.5:7b روی یک کامپیوتر خانگی است. خروجی شما فرق خواهد کرد — پاسخ‌های مدل واقعی احتمالی‌اند و سرعتش به سخت‌افزار بستگی دارد:

نمونهٔ خروجی واقعی با qwen2.5:7b
========================================================
 مقایسهٔ مدل‌ها  |  20 سؤال  |  دیتاست 3fc6bde6fdcf
========================================================

▶ mock / zehnyar-mock-v1
  دقت 50% (بازهٔ ۹۰٪: 31% تا 69%)  |  توهم 6%  |  خارج از دامنه 75%  |  دستورها 100%
  برش‌ها: تعریف 100% · غلط تایپی 25% · سؤال سخت 0% · خارج از دامنه 75%
  شرط‌های سخت: همه ✓
  تأخیر p90: 51.1 ms

▶ local / zehnyar-ngram-v1
  دقت 0% (بازهٔ ۹۰٪: 0% تا 0%)  |  توهم 100%  |  خارج از دامنه 0%  |  دستورها 45%
  برش‌ها: تعریف 0% · غلط تایپی 0% · سؤال سخت 0% · خارج از دامنه 0%
  شرط‌های سخت: رد شد در دقت، توهم، خارج از دامنه، پیروی از دستور
  تأخیر p90: 125.9 ms

▶ ollama / qwen2.5:7b
  دقت 6% (بازهٔ ۹۰٪: 0% تا 19%)  |  توهم 94%  |  خارج از دامنه 25%  |  دستورها 90%
  برش‌ها: تعریف 0% · غلط تایپی 0% · سؤال سخت 20% · خارج از دامنه 25%
  شرط‌های سخت: رد شد در دقت، توهم، خارج از دامنه، تأخیر p90
  تأخیر p90: 39028.4 ms

--------------------------------------------------------
انتخاب: mock — بهترین دقت میان مدل‌هایی که همهٔ شرط‌ها را دارند
داشبورد: reports/dashboard.html
دفترچهٔ آزمایش‌ها: runs/experiments.jsonl

چه چیزی ساختیم؟

تمرین شما

  1. «ندارم» را به فهرست REFUSALS در checks.py اضافه کنید و دوباره اجرا کنید (اگر Ollama دارید، با qwen). چه چیزی درست شد؟ حالا فکر کنید: آیا این تغییر ممکن است جای دیگری را خراب کند؟ (مثلاً پاسخ «شک ندارم که جواب سه لایه است» حالا رد کردن حساب می‌شود؟) به همین دلیل هر تغییر در بررسی‌ها را باید با چشم وارسی کرد.
  2. در CRITERIA حد تأخیر را به 60000 (یک دقیقه) ببرید. اگر Ollama دارید، qwen این بار به خاطر چه شرط‌هایی رد می‌شود؟ این همان پرسش «باید یا خوب است؟» درس ۵ است.
  3. اسکریپت را دو بار پشت هم اجرا کنید و دو خط آخر runs/experiments.jsonl را مقایسه کنید. کدام عددها یکسان‌اند و کدام نه؟ چرا تأخیر یکسان نیست ولی دقت هست؟
  4. یک برش تازه به نام «مقایسه» بسازید (مثلاً «فرق RAG و تنظیم دقیق چیست؟»): اسمش را به SLICES در evalset.py اضافه کنید و دو سؤال با کلیدواژه بنویسید. داشبورد خودش برش تازه را نشان می‌دهد.

خودآزمایی

چرا run_model وقتی می‌بیند bot.provider.name با اسم خواسته‌شده فرق دارد، None برمی‌گرداند؟
چرا اولین فراخوانی هر مدل («گرم کردن») در تأخیر حساب نمی‌شود؟
در داشبورد، q17 برای qwen «پاسخ بی‌جا» خورده ولی متن پاسخ یک رد کردن مؤدبانه است. این یعنی چه؟

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

قضاوت مهندسی

چرا خط لولهٔ ذهن‌یار مدلی را که به mock برگشته کنار می‌گذارد؟

برای دیدن پاسخ کلیک کنید یا Space بزنید

کارت ۱ از ۲

آکادمی هوش مصنوعی شیدهوش — بخشی از shidhoosh.com