138 lines
7.3 KiB
Markdown
138 lines
7.3 KiB
Markdown
# Классификация застряла на 68/84: диагностика 2026-08-27
|
||
|
||
## Наблюдение
|
||
|
||
На экране классификация остаётся на `68/84`, хотя контейнер продолжает работать.
|
||
Проверка через ВМ:
|
||
|
||
```text
|
||
GET /api/batch-progress?batch=acbdffe5-e3ec-43f4-ab61-84861cac35bd
|
||
{"counts":{"classified":68,"garbage":16},"ok":true,"total":84}
|
||
```
|
||
|
||
Повторный запрос вернул то же значение. Kubernetes показывает pod
|
||
`pythonk8s-574b5cf9b4-z4wxz` в состоянии `Running`, `Ready 1/1`, `RESTARTS 0`.
|
||
Событий Kubernetes нет. На ВМ `contracts.service` активен, `convert_server.py`
|
||
запущен в одном экземпляре.
|
||
|
||
## Причина в коде
|
||
|
||
`site/routes/api_bp.py` возвращает раздельные счётчики `classified`, `failed` и
|
||
`garbage`, а `total` равен числу всех документов батча.
|
||
|
||
`site/static/app.js` в `runClassify()` считает завершённые документы так:
|
||
|
||
```javascript
|
||
var done = (d.counts.classified || 0) + (d.counts.failed || 0);
|
||
```
|
||
|
||
`garbage` в эту сумму не включён. Для текущего батча:
|
||
|
||
```text
|
||
done = 68 classified + 0 failed = 68
|
||
total = 84
|
||
garbage = 16
|
||
фактически завершено = 68 + 0 + 16 = 84
|
||
```
|
||
|
||
Поэтому условие `done >= total` никогда не выполняется, polling не
|
||
останавливается, а кнопка остаётся на `68/84`. Это ошибка фронтендового
|
||
подсчёта прогресса, а не зависание Kubernetes или LLM на 68-м файле.
|
||
|
||
## Исправление
|
||
|
||
В `site/static/app.js` к числу завершённых документов добавлен конечный статус
|
||
`garbage`:
|
||
|
||
```javascript
|
||
var done = (d.counts.classified || 0)
|
||
+ (d.counts.failed || 0)
|
||
+ (d.counts.garbage || 0);
|
||
```
|
||
|
||
Версия приложения повышена с `2.0.17` до `2.0.18`; cache-buster статических
|
||
скриптов обновлён в `site/templates/index.html`.
|
||
|
||
Kubernetes проверялся только командами чтения через ВМ:
|
||
`kubectl get`, `kubectl describe`, `kubectl top`, `kubectl logs`.
|
||
|
||
## Дополнительная проверка загрузки ZIP
|
||
|
||
При проверке загрузки разными способами обнаружен отдельный дефект связи
|
||
файлов с архивом. `expandZipClient()` создаёт для каждого распакованного файла
|
||
поле `zip_source`, а `uploadFile(file, onProgress, zipSource)` умеет передавать
|
||
его в `/api/upload_refs`. Однако вызов `uploadFile()` в цикле `onFilesSelected()`
|
||
передавал только два аргумента. Поэтому backend получал `zip_source=null`, и
|
||
связь с исходным ZIP терялась.
|
||
|
||
Проверены архивы:
|
||
|
||
- `testgen/out/zips/duplicates_inside.zip` — целый ZIP, два файла с одинаковым именем;
|
||
- `testgen/out/zips/mixed_with_error.zip` — целый ZIP, корректный и повреждённый DOCX.
|
||
|
||
Исправление применено в `site/static/files.js`: третьим аргументом передаётся
|
||
`entry.zip_source`. Для обычных файлов значение `null`, поэтому их поведение не
|
||
изменяется. Версия приложения повышена с `2.0.18` до `2.0.19`, cache-buster
|
||
обновлён в `site/templates/index.html`.
|
||
|
||
## Проверки исправления
|
||
|
||
- `node --check site/static/files.js` — успешно;
|
||
- frontend upload-тесты — успешно;
|
||
- layer 2 upload-тесты — успешно;
|
||
- архивы `duplicates_inside.zip` и `mixed_with_error.zip` прошли `unzip -t`.
|
||
|
||
## Реальный end-to-end тест сверки
|
||
|
||
27.08.2026 выполнен тест через deployed API `v2.0.19` на batch
|
||
`0dd0d3a7-3e52-4888-95eb-1b6d3e37f40e`.
|
||
|
||
Загружены через VM-поток два реальных DOCX:
|
||
|
||
- `договор-XXX001-03700.docx` — распарсен, 20 элементов;
|
||
- `допник-1-XXX001-03700.docx` — распарсен, 16 элементов.
|
||
|
||
Классификация завершилась `2/2`, но результат классификации неожиданен:
|
||
|
||
- допсоглашение попало в группу `03700`;
|
||
- базовый договор попал в `__unresolved__` со статусом `garbage` и причиной
|
||
отсутствия matching-договора.
|
||
|
||
Это функциональный дефект/проблема классификации тестовой пары: без базового
|
||
договора группа не проверяет корректное сравнение договора с приложением.
|
||
|
||
Тем не менее реальный pipeline сверки для сформированной группы проверен:
|
||
|
||
- `/api/apply-groups` — `200`, создан contract ID
|
||
`b859e813-bac9-4eb9-82b5-6b5666f2ba4a`;
|
||
- `/process-v2` — SSE завершён событием `complete`;
|
||
- LLM вернул `6` операций в режиме `partial`;
|
||
- применено: `added=6`, `updated=0`, `deleted=0`;
|
||
- ошибок соединения и SSE не было;
|
||
- время pipeline: `9.4с`.
|
||
|
||
Следующий обязательный тест для проверки matching — загрузить базовый договор с
|
||
точным именем/содержимым, которое классификатор ожидает как базовый, и повторить
|
||
ту же пару. Код после этого прогона не изменялся.
|
||
|
||
## Исправление ложного garbage для базового договора
|
||
|
||
При повторной проверке реального файла `договор-XXX001-03700.docx` установлена
|
||
точная причина ошибочной классификации: второй garbage-фильтр искал подстроку
|
||
`УПД` в первых 2000 символах. В договоре эта аббревиатура встречается в
|
||
обычном условии оплаты: «на основании УПД и счета». Из-за этого договор
|
||
получал `garbage=header_keywords`, хотя его заголовок начинается со слова
|
||
«Договор».
|
||
|
||
Исправление в `site/services/classify.py`:
|
||
|
||
- удалено общее substring-срабатывание для `УПД`;
|
||
- добавлено распознавание `УПД` и полного названия только с начала строки,
|
||
когда это заголовок самого документа.
|
||
|
||
Добавлены регрессионные тесты: упоминание УПД внутри договора не является
|
||
garbage, а заголовок документа `УПД № ...` является garbage.
|
||
|
||
Версия приложения повышена до `2.0.20`, cache-buster обновлён в
|
||
`site/templates/index.html`.
|