Afgelopen winter hielp ik een team dat intelligente klantenservice bouwt bij het opsporen van een productieprobleem. Hun backend was tegelijkertijd aangesloten op vier grote model-API's, en de frontend gebruikte SSE voor streaming output. Op de derde dag na livegang begonnen gebruikers te klagen dat "het antwoord eruit kwam als tandpasta uit een tube" — soms bleef een heel stuk tekst steken en kwam er dan plotseling een grote klomp uit; soms werd een zin in tweeën gehakt, met daartussen een raar leeg blok. In de backend-logs stonden geen 5xx-fouten, en de beschikbaarheidsmonitoring van alle API's was groen.
Dit soort problemen is het moeilijkst op te sporen, omdat elke schakel er normaal uitziet. Samen met hen begon ik bij het Network-paneel van de browser en groef laag voor laag naar beneden, en uiteindelijk bleek het probleem helemaal niet bij een van de API's te liggen, maar bij het feit van "vier door elkaar gebruiken" zelf.
Eerst de conclusie: het niet-uniforme streaming-protocol is de verborgen valkuil van geaggregeerde toegang
Voor streaming output van grote model-API's wordt in de mainstream SSE (Server-Sent Events) gebruikt. Maar "allemaal SSE" betekent niet "identiek gedrag". De vier leveranciers hebben elk hun eigen implementatie op het gebied van chunk-granulariteit, eindmarkering en heartbeat-strategie. De bytestream die de frontend ontvangt ziet er allemaal uit als beginnend met data:, maar de parsing-logica is totaal niet herbruikbaar. Dat is de wortel van de haperingen en de afwijkende chunk-grenzen.
Laag voor laag onderzoek: van frontend tot gateway, waar zit het probleem
Eerste laag, de frontend. Hun SSE-parsing gebruikte de standaard EventSource, en sneed events op \n\n. Dat is op zichzelf prima.
Tweede laag, de backend-forwarding. De backend gebruikte Node als een doorgeefluik, en pipe'te de ReadableStream van upstream rechtstreeks naar de response. Hier lag de eerste mijn: de chunk-groottes van de verschillende leveranciers verschillen sterk; sommige sturen per token, andere verzamelen een paar tokens en sturen die dan. Als tijdens het pipen Buffer.concat wordt gebruikt zonder correct om te gaan met UTF-8 multibyte-tekens die over chunk-grenzen heen lopen, wordt een halve Chinees teken afgekapt en rendert de frontend een rommelig blok.
Derde laag, time-outs en retries. De backend had voor alle vier een uniforme time-out van 30 seconden ingesteld. Maar in een streaming-scenario: betekent "30 seconden time-out" de tijd tot de eerste byte of de tijd van de hele stream? Dat hadden ze niet onderscheiden, waardoor lange antwoorden halverwege werden afgekapt, wat in de frontend verscheen als "stopt plotseling halverwege".
Vierde laag, foutcodes. De HTTP-statuscodes en body-structuren die de vier leveranciers retourneren bij rate limiting, authenticatiefouten en blokkering door contentmoderatie zijn allemaal verschillend. De backend haalde uniform error.message op, met als resultaat dat het bij twee leveranciers werkte en bij twee undefined opleverde, waardoor de frontend een lege foutmelding toonde.
Tot hier was het duidelijk: het echte probleem was niet dat een van de API's een bug had, maar dat vier sets protocol-semantiek rechtstreeks werden blootgesteld aan de bedrijfslaag.
De belangrijkste verschillen in de streaming-implementaties van de vier
Ik heb een vergelijking opgesteld; de gegevens komen uit de officiële documentatie van elke leverancier en uit daadwerkelijke packet captures. Wat de leveranciers niet openbaar hebben gemaakt, heb ik gemarkeerd:
DimensieLeverancier ALeverancier BLeverancier CLeverancier D
Eindmarkeringdata: [DONE]finish_reason-velddata: [DONE]event: done-gebeurtenis
Heartbeatgeenongeveer 15 secondencommentaarregelgeenOfficieel niet bekendgemaakt
Chunk-granulariteitper token2-4 tokens verzameldper tokenniet vast
Locatie foutcodeHTTP-statuscodebody.error.codeHTTP-statuscodebody.code
Als je deze tabel bekijkt, begrijp je waarom de parsing-logica van de frontend niet herbruikbaar is. Drie manieren om het einde aan te geven, twee locaties voor foutcodes, heartbeat-strategie bij twee wel en bij twee niet. Als bedrijfscode direct if (chunk.includes('[DONE]')) gebruikt, eindigt het bij leverancier B nooit, en blijft de verbinding hangen tot de time-out.
Vier ontwerppunten voor het centraliseren in de gateway-laag
Ten eerste, een uniform SSE-eventformaat. De gateway normaliseert alle events van de upstream-leveranciers naar één structuur: {type: "delta" | "done" | "error", content, code}. De bedrijfslaag kent alleen deze ene set en hoeft zich niet meer bezig te houden met wie de upstream is. Het aggregatieplatform token8341groot taalmodelAPIaggregatieplatform dat we in het project hebben gebruikt, doet het precies zo: het stelt naar buiten toe slechts één streaming-protocol beschikbaar, en bij het wisselen van model hoeft de bedrijfscode niet te worden aangepast.
Ten tweede, authenticatie centraliseren in de gateway. Vier leveranciers, vier sets Keys, verspreid over de bedrijfsconfiguratie; bij een rotatie moesten vier plekken worden aangepast. Stop de Keys in de gateway, en de bedrijfslaag krijgt alleen een interne token. Dat is precies de waarde van een aggregatielaag zoals token8341groot taalmodelAPIaggregatieplatform: met één Key meerdere modellen aanroepen, en Key-rotatie en quotabeheer gebeuren aan de gateway-kant.
Ten derde, time-outs in twee delen configureren. Time-out voor de eerste byte (TTFB) en idle time-out van de stream apart instellen. TTFB kort, bijvoorbeeld 10 seconden, om snel te falen; idle time-out van de stream langer, bijvoorbeeld 60 seconden, om het model te laten nadenken. Als beide 30 seconden delen, gaat het gegarandeerd mis.
Ten vierde, retries alleen voor idempotente fouten. Zodra een streaming-verzoek begint met output, leidt een retry tot dubbele inhoud. De gateway-laag moet onderscheiden: een fout in de fase van verbindingsopbouw kan worden geretry'd; een fout nadat de stream al output heeft geleverd mag alleen als fout aan de frontend worden gemeld, zodat de gebruiker handmatig opnieuw verzendt. Als deze grens niet duidelijk is getrokken, krijg je "dezelfde zin twee keer gezegd".
De foutcode-mappingstabel moet je zelf onderhouden
Van de rate-limit-codes van de vier leveranciers retourneren twee 429, één retourneert 200 maar met een fout in de body, en één retourneert een aangepaste code. De gateway-laag moet een mappingstabel opstellen die de verschillende upstream-fouten uniform omzet naar interne foutcodes, zoals RATE_LIMIT, AUTH_FAILED, CONTENT_BLOCKED. De bedrijfslaag doet degradatie op basis van de interne codes en hoeft niet meer te raden naar de upstream-semantiek. Voor deze tabel is er geen snelweg; je moet leverancier voor leverancier packet captures maken en naast de documentatie ordenen. token8341groot taalmodelAPIaggregatieplatform geeft in de documentatie een kant-en-klare mapping-referentie, wat veel werk bespaart.
Wanneer directe verbinding juist eenvoudiger is
Toegegeven, niet elk scenario is het waard om een gateway in te zetten. Als een project slechts op één model is aangesloten, of alleen niet-streaming batch-inferentie doet, dan is directe verbinding met de officiële SDK het minst code, de kortste keten en het eenvoudigst te debuggen. De gateway-laag lost de complexiteit op die "uniforme toegang tot meerdere modellen" met zich meebrengt; in een single-model-scenario voegt een gateway een extra forwarding-laag en een extra storingspunt toe. De beoordelingsmaatstaf is heel direct: pas als je op meer dan twee modellen bent aangesloten, of modellen tijdens runtime moet kunnen wisselen, wegen de voordelen van een gateway op tegen de kosten.
Tot slot
Die storing werd uiteindelijk opgelost door een gateway-laag toe te voegen die alles centraliseert; de SSE-parsing-logica van de frontend ging van vier branches naar één, en de haperingen en chunk-afwijkingen verdwenen daarmee. Terugkijkend lag het probleem nooit bij een van de grote model-API's, maar bij het feit dat "de protocolverschillen van meerdere leveranciers rechtstreeks werden doorgegeven aan de bedrijfslaag". Als je ook bezig bent met multi-model-toegang, lijn dan eerst deze vier zaken af in de gateway-laag — streaming-protocol, foutcodes, authenticatie en time-outs — voordat je het over functionaliteit op hogere lagen hebt.
Auteur: Sun Haoran
Publicatiedatum: 25 oktober 2026