Как мы разгребли зоопарк Excel-прайсов от поставщиков с помощью ИИ-агента

Байер дистрибьютора электротехники открывает почту в понедельник — и там несколько десятков непрочитанных писем с прайс-листами. Один поставщик прислал xlsx на дюжину листов с объединёнными ячейками. Другой — xls двухтысячных годов, где артикул лежит в колонке F, а у соседнего поставщика тот же артикул — в колонке B. Третий вообще присылает PDF с экспортом из 1С, где цена набита в одну строку с НДС, скидкой и комментарием «оптом от 50 шт.».
К обеду из этой пачки нужно получить один обновлённый прайс в собственной номенклатуре — чтобы менеджеры формировали клиентам коммерческие предложения. По факту байер успевает разобрать десяток файлов и идёт пить очередной кофе. Остальное — «на завтра», а к завтра приходит новая порция.
Так живёт любой дистрибьютор, монтажная компания, торговый дом, у которого сотни поставщиков. Именно с этим к нам пришёл заказчик из промышленной автоматики: «сделайте нам, пожалуйста, ИИ-агента, который перестанет заставлять людей копипастить цифры из чужих экселек».
Что приходит на вход на самом деле
Когда мы сели разбирать реальный архив за месяц — а там были тысячи файлов, — стало понятно, что «сорок семь шаблонов» это оптимистичная оценка. Уникальных структур оказалось в разы больше. Внутри одного поставщика структура менялась от месяца к месяцу: то добавится колонка «акция», то заголовок переедет на третью строку, то цены вдруг приходят в евро без курса.
Зоопарк выглядел так:
- xlsx с шапкой на пять строк, где первые две — логотип и контакты, третья — пустая, четвёртая — категория товара жирным, и только пятая — настоящие заголовки колонок;
- xls с объединёнными ячейками, где «Кабель ВВГнг-LS» написан один раз сверху, а ниже идут десятки строк с сечениями — и openpyxl видит их как пустые;
- листы, где артикул и наименование склеены в одну ячейку через перенос строки: «
КВВГ 4х1.5\nкабель контрольный»; - цены в форматах «
1 245,50 руб.», «1245.5», «1.245,50 €», «1245,50 (от 100м — 1180)»; - отдельная категория ада — листы, где данные начинаются глубоко в файле, потому что сверху лежит «прайс прошлого месяца зачёркнут».
Первая попытка была скучная и предсказуемая: написать парсер под каждого поставщика. Через месяц у нас было несколько десятков парсеров, и они ломались буквально на каждом втором обновлении. Поставщик решил, что колонку «остаток» логично переставить, — и всё, разбор упал. Мы посмотрели на этот огород, честно признались друг другу, что дальше будет только хуже, и пошли думать заново.
Почему «просто скормить файл LLM» не работает
Соблазн скормить xlsx целиком российской LLM и попросить «верни мне JSON со списком товаров» велик. На демо это выглядит убедительно. В проде разваливается в три приёма.
Первое — токены. Большой прайс в текстовом представлении съедает столько контекста, что окно GigaChat Pro забивается одним файлом. Дальше начинается выборочное «забывание» середины: модель честно вернёт первые пару сотен позиций и последние сто, а серёдку потеряет. Причём тихо, без ошибки.
Второе — галлюцинации на артикулах. Модель видит «ВВГнг 3х2.5» много раз подряд и где-то в середине начинает «дорисовывать» несуществующие сечения — они же логично продолжают ряд. Мы это ловили руками. И потом уже никто не хотел проверять каждый прайс глазами.
Третье — цены. LLM любого производителя стабильно ошибаются в переносе чисел с пробелами и запятыми в разделителях. «1 245,50» превращается то в 124550, то в 1245.5, то в 12455 — редко, но регулярно. На длинной таблице это десятки ошибок в одном файле. Каждая — потенциальный убыток на сделке.
Настройка ИИ-агента: LLM как нормализатор схемы, а не парсер данных
В какой-то момент мы развернули задачу. LLM не должна видеть весь прайс. Её работа — посмотреть на структуру файла и сказать, где что лежит. Дальше — обычный код.
Настройка ИИ-агента получилась примерно такой:
# 1. Открываем файл, читаем верхние строки всех листов
sample = extract_sample(file_path, rows=30, sheets="all")
# 2. Скармливаем LLM только этот образец + промпт-инструкцию
schema = llm.extract_schema(sample, response_format=PriceSchema)
# Возвращает: header_row=4, sku_col="B", name_col="C",
# price_col="F", unit_col="G", category_source="merged_A"
# 3. Парсим весь файл по схеме обычным openpyxl
rows = parse_by_schema(file_path, schema)
# 4. Валидация: цены — regex + Decimal, артикулы — по словарю поставщика
clean = validate_and_clean(rows, supplier_id=42)
# 5. Матчинг с собственной номенклатурой через pgvector
matched = match_to_catalog(clean)
LLM здесь делает ровно одну вещь — отвечает на вопрос «где у этого поставщика лежат данные». Тридцать строк образца — по грубой оценке пара тысяч токенов. Промпт стабильный. Ответ — строгая JSON-схема через function calling. По API это укладывается в секунды и в копейки за вызов.
Дальше работает обычный код. Никаких галлюцинаций — потому что данные модель в глаза не видит. Никаких потерянных цен — потому что числа парсит Decimal, а не нейросеть.
Отдельно про интеграцию по API. Мы обернули GigaChat Pro в тонкий сервис, который умеет ретраить упавшие вызовы, кэшировать ответы по хешу образца (один поставщик — одна и та же схема из месяца в месяц, повторный вызов делать смысла нет) и уважать таймауты вышестоящего прокси. Прокси у нас режет любые HTTP-соединения на сотой секунде, так что синхронно ждать медленную модель нельзя в принципе — ИИ-агент через API отправляет запрос и подхватывает результат уже из очереди.
Объединённые ячейки, НДС и единицы измерения: грязная работа
Самая неприятная часть оказалась не там, где ждали. Объединённые ячейки openpyxl честно отдаёт как «значение в первой, None во всех остальных». Мы написали отдельный шаг — unmerge_forward_fill, который перед парсингом протягивает значение вниз по объединённому диапазону. Для прайсов, где категории оформлены «шапками», это закрыло сразу большой пласт случаев — на треть точно.
Цены оказались проще, чем казалось. Регулярка плюс приоритет правил: сначала убираем валютные символы, потом пробелы как разделители тысяч, потом нормализуем запятую в точку, потом конвертируем в Decimal. Если в ячейке скобка с альтернативной ценой («1245 (от 100 — 1180») — это отдельный признак, который кладём в поле price_tiers, а основной ценой считаем первое число.
Единицы измерения — м, метр, м.п., шт, упак, бухта — свели к словарю канонических значений, которых набралось несколько десятков. Тут ИИ-агент пригодился во второй раз: новые написания, которых в словаре нет, он раз в день собирает и предлагает админу — «вижу новое: рул., предлагаю смапить на упак». Один клик — и словарь пополнился. За пару месяцев словарь стабилизировался, и новых предложений почти не приходит.
ИИ-агент для поиска товара в собственной номенклатуре
Получив чистые строки от поставщика, нужно понять — это новый товар или у нас такой уже есть в каталоге под своим артикулом. Здесь работает отдельный ИИ-агент для поиска: эмбеддинг полного описания товара поставщика ищется в индексе нашей номенклатуры. Выше порога косинусной близости — автоматический match. Ниже — карточка попадает в очередь на ручное подтверждение байеру.
Эмбеддинги считаем российской моделью, индекс держим в pgvector с HNSW. На каталоге в сотни тысяч позиций поиск одной строки укладывается в единицы миллисекунд. Целый прайс в несколько тысяч строк проходит матчинг заметно быстрее, чем открывается в Excel.
Порог косинусной близости мы подбирали итеративно, глядя на выборку размеченных вручную пар. Сначала держали строго — байеру прилетало слишком много «на подтверждение». Ослабили — начали проскакивать похожие, но разные товары («провод ПВС 3х1.5» и «кабель ВВГ 3х1.5» модели кажутся близкими, но это разные позиции). В итоге пришли к двум порогам: жёсткий — автоматический match, средний — «предложить как вариант», ниже — «новый товар». Байер видит только среднюю зону.
Что изменилось для людей
Раньше байер закапывался в чужие эксельки на полдня — и всё равно к вечеру половина писем оставалась в почте. Прайсы обновлялись с задержкой в неделю, актуальность цен и остатков в коммерческих предложениях менеджеров была «как повезёт».
Сейчас письма из ящика уходят в очередь автоматически, через считаные минуты после получения прайс уже в системе. Байер занимается только тем, что ИИ-агент пометил «требует подтверждения» — это малый процент позиций, преимущественно новые товары и подозрительные скачки цен. Обработка одного прайса из полу-рабочего дня превратилась в короткую сверку. Освободившееся время байер тратит на переговоры с поставщиками — то, ради чего его вообще нанимали.
Точность цен по выборочной ручной проверке — практически стопроцентная. Единичные ошибки, которые находим, — это экзотические случаи с многоуровневыми скидками, и под каждую мы дописываем отдельное правило.
Что не сработало и где мы оставили человека
Сканы в PDF мы из основного пайплайна выкинули. Для них отдельный маршрут через OCR, и качество там пока не дотягивает до автомата — байер всё равно перепроверяет. Прайсы с фотографиями товара вместо артикула — тоже руками, благо их единицы.
И главное — мы не убирали человека из цепочки насовсем. Байер по-прежнему подтверждает новые товары и аномалии: цена выросла в полтора раза за неделю — стоп, проверь. Просто теперь он смотрит на короткий список подозрительных позиций, а не выгрызает файлы из завалов почты. Это та самая автоматизация, ради которой всё затевалось: не «уволить человека», а снять с него работу, в которой он не нужен.
Байер, кстати, первую неделю не верил, что «оно правда правильно всё разобрало» — открывал соседним окном исходную экселку и сравнивал руками. К концу месяца перестал.
А вы пробовали считать, сколько часов в неделю ваш отдел закупок тратит на копирование цифр из чужих Excel в свой? Если такая задача есть — расскажем, как разворачивали похожий пайплайн у себя. Личный ИИ-агент для закупщика, который живёт внутри вашего контура и разбирает почту за него, — это уже не футурология, а вполне рабочая инженерная задача.
Похожие статьи
- Как мы научили ИИ-агента на GigaChat Pro не путать живого клиента с ночным батчем
- Как мы настроили ИИ-агента для поиска в 1С: MCP-сервер вместо метаний между окнами
- Один вопрос — три темы: как мы научили ИИ-агента разбирать составные запросы
- Text2SQL для 340 таблиц: как мы собирали ИИ-агента для анализа корпоративных данных на GigaChat Pro




