25 KiB
Ответ Опуса — 30 прицельных тестовых кейсов
Ответ на History/opus-testcases-request.md от 25.06.2026.
Опус изучил реальный код пайплайна (classify → group → compare) и дал 30 кейсов, заточенных под конкретные уязвимости реализации.
Моя оценка
Сильные стороны
-
Опус реально читал код. Он нашёл
_smart_extract(первые 1500 символов + regex-маркеры),normalize_number(диапазонА-Яне включаетЁ),new_valuesсодержит ТОЛЬКО изменённые поля. Это не общие рекомендации — это точечные удары по слабым местам. -
Кейсы 13-14 — золото. Кириллическая
Оvs ноль0, латинскаяCvs кириллическаяС— это реально ломает группировку. Опус предлагает их как «баг-детекторы»: не исправлять, а задокументировать текущее поведение и ждать fuzzy-нормализации. -
Кейс 15 (Ё) — я даже не знал про диапазон
А-Я. Опус нашёл. -
Приоритет: Блок B (group) → Блок C (compare) → Блок A (classify). Правильно — group ломается детерминированно без LLM, баги воспроизводимы.
-
Кейс 9 (слепая зона) — 1500 символов выжимки. Практически важный кейс, может объяснить почему некоторые файлы не классифицируются.
Что можно добавить
-
Кейс на batch-progress при падении воркера: если
classify_workerупал на середине, progress застревает на N/69 и никогда не достигнет total. Фронт висит вечно. -
Кейс на два classify подряд с одним batch_id: наш lock-файл должен вернуть 409. Стоит проверить что второй запрос действительно отклоняется.
Итого
30 кейсов покрывают все три шага пайплайна. ~40% кейсов — group (самый хрупкий), ~30% — compare (LLM-зависимый), ~30% — classify. Можно брать в реализацию. Я генерирую docx по этим шаблонам.
Исходный ответ Опуса
Далее — полный текст ответа Опуса без сокращений.
Кейс 1: Эталонный договор (baseline)
Что проверяем: базовое извлечение всех 6 полей из чистого договора. Почему может сломаться: если падает даже это — проблема не в данных, а в промпте/парсинге. Файлы: договор-XXX001-03700.docx Ключевой текст договора: "Договор № XXX001-03700 на оказание технологических услуг от 15 марта 2025 г. ООО «Облако-Сервис» (Исполнитель)…" Ожидаем: doc_type=contract, own_number="XXX001-03700", parent_number=null, doc_date="2025-03-15", counterparty="ООО «Облако-Сервис»", confidence=ok
Кейс 2: Номер кириллицей
Что проверяем: own_number с кириллическим префиксом и слешем. Почему может сломаться: LLM может «перевести» кириллицу в латиницу или отбросить год после слеша. Файлы: договор-МЭС.docx Ключевой текст договора: "Договор № МЭС-123/2024 от 10.01.2024 г." Ожидаем: own_number="МЭС-123/2024" дословно (важно для парного group-кейса 12).
Кейс 3: Нестандартный заголовок (не слово «Договор»)
Что проверяем: определение doc_type=contract, когда документ называется иначе. Почему может сломаться: LLM привязывается к слову «Договор»; «Соглашение об оказании услуг» может уехать в other. Файлы: договор-нестандарт.docx Ключевой текст договора: "СОГЛАШЕНИЕ об оказании услуг связи № SVC-77 от 01.02.2025" Ожидаем: doc_type=contract (а не other).
Кейс 4: Допник с явным родителем
Что проверяем: разделение own_number и parent_number. Почему может сломаться: LLM путает «свой» номер ДС и номер базового договора местами. Файлы: допник-1-XXX003-01300_2.docx Ключевой текст договора: "Дополнительное соглашение № 1 к Договору № XXX003-01300 от 05.06.2024" Ожидаем: doc_type=supplement, own_number="1", parent_number="XXX003-01300".
Кейс 5: Спецификация как отдельный файл
Что проверяем: doc_type=specification и привязка parent_number. Почему может сломаться: спека без слова «договор» в шапке → other; parent потеряется. Файлы: спецификация-XXX001-03700.docx Ключевой текст договора: "Спецификация № 1 к Договору № XXX001-03700" Таблица спеки: № / Наименование / Цена / Объём / Сумма / Дата. Ожидаем: doc_type=specification, parent_number="XXX001-03700".
Кейс 6: Договор БЕЗ контрагента в шапке
Что проверяем: поведение, когда counterparty не извлекается. Почему может сломаться: LLM «галлюцинирует» контрагента или ставит реквизиты вместо названия. Файлы: договор-без-стороны.docx Ключевой текст договора: "Договор № NC-09 от 03.03.2025 на оказание услуг" (стороны — только в конце документа, см. кейс 9). Ожидаем: counterparty=null/"" , confidence=low (а не выдуманное ООО).
Кейс 7: Дата прописью и в нестандартном формате
Что проверяем: нормализацию doc_date → YYYY-MM-DD. Почему может сломаться: «пятнадцатое марта две тысячи двадцать пятого года» или «15.03.25» (двузначный год). Файлы: договор-дата-прописью.docx Ключевой текст договора: "Договор № DT-15 от «пятнадцатого» марта 2025 года" Ожидаем: doc_date="2025-03-15".
Кейс 8: Несколько дат в шапке (дата vs срок действия)
Что проверяем: выбор ПРАВИЛЬНОЙ даты (дата заключения, а не «действует до»). Почему может сломаться: LLM хватает первую попавшуюся дату. Файлы: договор-две-даты.docx Ключевой текст договора: "Договор № TD-21 от 01.04.2025, действует до 31.12.2026" Ожидаем: doc_date="2025-04-01".
Кейс 9: Реквизиты за пределами первых 1500 символов
Что проверяем: «слепую зону» _smart_extract. Почему может сломаться: номер/контрагент стоят после длинной преамбулы (>1500 симв.) и далеко от regex-маркеров → в выжимку не попадут. Файлы: договор-длинная-преамбула.docx Ключевой текст договора: первые 2 страницы — общие положения без слова «№»; и только потом "Договор № LATE-99 … ООО «Поздний Контрагент»". Ожидаем: документ должен классифицироваться (маркер №/договор рядом с данными). Если падает — это сигнал расширить окно выжимки.
Кейс 10: «Шумный» документ — несколько номеров на странице
Что проверяем: выбор own_number среди нескольких «№». Почему может сломаться: в шапке есть «Исх. № 456», «Лиц. № 789» и сам «Договор № MN-01» → LLM берёт чужой номер. Файлы: договор-много-номеров.docx Ключевой текст договора: "Исх. № 456 от 12.05.2025 … Лицензия № 789 … ДОГОВОР № MN-01 от 12.05.2025" Ожидаем: own_number="MN-01".
Кейс 11: doc_type=other (мусорный файл)
Что проверяем: что не-договор уходит в other, а не натягивается на contract. Почему может сломаться: LLM «обязательно» хочет найти договор. Файлы: акт-сверки.docx Ключевой текст договора: "Акт сверки взаимных расчётов за 1 квартал 2025" Ожидаем: doc_type=other, confidence=low.
Кейс 12: Разделители — нормализуются (позитив)
Что проверяем: «МЭС-123/2024» и «МЭС 123/2024» → одна группа. Почему может сломаться: baseline нормализации; обе дают МЭС1232024. Файлы: договор-МЭС.docx (own="МЭС-123/2024") + допник-МЭС.docx (parent="МЭС 123/2024") Ожидаем: документы в ОДНОЙ группе.
Кейс 13: Кириллическая «О» против нуля «0» (классическая опечатка)
Что проверяем: «O3700» с кириллической О против «03700» с нулём. Почему может сломаться: код НЕ приравнивает кириллицу к цифрам → О3700 ≠ 03700 → допник осиротеет в unresolved. Файлы: договор.docx (own="XXX001-03700", цифра ноль) + допник.docx (parent="XXX001-О3700", кириллическая О) Ожидаем (как баг-детектор): сейчас попадут в РАЗНЫЕ группы. Кейс фиксирует поведение и проверяет, появится ли fuzzy-нормализация.
Кейс 14: Латинская «C» против кириллической «С»
Что проверяем: визуально одинаковые префиксы из разных алфавитов. Почему может сломаться: normalize_number сохраняет оба алфавита → CBC-10 (лат) ≠ СВС-10 (кир). Файлы: договор.docx (own="CBC-10", латиница) + допник.docx (parent="СВС-10", кириллица) Ожидаем (баг-детектор): разные группы. Маркер необходимости юникод-конфьюзабл нормализации.
Кейс 15: Буква «Ё» в номере
Что проверяем: диапазон А-Я не включает Ё. Почему может сломаться: normalize_number("ЁЖ-5")="Ж5" — буква Ё выпадает. Если в одном документе «ЁЖ-5», в другом «ЖЕ-5» — рассинхрон. Файлы: договор.docx (own="ЁЖ-5") + допник.docx (parent="ЁЖ-5") Ожидаем: оба теряют Ё одинаково → совпадут как Ж5 (позитив, но по «неправильной» причине — кейс это документирует).
Кейс 16: parent_number отсутствует у допника
Что проверяем: ветку «осиротевших» документов. Почему может сломаться: допник без parent и без own-номера уходит в unresolved. Файлы: допник-без-родителя.docx Ключевой текст договора: "Дополнительное соглашение к договору оказания услуг" (без номеров вообще) Ожидаем: документ в группе unresolved, не приклеен к случайному договору.
Кейс 17: Допник ссылается на own_number, а не parent
Что проверяем: ветку матчинга «parent==c_norm ИЛИ own==c_norm». Почему может сломаться: если LLM записал номер базового договора в own_number допника (а parent=null), группировка всё равно должна склеить. Файлы: договор.docx (own="GR-50") + допник.docx (own="GR-50", parent=null) Ожидаем: одна группа (срабатывает ветка own==own).
Кейс 18: Два РАЗНЫХ договора с одинаковым нормализованным номером
Что проверяем: коллизию якорей групп. Почему может сломаться: «AB-12» и «A-B12» → оба AB12; допник приклеится не к тому/к обоим. Файлы: договор-A.docx (own="AB-12") + договор-B.docx (own="A-B12") + допник.docx (parent="AB12") Ожидаем: видно недетерминированность/двойную привязку — кейс ловит коллизии нормализации.
Кейс 19: Семья из 4 документов, разный порядок дат
Что проверяем: сортировку внутри группы по doc_date и метку initial/additional. Почему может сломаться: если даты парсятся криво, «initial» может стать не самый ранний документ. Файлы: договор(2025-01-10) + допник-2(2025-05-01) + спека(2025-02-01) + допник-1(2025-03-01) Ожидаем: порядок initial=договор, далее по возрастанию даты; type первого = initial.
Кейс 20: Допник с лишним суффиксом-копией в имени файла
Что проверяем: что нормализуется НОМЕР, а не имя файла. Почему может сломаться: имя «допник-1-XXX003-01300_2.docx» содержит _2 (копия), это не должно влиять на own_number/parent. Файлы: допник-1-XXX003-01300_2.docx Ключевой текст договора: "Дополнительное соглашение № 1 к Договору № XXX003-01300" Ожидаем: own_number="1", parent_number="XXX003-01300"; _2 игнорируется.
Кейс 21: Цена изменилась на 1 копейку
Что проверяем: чувствительность UPDATE к микроизменению. Почему может сломаться: LLM сочтёт разницу «несущественной» и не выдаст UPDATE; или округлит. Файлы: спека-v1.docx + допник-цена.docx Таблица спеки (current): Аренда стойко-места | 50000.00 | 1 | 50000.00 | 2025-01-01 Текст допника: "С 01.03.2025 стоимость аренды устанавливается 50 000,01 руб." Ожидаем: UPDATE r1 new_values={price:50000.01, sum:50000.01, date_start:"2025-03-01"}.
Кейс 22: Объём с 3 на 0 — это UPDATE или DELETE?
Что проверяем: трактовку «количество стало нулём». Почему может сломаться: граница UPDATE(qty=0) vs DELETE; разные модели решают по-разному. Файлы: спека-v1.docx + допник-обнуление.docx Таблица спеки (current): IP-адрес IPv4 | 300 | 3 | 900 | 2025-01-01 Текст допника: "С 01.04.2025 услуга предоставления IP-адресов исключается (количество — 0)." Ожидаем (фиксируем решение): один из {DELETE r1} ИЛИ {UPDATE r1 qty=0,sum=0}. Кейс закрепляет ожидаемую трактовку и ловит непостоянство.
Кейс 23: Услуга переименована, суть та же
Что проверяем: семантический матч UPDATE по смыслу, а не по символам. Почему может сломаться: LLM не свяжет «Аренда стойко-места» и «Размещение оборудования в стойке» → выдаст ADD+DELETE вместо UPDATE. Файлы: спека-v1.docx + допник-переименование.docx Таблица спеки (current): Аренда стойко-места | 50000 | 1 | 50000 | 2025-01-01 Текст допника: "Услугу «Размещение оборудования в стойке» с 01.05.2025 — 52 000 руб." Ожидаем: UPDATE r1 (а не ADD новой + DELETE старой).
Кейс 24: Полная замена приложения (full_replace)
Что проверяем: триггер mode=full_replace по фразе «изложить в следующей редакции». Почему может сломаться: LLM попытается diff'ить построчно (partial) вместо того, чтобы выдать все строки как ADD. Файлы: спека-v1.docx + допник-новая-редакция.docx Таблица спеки (current): r1 Аренда | 50000 | 1 | 50000; r2 IP | 300 | 8 | 2400 Текст допника: "Приложение № 1 изложить в следующей редакции:" + новая таблица (Аренда 55000; IP 12 шт; +Резервное копирование 4000). Ожидаем: mode=full_replace, ВСЕ строки новой редакции как ADD, без UPDATE/DELETE.
Кейс 25: Добавление новой услуги (чистый ADD)
Что проверяем: распознавание строки, которой не было. Почему может сломаться: LLM попробует «прицепить» к похожей существующей через UPDATE. Файлы: спека-v1.docx + допник-добавление.docx Таблица спеки (current): Аренда | 50000 | 1 | 50000 Текст допника: "С 01.06.2025 добавить услугу «Резервное копирование 1 ТБ» — 4 000 руб./мес., 1 шт." Ожидаем: ADD new_row={name:"Резервное копирование 1 ТБ", price:4000, qty:1, sum:4000, date_start:"2025-06-01"}.
Кейс 26: Удаление услуги (чистый DELETE)
Что проверяем: корректный target_id при удалении. Почему может сломаться: LLM удалит не ту строку (перепутает r1/r2) или выдаст UNRESOLVED. Файлы: спека-v1.docx + допник-удаление.docx Таблица спеки (current): r1 Аренда | 50000 | 1 | 50000; r2 Мониторинг | 2000 | 1 | 2000 Текст допника: "С 01.07.2025 услуга «Мониторинг 24/7» исключается из спецификации." Ожидаем: DELETE r2 (именно r2).
Кейс 27: Изменение только суммы при тех же цене×объёме (ловушка консистентности)
Что проверяем: что LLM не «досчитывает» поля, которых нет в допнике. Почему может сломаться: допник меняет только qty, а LLM забывает пересчитать sum (или наоборот, лезет в price). Файлы: спека-v1.docx + допник-объём.docx Таблица спеки (current): IP-адрес | 300 | 8 | 2400 | 2025-01-01 Текст допника: "Увеличить количество IP-адресов до 12 (с 01.08.2025)." Ожидаем: UPDATE r1 new_values={qty:12, sum:3600, date_start:"2025-08-01"} — price НЕ в new_values.
Кейс 28: Допник меняет услугу, которой нет в спеке (UNRESOLVED)
Что проверяем: ветку UNRESOLVED. Почему может сломаться: LLM «придумает» ADD вместо честного UNRESOLVED. Файлы: спека-v1.docx + допник-призрак.docx Таблица спеки (current): Аренда | 50000 | 1 | 50000 Текст допника: "Стоимость услуги «Услуга миграции данных» снизить на 10%." (такой услуги в спеке нет) Ожидаем: UNRESOLVED reason="услуги нет в текущей спецификации".
Кейс 29: Числа с пробелами-разделителями и запятой-десятичной
Что проверяем: парсинг «55 000,00» → 55000.0. Почему может сломаться: LLM вернёт строку «55 000,00» или 55.0 (обрежет по запятой). Файлы: спека-v1.docx + допник-формат-чисел.docx Таблица спеки (current): Аренда | 50000 | 1 | 50000 Текст допника: "Цена аренды с 01.09.2025 — 55 000,00 руб." Ожидаем: UPDATE r1 price=55000.0 (число, не строка).
Кейс 30: Болтливый LLM-ответ / JSON в markdown (робастность парсера)
Что проверяем: устойчивость парсинга на стороне Python (compare и classify).
Почему может сломаться: ответ обёрнут в json , есть текст «Вот результат:», висячая запятая.
Файлы: любой простой допник (UPDATE одной цены) — суть в форме ответа, не в данных.
Текст допника: "Цена аренды — 51 000 руб. с 01.10.2025."
Ожидаем: парсер извлекает JSON из markdown-блока и применяет UPDATE r1.
Матрица покрытия
| Аспект | Кейсы |
|---|---|
| classify: все поля / baseline | 1, 5 |
| classify: тип документа (contract/spec/other) | 3, 5, 11 |
| classify: own vs parent | 4, 17 |
| classify: дата | 7, 8 |
| classify: контрагент | 1, 6 |
| classify: слепая зона выжимки | 9, 10 |
| group: нормализация разделителей | 12, 15 |
| group: кириллица/латиница/цифры | 13, 14, 15 |
| group: сироты / unresolved | 16 |
| group: ветки матча и коллизии | 17, 18 |
| group: сортировка/порядок | 19, 20 |
| compare: UPDATE | 21, 23, 27, 29 |
| compare: ADD / DELETE | 22, 25, 26 |
| compare: full_replace | 24 |
| compare: UNRESOLVED | 22, 28 |
| robustness: JSON-парсинг | 30 |
Рекомендация по приоритету: сначала Блок B (кейсы 13–18) — там код ломается детерминированно и без LLM, баги воспроизводимы на 100%. Потом Блок C (LLM-логика), затем Блок A.