Архитектуры Test-Time Compute и модели с обучением с подкреплением (Reinforcement Learning) породили специфический класс системных дефектов — Overthinking Trap (гиперрефлексию и зацикливание самопроверок). В процессе RL-тренировок (PPO/GRPO) модели поощрялись за глубокий самоанализ: фразы рефлексии вроде "Wait, let me double check this assumption..." или "On second thought, what if there is an edge case..." коррелировали с успешным решением олимпиадных математических и криптографических задач.
Однако при переносе в промышленную программную инженерию этот механизм дает сбой. При решении умеренно сложных или детерминированных задач модель попадает в петлю бесконечной рефлексии:
Цикл бесконечной рефлексии (Reflection Trap):
┌─────────────────────────────────────────────────────────────────┐
│ Hypothesis A (Корректное решение) │
│ │ │
│ ▼ │
│ "Wait, but what if thread safety is broken here?" │
│ │ │
│ ▼ │
│ Hypothesis B (Избыточное усложнение, добавление лишних мьютексов)│
│ │ │
│ ▼ │
│ "Wait, that introduces potential deadlock. Let me rethink..." │
│ │ │
│ ▼ │
│ Hypothesis C (Возврат к Hypothesis A с незначительными правками)│
│ │ │
│ ▼ │
│ [ ПОВТОРЕНИЕ ЦИКЛА 5-10 РАЗ ПОКА НЕ ЗАКОНЧАТСЯ ТОКЕНЫ ] │
└─────────────────────────────────────────────────────────────────┘Для production-систем это означает: трехкратный рост задержки, деградацию качества финального ответа из-за переусложнения кода и перерасход облачного бюджета.
Инженерное решение проблемы требует комплексного подхода:
AbortController или передачи управляющих стоп-последовательностей.В архитектуре runtime-защиты между reasoning-моделью и потребителем внедряется сторожевой шлюз (Guarded Reasoning Engine), который анализирует поток скрытых размышлений на лету.
[ User Architectural Task ]
│
▼
[ System Invariant Injection (Premise Anchoring) ]
- Фиксация окружения: Node.js 22, Single Thread Event Loop
- Запрет на рассмотрение нерелевантных edge-cases (e.g., bit-rot, 32-bit int overflow)
│
▼
[ Reasoning LLM Stream (Claude 3.7 / DeepSeek R1) ]
│
├──────────────────────────────┐
│ (Thinking Token Chunks) │
▼ │
[ Real-time Overthinking Detector ] │
├─ Doubt Marker Density (DMD) │
├─ N-gram Repetition Entropy │
└─ Token Budget Burn Threshold │
│ │
▼ ▼
┌───────────────┐ [ UI / Client Stream ]
│ Loop Alert? │
└───┬───────┬───┘
YES │ │ NO
│ └──────────────────────────► (Continue normal thinking)
▼
[ ABORT / FORCE CUT-OFF ]
- Прерывание thinking фазы
- Принудительное переключение на Output Synthesisnull в поле с типом string, хотя схема строго запрещает nullability.Ниже представлена реализация GuardedReasoningStream на TypeScript. Класс перехватывает стрим reasoning-токенов, отслеживает плотность маркеров рефлексии (Doubt Marker Density) и энтропию n-грамм. При обнаружении зацикливания или превышении лимита сомнений генератор инициирует аварийное переключение на полезный ответ.
import { z } from "zod";
export interface GuardConfig {
maxDoubtMarkers: number;
windowSizeWords: number;
repetitionThreshold: number;
maxThinkingTokensHardCap: number;
}
export const DEFAULT_GUARD_CONFIG: GuardConfig = {
maxDoubtMarkers: 4, // Максимально допустимое число фраз пересмотра решения
windowSizeWords: 60, // Скользящее окно анализа слов
repetitionThreshold: 0.65, // Порог схожести n-грамм
maxThinkingTokensHardCap: 4096,
};
export interface GuardAnalysisResult {
isLoopDetected: boolean;
reason?: string;
doubtCount: number;
repetitionScore: number;
}
/**
* Класс детекции зацикливаний и гиперрефлексии (Overthinking) в реальном времени
*/
export class OverthinkingDetector {
private readonly config: GuardConfig;
private doubtCount = 0;
private tokensSeen = 0;
private rollingWords: string[] = [];
// Маркеры рекурсивного сомнения, характерные для оверсинкинга LLM
private static readonly DOUBT_PATTERNS = [
/\bwait,?\s+but\b/i,
/\blet me reconsider\b/i,
/\bon second thought\b/i,
/\bwait,?\s+what if\b/i,
/\bthis might be wrong\b/i,
/\bactually,?\s+let's rethink\b/i,
/\bhold on,?\s+there is a flaw\b/i,
/\balternatively,?\s+maybe\b/i,
];
constructor(config: Partial<GuardConfig> = {}) {
this.config = { ...DEFAULT_GUARD_CONFIG, ...config };
}
/**
* Анализ очередного чанка reasoning-токенов
*/
public analyzeChunk(chunkText: string): GuardAnalysisResult {
this.tokensSeen += Math.ceil(chunkText.length / 4);
// 1. Проверка жесткого лимита токенов
if (this.tokensSeen >= this.config.maxThinkingTokensHardCap) {
return {
isLoopDetected: true,
reason: `Hard cap of ${this.config.maxThinkingTokensHardCap} thinking tokens reached`,
doubtCount: this.doubtCount,
repetitionScore: 1.0,
};
}
// 2. Поиск маркеров сомнения
for (const pattern of OverthinkingDetector.DOUBT_PATTERNS) {
if (pattern.test(chunkText)) {
this.doubtCount++;
}
}
if (this.doubtCount >= this.config.maxDoubtMarkers) {
return {
isLoopDetected: true,
reason: `Excessive doubt markers threshold exceeded (${this.doubtCount} occurrences)`,
doubtCount: this.doubtCount,
repetitionScore: 0.8,
};
}
// 3. Анализ повторов слов в скользящем окне (N-gram Repetition)
const words = chunkText.toLowerCase().replace(/[^\w\s]/g, "").split(/\s+/).filter(Boolean);
this.rollingWords.push(...words);
if (this.rollingWords.length > this.config.windowSizeWords * 2) {
this.rollingWords = this.rollingWords.slice(-this.config.windowSizeWords * 2);
}
const repScore = this.calculateRepetitionScore(this.rollingWords);
if (repScore >= this.config.repetitionThreshold && this.rollingWords.length >= this.config.windowSizeWords) {
return {
isLoopDetected: true,
reason: `High semantic repetition detected (score: ${repScore.toFixed(2)})`,
doubtCount: this.doubtCount,
repetitionScore: repScore,
};
}
return {
isLoopDetected: false,
doubtCount: this.doubtCount,
repetitionScore: repScore,
};
}
private calculateRepetitionScore(words: string[]): number {
if (words.length < 10) return 0;
const unique = new Set(words);
// Чем меньше уникальных слов в окне, тем выше повторение
const ratio = 1 - unique.size / words.length;
return Math.max(0, Math.min(1, ratio));
}
public reset(): void {
this.doubtCount = 0;
this.tokensSeen = 0;
this.rollingWords = [];
}
}
/**
* Обертка безопасного исполнения запросов с ранним подавлением Overthinking
*/
export async function executeGuardedReasoning(params: {
apiKey: string;
systemPrompt: string;
userPrompt: string;
onLoopAborted?: (reason: string) => void;
}): Promise<{ output: string; thinkingTokens: number; wasAborted: boolean }> {
const detector = new OverthinkingDetector();
const abortController = new AbortController();
// Инжекция жестких предикатов для ограничения пространства рассуждений
const anchoredSystemPrompt = `
${params.systemPrompt}
[CRITICAL EXECUTION INVARIANTS]:
1. Do NOT reconsider the fundamental architectural premise once chosen.
2. Assume happy path runtime guarantees: Environment is Node.js 22 LTS, single-process, 64-bit architecture.
3. Do NOT invent hypothetical edge cases outside standard POSIX / ECMAScript specifications.
4. If a valid solution is reached, conclude reasoning immediately and emit the output.
`.trim();
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": params.apiKey,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
signal: abortController.signal,
body: JSON.stringify({
model: "claude-3-7-sonnet-20250219",
max_tokens: 6000,
system: anchoredSystemPrompt,
thinking: {
type: "enabled",
budget_tokens: 3000,
},
messages: [{ role: "user", content: params.userPrompt }],
stream: true,
}),
});
if (!response.ok || !response.body) {
throw new Error(`API call failed with status ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullOutput = "";
let thinkingTokens = 0;
let wasAborted = false;
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const jsonStr = line.slice(6).trim();
if (jsonStr === "[DONE]") continue;
try {
const event = JSON.parse(jsonStr);
if (event.type === "content_block_delta") {
const delta = event.delta;
// Анализ блока скрытых рассуждений
if (delta.type === "thinking_delta") {
thinkingTokens += Math.ceil(delta.thinking.length / 4);
const analysis = detector.analyzeChunk(delta.thinking);
if (analysis.isLoopDetected) {
wasAborted = true;
params.onLoopAborted?.(analysis.reason || "Loop detected");
// Прерываем стрим при зацикливании
abortController.abort();
break;
}
} else if (delta.type === "text_delta") {
fullOutput += delta.text;
}
}
} catch {
// Игнорируем синтаксические ошибки промежуточных чанков
}
}
if (wasAborted) break;
}
} catch (err: unknown) {
if (err instanceof Error && err.name === "AbortError") {
// Ожидаемый перехват раннего прерывания
} else {
throw err;
}
}
return {
output: fullOutput,
thinkingTokens,
wasAborted,
};
}Попытка купировать оверсинкинг наивными запретами вроде: "Do not overthink", "Be concise in your reasoning". В RL-политике reasoning-моделей эти фразы часто триггерят дополнительный слой рефлексии: модель начинает рассуждать о том, как именно ей не думать слишком много ("The user asked me not to overthink, so let me carefully analyze what constitutes overthinking here..."). Необходимо использовать архитектурные инварианты окружения (Anchoring) и программные лимиты токенов.
Вызов abortController.abort() при обнаружении зацикливания без резервного механизма. Клиент получает пустой ответ или обрыв сокета. Принудительное прерывание thinking-фазы должно сопровождаться повторным вызовом модели с thinking: { type: "disabled" } и передачей уже сформулированной гипотезы как вводных данных.
Постановка задачи без указания целевой платформы (например, «напиши потокобезопасный кэш»). Модель начинает перебирать все возможные платформы: от многопоточного C++20 с std::atomic до распределенного Redis-кластера и однопоточного JavaScript. Четкая фиксация среды в системном контексте ("Target environment: Node.js 22 single-thread process with in-memory Map") отсекает 95% ложных веток CoT.
Запуск открытых reasoning-моделей на vLLM/Ollama со стандартными параметрами сэмплинга без пенализации повторов. Модели могут зацикливаться на повторении одного и того же абзаца рассуждений до заполнения контекста в 32k токенов. Для открытых весов критически важно выставлять repetition_penalty: 1.05 - 1.1 и presence_penalty: 0.1.
Инжекция строгих технических инвариантов среды в системный промпт перед инференсом
Перехват потока thinking_delta в изолированном брокере стриминга
Скользящий анализ плотности фраз сомнения и информационной энтропии n-грамм
Аварийное прерывание соединения при детекции циклической гиперрефлексии
Быстрый синтез ответа в режиме disabled thinking на базе первой валидной гипотезы