Обзор Promptfoo: тесты ответов LLM — это разбор CLI и библиотеки для воспроизводимого сравнения промптов и моделей. Инструмент собирает промпты, провайдеров и тестовые случаи в одну матрицу, прогоняет её и показывает результаты в терминале и веб-просмотрщике. Ниже — назначение, границы, полный учебный пример и ручной расчёт приёмки.
Назначение и границы инструмента
README описывает Promptfoo как CLI и библиотеку для оценки и red teaming приложений на базе LLM. Getting started показывает сравнение выходов нескольких моделей. Наше упражнение проверяет конкретный контракт извлечения срока возврата.
Границы начинаются там, где заканчивается локальность. Сам CLI запускается локально. Маршрут данных определяется выбранным провайдером: при обращении к облачной модели запрос отправляется её владельцу. Если выбран облачный LLM-судья, ему также передаются данные для оценивания. Установка CLI на свой компьютер не делает эти два обращения локальными.
Второе ограничение — природа проверок. Cost и latency assertions проверяют уже полученный результат. Они не устанавливают денежный потолок и не обрывают долгий вызов. Документация Getting started прямо пишет: «Cost and latency assertions check completed responses; they do not cap spending or stop slow requests». Это значит, что защита бюджета проектируется отдельно от набора проверок.
Третье ограничение — настройка конкретного провайдера. В Getting started указаны 60+ вариантов, но для нашего учебного Q рабочий endpoint не задан. До его настройки запуск невозможен. Red teaming в этот учебный набор не входит.
Что читатель получает на выходе: готовые тестовые случаи, ручной расчёт приёмки и понимание, где автоматическая проверка даёт сигнал, а где требуется решение человека. Инструмент ускоряет сравнение вариантов. Решение о выкатке остаётся за вами.
Промпты, провайдеры, тесты и расширение матрицы
В promptfooconfig.yaml описывают prompts, providers и tests. Переменные промпта заключаются в двойные фигурные скобки. Для нашего набора это {{text}}: сюда подставляется полная строка CASE-A, CASE-B или CASE-C.
Матрица — это декартово произведение. Документация test-cases говорит: по умолчанию каждый тест выполняется против всех промптов. То же правило действует для провайдеров: без поля providers тест идёт по всем моделям. Число генераций считается как произведение числа промптов, числа провайдеров и числа тестов.
Поля providers и prompts ограничивают матрицу для отдельного теста; providers: [] исключает всех провайдеров. В документации test-cases описаны также шаблоны совпадения и совместная фильтрация с providerPromptMap. В нашем наборе фильтры не применяются.
Учебный пример ниже использует один провайдер, два промпта и три теста. Это шесть генераций при фактическом запуске без кэша. Провайдер Q обозначает условного участника матрицы; исполняемый endpoint для него не задан. Такой набор позволяет посчитать план вызовов вручную и увидеть, как правка промпта меняет результат.
Порядок действий для собственного набора:
- Создайте каталог и файл
promptfooconfig.yaml. - Опишите промпты с переменными в двойных фигурных скобках.
- Перечислите провайдеров и задайте ключи через переменные окружения.
- Добавьте тесты с
varsиassert. - Запустите
promptfoo eval, затемpromptfoo view.
Для создания исходного каталога документация Getting started предлагает promptfoo init. Версию установленного инструмента зафиксируйте перед переносом конфигурации.
Полные входы и ожидаемые выходы
Учебный набор извлекает срок возврата из текста. Три случая задают вход и эталон:
- CASE-A: «Возврат разрешён 14 календарных дней» →
{"days":14}. - CASE-B: «Доставка 3 дня. Возврат разрешён 7 дней» →
{"days":7}. - CASE-C: «Срок возврата не указан» →
{"days":null}.
Два промпта различаются точностью формулировки. P1 просит JSON с полем days. P2 уточняет: извлечь именно срок возврата, вернуть ровно один ключ days, значение целое или null, доставку не засчитывать.
P1: Верни JSON с days из текста: {{text}}
P2: Извлеки именно срок возврата. Верни ровно JSON-объект с единственным ключом days; значение целое или null, если срок не указан. Доставку не засчитывай. Текст: {{text}}
Заданная матрица выходов для учебного провайдера Q:
| Комбинация | Выход |
|---|---|
| Q / P1 / CASE-A | {"days":"14"} |
| Q / P1 / CASE-B | {"days":3} |
| Q / P1 / CASE-C | {"days":0} |
| Q / P2 / CASE-A | {"days":14} |
| Q / P2 / CASE-B | {"days":7} |
| Q / P2 / CASE-C | {"days":null} |
Правила приёмки: JSON разбирается; ровно один ключ days; значение — integer или null; значение равно эталону своего случая. Ручной подсчёт даёт P1 = 0/3 и P2 = 3/3. Все ответы заранее заданы для ручного разбора. Пример показывает различие вердиктов на этих строках; улучшение модели или причинный эффект правки P2 здесь не измерялись.
Конфигурационная заготовка учебного набора; строку провайдера необходимо заменить перед реальным запуском:
description: Учебный набор извлечения срока возврата
prompts:
- id: prompt-p1
label: P1
raw: 'Верни JSON с days из текста: {{text}}'
- id: prompt-p2
label: P2
raw: 'Извлеки именно срок возврата. Верни ровно JSON-объект с единственным ключом days; значение целое или null, если срок не указан. Доставку не засчитывай. Текст: {{text}}'
providers:
- id: q:methodical
label: Q
tests:
- description: CASE-A
vars:
text: 'Возврат разрешён 14 календарных дней'
assert:
- type: is-json
- type: javascript
value: |
const o = JSON.parse(output);
const keys = Object.keys(o);
if (keys.length !== 1 || keys[0] !== 'days') return false;
const v = o.days;
if (v !== null && !Number.isInteger(v)) return false;
return v === 14;
- description: CASE-B
vars:
text: 'Доставка 3 дня. Возврат разрешён 7 дней'
assert:
- type: is-json
- type: javascript
value: |
const o = JSON.parse(output);
const keys = Object.keys(o);
if (keys.length !== 1 || keys[0] !== 'days') return false;
const v = o.days;
if (v !== null && !Number.isInteger(v)) return false;
return v === 7;
- description: CASE-C
vars:
text: 'Срок возврата не указан'
assert:
- type: is-json
- type: javascript
value: |
const o = JSON.parse(output);
const keys = Object.keys(o);
if (keys.length !== 1 || keys[0] !== 'days') return false;
return o.days === null;
Провайдер q:methodical — учебное обозначение. Для реального запуска подставьте рабочую конфигурацию провайдера и ключ через переменную окружения.
Валидность JSON, схема и смысл
Три уровня проверки различаются по строгости. Документация expected-outputs перечисляет типы assertions. is-json проверяет, что выход — валидный JSON, с опциональной валидацией по схеме. contains-json проверяет, что выход содержит валидный JSON. equals требует точного совпадения. javascript и python запускают ваш код и возвращают булево значение или объект с полем score.
Разница между уровнями практическая. is-json отвечает на вопрос «разбирается ли строка». Схема отвечает на вопрос «те ли поля и типы». Смысловая проверка отвечает на вопрос «то ли значение». Учебный набор использует все три: is-json для разбора, javascript для структуры и типа, сравнение с эталоном для смысла.
| Уровень | Что проверяет | Пример типа |
|---|---|---|
| Разбор | Строка является валидным JSON | is-json |
| Схема | Поля и типы соответствуют ожиданию | is-json с schema |
| Смысл | Значение равно эталону | equals, javascript |
Документация expected-outputs описывает свойство threshold у теста. Если оно задано, статус прохождения определяется взвешенной суммой оценок assertions. Порог может переопределить вердикты отдельных проверок. Без порога на уровне теста требуется прохождение каждой проверки с ненулевым весом.
В нашей заготовке все проверки обязательны: одного правильного смысла при нарушенном формате недостаточно. Не добавляйте общий разрешающий порог, пока не определено, какие ошибки допустимы. Для срока возврата подмена неизвестного нулём должна остановить приёмку независимо от остальных удачных случаев.
Результат полезно хранить по случаям: идентификатор, фактическая строка, результат разбора, проверка типа и сравнение значения. Одна средняя оценка скрывает различие между неправильным числом и технической невозможностью прочитать ответ.
Отрицательная проба слабого contains
Проверка contains ищет подстроку в тексте выхода. Документация expected-outputs определяет её так: выход содержит строку или число как текст. Слабость проявляется на числах. Строка {"days":140} содержит подстроку 14. Проверка contains("14") её пропустит.
Отрицательная проба показывает границу. Если бы учебный набор использовал contains("14") для CASE-A, выход {"days":140} прошёл бы проверку. Полный эталон CASE-A его отклоняет: значение 140 не равно 14, и структура с одним ключом days формально верна, а смысл неверен.
Порядок действий для проверки собственного assertion:
- Возьмите эталонный выход и убедитесь, что проверка его пропускает.
- Составьте выход, который близок по форме, но неверен по смыслу.
- Убедитесь, что проверка его отклоняет.
- Если проходит — усильте проверку до разбора JSON и сравнения значения.
Для CASE-A добавьте ещё два ручных контроля. Строка {"days":14,"comment":"ok"} содержит верное число, но имеет лишнее поле. Строка {"days":true} разбирается как JSON, однако boolean не соответствует целому сроку. Оба результата должны быть отклонены правилом из заготовки.
Для CASE-B сначала найдите в исходном тексте фразу о возврате, затем убедитесь, что проверка сравнивает именно 7. Подмена эталона числом 3 из доставки испортит сам тест: модель, вернувшая неверный срок, начнёт проходить приёмку. Поэтому эталон проверяют по входу до запуска.
Для CASE-C отсутствие срока задаёт null. Это не ноль дней, не бессрочный возврат и не отказ в возврате. В тексте нет данных для этих выводов. Даже если прикладная система обычно использует 0 как служебное значение, такой контракт нужно определить отдельно и отразить в ожидаемом ответе.
Ошибку самой проверки храните отдельно от неправильного выхода. Если JSON.parse выбросит исключение, это не превращает строку в правильный ответ. Если ошибка возникла из-за неверно написанного assertion, сначала почините валидатор и повторите контроль на заранее известных положительных и отрицательных строках.
Детерминированные проверки и LLM-судья
Документация expected-outputs делит проверки на две группы. Детерминированные метрики — это программные тесты на выходе LLM. Модельно-вспомогательные метрики опираются на LLM или другие модели машинного обучения.
В этом примере разбор JSON и точное сравнение выполняются кодом. Чистая проверка повторяема при одинаковых входах и версии реализации. Произвольный пользовательский скрипт или webhook может обращаться к внешней системе, поэтому название программного типа само по себе не доказывает ни детерминированность, ни отсутствие сетевых расходов.
| Свойство | Детерминированные | LLM-судья |
|---|---|---|
| Повторяемость | У чистой функции при тех же данных | Зависит от модели и параметров |
| Стоимость | В нашем oracle нет вызова модели | Удалённый judge добавляет вызовы |
| Настройка | Код или встроенный тип | Провайдер и критерий |
| Риск | Ложный пропуск при слабом правиле | Смещение и разброс оценки |
Учебный набор обходится кодом: структура и значение days заданы точно. Для свободных требований Promptfoo поддерживает llm-rubric. Применять такую проверку к нашему числовому полю необходимости нет.
Перед выбором LLM-судьи выпишите критерий в проверяемом виде. Если речь о единственном поле days и заданном эталоне, достаточно кода. Если критерий звучит как «вежливо объяснить ограничения», понадобятся отдельная рубрика, примеры допустимых ответов и проверка согласованности оценок. Сам вызов другой модели не делает такую оценку независимой истиной.
План вызовов и отдельный бюджет
Число генераций считается до запуска. Формула: число промптов × число провайдеров × число тестов. Учебный набор даёт 1 × 2 × 3 = 6 генераций при фактическом запуске без кэша. План из шести генераций относится только к нашей матрице. Он не включает повторные попытки, повторные прогоны и дополнительные запросы проверки.
Повторы увеличивают счёт. Документация test-cases описывает options.repeat: положительное целое запускает тест несколько раз. Значение на уровне теста переопределяет --repeat, commandLineOptions.repeat и evaluateOptions.repeat. Индексы повторов используют отдельные записи кэша. Флаг --no-cache заставляет каждый запуск обращаться к провайдеру.
Оценивание добавляет вызовы. LLM-судья может добавлять обращения к модели оценки; их число зависит от выбранной проверки и её реализации. Red team стратегии могут добавлять обращения. Итоговый счёт складывается из генераций, повторов и оценивания.
Для нашего Q сначала нужно определить провайдера и тариф: пока они не заданы, стоимость шести генераций неизвестна. До реального запуска выполните следующие действия.
- Посчитайте базовое число генераций по формуле.
- Добавьте повторы и оценивание.
- Зафиксируйте провайдера, модель, версии и цену.
- Проверьте, предоставляет ли провайдер жёсткий лимит, и настройте доступный механизм ограничения до запуска.
- Запустите набор и сверьте фактический расход.
| Шаг | Что фиксируем | Где задаётся |
|---|---|---|
| Провайдер | Модель и endpoint | Конфиг и ключ окружения |
| Версии | Версия инструмента и модели | Конфиг и lock-файл |
| Цена | Тариф провайдера | Кабинет провайдера |
| Лимит | Потолок расхода | Настройка провайдера |
Cost assertion полезен как индикатор: он помечает дорогие ответы в отчёте. Механизм остановки расхода проверяется отдельно: уведомление о бюджете может не быть жёстким потолком. Для долгого запроса требуется настроенный таймаут клиента или провайдера.
Версии, regression gate и границы приёмки
Зафиксируйте версию Promptfoo, модель, параметры и версию набора. Документация test-cases предупреждает: resume и retry используют сохранённые строки; изменённые источники требуют новой оценки. Поэтому правку текста CASE-B нужно отличать от повторной проверки прежнего сохранённого ответа.
Regression gate встраивается в CI. README перечисляет автоматизацию проверок в CI/CD и просмотр pull request на предмет проблем безопасности и соответствия. Документация Getting started упоминает запуск оценок на каждом PR. Порог прохождения задаётся на уровне теста или набора.
Границы приёмки важны. Учебный набор использован для правки P2. Независимая проверка обобщения ещё не проведена. Это значит, что успех на трёх случаях подтверждает работу на этих случаях. Перенос на новые формулировки требует новых тестов.
Приёмка не становится независимой от автора набора. Тот, кто написал эталоны, задаёт критерий. LLM-судья добавляет свою модель и свой критерий. Детерминированные проверки дают повторяемость при том же входе. Смысловое соответствие остаётся предметом решения человека.
Практический порядок для gate:
- Сохраните эталоны, входные строки и версию набора в репозитории.
- Проверьте assertion на правильных и специально испорченных выходах.
- Зафиксируйте версию Promptfoo, провайдера и параметры модели.
- Запустите набор после изменения приложения и сохраните фактический отчёт.
- Разберите каждый провал, отделяя ошибку модели, провайдера и проверки.
- После исправления проверьте отдельные случаи, которые не использовались для настройки.
Заполненное решение для учебной матрицы: P1 отклоняется, потому что все три заданных выхода нарушают контракт. P2 проходит только заданный набор. Перевод P2 в реальное приложение пока не подтверждён: рабочий провайдер не настроен, фактических ответов и отдельной приёмочной выборки нет. Таблица 3/3 не должна превращаться в сообщение о состоявшемся выпуске.
Журнал разбора CASE-A: строка "14" не проходит проверку типа; правильное целое 14 проходит; число 140 не проходит смысловую проверку. Для CASE-B число 3 показывает смешение доставки и возврата. Для CASE-C число 0 показывает подмену неизвестности. Такой журнал объясняет причину провала и помогает не ослабить assertion ради красивой итоговой доли.
При переносе набора в свой продукт замените тексты случаев и эталоны вместе. Если поменять только промпт, в тестах останется прежний предмет проверки. Если поменять только эталоны, исчезнет связь с исходными условиями. Перед первым запуском другой участник команды может вручную восстановить ожидаемое значение по каждому входу и сравнить его с вашим JSON.
После запуска сохраните сырые ответы. Это позволяет повторно проверить валидатор без нового обращения к модели: если ошибка была в assertion, для диагностики полезен тот же фактический выход. Отдельно запишите, какие результаты взяты из кэша. Повторная оценка сохранённых строк и новая генерация ответа решают разные задачи и по-разному расходуют ресурсы.
Минимальная карточка результата для CASE-B: вход с доставкой 3 и возвратом 7; версия промпта; провайдер; фактическая строка ответа; разобранное значение; ожидаемое 7; вердикт; причина. Пустой ответ при сетевом сбое не приравнивайте к days=null: неизвестный срок в исходных данных и недоступность модели — разные события.
Частые вопросы
Что такое Promptfoo простыми словами?
Promptfoo — CLI и библиотека для оценки и red teaming приложений на базе LLM. Он собирает промпты, провайдеров и тестовые случаи в матрицу, прогоняет её и показывает результаты в терминале и веб-просмотрщике. Конфигурация хранится в файле promptfooconfig.yaml; маршрут данных зависит от выбранного провайдера.
Где взять Promptfoo на GitHub?
Исходный код и README находятся в репозитории promptfoo/promptfoo на GitHub. README описывает установку через npm install -g promptfoo, brew install promptfoo и pip install promptfoo. Запуск без установки доступен через npx promptfoo@latest. Там же указано, что проект стал частью OpenAI и остаётся открытым под лицензией MIT.
Как считаются вызовы модели в Promptfoo?
Число генераций равно произведению числа промптов, провайдеров и тестов. Учебный набор из одного провайдера, двух промптов и трёх тестов даёт шесть генераций при запуске без кэша. Повторы через options.repeat и оценивание через LLM-судью добавляют обращения. Количество обращений оценивания и повторных попыток учитывается отдельно.
Ограничивает ли cost assertion расход денег?
Cost assertion оценивает завершённый ответ. Для реального запуска нашего набора отдельно проверьте ограничение расхода и таймаут у выбранного провайдера. Сейчас тариф Q неизвестен и расход не рассчитан.
Когда нужен LLM-судья, а когда хватит кода?
Для нашего поля days достаточно разобрать JSON, проверить тип и сравнить значение. Свободные требования к ответу могут оцениваться моделью по заранее заданной рубрике, которую также нужно проверить.
