Zeszłej zimy pomagałem zespołowi zajmującemu się inteligentną obsługą klienta w diagnozowaniu problemu produkcyjnego. Ich backend jednocześnie łączył się z API czterech dużych modeli, a frontend używał SSE do wyjścia strumieniowego. Trzeciego dnia po wdrożeniu użytkownicy zaczęli zgłaszać, że „odpowiedź wygląda jak wyciskanie pasty do zębów” — czasami cały fragment tekstu zatrzymywał się w miejscu, a potem nagle wylewała się duża jego porcja; czasami zdanie było przecięte na pół, a pomiędzy znajdowały się niezrozumiałe puste bloki. W logach backendu nie było błędów 5xx, a monitoring dostępności API każdego dostawcy świecił na zielono.
Takie problemy są najtrudniejsze do zdiagnozowania, ponieważ każdy element wygląda na działający poprawnie. Razem z nimi zacząłem od panelu Network w przeglądarce i schodziłem warstwa po warstwie w dół, aż w końcu odkryłem, że problem wcale nie leży w API któregokolwiek dostawcy, lecz w samym fakcie „mieszania czterech dostawców”.
Najpierw wniosek: nieujednolicony protokół streamingu to ukryta pułapka agregowanego dostępu
Strumieniowe wyjście API dużych modeli w większości opiera się na SSE (Server-Sent Events). Jednak „wszyscy używają SSE” nie oznacza „wszyscy zachowują się identycznie”. Czterej dostawcy mają własne implementacje pod względem ziarnistości dzielenia na bloki, znacznika zakończenia i strategii heartbeatu. Strumień bajtów otrzymywany przez frontend wygląda na rozpoczynający się od data:, ale logiki parsowania nie da się w ogóle ponownie wykorzystać. To jest źródło zacięć i anomalii na granicach bloków.
Warstwa po warstwie: od frontendu do bramy, gdzie leży problem
Pierwsza warstwa, frontend. Ich parser SSE używał standardowego EventSource i dzielił zdarzenia po \n\n. To samo w sobie nie było problemem.
Druga warstwa, przekazywanie w backendzie. Backend używał Node do warstwy transmisji, przekazując strumień ReadableStream od upstream bezpośrednio przez pipe do odpowiedzi. Tutaj była ukryta pierwsza mina: rozmiary chunków u różnych dostawców znacznie się różnią — jedni wysyłają po tokenie, inni zbierają kilka tokenów i dopiero wtedy wysyłają. Jeśli w procesie pipe wykonano Buffer.concat i nieobsłużono poprawnie granic wielobajtowych znaków UTF-8 pomiędzy chunkami, dochodziło do przecięcia połowy chińskiego znaku, a frontend renderował go jako niezrozumiały kwadrat.
Trzecia warstwa, limity czasu i ponawianie. Backend ustawił jednolity 30-sekundowy limit czasu dla wszystkich czterech dostawców. Jednak w scenariuszu strumieniowym „30-sekundowy limit czasu” oznacza czas do pierwszego bajtu czy czas całego strumienia? Nie rozróżnili tego, co powodowało przerywanie długich odpowiedzi w połowie, a frontend pokazywał „mówi do połowy i nagle się zatrzymuje”.
Czwarta warstwa, kody błędów. Kody statusu HTTP i struktury body zwracane przez czterech dostawców przy ograniczeniu szybkości, błędzie uwierzytelniania i blokadzie moderacji treści były różne. Backend jednolicie pobierał error.message, w rezultacie u dwóch dostawców udawało się je pobrać, a u dwóch otrzymywano undefined, więc frontend wyświetlał pusty komunikat błędu.
Po dotarciu do tego miejsca wszystko stało się jasne: prawdziwym problemem nie był błąd w API któregokolwiek dostawcy, lecz fakt, że semantyka czterech zestawów protokołów była bezpośrednio wystawiona warstwie biznesowej.
Kluczowe różnice w implementacjach streamingu u czterech dostawców
Przygotowałem porównanie; dane pochodzą z oficjalnej dokumentacji każdego dostawcy i rzeczywistego przechwytywania pakietów, a części nieujawnione oficjalnie oznaczyłem:
WymiarDostawca ADostawca BDostawca CDostawca D
Znacznik zakończeniadata: [DONE]pole finish_reasondata: [DONE]zdarzenie event: done
Heartbeatbrakokoło 15 sekundlinia komentarzybraknieujawnione oficjalnie
Ziarnistość blokówpo tokenie2-4 tokeny narazpo tokenienieustalona
Położenie kodu błędukod statusu HTTPbody.error.codekod statusu HTTPbody.code
Patrząc na tę tabelę, widać, dlaczego logiki parsowania frontendu nie da się ponownie wykorzystać. Znacznik zakończenia w trzech wariantach, kod błędu w dwóch miejscach, strategia heartbeatu u dwóch dostawców obecna, u dwóch nie. Jeśli w kodzie biznesowym użyje się bezpośrednio if (chunk.includes('[DONE]')), to w przypadku dostawcy B nigdy się nie zakończy, a połączenie będzie wisieć do przekroczenia limitu czasu.
Cztery kluczowe punkty projektowe ujednolicenia w warstwie bramy
Po pierwsze, ujednolicić format zdarzeń SSE. Brama normalizuje wszystkie zdarzenia od poszczególnych dostawców upstream do jednej struktury: {type: "delta" | "done" | "error", content, code}. Warstwa biznesowa rozpoznaje tylko ten jeden zestaw i nie interesuje się już, kim jest upstream. W naszym projekcie użyta platforma agregacji API dużych modeli token8341 właśnie tak robi — na zewnątrz udostępnia tylko jeden protokół streamingu, więc przy zmianie modelu kod biznesowy nie wymaga modyfikacji.
Po drugie, skoncentrować uwierzytelnianie w bramie. Cztery zestawy kluczy dla czterech dostawców rozproszone w konfiguracji biznesowej oznaczały, że rotacja wymagała zmian w czterech miejscach. Po przeniesieniu kluczy do bramy warstwa biznesowa otrzymuje tylko wewnętrzny token. Wartość warstwy agregacji takiej jak platforma agregacji API dużych modeli token8341 polega właśnie na tym: jeden klucz wywołuje wiele modeli, a rotacja kluczy i kontrola limitów odbywają się po stronie bramy.
Po trzecie, limity czasu trzeba skonfigurować w dwóch etapach. Limit czasu do pierwszego bajtu (TTFB) i limit bezczynności strumienia należy rozdzielić. TTFB ustawić krócej, na przykład 10 sekund, aby szybko ponosić porażkę; limit bezczynności strumienia ustawić dłużej, na przykład 60 sekund, aby pozwolić modelowi „myśleć”. Wspólne 30 sekund dla obu z pewnością spowoduje problemy.
Po czwarte, ponawiać tylko błędy idempotentne. Gdy żądanie strumieniowe już rozpoczęło wyjście, ponowienie spowoduje duplikację treści. Warstwa bramy musi rozróżniać: błąd na etapie nawiązywania połączenia można ponowić, błąd po rozpoczęciu wyjścia strumienia można tylko zgłosić frontendowi, aby użytkownik ręcznie wysłał ponownie. Jeśli ta granica nie jest jasno wyznaczona, pojawi się „to samo zdanie wypowiedziane dwa razy”.
Tabelę mapowania kodów błędów trzeba utrzymywać samodzielnie
Kody ograniczenia szybkości u czterech dostawców: dwóch zwraca 429, jeden zwraca 200, ale w body znajduje się błąd, jeden zwraca własny kod. Warstwa bramy musi zbudować tabelę mapowania, ujednolicającą różne błędy upstream do wewnętrznych kodów błędów, takich jak RATE_LIMIT, AUTH_FAILED, CONTENT_BLOCKED. Warstwa biznesowa wykonuje degradację na podstawie kodów wewnętrznych i nie zgaduje już semantyki upstream. Ta tabela nie ma skrótów — trzeba ją zestawić dostawca po dostawcy, porównując przechwycone pakiety z dokumentacją. Platforma agregacji API dużych modeli token8341 podaje w dokumentacji gotowe odniesienie do mapowania, co oszczędziło sporo pracy.
W jakich przypadkach bezpośrednie połączenie jest jednak prostsze
Trzeba przyznać, że nie każdy scenariusz zasługuje na bramę. Jeśli projekt łączy się tylko z jednym modelem albo wykonuje wyłącznie nie strumieniowe wnioskowanie wsadowe, bezpośrednie połączenie z oficjalnym SDK oznacza najmniej kodu, najkrótszą ścieżkę i najprostsze diagnozowanie. Warstwa bramy rozwiązuje złożoność wynikającą z „ujednoliconego dostępu do wielu modeli”; w scenariuszu jednomodelowym wprowadzenie bramy to dodatkowa warstwa przekazywania i dodatkowy punkt awarii. Kryterium oceny jest proste: dopiero gdy liczba podłączonych modeli przekracza dwa albo gdy trzeba przełączać modele w czasie działania, korzyści z bramy przewyższają koszty.
Na koniec
Tamten incydent został ostatecznie rozwiązany przez dodanie warstwy bramy ujednolicającej; logika parsowania SSE we frontendzie została zredukowana z czterech gałęzi do jednej, a zacięcia i anomalie bloków zniknęły. Patrząc wstecz, problem nigdy nie leżał w API któregokolwiek dużego modelu, lecz w „bezpośrednim przekazywaniu różnic protokołów wielu dostawców do warstwy biznesowej”. Jeśli ty też zajmujesz się dostępem do wielu modeli, najpierw ujednolicij w warstwie bramy cztery rzeczy: protokół streamingu, kody błędów, uwierzytelnianie i limity czasu, a dopiero potem rozmawiaj o funkcjach wyższej warstwy.
Autor: Sun Haoran
Data publikacji: 25 października 2026 r.