Перейти к содержимому
14 авг. 2024 г.·7 мин чтения

Нормализация дат, валют и чисел после LLM без путаницы

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

Нормализация дат, валют и чисел после LLM без путаницы

Откуда берется разнобой в форматах

LLM не знает, что в вашей системе есть один допустимый формат, если вы не задали его явно. Для модели записи 01/02/24, 2024-02-01 и 1 февраля 2024 часто означают одно и то же.

Проблема начинается в обучающих данных. Модель видела тексты из разных стран, таблицы, письма и CRM, где даты, суммы и числа записывают по местным привычкам. Поэтому она легко смешивает дату в формате ISO, разговорную запись и форму из исходного документа в одном ответе.

С деньгами происходит то же самое. Одна и та же сумма может прийти как 1,200.50, 1 200,50 или 1200.50 тг. Для человека разница небольшая. Для кода это уже три разные строки.

Обычно разнобой возникает по четырем причинам:

  • промпт просит извлечь данные, но не задает точный шаблон вывода
  • модель копирует запись из письма, счета или PDF
  • локали по-разному используют точку, запятую и пробел
  • разные модели возвращают один и тот же смысл в разной форме

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

Без общего правила ошибки не всегда видны сразу. Импорт может молча пропустить часть строк, фильтр по дате не найдет нужные записи, а поиск по сумме покажет неполную выборку. На глаз строки похожи. Система сравнивает не смысл, а символы.

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

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

Что стоит приводить к одному виду

Если модель читает письма, счета, формы и чаты, она почти всегда вернет данные в смешанном виде. Один источник пишет 03.04.2025, другой April 3, 2025, третий просто вчера. То же самое происходит с суммами, процентами и пустыми полями. Если не привести это к одному виду, код потом начинает гадать, что имелось в виду.

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

С датами лучше сразу хранить один внутренний формат. Для даты это может быть YYYY-MM-DD, для времени HH:MM:SS, для метки времени - ISO 8601 с часовым поясом. Иначе 05/06/24 у одной команды станет 5 июня, а у другой 6 мая. Если источник пишет завтра или в прошлую пятницу, помечайте такое значение как относительное, а не точное.

Сумму почти всегда стоит делить на две части: число и валюта. Не храните 1 250 000 тг одной строкой, если дальше нужны фильтры, расчеты или сверка. Лучше иметь amount: 1250000 и currency: KZT. Так проще не потерять знак, не перепутать тенге с рублями и не сломать отчеты, когда модель в одном ответе пишет $1,200, а в другом 1 200 USD.

Обычные числа тоже требуют порядка. Выберите один десятичный разделитель, убирайте лишние пробелы, а единицы измерения храните отдельно. С процентами тоже нужен один подход: либо 12.5% как строка, либо 0.125 как число. Диапазоны вроде 10-15 или от 10 до 15 удобнее разбирать на min и max.

Отдельная ловушка - пустые значения. Не указано, н/д, -, пустая строка и 0 не означают одно и то же. Ноль говорит, что значение есть и оно равно нулю. Не указано значит, что данных нет. Если смешать эти случаи, появятся странные суммы, неверные проценты заполнения и шум в аналитике.

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

Сначала зафиксируйте канон

Если у вас нет одного внутреннего формата, нормализация быстро превращается в набор заплаток. Модель может вернуть 01.02.2024, 2024/02/01 или 1 Feb 2024, и все три записи будут равны только для человека. Для кода это три разных случая.

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

Для дат чаще всего хорошо работает ISO 8601: 2024-02-01. Этот формат короткий, однозначный и спокойно проходит через базы, API и таблицы. Если в данных есть время, решите это сразу: храните его в UTC, в локальном часовом поясе или вместе со смещением, например 2024-02-01T14:30:00+05:00.

С деньгами правило простое: сумма и валюта не должны жить в одном поле. Храните число отдельно и код валюты отдельно, например 15000.50 и KZT. Иначе строка вроде 15 000 тг, $15,000 или 15.000,00 начнет ломать и парсинг, и расчеты.

С числами тоже лучше не оставлять свободу. Выберите один десятичный разделитель, обычно точку, а разделители тысяч уберите при хранении. Тогда 12,5, 12.5 и 12 500 не перепутаются в одном потоке обработки.

В схеме стоит заранее закрепить несколько вещей:

  • формат даты: YYYY-MM-DD
  • правило для времени: UTC, локальное время или время со смещением
  • денежное поле: число отдельно, код валюты отдельно
  • один формат числа без пробелов и без разделителей тысяч
  • отдельный статус для неизвестных и сомнительных значений

Последний пункт часто недооценивают. Если модель не уверена, не заставляйте систему угадывать. Лучше хранить статус вроде unknown, missing или ambiguous, чем подставить неверную дату или валюту и потом исправлять всю цепочку.

Хороший внутренний формат выглядит скучно. Это нормально. Скучный формат живет в продакшене лучше, чем удобная для чтения строка, которую каждый источник пишет по-своему.

Как построить обработку после ответа модели

Надежная нормализация начинается не с парсера, а с формы ответа модели. Если оставить свободный текст, вы почти сразу получите смесь форматов: 01/02/24, 1 февраля 2024, 1,250.00 и 1 250,00 в одном потоке.

Рабочая схема простая.

Сначала просите JSON. Модель должна вернуть поля с понятными именами: invoice_date, amount, currency, raw_text. Так вы отделяете данные от лишних слов и не тратите время на разбор фраз вроде сумма к оплате составляет.

Потом разделите процесс на два шага. Сначала извлечение значения, потом приведение к вашему формату. Это важный момент. Модель может правильно вытащить строку 03/04/2025, но переводить ее в ISO дату должен уже ваш код по понятным правилам.

Локаль определяйте по контексту, а не по одному символу. Одна запятая ничего не доказывает. Смотрите на язык письма, страну контрагента, код валюты, формат других полей, подпись документа. Если в письме есть тенге или KZT, а сумма записана как 1,250.50, не спешите считать это американским форматом без других признаков.

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

Храните исходный фрагмент рядом с нормализованным значением. Например, сохраняйте raw_amount: "1 250 000,50 ₸" и отдельно amount: 1250000.50, currency: "KZT". Тогда команда быстро поймет, где ошиблась модель, а где ваш нормализатор.

На практике этого хватает для большинства задач. Если вы пропускаете документы через несколько моделей и один общий API, такой конвейер проще поддерживать, чем набор исключений под каждый новый формат. Для команд, которые работают через AI Router и меняют провайдера за одним совместимым с OpenAI эндпоинтом, единый слой нормализации особенно полезен: меняется модель, а правила разбора остаются теми же.

Где чаще всего ломаются даты

Сравните модели на практике
Прогоните один набор документов через разные модели и сравните формат ответа в одном API.

Больше всего ошибок дают даты, которые выглядят привычно для человека и опасно для кода. Строка 03/04/2024 может означать 3 апреля или 4 марта. Пока вы не знаете язык письма, страну отправителя или формат источника, такую дату лучше считать неясной.

После ответа LLM это всплывает постоянно. Модель может взять формат из письма, из таблицы или из текста пользователя. В одном месте она пишет 2024-04-03, в другом 03.04.24, а рядом добавляет Apr 3, 2024. Если парсер молча выберет один вариант, ошибка уйдет в базу и потом будет дорого стоить.

С датами полезно держать несколько правил:

  • храните исходную строку отдельно от нормализованного значения
  • не разбирайте 03/04/2024 без контекста страны или языка
  • если есть только месяц и год, не добавляйте день от себя
  • время и часовой пояс обрабатывайте отдельно от самой даты
  • слова сегодня, вчера, завтра переводите в дату только при наличии опорной даты

Частая ловушка - неполная дата. Если модель вернула февраль 2024, это не 2024-02-01. Это месяц и год без дня. Храните именно эту точность. Иначе отчет или срок оплаты сдвинется на начало месяца, хотя источник этого не говорил.

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

Отдельная тема - время. 03.04.2024 10:00 без часового пояса и 2024-04-03T10:00+05:00 нельзя считать одним и тем же значением. Для банка, колл-центра или доставки разница в несколько часов уже меняет порядок событий. Если зона не указана, так и помечайте: время есть, зона неизвестна.

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

Валюты и числа: где теряется знак и масштаб

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

Символ $ сам по себе ничего не гарантирует. Это может быть USD, CAD, AUD и не только. Если модель вернула $ 1,200, не угадывайте валюту по символу. Ищите код валюты рядом, страну документа, язык письма, счет продавца или явное поле currency: "USD". Если таких признаков нет, лучше пометить значение как неоднозначное.

С разделителями та же история. 1,234 в одном документе значит тысяча двести тридцать четыре, а в другом - 1.234. Пробел тоже важен: 1 234,56 и 1 234.56 выглядят похоже, но относятся к разным привычкам записи. Парсеру нужен понятный порядок проверки. Иначе он начнет гадать.

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

Отрицательные суммы часто прячутся не только в -1000. Бухгалтерские документы любят формат (1 000,50), а некоторые модели возвращают символ вместо обычного -. Если проверять только один вариант, система легко превратит расход в доход.

Масштаб ломается еще чаще. 1.2 млн, 1,2m и 1200000 должны стать одним числом, если контекст одинаковый. Но m не всегда означает million: в технических данных это может быть meter, а в финансах иногда встречается MM для миллионов. Поэтому исходную строку лучше хранить всегда.

С процентами тоже нужно выбрать одно правило до релиза. Если модель пишет 12,5%, система должна всегда превращать это либо в 12.5, либо в 0.125. Оба варианта рабочие. Плохо только одно: когда в одной таблице живут оба сразу.

Если вы прогоняете ответы через несколько моделей, разброс в записи будет встречаться чаще. Одна модель вернет KZT 1 200 000, другая 1.2 млн тг, третья отдаст JSON с числом 1200000. Пока нет общих правил для знака, валюты и масштаба, такие ответы нельзя честно сравнивать и отправлять в расчет.

Пример со счетами и письмами

Соберите аккуратный пайплайн
Проверьте, как один API помогает держать единые правила разбора для всех моделей.

Один и тот же поставщик может писать по-разному даже в пределах одной недели. В счете стоит оплатить до 15.03, в письме менеджер пишет March 15, а в чате появляется 15/03/24. Модель обычно понимает смысл, но для учета этого мало. Бухгалтерии нужен один формат.

С суммами ситуация такая же. В одном сообщении есть 125 000 ₸, в другом $3,500.00, в третьем 1.250,00 EUR, а иногда сумма приходит вообще без кода валюты. Если оставить все как есть, отчеты начнут расходиться: где-то число прочтется как 1250, а где-то как 1.25.

Простой поток выглядит так. Модель читает счет, письмо и чат поставщика, а потом извлекает три поля: срок оплаты, сумму и валюту.

На входе она может увидеть:

  • Оплатите до 15.03, сумма 125 000 ₸
  • Invoice total: $3,500.00, due March 15
  • К оплате 1.250,00 EUR, срок 15/03/24

После нормализации система приводит это к одному виду:

  • дата: 2024-03-15
  • сумма: 125000.00, 3500.00, 1250.00
  • валюта: KZT, USD, EUR

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

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

Ошибки, которые ломают результат

Самая частая ошибка проста: команда верит, что строгий промпт сам удержит формат. На практике модель может один раз вернуть 01.02.2025, потом 2025-02-01, а в следующем ответе написать 1 Feb 2025. Если после этого вы просто сохраняете строку как есть, разнобой почти неизбежен.

Вторая ошибка дороже: принять аккуратную строку за корректное значение. Внешне 1,250 и 1.250 похожи, но в одном случае это тысяча двести пятьдесят, а в другом одна целая и двести пятьдесят тысячных. Такая мелочь ломает отчеты, лимиты и сверку.

С валютами данные часто портят еще раньше. Команда убирает символ , $ или , а потом пытается угадать код валюты по числу и контексту. Лучше делать наоборот. Сначала определите валюту как отдельное поле: KZT, USD, EUR. Только потом очищайте сумму для парсинга. Иначе $ 5,000 и 5 000 тг превратятся в одинаковую строку 5000, хотя это разные деньги.

Еще один частый сбой - слепая замена разделителей. Разработчик пишет простое правило: заменить все запятые на точки, убрать пробелы, и готово. Но строка 1.234,56 после такой чистки может стать 1.234.56, а это уже мусор. Сначала нужно понять локаль или хотя бы проверить, какой символ играет роль десятичного разделителя в конкретной строке.

Здесь помогает короткий набор правил:

  • храните исходное значение рядом с нормализованным
  • валидируйте дату, число и код валюты по отдельности
  • отмечайте сомнительные случаи флагом, а не угадывайте
  • гоняйте тесты на примерах из разных локалей

Исходное значение удалять рано. Оно нужно, когда разбор падает, когда пользователь спорит с итоговой суммой и когда вы чините правило через неделю. Простая схема выглядит так: raw_value, parsed_value, currency_code, parse_status, error_reason.

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

Проверка перед релизом

Подключитесь без лишней миграции
Смените только base_url и продолжайте работать с теми же SDK и промптами.

Перед запуском проверяйте не только промпт, но и правила на выходе. Если у вас нет одного формата для дат, сумм и процентов, путаница появится в первую же неделю: одна модель вернет 03/04/2025, другая 2025-04-03, третья напишет 3 Apr 2025.

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

Перед релизом полезно пройтись по короткому списку:

  • у каждой даты есть один формат хранения и одно правило для часового пояса
  • у каждой суммы есть число, валюта и знак, а не одна строка вроде USD 1,200
  • тесты отдельно ловят двусмысленные даты: 04/05/2025, 05/04/2025 и похожие случаи
  • пустые и частичные значения не маскируются под нормальные
  • система сохраняет исходную строку и пишет в лог, почему запись не прошла проверку

Особенно часто ломаются частичные данные. Модель может вернуть только месяц и год, сумму без валюты или процент без знака. Не пытайтесь додумать за нее. Лучше сохранить статус partial, чем тихо превратить 05/2025 в 2025-05-01 и потом спорить с бухгалтерией или юристами.

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

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

Хороший релизный тест выглядит скучно, и это нормально. Возьмите 30-50 реальных строк из писем, счетов и форм, прогоните их через парсер и вручную проверьте спорные случаи. Если на этом наборе все прозрачно, дальше будет спокойнее.

Что сделать дальше в продакшене

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

Начните не с большой архитектуры, а с набора живых примеров. Возьмите 30-50 фрагментов из реальных счетов, писем, заявок и таблиц. Смешайте аккуратные случаи с неприятными: даты вроде 03/04/25, суммы с пробелами, запятыми и знаком валюты, отрицательные числа в скобках, сокращения вроде тыс. и млн.

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

Для первого спринта обычно достаточно четырех вещей:

  • собрать небольшой golden set из живых документов
  • зафиксировать единый формат для даты, валюты и чисел
  • добавить проверку схемы после нормализации
  • завести короткие коды ошибок и логировать сырой фрагмент

Коды ошибок лучше сделать понятными: DATE_AMBIGUOUS, CURRENCY_UNKNOWN, NUMBER_SCALE_CONFLICT, SCHEMA_MISSING_FIELD. Тогда разработчик сразу видит, что чинить, а аналитик понимает, почему запись ушла на ручную проверку.

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

Если вы используете несколько моделей через AI Router на airouter.kz, такой слой особенно полезен. У сервиса один совместимый с OpenAI API для разных провайдеров, поэтому модель можно менять без переписывания интеграции, а нормализация и валидация остаются общими для всего потока.

И еще одна вещь, которую часто зря откладывают: добавьте golden set в CI. Если после правки парсера дата из 05.06.2025 вдруг стала июнем вместо мая, тест поймает это до релиза. Для таких задач это обычная страховка, а не роскошь.

Нормализация дат, валют и чисел после LLM без путаницы | AI Router