Массовая генерация синтетического кода для дообучения моделей или автономных агентных пайплайнов несет в себе критический риск — деградацию качества моделей (Model Collapse) из-за накопления «синтетического шлака» (Synthetic Slop). Если дообучать нейросеть на непроверенном коде, сгенерированном другой LLM, скрытые логические ошибки, архитектурные антипаттерны, скрытые уязвимости и галлюцинированные зависимости мультиплицируются в геометрической прогрессии с каждым новым поколением.
Нефильтрованный синтез:
LLM Gen -> [Галлюцинации/Утечки памяти] -> Fine-Tuning -> Деградация весов (Model Collapse)
Rejection Sampling Pipeline:
LLM Gen (N=8) -> [Static Gate] -> [Sandbox Execution] -> [LLM Judge Rubric] -> Gold SampleДля гарантированного отбора исключительно эталонных образцов применяется вероятностный фильтрационный конвейер Rejection Sampling (выборка с отклонением) в комбинации с многоуровневым арбитражем.
Методология Rejection Sampling в кодогенерации базируется на стратегии Best-of-N:
tsc --noEmit), линтеры (ESLint) и инструменты статического анализа (PHPStan Level 8 / Psalm).Конвейер фильтрации представляет собой многоступенчатую воронку отсева, где каждый последующий этап требует больше вычислительных ресурсов, но применяется к строго уменьшающемуся объему кандидатов:
[Входная спецификация задачи / User Prompt]
│
▼ (Генерация при temperature = 0.7)
┌────────────────────────────────────────────────────────┐
│ Ансамбль кандидатов: Candidate [1..8] │
└──────────────────┬─────────────────────────────────────┘
│ 8 кандидатов
▼
┌────────────────────────────────────────────────────────┐
│ Gate 1: Детерминированный статический анализ │
│ - tsc --noEmit / AST Syntax Check │
│ - ESLint / PHPStan Level 8 │
│ - Регулярные проверки на запрещенные функции │
└──────────────────┬─────────────────────────────────────┘
│ Прошли: 5 кандидатов (3 отброшены)
▼
┌────────────────────────────────────────────────────────┐
│ Gate 2: Изолированная песочница тестов (Sandbox) │
│ - Прогон функциональных юнит-тестов (100% pass) │
│ - Ограничение по CPU Time (< 200 ms) и RAM (< 64 MB) │
│ - Проверка отсутствия утечек сокетов и дескрипторов │
└──────────────────┬─────────────────────────────────────┘
│ Прошли: 3 кандидата (2 отброшены)
▼
┌────────────────────────────────────────────────────────┐
│ Gate 3: LLM-as-a-Judge (Независимый арбитр) │
│ - Рандомизация порядка (борьба с Position Bias) │
│ - Скоринг по рубрике: чистый код, O(n) сложность │
│ - Штраф за многословие (борьба с Verbosity Bias) │
└──────────────────┬─────────────────────────────────────┘
│ Выбор лучшего (ArgMax Score)
▼
[Эталонный сэмпл (Gold Sample) для SFT датасета / Deploy]Ниже представлена реализация полного цикла Rejection Sampling на TypeScript. Конвейер генерирует N кандидатов, пропускает их через проверку компилятором, имитирует песочницу юнит-тестов и вызывает LLM-as-a-Judge с формализованной Zod-рубрикой оценки.
import { z } from "zod";
import OpenAI from "openai";
// Схема рубрики оценки качества кода арбитром (LLM-as-a-Judge)
export const CodeQualityRubricSchema = z.object({
candidateIndex: z.number().int(),
scores: z.object({
functionalCorrectness: z.number().min(1).max(10), // Точность реализации
architecturalClarity: z.number().min(1).max(10), // Читаемость и SOLID
errorHandlingRobustness: z.number().min(1).max(10), // Обработка неизвестных ошибок
algorithmicEfficiency: z.number().min(1).max(10), // Асимптотика времени и памяти
}),
hasSecurityVulnerabilities: z.boolean(),
concisenessPenaltyApplied: z.boolean(), // Флаг штрафа за избыточный boilerplate
totalWeightedScore: z.number().min(0).max(100),
reasoningNotes: z.string().max(1000),
});
export type CodeQualityRubric = z.infer<typeof CodeQualityRubricSchema>;
export interface GenerationCandidate {
id: string;
code: string;
staticCheckPassed: boolean;
unitTestsPassed: boolean;
executionTimeMs: number;
}
export class RejectionSamplingEngine {
private openai: OpenAI;
constructor(apiKey?: string) {
this.openai = new OpenAI({ apiKey: apiKey ?? process.env.OPENAI_API_KEY });
}
/**
* Генерация N вариантов реализации с повышенной температурой
*/
public async generateCandidates(
prompt: string,
sampleCount: number = 8
): Promise<GenerationCandidate[]> {
const candidates: GenerationCandidate[] = [];
// Параллельная генерация N независимых вариантов
const requests = Array.from({ length: sampleCount }, async (_, idx) => {
const response = await this.openai.chat.completions.create({
model: "gpt-4o",
temperature: 0.75, // Повышенная температура для архитектурного разнообразия
messages: [
{
role: "system",
content: "Ты — Senior Software Engineer. Напиши только production-ready код решения на TypeScript без markdown обертки.",
},
{ role: "user", content: prompt },
],
});
return {
id: `cand_${idx + 1}`,
code: response.choices[0]?.message.content ?? "",
staticCheckPassed: false,
unitTestsPassed: false,
executionTimeMs: 0,
};
});
return Promise.all(requests);
}
/**
* Gate 1 & 2: Статический анализ и тестовая песочница
*/
public runDeterministicGates(candidate: GenerationCandidate): GenerationCandidate {
const code = candidate.code;
// 1. Проверка синтаксических антипаттернов (Static Gate)
const hasAnyType = /\bany\b/.test(code);
const hasEval = /\beval\s*\(/.test(code);
const hasUncheckedCatch = /catch\s*\(\s*[a-zA-Z0-9_]+\s*\)/.test(code) && !code.includes("unknown");
if (hasAnyType || hasEval || hasUncheckedCatch || code.length < 50) {
candidate.staticCheckPassed = false;
return candidate;
}
candidate.staticCheckPassed = true;
// 2. Имитация изолированного запуска юнит-тестов (Sandbox Gate)
// В реальной системе: запуск контейнера Docker с `vitest run`
const hasRequiredExports = code.includes("export class") || code.includes("export function");
const hasErrorThrowing = code.includes("throw new");
candidate.unitTestsPassed = hasRequiredExports && hasErrorThrowing;
candidate.executionTimeMs = Math.floor(Math.random() * 80) + 20; // 20-100 мс
return candidate;
}
/**
* Gate 3: LLM-as-a-Judge с рандомизацией для устранения Position Bias
*/
public async arbitrateBestSample(
prompt: string,
candidates: GenerationCandidate[]
): Promise<GenerationCandidate> {
const validCandidates = candidates.filter(
(c) => c.staticCheckPassed && c.unitTestsPassed
);
if (validCandidates.length === 0) {
throw new Error("Все сэмплы были отклонены детерминированными гейтами.");
}
if (validCandidates.length === 1) {
return validCandidates[0];
}
// Рандомизация порядка для предотвращения Position Bias
const shuffled = [...validCandidates].sort(() => Math.random() - 0.5);
const candidatesPayload = shuffled.map((c, i) => `=== КАНДИДАТ #${i + 1} ===\n${c.code}\n`).join("\n");
const judgePrompt = `
Вы — беспристрастный арбитр качества программного кода (Staff/Principal Engineer).
Исходное техническое задание:
"""
${prompt}
"""
Оцените каждого кандидата строго по рубрике.
Критерии скоринга (каждый от 1 до 10):
- functionalCorrectness (вес 40%)
- architecturalClarity (вес 25%)
- errorHandlingRobustness (вес 20%)
- algorithmicEfficiency (вес 15%)
Правила борьбы со смещениями:
1. Игнорируйте избыточные многословные комментарии (Verbosity Bias). Код должен быть лаконичным и выразительным.
2. Немедленно дисквалифицируйте код при наличии уязвимостей безопасности (hasSecurityVulnerabilities = true).
3. Верните JSON с массивом оценок по схеме.
`;
const response = await this.openai.chat.completions.create({
model: "gpt-4o",
temperature: 0.1, // Минимальная температура для детерминированного судейства
response_format: {
type: "json_schema",
json_schema: {
name: "judge_evaluations",
schema: {
type: "object",
properties: {
evaluations: {
type: "array",
items: {
type: "object",
properties: {
candidateIndex: { type: "integer" },
scores: {
type: "object",
properties: {
functionalCorrectness: { type: "number" },
architecturalClarity: { type: "number" },
errorHandlingRobustness: { type: "number" },
algorithmicEfficiency: { type: "number" },
},
required: ["functionalCorrectness", "architecturalClarity", "errorHandlingRobustness", "algorithmicEfficiency"],
additionalProperties: false,
},
hasSecurityVulnerabilities: { type: "boolean" },
concisenessPenaltyApplied: { type: "boolean" },
totalWeightedScore: { type: "number" },
reasoningNotes: { type: "string" },
},
required: ["candidateIndex", "scores", "hasSecurityVulnerabilities", "concisenessPenaltyApplied", "totalWeightedScore", "reasoningNotes"],
additionalProperties: false,
},
},
},
required: ["evaluations"],
additionalProperties: false,
},
strict: true,
},
},
messages: [
{ role: "system", content: judgePrompt },
{ role: "user", content: candidatesPayload },
],
});
const parsedJson = JSON.parse(response.choices[0]?.message.content ?? "{}") as {
evaluations: CodeQualityRubric[];
};
// Фильтрация уязвимых и выбор максимума взвешенного балла (ArgMax)
const validEvaluations = parsedJson.evaluations.filter(
(e) => !e.hasSecurityVulnerabilities
);
if (validEvaluations.length === 0) {
throw new Error("Все кандидаты забракованы арбитром из-за проблем безопасности.");
}
validEvaluations.sort((a, b) => b.totalWeightedScore - a.totalWeightedScore);
const winnerIndex = validEvaluations[0].candidateIndex - 1;
return shuffled[winnerIndex];
}
}Попытка доверять оценку корректности кода исключительно модели-арбитру без фактического выполнения в песочнице с тестами. Модель-судья подвержена галлюцинациям точности (Hallucinated Correctness) и часто ставит высшие баллы коду, который выглядит профессионально, но падает с TypeError при исполнении первой же строки из-за несовместимости типов.
Модели-судьи систематически отдают предпочтение длинным решениям с сотнями строк комментариев, дублирующимися интерфейсами и избыточными слоями абстракций перед компактными, лаконичными функциями. В рубрику оценки необходимо вводить жесткие штрафные коэффициенты за избыточный бойлерплейт и измерять метрику плотности логики (Logic Density).
При одновременной подаче нескольких вариантов кода судья статистически чаще признает победителем кандидата #1 (первый токен) либо последнего кандидата (эффект свежести контекста). Для нейтрализации смещения порядок кандидатов обязан перемешиваться случайным образом перед формированием промпта, либо оценка должна производиться попарно (Pairwise Comparison) со сменой мест.
Выполнение сгенерированного кода в контексте хост-машины через стандартный exec или eval. Синтетический код может содержать как случайные деструктивные команды (rm -rf /, переполнение сокетов), так и пейлоады состязательных атак. Запуск обязан происходить исключительно в одноразовых контейнерах с изоляцией ядра (gVisor / Firecracker microVM) без сетевого доступа.
Генерация N=8 независимых решений при температуре 0.75 для широкого охвата архитектурных паттернов.
Проверка TypeScript компилятором, ESLint и AST-парсером на запрещенные конструкции и 'any'.
Прогон тест-сьюта в эфемерном контейнере с ограничением CPU/RAM и контролем таймаутов.
Перемешивание порядка сэмплов и скоринг через LLM-as-a-Judge по Zod-рубрике критериев.
Выбор образца с наивысшим баллом и экспорт в чистый 'Gold Standard' датасет для обучения.