Все записи
7 мин

Перевёл проект на DeepSeek ради экономии в 10 раз — и сломал все платные расшифровки

нейросетиclaude code

Короче, если вы захотели сэкономить и перевели рабочую LLM-фичу на дешёвую модель ради экономии раз в десять, а потом у вас всё легло, то почти наверняка дело не в самой модели и не в её «несовместимости», а в том, что вы не прогнали её на реальных данных перед переключением. У меня ровно так и вышло. Я флипнул decode-модель в одном чужом проекте на DeepSeek ради примерно 10× экономии на выходных токенах, не сделал ни одного честного боевого прогона, и это положило все платные расшифровки разом. И самое смешное, что первый диагноз, который я себе поставил, оказался неверным, а реальная причина была совсем в другом, но обо всём по порядку.

Проект, в котором это случилось, не мой. Это AI-расшифровка медицинских анализов, я туда захожу как руки и голова по части ИИ, оркестрирую Claude Code, а он уже правит код. И вот пользователь загружает фото или PDF своего бланка, дальше модель сначала через vision вытаскивает текст и цифры, а потом отдельная decode-модель превращает эту простыню в нормальную человеческую расшифровку в формате JSON, который фронт уже красиво показывает. Две модели, два звена цепи, и оба стоят денег за каждый прогон. А там, где деньги капают за каждый запрос, желание сэкономить появляется само собой, особенно когда платят за это реальные люди.

Как я решил сэкономить и что из этого вышло

Я смотрю на цифры и думаю, а чего бы не сэкономить-то. Реальная стоимость одного прогона на флагманских моделях выходила так: vision где-то 0.0368 доллара, decode 0.0396, то есть почти четыре цента только на расшифровку. Мелочь? Мелочь, пока пользователей десяток. А когда их будет тысяча в день, эти центы складываются в ощутимые деньги, и каждый лишний выход хочется удешевить. DeepSeek по выходным токенам дешевле раз в десять, и логика была простая: decode это же просто переформатировать готовый текст в JSON, тут не нужен топовый интеллект, справится и дешёвая модель. Звучит разумно, правда? Хотя, забегая вперёд, обещанная экономия на дешёвых моделях не всегда такая, как на ценнике, — с тем же DeepSeek я уже ловил миф про «-90% на кэше», но тогда меня подкосило другое.

И вот я переключаю decode на DeepSeek и иду дальше заниматься своими делами, потому что на тестах же недавно всё показывало результаты, я это помнил. А зря помнил, потому что одно дело «когда-то показывало», и совсем другое честный прогон именно сейчас, именно на этой модели, именно на боевых бланках. Этого прогона я как раз и не сделал, понадеялся на память. Классика, в общем.

Через какое-то время прилетает, что у людей не открываются расшифровки. Лезу смотреть и вижу картину: у decode-запросов полное тело ответа приходит пустым, стоимость прогона по нулям, а в логах ровно «response is not valid JSON». То есть модель отвечает кодом 2xx, всё как будто хорошо, запрос принят, но тело ответа пустое. А парсер ждёт JSON, получает пустоту и валится. И так как decode это последнее звено перед показом пользователю, легли вообще все платные расшифровки. Не часть, не иногда, а вообще все до единой.

Первый диагноз, который оказался неверным

Дальше я сделал то, что в стрессе делают почти все, и за что себя потом ругаешь. Я быстро придумал красивое объяснение: ну ясно, DeepSeek не дружит со строгим JSON-режимом так, как Gemini или другие, вот и отдаёт пустоту. Версия звучала логично, я в неё поверил и откатил decode обратно на Gemini. Расшифровки ожили, люди стали получать результат, я выдохнул и решил, что разобрался.

А заказчик не дал мне так легко соскочить и надавил по делу: погоди, так ты же тестил, когда всё работало, давай разбираться с этим дипсиком, что за фигня. И вопрос-то правильный, ведь если оно когда-то на тестах работало и выдавало результаты, то откуда взялась несовместимость на ровном месте. Не появляется же она внезапно у модели, которую ты уже гонял. И тут я честно сказал то, что и надо было сказать сразу: я поторопился с флипом decode на DeepSeek без реального прогона, и моё объяснение про JSON-режим это домысел, а не вывод из данных. Так что вместо красивой теории я полез в боевые трейсы, в реальные запросы и ответы, которые модель отдавала живым пользователям.

Реальная причина была другой

И в трейсах открылась совсем другая картина. Никакой системной несовместимости с JSON-режимом не было и в помине. Была транзиентная штука: модель на части запросов отдавала пустое 2xx-тело на таймауте. То есть запрос уходит, генерация по какой-то причине не успевает или обрывается, а наружу прилетает валидный по статусу ответ, только пустой внутри. Парсеру всё равно почему пусто, он видит не-JSON и падает. А поскольку это плавающая, непостоянная ошибка, она прекрасно маскируется под «модель сломана» или «несовместима», хотя на самом деле модель местами просто не успевала и отдавала пшик.

Вот это, кстати, важный урок про дешёвые модели вообще, не только про DeepSeek. Откат на Gemini «починил» проблему не потому, что Gemini умеет в JSON, а DeepSeek нет, а потому что я убрал звено, которое тупо чаще спотыкалось на таймаутах. Это была заплатка, а не лечение. И если бы заказчик не надавил, я бы так и жил с неправильной картиной мира в голове, объясняя себе и другим то, чего на самом деле не было.

Чем чинил по-настоящему

Когда стало ясно, что беда это пустое тело при формально успешном статусе, фикс нарисовался сам собой. Я попросил Claude Code добавить в слой, который ходит в OpenRouter за chat-ответом, нормальную обработку таких случаев: если пришёл 2xx, но тело непригодное, пустое или не парсится, то это не успех, это повод повторить запрос. Ретрай на непригодных 2xx плюс бэкофф между попытками, чтобы не долбить модель в лоб. Раньше код наивно верил коду статуса: 200, значит всё хорошо, иди парси. Теперь он проверяет, что внутри реально лежит то, что мы ждём, а не просто радуется зелёному статусу. Похожую болячку я уже разбирал отдельно, когда нейросеть-провайдер затупила и платные расклады молча превратились в мусор — тот же авто-ретрай и детектор деградаций спасали проект.

И вторую вещь, которую я раньше недооценивал, это честность перед пользователем, когда генерация затягивается. Вместо того чтобы молча крутить спиннер или вывалить ошибку, человеку показывается нормальное человеческое сообщение в духе «извините, мы чуть задерживаемся». Потому что подождать лишние несколько секунд и получить расшифровку это нормально, а вот получить пустоту или вечную загрузку это потеря доверия, особенно когда речь про чьи-то медицинские анализы и человек платит за это деньги.

А с vision-звеном я в итоге всё-таки сэкономил, но уже с прогоном, а не на честном слове. Переключил его на Gemini Flash, это примерно вчетверо дешевле, vision там стоит около 0.0059 доллара против 0.0368 на флагмане. И прежде чем оставить, я реально прогнал и фото, и PDF на боевых бланках, проверил что OCR держит, что цифры из анализов не плывут и не теряются. OCR держит. Вот так и надо было с decode сделать с самого начала, и никакого цирка с пустым JSON и неверными диагнозами бы не случилось.

Что я из этого вынес

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

Второе: не верьте красивым объяснениям, которые рождаются в панике. «DeepSeek не умеет в JSON» звучало складно и снимало с меня вину, но это был домысел, а правда лежала в боевых трейсах. Когда что-то сломалось, лезьте в реальные логи и реальные ответы, а не сочиняйте теорию, которая удобно ложится на ваше первое впечатление. Заказчик своим простым вопросом «так ты же тестил» по сути заставил меня вернуться к фактам, и это был лучший момент во всей истории.

И третье, чисто инженерное: код, который ходит к LLM по API, никогда не должен слепо верить статусу ответа. 2xx это ещё не успех, успех это валидное тело, которое можно распарсить и использовать. Ретрай на непригодных ответах, бэкофф и честное сообщение пользователю при долгой генерации это не украшательство, а базовая гигиена для любого, кто строит нейросети для бизнеса или просто прикручивает дешёвую модель к рабочему процессу ради автоматизации. Дешёвые модели нормальные, они реально экономят деньги, просто относитесь к ним как к живым, которые иногда отдают пустоту, и закладывайте это в код заранее.

Если интересно, как у меня вообще устроена эта связка из нескольких моделей через один роутер и почему я в своё время съехал на OpenRouter, я про этот переход рассказывал отдельно в истории про то, как два проекта чуть не умерли на OpenAI. А тут вывод простой: газуй по полной, но прогоняй на реальных данных перед тем как что-то менять в проде. Вот и делайте выводы.