Перейти к содержимому
ПереводыAnthropic

Курс Anthropic: оценка промптов, часть 2 — эвалы в promptfoo (Prompt evaluations, уроки 5–9)

Перевод уроков 5–9 курса Prompt evaluations из anthropics/courses: инструмент promptfoo, эвал с точным совпадением и трансформацией ответа, классификация с contains-all, свой грейдер на Python, LLM-судья через llm-rubric и своя рубрика с оценкой по трём критериям. Конфиги даны и для локальной модели в Ollama.

Вторая часть перевода курса Prompt evaluations. В первой части эвалы писались вручную на Python; здесь тот же цикл «промпт → эвал → правка» переносится в promptfoo — открытый инструмент, который сам прогоняет промпты по тест-кейсам и моделям, проверяет ответы и показывает результаты таблицей в браузере. Последние два урока — оценка ответов другой моделью (LLM-судья): встроенной рубрикой и своей функцией на Python.

  • Когда. После первой части и урока 7.3.2. Уроки 8–9 — практика к разделу «LLM-as-judge» урока 7.3.2; там же — смещения судьи и проверка судьи по людям, о которых курс почти не говорит. Регрессионный прогон эвалов в CI — урок 8.4.2.
  • Сколько времени. Около трёх часов; урок 9 с восемью длинными статьями на локальной модели идёт долго.
  • Что понадобится. Node.js 20+ (для npx promptfoo), Python 3 (promptfoo вызывает из него наши функции), Ollama с моделями qwen3:1.7b, qwen3:8b и, для роли судьи, qwen3:14b.

Этого раздела в оригинале нет.

  1. Скачайте папки уроков. Каждый урок курса — отдельная папка с конфигом и данными:

    Окно терминала
    git clone --depth 1 https://github.com/anthropics/courses.git
    cd courses/prompt_evaluations/05_prompt_foo_code_graded_animals
  2. Запустите Ollama с длинным контекстом — промпты урока 9 содержат статьи Википедии на тысячи слов:

    Окно терминала
    OLLAMA_CONTEXT_LENGTH=32768 ollama serve
    ollama pull qwen3:1.7b && ollama pull qwen3:8b && ollama pull qwen3:14b
  3. Проверьте promptfoo: npx promptfoo@latest --version. Первая загрузка займёт минуту.

Провайдер Ollama в promptfoo записывается так (по документации promptfoo):

providers:
- id: ollama:chat:qwen3:8b
config:
think: false # Qwen3 по умолчанию «думает»; без этого размышления попадут в ответ
num_ctx: 32768 # окно контекста для этой модели
temperature: 0

Примечание: урок лежит в папке с файлами кода. Чтобы повторить эвал, скачайте папку целиком.

Мы видели, как писать эвалы с нуля, — это работает, хотя и громоздко. На практике удобнее специализированные инструменты. Их сейчас много, и новые появляются постоянно: promptfoo, Vellum, Scale Evaluation, Prompt Layer, ChainForge и многие другие.

Один из открытых и простых вариантов — promptfoo. Он даёт готовую инфраструктуру для пакетного тестирования, контроля версий и аналитики, так что разработчики занимаются промптами, а не поддержкой самописного тестового фреймворка. С ним легко прогонять эвалы по многим промптам, моделям и провайдерам и наглядно сравнивать результаты. После прогона promptfoo строит дашборд с таблицей «тест-кейсы × промпты» и сводкой процентов (в оригинале — скриншот).

Возьмём эвал «сколько ног у животного» из урока 3 и перенесём в promptfoo набор данных и три промпта. Сам эвал нарочно простой: здесь важны инструмент и процесс.

Перейдите в папку, где будет код эвала, и выполните:

Окно терминала
npx promptfoo@latest init

Команда создаст файл promptfooconfig.yaml — в нём «вся магия». В нём настраиваются:

  • провайдеры (какие модели использовать в эвалах);
  • промпты, которые мы оцениваем;
  • тесты, которые запускаем.

В поле providers указываем одну или несколько моделей. В оригинале — строки вида anthropic:messages:claude-3-haiku-20240307 (тогда поддерживались Claude 3 Haiku, Sonnet, Opus, Claude 3.5 Sonnet и старые Claude 2.x и Instant 1.2). Удалите содержимое promptfooconfig.yaml и замените его таким:

description: "Эвал: ноги животных"
providers:
- id: ollama:chat:qwen3:1.7b # в оригинале: "anthropic:messages:claude-3-haiku-20240307"
config: {think: false, temperature: 0}
  • description — необязательная подпись: что оцениваем.
  • providers — какой моделью прогонять эвал. Моделей может быть несколько — увидим в конце урока.

Для моделей Claude promptfoo ищет переменную окружения ANTHROPIC_API_KEY:

Окно терминала
export ANTHROPIC_API_KEY=your_api_key_here

Промпты можно задать разными способами: текстом прямо в YAML, из файла JSON, текстового файла, другого YAML или файла Python. Авторы предпочитают держать все промпты в одном файле Python — каждый промпт функцией, возвращающей строку. Создайте файл prompts.py:

prompts.py
# promptfoo вызывает функцию с одним аргументом — словарём context,
# переменные теста лежат в context["vars"]
def simple_prompt(context):
animal_statement = context["vars"]["animal_statement"]
return f"""Тебе дадут утверждение о животном, и твоя задача — определить, сколько у этого животного ног.
Вот утверждение о животном.
<animal_statement>{animal_statement}</animal_statement>
Сколько ног у этого животного? Ответь числом"""
def better_prompt(context):
animal_statement = context["vars"]["animal_statement"]
return f"""Тебе дадут утверждение о животном, и твоя задача — определить, сколько у этого животного ног.
Вот утверждение о животном.
<animal_statement>{animal_statement}</animal_statement>
Сколько ног у этого животного? Ответь только одним числом цифрами, например 2 или 9"""

Теперь скажем конфигу, что промпты берутся из prompts.py:

description: "Эвал: ноги животных"
prompts:
- file://prompts.py:simple_prompt # в оригинале без file:// — так тоже работало
- file://prompts.py:better_prompt
providers:
- id: ollama:chat:qwen3:1.7b
config: {think: false, temperature: 0}

Каждая функция из prompts.py — отдельная строка. Мы сказали promptfoo, что хотим оценить оба промпта.

Тесты в promptfoo задаются многими способами; начнём с самого распространённого — CSV-файла. В нём можно сразу указать логику проверки. Встроенные проверки (assertions) мы увидим в следующих уроках, а здесь нужно лишь точное совпадение ответа модели с ожидаемым числом ног. Для этого у CSV два столбца:

  • animal_statement — входное утверждение вроде «Животное — слон»;
  • __expected — ожидаемый правильный ответ (обратите внимание на два подчёркивания: это синтаксис promptfoo).

Создайте файл animal_legs_tests.csv:

animal_statement,__expected
"Животное — человек.","2"
"Животное — змея.","0"
"Лис потерял лапу, но потом чудесным образом отрастил потерянную лапу и сверх того ещё одну, загадочную.","5"
"Животное — собака.","4"
"Животное — кошка с двумя лишними лапами.","6"
"Животное — слон.","4"
"Животное — птица.","2"
"Животное — рыба.","0"
"Животное — паук с двумя лишними ногами","10"
"Животное — осьминог.","8"
"Животное — осьминог, который потерял две ноги, а потом отрастил три.","9"
"Животное — двухголовое восьминогое мифическое существо.","8"

И подключим его в конфиг:

description: "Эвал: ноги животных"
prompts:
- file://prompts.py:simple_prompt
- file://prompts.py:better_prompt
providers:
- id: ollama:chat:qwen3:1.7b
config: {think: false, temperature: 0}
tests: file://animal_legs_tests.csv

Провайдер, промпты и тесты заданы — запускаем:

Окно терминала
npx promptfoo@latest eval

Для каждого промпта promptfoo:

  • берёт каждое animal_statement из CSV;
  • собирает полный промпт с этим утверждением;
  • отправляет запрос модели;
  • проверяет, совпадает ли ответ с ожидаемым из CSV.

Результаты печатаются в терминале таблицей. У авторов (Claude 3 Haiku): левый столбец — утверждение, средний — ответы и оценки simple_prompt, который проваливает все тест-кейсы, правый — better_prompt, который проходит большинство, кроме логически сложных.

promptfoo умеет запускать в браузере дашборд для изучения результатов:

Окно терминала
npx promptfoo@latest view

Команда спросит, запустить ли сервер (ответьте y), и откроет страницу. Вверху — главная сводка (проценты по каждому промпту). Можно разобраться в конкретных результатах: нажмите на значок лупы в ячейке — откроется окно с ответом и подробностями проверки. Для simple_prompt видно, что модель ответила правильно (например, 0), но добавила лишний пояснительный текст, из-за которого провалила точное совпадение. better_prompt отвечает одной цифрой вроде 5 или 0, но ошибается на утверждениях, где нужно рассуждать, — например, про лиса, отрастившего лишнюю лапу.

В уроке о проверке кодом лучший результат дала цепочка рассуждений. Добавим в prompts.py третий промпт:

def chain_of_thought_prompt(context):
animal_statement = context["vars"]["animal_statement"]
return f"""Тебе дадут утверждение о животном, и твоя задача — определить, сколько у этого животного ног.
Вот утверждение о животном.
<animal_statement>{animal_statement}</animal_statement>
Сколько ног у этого животного?
Сначала порассуждай о количестве ног у животного, думая по шагам внутри тегов <thinking>.
Затем выведи итоговый ответ внутри тегов <answer>.
Внутри тегов <answer> напиши только количество ног целым числом и больше ничего."""

Перед запуском нужно решить одну проблему: новый промпт отвечает с тегами <thinking> и <answer>. Чтобы честно оценить его, нужно достать число из тегов <answer> и сравнить с ожидаемым. promptfoo позволяет задавать свои трансформации (transforms), которые меняют ответ модели перед проверкой. Создайте файл transform.py:

transform.py
def get_transform(output, context):
if "<thinking>" in output: # ответ промпта с цепочкой рассуждений
try:
return output.split("<answer>")[1].split("</answer>")[0].strip()
except Exception as e:
print(f"Ошибка в get_transform: {e}")
return output
return output # ответы остальных промптов не трогаем

Функция get_transform получает ответ модели (о параметре context — в следующих уроках) и может превратить его во что угодно. Здесь два случая:

  • если в ответе есть теги <thinking>, это ответ промпта с рассуждением — вытаскиваем число из <answer>;
  • иначе возвращаем ответ как есть (для промптов без рассуждения).

Осталось сказать promptfoo, что трансформацию нужно применять ко всем тестам:

description: "Эвал: ноги животных"
prompts:
- file://prompts.py:simple_prompt
- file://prompts.py:better_prompt
- file://prompts.py:chain_of_thought_prompt
providers:
- id: ollama:chat:qwen3:1.7b
config: {think: false, temperature: 0}
tests: file://animal_legs_tests.csv
defaultTest:
options:
transform: file://transform.py

Блок в конце применяет функцию из transform.py ко всем тестам; по умолчанию promptfoo ищет в файле функцию get_transform. Снова запускаем npx promptfoo@latest eval и npx promptfoo@latest view. Теперь в таблице четыре столбца, и у авторов промпт с цепочкой рассуждений ответил верно на 100 % вопросов!

promptfoo позволяет легко прогнать эвал на разных моделях. Чтобы промпт набрал 100 % на Haiku, понадобилась работа над промптом. А что будет на более сильной модели? Достаточно добавить второго провайдера:

description: "Эвал: ноги животных"
prompts:
- file://prompts.py:simple_prompt
- file://prompts.py:better_prompt
- file://prompts.py:chain_of_thought_prompt
providers:
# в оригинале: anthropic:messages:claude-3-haiku-20240307 и anthropic:messages:claude-3-5-sonnet-20240620
- id: ollama:chat:qwen3:1.7b
config: {think: false, temperature: 0}
- id: ollama:chat:qwen3:8b
config: {think: false, temperature: 0}
tests: file://animal_legs_tests.csv
defaultTest:
options:
transform: file://transform.py

Одна строка в YAML — и эвал идёт на двух моделях. В дашборде авторов первые три столбца — ответы Claude 3 Haiku, последние три — Claude 3.5 Sonnet. Sonnet прошёл эвал на 100 % даже с simple_prompt, который на Haiku набрал 0 %. Такая информация бесценна: важно не только, какой промпт лучше, но и какая связка «модель + промпт» лучше для задачи.

Замечание на полях. Почему Claude 3.5 Sonnet не набрал 100 % с цепочкой рассуждений? Он ошибся на утверждении «Животное — осьминог»: в <thinking> модель рассудила, что ног у осьминога нет — его конечности называют «руками», но не «ногами». Перейдя на «более умную» модель, мы получили чуть худший результат с этим промптом: модель оказалась «слишком умной». Чтобы все модели отвечали одинаково, можно уточнить в промпте, что считается «ногой».

В этом уроке мы лишь попробовали promptfoo. Дальше — более сложная логика проверки кодом, свои грейдеры и оценка моделью.

Примечание: урок лежит в папке с файлами кода.

В первой части мы оценивали промпты, которые относят жалобы клиентов — например, «Как только я открываю ваше приложение, телефон начинает жутко тормозить» или «Не могу понять, как сменить пароль» — к пяти категориям: Ошибка ПО, Неисправность оборудования, Ошибка пользователя, Сбой сервиса, Запрос функции. Перенесём этот эвал в promptfoo.

Как и раньше, начинаем с npx promptfoo@latest init и очищаем promptfooconfig.yaml. Провайдер — маленькая модель: эвал мы прогоним несколько раз (в оригинале по той же причине выбран дешёвый Claude 3 Haiku):

description: "Эвал: классификация жалоб"
providers:
- id: ollama:chat:qwen3:1.7b # в оригинале: "anthropic:messages:claude-3-haiku-20240307"
config: {think: false, temperature: 0}

Как в прошлом уроке: каждый промпт — функция Python, возвращающая строку, все функции — в prompts.py. Это те же два промпта, что в уроке 4:

prompts.py
def basic_prompt(context):
complaint = context["vars"]["complaint"]
return f"""
Отнеси следующую жалобу клиента к одной или нескольким из этих категорий:
Ошибка ПО, Неисправность оборудования, Ошибка пользователя, Запрос функции, Сбой сервиса.
Ответь только категорией.
Жалоба: {complaint}
Категория:
"""
def improved_prompt(context):
complaint = context["vars"]["complaint"]
return f"""
Ты — ИИ-ассистент, специализирующийся на классификации обращений в службу поддержки. Твоя задача — анализировать жалобы клиентов и относить их к одной или нескольким из следующих категорий:
1. Ошибка ПО: программа работает не так, как задумано.
2. Неисправность оборудования: проблемы с физическими устройствами или компонентами.
3. Ошибка пользователя: трудности из-за того, что пользователь не понял или неправильно использовал продукт.
4. Запрос функции: предложения новых возможностей или улучшений.
5. Сбой сервиса: общесистемные проблемы, влияющие на доступность сервиса.
Важные правила:
- Жалоба может относиться к нескольким категориям. Тогда перечисли все подходящие, но по возможности старайся выбрать одну.
Примеры:
1. Жалоба: «Приложение вылетает, когда я пытаюсь сохранить прогресс».
Категория: Ошибка ПО
2. Жалоба: «Клавиатура не работает после того, как я пролил на неё кофе».
Категория: Неисправность оборудования
3. Жалоба: «Не могу найти на вашем сайте кнопку входа».
Категория: Ошибка пользователя
4. Жалоба: «Было бы здорово, если бы в вашем приложении была тёмная тема».
Категория: Запрос функции
5. Жалоба: «Ни один ваш сервис не загружается ни у меня, ни у коллег».
Категория: Сбой сервиса
6. Жалоба: «Приложение ломается каждый раз, когда я пытаюсь сменить фото профиля».
Категория: Ошибка ПО
7. Жалоба: «Приложение глючит у меня на телефоне, а ваш сайт, кажется, лежит, так что я совсем застрял!»
Категория: Ошибка ПО, Сбой сервиса
8. Жалоба: «Ваша программа делает мой компьютер жутко медленным, ненавижу её!»
Категория: Ошибка ПО
9. Жалоба: «Ваше дурацкое приложение всегда ломается, когда я пытаюсь что-то сделать с картинками».
Категория: Ошибка ПО
Теперь, пожалуйста, классифицируй следующую жалобу клиента:
<complaint>{complaint}</complaint>
Ответь только подходящими категориями и больше ничего не пиши.
Категория:
"""

Подключаем промпты в конфиг:

description: "Эвал: классификация жалоб"
prompts:
- file://prompts.py:basic_prompt
- file://prompts.py:improved_prompt
providers:
- id: ollama:chat:qwen3:1.7b
config: {think: false, temperature: 0}

Остаётся привести набор данных из урока 4 к виду, удобному для promptfoo. Главное отличие от прошлого урока: проверка — уже не точное совпадение. Нужно, чтобы ответ модели содержал правильную категорию (или категории). Например, для жалобы «Облачное хранилище не синхронизируется, и я не вижу свои файлы с других устройств» ответ должен содержать и «Ошибка ПО», и «Сбой сервиса» — а модель может назвать их в любом порядке.

К счастью, в promptfoo много встроенных проверок, например:

  • contains — ответ содержит подстроку;
  • contains-all — ответ содержит все подстроки из списка;
  • contains-any — ответ содержит хотя бы одну из подстрок;
  • contains-json — ответ содержит корректный JSON (с необязательной проверкой по JSON Schema);
  • contains-sql — ответ содержит корректный SQL;
  • contains-xml — ответ содержит корректный XML;
  • equals — ответ совпадает в точности;
  • icontains, icontains-all, icontains-any — то же без учёта регистра;
  • regex — ответ соответствует регулярному выражению;
  • и многие другие (полный список встроенных проверок).

Возьмём contains-all: ответ должен содержать все нужные категории. В столбце __expected CSV можно указать любую встроенную проверку. Создайте dataset.csv:

complaint,__expected
"Приложение вылетает каждый раз, когда я пытаюсь загрузить фото","contains-all:Ошибка ПО"
"Компьютер не видит мой принтер","contains-all:Неисправность оборудования"
"Не могу понять, как сменить пароль","contains-all:Ошибка пользователя"
"Сайт полностью лёг, не открывается ни одна страница","contains-all:Сбой сервиса"
"Было бы здорово, если бы в приложении появилась тёмная тема","contains-all:Запрос функции"
"Программа зависает, когда я пытаюсь сохранить большие файлы","contains-all:Ошибка ПО"
"Беспроводная мышь не работает даже с новыми батарейками","contains-all:Неисправность оборудования"
"Я случайно удалил важные файлы, поможете восстановить?","contains-all:Ошибка пользователя"
"Ни один ваш сервер не отвечает, у вас авария?","contains-all:Сбой сервиса"
"Можно добавить экспорт данных в CSV?","contains-all:Запрос функции"
"Приложение вылетает, а телефон перегревается","contains-all:Ошибка ПО,Неисправность оборудования"
"Я не помню свой пароль!","contains-all:Ошибка пользователя"
"Новое обновление что-то сломало, и приложение у меня больше не работает","contains-all:Ошибка ПО"
"Кажется, я что-то неправильно установил, и теперь компьютер вообще не включается","contains-all:Ошибка пользователя,Неисправность оборудования"
"Ваш сервис лежит, а мне срочно нужна функция пакетной обработки файлов","contains-all:Сбой сервиса,Запрос функции"
"Видеокарта издаёт странные звуки","contains-all:Неисправность оборудования"
"Клавиатура ни с того ни с сего совсем перестала работать","contains-all:Неисправность оборудования"
"Как только я открываю ваше приложение, телефон начинает жутко тормозить","contains-all:Ошибка ПО"
"Можно сделать интерфейс удобнее? Я вечно теряюсь в меню","contains-all:Запрос функции,Ошибка пользователя"
"Облачное хранилище не синхронизируется, и я не вижу свои файлы с других устройств","contains-all:Ошибка ПО,Сбой сервиса"

В CSV два столбца: complaint — сама жалоба, __expected — проверка contains-all. Например, строка про сервис и пакетную обработку требует, чтобы ответ модели содержал и «Сбой сервиса», и «Запрос функции».

Последний шаг — подключить тесты в promptfooconfig.yaml:

description: "Эвал: классификация жалоб"
prompts:
- file://prompts.py:basic_prompt
- file://prompts.py:improved_prompt
providers:
- id: ollama:chat:qwen3:1.7b
config: {think: false, temperature: 0}
tests: file://dataset.csv
Окно терминала
npx promptfoo@latest eval
npx promptfoo@latest view

У авторов улучшенный промпт с примерами заметно лучше простого: примеры помогают модели понять, в каких ситуациях нужно назначать несколько категорий. В веб-просмотре basic_prompt набрал 80 %, improved_prompt — 100 %.

Как всегда, помните: учебные наборы данных маленькие и не отражают реальных эвалов. Авторы всегда советуют не меньше 100 строк.

Примечание: урок лежит в папке с файлами кода.

Мы видели встроенные грейдеры promptfoo вроде точного совпадения и contains-all. Они часто полезны, но promptfoo позволяет писать и свою логику проверки для более специфичных задач. Для демонстрации возьмём очень простой шаблон промпта:

Напиши короткий абзац про {{topic}}. Упомяни слово «{{topic}}» ровно {{count}} раз, не больше и не меньше.

{{topic}} и {{count}} заполняются значениями вроде «пальто» и 7. Чтобы проверить ответ, нужна своя логика: в ответе слово «пальто» должно встретиться ровно 7 раз.

Как всегда, npx promptfoo@latest init и очищаем конфиг. Провайдеров два — сравним, как маленькая и большая модели справятся с задачей:

description: Подсчёт упоминаний
providers:
# в оригинале: anthropic:messages:claude-3-haiku-20240307 и anthropic:messages:claude-3-5-sonnet-20240620
- id: ollama:chat:qwen3:1.7b
config: {think: false}
- id: ollama:chat:qwen3:8b
config: {think: false}

Мы писали промпты функциями в файле Python — авторы рекомендуют этот способ, но есть и другие. Самый простой — написать промпт текстом прямо в YAML:

description: Подсчёт упоминаний
prompts:
- >-
Напиши короткий абзац про {{topic}}. Упомяни слово «{{topic}}» ровно {{count}} раз, не больше и не меньше. Пиши только строчными буквами.
providers:
- id: ollama:chat:qwen3:1.7b
config: {think: false}
- id: ollama:chat:qwen3:8b
config: {think: false}

Обратите внимание на переменные {{topic}} и {{count}} в двойных фигурных скобках: это синтаксис шаблонов Nunjucks, он скоро пригодится.

Раньше тесты и логика проверки лежали в CSV. Но их можно описать и прямо в конфиге:

description: Подсчёт упоминаний
prompts:
- >-
Напиши короткий абзац про {{topic}}. Упомяни слово «{{topic}}» ровно {{count}} раз, не больше и не меньше. Пиши только строчными буквами.
providers:
- id: ollama:chat:qwen3:1.7b
config: {think: false}
- id: ollama:chat:qwen3:8b
config: {think: false}
tests:
- vars:
topic: кенгуру
count: 3
- vars:
topic: какаду
count: 2
- vars:
topic: метро
count: 4
- vars:
topic: пальто
count: 7
- vars:
topic: такси
count: 6

Внизу — 5 тест-кейсов, у каждого свои topic и count. promptfoo прогонит каждый тест, подставив значения в шаблон. Логики проверки пока нет, но эвал уже можно запустить, чтобы убедиться, что переменные подставляются верно: npx promptfoo@latest eval. В каждой строке — абзацы на заданную тему. Осталось реализовать проверку числа упоминаний.

promptfoo позволяет определять свои функции-грейдеры на Python. Создайте файл count.py:

count.py
import re
def get_assert(output, context):
topic = context["vars"]["topic"]
goal_count = int(context["vars"]["count"])
# слово целиком: в начале текста или после пробела
pattern = fr'(^|\s)\b{re.escape(topic)}\b'
actual_count = len(re.findall(pattern, output.lower()))
pass_result = goal_count == actual_count
result = {
"pass": pass_result,
"score": 1 if pass_result else 0,
"reason": f"Ожидалось, что «{topic}» встретится {goal_count} раз. На деле: {actual_count}",
}
return result

promptfoo сам ищет в файле функцию get_assert и передаёт ей два аргумента:

  • ответ модели;
  • словарь context с переменными и промптом, который породил ответ.

Функция может вернуть:

  • bool (прошёл / не прошёл);
  • float (оценку);
  • словарь GradingResult.

Мы возвращаем словарь GradingResult с ключами pass (булево значение), score (число) и reason (строка с объяснением). Функция достаёт из context тему и число, регулярным выражением считает вхождения темы в ответ и возвращает результат.

Подключаем грейдер в конфиг:

description: Подсчёт упоминаний
prompts:
- >-
Напиши короткий абзац про {{topic}}. Упомяни слово «{{topic}}» ровно {{count}} раз, не больше и не меньше. Пиши только строчными буквами.
providers:
- id: ollama:chat:qwen3:1.7b
config: {think: false}
- id: ollama:chat:qwen3:8b
config: {think: false}
defaultTest:
assert:
- type: python
value: file://count.py
tests:
- vars: {topic: кенгуру, count: 3}
- vars: {topic: какаду, count: 2}
- vars: {topic: метро, count: 4}
- vars: {topic: пальто, count: 7}
- vars: {topic: такси, count: 6}

defaultTest говорит promptfoo, что к каждому тесту применяется грейдер из count.py.

npx promptfoo@latest eval, затем npx promptfoo@latest view. У авторов Claude 3.5 Sonnet набрал 100 %, а Claude 3 Haiku — 20 %. Чтобы убедиться, нажмите на лупу и посмотрите на промпт и ответ: у Haiku в абзаце про пинцет (tweezers) слово встречается не 7 раз, у Sonnet — ровно 7.

Этот эвал немного нелеп, но он показывает, как определить свою логику проверки на Python. Встроенные проверки promptfoo и свои функции-грейдеры вместе позволяют написать практически любой эвал с проверкой кодом.

Примечание: урок лежит в папке с файлами кода.

До сих пор мы писали только эвалы с проверкой кодом. Там, где это возможно, они самые простые и дешёвые: дают ясную объективную оценку по заранее заданным критериям и идеальны для задач с чёткими измеримыми результатами. Беда в том, что код может проверить лишь некоторые ответы — те, что сводятся к точному совпадению, сравнению чисел или другой программной логике.

Но многие реальные применения языковых моделей требуют более тонкой оценки. Допустим, мы делаем чат-бота для средней школы. Нужно проверить, что ответы написаны языком по возрасту, выдерживают учебный тон, не отвечают на неучебные вопросы и объясняют на уровне, понятном школьникам. Эти критерии субъективны и зависят от контекста, их трудно проверить кодом. Здесь помогает оценка моделью!

Оценка моделью использует способности языковых моделей, чтобы проверять ответы по сложным, тонким критериям. Беря другую модель в качестве оценщика, мы получаем то же понимание языка и контекста, которое породило исходный ответ. Так можно строить более сложные метрики: тон, уместность, соответствие ситуации и даже креативность — то, что обычно недоступно проверке кодом.

Главная идея: сама оценка — это задача обработки естественного языка. Модели-оценщику дают некоторое сочетание:

  • исходного промпта или вопроса;
  • ответа модели, который нужно оценить;
  • набора критериев или правил оценки;
  • инструкций, как оценивать и ставить балл.

Так оценка становится целостной: учитывает не только фактическую точность, но и стиль, следование правилам и общее качество ответа в его контексте. Типичные вопросы к модели-оценщику:

  • Насколько этот ответ извиняющийся?
  • Фактически ли точен ответ с учётом предоставленного контекста?
  • Не слишком ли ответ упоминает свой контекст и информацию?
  • Действительно ли ответ отвечает на вопрос как следует?
  • Насколько ответ соответствует нашим правилам тона, бренда и стиля?

Как и всё в promptfoo, оценку моделью можно сделать по-разному. Здесь — самый простой способ: встроенная проверка. В следующем уроке напишем свою функцию-оценщик.

Встроенная проверка llm-rubric — универсальный грейдер promptfoo для подхода «LLM как судья». Достаточно добавить в promptfooconfig.yaml:

assert:
- type: llm-rubric
# модель-оценщик (в оригинале: anthropic:messages:claude-3-opus-20240229)
provider:
id: ollama:chat:qwen3:14b
config: {think: false}
# критерий, по которому оценивается ответ
value: Не извиняется

Эта проверка попросит модель-оценщика решить, извиняется ли ответ.

Оценим промпты для учебного помощника школьников средних классов. Чат-бот должен отвечать на вопросы по школьным предметам, но не отвечать на посторонние. Начнём с простого промпта:

Ты — интерактивный помощник-репетитор для учеников средней школы.
Ученики задают тебе вопросы, а твоя задача — отвечать объяснениями, понятными ученикам средней школы.
Отвечай только на вопросы, связанные со школьной учёбой.
Вот вопрос ученика: {{question}}

Напишем эвал с оценкой моделью, чтобы проверить, что помощник отказывается отвечать на вопросы вроде «Как уговорить родителей купить мне телефон?» или «Какие есть весёлые бесплатные видеоигры?». Теоретически можно проверять кодом: искать регулярными выражениями фразы отказа вроде «Извините» или «Не могу помочь». Но это очень хрупкий подход; оценка моделью работает гораздо лучше.

Как всегда, в новой папке выполняем npx promptfoo@latest init. Для простоты промпты и тесты пишем прямо в promptfooconfig.yaml:

description: "Эвал: школьный помощник"
prompts:
- >-
Ты — интерактивный помощник-репетитор для учеников средней школы.
Ученики задают тебе вопросы, а твоя задача — отвечать объяснениями, понятными ученикам средней школы.
Отвечай только на вопросы, связанные со школьной учёбой.
Вот вопрос ученика: {{question}}
providers:
- id: ollama:chat:qwen3:8b # в оригинале: anthropic:messages:claude-3-5-sonnet-20240620
config: {think: false}
defaultTest:
assert:
- type: llm-rubric
provider: # в оригинале: anthropic:messages:claude-3-opus-20240229
id: ollama:chat:qwen3:14b
config: {think: false}
value: 'Отказывается отвечать на вопрос и вместо этого переводит разговор на учебные темы'
tests:
- vars:
question: Какая лучшая бесплатная мобильная игра?
- vars:
question: Как уговорить родителей разрешить мне ложиться спать позже?
- vars:
question: Какие есть весёлые идеи для темы пижамной вечеринки?
- vars:
question: Какие есть хорошие идеи для розыгрышей, за которые мне не влетит?
- vars:
question: Какие известные футболисты-левши ты знаешь?

Что здесь есть:

  • prompts — простой исходный промпт, который мы оцениваем. Обычно промпты лежат в файле Python, но можно писать их прямо в конфиге;
  • providers — модель, которая отвечает на вопросы;
  • defaultTest — каждый тест оценивается проверкой llm-rubric: ответ должен отказывать и переводить разговор на учебные темы. Оценивает модель посильнее (в оригинале — Claude 3 Opus при отвечающей Claude 3.5 Sonnet);
  • tests — набор тестов. Обычно их держат в CSV, но можно и прямо в YAML. Все тесты содержат вопросы, на которые модель должна отказаться отвечать.

Запускаем npx promptfoo@latest eval. У авторов промпт справился с большинством примеров (хотя набор очень мал), но охотно ответил на вопрос о футболистах. В веб-просмотре видно и ответ модели, и обоснование модели-оценщика.

Добавим второй, более подробный промпт — с перечнем тем, которые модель может обсуждать:

prompts:
- >-
Ты — интерактивный помощник-репетитор для учеников средней школы.
Ученики задают тебе вопросы, а твоя задача — отвечать объяснениями, понятными ученикам средней школы.
Отвечай только на вопросы, связанные со школьной учёбой.
Вот вопрос ученика: {{question}}
- >-
Ты — интерактивный помощник-репетитор для учеников средней школы.
Ученики задают тебе вопросы, а твоя задача — отвечать объяснениями, понятными ученикам средней школы.
Отвечай только на вопросы, связанные со школьной учёбой.
Допустимые темы, на которые нужно отвечать: математика, чтение, естественные науки, иностранные языки, обществознание и искусство.
Отказывайся отвечать на вопросы, не связанные с этими темами в учебном контексте.
Вот вопрос ученика: {{question}}

Остальная часть конфига прежняя. Запускаем снова: у авторов изменения во втором промпте помогли! Учтите, что набор слишком мал для реального использования.

Присмотревшись к ответам, замечаем: большинство начинается с извинений вроде «Извини» или «Прошу прощения». Для пользователей это не лучший опыт, и мы решили это исправить. Оценим третий промпт:

Ты — интерактивный помощник-репетитор для учеников средней школы.
Ученики задают тебе вопросы, а твоя задача — отвечать объяснениями, понятными ученикам средней школы.
Отвечай только на вопросы, связанные со школьной учёбой.
Допустимые темы, на которые нужно отвечать: математика, чтение, естественные науки, иностранные языки, обществознание и искусство.
Отказывайся отвечать на вопросы, не связанные с этими темами в учебном контексте.
Не извиняйся и не используй извиняющийся тон при отказе. Вместо этого мягко подталкивай ученика к школьным темам.
Вот вопрос ученика: {{question}}

Промпт прямо запрещает извиняться и просит мягко направлять учеников к учебным темам. Добавим вторую проверку llm-rubric — на извиняющийся тон. Полный конфиг:

description: "Эвал: школьный помощник"
prompts:
- >-
Ты — интерактивный помощник-репетитор для учеников средней школы.
Ученики задают тебе вопросы, а твоя задача — отвечать объяснениями, понятными ученикам средней школы.
Отвечай только на вопросы, связанные со школьной учёбой.
Вот вопрос ученика: {{question}}
- >-
Ты — интерактивный помощник-репетитор для учеников средней школы.
Ученики задают тебе вопросы, а твоя задача — отвечать объяснениями, понятными ученикам средней школы.
Отвечай только на вопросы, связанные со школьной учёбой.
Допустимые темы, на которые нужно отвечать: математика, чтение, естественные науки, иностранные языки, обществознание и искусство.
Отказывайся отвечать на вопросы, не связанные с этими темами в учебном контексте.
Вот вопрос ученика: {{question}}
- >-
Ты — интерактивный помощник-репетитор для учеников средней школы.
Ученики задают тебе вопросы, а твоя задача — отвечать объяснениями, понятными ученикам средней школы.
Отвечай только на вопросы, связанные со школьной учёбой.
Допустимые темы, на которые нужно отвечать: математика, чтение, естественные науки, иностранные языки, обществознание и искусство.
Отказывайся отвечать на вопросы, не связанные с этими темами в учебном контексте.
Не извиняйся и не используй извиняющийся тон при отказе. Вместо этого мягко подталкивай ученика к школьным темам.
Вот вопрос ученика: {{question}}
providers:
- id: ollama:chat:qwen3:8b
config: {think: false}
defaultTest:
assert:
- type: llm-rubric
provider:
id: ollama:chat:qwen3:14b
config: {think: false}
value: 'Отказывается отвечать на вопрос и вместо этого переводит разговор на учебные темы'
- type: llm-rubric
provider:
id: ollama:chat:qwen3:14b
config: {think: false}
value: 'Не извиняется'
tests:
- vars:
question: Какая лучшая бесплатная мобильная игра?
- vars:
question: Как уговорить родителей разрешить мне ложиться спать позже?
- vars:
question: Какие есть весёлые идеи для темы пижамной вечеринки?
- vars:
question: Какие есть хорошие идеи для розыгрышей, за которые мне не влетит?
- vars:
question: Какие известные футболисты-левши ты знаешь?

Теперь у нас три промпта, и каждый ответ модель оценивает по двум критериям: модель должна отказать и не должна извиняться. Запускаем npx promptfoo@latest eval: как и ожидалось, первые два промпта у авторов проваливают проверку на извинения, а третий проходит!

В веб-просмотре (npx promptfoo@latest view) нажмите на лупу у второго ответа в первой строке: ответ проходит первую проверку (отказывает отвечать на постороннее), но проваливает вторую, потому что, по словам оценщика, «ответ начинается с „Извини“ — это извиняющаяся фраза». А третий ответ в той же строке проходит обе проверки!

Помните: этот набор данных слишком мал для реалистичной оценки.

Встроенные проверки с оценкой моделью очень полезны, но иногда нужно больше контроля над метриками и процессом. В следующем уроке напишем свою функцию-оценщик.

Примечание: урок лежит в папке с файлами кода.

Цель промпта простая: превращать длинные, технически сложные статьи Википедии в короткие пересказы для младших школьников. Например, по всей статье Википедии о свёрточных нейросетях мы хотим получить примерно такой пересказ:

Свёрточные нейронные сети, или CNN, — это особые компьютерные программы, которые умеют учиться узнавать картинки и узоры. Они работают немного похоже на человеческий мозг: используют слои искусственных «нейронов», которые обрабатывают информацию. CNN очень хорошо справляются с задачами вроде поиска предметов на фотографиях или узнавания лиц. Для этого они разбивают картинку на маленькие кусочки и ищут в них важные признаки — немного похоже на сборку пазла. Особенность CNN в том, что они сами учатся находить эти признаки, разглядывая много примеров. Поэтому они становятся всё лучше и иногда даже не уступают людям. Учёные и инженеры используют CNN для самых разных интересных задач: помогают беспилотным машинам видеть дорогу, ищут новые лекарства и даже учат компьютеры играть в шахматы и го.

Чтобы оценить промпты, напишем свою проверку с оценкой моделью по трём метрикам:

  • краткость (1–5): насколько пересказ сжат?
  • точность (1–5): полностью ли пересказ верен по отношению к исходной статье?
  • тон (1–5): подходит ли пересказ младшему школьнику без технической подготовки?

Каждая метрика даёт оценку от 1 до 5. Мы усредним их и будем добиваться средней оценки не ниже 4,5 из 5. Для этого напишем свою функцию-оценщик.

В папке урока лежит каталог articles с восемью текстовыми файлами: в каждом — текст статьи Википедии (на английском: большие языковые модели, эмбеддинги слов, обратное распространение ошибки, свёрточные сети, распознавание рукописного текста, GPU, IBM Watson, обработка естественного языка). Эти статьи и будут входами эвала. Загляните в пару файлов, чтобы понять, насколько они длинные и сложные.

Восемь тест-кейсов — слишком мало для реального эвала. Как много раз говорилось в курсе, авторы настоятельно советуют наборы хотя бы из 100 примеров.

Файл prompts.py содержит три функции-промпта, которые мы оценим:

prompts.py
def basic_summarize(context):
article = context["vars"]["article"]
return f"Перескажи эту статью {article}"
def better_summarize(context):
article = context["vars"]["article"]
return f"""
Перескажи эту статью для младших школьников: {article}"""
def best_summarize(context):
article = context["vars"]["article"]
return f"""
Твоя задача — пересказывать длинные статьи Википедии для младших школьников.
Напиши короткий пересказ, как можно более сжатый.
Пересказ предназначен для младших школьников без технической подготовки.
Вот статья: {article}"""

Важно: все эти промпты, в общем, посредственные. Авторы нарочно сделали их короткими и не следовали лучшим практикам (например, не добавили подробных примеров), чтобы эвал тратил меньше токенов.

Поля в promptfooconfig.yaml нам в основном уже знакомы:

description: 'Эвал: пересказы статей'
prompts:
- file://prompts.py:basic_summarize
- file://prompts.py:better_summarize
- file://prompts.py:best_summarize
providers:
- id: ollama:chat:qwen3:8b # в оригинале: anthropic:messages:claude-3-5-sonnet-20240620
label: "Qwen3 8B" # в оригинале: "3.5 Sonnet"
config: {think: false, num_ctx: 32768}
tests:
- vars:
article: file://articles/article1.txt
- vars:
article: file://articles/article2.txt
- vars:
article: file://articles/article3.txt
- vars:
article: file://articles/article4.txt
- vars:
article: file://articles/article5.txt
- vars:
article: file://articles/article6.txt
- vars:
article: file://articles/article7.txt
- vars:
article: file://articles/article8.txt
defaultTest:
assert:
- type: python
value: file://custom_llm_eval.py

Мы используем три промпта из prompts.py и одну модель. Новое здесь одно: значения переменных загружаются из текстовых файлов. Статьи слишком длинные, чтобы писать их в YAML, поэтому запись

tests:
- vars:
article: file://articles/article1.txt

означает: тест, где переменная article равна содержимому файла article1.txt. И так для всех восьми файлов.

Последнее поле конфига:

defaultTest:
assert:
- type: python
value: file://custom_llm_eval.py

говорит, что к каждому тесту нужно применить проверку на Python из файла custom_llm_eval.py. Этот синтаксис мы видели для своего грейдера в уроке 7; разница лишь в том, что теперь функция оценивает ответ с помощью другой модели. Вот файл целиком (с исправлениями — о них ниже):

custom_llm_eval.py
import json
import os
import re
import anthropic
# Модель-оценщик: Claude, если есть ключ, иначе локальная модель через Anthropic-совместимый API Ollama
if os.getenv("ANTHROPIC_API_KEY"):
client = anthropic.Anthropic()
GRADER_MODEL = os.getenv("GRADER_MODEL", "claude-sonnet-4-6") # в оригинале: claude-3-5-sonnet-20240620
EXTRA = {}
else:
client = anthropic.Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL", "http://localhost:11434"), api_key="ollama")
GRADER_MODEL = os.getenv("GRADER_MODEL", "qwen3:14b")
EXTRA = {"thinking": {"type": "disabled"}} # без размышлений: нам нужен только JSON
def llm_eval(summary, article):
"""
Оценивает пересказ с помощью LLM.
Аргументы:
summary (str): пересказ, который нужно оценить.
article (str): исходный текст, который пересказывали.
Возвращает:
(float, str): среднюю оценку по трём критериям и полный ответ модели-оценщика.
"""
prompt = f"""Оцени следующий пересказ по таким критериям:
1. Краткость (1–5) — насколько пересказ сжат?
- Краткость 1: пересказ неоправданно длинный, в нём лишние подробности, повторы или посторонняя информация. Он не выделяет главное.
- Краткость 3: пересказ передаёт большую часть главного, но мог бы быть сфокусированнее. В нём могут быть лишние подробности или чрезмерные объяснения.
- Краткость 5: пересказ сжато передаёт главные идеи. В нём есть всё существенное и нет лишних подробностей и объяснений.
2. Точность (1–5) — полностью ли пересказ верен по отношению к исходной статье?
- Точность 1: в пересказе серьёзные ошибки, искажения или пропуски, которые меняют смысл или главные идеи статьи.
- Точность 3: часть главного передана верно, но есть мелкие неточности или пропуски. Общий смысл в целом верен, но некоторые детали неправильны.
- Точность 5: пересказ точно передаёт суть статьи без ошибок и искажений. Вся включённая информация верна и соответствует источнику.
3. Тон (1–5) — подходит ли пересказ младшему школьнику без технической подготовки?
- Тон 1: язык или понятия слишком сложные, технические или взрослые для младших школьников. Есть жаргон, специальные термины или неподходящие для детей темы.
- Тон 3: язык в основном подходит младшим школьникам, но иногда встречаются трудные слова или понятия. Для полного понимания нужны пояснения.
- Тон 5: пересказ последовательно написан простым, ясным языком, понятным младшим школьникам. Сложные идеи объяснены доступно и интересно.
4. Объяснение — общее описание того, как оценён пересказ
<examples>
<example>
Этот пересказ:
<summary>
Искусственные нейронные сети — это компьютерные системы, придуманные по образцу человеческого мозга. Они состоят из связанных между собой «нейронов», которые обрабатывают информацию. Такие сети учатся выполнять задачи, разглядывая много примеров, — похоже на то, как учатся люди.
Главное о нейросетях:
- они умеют находить закономерности и делать предсказания;
- они становятся лучше, когда получают больше данных и практики;
- их используют, чтобы узнавать предметы на картинках, переводить с языка на язык и играть в игры.
Нейросети — мощный инструмент искусственного интеллекта, на них работают многие «умные» технологии, которыми мы пользуемся. Они умеют удивительные вещи, но всё же не так сложны и способны, как человеческий мозг.
</summary>
Должен получить 5 за тон, 5 за точность и 5 за краткость
</example>
<example>
Этот пересказ:
<summary>
Вот краткое изложение главных положений статьи об искусственных нейронных сетях (ИНС):
1. ИНС — вычислительные модели, вдохновлённые биологическими нейронными сетями мозга животных. Они состоят из связанных искусственных нейронов, которые обрабатывают и передают сигналы.
2. Базовая структура:
- входной слой получает данные
- скрытые слои обрабатывают информацию
- выходной слой выдаёт результат
- нейроны соединены рёбрами с весами
3. Процесс обучения:
- ИНС учатся, настраивая веса связей
- используют методы вроде обратного распространения ошибки для минимизации ошибок
- могут обучаться с учителем, без учителя и с подкреплением
4. Ключевые разработки:
- свёрточные нейронные сети (CNN) для обработки изображений
- рекуррентные нейронные сети (RNN) для последовательных данных
- глубокое обучение с множеством скрытых слоёв
5. Применения:
- распознавание образов, классификация, регрессия
- компьютерное зрение, распознавание речи, обработка естественного языка
- игры, робототехника, финансовое моделирование
6. Преимущества:
- моделируют сложные нелинейные зависимости
- учатся на данных и обобщают
- подходят для множества разных задач
7. Трудности:
- требуют больших объёмов обучающих данных
- бывают вычислительно затратными
- природа «чёрного ящика» затрудняет интерпретацию
8. Последние достижения:
- улучшенное оборудование (GPU) позволяет строить более глубокие сети
- новые архитектуры вроде трансформеров для языковых задач
- прогресс в генеративном ИИ
Статья даёт всесторонний обзор понятий, истории, видов, применений и текущих исследований ИНС в области искусственного интеллекта и машинного обучения.
</summary>
Должен получить 1 за тон, 5 за точность и 3 за краткость
</example>
</examples>
Дай оценку по каждому критерию в формате JSON. Всегда следуй этому формату:
<json>
{{
"conciseness": <число>,
"accuracy": <число>,
"tone": <число>,
"explanation": <строка>
}}
</json>
Исходный текст: <original_article>{article}</original_article>
Пересказ для оценки: <summary>{summary}</summary>
"""
response = client.messages.create(
model=GRADER_MODEL,
max_tokens=1000,
temperature=0,
messages=[{"role": "user", "content": prompt}],
**EXTRA,
)
text = response.content[0].text
# Вырезаем JSON между тегами <json> (в оригинале здесь был prefill "<json>" и стоп-последовательность "</json>")
match = re.search(r"<json>\s*(.*?)\s*</json>", text, re.DOTALL)
evaluation = json.loads(match.group(1) if match else text)
# Отбрасываем нечисловые значения и считаем среднее
numeric_values = [value for key, value in evaluation.items() if isinstance(value, (int, float))]
avg_score = sum(numeric_values) / len(numeric_values)
# Возвращаем среднюю оценку и весь ответ модели-оценщика
return avg_score, text
def get_assert(output: str, context, threshold=4.5):
# Берём из контекста статью этого теста
article = context['vars']['article']
# Передаём ответ модели и статью в llm_eval, получаем оценку и объяснение
score, evaluation = llm_eval(output, article)
# Возвращаем: прошёл ли тест, его оценку и объяснение
return {
"pass": score >= threshold,
"score": score,
"reason": evaluation
}

Начнём с функции в конце файла. Как мы помним, promptfoo сам ищет в файле проверки функцию get_assert и передаёт ей ответ модели (output) и словарь context с переменными и промптом. Функция может вернуть bool, float или словарь GradingResult; мы возвращаем словарь с ключами pass (булево), score (число) и reason (объяснение). Функция достаёт из контекста статью, передаёт ответ модели и статью в llm_eval, получает среднюю оценку и объяснение и возвращает словарь: прошёл ли тест порог 4,5, оценку и объяснение.

Функция, которая выполняет саму оценку:

  1. задаёт очень длинный промпт-рубрику, объясняющий, как оценивать пересказы;
  2. отправляет промпт модели-оценщику;
  3. разбирает ответ и считает среднюю оценку;
  4. возвращает среднюю оценку и полный текст ответа модели.
Окно терминала
npx promptfoo@latest eval

Эвал может идти долго: сначала модель пишет пересказы, потом каждый пересказ отправляется на оценку. Затем открываем веб-просмотр: npx promptfoo@latest view. Нажав на лупу в ячейке, увидите подробности: например, ответ, который провалил проверку из-за очень низкой оценки за тон. В верхней строке — сводка оценок по каждому промпту: best_summarize, что неудивительно, работает лучше всех. Вверху дашборда есть и графики распределения оценок: у авторов красный — basic_summarize, синий — better_summarize, зелёный — best_summarize. Видно, что best_summarize не только ни разу не провалил тест, но и обошёл остальные промпты на всех входах.

  1. Повторите урок 5 на трёх моделях (qwen3:0.6b, qwen3:1.7b, qwen3:8b) и постройте таблицу «промпт × модель». Где проходит граница, после которой хватает простого промпта?
  2. Сравните проверки урока 6: добавьте в CSV второй столбец проверки или поменяйте contains-all на функцию на Python, которая сравнивает множества категорий, как в уроке 4. На сколько процентов меняется оценка тех же ответов?
  3. Почините грейдер урока 7: перепишите шаблон так, чтобы слово в кавычках-ёлочках тоже засчитывалось (например, (?<!\w)…(?!\w)), и посмотрите, изменились ли вердикты.
  4. Проверьте судью урока 8: проставьте свои вердикты «отказал / не отказал» и «извиняется / нет» для 15 ответов и посчитайте долю совпадений с llm-rubric. Повторите с судьёй qwen3:8b вместо qwen3:14b.
  5. Эвал в CI. Добавьте в свой проект с промптом команду npx promptfoo@latest eval -c promptfooconfig.yaml в GitHub Actions по уроку 8.4.2.
EnglishРусский
providerпровайдер (модель и API, через которые идёт запрос)
assertionпроверка (условие на ответ модели)
__expectedстолбец ожидаемого ответа или проверки в CSV promptfoo
transformтрансформация (обработка ответа перед проверкой)
custom graderсвой грейдер (функция проверки)
GradingResultрезультат проверки: pass, score, reason
model-graded evalэвал с оценкой моделью
LLM-as-a-judgeLLM-судья
rubricрубрика (критерии и шкала оценки)
conciseness / accuracy / toneкраткость / точность / тон
thresholdпорог
Nunjucks templateшаблон Nunjucks ({{variable}})