Jeden wątek, trzy zgłoszenia. Po czym agent pocztowy poznaje, że to ta sama sprawa
Agent zakłada trzy sprawy z jednego wątku mailowego. Jak ustalić tożsamość wątku z nagłówków, dodać klucz idempotencji i przerwać pętlę autorespondera.
- Zdarzenie nowy mail
- Klasyfikacja typ, pilność, klient
- Narzędzia crm_search
- Zatrzymanie odpowiedź do klienta
- Działanie mail_send
- Log wejście, decyzja, wynik
Klient odpisał trzy razy w tym samym wątku. W systemie zgłoszeń są trzy sprawy: dwie zamknięte bez odpowiedzi, jedna z adnotacją „duplikat”, a historia rozmowy rozsypana na trzy numery. Agent nie pomylił się w rozumowaniu — zabrakło mu dwóch rzeczy, których model nie dostarczy sam: tożsamości wątku i klucza, po którym poznaje wiadomość już obsłużoną.
Rozszczepione wątki i podwojone zgłoszenia to nie usterka modelu, tylko luka w przepływie. Poniżej ten przepływ rozpisany na kroki, cztery miejsca, w których rodzą się duplikaty, mechanika, która je zamyka, i rachunek za miesiąc, w którym jej nie ma.
Agent czyta skrzynkę i zakłada zgłoszenie: gdzie pęka przepływ
Przepływ ma sześć kroków i w każdym da się wskazać awarię:
- Krok 1 — przychodzi powiadomienie o nowej wiadomości. Awaria: to samo powiadomienie przychodzi dwa razy albo nie przychodzi wcale.
- Krok 2 — agent pobiera treść i nagłówki. Awaria: pobiera tylko treść i temat, bo nagłówki wymagają osobnego parametru.
- Krok 3 — agent ustala, czy to nowa sprawa, czy ciąg dalszy istniejącej. Awaria: rozstrzyga po temacie.
- Krok 4 — agent zapisuje zgłoszenie albo dopisuje wiadomość do istniejącego. Awaria: zapis bez klucza unikalnego, więc dwa równoległe procesy zakładają dwie sprawy.
- Krok 5 — agent odpowiada nadawcy. Awaria: nadawcą jest autoresponder, który odpowie na odpowiedź.
- Krok 6 — agent zamyka albo eskaluje. Awaria: zamyka sprawę, która nigdy nie była jedną sprawą.
Kroki 3, 4 i 5 odpowiadają za niemal wszystkie duplikaty w takim przepływie, bo każdy z nich opiera się na założeniu, które poczta elektroniczna łamie codziennie: że temat identyfikuje rozmowę, że powiadomienie przychodzi dokładnie raz i że po drugiej stronie jest człowiek.
Ustal tożsamość wątku z nagłówków, nie z tematu
Poczta ma do tego własny mechanizm, starszy od wszystkich systemów zgłoszeń w firmie. RFC 5322 w sekcji 3.6.4 definiuje trzy nagłówki: Message-ID identyfikuje konkretną wiadomość, In-Reply-To zawiera identyfikator wiadomości, na którą ta jest odpowiedzią, a References gromadzi całą dotychczasową ścieżkę wątku. Odpowiedź dopisuje identyfikator rodzica na koniec jego References — to jest gotowa, uporządkowana lista przodków, i nie trzeba jej odtwarzać zgadywaniem.
Temat nie jest identyfikatorem. Klient pisze „Re:”, polski Outlook „Odp:”, ktoś przepisuje temat przy przekazaniu, a dwóch różnych klientów wysyła tego samego dnia wiadomość o temacie „Faktura”. Dopasowanie po temacie daje więc dwa rodzaje błędu naraz: rozcina wątek, gdy prefiks się zmienił, i scala obce sprawy, gdy temat jest pospolity.
| Sygnał tożsamości | Skąd go wziąć | Kiedy zawodzi |
|---|---|---|
Message-ID + References |
Nagłówki RFC 5322; w Microsoft Graph pola internetMessageId i internetMessageHeaders |
Nadawca używa klienta, który nie ustawia References — zostaje In-Reply-To |
threadId (Gmail) |
Pole wiadomości w Gmail API | Wątek sklejony przez dostawcę inaczej, niż widzi go klient po drugiej stronie |
conversationId (Outlook) |
Właściwość zasobu message w Graph |
Rozmowa prowadzona poza tenantem i wracająca po przekazaniu |
| Temat wiadomości | Pole subject |
Przy każdym prefiksie, przekazaniu i powtarzalnym temacie |
Kolejność rozstrzygania jest prosta: najpierw References i In-Reply-To przeszukane wstecz po własnej bazie, potem identyfikator wątku dostawcy, a temat dopiero jako sygnał pomocniczy — i nigdy jako jedyna przesłanka scalenia. Warto to sprawdzić także po stronie wysyłki: żeby dopisać wiadomość do istniejącego wątku, Gmail API wymaga trzech rzeczy jednocześnie — podanego threadId, nagłówków References i In-Reply-To zgodnych ze standardem oraz zgodnego tematu. Agent, który odpowiada bez tych nagłówków, rozszczepia wątek po własnej stronie.
Jedna pułapka techniczna: w Graph nagłówki trzeba zażądać jawnie przez $select, bo internetMessageHeaders nie wraca domyślnie. Agent, któremu nikt tego nie ustawił, widzi tylko temat i treść — i wtedy rozstrzyga po temacie, bo nic innego nie ma.
Powiadomienie przyjdzie dwa razy: klucz idempotencji przed zapisem
Druga przesłanka, która nie jest prawdziwa, to „każda wiadomość wywoła agenta dokładnie raz”. W obu dużych ekosystemach powiadomienia są projektowane jako co najmniej jednokrotne. W Pub/Sub, przez który idą powiadomienia Gmaila, dostawa dokładnie jednokrotna jest wyłączona domyślnie, a sama dokumentacja zastrzega, że nawet przy niej subskrypcja może dostać kilka kopii wiadomości z powodu duplikatów po stronie publikacji. Ponowne dostawy po wygaśnięciu terminu potwierdzenia są tam opisane jako działanie zgodne z zamysłem, nie jako awaria.
Dlatego klucz idempotencji musi stać przed wywołaniem modelu i przed zapisem, a nie po. Praktycznie: unikalny indeks w bazie na skrót z Message-ID, wstawienie w trybie „wstaw albo nic nie rób” i dopiero po udanym wstawieniu klasyfikacja i odpowiedź. Sekwencja „sprawdź, czy istnieje, potem wstaw” wygląda tak samo, a przy dwóch równoległych procesach i jednej ponownej dostawie zakłada dwie sprawy — obie przeszły sprawdzenie, zanim którakolwiek zapisała.
Czego nie wolno użyć jako tego klucza: identyfikatora wiadomości w Graph. Dokumentacja zasobu message mówi wprost, że id domyślnie zmienia się, gdy element przeniesie się między folderami, o ile żądanie nie poprosi o identyfikator niezmienny nagłówkiem Prefer: IdType="ImmutableId". Reguła w skrzynce, która przenosi pocztę do podfolderu, wystarczy, żeby ta sama wiadomość dostała nowy identyfikator i drugie zgłoszenie.
Agent odpowiada autoresponderowi, autoresponder odpowiada agentowi
Pętla dwóch automatów to najdroższy rodzaj duplikatu, bo rośnie sama. Standard na to jest i ma dwadzieścia lat: RFC 3834 stawia wymóg, żeby automatyczny responder nie odpowiadał bezmyślnie na każdą otrzymaną wiadomość, nie odpowiadał na wiadomości z nagłówkiem Auto-Submitted o wartości innej niż no i nie generował odpowiedzi tam, gdzie adresem zwrotnym jest adres pusty. Ten sam dokument zaleca, by nie wysyłać tej samej odpowiedzi do tego samego nadawcy częściej niż raz na kilka dni — domyślnie siedem dni.
To daje trzy konkretne reguły do wpisania w przepływ, po jednej na każdą stronę:
- Na wejściu: odrzuć bez odpowiedzi wiadomość z
Auto-Submittedinnym niżno, z pustym adresem zwrotnym oraz od adresów typuMAILER-DAEMON,*-requestiowner-*, które RFC 3834 wymienia jako zwyczajowo automatyczne. Taka wiadomość może założyć zgłoszenie do przejrzenia, ale nie może wywołać odpowiedzi. - Na wyjściu: oznacz własną odpowiedź nagłówkiem
Auto-Submitted: auto-replied, żeby druga strona miała po czym poznać automat. Dla skrzynek Exchange dochodziX-Auto-Response-Suppress— specyfikacja MS-OXCMAIL definiuje wartościDR,NDR,RN,NRN,OOF,AutoReplyiAll;OOF, AutoReplywycisza odpowiedzi nieobecnościowe i pozostałe automatyczne. - Jako bezpiecznik: licznik wiadomości wysłanych przez agenta w jednym wątku bez żadnej wiadomości od człowieka. Po przekroczeniu progu (trzy to rozsądny start) wątek idzie do kolejki ludzkiej i agent w nim milczy, niezależnie od tego, co pokazały nagłówki.
Bezpiecznik jest tu istotniejszy od nagłówków, bo nagłówki ustawia druga strona, a licznik liczysz u siebie. Masowa odpowiedź nieobecnościowa po długim weekendzie potrafi wejść bez żadnego oznaczenia.
Subskrypcja wygasa w środku tygodnia: przebieg uzgadniający
Ostatnie źródło duplikatów działa odwrotnie: nie dubluje zgłoszeń, tylko gubi wiadomości, a te wracają później jako „dlaczego nikt nie odpisał” i zostają założone drugi raz ręcznie, obok tego, co agent dopisał do wątku z opóźnieniem.
Terminy są twarde i trzeba je mieć w kalendarzu wdrożenia. W Gmailu metodę watch trzeba wywołać co najmniej raz na siedem dni, w przeciwnym razie powiadomienia dla tej skrzynki się kończą; dokumentacja zaleca wołać ją raz na dobę i zastrzega, że w rzadkich sytuacjach powiadomienia mogą się opóźnić albo zginąć. W Microsoft Graph subskrypcja na zasób message żyje maksymalnie 10 080 minut, czyli niespełna siedem dni, ale subskrypcja z danymi zasobu w powiadomieniu — tylko 1440 minut, czyli niespełna dobę; na jedną skrzynkę przypada przy tym limit 1000 aktywnych subskrypcji dla wszystkich aplikacji razem. Opóźnienie dostawy powiadomienia o nowej wiadomości ta sama dokumentacja podaje jako średnio poniżej minuty i maksymalnie trzy minuty — i to jest liczba, do której należy przykładać własne alerty, zamiast zakładać natychmiastowość.
Stąd wniosek, który zmienia architekturę: powiadomienia są przyspieszeniem, nie źródłem prawdy. Źródłem prawdy jest przebieg uzgadniający — co godzinę pobierz listę wiadomości z ostatnich dwudziestu czterech godzin, porównaj po Message-ID z własną bazą i dociągnij to, czego nie ma. Ten sam przebieg daje darmowy wskaźnik: liczba wiadomości, które znalazł jako brakujące, to zmierzona utrata powiadomień. Jeśli rośnie, subskrypcja wygasa wcześniej, niż ją odnawiasz.
Co zatwierdza człowiek w tym przepływie
Granica nie przebiega między „łatwym” a „trudnym”, tylko między operacjami odwracalnymi i nieodwracalnymi. Dopisanie wiadomości do wątku jest odwracalne jednym ruchem. Scalenie dwóch zgłoszeń — już nie, bo po scaleniu nie wiadomo, która wiadomość należała do której sprawy.
| Operacja | Kto decyduje | Dlaczego tak |
|---|---|---|
Dopisanie wiadomości do wątku na podstawie References |
Agent | Odwracalne, oparte na jednoznacznym identyfikatorze |
| Założenie nowego zgłoszenia | Agent | Nadmiarowe zgłoszenie jest tańsze niż zgubiona sprawa |
| Scalenie dwóch zgłoszeń w jedno | Człowiek | Nieodwracalne, miesza historie dwóch spraw |
| Rozdzielenie wątku na dwie sprawy | Człowiek | To samo w drugą stronę |
| Wysłanie odpowiedzi do nadawcy, którego agent nie rozpoznał | Człowiek, dopóki wskaźnik rozszczepień nie zejdzie do zera | Odpowiedź w złym wątku ujawnia dane innej sprawy |
| Zamknięcie zgłoszenia | Człowiek albo reguła czasowa | Zamknięcie przez agenta maskuje właśnie te błędy, które chcesz zmierzyć |
Osobna sprawa to właścicielstwo przebiegu uzgadniającego i odnawiania subskrypcji. To nie jest praca na wdrożenie, to praca na zawsze: ktoś musi dostać alert, gdy odnowienie nie przeszło, i mieć prawo wyłączyć agenta na godzinę. Jeśli w zespole nie ma kogoś z takim dyżurem, rozsądniej jest oddać utrzymanie na zewnątrz — wynajęcie zespołu, który obejmuje taki przepływ dyżurem po wdrożeniu jest tańsze niż tygodniowa cisza w skrzynce, której nikt nie zauważył.
Lista kontrolna przed uruchomieniem
- Unikalny indeks na skrót z
Message-IDistnieje w bazie, a wstawienie jest w trybie „wstaw albo nic nie rób”. - Pobranie wiadomości żąda nagłówków jawnie (w Graph przez
$select, zPrefer: IdType="ImmutableId"). - Rozstrzyganie wątku idzie w kolejności:
ReferencesiIn-Reply-To, identyfikator wątku dostawcy, temat jako sygnał pomocniczy. - Wejściowy filtr automatów:
Auto-Submitted, pusty adres zwrotny, lista adresów technicznych. - Wyjście oznaczone
Auto-Submitted: auto-repliediX-Auto-Response-Suppress. - Licznik wiadomości agenta w wątku bez udziału człowieka, z progiem i przekazaniem do kolejki.
- Odnawianie subskrypcji w harmonogramie z zapasem (Gmail — dobowo, Graph — z marginesem wobec limitu zasobu) plus alert, gdy odnowienie nie przeszło.
- Przebieg uzgadniający co godzinę, z licznikiem braków wystawionym jako metryka.
- Scalanie i rozdzielanie wątków dostępne tylko dla człowieka, z zapisem kto i kiedy.
Policz miesiąc duplikatów i zmierz trzy wskaźniki
Rachunek liczony przy jawnych warunkach: skrzynka przyjmuje 1200 wiadomości miesięcznie, 40 proc. z nich (480) to kolejne wiadomości w już istniejących wątkach. Agent klasyfikuje każdą wiadomość osobno: instrukcja systemowa i schemat kategorii zajmują 1800 tokenów i są w pamięci podręcznej, nowy tekst wiadomości to średnio 1500 tokenów wejścia, odpowiedź agenta 250 tokenów wyjścia. Stawki pierwszej strony dla modelu klasy Claude Haiku 5.5 przy promptach do 100 tys. tokenów to 0,10 USD za milion tokenów wejścia, 0,01 USD za milion tokenów odczytanych z pamięci podręcznej i 0,50 USD za milion tokenów wyjścia (sprawdzone 24 października 2026; stawki się zmieniają, więc podstaw aktualne). Jedna klasyfikacja kosztuje przy tym około 0,00029 USD.
Udziały błędów w tabeli poniżej są założeniem, nie pomiarem — to miejsca, w które trzeba wstawić własne liczby po pierwszym miesiącu. Czas ludzki wyceniony jest trzema minutami na scalenie i stawką 60 zł za godzinę pracy osoby, która to robi.
| Pozycja | Zdarzeń w miesiącu | Tokeny | Minuty człowieka |
|---|---|---|---|
| Klasyfikacja wiadomości | 1200 | 0,35 USD | 0 |
| Ponowne dostawy powiadomień bez klucza idempotencji (3 proc.) | 36 | 0,01 USD | 108 |
| Wątki rozszczepione przez dopasowanie po temacie (10 proc. z 480) | 48 | 0,01 USD | 144 |
| Pętla z autoresponderem (2 wątki po 6 wiadomości) | 12 | <0,01 USD | 30 |
| Razem | — | około 0,38 USD | 282, czyli 4,7 godziny (około 280 zł) |
Proporcja jest tu całym wnioskiem: tokeny tego przepływu kosztują mniej niż jedna kawa, a brak klucza idempotencji i rozstrzyganie po temacie kosztują prawie dzień pracy miesięcznie plus klienci, którzy dostali dwie różne odpowiedzi na jedno pytanie. Mechanika z sekcji wyżej nie jest optymalizacją rachunku za model — model jest tani. Jest optymalizacją tego, co dzieje się obok. Jeśli przepływ stoi na platformie automatyzacji zamiast na własnym kodzie, do rachunku dochodzi jeszcze jednostka rozliczeniowa dostawcy — ten sam scenariusz bywa liczony jako kilka zadań, kilka kredytów albo jedno wykonanie, a ponowna dostawa powiadomienia jest wtedy płatna dwa razy.
Trzy wskaźniki, które mówią, czy to działa, i sposób ich liczenia:
- Wskaźnik rozszczepień — liczba zgłoszeń scalonych ręcznie w miesiącu podzielona przez liczbę wiadomości będących kontynuacją wątku. Liczony z logu scaleń, nie z opinii zespołu.
- Duplikaty na 1000 wiadomości — ile razy indeks unikalny odrzucił wstawienie. Ten licznik nie wymaga nic poza odczytem błędów konfliktu i pokazuje realną skalę ponownych dostaw w twojej skrzynce.
- Braki znalezione przez przebieg uzgadniający — liczba wiadomości, które przebieg dociągnął, bo powiadomienie nie przyszło. Zero oznacza zdrową subskrypcję; wzrost w jednym dniu tygodnia oznacza, że odnowienie wypada za późno.
Żadnego z nich nie da się policzyć po wdrożeniu, jeśli agent sam zamyka zgłoszenia — zamknięte duplikaty wypadają z licznika i wskaźnik rozszczepień spada do zera przy niezmienionym bałaganie. Dlatego zamykanie trafiło w tabeli wyżej do kolumny człowieka.
Decyzja do podjęcia przed uruchomieniem jest jedna i nie da się jej odłożyć: albo agent dostaje tożsamość wątku z nagłówków i klucz idempotencji przed pierwszym zapisem, albo w pierwszym miesiącu tylko czyta i proponuje kategorię, a zgłoszenia zakłada człowiek. Trzeciej drogi — agenta, który zakłada sprawy, rozstrzygając po temacie — nie ma, bo jej koszt nie pojawia się na rachunku za tokeny, tylko w kolejce scaleń i w skrzynce klienta.