Знакомая ситуация: вы скармливаете нейросети сложную бизнес-логику или запутанный 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") и генерируют огромные полотна текста.
Лучший паттерн, который мы выработали:
- Сворачиваемый блок (Accordion). Пока модель думает, блок развернут, текст внутри рендерится серым цветом или курсивом. Обязательно добавьте автоскролл вниз, чтобы создавалась динамика.
- Индикация процесса. Заголовок блока должен пульсировать или показывать спиннер ("Анализирую схему БД..", "Проверяю краевые случаи..").
- Авто-сворачивание. Как только приходит первый байт
content(финального ответа), блок с мыслями плавно сворачивается, оставляя только заголовок "Время размышлений: 14 сек". Пользователь может развернуть его при желании, но фокус смещается на результат.
Итог
Разделение на "мысли" и "результат" — это не просто фича ради фичи. Это мощный сдвиг в том, как мы взаимодействуем с LLM. Мы больше не заставляем модель выдавать правильный ответ с первой попытки, полагаясь на удачу. Мы даем ей право на ошибку в песочнице reasoning_content.
А благодаря единому шлюзу вроде RouterAPI, интеграция этого сложного механизма сводится к проверке одного дополнительного поля в JSON-чанке. Вы получаете умные модели, которые не отваливаются по таймауту, и пользователей, которые видят прозрачный процесс работы, а не зависший интерфейс.