LLM извлекает список позиций: как считать пропуски, лишние строки и дубли
Модель переносит позиции накладной в JSON. На первый взгляд всё хорошо: артикулы знакомые, количества — числа. Но одну строку модель повторила, другую пропустила, третью придумала из похожего названия. Проверка схемы принимает такой JSON, а обычное сравнение множеств может скрыть повтор.
Для списка позиций полезно отдельно считать верно извлечённые строки, пропуски и лишние записи. При этом сначала нужно определить, имеет ли значение число одинаковых строк. В накладной две одинаковые позиции могут относиться к разным партиям или местам отгрузки. Автоматическое удаление дублей изменит исходные данные.
Зафиксируйте единицу сравнения
В примере строка определяется парой sku и qty. Ожидаются две позиции A-2 с количеством 2 и одна B-1 с количеством 1. Порядок строк не важен, количество повторений важно. Поэтому результат сравнивается как мультимножество: каждому значению соответствует число его появлений.
Если документ содержит line_id, номер партии или цену, включите нужные поля в идентичность строки. Нельзя считать совпавшим артикул, когда модель перенесла цену соседней позиции. И наоборот, косметическое различие вроде пробела по краям можно нормализовать заранее, если такое правило согласовано. Регистр и ведущие нули у кодов обычно требуют отдельного решения.
Дайте модели инструкцию сохранить строки
Учебный вход: «Строка 1: A-2, количество 2. Строка 2: A-2, количество 2. Строка 3: B-1, количество 1». Инструкция извлечения: «Верни items с полями sku и qty. Каждая исходная строка должна дать отдельный элемент. Сохраняй одинаковые строки и исходное написание артикула. Не объединяй количества и не добавляй позиции, которых нет в тексте».
Схема JSON проверяет, что items является массивом, sku — строкой, qty — допустимым количеством. После этого содержимое сравнивается с эталоном, размеченным по документу. В приведённом ниже кандидате три строки A-2 и одна лишняя C-1. Это специально подготовленный ошибочный ответ для проверки расчёта, а не результат вызова провайдера.
from collections import Counter
expected = [
{"sku": "A-2", "qty": 2},
{"sku": "A-2", "qty": 2},
{"sku": "B-1", "qty": 1},
]
predicted = [
{"sku": "A-2", "qty": 2},
{"sku": "A-2", "qty": 2},
{"sku": "A-2", "qty": 2},
{"sku": "C-1", "qty": 1},
]
def bag(rows):
result = Counter()
for row in rows:
if set(row) != {"sku", "qty"} or not isinstance(row["sku"], str):
raise ValueError("Invalid row schema")
if type(row["qty"]) is not int or row["qty"] <= 0:
raise ValueError("Expected positive integer qty")
result[(row["sku"], row["qty"])] += 1
return result
gold, actual = bag(expected), bag(predicted)
correct = sum((gold & actual).values())
missing = gold - actual
extra = actual - gold
precision = correct / sum(actual.values()) if actual else None
recall = correct / sum(gold.values()) if gold else None
assert correct == 2
assert missing == Counter({("B-1", 1): 1})
assert extra == Counter({("A-2", 2): 1, ("C-1", 1): 1})
assert precision == 0.5 and recall == 2 / 3
assert bag(list(reversed(expected))) == gold
assert bag([{"sku": "A-2", "qty": 4}]) != bag(expected[:2])
print({"correct": correct, "precision": precision, "recall": recall,
"missing": list(missing.elements()), "extra": list(extra.elements())})Здесь правильно извлечены две строки. Precision равна 2/4: половина выданных строк допустима. Recall равна 2/3: найдены две из трёх нужных строк. В extra отдельно видны повтор A-2 и выдуманная C-1; в missing — пропущенная B-1. По одному итоговому проценту эти причины не различить.
Не прячьте ошибки агрегацией
Две строки A-2 по 2 штуки и одна A-2 на 4 штуки могут давать одинаковый суммарный остаток, но это разные результаты извлечения. Если бизнесу действительно нужен только итог по артикулу, сначала проверьте перенос строк, а затем выполняйте согласованную агрегацию кодом. Так можно понять, на каком этапе потерялась партия или цена.
Пустой эталон и пустой ответ требуют явного правила отчёта. В коде неопределённая доля обозначена None, чтобы её не принять за нулевое качество. В общей статистике считайте суммы правильных, ожидаемых и выданных строк, а долю полностью совпавших документов показывайте отдельно. Иначе сотня простых накладных может скрыть несколько сложных с большими потерями.
Для дробных количеств заранее выберите точное представление и правила округления. Сравнение float с произвольным допуском способно засчитать неверную дозировку или массу. Для товарного извлечения полезнее строковый decimal с согласованной единицей, чем «почти совпадающее» число без контекста.
Проверьте новые версии на неудобных документах
Включите накладные с одинаковыми строками, несколькими страницами, переносом позиции на следующую страницу и пустыми ячейками. Сохраняйте ID документа и координаты исходной строки, чтобы разбор пропуска занимал минуты. При смене промпта повторите сравнение на тех же исходниках: рост среднего recall не должен сопровождаться ростом лишних строк в документах, которые уходят в учётную систему.
В ручной проверке смотрите не только строки из extra. Ошибка количества часто создаёт одновременно одну пропущенную и одну лишнюю запись с тем же артикулом. Сгруппировав такую пару для диагностики, можно показать «ожидалось 2, получено 3». Итоговый расчёт при этом сохраняйте прежним: диагностическое объединение не должно случайно превратить частично верную строку в полностью правильную.
Подготовьте проверку извлечения перед загрузкой в учёт
Далее понадобится набор документов с эталонами и отчёт, который останавливает выпуск при росте пропусков или лишних строк. В платном курсе LLMOps & Evaluation вы научитесь собирать тестовые наборы, строить контур оценки и quality-гейты для LLM-приложений, версионировать промпты и отслеживать качество после релиза. Это превращает разовую проверку JSON в повторяемый процесс приёмки. Откройте программу платного курса «LLMOps & Evaluation: из песочницы в продакшн».
Продолжить на практике
- LLMOps & Evaluation: из песочницы в продакшн ↗
Практический курс по LLMOps. Соберёте продакшн-контур для LLM: eval-фреймворк (LangSmith/Ragas/OpenAI Evals), quality-гейты в CI/CD, алёрты на дрейф, монитринг скорости и стоимости, управление промпт-версиями и безопасность. Плюс профессиональные блоки: synthetic data для тестов, LLM-based judges, отчётность для релиз-комитета и runbook’и инцидентов.
