- Pradžia
- Atvejų analizės
- Kelių tiekėjų DI vartai su integruota sąnaudų apskaita
Kelių tiekėjų DI vartai su integruota sąnaudų apskaita
Kelių DI modelių tiekėjų sutelkimas už vienų vartų, kad produktai gautų atsarginius perjungimus, ribas, srautą ir gyvą sąnaudų apskaitą be tiekėjų chaoso.
Paskelbta 2026-06-30Peržiūrėta 2026-07-02
Trumpai
- Problema
- Tiesioginiai modelių tiekėjų kvietimai išbarstė SDK ypatumus, greičio ribas, gedimus ir kainodarą po funkcijų kodą.
- Apribojimas
- Vartai turėjo palaikyti ir paketinius, ir srautinius kvietėjus, o sąnaudas, perjungimą ir tiekėjo pasirinkimą laikyti vienoje vietoje.
- Sukurta sistema
- Vienodas adapterių sluoksnis su maršrutizatoriaus lygiagretumo ribomis, tiekėjų sveikatos perjungimu, async REST, srautiniu gRPC ir sesijos, dienos bei bendromis metrikomis.
- Rezultatas
- Modelio pasirinkimas tapo konfigūracija, nauji tiekėjai paveldi bendras kontroles, o išlaidos matomos dar užklausoms vykstant.
- Kur tai pritaikoma
- Tinka DI produktams, kuriems reikia kelių tiekėjų atsparumo ar sąnaudų kontrolės prieš LLM naudojimui išsibarstant po visas komandas.
- Technologijos
- Go adapters · gRPC / protobuf · persisted in-memory queue
Kodėl tai svarbu
Jei jūsų komanda planuoja sistemą su tais pačiais gedimo scenarijais — būtent nuo tokio mąstymo prasideda pirmas pokalbis.


Apžvalga
Pirmiau aprašytas apribojimas ir yra visa užduotis: augantis produktas kreipiasi į kelis DI modelių tiekėjus, kiekvienas su savo SDK, savo greičio ribomis, savo gedimų būdais ir savo kainodara — ir nėra nė vienos vietos, kuri atsakytų, kiek kainavo užklausa, kuris tiekėjas ją aptarnavo, ar kas nutiks, jei tas tiekėjas sustos. Vieniems kvietėjams tereikia atsakymo, kai darbas baigtas; kitiems reikia, kad ženklai tekėtų naudotojui realiu laiku, tuo pat metu kai jie generuojami. Kas bebūtų priešais tiekėjus, turi aptarnauti abu tipus, netapdamas dviem atskiromis sistemomis, ir turi laikyti lygiagretumo ribas, perjungimo elgseną ir sąnaudų apskaitą vienodas, nepriklausomai nuo to, kuriuo keliu užklausa nueina. Vieni vartai, daug tiekėjų, jokio pakartotinio šių dalykų įgyvendinimo kiekvienoje funkcijoje.
Adapterio sutartis: viena sąsaja, daug tiekėjų
Kiekvienas tiekėjo adapteris vartuose įgyvendina tą pačią sąsają: gavęs užklausą — modelio pavadinimą, žinutes, parametrus — jis arba grąžina atsakymą, arba jį transliuoja srautu, ir praneša, kiek užklausa kainavo bei kiek laiko ji truko. Niekas virš šios sąsajos nežino ir nesirūpina, kuris komercinis API iš tikrųjų aptarnavo konkretų kvietimą; maršrutizatorius, apskaita ir perjungimo logika parašyti vieną kartą, prieš sąsają, o ne prieš konkretaus tiekėjo SDK.
type ProviderAdapter interface {
Complete(ctx context.Context, req Request) (Response, error)
Stream(ctx context.Context, req Request) (<-chan Chunk, error)
HealthCheck(ctx context.Context) error
}
Sutartis, kurią tenkina kiekvienas tiekėjo adapteris
Naujo adapterio rašymas reiškia įgyvendinti šiuos tris metodus prieš konkretaus tiekėjo tikrąjį SDK, paverčiant jo užklausos ir atsakymo formas, klaidų kodus ir naudojimo ataskaitų laukus į vartų nuosavus tipus. Viskas žemiau to — maršrutizavimas, ribos, perjungimas, sąnaudų sumavimas — jau egzistuoja ir nesikeičia.
Kodėl nekviesti kiekvieno tiekėjo SDK tiesiai iš funkcijų kodo
Alternatyva — tai, nuo ko pradeda dauguma komandų: kiekviena funkcija importuoja tą tiekėjo SDK, kurio jai reikia, ir kviečia jį tiesiogiai. Tai veikia, kol atsiranda antras tiekėjas, o paskui trečias, ir kiekviena tų kvietimo vietų turi atskirai apdoroti kitokį greičio ribos antraštės formatą, kitokią pakartojimo politiką, kitokį naudojimo ženklų pranešimo būdą ir kitokią klaidos formą. Padauginus tai iš to, kiek funkcijų kreipiasi į modelį, būtent SDK ypatumai — ne produkto logika — galiausiai pasikartoja visame kode.
Dar blogiau — nė vienas iš šių sprendimų nesuderinamas tarpusavyje. Vienoje funkcijos kvietimo vietoje pridėta lygiagretumo riba negalioja kitai funkcijai, kviečiančiai tą patį tiekėją iš kitur. Vienam integravimui pridėtas perjungimas nepadeda kitam, patyrusiam tą pačią avariją. O sąnaudos, išbarstytos po tiek kvietimo vietų, kiek yra funkcijų, gali būti atkurtos tik vėliau — iš žurnalų, iš tiekėjų prietaisų skydelių, iš kartą parašyto scenarijaus, kuris pasensta, vos tik nauja funkcija pradeda kviesti modelį tiesiogiai. Sutelkimas už vienos adapterio sutarties nėra estetinis pasirinkimas — tai vienintelis būdas, kuriuo bet kuris iš šių dalykų užtikrinamas vieną kartą, o ne perdaromas kiekvienam kvietėjui atskirai.
Lygiagretumo ribos ir išsaugoma eilė
Kiekvienas tiekėjas riboja, kiek lygiagretaus srauto priims konkrečiam modeliui, ir ta riba priklauso tiekėjo paskyrai, o ne kuriai nors vienai ją kviečiančiai funkcijai. Jei kiekviena funkcija savo ribą taikytų nepriklausomai, bendra visų suma vis tiek galėtų viršyti tai, ką leidžia tiekėjas — atskiros ribos būtų teisingos kiekviena atskirai, bet kartu klaidingos. Vietoj to šią atsakomybę turi maršrutizatorius: lygiagretumo ribos konfigūruojamos kiekvienam modeliui vienoje vietoje ir taikomos bendram visų kvietėjų srautui, o ne bet kurio vieno kvietėjo požiūriui.
Užklausos, atėjusios, kai modelis jau pasiekė lygiagretumo ribą, nesugriūva iškart — jos stoja į eilę. Eilė laikoma atmintyje dėl greičio, bet yra išsaugoma, todėl maršrutizatoriaus perkrovimas tyliai nepraranda jau priimto darbo; užklausa, kurią vartai patvirtino priėmę, yra užklausa, už kurios galiausiai užbaigimą ar aiškų atmetimą vartai atsako, o ne tokia, kuri tiesiog dingsta, jei procesas, laikęs ją atmintyje, netikėtai persikrauna. Vos atsilaisvinus vietai — tiekėjo lizdui atsilaisvinus, kai baigiasi ankstesnė užklausa — maršrutizatorius paima kitą eilėje laukiančią užklausą ir ją išsiunčia, iš eilės, be kvietėjo poreikio apklausinėti ar kartoti bandymą.
Čia taip pat sugeriamos, o ne perduodamos toliau, paties tiekėjo greičio ribos. Kvietėjas, kuris kitu atveju matytų tiekėjo greičio ribos klaidą, vietoj to mato, kaip jo užklausa trumpai palaukia ir tada įvykdoma — riba yra tikra, bet ją valdyti — maršrutizatoriaus, o ne kiekvienos funkcijos, kuriai reikėtų pačiai tai gaudyti ir kartoti, darbas.
Tiekėjo sveikata ir automatinis perjungimas
Lygiagretumo ribos daro prielaidą, kad tiekėjas už jų yra sveikas; jos niekuo nepadeda, jei tiekėjas veikia, bet suprastėjęs — padidėjęs klaidų dažnis, į srautinį kvietėją nebetelpanti delsa, arba visiška avarija. Maršrutizatorius seka kiekvieno tiekėjo sveikatą pagal jau tekančių per jį užklausų rezultatus: kylantis klaidų dažnis ar keli laiko limito viršijimai iš eilės pažymi tiekėją kaip suprastėjusį, ir naujos to modelio užklausos nukreipiamos į sveiką alternatyvą, be jokio kvietėjo pusėje esančio kodo, sprendžiančio kartoti bandymą su kitu tiekėju.
Schema: tiekėjų maršrutizavimas — užklausa į vartus patenka arba per async REST, arba per srautinį gRPC kelią, maršrutizatorius pritaiko kiekvieno modelio lygiagretumo ribas, patikrina tiekėjo sveikatą ir nukreipia į sveiką tiekėją (automatiškai perjungdamas, jei pagrindinis suprastėjęs), atsakymas srautu ar iškart grįžta kvietėjui, o užklausa visą laiką matuojama pagal sesijos, dienos ir bendrus sąnaudų skaitiklius.
Perjungimas čia veikia tik todėl, kad po juo yra adapterio sutartis: kadangi kiekvienas tiekėjo adapteris turi tuos pačius Complete/Stream metodus, maršrutizatorius gali pakeisti vieną tiekėją kitu, ir nei kvietėjui, nei likusiai vartų daliai nereikia žinoti, kad pakeitimas įvyko.
Toks pakeitimas nematomas sąmoningai — kvietėjas prašė modelio, o ne konkrečios tiekėjo paskyros, ir jam iš tikrųjų reikėjo atsakymo iš sveiko tiekėjo.
Dvigubas transportas: asinchroninis REST ir srautinis gRPC
Ne kiekvienas kvietėjas nori to paties atgal. Paketinė užduotis, per naktį vertinanti kelis tūkstančius įrašų, nori pateikti užklausą ir vėliau pasiimti rezultatą — jai nereikia laikyti atviro ryšio tas sekundes ar minutes, kurių gali prireikti dideliam darbui, ir ji verčiau apklaus dėl rezultato, nei laikys atvirą lizdą. Interaktyvi funkcija, transliuojanti atsakymą į pokalbių sąsają, nori priešingo: pirmojo ženklo iškart, kai tik jis atsiranda, o ne viso atsakymo, sukaupto buferyje ir grąžinto iš karto. Priversti bet kurį kvietėją naudoti kito transportą — blogas sprendimas viena ar kita kryptimi: apklausa dėl rezultato, kuris galėjo tekėti srautu, prideda naudotojo jaučiamą delsą, o atviro srauto laikymas paketiniam darbui, kuriam jo nereikia, be reikalo eikvoja ryšį.
Vartai suteikia abu: asinchroninį REST galinį tašką paketiniams ir „nusiųsk ir pamiršk“ kvietėjams bei srautinį gRPC galinį tašką, apibrėžtą per protobuf, interaktyviems kvietėjams, norintiems ženklų tuo pat metu, kai jie generuojami.
service Gateway {
rpc Complete(CompletionRequest) returns (CompletionResponse);
rpc Stream(CompletionRequest) returns (stream CompletionChunk);
}
Viena schema, du būdai naudoti tą pačią užklausą
Abu transportai baigiasi tame pačiame maršrutizatoriuje, tose pačiose lygiagretumo ribose, toje pačioje perjungimo logikoje ir toje pačioje apskaitoje — kvietėjo pasirinktas transportas keičia, kaip pristatomas atsakymas, o ne tai, kuris tiekėjas jį aptarnauja, kiek jis kainuoja ar kas nutinka, jei tas tiekėjas pasirodo esąs nesveikas. Vartai, kurie vietoj to REST ir gRPC vykdytų kaip atskirus diegimus, ar dar blogiau — atskirus kodo bazes, turėtų sinchronizuoti po dvi kiekvieno šių dalykų versijas; čia yra lygiai viena.
Gyva sąnaudų apskaita
Kiekvienas tiekėjo adapteris praneša, kiek iš tikrųjų kainavo baigta užklausa — ženklų skaičių ir jam pritaikytą kainodarą — o maršrutizatorius tą skaičių sumuoja tą pačią akimirką, kai užklausa baigiasi, o ne pagal tvarkaraštį. Tas skaičius vienu metu prisideda prie trijų skaitiklių: sesijos, kuriai priklauso užklausa, kalendorinės dienos, kurią ji įvyko, ir bendros sumos. Nė vienas iš jų neapskaičiuojamas vėliau iš žurnalų — jie padidinami tame pačiame kodo kelyje, kuris grąžina atsakymą, todėl skaičius, kurį mato kvietėjas ar prietaisų skydelis, niekada nėra atsilikęs daugiau nei viena jau baigta užklausa nuo tikrovės.
Tai, kad apskaita yra maršrutizatoriaus, o ne kiekvienos funkcijos atsakomybė, ir daro sumavimą patikimą. Jei kiekviena funkcija praneštų savo pačios išlaidas, ta ataskaita būtų tik tokia pilna, kokia yra kiekvieno inžinieriaus, kada nors pridėjusio kvietimo vietą, drausmė — praleidus vieną kvietimą, bendra suma tyliai sumažėtų. Kadangi kiekviena užklausa jau turi pereiti per maršrutizatorių, kad iš viso pasiektų tiekėją, sąnaudų apskaita jai nėra pasirenkama ir negali būti netyčia praleista naujos funkcijos.
Kompromisai ir apribojimai
Nė vienas iš šių dalykų nėra nemokamas, o sąžiningumas dėl to, ko šis sprendimas neišsprendžia, yra projekto dalis:
- Vienoda adapterio sutartis gali atskleisti tik tai, kas bendra visiems tiekėjams; tiekėjui būdinga galimybė, netelpanti į bendrą sąsają, arba paliekama nuošalyje, arba priverčia pačią sutartį augti, o tada kiekvienas kitas adapteris turi bent toleruoti naują metodą.
- Perjungimas pakeičia, kuris tiekėjas iš tikrųjų aptarnauja užklausą, o skirtingi tiekėjai gali duoti skirtingas atsakymo savybes — kitokią frazuotę, kitokią delsą, kitokią elgseną prompto ribose — net kai abu, maršrutizatoriaus požiūriu, „sveiki“; tam jautrus kvietėjas neturi jokio matomumo, kada perjungimas įvyko sesijos viduryje.
- Lygiagretumo ribos yra tik tokios tikslios, kokia yra jų konfigūracija, o tikrosios tiekėjo ribos ir kainodara keičiasi tiekėjo, ne vartų, tvarkaraščiu — kažkam reikia nuolat atnaujinti maršrutizatoriaus kiekvieno modelio konfigūraciją pagal pasikeitusias sąlygas, kitaip ribos arba be reikalo stabdo, arba nustoja saugoję paskyrą.
- Sutelkta apskaita reiškia, kad verslo pasitikimas sąnaudų skaičius turi lygiai vieną šaltinį; klaida toje vienoje vietoje — klaidingai praneštas ženklų skaičius iš tiekėjo, sumavimo klaida — būna klaidinga visur vienu metu, ir nebelieka nepriklausomo, kiekvienos funkcijos skaičiaus, su kuriuo palyginti.
- Išsaugoma eilė sušvelnina laikiną tiekėjo perkrovą, bet nesukuria pajėgumo, kurio nėra; eilė, kuri vis auga, nes visi to modelio tiekėjai vienu metu ribojami, tik atideda gedimą, o ne jo išvengia.
- Iš tikrųjų nauja tiekėjo galimybė — ne ypatumas, o galimybė, kurios šiandien neturi nė vienas tiekėjas — vis tiek reiškia pačios adapterio sutarties plėtimą, kol dar joks kvietėjas negali ja naudotis; adapterio parašymas nuveda tik tiek toli, kiek jau siekia pati sąsaja.
Šaltiniai ir tolesnis skaitymas
Šie principai nėra originalūs — jie gerai įsitvirtinę, ir verta juos paskaityti originalo forma:
Susijusios atvejų analizės
Sistemos, kurias su šia sieja bendras apribojimas — pakartojimai, izoliacija, kaina, atkūrimas.
Evoliucinis užklausų (prompt) optimizavimas
Užklausų kandidatus galima lyginti su atidėtais pavyzdžiais, todėl tobulinimas tampa matuojamas, o ne anekdotinis.
BLEURT · LLM APIs · evolutionary operators · NumPyIlgai trunkančios inferencijos orkestravimas be serverių veikiančiuose GPU
Trumpalaikiai atsijungimai nebežudo naudingo darbo, o baigtos ar paliktos užduotys paleidžia GPU išteklius vietoj brangiai kabančių darbininkų.
RunPod · ComfyUI · WebSockets · S3 offload