Как заставить LLM считать точно: генерация кода вместо генерации ответов

Как заставить LLM считать точно: генерация кода вместо генерации ответов

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

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

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

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

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

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

Вот как это работает на практике, например, при расчёте коммунальных платежей:

  1. Модель «вспоминает» тарифы из обучающих данных — часто устаревшие или из другого региона.
  2. Выполняет умножение через предсказание токенов.
  3. Красиво оформляет результат.
  4. Вы получаете правдоподобное, но потенциально ошибочное число — с погрешностью до 10–30%.

Проверить результат сложно: ведь вы и обратились к ИИ, чтобы не считать вручную.

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

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

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

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

Реализация

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

  1. Пользователь отправляет задачу: «Томск, ХВ 320, ГВ 229, эл 7422, пред: ХВ 302, ГВ 222, эл 7133».
  2. LLM получает системный промпт с контекстом и актуальными тарифами региона.
  3. Модель генерирует Python-скрипт: вычисление расхода, умножение на тарифы, формирование таблицы.
  4. Скрипт выполняется в изолированной Docker-песочнице.
  5. Результат: текстовый ответ с разбивкой по услугам + Excel-файл.

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

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

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

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

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

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

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

  1. Безопасность. LLM генерирует произвольный Python-код. Без песочницы это критическая уязвимость.
  2. Изоляция данных. Один пользователь не видит данные другого. Контейнер создаётся на время задачи, после выполнения — уничтожается.

Таймаут выполнения — 30 секунд. Если модель сгенерировала бесконечный цикл, контейнер убивается, а пользователь получает предложение переформулировать задачу.

Грабли и решения

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

Первая версия позволяла модели самой определять тарифы. Она уверенно выдавала красивые, но неверные числа.

Например, тариф на холодную воду в Томске — 38.76 руб/м³. Модель выдала 42.50. Вероятно, взяла данные из другого региона или устаревшего источника.

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

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

Это принципиально: если дать модели доступ в интернет, она может вытащить данные с форума или из статьи про другой регион. Если доступа нет — возьмёт устаревшие данные из обучающей выборки.

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

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

Некоторые модели (особенно бесплатные) игнорируют просьбу сгенерировать код и просто выдают округлённый ответ: «итого примерно 697 рублей».

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

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

Модель генерирует код через openpyxl, но забывает про кодировку. В итоге — кириллица превращается в кракозябры.

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

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

Модель генерирует import pandas, хотя в песочнице её нет. Зачем тащить 50 МБ ради сложения двух чисел?

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

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

Счётчик показывал пользователю количество символов в DOM-интерфейсе, а не реальное потребление токенов API. Вместо 320 токенов — 128 000. Пользователь в шоке.

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

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

Первая версия при ошибке возвращала только код выхода. Модель не видела traceback и генерировала новый скрипт с той же ошибкой.

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

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

Бухгалтерские расчёты — задачи с фиксированной структурой. Модели легко генерировать код, когда формула проста: расход × тариф.

А вот проверка смет — сложнее. Пользователь присылает Excel, указывает город. Нужно:

  • Проверить завышение цен.
  • Сравнить с рыночными данными.
  • Найти арифметические ошибки.

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

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

  • Завышение — 54 168 руб. (25.9%).
  • 8 позиций завышены более чем на 50%.
  • Обнаружены арифметические ошибки (расхождения 1–4 рубля).
  • Excel с цветовой разметкой: зелёный (норма), жёлтый (умеренное завышение), красный (значительное).

Принцип тот же: LLM программирует, Python считает. Арифметическая точность — 100%. Считает интерпретатор, не модель.

Точность интерпретации задачи — около 95%. Остальные 5% — ошибки понимания контекста. Например, «ХВ 320» модель может принять за расход, а не за показание счётчика. Решается уточняющими вопросами и примерами в промпте.

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

Итоги

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

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

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

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

  1. Тарифы и справочники — в контекст, не в модель. Всё, что может быть сгаллюцинировано, должно подаваться явно.
  2. Валидация ответа обязательна. Если модель не сгенерировала код — повторный запрос с жёсткой инструкцией.
  3. stderr спасает. Без полного вывода ошибок модель не может исправить свой код. Это самый результативный фикс.
  4. Песочница не опциональна. LLM генерирует произвольный код. Выполнять его без изоляции — путь к катастрофе.
  5. Бесплатные модели достаточны. Для генерации скриптов на 20–200 строк разница между GPT-4 и Qwen минимальна. А разница в стоимости — на порядки.
Читать оригинал