Сервис оценки ответов AI-агентов по декларативным критериям. Задача описывается промптом и rubric — списком проверок; агент присылает ответ; сервис возвращает вердикт с разбором каждой проверки и копит статистику по задаче.
FastAPI · PostgreSQL · SQLAlchemy 2 (async) · pytest · Docker · GitHub Actions
Когда агента гоняют по набору задач, обычно ломается не модель, а учёт вокруг неё: ретрай по таймауту записывается как вторая попытка и портит pass rate; кривой критерий выглядит как «агент не справился»; p95 считается интерполяцией и показывает задержку, которой никто не наблюдал. Этот сервис написан вокруг таких мест, а не вокруг счастливого пути.
Присланное не исполняется. Проверки декларативные — contains,
not_contains, regex, json_equals, numeric_range, max_latency_ms. Это
ограничивает выразительность, зато харнесс оценки не превращается в способ
выполнить произвольный код у себя.
Rubric проверяется при создании задачи. Сломанный критерий отдаёт 422 с
причиной автору задачи, а не всплывает через неделю как «агент не прошёл».
Повторы схлопываются по idempotency_key. Тот же ключ в пределах задачи
возвращает исходный результат и 200 вместо 201, а уникальный индекс в базе
удерживает это и при одновременных ретраях. Тест на четыре параллельных запроса
с одним ключом нашёл в этом месте настоящую ошибку — она разобрана в комментарии
в app/main.py.
| Метод | Путь | Что делает |
|---|---|---|
GET |
/health |
проверка живости |
POST |
/tasks |
создать задачу с rubric (валидируется сразу) |
GET |
/tasks |
список задач |
POST |
/tasks/{slug}/submissions |
оценить ответ агента |
GET |
/tasks/{slug}/stats |
pass rate, средний score, p95 задержки, разбивка провалов |
Интерактивная схема — /docs.
curl -X POST localhost:8000/tasks -H 'content-type: application/json' -d '{
"slug": "json-status",
"prompt": "Верни {\"status\": \"done\"}",
"rubric": [
{"kind": "json_equals", "path": "status", "value": "done"},
{"kind": "not_contains", "value": "api_key", "weight": 3},
{"kind": "max_latency_ms", "value": 500}
]
}'
curl -X POST localhost:8000/tasks/json-status/submissions \
-H 'content-type: application/json' \
-d '{"agent": "claude", "output": "{\"status\": \"done\"}", "latency_ms": 120, "idempotency_key": "run-1"}'Ответ содержит passed (прошли ли все проверки), score (доля веса
пройденных — чтобы отличать «мимо целиком» от «почти верно») и checks с
объяснением по каждой.
docker compose up --build # api на :8000, postgres на :5432Локально без Docker:
pip install -r requirements-dev.txt
DATABASE_URL="postgresql+asyncpg://grader:grader@localhost:5432/grader" \
uvicorn app.main:app --reloadpytest # SQLite, быстро
DATABASE_URL=postgresql+asyncpg://... pytest # как в CI34 теста: движок оценки отдельно, HTTP-слой через ASGI-транспорт целиком.
CI гоняет их на PostgreSQL — том же движке, что в проде, потому что расхождения
между базами SQLite-прогон не ловит, — плюс собирает образ и поднимает
docker compose, проверяя /health живым запросом.
- Нет Alembic. Схема создаётся из моделей. Для сервиса такого размера это разумный размен; как только схема начнёт меняться на живых данных, размен станет неверным.
- Нет аутентификации. Сервис задуман как внутренний. Ставить его наружу без слоя авторизации нельзя.
- Нет очереди. Оценка синхронная и быстрая, потому что не исполняет код. Появятся долгие проверки — понадобится воркер, и это уже другой проект.