פוסט

Evals: למדוד מערכת LLM

למה כל מערכת LLM צריכה סט הערכה, איך בונים אחד, בדיקות בקוד, LLM-as-judge עם רובריקה, ואיך משתמשים ב-evals כדי להשוות prompts, מודלים והגדרות.

Eval הוא סט של מקרי בדיקה עם דרך לתת ציון לפלט, שמריצים בכל פעם שמשנים את ה-prompt, את המודל, את השליפה או הגדרה כלשהי. בלי eval, כל שינוי הוא ניחוש, ואת הרגרסיות מגלים מהמשתמשים.

ה-harness הורץ עם Python 3.9 והפלט שלו אמיתי; הקריאה למודל בתוכו היא תחליף. קוד השופט מבוסס על המדריך לפיתוח בדיקות ולא הורץ.

מה הופך eval לטוב

המדריך הרשמי נותן ארבעה עקרונות:

  • קריטריונים ספציפיים ומדידים. לא “פלט בטוח”, אלא, בדוגמה של המדריך, “פחות מ-0.1% מהפלטים מתוך 10,000 ניסיונות מסומנים כרעילים”.
  • מקרים ספציפיים למשימה שמשקפים את התעבורה האמיתית, כולל מקרי קצה: קלט לא רלוונטי, קלט ארוך מאוד, בקשות עמומות.
  • אוטומציה כשאפשר, כדי שתוכלו להריץ בכל שינוי.
  • כמות לפני ליטוש. המדריך אומר ש"יותר שאלות עם ציון אוטומטי באות מעט נמוך יותר עדיפות על פחות שאלות עם ציון אנושי באיכות גבוהה".

התחילו מדוגמאות אמיתיות: שאלות של משתמשים, פניות, מסמכים שאתם מעבדים. המאמר של Anthropic על מערכת מרובת סוכנים התחיל מכ-20 שאלות מציאותיות, וכמה עשרות מקרים טובים כבר תופסים את רוב הרגרסיות.

ציון בקוד

כשקוד יכול לבדוק את התשובה, בודקים בקוד: זה מהיר, זול ומדויק. eval לסיווג עם התאמה מדויקת:

# eval זעיר: לכל מקרה יש קלט ותווית צפויה
cases = [
    {"input": "I was charged twice", "expected": "billing"},
    {"input": "App crashes on upload", "expected": "bug"},
    {"input": "How do I change my email?", "expected": "account"},
    {"input": "Refund for last month please", "expected": "billing"},
]

def classify(text):
    # תחליף לקריאה למודל, כדי שאפשר יהיה לבדוק את ה-harness עצמו
    fake_answers = {"I was charged twice": "Billing", "App crashes on upload": "bug",
                    "How do I change my email?": "billing", "Refund for last month please": "billing"}
    return fake_answers[text]

def exact_match(output, expected):
    return output.strip().lower() == expected.lower()

failures = []
for case in cases:
    output = classify(case["input"])
    if not exact_match(output, case["expected"]):
        failures.append((case["input"], case["expected"], output))

passed = len(cases) - len(failures)
print(f"passed {passed}/{len(cases)} ({passed / len(cases):.0%})")
for text, expected, got in failures:
    print(f"FAIL: {text!r}: expected {expected!r}, got {got!r}")
passed 3/4 (75%)
FAIL: 'How do I change my email?': expected 'account', got 'billing'

בדיקות קוד נוספות מהמדריך: האם מחרוזת מופיעה, JSON תקין מול סכמה, דמיון embeddings בין תשובות (לעקביות), ו-ROUGE לסיכומים. ב-RAG, בדקו גם את השליפה בנפרד: האם ה-chunk הנכון היה בתוצאות המובילות?

LLM-as-judge

טון, מועילות ונאמנות למסמך דורשים מודל שני שנותן ציון לפלט לפי רובריקה:

import anthropic

client = anthropic.Anthropic()

def judge(question, answer, documents):
    prompt = f"""Grade the answer using only the documents.

<documents>{documents}</documents>
<question>{question}</question>
<answer>{answer}</answer>

Rubric:
- 1: every claim in the answer is supported by the documents
- 0: at least one claim is not supported, or the answer contradicts the documents

Think about each claim first, then end with a line "SCORE: 0" or "SCORE: 1"."""
    reply = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=2000,
        messages=[{"role": "user", "content": prompt}],
    )
    text = next(b.text for b in reply.content if b.type == "text")
    return int(text.strip().splitlines()[-1].removeprefix("SCORE:").strip())

מה הופך שופט לאמין:

  • רובריקה מפורטת עם רמות ברורות, לא “האם זה טוב?”. סקאלות בינאריות או קטנות (0/1, 1 עד 5) עקביות יותר מציונים פתוחים.
  • נימוק לפני הציון, כדי שהשופט יבחן כל טענה לפני שהוא מחליט.
  • מודל שונה מזה שנבדק, מה שהמדריך מכנה “בדרך כלל best practice”.
  • לבדוק את השופט מול כמה עשרות מקרים שדירגתם ביד. אם הוא חולק עליכם הרבה, תקנו את הרובריקה לפני שסומכים עליו.

איך משתמשים ב-evals

  • לפני שינוי: מריצים ושומרים את הציון. אחרי: מריצים שוב ומשווים. מסתכלים על המקרים שהתהפכו, לא רק על הסך הכל.
  • בחירת מודל או רמת effort: התיעוד ממליץ על effort sweep, הרצה של אותו eval בכמה רמות ובחירה בזולה ביותר ששומרת על האיכות.
  • ב-CI: eval קטן ומהיר בכל שינוי ב-prompt, כמו בדיקות יחידה, וגדול יותר לפני release.
  • מה-production: תשובה גרועה שמשתמש מדווח עליה נוספת כמקרה, וה-eval גדל מכשלים אמיתיים.

פלט של LLM משתנה בין הרצות, ולכן בהחלטות חשובות מריצים כל מקרה יותר מפעם אחת ומשווים ממוצעים.

הפוסט הבא עוסק ב-observability: לראות מה המערכת שלכם עושה בפועל ב-production.