vegman.dev
Все статьи

Как не дать модели врать цифрами

2026-04-09кейс

В моем контентном конвейере модель писала короткую заметку: один провайдер снизил цены на API. Я прочитала черновик и не нашла, к чему придраться. Цена основной модели совпадала с прайсом. Абзац был связный, вывод - обоснованный.

Выдуманными оказались числа рядом. Для двух соседних моделей того же провайдера модель назвала выход по 8 долларов вместо 12 и вход по 0,50 вместо 0,20. Это не галлюцинация про несуществующий продукт, не выдуманная компания и не ссылка в никуда. Четыре цифры, которых не было ни в одном источнике, стоящие ровно там, где читатель ждет цифру.

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

Модель врет уверенно и в мелочах

Опасна не крупная выдумка. Крупную видно: если модель напишет, что вход стоит 400 долларов за миллион токенов, вы споткнетесь на первом же чтении, потому что число выпадает из привычного диапазона. Ошибка, которая доходит до публикации, выглядит иначе.

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

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

Почему вычитка человеком это не ловит

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

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

Есть еще одна причина, менее приятная. Вы читаете текст, который сами заказали, - и читаете его как автор, а не как скептик. Вы уже знаете, что в заметке должно быть написано, и глаз подтверждает ожидание вместо того, чтобы его проверять. Внимание - плохой инструмент против ошибки, которая маскируется под норму. Оно кончается раньше, чем поток материалов.

flowchart LR
  A[Файл фактуры: выверенные числа] --> B[Генерация черновика]
  B --> C[Извлечение сумм и процентов из текста]
  A --> D{Число есть в фактуре?}
  C --> D
  D -- да --> E[Публикация]
  D -- нет --> F[Стоп: разбор вручную]
Гейт стоит между черновиком и публикацией: числа из текста сверяются с файлом фактуры, любое несовпадение останавливает материал

Механическая сверка: числа против фактуры

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

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

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

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

Принципиально, что решение принимает не модель. Никаких «попроси вторую модель проверить первую»: это сравнение конечного множества чисел из текста с конечным множеством чисел из файла, обычным кодом, с детерминированным результатом. В той истории с ценами на API гейт остановил бы заметку на первом же выдуманном значении: 8 и 0,50 в фактуре просто нет, там 12 и 0,20.

import re, sys

# денежные суммы и проценты: $0.20, 12 долларов, 0,50 долл., 8%
PATTERN = re.compile(
    r"\$\s*\d+(?:[.,]\d+)?"
    r"|\d+(?:[.,]\d+)?\s*(?:%|долл\w*|центов?|рубл\w*)"
)

def values(text):
    found = set()
    for m in PATTERN.finditer(text):
        raw = re.search(r"\d+(?:[.,]\d+)?", m.group()).group()
        found.add(float(raw.replace(",", ".")))
    return found

facts = values(open("facts.md").read()) # только выверенные числа по теме
draft = values(open("draft.html").read())

unknown = sorted(draft - facts)
if unknown:
    print("чисел нет в фактуре:", unknown)
    sys.exit(1) # публикация не состоится
Вся сверка - это разница двух множеств чисел и ненулевой код возврата

Где прием не срабатывает и что с этим делать

Два ограничения я нашла на практике, и оба стоит знать до того, как вы начнете доверять зеленому статусу.

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

Сверка по значению не ловит перепутанные числа. Она проверяет наличие числа в списке, а не его привязку к объекту. Если модель возьмет настоящую цену одной модели и припишет ее другой, оба числа окажутся в фактуре, множества сойдутся, гейт промолчит. Этот класс ошибок остается на человеке и на редакторе - и я предпочитаю говорить это прямо, а не делать вид, что автоматика закрыла тему.

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

Применить у себя

Что проверить на практике

  • Возьмите последние три сгенерированных материала с числами и выпишите из них все денежные суммы и проценты. Проверьте каждое по первоисточнику руками. Если хотя бы одно не подтвердилось - у вас та же проблема, что была у меня.
  • Заведите файл фактуры на один материал. Каждая строка - одно число, своими словами, с пояснением, что это за число и к чему оно относится.
  • Напишите извлечение чисел из текста регулярным выражением по двум формам: денежные суммы и проценты. Начните с этих двух, они дают основную долю выдумок.
  • Сравните множество чисел из текста с множеством из фактуры. Разницу выведите списком.
  • Поставьте выход скрипта в пайплайн так, чтобы ненулевой код возврата останавливал публикацию, а не писал предупреждение.
  • Отдельно проверьте фактуру на дампы: если в файле есть скопированная таблица целиком, вырежьте ее и оставьте только строки по теме материала.

Как понять, что получилось

Главный критерий - гейт иногда срабатывает. Проверка, которая на потоке материалов не остановила ни одного, скорее всего не работает: загляните в фактуру, там почти наверняка лежит слишком широкий список.

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

И бытовой критерий, по которому я поняла, что прием прижился: я перестала открывать прайс на каждой заметке. Не потому что расслабилась, а потому что этот вопрос теперь закрыт кодом. Осталась ровно одна ручная проверка - правильно ли число привязано к объекту, - и ее я делаю осознанно, а не в конце длинной вычитки, когда внимания уже нет.