Урок курса

Почему «accuracy» ≠ качество в LLM

LLMOps & Evaluation: из песочницы в продакшн

Почему «accuracy» ≠ качество в LLM

В классическом машинном обучении accuracy (точность) часто служит основным показателем качества моделей. Например, для задачи классификации accuracy показывает долю правильно предсказанных меток из всех примеров. Однако для современных больших языковых моделей (LLM) понятие качества гораздо шире и сложнее, чем простая доля «правильных» ответов.

Accuracy не отражает многогранность качества. Когда мы задаём LLM открытый вопрос (например: «Объясни смысл квантовой запутанности простыми словами»), нельзя дать единственный «правильный» ответ, с которым можно было бы напрямую сравнить ответ модели. В таких генеративных задачах может быть множество корректных ответов, отличающихся стилем, уровнем детализации и формулировками. Даже в задачах с явным фактическим ответом (как вопрос отвечаемый одним фактом) качество ответа LLM не сводится к бинарному «правильно/неправильно». Модель может предоставить частично верный ответ, или верный по факту, но сформулированный так, что пользователь остаётся недоволен. Поэтому метрика, рассчитанная как доля точно совпадающих ответов, пропустит много аспектов качества.

Качество LLM включает несколько измерений. Помимо фактической корректности (правильности информации) важны такие критерии, как:

  • Релевантность и полнота ответа. Насколько ответ адресует поставленный вопрос и покрывает все важные детали. Модель может дать формально верный факт, но упустить контекст или ключевое уточнение – с точки зрения accuracy факт верен, но ответ всё же неполноценен.

  • Ясность и связность изложения. Понятно ли сформулирован ответ, логично ли он структурирован. LLM может генерировать грамматически правильные, но сумбурные тексты. Пользователь же ценит связность, простоту и отсутствие противоречий в ответе, что accuracy не измеряет.

  • Стиль и уместность. Для прикладных систем важно, чтобы тон и стиль ответа соответствовали задаче. Например, в диалоге с клиентом модель должна быть вежлива и соблюдаать профессиональный тон. С точки зрения обычной точности текст может быть верным по содержанию, но неподходящим по форме, что снижает качество. (Например, модель может точно ответить на вопрос пациента о его диагнозе, но сделать это грубо или пугающе – такой ответ нельзя назвать качественным, хоть фактически он верен.)

  • Полезность и удовлетворённость пользователя. Качество LLM-системы в конечном счёте определяется тем, насколько она помогает пользователям. Даже если ответы точны, но пользователь остаётся недоволен (слишком кратко, без примеров, либо наоборот – перегружено), это означает низкое качество обслуживания. Удовлетворённость – субъективный, но критически важный показатель качества работы LLM.

  • Надёжность и безопасность. Важный аспект – отсутствие галлюцинаций (выдуманных фактов) и нежелательного контента. Модель может отвечать уверенно, но если информация ложная (галлюцинация) – это провал качества, не отражаемый классической accuracy. Точно так же, ответ без фактических ошибок, но содержащий оскорбительный или небезопасный текст, нельзя считать качественным. Современные представления о качестве включают метрики правдивости, токсичности, предвзятости и др., отражающие доверие к ответам модели.

Таким образом, прямая метрика accuracy, полезная для простых задач (например, распознавание цифр или анализ тональности текста), не подходит для оценки сложных ответов LLM. Высокая accuracy не гарантирует, что модель не генерирует лишний текст, не сбивается с контекста или не раздражает пользователя тоном. И наоборот, модель может не иметь 100% точности по фактам, но быть предпочитаемой пользователями за счёт вежливости и понятности.

Чтобы подчеркнуть разницу: представим, что LLM решает задачу чат-бота службы поддержки. Один вариант модели отвечает на 85% запросов совершенно корректно по данным (accuracy = 85%), но часто сухо или отказывает без попытки помочь. Другой вариант может отвечать точно лишь на 75% вопросов, но старается помочь с каждым запросом: если не знает ответ – предлагает, куда обратиться, даёт разъяснения. Пользователи могут оценивать вторую модель выше, несмотря на более низкую «формальную» точность, потому что общий опыт взаимодействия качественнее.

Кроме того, стремление максимизировать только accuracy может навредить качеству. Например, если настроить модель отвечать только когда она абсолютно уверена (стремясь избежать неточных ответов), она начнёт слишком часто отказываться отвечать. Accuracy таких ответов будет формально высокой (ведь модель почти не ошибается – она просто молчит при малейшей неуверенности), но пользовательский опыт ухудшится. Пользователи предпочтут пусть неидеальный, но информативный ответ вместо постоянного «Извините, я не могу помочь». Значит, нам нужны метрики, учитывающие баланс между полнотой и точностью, между инициативностью и риском ошибок.

Наконец, оценка качества LLM почти всегда требует человеческого суждения. То, что считается «хорошим» ответом, часто субъективно. Поэтому в разработке LLM-систем широко применяются человеческие оценки: например, эксперт просматривает ответы модели и оценивает по нескольким критериям (правильность, ясность, стиль, полезность) по шкале. Такие оценки затем можно агрегировать в интегральную метрику качества. Конечно, ручная оценка дорогостоящая и не масштабируется на каждый ответ, поэтому далее мы рассмотрим автоматические метрики, которые приближаются к человеческой оценке. Но важно понимать: ни одна цифра (даже современная сложная метрика) не отразит всего, что мы вкладываем в понятие «качественный ответ LLM». Поэтому в практике LLMOps часто комбинируют несколько метрик и регулярно сверяют их с человеческой оценкой.

Ручная рубрика 0–2 по 4 граням + быстрый отчёт «почему accuracy обманывает»

Цель: получить свой первый, руками размеченный срез качества по четырём граням (релевантность/полнота, ясность/связность, стиль/уместность, фактическая корректность) и наглядно увидеть, как «наивная accuracy» (точное совпадение/ключевые слова) расходится с реальной полезностью ответов.

1) Мини-теория

  • Почему не хватает accuracy: в генерации допустимо множество корректных формулировок; полезность зависит не только от «правильных фактов», но и от того, как они поданы (ясность, тон, полнота).
  • Рубрика 0–2 (каждая грань): 0 — плохо/нет, 1 — частично/терпимо, 2 — хорошо/чётко.
  • Сравниваем многогранный балл (сумма 0..8) с наивной accuracy (строгое совпадение или «все ключевые слова найдены»).

2) Что вы получите на выходе

  • Готовый шаблон датасета из 8 кейсов (можно расширить до 20+).
  • Скрипт, который: (а) создаёт заготовку для разметки, (б) собирает ваши оценки, (в) строит отчёт и показывает расхождения с accuracy.
  • Автотесты на «симуляции», чтобы вы видели целевое поведение до своей разметки.

3) Файлы рубрики и датасета

Сохраните quality_rubric.yaml рядом со скриптом:

# quality_rubric.yaml
dimensions:
  - id: relevance
    name: "Релевантность/полнота"
    scale: {min: 0, max: 2}
    guide:
      0: "Не отвечает на вопрос/пропущены ключевые детали"
      1: "Отвечает частично, важные детали упущены"
      2: "Полный и по делу ответ"
  - id: clarity
    name: "Ясность/связность"
    scale: {min: 0, max: 2}
    guide:
      0: "Сумбурно/противоречиво/трудно понять"
      1: "Понятно, но местами сумбурно"
      2: "Чётко структурировано, легко читать"
  - id: style
    name: "Стиль/уместность"
    scale: {min: 0, max: 2}
    guide:
      0: "Тон неуместен/канцелярит/избыточно"
      1: "Допустимо, но можно лучше"
      2: "Тон и форма уместны для аудитории"
  - id: factual
    name: "Фактическая корректность"
    scale: {min: 0, max: 2}
    guide:
      0: "Есть ошибки/галлюцинации"
      1: "В целом верно, но неточности"
      2: "Факты корректны"
scoring:
  total_max: 8
notes: "Оценивайте готовый ответ модели по 4 граням. Комментарии приветствуются."

Сохраните m3_l1_cases.jsonl (пример на 8 кейсов; расширьте до 20+ по желанию):

{"id":"r1","question":"Сколько дней базового отпуска?","canonical":"28 дней","keywords":["28","дней","отпуск"],"model_answer":""}
{"id":"r2","question":"Оплачивается ли больничный?","canonical":"Да, оплачивается по нормам","keywords":["больничный","оплачивается"],"model_answer":""}
{"id":"r3","question":"Есть ли отпуск без содержания?","canonical":"Да, предоставляется по заявлению","keywords":["без","содерж","заявлен"],"model_answer":""}
{"id":"r4","question":"Какие правила командировок?","canonical":"Суточные, авиа/жд, отчёт","keywords":["суточные","отчёт"],"model_answer":""}
{"id":"r5","question":"Как оформить переработки?","canonical":"По согласованию, доп. отпуск","keywords":["переработ","доп","отпуск"],"model_answer":""}
{"id":"r6","question":"Разрешена ли удалённая работа?","canonical":"Разрешена по согласованию; правила коммуникации","keywords":["удалён","соглас","правила"],"model_answer":""}
{"id":"r7","question":"Как возмещают обучение?","canonical":"По договору/бюджету, после согласования","keywords":["обучен","возмещ","соглас"],"model_answer":""}
{"id":"r8","question":"Когда проходить обучение по безопасности?","canonical":"Ежегодно","keywords":["ежегодно","обучен","безопас"],"model_answer":""}

4) Скрипт для шаблона разметки и отчёта

Сохраните как quality_step1.py и запустите. Он создаст CSV-шаблон для оценок, умеет «симулировать» ответы и собрать отчёт.

# quality_step1.py
from __future__ import annotations
import csv, json, re, sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Dict, Any, Tuple

CASES = Path("m3_l1_cases.jsonl")
LABELS = Path("labels.csv")
REPORT_MD = Path("quality_report.md")

@dataclass
class Case:
    id: str
    question: str
    canonical: str
    keywords: List[str]
    model_answer: str

def load_cases(p=CASES) -> List[Case]:
    out=[]
    for line in p.read_text(encoding="utf-8").splitlines():
        if not line.strip(): continue
        r=json.loads(line)
        out.append(Case(id=r["id"], question=r["question"], canonical=r["canonical"], keywords=r["keywords"], model_answer=r.get("model_answer","")))
    return out

def write_labels_template(cases: List[Case], path=LABELS):
    if path.exists(): return
    with path.open("w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["id","relevance","clarity","style","factual","comments"])
        for c in cases:
            w.writerow([c.id,"","","","",""])
    print(f"Шаблон для разметки создан: {path}")

# ----- наивные метрики accuracy -----
def exact_match(a: str, canonical: str) -> int:
    return int(a.strip().lower() == canonical.strip().lower())
def all_keywords(a: str, kws: List[str]) -> int:
    t=a.lower()
    return int(all(k.lower() in t for k in kws))

# ----- симуляция ответов (для автотестов и примера) -----
def simulate_answers(cases: List[Case]) -> Dict[str,str]:
    sim={}
    for c in cases:
        if c.id=="r1": sim[c.id] = "Базовый отпуск — 28 дней. Этого достаточно? [d1]"
        elif c.id=="r2": sim[c.id] = "Да, больничный у нас оплачивается согласно нормам. Подробности в разделе HR."
        elif c.id=="r3": sim[c.id] = "Возможен отпуск без сохранения зарплаты, по заявлению руководителю."
        elif c.id=="r4": sim[c.id] = "Командировки... ну, что-то про отчёты. (не уверен)"
        elif c.id=="r5": sim[c.id] = "Переработки: да, можно оформить. (но детали не указаны)"
        elif c.id=="r6": sim[c.id] = "Разрешена, но соблюдайте правила коммуникации и график встреч."
        elif c.id=="r7": sim[c.id] = "Возмещаем обучение, если это важно компании и есть бюджет. Согласуйте заранее."
        elif c.id=="r8": sim[c.id] = "Раз в год."
    return sim

# ----- чтение ручных оценок -----
def load_labels(path=LABELS) -> Dict[str,Dict[str,int]]:
    if not path.exists(): return {}
    out={}
    with path.open("r", encoding="utf-8") as f:
        rd = csv.DictReader(f)
        for r in rd:
            try:
                out[r["id"]] = {
                    "relevance": int(r["relevance"]),
                    "clarity": int(r["clarity"]),
                    "style": int(r["style"]),
                    "factual": int(r["factual"]),
                }
            except Exception:
                # пропускаем пустые строки/некорректные значения
                pass
    return out

def score_total(lbl: Dict[str,int]) -> int:
    return sum(lbl.get(k,0) for k in ("relevance","clarity","style","factual"))

def build_report():
    cases = load_cases()
    write_labels_template(cases, LABELS)
    labels = load_labels(LABELS)

    # если пользователь ещё не размечал, используем симуляцию + авто-метки (демо)
    if not labels:
        sim = simulate_answers(cases)
        auto_labels={}
        for c in cases:
            a = sim.get(c.id,"")
            # простая эвристика для демо
            rel = 2 if all_keywords(a, c.keywords) else (1 if any(k in a.lower() for k in c.keywords) else 0)
            fact = 2 if ("не уверен" not in a and "?" not in a and "можно" not in a.lower()) else 1
            clar = 2 if len(a.split("."))>=2 and len(a)>20 else 1
            style = 1 if ("ну" in a.lower() or "..." in a) else 2
            auto_labels[c.id] = {"relevance":rel,"clarity":clar,"style":style,"factual":fact}
        labels = auto_labels
        print("Используем авто-разметку (демо). Для своей разметки заполните labels.csv целыми 0/1/2 и перезапустите.")

    rows=[]
    acc_exact_hits=0
    acc_kw_hits=0
    for c in cases:
        ans = c.model_answer.strip() or simulate_answers(cases).get(c.id,"")
        em = exact_match(ans, c.canonical)
        kw = all_keywords(ans, c.keywords)
        acc_exact_hits += em
        acc_kw_hits += kw
        q = labels.get(c.id, {"relevance":0,"clarity":0,"style":0,"factual":0})
        rows.append({
            "id": c.id,
            "question": c.question,
            "answer": ans,
            "accuracy_exact": em,
            "accuracy_keywords": kw,
            "relevance": q["relevance"],
            "clarity": q["clarity"],
            "style": q["style"],
            "factual": q["factual"],
            "quality_total": score_total(q)
        })

    # агрегаты
    n=len(cases)
    acc_exact = acc_exact_hits / n
    acc_kw = acc_kw_hits / n
    avg_total = sum(r["quality_total"] for r in rows) / (n*8)  # доля от максимума
    # топ-расхождения
    worse_quality = [r for r in rows if r["accuracy_keywords"]==1 and r["quality_total"] <= 3]
    useful_but_inaccurate = [r for r in rows if r["accuracy_keywords"]==0 and r["quality_total"] >= 5]

    # пишем отчёт
    lines=[]
    lines.append(f"# Quality vs Accuracy — Report")
    lines.append("")
    lines.append(f"- Cases: {n}")
    lines.append(f"- Accuracy (exact string): {acc_exact:.2f}")
    lines.append(f"- Accuracy (all keywords): {acc_kw:.2f}")
    lines.append(f"- Avg quality (share of max 8): {avg_total:.2f}")
    lines.append("")
    lines.append("## Examples: high accuracy but low quality")
    if not worse_quality:
        lines.append("_none in this run_")
    for r in worse_quality[:5]:
        lines.append(f"- {r['id']}: acc_kw=1, quality_total={r['quality_total']} → провал по {['relevance','clarity','style','factual']}")
    lines.append("")
    lines.append("## Examples: low accuracy but useful")
    if not useful_but_inaccurate:
        lines.append("_none in this run_")
    for r in useful_but_inaccurate[:5]:
        lines.append(f"- {r['id']}: acc_kw=0, quality_total={r['quality_total']} — ответ полезный, но не прошёл ключевые слова/строгое совпадение")
    lines.append("")
    lines.append("## Raw rows")
    lines.append("| id | acc_exact | acc_kw | rel | clar | style | fact | total |")
    lines.append("|---|---:|---:|---:|---:|---:|---:|---:|")
    for r in rows:
        lines.append(f"| {r['id]()

«До/после»: перепрошивка промпта и честное сравнение качества vs accuracy

Цель. Зафиксировать базовый уровень качества «до», внести одну-две правки промпта/шаблона ответа и сравнить «до→после» по 4 граням качества и двум наивным метрикам accuracy (точное совпадение и «все ключевые слова есть»). Отчёт покажет, где качество выросло при том же accuracy — и наоборот.

Что понадобится

  • Файлы из шага 1: quality_rubric.yaml, m3_l1_cases.jsonl, labels.csv.
  • Два набора ответов модели: answers_before.jsonl и answers_after.jsonl (формат см. ниже). Если их нет — скрипт сделает «симулированные» ответы, чтобы вы могли запустить процесс целиком.
  • Две разметки качества: labels_before.csv (скопируйте ваш labels.csv) и labels_after.csv (заполните после правок промпта).

Форматы входных данных

answers_*.jsonl (по строке на кейс):

{"id":"r1","answer":"Базовый отпуск — 28 дней. [d1] SOURCE: ..."}
{"id":"r2","answer":"Да, больничный оплачивается по нормам."}

labels_*.csv — ровно как в шаге 1: колонки id,relevance,clarity,style,factual,comments со значениями 0/1/2.

Скрипт сравнения «до→после»

Сохраните как quality_step2_compare.py. Никаких внешних зависимостей.

# quality_step2_compare.py
from __future__ import annotations
import csv, json, math, sys
from pathlib import Path
from typing import Dict, List, Tuple

CASES = Path("m3_l1_cases.jsonl")
ANS_BEFORE = Path("answers_before.jsonl")
ANS_AFTER  = Path("answers_after.jsonl")
LBL_BEFORE = Path("labels_before.csv")   # скопируйте labels.csv из шага 1
LBL_AFTER  = Path("labels_after.csv")    # заполните после правок промпта
REPORT = Path("quality_diff_report.md")

# ---------- утилиты ----------
def load_cases() -> Dict[str, dict]:
    m={}
    for line in CASES.read_text(encoding="utf-8").splitlines():
        if not line.strip(): continue
        r = json.loads(line)
        m[r["id"]] = r
    return m

def load_answers(p: Path) -> Dict[str,str]:
    if not p.exists(): return {}
    m={}
    for line in p.read_text(encoding="utf-8").splitlines():
        if not line.strip(): continue
        r=json.loads(line)
        m[r["id"]] = r.get("answer","")
    return m

def load_labels(p: Path) -> Dict[str, Dict[str,int]]:
    if not p.exists(): return {}
    out={}
    with p.open("r", encoding="utf-8") as f:
        rd = csv.DictReader(f)
        for r in rd:
            try:
                out[r["id"]] = {
                    "relevance": int(r["relevance"]),
                    "clarity": int(r["clarity"]),
                    "style": int(r["style"]),
                    "factual": int(r["factual"]),
                }
            except Exception:
                pass
    return out

def exact_match(ans: str, canonical: str) -> int:
    return int((ans or "").strip().lower() == canonical.strip().lower())

def all_keywords(ans: str, kws: List[str]) -> int:
    t = (ans or "").lower()
    return int(all(k.lower() in t for k in kws))

def score_total(lbl: Dict[str,int]) -> int:
    return sum(lbl.get(k,0) for k in ("relevance","clarity","style","factual"))

# симуляции на случай отсутствия файлов: «до» и «после» (после — более структурно/полно)
def simulate_before(cases: Dict[str,dict]) -> Dict[str,str]:
    out={}
    for cid,c in cases.items():
        q=c["question"].lower()
        if "отпуск" in q and "сколько" in q: out[cid]="Базовый отпуск — 28 дней. [d1]"
        elif "больнич" in q: out[cid]="Да, больничный оплачивается по нормам."
        elif "без содерж" in q or "без содержания" in q: out[cid]="Возможен отпуск без сохранения зарплаты."
        elif "командиров" in q: out[cid]="Командировки... что-то про отчёты."
        else: out[cid]="Короткий ответ без деталей."
    return out

def simulate_after(cases: Dict[str,dict]) -> Dict[str,str]:
    out={}
    for cid,c in cases.items():
        kws="; ".join(c["keywords"])
        out[cid]=f"Краткий ответ: {c['canonical']}. Причины/детали: {kws}. [d1] SOURCE: kb://doc"
    return out

# простой двусторонний точный биномиальный тест знаков (delta>0 vs delta<0)
def sign_test_p(n_pos: int, n_neg: int) -> float:
    n = n_pos + n_neg
    if n == 0: return 1.0
    k = max(n_pos, n_neg)
    # p = 2 * sum_{i=k..n} C(n,i) * 0.5^n
    # считаем хвост двумя циклами без внешних либ
    def comb(n,r):
        r=min(r, n-r); num=1; den=1
        for i in range(1, r+1):
            num*= (n - r + i)
            den*= i
        return num//den
    tail = sum(comb(n,i) for i in range(k, n+1)) * (0.5 ** n)
    return min(1.0, 2*tail)

def main():
    cases = load_cases()
    # ответы
    ans_before = load_answers(ANS_BEFORE) or simulate_before(cases)
    ans_after  = load_answers(ANS_AFTER)  or simulate_after(cases)
    # разметка
    lbl_before = load_labels(LBL_BEFORE)
    lbl_after  = load_labels(LBL_AFTER)
    # если нет разметки, попробуем приблизить: «после» лучше по ясности/полноте
    if not lbl_before or not lbl_after:
        auto_b={}
        auto_a={}
        for cid,c in cases.items():
            # эвристика «до»
            kw=all_keywords(ans_before.get(cid,""), c["keywords"])
            auto_b[cid]={"relevance": 2 if kw else 1, "clarity":1, "style":1, "factual":2}
            # «после»: структурнее и полнее
            kw2=all_keywords(ans_after.get(cid,""), c["keywords"])
            auto_a[cid]={"relevance": 2 if kw2 else 1, "clarity":2, "style":2, "factual":2}
        lbl_before = lbl_before or auto_b
        lbl_after  = lbl_after  or auto_a

    rows=[]
    dpos=dneg=0
    dexact_up=dexact_down=0
    dkw_up=dkw_down=0

    for cid,c in cases.items():
        b = lbl_before.get(cid, {"relevance":0,"clarity":0,"style":0,"factual":0})
        a = lbl_after.get(cid,  {"relevance":0,"clarity":0,"style":0,"factual":0})
        tb = score_total(b)
        ta = score_total(a)
        delta = ta - tb
        if delta>0: dpos+=1
        if delta<0: dneg+=1

        em_b = exact_match(ans_before.get(cid,""), c["canonical"])
        em_a = exact_match(ans_after.get(cid,""),  c["canonical"])
        if em_a>em_b: dexact_up+=1
        if em_akw_b: dkw_up+=1
        if kw_a0 and (r["delta_em"]==0 and r["delta_kw"]==0)]
    worse_same_acc    = [r for r in rows if r["delta_q"]<0 and (r["delta_em"]==0 and r["delta_kw"]==0)]
    useful_but_inacc  = [r for r in rows if r["quality_after"]>=5 and r["em_after"]==0 and r["kw_after"]==0]

    # отчёт
    lines=[]
    lines += [
      "# Quality Do/After — Diff Report", "",
      f"- Cases: {n}",
      f"- Avg quality BEFORE: {avg_b:.2f}",
      f"- Avg quality AFTER : {avg_a:.2f}  (Δ = {avg_delta:+.2f})",
      f"- Sign test (delta>0 vs delta<0): pos={dpos}, neg={dneg}, p≈{p_sign:.4f}",
      f"- Accuracy exact Δ: up={dexact_up}, down={dexact_down}",
      f"- Accuracy keywords Δ: up={dkw_up}, down={dkw_down}",
      "", "## Improved quality with unchanged accuracy (examples)"
    ]
    if not improved_same_acc: lines.append("_none_")
    for r in improved_same_acc[:5]:
        lines.append(f"- {r['id']}: ΔQ={r['delta_q']} (acc exact {r['em_after']}, kw {r['kw_after']}) — стало лучше без роста accuracy")

    lines += ["", "## Worse quality with unchanged accuracy (watch out)"]
    if not worse_same_acc: lines.append("_none_")
    for r in worse_same_acc[:5]:
        lines.append(f"- {r['id']}: ΔQ={r['delta_q']} (acc неизменна) — качество упало, изучите промпт")

    lines += ["", "## Useful but 'inaccurate' AFTER (low accuracy, high quality)"]
    if not useful_but_inacc: lines.append("_none_")
    for r in useful_but_inacc[:5]:
        lines.append(f"- {r['id']}: Q_after={r['quality_after']} при acc=0 — хороший пример, где accuracy 'не видит' пользу")

    lines += ["", "## Per-case table", "| id | Qb | Qa | ΔQ | EMb | EMa | KWb | KWa |", "|---|---:|---:|---:|---:|---:|---:|---:|"]
    for r in rows:
        lines.append(f"| {r['id']} | {r['quality_before']} | {r['quality_after']} | {r['delta_q']:+} | {r['em_before']} | {r['em_after']} | {r['kw_before']} | {r['kw_after']} |")

    REPORT.write_text("\n".join(lines), encoding="utf-8")
    print(f"Готово: {REPORT}\n"
          f"• Если у вас ещё нет файлов answers_*.jsonl и labels_*.csv — отчёт собран на симуляциях.\n"
          f"• Для реального сравнения заполните их и запустите снова.")

if __name__ == "__main__":
    if not CASES.exists():
        raise SystemExit("Нет m3_l1_cases.jsonl — запустите шаг 1 сначала.")
    main()

Как запускать

  1. База «до». Скопируйте вашу разметку из шага 1:
    cp labels.csv labels_before.csv
    (Опционально) выгрузите ответы модели «до» в answers_before.jsonl.
  2. Сделайте 1–2 правки промпта/шаблона ответа (например: требовать список шагов и краткий вывод).
  3. «После». Соберите ответы «после» в answers_after.jsonl и разметьте качество в labels_after.csv (0/1/2).
  4. Запустите сравнение:
    python quality_step2_compare.py
    Получите quality_diff_report.md с дельтами по качеству и изменениям accuracy.

Микро-лаба (что проверить)

  • Выросла ли Avg quality при неизменном Accuracy keywords? Это признак удачной правки формы/структуры.
  • Есть ли кейсы «useful but inaccurate» — высокая полезность при нулевой accuracy (важные для защиты решений).
  • Сошлась ли «значимость» по знаковому тесту (p < 0.05) — тогда улучшение устойчиво, а не случайно.

Подсказки и расширение

  • Держите один «тип правки» за раз (например, только формат ответа), чтобы интерпретация дельт была честной.
  • Разбейте отчёт по классам запросов (FAQ/числа/длинные/ошибочные формулировки) — добавьте колонку tag в набор кейсов.
  • Дальше можно подключить офлайн-«судью» для грубой автопроверки (релевантность/цитаты/числа) — и сверять с человеческой разметкой.

Задача 1

Почему accuracy плохо описывает качество генеративных ответов LLM?
 

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Выберите измерения качества, которые accuracy не покрывает.
 

Выберите все подходящие варианты
Без регистрации · результат не сохраняется

Задача 3

Модель A редко отвечает и часто «отказывается», но почти не ошибается; модель B отвечает почти всегда, иногда ошибается, но помогает чаще. Какая метрика, дополняющая accuracy, нужна, чтобы принять решение?
 

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 4

Сопоставьте ситуацию и проблему метрики accuracy.

Сопоставьте элементы

Без регистрации · результат не сохраняется

Задача 5

Что верно про «accuracy» в LLM-продуктах?
 

Выберите все подходящие варианты
Без регистрации · результат не сохраняется

Задача 6

В каком из кейсов accuracy наиболее уместна как основная метрика?

 

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 7

Команда оптимизирует единственную метрику accuracy и хвастается ростом, но на дашборде падает «доля успешных сессий». Что вероятнее всего происходит?
 

Выберите один вариант
Без регистрации · результат не сохраняется

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку