Po podniesieniu wersji API ten sam plik przestaje się dzielić. Domyślna wartość splitMode to none
Jeden plik, czternaście dokumentów, jeden rekord na wyjściu. Zakresy stron w pliku zbiorczym: domyślne tryby podziału, limity, dwa progi i rachunek.
- Zdarzenie nowy mail
- Klasyfikacja typ, pilność, klient
- Narzędzia crm_search
- Zatrzymanie odpowiedź do klienta
- Działanie mail_send
- Log wejście, decyzja, wynik
Ten sam kod, ta sama paczka 60 stron, inna liczba wykrytych dokumentów: czternaście przed podniesieniem wersji API i jeden po. Odpowiedź nadal jest poprawna składniowo, kod 200, zero wyjątków w logu. Zmieniła się domyślna wartość jednego parametru. Dokumentacja Azure mówi, że w v4.0 GA (2024-11-30) model klasyfikujący nie dzieli dokumentów domyślnie, domyślną wartością splitMode jest none, a żeby zachować zachowanie z poprzednich wydań, trzeba jawnie ustawić splitMode na auto.
Tego nie zobaczy żadna bramka stojąca na pewności pola, bo to nie jest błąd odczytu. To wyłączony etap przepływu: wyznaczenie zakresów stron, czyli granic dokumentów wewnątrz pliku. Jeden plik nie równa się jeden dokument i żadna warstwa dalej w potoku tego za ciebie nie sprawdzi.
Ta sama rzecz wygląda inaczej u drugiego dostawcy. Agent wysyła 60-stronicowy skan z księgowości, w którym leży czternaście faktur jedna po drugiej, do parsera faktur Google, dostaje odpowiedź i zapisuje jeden rekord. Trzynaście pozostałych nie trafia nigdzie i znowu nie ma błędu, bo usługa zrobiła to, co ma w dokumentacji: Google pisze wprost, że jeśli dokument wejściowy zawiera wiele pasujących formularzy, przetwarzany i rozliczany jest tylko pierwszy z nich.
Agent dostaje plik, a przetwarza dokumenty
Wejście do agenta dokumentowego prawie nigdy nie jest strumieniem pojedynczych dokumentów. Typowe źródła wyglądają tak: skan zbiorczy z jednego przebiegu podajnika, załącznik od kontrahenta z fakturą i trzema załącznikami, eksport z portalu z całym miesiącem w jednym PDF-ie, zdjęcie, na którym leżą dwa paragony. Każde z tych wejść to jeden plik i od dwóch do kilkudziesięciu dokumentów.
Przepływ, który się spina, ma więc trzy etapy, nie dwa, i każdy wymaga osobnego wywołania i osobnego progu:
- Segmentacja – z pliku wychodzi lista zakresów stron z etykietą typu dokumentu. Wejście: jeden plik. Wyjście: N par (typ, zakres stron).
- Ekstrakcja – osobne wywołanie na każdy zakres. Wejście: plik plus zakres stron albo wycięty podplik. Wyjście: pola.
- Uzgodnienie – kontrole deterministyczne na wyniku segmentacji i ekstrakcji razem, bez pytania modelu. To tutaj łapie się zły podział.
Kto pominie etap pierwszy, dostaje potok, który na pliku jednodokumentowym działa bezbłędnie, a na paczce milczy. Rozkład wejścia decyduje, jak często. Przy wejściu, w którym paczki to jedna piąta plików, demo wygląda poprawnie i miesięczne uzgodnienie nie.
Trzy usługi, trzy różne domyślne zachowania na tym samym pliku
Ten sam PDF z czternastoma dokumentami daje trzy różne wyniki w zależności od dostawcy, i w dwóch przypadkach o wyniku decyduje domyślna wartość parametru, której nikt nie ustawiał.
Azure ma trzy tryby podziału: none traktuje cały plik jako jeden dokument i zwraca jedną klasę, perPage klasyfikuje każdą stronę osobno, auto każe usłudze samej znaleźć dokumenty i przypisane im zakresy stron. Konsekwencja dla kodu napisanego pod v3.1 jest taka, jak w akapicie otwierającym: po podniesieniu wersji API ten sam plik przestaje być dzielony, odpowiedź nadal przechodzi walidację schematu, a liczba wykrytych dokumentów spada do jednego. Nic w kodzie nie było zmieniane, więc nic w przeglądzie zmian nie zwróci uwagi.
AWS podchodzi odwrotnie – podział jest w odpowiedzi, niezależnie od tego, czy się o niego prosiło. Struktura wyniku AnalyzeExpense ma pole ExpenseIndex, które w dokumentacji opisane jest jako wskazanie, z której faktury albo którego paragonu w dokumencie pochodzi informacja: pierwszy dokument to 1, drugi 2 i tak dalej. Agent, który czyta tylko pierwszy element tablicy, gubi resztę po własnej stronie, nie po stronie usługi. To ważna różnica, bo ten błąd widać w kodzie i da się go wyłapać w przeglądzie.
Google w parserze faktur nie dzieli i nie zgłasza, że było co dzielić. Podział jest osobnym produktem: custom splitter albo custom classifier, osobne wywołanie, osobna pozycja na rachunku.
Pewność pola nie jest pewnością podziału
Standardowa bramka w agencie dokumentowym stoi na liczbie zwracanej razem z polem: poniżej progu rekord idzie do człowieka, powyżej leci dalej. Ta bramka nie widzi błędu segmentacji, i to jest najdroższa cecha takiego potoku.
Powód jest mechaniczny. Liczba pod polem odpowiada na pytanie, na ile model jest przekonany, że w tym fragmencie to właśnie ta wartość. Pytanie, czy fragment był właściwym fragmentem, zostało rozstrzygnięte wcześniej i gdzie indziej. Jeśli granica wypadła o jedną stronę za daleko, model odczyta kwotę końcową z następnego dokumentu w paczce – poprawnie, pewnie i nie na tym dokumencie. Taki rekord przechodzi bramkę, bo pod każdym polem stoi wysoka liczba.
Dlatego progów jest dwa, nie jeden. Odpowiedź klasyfikatora Azure zwraca tablicę documents, a w każdym elemencie docType, zakres stron w boundingRegions i własne pole confidence – to pewność przypisania dokumentu, nie pól. Dokumentacja dodaje praktyczną uwagę: klasyfikator próbuje przypisać każdy dokument do jednej z wytrenowanych klas, więc jeśli spodziewasz się typów, których nie trenowałeś, trzeba ustawić próg na wyniku klasyfikacji albo dodać klasę other z kilkoma próbkami. Bez tego nieznany typ dostaje etykietę najbliższej znanej klasy i idzie do przetworzenia, które go nie dotyczy.
Limit stron na żądanie przesuwa podział przed ekstrakcję
Niezależnie od poprawności jest twardsze ograniczenie: paczka często nie wejdzie w jedno wywołanie. Limity są publikowane i różnią się o dwa rzędy wielkości.
| Usługa i tryb | Limit stron na żądanie | Limit rozmiaru |
|---|---|---|
| Textract, operacje synchroniczne (PDF, TIFF) | 1 strona | 10 MB |
| Textract, operacje asynchroniczne (PDF, TIFF) | 3 000 stron | 500 MB |
| Document AI, parser faktur online | 15 stron (30 w trybie imageless) | 40 MB |
| Document AI, parser faktur wsadowo | 200 stron | 1 GB |
| Document AI, custom splitter wsadowo | 1 000 stron | 1 GB |
| Azure Document Intelligence, PDF i TIFF | 2 000 stron | 500 MB (S0), 4 MB (F0) |
Liczby dla Document AI pochodzą ze strony limitów Document AI, gdzie jedno żądanie wsadowe przyjmuje do 5 000 plików, a tryb imageless podnosi limit online tylko wtedy, gdy strony są przetwarzane w sposób ciągły od pierwszej. Limity Textract są w tabeli stałych limitów usługi, a limity wejścia Azure w dokumentacji modelu klasyfikującego, razem z informacją, że w warstwie darmowej F0 przetwarzane są tylko dwie pierwsze strony.
Z tej tabeli wynika jedna rzecz, która przesądza architekturę: na Textract synchroniczna ścieżka dla wielostronicowego PDF-a nie istnieje. Limit jednej strony oznacza, że paczkę obsługuje się wyłącznie asynchronicznie, a to już jest kolejka, stan zadania i obsługa powiadomienia o zakończeniu, nie proste wywołanie w pętli. Projekt, który zakładał „sprawdzimy dokument w locie przy wgraniu pliku”, trzeba przepisać, a nie dostroić.
Podział zmienia mianownik na rachunku
Druga konsekwencja jest finansowa i działa w przeciwną stronę niż intuicja. Dostawcy liczą w dwóch różnych jednostkach: za stronę albo za dokument. Podział nie zmienia liczby stron, zmienia liczbę dokumentów – więc w jednym modelu rozliczeń jest obojętny, a w drugim podnosi rachunek.
Cennik Document AI definiuje jednostkę jednoznacznie: jeden count to maksymalnie 10 stron w dokumencie, parser faktur kosztuje 0,10 USD za count, a custom splitter i custom classifier 5,00 USD za 1 000 counts w pierwszym progu. W przykładach z tej samej strony dokument o 1-10 stronach kosztuje 0,10 USD, o 11-20 stronach 0,20 USD, a o 91-100 stronach 1 USD. Dokument jednostronicowy i dziesięciostronicowy rozlicza więc ta sama kwota. Textract liczy inaczej – według cennika Textract AnalyzeExpense to 0,01 USD za stronę do miliona stron miesięcznie i 0,008 USD powyżej, a DetectDocumentText odpowiednio 0,0015 i 0,0006 USD (przykłady na tej stronie są dla regionu US West Oregon, w innych regionach stawki się różnią).
Rachunek na przyjętym modelu wejścia: 300 plików miesięcznie, w nich 4 000 stron i 900 dokumentów, przy czym 210 plików to pojedyncze dokumenty na 1-3 stronach, a 90 plików to paczki po średnio osiem. To założenie, nie pomiar – swoje liczby weź z własnego miesiąca.
| Droga | Jednostka rozliczenia | Koszt miesiąca w modelu | Co wychodzi z paczki czternastu dokumentów |
|---|---|---|---|
| Parser bez segmentacji | count = do 10 stron | ok. 60 USD (ok. 600 counts) | 1 dokument, bez błędu w logu |
| Custom splitter, potem parser na każdy dokument | count za podział plus count za dokument | ok. 3 USD + 90 USD | 14 dokumentów |
| Textract AnalyzeExpense asynchronicznie | strona | ok. 40 USD | 14 pozycji w tablicy ExpenseIndex |
Wniosek z tej arytmetyki jest nieoczywisty: poprawny podział na Document AI podnosi rachunek około półtorakrotnie, bo przestawia rozliczenie ze stron na dokumenty, a dokumenty w tym wejściu są krótkie – 900 jednostronicowych dokumentów to 900 counts, czyli tyle, ile 900 dokumentów dziesięciostronicowych. Tam, gdzie jednostką jest strona, ten sam podział nie kosztuje nic. Jeżeli twoje wejście to dużo krótkich dokumentów w niewielu plikach, model rozliczeń za stronę jest tańszy i różnica rośnie wraz z liczbą dokumentów, nie z liczbą stron. Jednej rzeczy nie da się rozstrzygnąć z dokumentacji: ile dokładnie nalicza się za plik wielodokumentowy wysłany do parsera, bo cennik mówi o rozliczeniu pierwszego pasującego formularza, nie o stronach całego pliku. To sprawdza się na własnym rachunku za pierwszy miesiąc, nie szacuje.
Kontrole, które łapią zły podział bez pytania modelu
Segmentację da się sprawdzić deterministycznie i jest to najtańsza bramka w całym potoku, bo nie wymaga ani jednego wywołania modelu. Lista do uruchomienia na wyniku segmentacji, przed ekstrakcją albo zaraz po niej:
- Pokrycie stron. Suma długości zakresów równa się liczbie stron pliku, zakresy nie nakładają się i nie mają dziur. Dziura to zgubiony dokument, nakładka to jedna strona przypisana dwa razy.
- Unikalność numeru dokumentu w pliku. Ten sam numer w dwóch zakresach oznacza, że podział przeciął jeden dokument na pół. Ten sam sygnał, odwrotnie odczytany, wyłapuje dublet – a dublet w kolejce zadań ma dokładnie tę samą mechanikę co podwójne wykonanie zlecenia w warsztacie, gdzie stan zadania potwierdza się u wykonawcy, a nie u nadawcy.
- Własna deklaracja dokumentu. Stopka „strona 1 z 3″ wobec długości przypisanego zakresu. Rozbieżność to sygnał bez udziału modelu.
- Arytmetyka na dokumencie. Suma pozycji wobec kwoty końcowej w obrębie jednego zakresu. Jeśli pozycje pochodzą z dwóch sklejonych dokumentów, suma się nie zgadza.
- Stałość identyfikatora wystawcy. Dwa różne NIP-y w jednym zakresie to prawie zawsze granica postawiona o stronę za daleko.
- Zgrubny licznik wzorca. Liczba wystąpień frazy otwierającej dokument w warstwie tekstowej OCR wobec liczby wykrytych zakresów. Rozbieżność jest sygnałem do przeglądu, nie wynikiem – ale wyłapuje przypadek, w którym podział w ogóle się nie wykonał.
Każdy wyekstrahowany rekord zapisuj razem z zakresem stron, z którego powstał, i identyfikatorem pliku źródłowego. Bez tego poprawka granicy wymaga ponownego przetworzenia całego pliku i ręcznego odszukania, które rekordy unieważnić.
Gdzie postawić człowieka i jak routować wyjątki
Punkt zatwierdzenia na podziale to inne zadanie niż punkt zatwierdzenia na polach i nie da się ich obsłużyć w jednym widoku. Zatwierdzający podział patrzy na miniatury stron z zaznaczonymi granicami i rozstrzyga, gdzie kończy się dokument – nie musi znać treści. Zatwierdzający pole musi przeczytać dokument i porównać wartość ze źródłem. Pierwsza kolejka jest szybka i pozwala obsłużyć dużo pozycji, druga nie.
Wyjątki warto routować według przyczyny, bo koszt naprawy jest różny:
- Zły podział – poprawka granic i ponowna ekstrakcja na nowym zakresie. Odwracalne, tanie, nie wymaga przepisywania danych ręcznie.
- Zły odczyt pola przy poprawnym podziale – ręczne wpisanie wartości. Odwracalne, ale kosztuje czas na dokument.
- Typ poza zbiorem klas – dokument wychodzi z przepływu, nie dostaje etykiety na siłę. To jest ta sytuacja, dla której istnieje klasa other.
Trening klasyfikatora ustawia granice tego, co w ogóle można rozróżnić. Azure wymaga co najmniej dwóch odrębnych klas i minimum pięciu próbek na klasę, dopuszcza do 1 000 klas i do 100 próbek na klasę, a zbiór treningowy klasyfikatora może mieć do 2 GB i 25 000 stron. Pięć próbek to minimum techniczne, nie zalecenie – dokumentacja dodaje, że przy klasach podobnych do siebie trafność rośnie wraz z liczbą próbek.
Zmierz rozkład dokumentów w pliku, zanim dołożysz splitter
Decyzja do podjęcia nie brzmi „czy dodać segmentację”, bo odpowiedź zależy od wejścia i da się ją policzyć w pół dnia. Weź pliki z ostatniego miesiąca i policz rozkład liczby dokumentów na plik – choćby zgrubnie, licząc wystąpienia wzorca otwierającego dokument w warstwie tekstowej.
Jeśli 95 procent plików to jeden dokument, osobny splitter na całym strumieniu jest kosztem bez pokrycia. Postaw tani detektor – liczba wystąpień wzorca większa od jednego albo liczba stron powyżej progu – i kieruj do segmentacji tylko te pliki, które się zapalą. Jeśli paczki to norma, segmentacja jest pierwszym etapem potoku, a budżetować trzeba liczbę dokumentów, nie liczbę stron, i wtedy model rozliczeń za stronę wypada taniej od rozliczenia za dokument.
W obu przypadkach jedna rzecz jest wspólna i nie zależy od dostawcy: dopóki w rekordzie wyjściowym nie ma zakresu stron, z którego powstał, nie wiesz, czy agent przeczytał czternaście dokumentów, czy jeden i trzynaście razy nic.