LLM-as-judge — методология автоматической оценки качества LLM-ответов через другую LLM, которая по YAML-критерию выставляет рейтинг 1-5 и объяснение. Используется в data-quality-dashboard (Analysis-слой) и в standalone evaluation CLI для batch-прогонов off-line. Это частичный TypeScript-порт evaluation framework Никиты из репо Realai-plus/eval — взяли core TraceJudge (G-Eval), 5 критериев из 11, без DeepEval-зависимости и без composite-scoring path.
Eval-репо: происхождение
Внешний eval-репо — Realai-plus/eval (private GitHub), локально /home/i/JOBS/BloodGPT/eval/. Период активной разработки: сентябрь — декабрь 2025, 112 коммитов:
- Никита Ястреб — 57 коммитов (core framework, evaluators, criteria)
- Артур Казукевич — 33 коммита (recognition pipelines, ensemble eval)
- Ильдар — 11 коммитов
- Стас — 4 коммита
В том же monorepo идёт параллельная дорожка — recognize-benchmark про сравнение OCR / VLM pipelines для распознавания (эксперимент в paper-формате, декабрь 2025). Это другая методология и другой фокус, не связанная с judges Никиты технически — общий только репозиторий. Last commit Никиты в eval-репо — 2025-12-17, потом переключился на bloodgpt-for-business (Text-to-FHIR, narrative→FHIR multimodal pipeline).
Соответственно, eval-репо в апреле 2026 — это frozen artifact: огромная кодовая база (Python, 2536 строк только в evaluators/), 100+ JSON test cases (~25k строк, языки от Greek и Hebrew до Chinese и Polish), но без активного развития. Ильдар прочитал его в марте 2026 (сессия 920d5967), портировал минимум для DQD.
Что в upstream-репо есть (что мы оставили снаружи)
eval/eval/ — Python framework, который умеет два типа оценки и две стратегии запуска:
Типы оценки
| Тип | Threshold | Как работает | Портировано |
|---|---|---|---|
| LLM judge (G-Eval) | 0.7 default | DeepEval GEval метрика, рейтинг 1-5 через logprobs | Да |
| Deterministic | 0.95 default, 5% tolerance | Code-based проверки (математика, формат, structure) | Нет |
Для unit_converter критерий упомянут как deterministic (threshold 0.9, проверка точности конверсий типа glucose: 1 mmol/L = 18.018 mg/dL). У нас в bloodgpt-for-business нет deterministic пути вообще.
Стратегии запуска
| Стратегия | Что делает | Портировано |
|---|---|---|
Test runner (run_tests.py) | Запускает test cases из data/test_cases/*.json против промптов в Langfuse, считает score, отчёт | Нет |
Trace evaluation (evaluate_traces.py) | Берёт реальные traces из продакшена, оценивает retrospect, автоматически assign-ит scores обратно в Langfuse observations | Частично — мы делаем то же самое в Inngest cron, но scores пишем в свою БД, не в Langfuse |
Test runner upstream имеет: parallel execution (10 workers ThreadPoolExecutor), smart caching (LLM response + prompt cache), cost tracking с budget alerts, FDA compliance context-aware фильтр (не считает internal-only поля типа is_healthy), tag-based filtering, Pydantic validation, dry-run mode, environment filtering (production / staging / dev), time-window filter (--hours 48), trace-ID specific filtering. Из всего этого мы перенесли только: per-org feature flag (аналог dry-run), batch size limit, char truncation. Cost tracking, FDA-compliance фильтр, parallel execution за нас делает Inngest.
Evaluator файлы upstream
eval/eval/src/evaluators/, 2536 строк всего:
| Файл | Строк | Назначение | Портирован |
|---|---|---|---|
trace_judge.py | 344 | TraceJudge(GEval) — простой G-Eval judge | Да (как judge.ts) |
criteria_evaluator.py | 418 | CompositeTraceEvaluator — composite path | Нет (BG-1216) |
criteria_registry.py | 382 | Registry для динамической регистрации evaluators | Нет — мы используем простой dict OBSERVATION_TO_CRITERIA |
parameter_analysis.py | 382 | Structure validation для параметров | Нет |
llm_judge.py | 355 | Generic LLM judge wrappers | Нет |
fda_compliance.py | 339 | FDA non-device compliance проверка | Нет (вшита в YAML критерии) |
deterministic.py | 247 | Deterministic test framework | Нет |
base.py | 69 | Base evaluator class | Нет |
Полный список 11 критериев upstream
eval/eval/config/trace_criteria/:
| Критерий | Threshold | Layer | Портировано |
|---|---|---|---|
single_parameter_analysis.yaml | 0.7 | Analysis | Да |
panel_overview.yaml | 0.75 | Analysis | Да |
test_overview.yaml | 0.75 | Analysis | Да |
trend_analysis.yaml | 0.75 | Analysis | Да |
followup_generation.yaml | 0.75 | Analysis | Да |
panel_description.yaml | ? | Analysis | Нет |
unit_converter.yaml | 0.9 (deterministic) | Normalization | Нет |
parameter_range_gpt.yaml | ? | Analysis (range generation) | Нет |
recognize_gpt.yaml | 0.75 | Recognition | Нет |
recognize_medical_context.yaml | ? | Recognition | Нет |
default.yaml | — | fallback | Нет |
Recognition layer judges уже существуют у Никиты — recognize_gpt проверяет data preservation integrity (original_name на cyrillic, no translation/conversion), translation rules compliance, completeness, document classification. Это критерий для recognize-промпта. Если включить — Recognition-слой DQD сразу получит метрики через тот же judge cron, нужно только дописать YAML и расширить OBSERVATION_TO_CRITERIA.
Аналогично unit_converter.yaml — это математическая проверка конверсий (glucose mmol/L ↔ mg/dL и т.д.) с фиксированными формулами. Проверяет тоже Recognition / Normalization input.
Framework: DeepEval Python → manual TypeScript
Upstream использует DeepEval Python библиотеку — from deepeval.metrics import GEval. TraceJudge наследует от GEval:
class TraceJudge(GEval):
def __init__(self, prompt_name, criteria, judge_model):
super().__init__(
name=f"Trace Judge - {prompt_name}",
criteria=criteria.get_full_criteria(),
evaluation_params=[INPUT, ACTUAL_OUTPUT],
evaluation_steps=criteria.evaluation_steps,
threshold=criteria.threshold,
model=model,
)
def evaluate_trace(input, output):
test_case = LLMTestCase(input, output)
self.measure(test_case)
return {score: self.score, passed: ..., reason: self.reason, ...}DeepEval сам делает: G-Eval промпт, logprobs scoring, parsing, threshold compare, ошибки. У нас в TypeScript deepeval нет (npm пакет существует, но в нашем стеке не используется) — judge.ts делает это руками: формирует system prompt с rating scale 1-5, вызывает OpenAI API с logprobs: true, top_logprobs: 5, scan’ит response с конца на rating-token, считает weighted sum через top_logprobs.
Цитата Ильдара из сессии 920d5967: «что-то я не очень понял про эти цифры, давай продолжим пока, потом может пойму лучше». Механика осталась полупонятной — это ровно та часть, которую DeepEval скрывает за абстракцией.
CompositeTraceEvaluator (не портирован)
Upstream имеет два уровня evaluator’ов:
TraceJudge(GEval)— простой semantic judge, выдаёт один score 0-1 (это мы взяли)CompositeTraceEvaluator(GEval)— наследует GEval, но добавляет composite scoring через sub-metrics с весами:- semantic (G-Eval по criteria, основная часть)
- structural (
required_fieldsприсутствуют в output) - FDA compliance (
check_fda_compliance— нет prohibited терминов типа «diagnose», «cure», «treatment recommendation») - prohibited terms (
check_prohibited_terms) - weights dict — как взвешивать sub-metrics
В нашей criteria-loader.ts поля checkStructure, checkFdaCompliance, checkProhibitedTerms, requiredFields, weights оставлены в схеме «vestigial» — чтобы YAML файлы синхронизировались с upstream. Но buildEvaluationPrompt их игнорирует. Завершение порта — BG-1216.
Эффект: наш score — это чистый semantic G-Eval. Не учитываются ни структура output (есть ли reasoning поле и т.п.), ни FDA-нарушения отдельно от семантики. На практике YAML критерии это проверяют через сам criteria-текст (например, в single_parameter_analysis.yaml есть раздел «PARAMETER IDENTITY VERIFICATION (MUST DO FIRST)» — судит сам LLM-judge), но без отдельной структурной составляющей.
Что портировано в bloodgpt-for-business
packages/evaluation/:
judge.ts— основной runner. Берёт(rawInput, rawOutput, criteria), отправляет на LLM-судью, возвращает{score, passed, reason, judgeModel, threshold}.criteria-loader.ts—loadCriteria(name)грузит YAML, кеширует, защищает от path traversal.buildEvaluationPrompt(criteria)собирает финальный prompt изcriteria+evaluation_steps+feedback_instructions.- 5 YAML-критериев в
src/criteria/:
| Файл | Threshold | Строк | Что покрывает |
|---|---|---|---|
single_parameter_analysis.yaml | 0.7 | 627 | FDA non-device compliance, parameter identity verification, reasoning quality (Staging v24 schema) |
trend_analysis.yaml | 0.75 | 350 | 5-Step Clinical Reasoning Protocol (v15+), insight quality |
followup_generation.yaml | 0.75 | 326 | 10-step reasoning, non-duplication, медицинские follow-up рекомендации (v11) |
test_overview.yaml | 0.75 | 220 | Educational depth, personalization, safety compliance |
panel_overview.yaml | 0.75 | 195 | Reasoning depth, completeness, compliance |
Scoring через logprobs (G-Eval)
judge.ts:calculateWeightedScore:
- LLM просят выдать reasoning и в последней строке — integer 1-5 (rating scale)
- Запрос идёт с
logprobs: true, top_logprobs: 5, temperature: 0, max_tokens: 2000 - Сканируется response с конца — ищется token, который parse-ится как 1-5
- Из
top_logprobsсобираются все альтернативные ratings 1-5 с их вероятностями (exp(logprob)) - Считается weighted sum:
Σ (rating/5) × normalized_prob
Пример: если LLM вернула 4 с вероятностями {3: 0.1, 4: 0.7, 5: 0.2}, итоговый score = 0.6×0.1 + 0.8×0.7 + 1.0×0.2 = 0.82, не просто 0.8.
Logprobs обязательны. Text-parsing fallback убран явно (commit 49c3a4f9 refactor: remove text-parsing fallback, require logprobs). Если модель не возвращает logprobs — judge падает.
Format YAML-критерия
prompt_name: <соответствует имени промпта>
description: <что и зачем оцениваем>
threshold: 0.7-0.75 # > threshold → passed=true
judge_model: gpt-4.1 # default
# Vestigial из upstream (composite scoring path, не используются):
check_structure: bool
check_fda_compliance: bool
check_prohibited_terms: bool
required_fields: [<список полей в output>]
weights: {<sub-metric>: <weight>}
criteria: |
<главный текст для judge — детальные требования по разделам>
evaluation_steps:
- <step 1>
- <step 2>
- ...
feedback_instructions: |
<опционально — как форматировать reason>Versioning критериев
criteriaVersion = sha256(YAML)[:8] — пишется в EvaluationScore вместе с каждой оценкой. Это позволяет:
- Пересчитать после правки YAML — старые scores с другим
criteriaVersionпонятно отличить - Сравнить версии критерия (apples-to-apples) — какая версия дала более строгий pass rate
LLM-провайдер: цепочка proxy
judge.ts:getJudgeClient — приоритет:
JUDGE_BASE_URL(явный override)BIFROST_BASE_URL(bifrost — internal LLM proxy, продакшен)LITELLM_BASE_URL(litellm — старый proxy)OPENAI_API_KEY(direct)
Через Bifrost / LiteLLM модель формат openai/gpt-4.1; direct — просто gpt-4.1. Этот auto-prefix сделан в judge.ts.
Char-limits на input / output
100k chars input, 50k chars output — обрезаются с ... [truncated] суффиксом. Под GPT-4.1 (128k token / ~400k chars) запас комфортный, обрезка только outliers. Реальные медианы: input 18-53k (followup максимум), output 1-20k.
Два режима использования у нас
Продакшен — Inngest cron в DQD
Вызывается раз в час из evaluation-judge.function.ts (data-quality-dashboard). Маппинг observation prefix → criteria name; результат пишется в EvaluationScore (Prisma). Per-org feature flag LLM_JUDGE через WorkOS. Триггер "cron" в БД.
Это аналог upstream evaluate_traces.py, но scores не пишутся обратно в Langfuse — они только в нашей БД. Drill-down идёт через langfuseTraceId / langfuseObservationId в EvaluationScore, но Langfuse score view остаётся пустым.
Standalone — apps/evaluation/
Отдельный TypeScript-app: CLI + web server + sqlite storage. Команды: analyze (прогнать judge на dataset), compare (сравнить два прогона), show (детали отдельного результата), list-reports, serve (web UI). Независим от продакшена — для batch-прогонов оффлайн (например, проверить prompt regression на тестовом датасете до деплоя). Это упрощённый аналог run_tests.py upstream, без 10-worker parallelization, FDA-compliance фильтра и tag-based filtering.
НЕ путать с benchmark — это другая фича: benchmark делает multi-eval (recognition / normalization / LOINC / etc.) с bbox annotation UI и CLI↔Web interop, evaluation — только off-line LLM-judge runs. Возможно в будущем сольются, но сейчас разные scope.
Test corpus у Никиты (НЕ портирован)
eval/eval/api_test/test_cases/ — 100+ JSON test cases, ~25 тыс. строк суммарно. Покрытие по panels (CBC, lipid_profile, kidney_function, thyroid_function, hormones, electrolytes, autoimmune, allergy, hepatitis, immunology, cardiac_markers, transplant_immunosuppressants, geriatric, pediatric, neonatal, pregnancy, sepsis, и т.д.) × языки (English, German, Russian, French, Spanish, Czech, Greek, Hebrew, Arabic, Chinese, Polish). Trends — отдельная папка с timeline-сценариями.
У нас этот corpus не задействован — в apps/evaluation/ тестовые данные другие.
Что НЕ закрыто
- 6 непортированных критериев — особенно
recognize_gptиrecognize_medical_context(Recognition layer для DQD),unit_converter(Normalization layer) - CompositeTraceEvaluator path (
check_structure/check_fda_compliance/check_prohibited_terms/required_fields/weights) — BG-1216 - Deterministic тестовый framework (95% threshold, math accuracy) — для unit conversions нужен другой подход
- Auto-assign scores в Langfuse — сейчас только в свою БД, drill-down работает но Langfuse Score Analytics пустой
- Прозрачность logprobs механики для команды — у G-Eval авторы тоже отмечают это как trade-off interpretability
- Test corpus Никиты (~100 JSON files в 10+ языках) не подключён ни к продакшену, ни к standalone CLI
- Re-evaluation после правки критерия автоматически не происходит — нужен manual trigger
- Consistency re-runs (отложенный «этап 4» из сессии 920d5967) — судить consistency между N запусками одного теста не реализовано
Связано
- data-quality-dashboard — главный потребитель в продакшене
- recognize-benchmark — параллельная дорожка в том же monorepo, про распознавание (P/R/F1 на датасете), не про live judges
- bifrost — LLM proxy через который ходит judge в продакшене
- litellm — fallback proxy
- dqd-implementation-approach — почему Hybrid (judge + Prisma store) вместо LangFuse-only
Источники
Сноски
-
G-Eval оригинальная статья: Liu et al. 2023, «G-Eval: NLG Evaluation using GPT-4 with Better Human Alignment», accessed 2026-05-17, https://arxiv.org/abs/2303.16634. ↩
-
DeepEval framework (используется upstream, у нас не используется), accessed 2026-05-17, https://github.com/confident-ai/deepeval. ↩
-
Сессия
ildar/920d5967, 2026-03-26 — ** (Mar 30-31. ↩