Как мы решили проблему "стохастической дивергенции" при генерации уроков и снизили затраты на валидацию в 17,000 раз по сравнению с ручной проверкой
Контекст: кто пишет и о чем эта статья
Игорь Масленников. В IT с 2013 года. Последние два года развиваю AI Dev Team в DNA IT — подразделение, которое работает на мульти-модельной архитектуре. Мы генерируем образовательные курсы для клиентов с бюджетом 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.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.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 => `- ${s}`).join('\n')}
SPECIFIC REVISIONS NEEDED:
${ctx.judgeIssues.map((issue, i) => `
${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]) => `"${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) => `
--- 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 => `- ${i}`).join('\n')}
NEW ISSUES TO ADDRESS:
${history.currentIssues.map(i => `- ${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<'accept' | 'targeted_fix' | 'iterative_refine' | 'regenerate' | 'escalate'> {
const profile = ITERATION_PROFILES[model] || ITERATION_PROFILES.default;
// Score > 0.90: Accept
if (score >= 0.90) {
return 'accept';
}
// Score 0.75-0.90 with localized issues
if (score >= 0.75) {
const localizedIssues = issues.filter(i => i.location !== 'entire lesson');
if (localizedIssues.length / issues.length > 0.7) {
return 'targeted_fix';
}
return 'iterative_refine';
}
// Score 0.60-0.75: Iterative refinement if iterations remain
if (score >= 0.60) {
if (iterationCount < profile.maxIterations) {
return 'iterative_refine';
}
return 'regenerate';
}
// Score < 0.60: Immediate regenerate
if (score >= 0.40) {
return 'regenerate';
}
// Score < 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 => 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 >= config.maxIterations) {
return {
break: true,
reason: 'max_iterations_exceeded',
action: currentScore >= config.minFinalScore
? 'accept_with_warning'
: 'escalate_to_human',
};
}
// Rule 2: Cost budget exceeded
if (state.totalCost >= config.maxTotalCost) {
return {
break: true,
reason: 'cost_budget_exceeded',
action: 'accept_current_best',
};
}
// Rule 3: Diminishing returns detection
if (state.scoreHistory.length >= 2) {
const lastScore = state.scoreHistory[state.scoreHistory.length - 1];
const improvement = currentScore - lastScore;
if (improvement < config.minImprovementPerIteration) {
return {
break: true,
reason: 'diminishing_returns',
action: currentScore >= config.minFinalScore
? 'accept'
: 'escalate_to_human',
};
}
}
// Rule 4: Score oscillation detection
if (state.scoreHistory.length >= 3) {
const recent = state.scoreHistory.slice(-3);
const isOscillating =
(recent[0] < recent[1] && recent[1] > recent[2]) ||
(recent[0] > recent[1] && recent[1] < recent[2]);
if (isOscillating) {
return {
break: true,
reason: 'score_oscillation',
action: 'accept_best_from_history',
};
}
}
// Rule 5: Critical failure threshold
if (currentScore < 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 > 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 >= 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) => Promise,
maxRetries: number = 2
): Promise<{ result: T; modelUsed: string; fallbacksUsed: number }> {
let fallbacksUsed = 0;
for (const model of chain) {
if (model === 'HUMAN') {
throw new Error('Human intervention required');
}
for (let retry = 0; retry < 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.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 < spec.minWords || wordCount > 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) > 2) {
issues.push(`Flesch-Kincaid ${fk} differs from target ${targetGrade} by >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 => !lesson.content.toLowerCase().includes(kw.toLowerCase())
);
if (missingKeywords.length > keywords.length * 0.3) {
issues.push(`Missing >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 > 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) => ({
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
Cross-Model Pairing: Генератор ≠ Judge family
CLEV Voting: 2 judges default, 3rd on disagreement
OSCQR Rubric: Weighted criteria with VETO thresholds
Entropy Pre-screening: Flag high-uncertainty factual claims
Circuit Breaker: Max 3 iterations, diminishing returns detection
Prompt Caching: 60-90% cost reduction on static portions
Monitoring Dashboard
interface JudgeMetrics {
// Quality
judgeHumanAgreement: number; // Target: >80%
falsePositiveRate: number; // Target: <10%
falseNegativeRate: number; // Target: <5%
// Cost
averageCostPerLesson: number; // Target: <$0.002
clevActivationRate: number; // Expect: 15-30%
refinementRate: number; // Target: <30%
// Operations
circuitBreakerTriggerRate: number; // Target: <5%
humanEscalationRate: number; // Target: <2%
averageIterationsPerLesson: number; // Target: <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 года
Источники
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/
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
OSCQR Rubric: SUNY Online Course Quality Review https://oscqr.suny.edu/
Self-Refine: OpenReview — "Iterative Refinement with Self-Feedback" https://openreview.net/forum?id=S37hOerQLB
Entropy Hallucination Detection: Arch Gateway — "Detecting Hallucinations with Entropy" https://www.archgw.com/blogs/detecting-hallucinations-in-llm-function-calling-with-entropy-and-varentropy
Log-Probability Uncertainty: ResearchGate — "Logprobs Know Uncertainty" https://www.researchgate.net/publication/394078106_Logprobs_Know_Uncertainty_Fighting_LLM_Hallucinations
DeepSeek Pricing: DeepSeek API Docs https://api-docs.deepseek.com/quick_start/pricing-details-usd
Temperature Effects: arXiv — "The Effect of Sampling Temperature on Problem Solving" https://arxiv.org/html/2402.05201v1
LLM Judge Evaluation: Galileo AI — "LLM-as-a-Judge vs Human Evaluation" https://galileo.ai/blog/llm-as-a-judge-vs-human-evaluation
Semantic Entropy: NIH PMC — "Detecting hallucinations using semantic entropy" https://pmc.ncbi.nlm.nih.gov/articles/PMC11186750/
Kamil_GR
Самое забавное, уровень энтропии при уверенной галлюцинации может быть вполне себе низким.
В целом, статья не похожа на реальный кейс. И в целом, всё начало вызывать у меня подозрения.