Недавно в популярном Facebook-посте: «GPT работает всё хуже. Просишь пересчитать формулу на 600 грамм, он бодро выдаёт две по 300. Пора, видимо, валить».

Проблема знакомая каждому, кто пытался использовать LLM для расчётов. Но это не деградация конкретной модели. Это фундаментальное ограничение архитектуры. И у него есть решение.

Почему LLM не умеют считать

Transformer предсказывает следующий токен на основе вероятностного распределения. Когда вы просите модель умножить 18 на 38.76, она не вызывает калькулятор. Она генерирует последовательность символов, которая «похожа» на правильный ответ.

Иногда совпадает. Иногда нет. И вы никогда не знаете заранее, когда модель решит, что результат «примерно 680» вместо 697.68.

Это не баг. Это следствие архитектуры. Модель обучена на текстах, а не на арифметических операциях. Она «видела» миллионы примеров умножения в обучающих данных и научилась воспроизводить паттерн. Но воспроизведение паттерна и выполнение операции — разные вещи.

Конкретный пример. Просим модель посчитать коммунальные платежи:

  1. Модель «вспоминает» примерные тарифы из обучающих данных (часто устаревшие или из другого региона)

  2. Выполняет умножение путём предсказания токенов

  3. Красиво оформляет результат

  4. Вы получаете число, которое выглядит правдоподобно, но может быть неверным на 10-30%

Проверить сложно. Вы ведь и обратились к ИИ потому что не хотели считать вручную.

Идея: пусть модель программирует, а не считает

Решение оказалось простым. LLM отлично генерирует код. Код отлично выполняет арифметику. Значит, нужно убрать модель из цепочки вычислений и оставить её там, где она сильна: в понимании задачи и генерации программы.

 Схема архитектуры: Пользователь → LLM → Python-код → Docker Sandbox → Результат (Excel/PDF)
Схема архитектуры: Пользователь → LLM → Python-код → Docker Sandbox → Результат (Excel/PDF)

Когда Python выполняет 18 * 38.76, результат всегда 697.68. Не «примерно 700». Не «около 680». Точное число. Каждый раз.

Модель не считает. Модель программирует. А программа считает.

Сравнение двух подходов: слева LLM считает напрямую (ошибка 2.5%), справа LLM генерирует код (точный результат)
Сравнение двух подходов: слева LLM считает напрямую (ошибка 2.5%), справа LLM генерирует код (точный результат)

Реализация

Мы применили этот подход в боте для бухгалтерских расчётов. Вот как выглядит процесс:

  1. Пользователь отправляет задачу в мессенджер: «Томск, ХВ 320, ГВ 229, эл 7422, пред: ХВ 302, ГВ 222, эл 7133»

  2. LLM получает системный промпт с контекстом задачи и актуальными тарифами региона

  3. Модель генерирует Python-скрипт: вычисление расхода, умножение на тарифы, формирование таблицы

  4. Скрипт выполняется в изолированной Docker-песочнице

  5. Результат: текстовый ответ с разбивкой по услугам + Excel-файл

Пример сгенерированного кода (упрощённый, в качестве примера):

# Показания счётчиков
cold_current, cold_prev = 320, 302
hot_current, hot_prev = 229, 222
elec_current, elec_prev = 7422, 7133

# Расход
cold_usage = cold_current - cold_prev   # 18 м³
hot_usage = hot_current - hot_prev      # 7 м³
elec_usage = elec_current - elec_prev   # 289 кВт·ч

# Тарифы Томск 2025 (из конфига, не из модели)
tariffs = {
    'cold_water': 38.76,
    'hot_water': 142.63,
    'electricity': 4.94,
    'drainage': 27.04,
}

# Расчёт
cold_cost = cold_usage * tariffs['cold_water']       # 697.68
hot_cost = hot_usage * tariffs['hot_water']           # 998.41
elec_cost = elec_usage * tariffs['electricity']       # 1427.66
drain_cost = (cold_usage + hot_usage) * tariffs['drainage']  # 676.00

total = cold_cost + hot_cost + elec_cost + drain_cost  # 3799.75

# Формирование Excel
import openpyxl
wb = openpyxl.Workbook()
ws = wb.active
ws.append(['Услуга', 'Расход', 'Тариф, ₽', 'Сумма, ₽'])
ws.append(['ХВС', f'{cold_usage} м³', tariffs['cold_water'], cold_cost])
ws.append(['ГВС', f'{hot_usage} м³', tariffs['hot_water'], hot_cost])
ws.append(['Электричество', f'{elec_usage} кВт·ч', tariffs['electricity'], elec_cost])
ws.append(['Водоотведение', f'{cold_usage + hot_usage} м³', tariffs['drainage'], drain_cost])
ws.append(['ИТОГО', '', '', total])
wb.save('communal.xlsx')

Ключевой момент: тарифы инжектируются в контекст из конфига, а не берутся из «памяти» модели. Когда тариф меняется, обновляется одна строка в конфиге. Модель не нужно переобучать или даже перезагружать.

Реальный результат: таблица с услугами, расходом, тарифами и суммами. Без интерфейса бота, только файл.
Реальный результат: таблица с услугами, расходом, тарифами и суммами. Без интерфейса бота, только файл.

Выбор моделей

Используем Qwen и DeepSeek. Выбор прагматический:

Qwen (Alibaba): бесплатный tier на Alibaba Cloud, достаточное качество генерации кода, стабильный API. Для задач типа «посчитай коммуналку» хватает с запасом.

DeepSeek V3: платный, но с кэшированием промптов даёт хорошую маржу. Используем для более сложных задач: анализ смет, многостраничные документы.

Почему не GPT-4 или Claude? Для задачи «сгенерируй Python-скрипт на 20 строк для расчёта коммуналки» они избыточны. Разница в качестве генерации кода для таких задач минимальна, а разница в цене и доступности — существенна.

Песочница

Каждый пользователь получает изолированный Docker-контейнер. Это важно по двум причинам:

  1. Безопасность. LLM генерирует произвольный Python-код. Без песочницы это дыра в безопасности размером с Марианскую впадину.

  2. Изоляция данных. Один пользователь не видит данные другого. Контейнер создаётся на время задачи, после выполнения файлы извлекаются и контейнер уничтожается.

Таймаут выполнения: 30 секунд. Если модель сгенерировала бесконечный цикл (бывает), контейнер убивается, пользователь получает сообщение «попробуйте переформулировать задачу».

Грабли

Подход «LLM генерирует код» звучит элегантно. На практике грабли расставлены густо.

Грабля 1: галлюцинация тарифов

Первая версия просила модель самостоятельно определить тарифы ЖКХ для города. Модель уверенно выдавала числа. Красиво отформатированные. Абсолютно неправильные.

Тариф на холодную воду в Томске: 38.76 руб/м³. Модель выдала 42.50. Откуда? Из обучающих данных за 2023 год, возможно из другого региона. Выглядело правдоподобно. Было неверным.

Решение: тарифы хранятся в конфиге и инжектируются в системный промпт. Модель не придумывает тарифы, а подставляет предоставленные. Галлюцинации ушли.

Тарифы берём из официальных источников: vodokanal.tomsk.ru, tomskenergosbyt.ru, данные ТомскРТС. Модель сама не ходит в интернет за тарифами. Мы парсим источники, обновляем конфиг, инжектируем в промпт. Модель получает готовые числа и подставляет их в код.

Это принципиально. Если дать модели доступ в интернет и попросить "найди тариф на холодную воду в Томске", она может вытащить число с форума 2019 года, из статьи про другой регион или вообще сгаллюцинировать. А если у модели нет доступа в интернет (как у большинства code-генераторов), она возьмёт тариф из обучающих данных, которые устарели на год-два.

Единственный надёжный путь: тарифы как данные, не как знания модели.

Грабля 2: модель «округляет в уме»

Даже с явной инструкцией «напиши Python-код», некоторые модели (особенно на бесплатных тирах) иногда ленятся и выдают результат напрямую, без кода. «Расход воды: 18 м³, тариф 38.76, итого примерно 697 рублей». Без скрипта. С округлением.

Решение: валидация ответа. Если в ответе модели нет блока кода (python), запрос повторяется с усиленной инструкцией: «Ты ОБЯЗАН написать Python-скрипт. Не считай в уме. Не округляй. Напиши код.» Со второй попытки срабатывает в 99% случаев.

Грабля 3: кракозябры в Excel

Модель генерирует код создания Excel через openpyxl. Всё работает. Открываешь файл: кракозябры вместо кириллицы.

Решение: явное указание encoding='utf-8' в системном промпте как обязательное требование. Плюс пост-валидация: если в сгенерированном коде нет utf-8 при работе с файлами, добавляем автоматически перед выполнением.

Грабля 4: import несуществующих библиотек

Модель иногда генерирует import pandas или import numpy для задачи, где достаточно стандартной библиотеки. В песочнице pandas может не быть (зачем тащить 50 МБ ради сложения двух чисел?).

Решение: базовый набор библиотек в Docker-образе (openpyxl, json, csv, math, datetime). В системном промпте явно: «Используй только стандартную библиотеку Python и openpyxl. Не используй pandas, numpy, scipy.»

Грабля 5: счётчик токенов считал не то

Это не про LLM, а про UX. Счётчик показывал пользователю количество символов в ответе (включая весь DOM интерфейса), а не реальное потребление токенов API. Вместо 320 токенов за задачу пользователь видел 128 000. Представьте реакцию.

Решение: считать токены на стороне бэкенда, из ответа API. Показывать пользователю реальное потребление с конвертацией в рубли.

Грабля 6: stderr критичен

Когда скрипт падает с ошибкой, первая версия возвращала только exit code. Модель не видела traceback и не могла исправить ошибку. Вместо этого она генерировала новый скрипт с нуля, часто с той же ошибкой.

Решение: полный stderr возвращается модели. Она читает traceback, понимает проблему, исправляет конкретную строку. Количество «мусорных» итераций упало в 5 раз. Это оказался самый высокоэффективный фикс из всех.

Усложнение: анализ смет

Бухгалтерские расчёты — задачи с фиксированной структурой. Коммуналка — это всегда расход * тариф. Счёт — это всегда шаблон с реквизитами. Модели легко генерировать код для таких задач.

Проверка смет оказалась значительно сложнее.

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

Здесь модель генерирует более сложный скрипт: парсинг Excel (структура у каждой сметы своя), поиск актуальных цен, сравнение, формирование отчёта с цветовой разметкой.

Реальный тест: смета на ремонт ванной, Томск. Результат:

  • Завышение 54 168 руб. (25.9%)

  • 8 позиций завышены более чем на 50%

  • Обнаружены арифметические ошибки (расхождения 1-4 рубля на позицию)

  • Excel с цветовой разметкой: зелёный (норма), жёлтый (умеренное завышение), красный (значительное)

Реальный результат(одного из расчётов): таблица с позициями сметы, рыночными ценами и цветовой индикацией завышений.
Реальный результат(одного из расчётов): таблица с позициями сметы, рыночными ценами и цветовой индикацией завышений.

Принцип тот же: LLM программирует, Python считает. Но объём генерируемого кода вырос с 20 строк (коммуналка) до 150-200 строк (анализ сметы). И количество граблей — пропорционально.

Точность

Арифметическая точность: 100%. Считает Python, не LLM. Тут ошибок нет по определению.

Точность интерпретации задачи: ~95%. Оставшиеся 5% — это случаи когда модель неправильно понимает контекст. Пользователь пишет «ХВ 320», модель интерпретирует как расход 320 м³, а не показание счётчика 320. Решается уточняющими вопросами и примерами в промпте.

Точность тарифов: зависит от актуальности конфига. Тарифы ЖКХ меняются раз в полгода. Пока обновляем вручную. В планах — парсинг с официальных сайтов ресурсоснабжающих организаций.

Выводы

LLM плохо считают. Это не деградация, не баг, не проблема конкретного провайдера. Это архитектурное ограничение Transformer: предсказание токенов ≠ вычисление.

Решение: вынести вычисления из модели. Пусть LLM делает то, что умеет хорошо (понимание задачи, генерация кода), а арифметику оставить интерпретатору Python.

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

Главные уроки:

  1. Тарифы и справочники — в контекст, не в модель. Всё, что модель может нагаллюцинировать, нужно предоставить явно.

  2. Валидация ответа обязательна. Если модель не сгенерировала код, а «посчитала в уме» — повторный запрос.

  3. stderr спасает. Без полного вывода ошибок модель не может дебажить свой же код. Это был самый результативный фикс.

  4. Песочница не опциональна. LLM генерирует произвольный код. Выполнять его без изоляции — приглашение к катастрофе.

  5. Бесплатные модели достаточны. Для генерации Python-скриптов на 20-200 строк разница между GPT-4 и Qwen минимальна. А разница в стоимости — на порядки.


Если у кого-то есть опыт с аналогичным подходом (LLM как генератор кода для расчётных задач), интересно сравнить грабли. Какие модели лучше справляются с генерацией вычислительных скриптов? Как решаете проблему устаревших данных в контексте?