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 defaultDeepEval GEval метрика, рейтинг 1-5 через logprobsДа
Deterministic0.95 default, 5% toleranceCode-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.py344TraceJudge(GEval) — простой G-Eval judgeДа (как judge.ts)
criteria_evaluator.py418CompositeTraceEvaluator — composite pathНет (BG-1216)
criteria_registry.py382Registry для динамической регистрации evaluatorsНет — мы используем простой dict OBSERVATION_TO_CRITERIA
parameter_analysis.py382Structure validation для параметровНет
llm_judge.py355Generic LLM judge wrappersНет
fda_compliance.py339FDA non-device compliance проверкаНет (вшита в YAML критерии)
deterministic.py247Deterministic test frameworkНет
base.py69Base evaluator classНет

Полный список 11 критериев upstream

eval/eval/config/trace_criteria/:

КритерийThresholdLayerПортировано
single_parameter_analysis.yaml0.7AnalysisДа
panel_overview.yaml0.75AnalysisДа
test_overview.yaml0.75AnalysisДа
trend_analysis.yaml0.75AnalysisДа
followup_generation.yaml0.75AnalysisДа
panel_description.yaml?AnalysisНет
unit_converter.yaml0.9 (deterministic)NormalizationНет
parameter_range_gpt.yaml?Analysis (range generation)Нет
recognize_gpt.yaml0.75RecognitionНет
recognize_medical_context.yaml?RecognitionНет
default.yamlfallbackНет

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.tsloadCriteria(name) грузит YAML, кеширует, защищает от path traversal. buildEvaluationPrompt(criteria) собирает финальный prompt из criteria + evaluation_steps + feedback_instructions.
  • 5 YAML-критериев в src/criteria/:
ФайлThresholdСтрокЧто покрывает
single_parameter_analysis.yaml0.7627FDA non-device compliance, parameter identity verification, reasoning quality (Staging v24 schema)
trend_analysis.yaml0.753505-Step Clinical Reasoning Protocol (v15+), insight quality
followup_generation.yaml0.7532610-step reasoning, non-duplication, медицинские follow-up рекомендации (v11)
test_overview.yaml0.75220Educational depth, personalization, safety compliance
panel_overview.yaml0.75195Reasoning depth, completeness, compliance

Scoring через logprobs (G-Eval)

judge.ts:calculateWeightedScore:

  1. LLM просят выдать reasoning и в последней строке — integer 1-5 (rating scale)
  2. Запрос идёт с logprobs: true, top_logprobs: 5, temperature: 0, max_tokens: 2000
  3. Сканируется response с конца — ищется token, который parse-ится как 1-5
  4. Из top_logprobs собираются все альтернативные ratings 1-5 с их вероятностями (exp(logprob))
  5. Считается 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 — приоритет:

  1. JUDGE_BASE_URL (явный override)
  2. BIFROST_BASE_URL (bifrost — internal LLM proxy, продакшен)
  3. LITELLM_BASE_URL (litellm — старый proxy)
  4. 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

Источники

Источники: 1 2 3.

Сноски

  1. 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.

  2. DeepEval framework (используется upstream, у нас не используется), accessed 2026-05-17, https://github.com/confident-ai/deepeval.

  3. Сессия ildar/920d5967, 2026-03-26 — ** (Mar 30-31.