Chain-of-Thought на стероидах: Заставляем модель думать вслух

09.06.2026 13:00

Знакомая ситуация: вы скармливаете нейросети сложную бизнес-логику или запутанный SQL-запрос с пачкой джоинов и оконных функций. Модель моментально выплевывает красивый, синтаксически безупречный код. Вы копируете его в IDE, запускаете — и база ложится от декартова произведения, либо скрипт падает с неочевидной ошибкой.

Почему так происходит? Проблема кроется в самой архитектуре LLM. Авторегрессионные модели — это генераторы следующего токена. У них нет встроенного «черновика», где можно набросать план, понять, что логика свернула не туда, зачеркнуть и начать заново. Сгенерированный токен навсегда становится частью контекста. Если на третьем шаге рассуждений модель допустила микро-ошибку, дальше она будет героически выкручиваться и нагромождать костыли, лишь бы сохранить связность собственного бреда.

Анатомия глупости и костыли прошлого

Долгое время индустрия спасалась промпт-инжинирингом. Магическое заклинание Let's think step by step заставляло модель выводить свои рассуждения в текстовый ответ. Это работало: мы тратили токены на текст-размышление, давая модели «пространство» для вычислений перед финальным ответом.

Но для продакшена это был чудовищный костыль. Пользователю не нужны простыни текста о том, как модель перемножает матрицы или анализирует схему БД. Пользователю нужен результат. Бэкендерам приходилось писать хрупкие регулярки, вырезающие всё до слова "Ответ:", что регулярно ломалось при малейшем изменении формата или языка ответа.

Ситуация кардинально изменилась с выходом моделей нового поколения: DeepSeek R1, OpenAI o1/o3-mini, Claude 3.7 Sonnet. Они внедрили нативный Chain-of-Thought (CoT). Теперь процесс мышления изолирован от финального ответа на уровне API. Модель генерирует reasoning_tokens (токены размышлений), а затем — обычные токены ответа. Мы получили идеальное разделение: «мысли» отдельно, «результат» отдельно.

Но с точки зрения бэкенд-инженера и UX-дизайнера, этот прорыв принес серьезную головную боль.

TTFB улетает в космос

Главная проблема нативного CoT — катастрофический рост TTFB (Time To First Byte) для финального ответа. Если модель думает 15-20 секунд, прежде чем выдать первый токен полезного ответа, стандартный HTTP-запрос превращается в пытку. Пользователь смотрит на крутящийся лоадер, думает, что всё зависло, и обновляет страницу. Соединения отваливаются по таймауту балансировщика (привет, дефолтные 60 секунд в Nginx).

Единственный адекватный выход — стримить процесс размышления в реальном времени, показывая пользователю, что работа идет. И здесь мы врезаемся в стену зоопарка форматов.

OpenAI отдает мысли в одном формате, Anthropic придумал свои блоки thinking и redacted_thinking, DeepSeek использует поле reasoning_content в дельтах чанков. Поддерживать этот хаос в коде приложения — значит обречь себя на вечный рефакторинг парсеров Server-Sent Events (SSE).

Единый шлюз: RouterAPI

Вместо того чтобы писать адаптеры под каждый чих очередного провайдера, мы перевели инфраструктуру на RouterAPI. Шлюз нормализует ответы от десятков провайдеров к единому OpenAI-совместимому стандарту. Нам больше не нужно знать, работает ли под капотом Claude или DeepSeek. Шлюз сам разбирается с проприетарными форматами и отдает аккуратный SSE-поток.

В этом потоке токены размышлений всегда приходят в предсказуемом поле reasoning_content, а токены ответа — в content.

Давайте посмотрим, как выглядит правильная обработка такого потока на клиенте (TypeScript), чтобы создать современный UX:

async function fetchWithThoughts(prompt: string) {
 const response = await fetch('https://api.RouterAPI.host/v1/chat/completions', {
 method: 'POST',
 headers: {
 'Content-Type': 'application/json',
 'Authorization': `Bearer ${API_KEY}`
 },
 body: JSON.stringify({
 model: 'deepseek/deepseek-r1', // RouterAPI смаршрутизирует куда нужно
 messages: [{ role: 'user', content: prompt }],
 stream: true
 })
 });

 const reader = response.body?.getReader;
 const decoder = new TextDecoder;
 
 let thoughts = '';
 let finalAnswer = '';
 let isThinking = true;

 while (true) {
 const { done, value } = await reader!.read;
 if (done) break;
 
 const chunk = decoder.decode(value, { stream: true });
 const lines = chunk.split('\n').filter(line => line.trim !== '');
 
 for (const line of lines) {
 if (line === 'data: [DONE]') return;
 if (!line.startsWith('data: ')) continue;
 
 try {
 const data = JSON.parse(line.slice(6));
 const delta = data.choices[0].delta;

 // Парсим мысли
 if (delta.reasoning_content) {
 thoughts += delta.reasoning_content;
 updateUI('thoughts-container', thoughts);
 }
 
 // Парсим финальный ответ
 if (delta.content) {
 if (isThinking) {
 isThinking = false;
 collapseThoughtsUI; // Сворачиваем блок мыслей
 }
 finalAnswer += delta.content;
 updateUI('answer-container', finalAnswer);
 }
 } catch (e) {
 console.error('Ошибка парсинга чанка:', e);
 }
 }
 }
}

UX-паттерны: Как не напугать пользователя

Получить reasoning_content — это полдела. Нужно правильно его отрендерить. Если вы просто вывалите сырые мысли модели на экран, пользователь сойдет с ума. Модели часто "думают" на смеси английского и русского, используют внутренние теги, ругают сами себя ("No, this is wrong, let's recalculate") и генерируют огромные полотна текста.

Лучший паттерн, который мы выработали:

  1. Сворачиваемый блок (Accordion). Пока модель думает, блок развернут, текст внутри рендерится серым цветом или курсивом. Обязательно добавьте автоскролл вниз, чтобы создавалась динамика.
  2. Индикация процесса. Заголовок блока должен пульсировать или показывать спиннер ("Анализирую схему БД..", "Проверяю краевые случаи..").
  3. Авто-сворачивание. Как только приходит первый байт content (финального ответа), блок с мыслями плавно сворачивается, оставляя только заголовок "Время размышлений: 14 сек". Пользователь может развернуть его при желании, но фокус смещается на результат.

Итог

Разделение на "мысли" и "результат" — это не просто фича ради фичи. Это мощный сдвиг в том, как мы взаимодействуем с LLM. Мы больше не заставляем модель выдавать правильный ответ с первой попытки, полагаясь на удачу. Мы даем ей право на ошибку в песочнице reasoning_content.

А благодаря единому шлюзу вроде RouterAPI, интеграция этого сложного механизма сводится к проверке одного дополнительного поля в JSON-чанке. Вы получаете умные модели, которые не отваливаются по таймауту, и пользователей, которые видят прозрачный процесс работы, а не зависший интерфейс.

Теги

Ещё по теме