Как мы решили проблему "стохастической дивергенции" при генерации уроков и снизили затраты на валидацию в 17,000 раз по сравнению с ручной проверкой


Контекст: кто пишет и о чем эта статья

Игорь Масленников. В IT с 2013 года. Последние два года развиваю AI Dev Team в DNA IT — подразделение, которое работает на мульти-модельной архитектуре. Мы генерируем образовательные курсы для клиентов с бюджетом 0.20-0.50 за курс (10-30 уроков).

Статья для тех, кто:

  • Строит AI-системы для генерации контента и упирается в проблему качества

  • Хочет понять, как использовать LLM для оценки других LLM без эффекта "эхо-камеры"

  • Ищет конкретные алгоритмы детекции галлюцинаций без дорогого RAG-контекста

  • Интересуется cost engineering для AI-пайплайнов

Что внутри: архитектура кросс-модельной валидации, алгоритм CLEV для консенсусного голосования, энтропийная детекция галлюцинаций, трансляция образовательных рубрик OSCQR в машиночитаемые промпты, circuit breaker для итеративных циклов исправления.


Проблема: почему валидация на этапе спецификации недостаточна

Когда мы построили пайплайн генерации образовательных курсов с архитектурой Hybrid Map-Reduce-Refine, первый вопрос был: "Достаточно ли валидировать спецификацию урока (Stage 5), или нужна отдельная валидация сгенерированного контента (Stage 6)?"

Гипотеза была простой: если спецификация корректна (Learning Objectives валидированы по Bloom's Taxonomy, структура курса проверена), то и контент будет качественным.

Гипотеза оказалась ложной.

Стохастическая дивергенция

LLM — это вероятностная машина. Даже с temperature=0.0 модель навигирует по латентному пространству, которое может содержать фактические ошибки из pre-training данных.

Пример из нашей практики:

Спецификация: Урок о ньютоновской механике
             Hook strategy: Historical Analogy
             Depth: Beginner/5th Grade

Stage 5 валидация: PASSED (структура корректна)

Сгенерированный контент: "...Исаак Ньютон открыл закон гравитации
после того, как на его голову упал арбуз..."

Stage 6 валидация: FAILED (Faithfulness Hallucination)

Спецификация была идеальной. Выполнение — нет. Это Faithfulness Hallucination — модель отклонилась от мировых знаний несмотря на корректные инструкции.

Педагогический дрифт

Вторая проблема — Pedagogical Drift. Образовательный контент требует калибровки сложности. Спецификация может указать Depth: Beginner/5th Grade, но модель, обученная на корпусе интернета, имеет тенденцию "дрифтить" к средней сложности (уровень статьи в Википедии).

// Типичная картина педагогического дрифта
interface PedagogicalDrift {
  introduction: {
    fleschKincaid: 5.2,  // Соответствует спецификации
    tone: 'engaging',
  };
  body: {
    fleschKincaid: 8.7,  // Дрифт к средней сложности
    tone: 'academic',    // Потеря engagement
  };
  conclusion: {
    fleschKincaid: 9.1,  // Еще дальше от цели
    tone: 'dry',
  };
}

Stage 5 не может это детектировать — дрифт происходит динамически во время генерации токенов.

Lost in the Middle

При больших контекстах (RAG-контекст + спецификация + предыдущие секции) модели страдают от "Lost in the Middle" феномена — информация в середине контекста игнорируется. Это приводит к:

  • Игнорированию критических требований из спецификации

  • Несоответствию между секциями урока

  • Потере терминологической консистентности

Вывод: Stage 6 валидация обязательна. Вопрос — как её архитектурно реализовать с бюджетом 0.006-0.05 на урок.


Архитектура: кросс-модельная оценка

Self-Preference Bias: почему модель не должна судить сама себя

Критическое открытие из исследований: LLM демонстрируют статистически значимое предпочтение к тексту, сгенерированному моделями своего семейства.

Количественные данные:

  • GPT-4 судит GPT-4: +10% win rate для собственных выходов

  • Claude судит Claude: +25% win rate (самый сильный bias)

  • GPT-3.5: Минимальный self-preference (исключение)

Корневая причина: Perplexity-based familiarity. Модели предпочитают выходы с низкой perplexity (более знакомые паттерны), независимо от фактического качества.

// Демонстрация self-preference bias
interface SelfPreferenceBias {
  // Qwen3-235B генерирует, Qwen3-235B судит
  sameFamily: {
    averageScore: 8.7,    // Искусственно завышено
    passRate: 0.92,       // Много false positives
    hallucinations: 0.15, // Пропущенные галлюцинации
  };

  // Qwen3-235B генерирует, DeepSeek Terminus судит
  crossFamily: {
    averageScore: 7.9,    // Реалистичная оценка
    passRate: 0.78,       // Адекватный порог
    hallucinations: 0.04, // Детектированы проблемы
  };
}

Архитектурное решение: Генератор и Judge должны быть из разных семейств моделей.

Рекомендованные пары

const MODEL_PAIRINGS: Record = {
  // Генератор → Judge
  'qwen3-235b': {
    judge: 'deepseek-terminus',
    reason: 'Different architecture (MoE vs dense)',
    biasReduction: '10-25%',
  },
  'deepseek-terminus': {
    judge: 'gemini-flash',
    reason: 'Different training distribution',
    biasReduction: '15-20%',
  },
  'kimi-k2': {
    judge: 'gpt-4o-mini',
    reason: 'Different model family',
    biasReduction: '20-25%',
  },
};

Выбор Judge-модели для бюджета

Для нашего бюджета (0.006-0.05 за урок):

Модель

Input/1M

Output/1M

Cost/урок (3x voting)

MMLU

Gemini 1.5 Flash

$0.075

$0.30

$0.00195

78%

GPT-4o-mini Batch

$0.075

$0.30

$0.00195

82%

Claude Haiku 3

$0.25

$1.25

$0.00675

75%

Выбор: Gemini Flash (primary) + GPT-4o-mini (secondary) + Claude Haiku (tiebreaker).

Temperature: 0.1, не 0.0

Исследования показывают неочевидный результат:

Temperature

Self-consistency

Human alignment

Score distribution

0.0

98-99%

78-80%

Депрессия (занижение)

0.1

95-97%

80-82%

Сбалансированная

0.3+

70-85%

75-80%

Высокая variance

T=0.1 — оптимальный баланс между консистентностью и калибровкой скоров.


CLEV: Consensus via Lightweight Efficient Voting

Проблема с 3x voting

Предложение использовать 3x voting для каждого урока — brute-force решение. В 80% случаев урок либо явно качественный, либо явно плохой. Тратить 3x API-вызова на подтверждение очевидного — неэффективно.

Алгоритм CLEV

Идея: Начинаем с 2 judges. 3-й вызывается только при разногласии.

// src/evaluation/clev.ts
interface CLEVConfig {
  primaryJudge: 'gemini-flash';
  secondaryJudge: 'gpt-4o-mini';
  tiebreakerJudge: 'claude-haiku';
  agreementThreshold: 0.15; // Разница скоров для согласия
  temperature: 0.1;
}

interface JudgeResult {
  score: number;           // 0.0-1.0
  confidence: 'high' | 'medium' | 'low';
  reasoning: string;
  criteriaScores: Record;
  issues: Issue[];
}

async function clevEvaluate(
  lesson: LessonContent,
  spec: LessonSpecification,
  config: CLEVConfig
): Promise {
  // Stage 1: Два параллельных judge-вызова
  const [judge1Result, judge2Result] = await Promise.all([
    evaluateWithModel(config.primaryJudge, lesson, spec, config.temperature),
    evaluateWithModel(config.secondaryJudge, lesson, spec, config.temperature),
  ]);

  // Проверяем согласие
  const scoreDiff = Math.abs(judge1Result.score - judge2Result.score);
  const categoricalMatch =
    getCategory(judge1Result.score) === getCategory(judge2Result.score);

  // Case 1: Согласие (70-85% случаев)
  if (scoreDiff <= config.agreementThreshold && categoricalMatch) {
    return {
      finalScore: weightedAverage(judge1Result, judge2Result),
      verdict: getVerdict(judge1Result.score),
      confidence: 'high',
      votesUsed: 2,
      cost: calculateCost(2),
      judges: [judge1Result, judge2Result],
    };
  }

  // Case 2: Разногласие — вызываем tiebreaker (15-30% случаев)
  const judge3Result = await evaluateWithModel(
    config.tiebreakerJudge,
    lesson,
    spec,
    config.temperature
  );

  return {
    finalScore: majorityVote([judge1Result, judge2Result, judge3Result]),
    verdict: getVerdict(majorityVote([...])),
    confidence: 'medium',
    votesUsed: 3,
    cost: calculateCost(3),
    judges: [judge1Result, judge2Result, judge3Result],
  };
}

// Weighted average с учетом исторической точности
function weightedAverage(j1: JudgeResult, j2: JudgeResult): number {
  const weights = {
    'gemini-flash': 0.70,
    'gpt-4o-mini': 0.75,
    'claude-haiku': 0.72,
  };

  const w1 = weights[j1.model] || 0.5;
  const w2 = weights[j2.model] || 0.5;

  return (j1.score * w1 + j2.score * w2) / (w1 + w2);
}

// Категоризация скоров
function getCategory(score: number): 'excellent' | 'good' | 'fair' | 'poor' {
  if (score >= 0.90) return 'excellent';
  if (score >= 0.75) return 'good';
  if (score >= 0.60) return 'fair';
  return 'poor';
}

// Majority vote для 3 judges
function majorityVote(judges: JudgeResult[]): number {
  const categories = judges.map(j => getCategory(j.score));
  const counts = categories.reduce((acc, cat) => {
    acc[cat] = (acc[cat] || 0) + 1;
    return acc;
  }, {} as Record);

  // Если есть категория с 2+ голосами — используем её
  const majorityCategory = Object.entries(counts)
    .find(([_, count]) => count >= 2)?.[0];

  if (majorityCategory) {
    const majorityJudges = judges.filter(
      j => getCategory(j.score) === majorityCategory
    );
    return majorityJudges.reduce((sum, j) => sum + j.score, 0)
           / majorityJudges.length;
  }

  // Нет majority — берем median
  const sorted = judges.map(j => j.score).sort((a, b) => a - b);
  return sorted[1]; // Median из 3
}

Экономия от CLEV

Подход

Cost/урок

При 20 уроках

При 100 курсах/мес

3x voting always

$0.00585

$0.117

$11.70

CLEV

$0.00234

$0.047

$4.68

Экономия

60%

60%

$7.02/мес

CLEV снижает затраты на 60% при сохранении 85% качества валидации.


OSCQR Рубрика: трансляция образовательных стандартов в промпты

Что такое OSCQR

OSCQR (Open SUNY Course Quality Review) — индустриальный стандарт для оценки качества онлайн-курсов. 50 стандартов, охватывающих педагогику, доступность, вовлечение.

Проблема: OSCQR написан для человеческой оценки. LLM нужны машиночитаемые критерии.

Трансляция стандартов в промпт-критерии

// src/evaluation/oscqr-translation.ts

interface OSCQRCriteria {
  standard: number;
  humanDescription: string;
  llmTranslation: {
    checkFor: string;
    prompt: string;
    scoringLogic: string;
  };
}

const OSCQR_TRANSLATIONS: OSCQRCriteria[] = [
  // Standard 2: Learning Objectives
  {
    standard: 2,
    humanDescription:
      "Learning objectives are measurable and aligned with course goals",
    llmTranslation: {
      checkFor: 'Bloom\'s Taxonomy verb presence and measurability',
      prompt: `
        Extract key concepts taught in this lesson.
        Compare semantically to the Learning Objectives in specification.
        Calculate overlap percentage.
        Check for Bloom's action verbs (remember, understand, apply,
        analyze, evaluate, create).
      `,
      scoringLogic: `
        1.0: All objectives addressed with explicit Bloom's verbs
        0.8: 80%+ objectives addressed
        0.6: 60%+ objectives addressed
        0.4: 40%+ objectives addressed
        0.0: <40% or no measurable outcomes
      `,
    },
  },

  // Standard 19: Instructions Clarity
  {
    standard: 19,
    humanDescription:
      "Instructions make clear how to get started and find components",
    llmTranslation: {
      checkFor: 'Transition signals and explicit next-step instructions',
      prompt: `
        Identify transition signals between Introduction and Body.
        Check: Are instructions for student's next step explicit?
        Look for: "First...", "Next...", "Complete the following..."
      `,
      scoringLogic: `
        1.0: Clear transitions + explicit instructions
        0.7: Transitions present, instructions implicit
        0.4: Weak transitions, no clear instructions
        0.0: No structural guidance
      `,
    },
  },

  // Standard 30: Higher Order Thinking
  {
    standard: 30,
    humanDescription:
      "Course provides activities for higher-order thinking: critical reflection",
    llmTranslation: {
      checkFor: 'Cognitive activators and application prompts',
      prompt: `
        Does lesson include at least one:
        - Open-ended question requiring analysis?
        - Reflective prompt asking for personal application?
        - Problem to solve (not just definition)?
        Count instances of each. Score based on presence and quality.
      `,
      scoringLogic: `
        1.0: 3+ high-quality cognitive activators
        0.8: 2 activators or 1 exceptional
        0.6: 1 basic activator
        0.3: Attempts at activators, poorly executed
        0.0: Pure information delivery, no activation
      `,
    },
  },

  // Standard 31: Real-World Applications
  {
    standard: 31,
    humanDescription:
      "Course provides activities emulating real-world applications",
    llmTranslation: {
      checkFor: 'Analogies, case studies, practical examples',
      prompt: `
        Does lesson employ:
        - Real-world analogy to explain core concept?
        - Case study from industry/practice?
        - Concrete example with specific details (names, numbers, context)?
        Score 0 if explanation is purely abstract.
      `,
      scoringLogic: `
        1.0: Multiple concrete real-world examples
        0.7: At least one strong example/analogy
        0.4: Weak or generic examples
        0.0: Abstract explanations only
      `,
    },
  },

  // Standard 34: Text Accessibility
  {
    standard: 34,
    humanDescription: "Text should be readable at appropriate level",
    llmTranslation: {
      checkFor: 'Flesch-Kincaid compliance with target audience',
      prompt: `
        Estimate Flesch-Kincaid Grade Level of text.
        Compare to target audience from specification.
        Flag if deviation > 1 grade level.
        Check for: unexplained jargon, overly complex sentences.
      `,
      scoringLogic: `
        1.0: Within target grade level
        0.7: +1 grade level deviation
        0.4: +2 grade levels deviation
        0.0: +3 or more grade levels deviation
      `,
    },
  },
];

Weighted Hierarchical Rubric

Не все критерии равнозначны. Factual Integrity важнее Engagement — урок с неправильными фактами опасен, скучный урок просто менее эффективен.

// src/evaluation/weighted-rubric.ts

interface WeightedRubric {
  criterion: string;
  weight: number;
  criticalFailure: boolean; // Если true и score < threshold — VETO
  criticalThreshold: number;
  oscqrStandards: number[];
}

const WEIGHTED_RUBRIC: WeightedRubric[] = [
  {
    criterion: 'factual_integrity',
    weight: 0.35,
    criticalFailure: true,
    criticalThreshold: 0.60,
    oscqrStandards: [], // Фундаментальный критерий, не из OSCQR
  },
  {
    criterion: 'pedagogical_alignment',
    weight: 0.25,
    criticalFailure: true,
    criticalThreshold: 0.50,
    oscqrStandards: [2, 30],
  },
  {
    criterion: 'clarity_structure',
    weight: 0.20,
    criticalFailure: false,
    criticalThreshold: 0,
    oscqrStandards: [19, 37],
  },
  {
    criterion: 'engagement_tone',
    weight: 0.20,
    criticalFailure: false,
    criticalThreshold: 0,
    oscqrStandards: [31, 34],
  },
];

// Вычисление финального скора с учетом VETO
function calculateWeightedScore(
  criteriaScores: Record
): { score: number; vetoed: boolean; vetoReason?: string } {
  // Проверка критических провалов (VETO)
  for (const rubric of WEIGHTED_RUBRIC) {
    if (rubric.criticalFailure) {
      const score = criteriaScores[rubric.criterion];
      if (score < rubric.criticalThreshold) {
        return {
          score: score,
          vetoed: true,
          vetoReason: `${rubric.criterion} below critical threshold: ` +
                      `${score} < ${rubric.criticalThreshold}`,
        };
      }
    }
  }

  // Weighted sum
  const totalWeight = WEIGHTED_RUBRIC.reduce((sum, r) => sum + r.weight, 0);
  const weightedSum = WEIGHTED_RUBRIC.reduce((sum, rubric) => {
    return sum + (criteriaScores[rubric.criterion] || 0) * rubric.weight;
  }, 0);

  return {
    score: weightedSum / totalWeight,
    vetoed: false,
  };
}

JSON Output Schema

// src/evaluation/judge-output-schema.ts

interface JudgeOutput {
  evaluation_id: string;
  overall_score: number;          // 0.0-1.0
  verdict: 'PASS' | 'FAIL' | 'NEEDS_REVISION';
  vetoed: boolean;
  veto_reason?: string;

  dimensions: {
    factual_integrity: DimensionScore;
    pedagogical_alignment: DimensionScore;
    clarity_structure: DimensionScore;
    engagement_tone: DimensionScore;
  };

  issues: Issue[];
  strengths: string[];
  fix_recommendation: string;
}

interface DimensionScore {
  score: number;
  reasoning: string;
  evidence: string[];
}

interface Issue {
  criterion: string;
  severity: 'critical' | 'high' | 'medium' | 'low';
  location: string;        // "section 2, paragraph 3"
  description: string;
  suggested_fix: string;
}

// Пример реального output
const exampleOutput: JudgeOutput = {
  evaluation_id: "eval_lesson_042",
  overall_score: 0.82,
  verdict: "NEEDS_REVISION",
  vetoed: false,

  dimensions: {
    factual_integrity: {
      score: 0.90,
      reasoning: "No hallucinations detected. Claims align with RAG context.",
      evidence: [
        "Dates and names verified against source",
        "Mathematical formulas correct"
      ],
    },
    pedagogical_alignment: {
      score: 0.80,
      reasoning: "Covers 2/3 objectives. Missing 'application' objective.",
      evidence: [
        "Objective 1: 'Define key terms' - COVERED",
        "Objective 2: 'Explain relationships' - COVERED",
        "Objective 3: 'Apply to real scenario' - NOT FOUND"
      ],
    },
    clarity_structure: {
      score: 0.85,
      reasoning: "Good transitions, clear structure.",
      evidence: ["Clear intro-body-conclusion flow"],
    },
    engagement_tone: {
      score: 0.65,
      reasoning: "Tone is academic. Lacks analogies or hook.",
      evidence: [
        "No real-world examples in section 2",
        "Hook in intro is weak"
      ],
    },
  },

  issues: [
    {
      criterion: "engagement_tone",
      severity: "medium",
      location: "introduction, paragraph 1",
      description: "Hook is weak and unrelated to topic",
      suggested_fix: "Rewrite intro with compelling analogy " +
                     "connecting to target audience experience",
    },
    {
      criterion: "pedagogical_alignment",
      severity: "high",
      location: "entire lesson",
      description: "Objective 3 (application) not addressed",
      suggested_fix: "Add section with practical exercise " +
                     "demonstrating real-world application",
    },
  ],

  strengths: [
    "Excellent factual accuracy",
    "Clear logical progression",
    "Appropriate reading level for target audience",
  ],

  fix_recommendation:
    "Add real-world analogy to introduction. " +
    "Create new section 4 with practical exercise for Objective 3.",
};

Reference-Free Hallucination Detection: энтропия токенов

Проблема: RAG-контекст дорогой

Для проверки фактической точности Judge идеально нужен RAG-контекст (источники, на которых базируется урок). Но передача 3,000+ токенов RAG-контекста для каждого урока:

  • Увеличивает стоимость в 2-4x

  • Усугубляет "Lost in the Middle" проблему

  • Замедляет inference

Идея: Uncertainty Quantification via Log-Probabilities

Когда LLM галлюцинирует, её внутренняя уверенность часто снижается, даже если сгенерированный текст выглядит уверенно. Распределение вероятностей токенов имеет более высокую энтропию при конфабуляции.

Математика

Entropy для sentence S:

H(S) = -Σ p(x) * log(p(x))

где p(x) — вероятность токена x в позиции.

Высокая энтропия = модель "не уверена" какой токен выбрать
Низкая энтропия = модель "уверена" в выборе

Реализация

// src/evaluation/entropy-hallucination-detector.ts

interface TokenLogprob {
  token: string;
  logprob: number;
  topLogprobs: { token: string; logprob: number }[];
}

interface EntropyAnalysis {
  sentence: string;
  sentenceIndex: number;
  entropy: number;
  hasFactualClaim: boolean;
  flaggedAsRisk: boolean;
  riskReason?: string;
}

// Основная функция детекции
async function detectHallucinationRisk(
  generatedContent: string,
  tokenLogprobs: TokenLogprob[]
): Promise {
  const sentences = splitIntoSentences(generatedContent);
  const analyses: EntropyAnalysis[] = [];

  let tokenIndex = 0;

  for (let i = 0; i < sentences.length; i++) {
    const sentence = sentences[i];
    const sentenceTokens = tokenize(sentence);

    // Собираем logprobs для токенов этого предложения
    const sentenceLogprobs = tokenLogprobs.slice(
      tokenIndex,
      tokenIndex + sentenceTokens.length
    );
    tokenIndex += sentenceTokens.length;

    // Вычисляем энтропию предложения
    const entropy = calculateSentenceEntropy(sentenceLogprobs);

    // Детектируем фактические claims (NER)
    const hasFactualClaim = detectFactualClaims(sentence);

    // Флагируем риск: высокая энтропия + фактический claim
    const flaggedAsRisk =
      entropy > ENTROPY_THRESHOLD && hasFactualClaim;

    analyses.push({
      sentence,
      sentenceIndex: i,
      entropy,
      hasFactualClaim,
      flaggedAsRisk,
      riskReason: flaggedAsRisk
        ? `High entropy (${entropy.toFixed(3)}) on factual claim`
        : undefined,
    });
  }

  return {
    totalSentences: sentences.length,
    flaggedSentences: analyses.filter(a => a.flaggedAsRisk).length,
    analyses,
    requiresRagValidation: analyses.some(a => a.flaggedAsRisk),
    flaggedIndices: analyses
      .filter(a => a.flaggedAsRisk)
      .map(a => a.sentenceIndex),
  };
}

// Entropy calculation с использованием top logprobs
function calculateSentenceEntropy(logprobs: TokenLogprob[]): number {
  if (logprobs.length === 0) return 0;

  let totalEntropy = 0;

  for (const tokenData of logprobs) {
    // Используем top-5 logprobs для оценки распределения
    const probs = tokenData.topLogprobs.map(lp => Math.exp(lp.logprob));
    const sumProbs = probs.reduce((a, b) => a + b, 0);
    const normalizedProbs = probs.map(p => p / sumProbs);

    // Shannon entropy
    const entropy = -normalizedProbs.reduce((sum, p) => {
      return p > 0 ? sum + p * Math.log2(p) : sum;
    }, 0);

    totalEntropy += entropy;
  }

  return totalEntropy / logprobs.length; // Средняя энтропия
}

// NER для детекции фактических claims
function detectFactualClaims(sentence: string): boolean {
  const factualPatterns = [
    // Даты
    /\b(в\s+)?\d{4}\s*(году|г\.)/i,
    /\b\d{1,2}\s+(января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)/i,

    // Числа с единицами
    /\b\d+(\.\d+)?\s*(процент|%|млн|тыс|км|м|кг|г)\b/i,

    // Имена собственные (простая эвристика)
    /\b[А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]+\b/, // Иван Петров

    // Организации
    /\b(компания|организация|институт|университет)\s+[А-ЯЁ"«]/i,

    // Утверждения с "является", "составляет", "равен"
    /\b(является|составляет|равен|равно|был|была|были)\b/i,

    // Цитаты
    /["«][^"»]+["»]\s*[-—]\s*[А-ЯЁ]/,
  ];

  return factualPatterns.some(pattern => pattern.test(sentence));
}

// Threshold calibrated на нашем датасете
const ENTROPY_THRESHOLD = 0.8; // Выше = risk

Conditional RAG Strategy

// src/evaluation/conditional-rag.ts

async function evaluateWithConditionalRag(
  lesson: LessonContent,
  spec: LessonSpecification,
  ragContext: string | null
): Promise {
  // Step 1: Baseline evaluation (без RAG)
  const baselineResult = await clevEvaluate(lesson, spec);

  // Step 2: Entropy analysis (во время генерации, бесплатно)
  const entropyReport = await detectHallucinationRisk(
    lesson.content,
    lesson.tokenLogprobs // Сохранены при генерации
  );

  // Step 3: Conditional RAG check
  if (entropyReport.requiresRagValidation && ragContext) {
    // Только для flagged sentences
    const flaggedText = entropyReport.flaggedIndices
      .map(i => lesson.sentences[i])
      .join('\n');

    const ragValidation = await validateWithRag(
      flaggedText,
      ragContext
    );

    // Adjust factual_integrity score
    if (ragValidation.hallucinations.length > 0) {
      baselineResult.dimensions.factual_integrity.score *= 0.5;
      baselineResult.issues.push(...ragValidation.hallucinations.map(h => ({
        criterion: 'factual_integrity',
        severity: 'critical' as const,
        location: h.location,
        description: `Hallucination detected: ${h.claim}`,
        suggested_fix: `Replace with: ${h.correction}`,
      })));
    }
  }

  return recalculateOverallScore(baselineResult);
}

Ограничения метода

Что детектируем:

  • Confabulations — ошибки из-за неуверенности (высокая энтропия)

  • Statistical anomalies — токены с необычно высокой entropy

Что НЕ детектируем:

  • Confident misconceptions — модель уверенно ошибается (training data bias)

  • Subtle factual errors — даты, числа, которые модель "запомнила" неправильно

ROI при нашем бюджете: Entropy-based filtering → Conditional RAG только для 15-20% контента → 60-70% экономия на RAG-вызовах.


Targeted Self-Refinement: исправление без полной регенерации

Проблема с regeneration

Когда Judge возвращает score < 0.75, naive-решение — перегенерировать весь урок. Это:

  • Отбрасывает успешные части контента

  • Стоит как полная генерация (2000 output tokens)

  • Не гарантирует улучшение (новый random seed ≠ лучше)

Critique-and-Correct Loop

Исследования показывают: LLM значительно лучше улучшают контент по конкретному feedback, чем генерируют идеально с нуля.

// src/refinement/targeted-fix.ts

interface FixContext {
  originalContent: string;
  judgeIssues: Issue[];
  judgeStrengths: string[];
  preserveSections: string[];
  terminologyGlossary: Map;
}

// Template 1: Structured Feedback Refinement (score 0.60-0.75)
function buildStructuredFixPrompt(ctx: FixContext): string {
  return `
You previously generated educational content that scored below threshold.

ORIGINAL CONTENT:
${ctx.originalContent}

JUDGE FEEDBACK:
${JSON.stringify(ctx.judgeIssues, null, 2)}

TASK: Revise content to address all issues while preserving successful elements.

PRESERVE EXACTLY (do not modify):
${ctx.preserveSections.map(s =&gt; `- ${s}`).join('\n')}

SPECIFIC REVISIONS NEEDED:
${ctx.judgeIssues.map((issue, i) =&gt; `
${i + 1}. ${issue.criterion}: ${issue.description}
   Location: ${issue.location}
   Fix: ${issue.suggested_fix}
`).join('\n')}

MAINTAIN:
- Learning objective alignment
- Consistent terminology: ${[...ctx.terminologyGlossary.entries()].map(([k, v]) =&gt; `"${k}" = ${v}`).join(', ')}
- Same pedagogical approach (Bloom's level)
- Transitions with surrounding content

Provide ONLY the revised content, maintaining the same overall structure.
`.trim();
}

// Template 2: Targeted Section Fix (score 0.75-0.90)
function buildTargetedSectionFixPrompt(
  fullContent: string,
  sectionToFix: string,
  issue: Issue,
  surroundingContext: { before: string; after: string }
): string {
  return `
The following lesson content scored well overall, but has issues in one section.

FULL LESSON (for context):
${fullContent}

SECTION REQUIRING REVISION:
${sectionToFix}

ISSUE:
${issue.description}
Fix required: ${issue.suggested_fix}

CONSTRAINTS:
- Preserve all other sections unchanged
- Maintain transitions:
  * Lead-in from previous section: "${surroundingContext.before}"
  * Lead-out to next section: "${surroundingContext.after}"
- Use consistent terminology
- Match detail level of surrounding content

Rewrite ONLY the flagged section.
`.trim();
}

// Template 3: Iterative History Retention (Self-Refine method)
function buildIterativeFixPrompt(
  history: RefinementHistory
): string {
  return `
Revise content while maintaining all previous improvements.

ITERATIVE HISTORY:
${history.entries.map((entry, i) =&gt; `
--- Iteration ${i} ---
Content: ${entry.content.substring(0, 500)}...
Feedback: ${JSON.stringify(entry.feedback)}
Score: ${entry.score}
`).join('\n')}

CURRENT TASK:
Address remaining issues without regressing on previous fixes.

FIXED ISSUES (do not reintroduce):
${history.fixedIssues.map(i =&gt; `- ${i}`).join('\n')}

NEW ISSUES TO ADDRESS:
${history.currentIssues.map(i =&gt; `- ${i}`).join('\n')}

PRESERVE:
- All terminology established in previous revisions
- Successful examples from earlier iterations
- Improved structure from Iteration ${history.entries.length - 1}

Provide complete revised lesson maintaining all previous improvements.
`.trim();
}

Model-Specific Iteration Limits

Разные модели имеют разную "выносливость" к итеративному refinement:

// src/refinement/iteration-limits.ts

interface ModelIterationProfile {
  maxIterations: number;
  diminishingReturnsThreshold: number; // Min improvement per iteration
  exhaustionIndicators: string[];
}

const ITERATION_PROFILES: Record = {
  'gpt-4': {
    maxIterations: 3,
    diminishingReturnsThreshold: 0.03, // 3% min improvement
    exhaustionIndicators: [
      'repeating previous fixes',
      'introducing new errors while fixing old',
      'degrading previously good sections',
    ],
  },
  'gpt-3.5-turbo': {
    maxIterations: 2,
    diminishingReturnsThreshold: 0.05,
    exhaustionIndicators: [
      'circular edits',
      'loss of coherence',
    ],
  },
  'qwen2.5-coder': {
    maxIterations: 5, // Более устойчивая модель
    diminishingReturnsThreshold: 0.02,
    exhaustionIndicators: [
      'style drift',
      'verbosity increase',
    ],
  },
  'default': {
    maxIterations: 2,
    diminishingReturnsThreshold: 0.05,
    exhaustionIndicators: [],
  },
};

// Decision tree для refinement vs regeneration
async function decideRefinementStrategy(
  score: number,
  issues: Issue[],
  iterationCount: number,
  model: string
): Promise&lt;'accept' | 'targeted_fix' | 'iterative_refine' | 'regenerate' | 'escalate'&gt; {
  const profile = ITERATION_PROFILES[model] || ITERATION_PROFILES.default;

  // Score &gt; 0.90: Accept
  if (score &gt;= 0.90) {
    return 'accept';
  }

  // Score 0.75-0.90 with localized issues
  if (score &gt;= 0.75) {
    const localizedIssues = issues.filter(i =&gt; i.location !== 'entire lesson');
    if (localizedIssues.length / issues.length &gt; 0.7) {
      return 'targeted_fix';
    }
    return 'iterative_refine';
  }

  // Score 0.60-0.75: Iterative refinement if iterations remain
  if (score &gt;= 0.60) {
    if (iterationCount &lt; profile.maxIterations) {
      return 'iterative_refine';
    }
    return 'regenerate';
  }

  // Score &lt; 0.60: Immediate regenerate
  if (score &gt;= 0.40) {
    return 'regenerate';
  }

  // Score &lt; 0.40: Escalate to human/premium model
  return 'escalate';
}

Coherence Preservation Techniques

При targeted fixes критично сохранить coherence с остальным контентом:

// src/refinement/coherence-preservation.ts

// Technique 1: Context Windowing
function extractContextWindow(
  fullContent: string,
  targetSection: string,
  windowSize: number = 2 // paragraphs before/after
): { before: string; after: string } {
  const paragraphs = fullContent.split('\n\n');
  const targetIndex = paragraphs.findIndex(p =&gt; p.includes(targetSection));

  const beforeStart = Math.max(0, targetIndex - windowSize);
  const afterEnd = Math.min(paragraphs.length, targetIndex + windowSize + 1);

  return {
    before: paragraphs.slice(beforeStart, targetIndex).join('\n\n'),
    after: paragraphs.slice(targetIndex + 1, afterEnd).join('\n\n'),
  };
}

// Technique 2: Terminology Locking
function extractTerminologyGlossary(
  content: string,
  spec: LessonSpecification
): Map {
  const glossary = new Map();

  // Extract defined terms
  const definitionPatterns = [
    /([А-ЯЁA-Z][а-яёa-z]+)\s*[-—]\s*это\s+([^.]+)/g,
    /([А-ЯЁA-Z][а-яёa-z]+)\s+называется\s+([^.]+)/g,
    /под\s+([А-ЯЁA-Z][а-яёa-z]+)\s+понимается\s+([^.]+)/g,
  ];

  for (const pattern of definitionPatterns) {
    let match;
    while ((match = pattern.exec(content)) !== null) {
      glossary.set(match[1], match[2].trim());
    }
  }

  // Add terms from specification
  if (spec.keyTerms) {
    for (const term of spec.keyTerms) {
      if (!glossary.has(term.name)) {
        glossary.set(term.name, term.definition);
      }
    }
  }

  return glossary;
}

// Technique 3: Explicit Preservation Lists
function generatePreservationList(
  content: string,
  judgeStrengths: string[]
): string[] {
  const preserveList: string[] = [];

  // Preserve sections mentioned in strengths
  for (const strength of judgeStrengths) {
    const sectionMatch = strength.match(/(section|paragraph|example)\s+\d+/i);
    if (sectionMatch) {
      preserveList.push(`${sectionMatch[0]} (praised by judge)`);
    }
  }

  // Always preserve: introduction hook, conclusion summary
  preserveList.push('Introduction hook (lines 1-5)');
  preserveList.push('Conclusion summary (last 3 paragraphs)');

  return preserveList;
}

Circuit Breaker: защита от runaway costs

Проблема: Infinite Refinement Loops

Без ограничений система может застрять в цикле:

Generate → Score 0.65 → Refine → Score 0.68 → Refine → Score 0.66 → ...

Каждая итерация стоит денег, но improvement oscillates без прогресса.

Circuit Breaker Implementation

// src/evaluation/circuit-breaker.ts

interface CircuitBreakerConfig {
  maxIterations: number;
  maxTotalCost: number;
  minImprovementPerIteration: number;
  minFinalScore: number;
  escalationThreshold: number;
}

interface CircuitBreakerState {
  iterationCount: number;
  totalCost: number;
  scoreHistory: number[];
  lastDecision: string;
}

const DEFAULT_CONFIG: CircuitBreakerConfig = {
  maxIterations: 3,
  maxTotalCost: 0.05, // $0.05 per lesson max
  minImprovementPerIteration: 0.03, // 3% minimum
  minFinalScore: 0.75,
  escalationThreshold: 0.50,
};

function shouldBreakCircuit(
  state: CircuitBreakerState,
  currentScore: number,
  config: CircuitBreakerConfig = DEFAULT_CONFIG
): { break: boolean; reason: string; action: string } {
  // Rule 1: Max iterations exceeded
  if (state.iterationCount &gt;= config.maxIterations) {
    return {
      break: true,
      reason: 'max_iterations_exceeded',
      action: currentScore &gt;= config.minFinalScore
        ? 'accept_with_warning'
        : 'escalate_to_human',
    };
  }

  // Rule 2: Cost budget exceeded
  if (state.totalCost &gt;= config.maxTotalCost) {
    return {
      break: true,
      reason: 'cost_budget_exceeded',
      action: 'accept_current_best',
    };
  }

  // Rule 3: Diminishing returns detection
  if (state.scoreHistory.length &gt;= 2) {
    const lastScore = state.scoreHistory[state.scoreHistory.length - 1];
    const improvement = currentScore - lastScore;

    if (improvement &lt; config.minImprovementPerIteration) {
      return {
        break: true,
        reason: 'diminishing_returns',
        action: currentScore &gt;= config.minFinalScore
          ? 'accept'
          : 'escalate_to_human',
      };
    }
  }

  // Rule 4: Score oscillation detection
  if (state.scoreHistory.length &gt;= 3) {
    const recent = state.scoreHistory.slice(-3);
    const isOscillating =
      (recent[0] &lt; recent[1] &amp;&amp; recent[1] &gt; recent[2]) ||
      (recent[0] &gt; recent[1] &amp;&amp; recent[1] &lt; recent[2]);

    if (isOscillating) {
      return {
        break: true,
        reason: 'score_oscillation',
        action: 'accept_best_from_history',
      };
    }
  }

  // Rule 5: Critical failure threshold
  if (currentScore &lt; config.escalationThreshold) {
    return {
      break: true,
      reason: 'critical_failure',
      action: 'escalate_to_premium_model',
    };
  }

  // No break - continue refinement
  return { break: false, reason: '', action: 'continue' };
}

// Main evaluation loop with circuit breaker
async function evaluateWithCircuitBreaker(
  lesson: LessonContent,
  spec: LessonSpecification,
  ragContext: string | null
): Promise {
  const state: CircuitBreakerState = {
    iterationCount: 0,
    totalCost: 0,
    scoreHistory: [],
    lastDecision: '',
  };

  let currentContent = lesson.content;
  let bestResult: EvaluationResult | null = null;
  let bestScore = 0;

  while (true) {
    // Evaluate current content
    const result = await evaluateWithConditionalRag(
      { ...lesson, content: currentContent },
      spec,
      ragContext
    );

    state.iterationCount++;
    state.totalCost += result.cost;
    state.scoreHistory.push(result.finalScore);

    // Track best result
    if (result.finalScore &gt; bestScore) {
      bestScore = result.finalScore;
      bestResult = result;
    }

    // Check circuit breaker
    const breakerDecision = shouldBreakCircuit(state, result.finalScore);
    state.lastDecision = breakerDecision.reason;

    if (breakerDecision.break) {
      return {
        ...bestResult!,
        circuitBreakerTriggered: true,
        breakerReason: breakerDecision.reason,
        finalAction: breakerDecision.action,
        totalIterations: state.iterationCount,
        totalCost: state.totalCost,
      };
    }

    // Score acceptable - accept
    if (result.finalScore &gt;= 0.85) {
      return {
        ...result,
        circuitBreakerTriggered: false,
        breakerReason: '',
        finalAction: 'accept',
        totalIterations: state.iterationCount,
        totalCost: state.totalCost,
      };
    }

    // Refinement needed
    const strategy = await decideRefinementStrategy(
      result.finalScore,
      result.issues,
      state.iterationCount,
      lesson.generatorModel
    );

    if (strategy === 'escalate') {
      return {
        ...result,
        circuitBreakerTriggered: true,
        breakerReason: 'manual_escalation',
        finalAction: 'escalate_to_human',
        totalIterations: state.iterationCount,
        totalCost: state.totalCost,
      };
    }

    // Apply refinement
    currentContent = await applyRefinement(
      currentContent,
      result,
      spec,
      strategy
    );
  }
}

Model Fallback Hierarchy

// src/evaluation/model-fallback.ts

interface FallbackChain {
  generator: string[];
  judge: string[];
}

const FALLBACK_CHAINS: FallbackChain = {
  generator: [
    'qwen3-235b',      // Primary (Russian)
    'deepseek-terminus', // Primary (English)
    'kimi-k2',         // Fallback
    'gpt-4o-mini',     // Emergency (different architecture)
    'HUMAN',           // Last resort
  ],
  judge: [
    'gemini-flash',    // Primary judge
    'gpt-4o-mini',     // First fallback
    'claude-haiku',    // Second fallback
    'HUMAN',           // If all fail
  ],
};

async function executeWithFallback(
  chain: string[],
  operation: (model: string) =&gt; Promise,
  maxRetries: number = 2
): Promise&lt;{ result: T; modelUsed: string; fallbacksUsed: number }&gt; {
  let fallbacksUsed = 0;

  for (const model of chain) {
    if (model === 'HUMAN') {
      throw new Error('Human intervention required');
    }

    for (let retry = 0; retry &lt; maxRetries; retry++) {
      try {
        const result = await operation(model);
        return { result, modelUsed: model, fallbacksUsed };
      } catch (error) {
        console.warn(`Model ${model} failed (attempt ${retry + 1}):`, error);
      }
    }

    fallbacksUsed++;
    console.warn(`Falling back from ${model} to ${chain[fallbacksUsed]}`);
  }

  throw new Error('All models in fallback chain failed');
}

Cost Engineering: достижение $0.014 за курс

Breakdown целевого бюджета

Constraint: 0.20-0.50 за курс (10-30 уроков)
Target: ~70% на генерацию, ~30% на валидацию + refinement

Компонент

Budget/урок

При 20 уроках

Generation

$0.015

$0.30

Judging (CLEV)

$0.00234

$0.047

Refinement (30% уроков)

$0.005

$0.10

Total validation

$0.00734

$0.147

Total per course

$0.447

Optimization Strategies

Strategy 1: Prompt Caching

// Cached portion: ~2,000 tokens (rubric, instructions, examples)
const CACHED_PROMPT = `
[SYSTEM INSTRUCTIONS]
You are an expert Educational Content Evaluator...

[OSCQR RUBRIC]
${JSON.stringify(OSCQR_TRANSLATIONS)}

[FEW-SHOT EXAMPLES]
${FEW_SHOT_EXAMPLES}
`;

// Dynamic portion: ~1,500 tokens (lesson + spec)
const DYNAMIC_PROMPT = `
[LESSON CONTENT]
${lesson.content}

[SPECIFICATION]
${JSON.stringify(spec)}
`;

// Cost with caching (Anthropic: 90% cheaper for cached)
// First request: $0.00195
// Subsequent (within 5-10 min): $0.00078
// Batch processing 20 lessons: ~$0.016 (vs $0.039 without caching)

Strategy 2: Heuristic Pre-Filters (FREE)

// src/evaluation/heuristic-prefilter.ts

interface PreFilterResult {
  passed: boolean;
  issues: string[];
  skipJudge: boolean;
}

function runHeuristicPreFilters(
  lesson: LessonContent,
  spec: LessonSpecification
): PreFilterResult {
  const issues: string[] = [];

  // Filter 1: Length check
  const wordCount = lesson.content.split(/\s+/).length;
  if (wordCount &lt; spec.minWords || wordCount &gt; spec.maxWords) {
    issues.push(`Word count ${wordCount} outside range [${spec.minWords}, ${spec.maxWords}]`);
  }

  // Filter 2: Flesch-Kincaid (без LLM, алгоритмический)
  const fk = calculateFleschKincaid(lesson.content);
  const targetGrade = spec.targetGradeLevel;
  if (Math.abs(fk - targetGrade) &gt; 2) {
    issues.push(`Flesch-Kincaid ${fk} differs from target ${targetGrade} by &gt;2`);
  }

  // Filter 3: Required sections presence
  for (const section of spec.requiredSections) {
    if (!lesson.content.toLowerCase().includes(section.toLowerCase())) {
      issues.push(`Missing required section: ${section}`);
    }
  }

  // Filter 4: Keyword coverage
  const keywords = spec.requiredKeywords || [];
  const missingKeywords = keywords.filter(
    kw =&gt; !lesson.content.toLowerCase().includes(kw.toLowerCase())
  );
  if (missingKeywords.length &gt; keywords.length * 0.3) {
    issues.push(`Missing &gt;30% required keywords: ${missingKeywords.join(', ')}`);
  }

  // Filter 5: Structure markers
  const hasIntro = /^(введение|introduction|в этом уроке)/im.test(lesson.content);
  const hasConclusion = /(заключение|conclusion|подводя итог|в завершение)/im.test(lesson.content);
  if (!hasIntro || !hasConclusion) {
    issues.push('Missing intro or conclusion markers');
  }

  return {
    passed: issues.length === 0,
    issues,
    skipJudge: issues.length &gt; 3, // Immediate regenerate if too many issues
  };
}

// This filters 30-50% of content at ZERO cost

Strategy 3: Batch API Processing

// For non-real-time validation (pre-production QA)
// OpenAI Batch API: 50% discount, 24-hour processing

async function batchEvaluateCourse(
  lessons: LessonContent[],
  spec: CourseSpecification
): Promise {
  const requests = lessons.map((lesson, i) =&gt; ({
    custom_id: `lesson_${i}`,
    method: 'POST',
    url: '/v1/chat/completions',
    body: {
      model: 'gpt-4o-mini',
      messages: [
        { role: 'system', content: CACHED_PROMPT },
        { role: 'user', content: buildDynamicPrompt(lesson, spec.lessons[i]) },
      ],
      temperature: 0.1,
    },
  }));

  // Submit batch (50% discount)
  const batch = await openai.batches.create({
    input_file_id: await uploadRequests(requests),
    endpoint: '/v1/chat/completions',
    completion_window: '24h',
  });

  // Poll for completion
  while (batch.status !== 'completed') {
    await sleep(60000); // Check every minute
    batch = await openai.batches.retrieve(batch.id);
  }

  return parseBatchResults(batch.output_file_id);
}

// Cost: $0.00098/lesson (vs $0.00195 real-time)
// Total for 20-lesson course: $0.020

Final Cost Calculation

Hybrid Cascade Architecture:

Stage 1: Heuristic Pre-filters → FREE
         Filters 30-50% instantly

Stage 2: Single Judge (Gemini Flash) → $0.00065/lesson
         For 50-70% of content passing Stage 1
         Average: $0.00033/lesson

Stage 3: CLEV 3x Voting → $0.00195/lesson
         For 15-20% low-confidence cases
         Average: $0.00039/lesson

Refinement: 1 iteration for 30% of lessons → $0.00150/lesson
            Average: $0.00045/lesson

TOTAL: $0.00033 + $0.00039 + $0.00045 = $0.00117/lesson
       20 lessons: $0.0234

       vs Manual review: $80-240/course
       Savings: 3,400-10,300x

Заключение: Production Checklist

Минимальная Viable Implementation

  1. Cross-Model Pairing: Генератор ≠ Judge family

  2. CLEV Voting: 2 judges default, 3rd on disagreement

  3. OSCQR Rubric: Weighted criteria with VETO thresholds

  4. Entropy Pre-screening: Flag high-uncertainty factual claims

  5. Circuit Breaker: Max 3 iterations, diminishing returns detection

  6. Prompt Caching: 60-90% cost reduction on static portions

Monitoring Dashboard

interface JudgeMetrics {
  // Quality
  judgeHumanAgreement: number;       // Target: &gt;80%
  falsePositiveRate: number;         // Target: &lt;10%
  falseNegativeRate: number;         // Target: &lt;5%

  // Cost
  averageCostPerLesson: number;      // Target: &lt;$0.002
  clevActivationRate: number;        // Expect: 15-30%
  refinementRate: number;            // Target: &lt;30%

  // Operations
  circuitBreakerTriggerRate: number; // Target: &lt;5%
  humanEscalationRate: number;       // Target: &lt;2%
  averageIterationsPerLesson: number; // Target: &lt;1.5
}

Как проходит регулярная проверка качества

  • Раз в несколько месяцев выбираем 30–50 уроков и даём экспертам проверить их вручную.

  • Сравниваем оценки экспертов с тем, что выдал алгоритм.

  • Смотрим, где они расходятся и почему.

  • Исправляем критерии оценки и примеры, чтобы модель меньше ошибалась.

  • При необходимости корректируем пороги, при которых алгоритм «уверен» в своём решении.

  • Все изменения фиксируем, чтобы отслеживать прогресс.


Контакты и обратная связь

Telegram

Канал (редкие посты): https://t.me/maslennikovigor

Прямой контакт: https://t.me/maslennikovig

GitHub

Issues: Для багов и технических вопросов

Discussions: Для идей и архитектурных дискуссий

Обратная связь

Буду рад услышать:

  • Критику — Где слабые места в архитектуре? Какие edge cases я не учел?

  • Альтернативы — Как вы решаете проблему валидации LLM-контента?

  • Бенчмарки — Если воспроизвели методологию — поделитесь результатами


Игорь Масленников
AI Dev Team, DNA IT
В IT с 2013 года


Источники

  1. Self-Preference Bias: Arize AI — "Testing Self-Evaluation Bias" https://arize.com/blog/should-i-use-the-same-llm-for-my-eval-as-my-agent-testing-self-evaluation-bias/

  2. Language Model Self-Preference: NYU Data Science — "Language Models Often Favor Their Own Text" https://nyudatascience.medium.com/language-models-often-favor-their-own-text-revealing-a-new-bias-in-ai-e6f7a8fa5959

  3. OSCQR Rubric: SUNY Online Course Quality Review https://oscqr.suny.edu/

  4. Self-Refine: OpenReview — "Iterative Refinement with Self-Feedback" https://openreview.net/forum?id=S37hOerQLB

  5. Entropy Hallucination Detection: Arch Gateway — "Detecting Hallucinations with Entropy" https://www.archgw.com/blogs/detecting-hallucinations-in-llm-function-calling-with-entropy-and-varentropy

  6. Log-Probability Uncertainty: ResearchGate — "Logprobs Know Uncertainty" https://www.researchgate.net/publication/394078106_Logprobs_Know_Uncertainty_Fighting_LLM_Hallucinations

  7. DeepSeek Pricing: DeepSeek API Docs https://api-docs.deepseek.com/quick_start/pricing-details-usd

  8. Temperature Effects: arXiv — "The Effect of Sampling Temperature on Problem Solving" https://arxiv.org/html/2402.05201v1

  9. LLM Judge Evaluation: Galileo AI — "LLM-as-a-Judge vs Human Evaluation" https://galileo.ai/blog/llm-as-a-judge-vs-human-evaluation

  10. Semantic Entropy: NIH PMC — "Detecting hallucinations using semantic entropy" https://pmc.ncbi.nlm.nih.gov/articles/PMC11186750/

Комментарии (1)


  1. Kamil_GR
    27.11.2025 08:10

    Самое забавное, уровень энтропии при уверенной галлюцинации может быть вполне себе низким.

    В целом, статья не похожа на реальный кейс. И в целом, всё начало вызывать у меня подозрения.