Skip to main content
Glama

evalmine

Числа в лидербордах никогда не предсказывали, как изменение модели скажется на тех сорока с лишним задачах, которые вы реально запускаете. Одна модель возглавляет бенчмарк — вы подменяете её, и она незаметно хуже справляется с работой, от которой вы зависите.

evalmine отвечает на один вопрос об изменении модели на ваших задачах: помогло оно, навредило или обошлось дороже за тот же результат? Вы пишете YAML-набор своих задач. Инструмент прогоняет их на двух или более моделях, проверяет каждый ответ по схеме, замеряет время и заставляет LLM-судью сравнивать ответы попарно в обоих порядках, чтобы предпочтение судьи к тому, что он видит первым, взаимно сокращалось. Он оценивает судью по вашим меткам предпочтений с помощью каппы Коэна и отказывается выносить в заголовок процент побед, если не может показать, что судья согласен с вами. Стоимость берётся из таблицы цен, привязанной к дате; неизвестная модель приводит к провалу запуска, а не к нулевой цене. Отчёты версионируются по хэшу набора; MCP-сервер из трёх инструментов позволяет агенту запускать эвалы прямо в середине задачи.

Результат на примере набора здесь с фейковым адаптером: каппа 0.25 по 12 меткам ниже порога 0.40, поэтому процент побед 0.463 печатается с пометкой, а не в заголовке. Этот отказ и есть работа инструмента:

$ evalmine run examples/everyday-eight.yaml \
    --models anthropic/claude-haiku-4-5,google/gemini-2.5-flash --fake

run 20260823T210009Z_c4545e4e_dbc76614  (everyday-eight)
  report: reports/everyday-eight/20260823T210009Z_c4545e4e_dbc76614/report.md
  calibration: below_floor - kappa 0.25 (fair) over 12 labels - headline eligible: false
  google/gemini-2.5-flash vs anthropic/claude-haiku-4-5: win-rate 0.463 (UNCALIBRATED) [0.325-0.613] over schema-passing pairs only, n=20 - flips 3 - excluded 0
  cost: $0.0658 this run (answers $0.0081, judge $0.0578); if uncached $0.0658

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

evalmine: проверьте набор, запустите его на фейковом адаптере, прочитайте разделы калибровки и процента побед в созданном отчёте

Каждый кадр этого — реальный запуск. Перезапишите его с помощью vhs docs/demo.tape (vhs, brew install vhs).

Статус. v0.1.0, предрелиз. Ядро, три адаптера провайдеров, проверки выполнения и MCP-поверхность собраны и протестированы; таблица цен сверена с публичной страницей цен каждого провайдера на указанную дату. Записи в журнале решений пока нет — см. Пока нет.

Спецификация: docs/spec.md. Это контракт, под который написана кодовая база, и он имеет приоритет над этим README в любых расхождениях. Как это работает, подробно: docs/learning/how-it-works.md (стилизованный HTML-рендер).

Быстрый старт

git clone https://github.com/hishamalward/evalmine.git && cd evalmine
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"          # add ,mcp -> ".[dev,mcp]" for the MCP server

Python 3.10 или новее. Три зависимости времени выполнения: PyYAML, jsonschema, httpx.

Проверьте набор без каких-либо затрат. validate разбирает файл, применяет JSON Schema, рендерит каждый промпт (несоответствующий {{placeholder}} — это жёсткая ошибка) и разрешает каждую строку модели по таблице цен. Ноль сетевых вызовов.

evalmine validate examples/everyday-eight.yaml
# ok: examples/everyday-eight.yaml - 8 tasks, 20 cases, 12 labels; every prompt
# rendered; 3 model strings resolved against prices-2026-08-23.yaml

Запустите его на фейковом адаптере. --fake направляет каждую строку модели во встроенный детерминированный адаптер: без ключа, без сети, без затрат. Две строки моделей ниже — это те, к которым относятся двенадцать человеческих меток в примере набора, так что этот запуск проверяет путь калибровки от начала до конца.

evalmine run examples/everyday-eight.yaml \
  --models anthropic/claude-haiku-4-5,google/gemini-2.5-flash --fake

Запустите его по-настоящему. Ключи берутся из окружения и ниоткуда больше. Скопируйте .env.example, заполните его вне репозитория и экспортируйте то, что нужно.

export ANTHROPIC_API_KEY=...
export GOOGLE_API_KEY=...

evalmine run examples/everyday-eight.yaml \
  --models anthropic/claude-haiku-4-5,google/gemini-2.5-flash \
  --max-cost 0.50

Предварительная оценка стоимости запускается перед первым живым вызовом. Если она превышает --max-cost, запуск отклоняется (код выхода 4), и ничего не тратится. Без указания лимита где-либо значение CLI по умолчанию — $2.00. Каждый вызов кэшируется на диске по хэшу содержимого, так что повторный запуск бесплатен, а отчёт воспроизводим; --no-cache принудительно делает свежие вызовы и всё равно записывает их.

Другие команды: evalmine prices [--for suite.yaml], evalmine last suite.yaml, evalmine report <run-id>, evalmine compare <report_a> <report_b>.

Related MCP server: AgentOps EvalBench MCP

Файл набора

Один YAML-файл содержит ваши задачи, конфигурацию судьи и ваши метки. Прилагаемый пример — examples/everyday-eight.yaml: восемь выдуманных задач (переписать, извлечь, классифицировать, объяснить, небольшое изменение кода) по двадцати случаям, три из которых содержат схему вывода, с двенадцатью метками предпочтений. Полная схема — в спецификации §5; форма выглядит так:

suite: everyday-eight
version: 1

defaults: { temperature: 0, max_tokens: 700, timeout_s: 60 }
limits:   { max_cost_usd: 1.50 }

judge:
  model: anthropic/claude-sonnet-4-6
  rubric: |
    Prefer the answer that a competent colleague would ship without editing.
    ...
  calibration: { min_kappa: 0.40, min_labels: 10, on_below_floor: flag }

tasks:
  - id: ticket-triage
    kind: classify                # a free label, used only to group report rows
    prompt: |
      Classify this support ticket. Return JSON only.

      Ticket:
      {{ticket}}
    schema: { type: object, required: [category, severity], ... }
    rubric: |                     # appended to the suite rubric for this task
      In addition to the suite rubric: ...
    cases:
      - id: charged-twice
        vars: { ticket: "I was charged twice this month..." }

labels:
  - { task: ticket-triage, case: charged-twice,
      baseline: anthropic/claude-haiku-4-5,
      candidate: google/gemini-2.5-flash,
      prefer: candidate, note: "team-wide lockout is high, not medium" }

Три особенности этого файла, которые сделаны намеренно:

  • Шаблонизация — это не Jinja. Ровно {{name}}, подставляется один раз, без выражений и фильтров. Плейсхолдер без соответствующей переменной — это жёсткая ошибка при загрузке, потому что молча пустая переменная — самый лёгкий способ сделать эвал тихо бессмысленным.

  • Неизвестные ключи — это ошибки на каждом уровне. Опечатка в rubrik:, которая игнорируется, даёт отчёт, который выглядит нормально, но ничего не значит.

  • labels — источник доверия к инструменту. Это ваши суждения, записанные до того, как вы увидите процент побед, и судья оценивается по ним. Набор без меток всё равно запускается; просто он не может дать заголовочное число.

Замените пример своими задачами. В этом и заключается весь смысл инструмента.

Проверки выполнения для задач с кодом

Проза — плохой прокси для кода, который должен работать. Случай может объявить check: bash-фрагмент, который получает код ответа ($ANSWER — файл, $ANSWER_TEXT — текст) и завершается с кодом 0, если всё работает. Он выполняется в свежей временной директории, с таймаутом, с удалёнными из окружения секретами, и никогда не кэшируется. Каждый блок в ответе, обёрнутый в кавычки, выполняется по порядку, каждый на своём фикстуре; последний блок — это вердикт, а предыдущие записываются рядом с ним, так что ответ, который отзывает неправильный блок и пишет второй, оценивается по второму и показывает отзыв.

- id: jq-remote
  vars: { task: "Write a jq filter ... the JSON is in postings.json" }
  check:
    setup: 'printf "[{\"t\":\"a\",\"remote\":true}]" > postings.json'
    run: 'jq -r "$(cat "$ANSWER")" postings.json | grep -q a'

Результат — пройдено/не пройдено, код выхода, вывод — находится рядом с ответом в answers.jsonl, в карточке результатов и в HTML-представлении пар, и судье он показывается с одним фиксированным правилом: ответ, чья проверка не прошла, не может победить тот, что прошёл. Спецификация §6.6.

Как читать отчёт

reports/<suite>/<run-id>/report.md вместе с report.json, report.html, answers.jsonl и pairs.jsonl. Читайте в таком порядке.

1. Сначала калибровка. Она печатается над процентами побед намеренно. Вам нужна каппа Коэна между вердиктами судьи и вашими метками, с названием диапазона Лэндиса-Коха, и матрица путаницы 3x3 под ней. Каппа, а не простое согласие, потому что согласие завышается, как только одна категория доминирует, а так и будет: судьи учатся, что ничьи безопасны. Матрица показывает, как судья ошибается, а это важно — судья, который никогда не говорит «ничья», когда вы говорите, — это другая проблема, чем тот, кто систематически предпочитает что-то новое. Под ней разбивка по задачам показывает, где: одна каппа может скрывать судью, который отличен в вашей задаче переписывания и бесполезен в задаче триажа, и среднее — это то, что вы бы потеряли.

2. Процент побед, которому не стоит доверять. Три условия, любого из которых достаточно:

  • headline_eligible: false — каппа ниже порога, меток слишком мало, или каппа не определена, потому что оба оценщика использовали одну категорию на протяжении всего. Отчёт запрещает числу быть заголовком, помечает каждую цифру крестиком, и JSON и каждый MCP-ответ несут тот же флаг, так что агент, читающий сводку, не может процитировать число без оговорки.

  • Доля переворотов выше 0.30. Переворот — это пара, где судья изменил свой ответ, когда два ответа поменялись местами. Выше примерно трети процент побед измеряет порядок предъявления, а не качество. Отчёт говорит об этом в той же таблице.

  • Маленький n или уменьшающийся. Процент побед вычисляется только по парам, прошедшим схему: пара, где любая сторона не смогла разобраться или не прошла схему, исключается, а не засчитывается как поражение, чтобы модель, плохая в выдаче JSON, не проигрывала сравнение качества из-за ошибки форматирования. Цена — уменьшение n, поэтому раздел называется «только по парам, прошедшим схему, n=…», и n никогда не печатается без доли прохождения схемы на том же экране.

Прежде чем публиковать число, поднимите min_kappa до 0.60. Значение по умолчанию в поставке — 0.40 — обычная нижняя граница умеренного согласия, достаточно низкая, чтобы первый набор с дюжиной меток мог её преодолеть. Это порог для использования числа самому, с вашей собственной памятью о том, как проходила разметка. 0.60 — «существенное» — это порог для сообщения числа кому-то другому, где эта память не передаётся. Инструмент поставляется с мягкими настройками, чтобы первый набор стоило запустить дважды; эта рекомендация существует, чтобы первый набор не оказался в блог-посте.

3. Затем карточка результатов, и читайте стоимость вместе с качеством, а не после него. Доля прохождения схемы (помечена native или prompted, потому что провайдер, который обеспечивает схему за вас, и тот, кого просто вежливо попросили, — это не одно и то же измерение), доля прохождения выполнения с её n, где задача объявляет проверки выполнения, p50 и p95 задержки с их n, стоимость этого запуска и стоимость без кэша. Кандидат, который выигрывает 0.55 за тройную цену, — это другое решение, чем тот, кто выигрывает 0.55 за половину.

4. Таблица по задачам, отсортированная от худшей к лучшей. Заголовочный процент побед, который не сдвинулся, пока три задачи сдвинулись на 0.4 в противоположных направлениях, — это то, что вы бы иначе упустили. evalmine compare A B печатает именно эти движущиеся элементы между двумя запусками.

5. report.html и процесс разметки. Каждый запуск также пишет одну автономную страницу — без сервера, без зависимостей, открывается по пути file://. Те же разделы, плюс каждая оценённая пара бок о бок с скрытыми именами моделей и свёрнутым вердиктом судьи, чтобы вы читали ответы ровно так, как их читал судья. Предпочесть A · Ничья · Предпочесть B под каждой, затем копировать метки YAML даёт вам записи labels:, которые можно вставить обратно в ваш набор: десять минут кликов вместо получаса ручного редактирования, а это разница между набором калибровки, который растёт, и тем, который нет.

В отчёте нет прилагательных и нет рекомендаций. Суждение вносится в DECISIONS.md, сформулированное по вашему вердикту — отчёт предзаполняет шаблон для вас внизу каждого запуска.

MCP

evalmine-mcp — это stdio MCP-сервер, предоставляющий ровно три инструмента, которые вызывают те же функции core.py, что и CLI:

инструмент

что делает

тратит

run_suite(suite_path, models, max_cost, baseline, no_cache)

запускает набор, возвращает сводку и пути к отчётам

до лимита

compare(report_a, report_b)

разницу между двумя отчётами

ничего

last_report(suite_path)

самый свежий отчёт для набора

ничего

Зарегистрируйте его, скопировав .mcp.json.example в .mcp.json. Сначала установите дополнительный пакет: pip install -e ".[mcp]".

Смысл в том, что агент может запускать ваши эвалы в середине задачи — «прежде чем менять модель в этом файле, запусти набор и скажи мне процент побед» — вместо того, чтобы человек читал отчёт потом.

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

Лимиты и почему у агента по умолчанию ниже, чем у вас. Лимит — это параметр core.run_suite(), а не флаг CLI, который MCP переопределяет: есть ровно одно место, где можно потратить деньги, и оно ограничено там. Если агент передаёт max_cost, он используется, но запрос выше EVALMINE_MCP_MAX_COST_CEILING (по умолчанию $5.00) отклоняется outright, а не ужимается и выполняется. Если агент опускает его, лимит равен min(suite.limits.max_cost_usd, EVALMINE_MCP_MAX_COST), по умолчанию $1.00 — половина CLI-шных $2.00, потому что человек за CLI ввёл число, а агент нет. Запуск сверх лимита возвращает структурированный отказ, ничего не тратит и никогда молча не обрезается, чтобы вписаться; обрезанный запуск даёт меньшее число, которое выглядит как полное.

run_suite возвращает сводку и пути, но никогда не возвращает сырые ответы провайдера. Они остаются в answers.jsonl на диске. Инструмент, который передаёт каждый ответ обратно в контекст агента, обходится вызывающему дороже, чем сам eval, и превращает eval-харнесс в путь для эксфильтрации всего, что есть в ваших промптах. suite_path также должен разрешаться внутри EVALMINE_MCP_SUITE_ROOT (по умолчанию: рабочая директория сервера).

Существующие аналоги

promptfoo и Braintrust — очевидные инструменты в этой области, и оба более функциональны, чем этот.

promptfoo имеет гораздо больше типов утверждений, веб-просмотрщик, red-teaming и покрытие провайдеров, которое не ограничивается тремя. Braintrust — это хостинговая платформа: трассировка, наборы данных, построенные из производственных логов, настоящий UI, совместная работа и операционная зрелость, которая приходит с тем, что это чей-то продукт. Если вам нужна широта или команда, смотрящая на одни и те же цифры, используйте один из них.

evalmine существует по трём более узким причинам.

  • Судья калибруется по вашим данным, или его число не печатается. Оба вышеупомянутых инструмента могут оценивать с помощью LLM-судьи. Ни один из них не делает калибровку по вашим меткам условием того, можно ли цитировать процент побед. Эта инверсия — отказ по умолчанию — и есть вся суть, и это не функция, которую можно прикрутить к инструменту, который в любом случае выдаёт число.

  • Журнал решений — это артефакт первого класса. Результат eval — это не число, а решение, которое вам придётся защищать через шесть месяцев. DECISIONS.md предварительно заполняется отчётом и пишется человеком, и он живёт в вашем репозитории рядом с кодом, о котором было принято решение.

  • Поверхность достаточно мала, чтобы прочитать её за один присест. Примерно 6 000 строк, включая четыре адаптера, отчёты и проверки выполнения. Никакого LLM-фреймворка, никаких SDK провайдеров — три написанных вручную POST-запроса к документированным JSON-эндпоинтам. Эта цена реальна, и её стоит озвучить: когда провайдер меняет свой API, мы узнаём об этом из-за поломки, а не из-за обновления.

Если эти три пункта для вас не важны, честная рекомендация — promptfoo.

Пока нет

Вне рамок v0.1.0, и README говорит об этом прямо, а не оставляет вам это выяснять: RAG или retrieval eval; агентные или многоходовые траектории; тонкая настройка чего-либо; веб-UI; что-либо хостинговое; более трёх провайдеров; автоматическая генерация рубрик; MCP-инструменты, помимо трёх вышеперечисленных.

Каждое число в этом README получено с помощью фейкового адаптера на вымышленном примере набора тестов. Ни один размеченный прогон на реальном наборе ещё не дал калиброванного числа или записи в DECISIONS.md; это появится до любого тега v0.1.0.

Разработка

pip install -e ".[dev,mcp]"
python -m pytest -q          # 310 tests, none of which make a network call
python -m ruff check src tests

CI запускает {ubuntu, macos, windows} x {3.10, 3.13}, каждая ветка выполняет все тесты, плюс сканирование секретов по рабочему дереву и всей истории git. Ни один API-ключ не должен находиться в этом репозитории, и evalmine run отказывается запускаться, если файл набора содержит строку, совпадающую с известным префиксом ключа.

См. CONTRIBUTING.md. Изменения начинаются в docs/spec.md.

Лицензия

MIT. См. LICENSE.

Related MCP Connectors

Related MCP Servers