Первый eval: от «вроде работает» к проверке на данных
Выбираем одну задачу, собираем примеры и пишем проверку на Python. В конце — воспроизводимый результат и todo для следующей итерации.
Ты поменял промпт. Ответы выглядят лучше. Но стали ли они лучше на самом деле? Один удачный диалог этого не покажет. Нужна небольшая проверка, которую можно повторить после каждого изменения.
Eval — это набор примеров, запуск системы на них и правило оценки результата. Начнём с классификации обращений в поддержку: здесь правильность можно проверить обычным кодом, без второй модели в роли судьи.
Что получится: шесть учебных примеров, файл ответов и скрипт, который считает accuracy и показывает ошибки. Нужен Python 3.10+; внешние библиотеки и API-ключ для самого проверяющего скрипта не нужны.
01 / Выбери одну задачу
Пусть модель относит обращение к одному из трёх классов:
billing— списания, возвраты, счета;technical— ошибки и неработающие функции;other— всё остальное.
Сразу задай правило для пересечений: если пользователь сообщает о технической ошибке, выбираем technical, даже если в сообщении упоминается оплата. Иначе два человека могут разметить один пример по-разному, а метрика будет оценивать неопределённость твоих инструкций.
Todo: запиши задачу одним предложением и определи, что именно считается правильным ответом. Не пытайся одним числом оценить всю полезность ассистента.
02 / Собери небольшой датасет
Создай папку first-eval и положи в неё файл cases.jsonl. Каждая строка — отдельный JSON-объект: стабильный ID, вход и ожидаемый класс.
{"id":"01","input":"Списали деньги дважды","expected":"billing"}
{"id":"02","input":"Приложение падает при входе","expected":"technical"}
{"id":"03","input":"Где скачать счёт за август?","expected":"billing"}
{"id":"04","input":"Хочу предложить партнёрство","expected":"other"}
{"id":"05","input":"После оплаты не могу войти: ошибка 500","expected":"technical"}
{"id":"06","input":"Спасибо, всё заработало","expected":"other"}
Эти шесть примеров придуманы для обучения. Для своей задачи начни, например, с 20–30 обезличенных обращений: обычных, неоднозначных и тех, на которых система уже ошибалась. Проверь разметку вручную. Отдельно отложи набор, на котором не будешь подбирать промпт: он пригодится для финального сравнения.
Todo: добавь минимум один пограничный пример и опиши правило его разметки. Не отправляй модели поле expected — это готовый ответ.
03 / Получи ответы модели
Для первого запуска подойдёт привычный чат с моделью. Отправляй каждое обращение в новом диалоге с одной и той же инструкцией; сохрани имя модели, дату и точный текст промпта. Потом этот ручной шаг можно заменить вызовом API.
Начальный промпт для эксперимента:
Классифицируй обращение пользователя.
billing — вопросы об оплате, возвратах и счетах.
technical — ошибки и неработающие функции.
other — остальные обращения.
Верни только одно слово: billing, technical или other.
Создай predictions.jsonl: одна строка на каждый ID. В label запиши фактический ответ модели, не исправляя его. Если она вернула объяснение вместо метки, сохрани всю строку: это тоже нарушение контракта.
Ниже — искусственный результат с одной намеренной ошибкой, чтобы проверить сам eval. Это не измерение качества какой-либо модели.
{"id":"01","label":"billing"}
{"id":"02","label":"technical"}
{"id":"03","label":"billing"}
{"id":"04","label":"other"}
{"id":"05","label":"billing"}
{"id":"06","label":"other"}
Скачать демонстрационный predictions.jsonl
Todo: сначала проверь инструмент на демонстрационных данных, затем замени ответы реальными. Храни результаты разных промптов в отдельных файлах.
04 / Запусти проверку
Здесь подходит точное совпадение: ответ либо равен ожидаемой метке, либо нет. Accuracy — доля совпадений. Отсутствующий ответ также считается ошибкой; дубликаты и неизвестные ID останавливают запуск, чтобы не спрятать проблему в данных.
Сохрани этот код как eval.py рядом с двумя JSONL-файлами:
import json
import sys
from pathlib import Path
LABELS = {"billing", "technical", "other"}
def read_jsonl(path):
rows = [json.loads(line) for line in
Path(path).read_text(encoding="utf-8").splitlines()
if line.strip()]
ids = [row["id"] for row in rows]
if len(ids) != len(set(ids)):
raise ValueError(f"Duplicate IDs in {path}")
return rows
def main():
cases = read_jsonl(sys.argv[1])
if not cases:
raise ValueError("Dataset is empty")
if any(c["expected"] not in LABELS for c in cases):
raise ValueError("Unknown expected label")
predictions = {
row["id"]: row.get("label")
for row in read_jsonl(sys.argv[2])
}
unknown = set(predictions) - {c["id"] for c in cases}
if unknown:
raise ValueError(f"Unknown prediction IDs: {unknown}")
passed = 0
for case in cases:
actual = predictions.get(case["id"])
ok = actual == case["expected"]
passed += ok
if not ok:
print(f"FAIL {case['id']}: "
f"expected={case['expected']!r}, got={actual!r}")
print(f"Accuracy: {passed}/{len(cases)} = "
f"{passed / len(cases):.1%}")
return 0 if passed == len(cases) else 1
if __name__ == "__main__":
raise SystemExit(main())
В терминале, из папки с файлами:
python3 eval.py cases.jsonl predictions.jsonl
Ожидаемый результат для демонстрационных ответов:
FAIL 05: expected='technical', got='billing'
Accuracy: 5/6 = 83.3%
Код завершения 1 здесь ожидаем: учебная проверка требует, чтобы прошли все примеры. Это удобное начало для автоматизации, но не универсальный порог релиза. Порог для продукта выбирают по цене конкретных ошибок.
Todo: убедись, что намеренно неправильный ответ действительно даёт FAIL. Проверяющий код тоже можно написать неверно.
05 / Разбери ошибки, а не только процент
В примере 05 встречаются и оплата, и ошибка входа. Мы заранее решили отдавать приоритет технической проблеме, но не включили это правило в стартовый промпт. Добавь его:
Если в обращении описана техническая ошибка,
выбирай technical, даже при упоминании оплаты.
Теперь заново получи ответы на весь набор и сохрани их в predictions-v2.jsonl. Не исправляй вручную единственную ошибку в старом файле: это изменит результат, но ничего не скажет о системе.
Для каждой ошибки спроси себя: неверная разметка, неполная инструкция, неподходящий класс или нестабильность модели? Когда примеров станет больше, считай результаты и по классам. Высокая общая accuracy может скрывать плохую работу с редкими обращениями.
Todo: внеси одно осмысленное изменение, сохрани обе версии промпта и сравни два запуска на одинаковых входах.
06 / Повтори эксперимент и зафиксируй результат
python3 eval.py cases.jsonl predictions.jsonl
python3 eval.py cases.jsonl predictions-v2.jsonl
Не обещай себе, что исправление обязательно даст 6/6: это нужно измерить. Даже 6/6 на маленьком учебном наборе не доказывает надёжность в реальном продукте. Повтори запуск несколько раз, чтобы увидеть разброс, а затем проверь промпт на отложенных примерах.
Сохраняй рядом с результатами версию промпта, модель и её доступные настройки, дату, версию датасета и сырые ответы. После автоматизации добавь время выполнения и стоимость. Ошибки API учитывай отдельно от неверной классификации, но не выбрасывай их молча из общей статистики.
Todo: запускай этот набор после изменения промпта или модели. Пополняй его реальными сбоями и периодически обновляй отложенный набор.
Когда этот eval станет тесным
Точное совпадение хорошо работает для наших фиксированных меток. Для свободного текста нужны другие критерии: наличие необходимых фактов, корректность ссылок или ручная оценка по ясной рубрике. Если подключишь модель-судью, сначала проверь её оценки на примерах, размеченных человеком.
Для дальнейшего чтения: Anthropic — Demystifying evals for AI agents. Там разобраны типы проверок, повторные запуски и оценка более сложных систем. Наш пример намеренно ограничен одной задачей, чтобы первый цикл «изменение → измерение → разбор» можно было пройти сразу.
Собери своего ИИ-агента и проверь результат
«AI Agents: от vibe coding к AI-команде»: desktop agents, MCP, skills, evals и harness. Отдельные воркшопы — от подключения инструментов до контроля качества.
7 практических воркшопов · записи · вопросы автору в чате
Посмотреть программу курса ↗