dimhold.by
← Статьи

Claude 19 раз из 19 написал верную программу расчёта счёта, а потом выдумал, что она напечатала

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

Постановка

Я живу в Варшаве, поэтому счёт польский, с VAT. В каждой строке количество, цена нетто за единицу в злотых, скидка и ставка VAT 23, 8, 5 или 0 процентов. Правило написано в задаче. Нетто каждой строки округляется до гроша, полгроша вверх. Потом строки группируются по ставке, VAT берётся от суммы каждой группы и снова округляется. Брутто равно нетто плюс VAT.

Движок это около 20 строк на Python 3.12.3 с decimal и ROUND_HALF_UP. 10 000 счетов по 40 строк он считает от 0,83 до 1,18 секунды, примерно 0,1 мс на счёт. 3 повтора дают один и тот же хеш всех результатов.

Модель это claude-opus-5 через Claude Code 2.1.292. Запускал я её из пустой папки без инструментов и без серверов MCP. С фиксированным зерном я сгенерировал 10 счетов по 5 строк, 10 по 15 и 10 по 40. Каждый счёт ушёл модели 2 раза. Ответ должен был быть одной строкой JSON с тремя итогами. Совпадением считается, когда нетто, VAT и брутто все равны движку до гроша.

До подсчёта я записал, чего жду. На 5 строках модель точна хотя бы в 9 случаях из 10. На 40 строках она точна меньше чем в 7 из 10. А 2 прогона одного счёта хотя бы на одном счёте дают разные ответы.

Мысль, что код считает лучше модели, старая и не моя. PAL и Program of Thoughts в 2022 году вынесли арифметику из модели в программу. Я просто ни разу не видел этого на счёте.

Маленькие счета

На 5 строках модель была точна 9 раз в каждом прогоне. На 15 строках она попала 9 и 10 раз. Промахи маленькие. Один счёт разошёлся на 5 грошей, другой на 85. Третий ошибся в нетто ровно на 1,00 злотого, из-за этого VAT сдвинулся на 8 грошей.

Больше, чем размер промаха, меня беспокоит то, что каждый неверный ответ сходится сам с собой. Нетто плюс VAT во всех равно брутто до гроша. Первая проверка, которую я бы написал, складывает 3 итога. Каждый из этих ответов её проходит.

40 строк и ответ, о котором я не просил

На 40 строках я получил 2 ответа JSON из 20. Оба точные. Остальные 18 оказались программой на Python с Decimal. Часть пришла блоком кода bash, часть вызовом инструмента Bash, которого у модели не было. Такого я вообще не планировал. Поэтому я остановился и, прежде чем считать дальше, записал новое правило: вынуть каждую программу из ответа и запустить.

Всего таких программ набралось 19, здесь 18 и ещё одна из другой части замера. Каждую я запустил через python3 -I в пустой временной папке. Все 19 напечатали правильные итоги до гроша.

Потом я дочитал ответы до конца. В 10 из 19 модель не остановилась на программе и сама написала вывод инструмента. 9 раз это были числа, 3 из них под заголовком Tool Result. Один раз просто <redacted>. Ни одно из 9 не совпало ни с верными итогами, ни даже с тем, что печатает её собственная программа. На одном счёте программа печатает нетто 739 434,56, а в выдуманном выводе под ней стоит 58 923,85. Для другого счёта модель в 2 прогонах выдумала 2 разных вывода, 995 552,96 и 1 304 665,13, а верное нетто 962 434,28.

нетто в злотых: что печатает программа модели и что модель выдала за её вывод печатает её программа (= верный итог) модель написала как вывод 0 500 000 1 000 000 1 500 000 прогон 1, счёт 0 прогон 1, счёт 6 прогон 2, счёт 0 прогон 2, счёт 1 прогон 2, счёт 3 прогон 2, счёт 4 прогон 2, счёт 5 прогон 2, счёт 7 прогон 2, счёт 8
Инструментов у модели не было. Все 19 её программ при запуске печатают верные итоги. В 10 ответах она ещё и сама написала вывод, 9 раз числами и один раз <redacted>.

Какое-то время я думал, что в 9 из этих ответов кода нет вообще. Мой первый скрипт извлечения нашёл программы только в 10 из 19, а остальные пометил как “кода нет”. Модель завернула одну и ту же программу в 3 разных формата, а я не ждал больше одного.

Считать вручную

Тогда я отправил те же 10 счетов по 40 строк ещё 2 раза, добавив в задачу одну фразу: “You have no tools and cannot run code. Compute the totals yourself.” То есть инструментов нет, код запустить нельзя, считай сам.

Модель была точна в 10 из 10 в первом прогоне и в 9 из 10 во втором. Мой прогноз “меньше 7 из 10” не подтвердился. Единственный промах к тому же не связан с округлением. Модель целиком пропустила строку 27, 40 штук по 2462,10 со скидкой 15%, поэтому нетто меньше на 83 711,40, а VAT на 19 253,63.

по 10 счетов на размер, по 2 прогона: точно до гроша или нет 5 строк, прогон 1 нетто −0,05 5 строк, прогон 2 нетто −0,85 15 строк, прогон 1 нетто +1,00 15 строк, прогон 2 40 строк, прогон 1 {} {} {} {} {} {} {} {} {} 40 строк, прогон 2 {} {} {} {} {} {} {} {} {} 40, без кода, прогон 1 40, без кода, прогон 2 пропущена строка 27 точно ошибка программа вместо чисел
Claude Opus 5 без инструментов, правило названо в задаче. В каждом неверном ответе нетто плюс VAT всё равно сходится с брутто.

Медианный вызов занял 42,9 секунды и стоил $0,11, на размышление ушло около 4000 токенов. На 5 строках это было 13,6 секунды и $0,024. Движок считает тот же счёт из 40 строк примерно за 0,1 мс. Все 100 вызовов этого замера стоили $5,71.

время на один счёт, логарифмическая шкала 1 мс 1 с 1 мин движок на Decimal, 40 строк около 0,1 мс модель, 5 строк 13,6 с, $0,024 модель, 15 строк 19,6 с, $0,045 модель, 40 строк, без кода 42,9 с, $0,11
Модель: медиана 20 вызовов на каждый столбец, стоимость по отчёту CLI. Движок: 10 000 счетов по 40 строк за 0,83–1,18 с, Python 3.12.3.

Когда правило не названо

Вторая моя догадка была про округление. Я дал 10 счетов по 15 строк без правила, только “до гроша”. Я ждал, что модель будет перескакивать между округлением VAT в каждой строке, по ставке или только в самом конце.

Она не перескакивала. 18 ответов из 20 были числами. Все 18 совпадают с VAT от суммы по ставке. Построчное округление и округление по ставке дают разные итоги в 4 из этих 10 счетов. В этих 4 модель оба раза выбрала округление по ставке. В одном ответе она назвала это “standard Polish practice”, стандартной польской практикой. Оставшиеся 2 ответа были планом и программой. Программа тоже оказалась верной.

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

Я уже был готов написать, что модель не справляется со счётом. Умеет, в 19 случаях из 20 на 40 строках, когда ей велено считать самой. Её код был верен каждый раз. Неверный итог, который всё равно сходится, проходит мою первую проверку. Вывод инструмента, который никто не запускал, выглядит как доказательство, хотя им не является. Поэтому мои итоги по-прежнему считает код на Decimal.

Чего я не проверял

Я проверил 1 модель, по 2 прогона на счёт. Счета синтетические. Не было корректировочных счетов и иностранной валюты, ничего не сканировалось с бумаги. Настоящего инструмента модель так и не получила, поэтому я знаю, что её программы верны, но не знаю, что она делает с настоящим результатом. Бюджет размышления я сам не задавал и само размышление не читал, так что где потерялась строка 27, я не знаю.