ChatGPT i Google działają bez konta. Do Claude skopiujemy polecenie (wymaga konta).
Większość projektów RPA zaczyna się od proof of concept, który działa świetnie na 20 testowych rekordach i demo dla biznesu. Problem pojawia się mniej więcej w trzecim tygodniu po wdrożeniu produkcyjnym, kiedy workflow pada na 347. rekordzie z 1000, a jedyną opcją jest restart całego procesu od początku. Albo – co gorsze – nikt nie zauważa, że 347 rekordów cicho się nie przetworzył, bo system nie miał porządnego mechanizmu raportowania partial failures.
W projekcie automatyzacji analizy ksiąg wieczystych dla firmy asset management te problemy uderzyły szczególnie mocno. System miał pobierać dane z rejestrów publicznych, parsować dokumenty prawne, wyciągać structured data przez NLP i klasyfikować potencjał inwestycyjny nieruchomości. Brzmi prosto na papierze. W praktyce każdy z tych kroków ma dziesiątki failure modes, a monolityczny workflow oznaczał, że błąd w parsowaniu dokumentu 500/1000 kasował godzinę pracy i wymuszał restart.
Monolityczny workflow jako single point of failure
Typowe podejście do RPA to jeden duży workflow: pobierz listę → dla każdego rekordu: pobierz szczegóły → parsuj dokument → wyciągnij dane → waliduj → zapisz → generuj raport. Projektując taki sekwencyjny flow zakładasz, że wszystko będzie działało idealnie. W rzeczywistości rejestry publiczne mają timeouty, captcha pojawia się losowo, struktura HTML zmienia się bez ostrzeżenia, a parsery PDF-ów łamią się na skanowanych dokumentach z lat 90.
Problem nie leży w tym, że te błędy występują – one zawsze będą występować. Problem leży w tym, że jeden timeout w kroku 7/10 zatrzymuje cały pipeline i wymaga ponownego przetworzenia kroków 1-6, które już się udały. W systemie księgowym to oznacza, że ten sam wpis zostanie ponownie wyciągnięty, ponownie sparsowany i potencjalnie zduplikowany w bazie wyników, jeśli nie było porządnej logiki deduplication.
Najgorsze jest to, że takie workflows mają bardzo długi MTTR. Błąd występuje w nocy podczas batch processingu, rano zespół widzi, że z 2000 planowanych rekordów przetworzył się tylko 346, ale nie wiadomo dlaczego. Logi pokazują generic exception bez kontekstu. Trzeba ręcznie przejrzeć stan każdego rekordu, znaleźć ten problematyczny, naprawić źródło błędu, i uruchomić cały batch ponownie – z ryzykiem, że tym razem padnie na innym rekordzie.
Fundamentalny problem architektury to brak izolacji między krokami. Jeśli fetch, parse i validation są jednym monolitem, to nie ma możliwości częściowego sukcesu. Albo wszystko się udało, albo nic. W distributed systemsach dawno nauczyliśmy się, że partial success to norma, nie wyjątek – ale w świecie RPA ten pattern jakoś nie przebił się do mainstream approaches.
Rozbicie na mikro-workflows z persystencją stanu
Pierwsza rzecz, którą trzeba zrobić, to rozbić monolityczny workflow na niezależne kroki z jawną persystencją stanu między nimi. Zamiast „pobierz i przetwórz wszystko”, masz: workflow A pobiera listę nieruchomości i zapisuje identyfikatory do kolejki, workflow B konsumuje kolejkę i pobiera szczegóły każdej nieruchomości zapisując raw data, workflow C parsuje zapisane dokumenty i wyciąga structured fields, workflow D waliduje i klasyfikuje.
Kluczowe pytanie to granularność. Jak małe mają być te kroki? Zbyt małe – masz operational overhead z tysiącami mini-tasków i kosztowną orchestrację. Zbyt duże – wracasz do problemu single point of failure. Praktyczna heurystyka: krok powinien być atomową jednostką, która albo w całości się udała, albo w całości failuje, i której retry nie ma side effects. Dla systemu ksiąg wieczystych naturalnym podziałem okazało się: fetch listing → fetch details per property → parse document → extract entities → validate schema → classify investment potential.
Każdy krok zapisuje swój output do storage z explicit linking do input. Rekord w bazie nie jest po prostu „property X”, tylko „property X, fetched at timestamp T1, parsed at T2 from document version hash H, extracted fields validated at T3”. To daje pełny lineage tracking i możliwość reprocessingu tylko części pipeline bez utraty kontekstu.
Persystencja stanu między krokami wymaga decyzji technologicznej: gdzie trzymać ten stan? Opcje to relacyjna baza, object storage, queue attributes, albo dedykowany workflow engine z built-in state management. W praktyce wybraliśmy kombinację: lightweight metadata w PostgreSQL (status, timestamps, error info), heavy payloads (raw dokumenty, parsed JSON) w S3, queue w AWS SQS do koordinacji. Próba trzymania wszystkiego w bazie szybko prowadzi do problemów z rozmiarem – pojedynczy dokument PDF to często kilka MB, a przy tysiącach rekordów storage costs rosną szybko.
Trade-off tego podejścia to złożoność orchestracji. Zamiast jednego procesu masz teraz kilka loosely coupled services, które muszą koordynować przez queues. Debugging staje się trudniejszy – nie wystarczy spojrzeć na logi jednego procesu, musisz śledzić flow przez wiele komponentów. Ale alternatywa – monolityczny workflow, który pada na każdym edge case – jest znacznie gorsza w praktyce.
Idempotentność jako fundament bezpiecznego retry
Najbardziej krytyczna właściwość każdego kroku w workflow to idempotentność: możliwość wielokrotnego wykonania tej samej operacji bez zmiany end result poza pierwszym wywołaniem. W kontekście RPA oznacza to, że jeśli krok „parsuj dokument dla nieruchomości X” wykona się dwa razy (bo retry po timeout), nie skończy się duplikacją danych ani corruption istniejących recordów.
W systemie księgowym naturalnym kluczem jest numer księgi wieczystej. Operacja „extract hipoteki z KW nr 123” powinna działać jako upsert: jeśli dane dla tej KW już istnieją, nadpisz je nową wersją. Jeśli nie ma – wstaw. Problem pojawia się, kiedy operation ma side effects, które nie są idempotent by design: wysłanie notyfikacji email, inkrementacja licznika, append do audit logu.
Praktyczne podejście to rozróżnienie między business logic (idempotent) a side effects (explicit deduplication). Business logic działa na natural keys z upsert semantyką. Side effects mają explicit check: „czy ten event już został wyemitowany?”. W PostgreSQL to może być unique constraint na (property_id, event_type, timestamp). Próba drugiego inserta tego samego eventu failuje, ale gracefully – aplikacja wie, że to duplicate i kontynuuje.
def process_property_document(property_id, document_hash):
# Check if already processed
existing = db.query(
"SELECT status FROM processed_documents
WHERE property_id = %s AND document_hash = %s",
(property_id, document_hash)
).first()
if existing and existing.status == 'completed':
return existing # Already done, idempotent exit
# Do heavy work
parsed_data = parse_document(property_id)
# Upsert result
db.execute(
"INSERT INTO processed_documents (property_id, document_hash, data, status)
VALUES (%s, %s, %s, 'completed')
ON CONFLICT (property_id, document_hash)
DO UPDATE SET data = EXCLUDED.data, updated_at = NOW()",
(property_id, document_hash, parsed_data)
)
Edge case, który łamie idempotency: concurrent processing tego samego rekordu. Jeśli dwa workery jednocześnie ściągną ten sam task z kolejki (przez visibility timeout issues lub manual reprocessing), mogą oba próbować parsować i zapisywać wyniki. W najlepszym przypadku ostatni zapis wygrywa i masz zduplikowaną pracę. W gorszym – jeden process przeczyta częściowo zapisane dane drugiego i skorupcji state. Ochrona to albo pessimistic locking (SELECT FOR UPDATE przed rozpoczęciem pracy), albo optimistic locking (version field, update tylko jeśli version się zgadza). W distributed queue systems często używasz lease mechanism – task jest „locked” na czas przetwarzania przez konkretnego workera.
"Retry jest bezpieczny dokładnie do chwili, gdy visibility timeout okaże się krótszy niż parsowanie najgorszego skanu z lat 90. Od tego momentu ten sam rekord ma dwóch bardzo pracowitych właścicieli."
Checkpointing strategy w długich procesach
Niektóre kroki w workflow są inherently długotrwałe. Parsowanie dokumentu PDF z 50 stronami i OCR na złej jakości skanach może trwać minuty. NLP extraction z fine-tuned modelu na złożonym tekście prawniczym – podobnie. Jeśli taki krok failuje w połowie, naturalnym pytaniem jest: czy można wznowić od miejsca przerwania, czy trzeba zaczynać od zera?
Dla dokumentów księgowych, które często składają się z wielu sekcji (dane właściciela, hipoteki, służebności, wpisy historyczne), sensownym podejściem było wprowadzenie checkpointingu per-sekcja. Parser nie zapisuje wyniku tylko na końcu całego dokumentu, ale po przetworzeniu każdej sekcji. Jeśli crash następuje podczas parsowania sekcji 3/5, wznowienie zaczyna się od sekcji 3, nie od 1.
Implementacja checkpointingu wymaga decyzji: jak często zapisywać state? Za często – overhead I/O może spowolnić processing bardziej niż pomaga. Za rzadko – niewiele korzyści z mechanizmu. Praktyczna heurystyka: checkpoint po każdym „drogim” kroku (heavy computation, external API call, znaczący postęp w pracy). Dla batch processing tysiąca rekordów rozsądnym punktem był checkpoint co 100 rekordów – wystarczająco często, żeby retry nie był bardzo kosztowny, wystarczająco rzadko, żeby nie zabijać performance.
Storage dla checkpoints to często ta sama baza co metadata workflow, ale z explicit TTL. Nie potrzebujesz checkpointów starszych niż kilka dni – jeśli workflow nie wznowił się w rozsądnym czasie, prawdopodobnie trzeba go uruchomić od nowa anyway. Garbage collection starych checkpoints jest konieczny, bo inaczej storage costs rosną w nieskończoność. Prosty cron job usuwający checkpoints starsze niż 7 dni zazwyczaj wystarcza.
Resume logic po crash wygląda tak: przy starcie workflow sprawdza, czy istnieje checkpoint dla danego property_id. Jeśli tak – ładuje zapisany state i kontynuuje od zapisanego kroku. Jeśli nie – zaczyna od początku. Kluczowe jest, żeby ten check był szybki – jeśli każdy workflow start musi skanować całą tabelę checkpoints, to sam mechanizm staje się bottleneckiem. Index na (property_id, created_at DESC) rozwiązuje problem.
W projekcie ksiąg wieczystych checkpointing najbardziej pomógł w obszarze NLP extraction. Modele językowe są niestabilne w sensie: czasem timeout, czasem OOM na szczególnie długich dokumentach. Bez checkpointingu każdy taki failure wymagał reprocessingu całego dokumentu od początku, co mogło oznaczać stratę 5-10 minut pracy per rekord. Z checkpointingiem per-sekcja maksymalna strata to ~1 minuta. Przy tysiącach dokumentów dziennie różnica jest znacząca.
Dead letter queue dla failed records
Nawet z retry logic i checkpointingiem niektóre rekordy po prostu nie chcą się przetworzyć. Dokument jest tak skorumpowany, że żaden parser nie da rady. OCR zwraca kompletny garbage. Struktura HTML rejestru publicznego zmieniła się i extractor nie rozpoznaje już pól. Albo – co najgorsze – system źródłowy zwraca 500 dla konkretnego property_id i nie wiadomo dlaczego.
Typowym błędem jest nieskończony retry loop. Rekord failuje, wraca do kolejki, failuje znowu, wraca, failuje. W rezultacie ten jeden broken record blokuje processing queue, bo workery ciągle próbują go przetworzyć zamiast przejść do następnych. Drugi typowy błąd to ciche pominięcie – rekord failuje, system loguje błąd i przechodzi dalej. Nikt nie wie, że 5% danych się nie przetworzyło dopóki ktoś ręcznie nie sprawdzi completeness.
Dead letter queue to wzorzec z messaging systems: po N nieudanych próbach przetworzenia message jest przenoszony do specjalnej kolejki dla failed messages. To usuwa go z głównego flow, ale nie kasuje – jest dostępny do manual review i eventual reprocessing. W kontekście workflow RPA DLQ przechowuje rekordy, które przekroczyły retry limit.
Kluczowe pytanie: kiedy przenieść do DLQ? Za wcześnie (np. po 1 retry) – będziesz mieć false positives, gdzie transient network issue mógłby się rozwiązać przy kolejnej próbie. Za późno (np. 100 retries) – marnujesz compute i opóźniasz discovery problemu. Praktyczna wartość to 3-5 retries z exponential backoff. Dla retriable errors (timeout, 503) możesz być bardziej agresywny. Dla non-retriable errors (400 validation error, malformed document) pierwsza próba może od razu iść do DLQ.
def handle_processing_failure(property_id, error, attempt_count):
if is_retriable_error(error) and attempt_count < MAX_RETRIES:
backoff_seconds = (2 ** attempt_count) + random.uniform(0, 1)
requeue_with_delay(property_id, backoff_seconds)
else:
move_to_dlq(property_id, error,
context={'attempts': attempt_count,
'last_error': str(error)})
alert_monitoring("DLQ threshold exceeded", property_id)
DLQ nie jest śmietnikiem. To active queue wymagająca monitoringu. Podstawowe metryki: rozmiar DLQ, rate nowych wpisów, age najstarszego rekordu. Jeśli DLQ nagle zaczyna rosnąć, to sygnał systemowego problemu – może źródło danych zmieniło format, może deploy wprowadził regresję w parserze. Alerty na „DLQ size > 100” lub „DLQ growth rate > 10/hour” pozwalają wyłapać problem zanim stanie się krytyczny.
Reprocessing z DLQ może być manual (deweloper poprawia kod, ręcznie triggeruje retry dla wybranych rekordów) albo semi-automated (fix-forward script, który próbuje zastosować znane workaroundy dla common failure patterns). W systemie księgowym typowym przypadkiem DLQ było „dokument w formacie, którego parser nie obsługuje”. Fix wymagał albo update parsera, albo manual extraction danych i inject do systemu. Ale przynajmniej te przypadki były widoczne i tracked, zamiast cicho gubionych.
"Najstarszy rekord w DLQ przeżył już kilka wersji parsera i jedną zmianę formatu w rejestrze. Manual review to w praktyce kolejka do osoby, która kiedyś będzie miała wolne popołudnie."
RPA jako executor, nie decision maker
Jeden z największych błędów w projektach RPA to pakowanie całej logiki biznesowej w RPA tool. Platformy typu UiPath, Automation Anywhere mają GUI designers, gdzie „programujesz” przez przeciąganie boxów. To wygląda user-friendly dla nie-programistów, ale tworzy gigantyczny technical debt.
Po pierwsze, takie workflows są nie-testable w normalnym sensie. Nie możesz napisać unit testa dla diagramu flowchart. Nie masz type safety. Debugging to klikanie przez GUI, nie analiza stack trace. Code review jest niemożliwy – jak zrobić diff dwóch wersji flowchart zapisanego w proprietary XML?
Po drugie, vendor lock-in. Jeśli cała logika biznesowa jest zakodowana w UiPath workflows, to migracja na inną platformę (lub własne rozwiązanie) wymaga przepisania wszystkiego od zera. I nie masz nawet źródłowego kodu do referencji, tylko screenshoty flowchartów.
W projekcie ksiąg wieczystych biznes początkowo chciał „wszystko w RPA tool”. Argument był prosty: „przecież to właśnie do tego te narzędzia są stworzone”. Problem ujawnił się podczas próby wdrożenia pierwszej wersji logiki klasyfikacji ryzyka inwestycyjnego. Business rules były skomplikowane – różne wagi dla różnych typów obciążeń, specjalne przypadki dla nieruchomości rolnych, heurystyki oparte na historycznych wzorcach. Zakodowanie tego w flowchart było możliwe, ale resulted in nieczytelnym spaghetti 50+ boxów z zagnieżdżonymi if-ami.
Zespół potrzebował tygodnia na iterację – zmiana business rule wymagała modyfikacji flowchart, re-deploy, manual test na produkcji (bo automated tests nie istniały), i często rollback kiedy okazywało się, że coś się zepsuło. Deweloperzy nie mogli pracować równolegle, bo flowchart to jeden plik – merge conflicts były niemożliwe do rozwiązania.
Ostatecznie podjęliśmy decyzję: RPA tool jest tylko „rękami” – automatyzuje interakcję z UI rejestrów publicznych (klikanie, wypełnianie formularzy, pobieranie PDF-ów). Cała logika biznesowa wyszła do Python services: parsowanie, extraction, validation, classification. RPA workflow wywołuje te services przez REST API i przekazuje im pobrane dokumenty. Services zwracają structured data, które RPA zapisuje do docelowych systemów.
To podejście natychmiast odblokowało normalne software engineering practices. Python kod był w git, z pull requestami i code review. Unit testy pokrywały business logic. Type hints dawały basic safety. Deployment był przez CI/CD pipeline. Deweloperzy mogli pracować równolegle na różnych częściach logiki. Iteracja na business rules skróciła się z tygodnia do godzin.
Trade-off to dodatkowa warstwa API i operacyjna złożoność – teraz zamiast jednego procesu RPA masz RPA + Python services, które muszą być deployed i monitored osobno. Ale w dłuższej perspektywie cost był znikomy w porównaniu do korzyści z maintainability i velocity.
Praktyczne wyzwania w produkcji
Największy problem, który ujawnił się dopiero w produkcji, nie był techniczny – był ludzki. Zespół inwestycyjny przyzwyczaił się do manualnej analizy ksiąg wieczystych i miał głębokie domain knowledge o edge cases. Kiedy system zaczął automatycznie klasyfikować nieruchomości, zaufanie było zerowe. Każda rekomendacja była manual review przez analityka – co całkowicie negowało cel automatyzacji.
Przełomem było dodanie explicit confidence scoring i pokazywanie not just wyników, ale reasoning. Zamiast „ta nieruchomość ma niski potencjał”, system raportował „wykryto hipotekę na 80% wartości rynkowej (confidence: 95%), brak obciążeń środowiskowych (confidence: 60%), struktura własności: współwłasność ułamkowa 3 osób (confidence: 100%)”. Analitycy mogli zobaczyć które części analizy są pewne, a które wymagają weryfikacji.
Co ciekawe, to też ujawniło błędy w source data. W kilku przypadkach system raportował low confidence na podstawowych danych (jak numer działki), co wydawało się dziwne. Manual check pokazał, że źródłowy dokument w rejestrze publicznym faktycznie był niejasny albo zawierał sprzeczne informacje. Analitycy wcześniej tego nie zauważali, bo nie mieli systematic way to trackować confidence – po prostu „odhaczali” te pola jako kompletne. System wymuszał jawność o uncertainty.
Druga rzecz: początkowo próbowaliśmy osiągnąć 100% automatic processing. Okazało się to nierealistyczne i kontrproduktywne. ~15% przypadków było na tyle nietypowych (historyczne księgi sprzed 1990, nieruchomości z trwającymi postępowaniami sądowymi, cooperative ownership structures), że automatic classification była unreliable. Zamiast walczyć o te ostatnie 15%, wprowadziliśmy explicit „requires manual review” bucket. System automatycznie flagował nietypowe przypadki i routował je do analityków. 85% automatic processing okazało się wystarczające do massive efficiency gains, a próba forsowania remaining 15% generowałaby więcej błędów niż value.
Co działa się psuje w produkcji
Monitoring workflow reliability wymaga innych metryk niż typowe application monitoring. Nie interesuje cię tylko czy proces żyje (health check), ale czy faktycznie przetwarza dane i z jakim success rate. Kluczowe metryki okazały się:
Processing lag – różnica między „rekordy do przetworzenia” a „rekordy przetworzone”. Jeśli lag rośnie, znaczy że throughput nie nadąża za incoming data. To może być bottleneck w parsowaniu, rate limiting source systems, albo po prostu underprovisioned workers. Alert na „lag > 6 hours” dał nam advance warning zanim backlog urósł do krytycznego poziomu.
DLQ size per error type – nie tylko ile rekordów w DLQ, ale dlaczego tam trafiły. Nagły spike w „document parse error” sugerował zmianę formatu w source system. Spike w „timeout” mógł oznaczać degradację performance external API. Breakdown per error type był krytyczny dla szybkiego root cause analysis.
Processing time percentiles – p50/p95/p99 per workflow step. Średnia jest bezużyteczna, bo maskuje outliers. p99 pokazuje prawdziwy worst case experience. W systemie księgowym typowy dokument parsował się w ~30s (p50), ale p99 było 5 minut. Okazało się, że niektóre stare dokumenty miały quality issues wymagające multiple OCR passes. To był argument za wprowadzeniem timeout + fallback to manual processing dla długo przetwarzanych rekordów.
Reprocessing rate – ile rekordów wymaga retry vs first-time success. Wysoki reprocessing rate sugerował problemy z source stability albo zbyt agresywne retry policies. W health systemie rate powinien być < 5%. Kiedy wzrósł do 20%, znaleźliśmy bug w retry logic, który kasował checkpoint state i wymuszał full reprocessing nawet po drobnych błędach.
Data freshness – jak stare są dane źródłowe, na których bazują wyniki. Rejestry publiczne aktualizują się z opóźnieniem. Kluczowe było trackowanie „ten raport bazuje na danych z [timestamp]” i alertowanie, jeśli freshness przekraczała SLA. Kilka razy system wydawał rekomendacje na podstawie danych starszych niż tydzień, bo fetch z source system cicho failowało ale nie było visibility.
"Health check przez cały tydzień był zielony. Proces żył i nie zgłaszał błędów, tylko rekomendacje powstawały na danych, których od tygodnia nikt nie pobrał z rejestru."
Refleksje po deploymencie
Projektując RPA workflows instynkt podpowiada żeby myśleć o happy path – jak ma działać kiedy wszystko idzie dobrze. Produkcja bardzo szybko weryfikuje te naiwne założenia. W distributed systemach dawno zaakceptowaliśmy, że failure jest normą i projektujemy defensywnie. W RPA jakoś kultura tej defensive architecture nie dotarła – narzędzia promują „szybkie prototypy” i drag-and-drop simplicity kosztem resilience patterns.
Prawdziwy koszt monolitycznego workflow nie jest widoczny w POC ani przez pierwsze tygodnie produkcji. Ujawnia się dopiero kiedy system ma przetwarzać tysiące rekordów dziennie i statistycznie któryś musi failnąć. Wtedy brak checkpointingu, brak idempotency, brak DLQ przestają być „nice to have” i stają się blokerami operacyjnymi. Restart batch procesu, który pada co 200 rekord, kiedy masz ich 2000 do przetworzenia dziennie, to matematyczny absurd.
Ironiczne jest to, że wszystkie te patterns – idempotency, checkpointing, dead letter queues – są dobrze znane w backend engineering od dekad. Ale RPA jest często traktowane jako „nie-programowanie”, więc te lekcje muszą być odkrywane na nowo przez każdy zespół który uderza w production scale. Może to jest właśnie problem z low-code platforms – sprawiają wrażenie, że trudne problemy znikają jeśli tylko masz ładny GUI.
Podsumowanie
Po kilku miesiącach utrzymania takiego systemu liczba przetworzonych rekordów w raporcie budzi więcej pytań niż spokoju. Chcesz wiedzieć, ile razy ten sam krok wykonał się dla jednej księgi i który z tych przebiegów zapisał wynik do bazy.
Najwięcej uczą awarie mechanizmów, które miały chronić przed awariami. Retry, który po drobnym błędzie kasuje checkpoint, zamienia pojedynczy timeout w pełny reprocessing i przez dłuższy czas uchodzi za zwykłą niestabilność rejestru publicznego.
Z czasem DLQ i kosz „requires manual review” okazują się najbardziej szczerą częścią architektury. Trafiają tam księgi sprzed 1990 roku i nieruchomości w trakcie postępowań sądowych, czyli wszystko, czego diagram nie przewidział. Przy kilkunastu procentach takich rekordów trudno dalej udawać, że to wyjątki.
Prawdziwy test takiego workflow odbywa się rano, po nocnym batchu, który padł gdzieś w połowie. Jeśli da się go wznowić od miejsca przerwania, a nikt nie musi zgadywać, które rekordy zapisały się dwa razy, system przeżył kolejną noc. Proof of concept na 20 rekordach nigdy nie dociera do tego miejsca.
Zobacz powiązane case studies i analizy
Procesy, architektura i workflow powiązane z tematami poruszanymi w tym materiale – od integracji i realtime systems po automatyzacje operacyjne.