Przejdź do treści
Agent do dokumentów

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.

Redakcja2 320 słów12 min czytania
2 320słów w tekście
Schemat przepływu z tego tekstu2 000 zdarzeń / mies.
  1. Zdarzenie nowy mail
  2. Klasyfikacja typ, pilność, klient
  3. Narzędzia crm_search
  4. Zatrzymanie odpowiedź do klienta
  5. Działanie mail_send
  6. 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.