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’и инцидентов.