Как ИИ-тренер по CS2 на DeepSeek ошибается и почему недельная подготовка обошлась в ,45

Как ИИ-тренер по CS2 на DeepSeek ошибается и почему недельная подготовка обошлась в $1,45

Прокомментировать Просмотры: 11

Привет всем. Я уже много лет увлекаюсь Counter-Strike и тренируюсь стандартным методом: иду в Deathmatch для «разогрева», провожу три матча, ловлю тильт и закрываю игру. Сторонние трекеры статистики при этом исправно фиксируют падение точности и увеличение времени реакции. Спасибо за информацию, но что с этим делать?

Весной я решил разработать персонального тренера своими силами. Сразу обозначу контекст: я самоучка, в профессиональных командах не состоял, мой основной профиль — десктопные утилиты и автоматизация, а из веб-разработки за плечами были лишь простенькие админки для внутренних задач. В самой CS звезд с неба не хватаю: Premier-рейтинг крутится около трех тысяч, а на FACEIT вообще первый уровень. Так что эта история вовсе не про «успешный успех», а про череду набитых шишек и извлеченные уроки.

Суть платформы следующая: она подтягивает историю матчей, передает агрегированные данные LLM, а искусственный интеллект анализирует слабые и сильные стороны игрока, формируя персонализированный недельный график тренировок с указанием конкретных карт, игровых режимов и хронометража. Отметил выполненные пункты, сыграл новые матчи — программа динамически перестраивает расписание.

Проект полностью мой, ссылка будет ровно одна и в самом конце. Статья разделена на три логические части: во сколько обходятся токены, где модель занимается вымыслом и как я с этим борюсь программно, а также технические подводные камни при работе с DeepSeek API и связкой EF Core + SQLite.

Что это за проект

Кратко обрисую концепт перед погружением в технические детали. Описанный выше цикл обратной связи учитывает не просто факт выполнения задач («сделал / не сделал»), но и причины пропусков: усталость, нехватка времени или недомогание. Помимо этого, система дает рекомендации ситуативного характера — например, стоит ли сегодня запускать матчмейкинг или лучше завершить игровую сессию, чтобы избежать слива очков.

Технологический стек вполне стандартный: ASP.NET Core 9 (MVC), EF Core, DeepSeek в роли интеллектуального ядра и недорогой VPS. В качестве СУБД используется SQLite, причем выбор обусловлен исключительно экономикой процесса: изначально развернутый в Docker MSSQL оказался слишком прожорливым для pet-проекта. Переезд занял ровно сутки и преподнес один занятный сюрприз, о котором расскажу позже.

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

Во сколько обходится инфраструктура

Учет затрат на токены я запустил в начале июня. За прошедшие семь недель четыре игрока (включая меня) совершили порядка 350 вызовов LLM, израсходовав 2,8 млн токенов.

Итоговая сумма по тарифам DeepSeek составила $1,45. За всё время и для всех участников.

Огромное достоинство DeepSeek — кэширование промптов: стоимость кэшированного ввода примерно на два порядка ниже обычного. Поскольку мои системные промпты объемные и статичные, кэш срабатывает регулярно. Самой затратной операцией оказалась генерация недельного плана, включающая объемный контекст и развернутый структурированный ответ, — на нее пришлась примерно половина расходов.

Вывод банален, но показателен: если избегать архитектурных глупостей (передавать агрегаты вместо сырых логов, кэшировать результаты в базе данных и запускать генерацию по событиям, а не каждые пять минут по cron), затраты на LLM в пет-проекте можно практически не замечать. Основной бюджет уходит на оплату VPS и доменного имени.

Борьба с галлюцинациями нейросети

Главный вывод за эти месяцы: галлюцинации LLM — это не аномальный сбой, а фоновое явление, учитывать которое приходится на уровне архитектуры. Разберу четыре характерных кейса.

Оценка несуществующих параметров

Ранние версии аналитических отчетов выставляли пользователям баллы по таким критериям, как «коммуникация» и «экономика». Ни один источник не предоставляет данных о голосовом чате, но модель это нисколько не смущало: она уверенно оценивала навык на шестерку из десяти и советовала «активнее общаться с командой». Звучит убедительно, опровергнуть невозможно, проверить нечем.

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

Метрика-призрак

Этот инцидент обошелся дороже. Интеграция со статистическим сервисом изначально предполагала наличие поля KAST (доля раундов, в которых игрок внес импакт). Колонка присутствовала в БД, модель рассуждала о ней в отчетах, а на дашборде красовался соответствующий виджет. Спустя время я решил проверить распределение в продакшене и обнаружил картину: 527 матчей — 527 значений NULL.

Внешнее API эту метрику не возвращает от слова совсем. Она никогда ее не отдавала: колонка в базе и поля в моделях были созданы «на перспективу» еще на этапе первичной интеграции, парсер честно записывал туда null, а все участники процесса искренне верили в существование метрики. Модель, впрочем, отсутствию данных тоже не удивилась.

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

Доказательства с выдуманной статистикой

Чтобы исключить превращение аналитики в астрологический прогноз, каждый тезис отчета должен подкрепляться конкретным числом из статистики: «время реакции составляет 650 мс при нормативре для вашего ранга 600». И вот здесь начинается самое интересное: модель с легкостью генерирует цифры, которых не было во входных данных. Написала 620 вместо 650 — округлила, усреднила показатели или просто придумала. Визуально это неотличимо от правды.

Проблема решается концептуально просто: ответ модели верифицируется программно, и каждая цифра из блока обоснований обязана строго соответствовать входному массиву. Тезис с вымышленным числом безжалостно отбраковывается. Лучше выдать отчет на один пункт короче, чем публиковать недостоверную информацию.

Динамика на основе трех матчей

Дайте нейросети два числа, и она мгновенно усмотрит в них тренд. Если за всю неделю пользователь сыграл всего три матча, статистического смысла в сопоставлении показателей «до и после» нет никакого, но модель с энтузиазмом объявит о «заметном прогрессе». Попытки запретить это через промпт дают нестабильный результат. Гораздо надежнее пресекать это на уровне кода: при недостаточном объеме данных дельта просто не рассчитывается и не передается в модель.

Ключевой принцип: ИИ предлагает, код принимает решения

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

Технические грабли

В процессе работы с DeepSeek и миграции на SQLite я собрал коллекцию неочевидных проблем.

Молчаливый игнор параметра

У DeepSeek предусмотрен инструмент управления глубиной рассуждений — reasoning_effort. Интуитивно я разместил его внутри объекта thinking, рядом с активацией самого режима:

{
  "model": "...",
  "thinking": { "type": "enabled", "reasoning_effort": "max" }
}

API успешно принял запрос, вернул статус 200, все работало. Спустя три недели выяснилось, что параметр должен располагаться на корневом уровне запроса, тогда как неизвестные поля внутри объекта thinking просто игнорируются без каких-либо ошибок. Все это время модель размышляла на стандартной глубине.

{
  "model": "...",
  "thinking": { "type": "enabled" },
  "reasoning_effort": "max"
}

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

Пустой контент при успешном статусе

Реальный симптом из продакшена: пользователю приходит сгенерированный ИИ разбор матча, но сам текст отсутствует — есть лишь шапка со статистикой. При этом в логах зафиксирован успешный запрос и списание токенов.

Причина кроется в том, что у моделей с механизмом рассуждений токены для внутренних размышлений и финального ответа расходуются из общего лимита. У меня стояло ограничение max_tokens: 8000; при активированном режиме thinking модель тратила весь лимит на раздумья, упираясь в ограничение с флагом finish_reason: length, в результате чего поле content возвращалось пустым.

Проблема решилась в лоб, но эффективно: лимит был поднят до максимального значения (у DeepSeek это 32k, при текущей ценовой политике экономить бессмысленно), а если поле content все же приходит пустым, инициируется повторный запрос с отключенным режимом thinking. Ответ получается менее детальным, зато гарантированно присутствует.

LINQ-запросы, которые компилируются, но не работают

Обещанный сюрприз переезда с MSSQL на SQLite. Вот фрагмент кода, написанный по привычке после работы с MSSQL:

var used = ctx.Plans
    .Where(p => p.PlayerId == playerId)
    .SelectMany(p => p.Items.Select(i => new { p.WeekStart, i.DrillId }));

Коррелированный оператор SelectMany с проекцией внешнего поля транслируется в SQL-конструкцию APPLY, которая не поддерживается в SQLite. Проблема устраняется разворотом выборки через навигационные свойства дочерней таблицы:

var used = ctx.PlanItems
    .Where(i => i.Plan.PlayerId == playerId)
    .Select(i => new { i.Plan.WeekStart, i.DrillId });

Тихая ошибка в масштабных коэффициентах

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

Функция детекции тильта, призванная рекомендовать паузу после череды неудачных игр, была полностью написана, успешно компилировалась, но за целый месяц не сработала ни разу. По иронии судьбы, тестов на этот сценарий предусмотрено не было, хотя проверка на реальных данных из продакшена выявила бы несоответствие за минуту. Ошибка обнаружилась в середине июня во время постраничной ревизии кода. Теперь конвертация единиц на границе стороннего API вынесена в единый изолированный метод, а пороговые значения проверяются на реальных продакшн-данных.

Что реализовать не удалось

Выявление причинно-следственной связи «потренировался — стал играть сильнее» требует формирования контрольных групп и объемов данных, недоступных рядовому игроку. Ранние версии интерфейса демонстрировали красивые графики прироста навыков; после ужесточения методологии страницы у большинства пользователей опустели. Я оставил этот вариант без изменений, поскольку демонстрация случайного шума под видом полезного сигнала возвращала проект к исходным «астрологическим» прогнозам.

Уровень «температуры» генерации не был нулевым, и пока я не объяснил системе, что любые колебания между двумя последовательными прогонами на одних и тех же данных — это статистический шум, а не реальные изменения в игре, модель регулярно спешила обрадовать игроков сообщением об «изменившейся оценке» после каждого обновления.

И главное: активных пользователей пока всего четко четыре человека. Говорить о продуктовых метриках, удержании (retention) и масштабировании пока преждевременно — если через полгода ситуация изменится, обязательно поделюсь результатами.

Итоги

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

Сам ресурс доступен по адресу drillmaster.pro, проект бесплатный, авторизация реализована через Steam.

С удовольствием отвечу на любые вопросы в комментариях — подробно обсудим архитектуру, работу с LLM, технологический стек и подводные камни. Благодарю за уделенное время.

 

Источник

Поделиться:

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов