Am o regulă pe care o aplic acum la fiecare produs pe care îl construiesc: nicio cheie AI, niciun secret de provider, nicio logică de orchestrare nu ajunge vreodată pe client. Nici în aplicația mobilă, nici în widget-ul din browser, nici într-un build de debug „temporar". Modelul stă în spatele serverului meu, clientul cere lucruri de la serverul meu, iar serverul meu vorbește cu LLM-ul. Granița asta, una singură, decide mai mult despre securitatea, costul și mentenanța unui produs AI decât alegerea modelului în sine.
Nu e o preferință teoretică. E forma spre care au convers trei dintre produsele mele, independent unul de altul: Rețetele Mele, o aplicație de rețete care importă din URL-uri și estimează valori nutriționale cu Gemini; OnboardingVoice, un asistent vocal pe care îl pui în orice SaaS; și Studiază Biblia, un însoțitor de studiu biblic construit în jurul unui motor AI. Domenii diferite, stive diferite, aceeași coloană vertebrală de arhitectură. Articolul ăsta e pattern-ul, cu dovezile concrete din fiecare produs.
Scurgerea pe care n-o mai poți lua înapoi
Să începem cu greșeala pe care lumea o face prima: să livrezi o cheie de provider în client.
Dacă aplicația ta mobilă ține cheia de Gemini, cheia aia poate fi extrasă. Decompilezi APK-ul, asculți traficul, scoți din keychain — efortul variază, dar un secret care ajunge pe un device e un secret care a ieșit de sub controlul tău. La fel e și cu un widget de browser. OnboardingVoice se încarcă în aplicații SaaS terțe, oarecare, printr-un singur script tag. Un widget care ajunge pe site-uri pe care nu le deții tu e cel mai prost loc posibil pentru o cheie de OpenAI, fiindcă acum cheia ta stă în DOM-ul fiecărui site de producție al fiecărui client.
Raza de impact nu e doar o cheie furată. E un atacator care îți rulează modelul plătit pe workload-ul lui, facturat ție, până observi vârful pe factură. E o cheie pe care trebuie să o rotești în toți clienții instalați, ceea ce, pentru o aplicație mobilă, înseamnă un ciclu de release și apoi așteptarea ca utilizatorii să se actualizeze. Iar rotirea unei chei coapte într-o aplicație deja publicată nu e un hotfix; e o migrare.
Cea mai clară decizie de design din Rețetele Mele e exact cea pe care aș repeta-o cuvânt cu cuvânt: „fără chei AI pe device". Faza 10 a acelui build a fost un cutover deliberat care a mutat toate apelurile către Gemini și YouTube pe server. Clientul Android nu știe ce model se cheamă și nici cum arată prompt-ul. Lipește un URL și cere backend-ului să facă treaba. Asta e toată ideea.
Proxy-ul este granița produsului
Odată ce cheile sunt pe server, treaba clientului se reduce la „fac cereri autentificate către API-ul meu". Serverul devine un gateway în fața provider-ului, iar gateway-ul ăsta e locul unde stă tot controlul interesant.
OnboardingVoice e exemplul cel mai pur, pentru că cea mai grea constrângere a lui e vocea în timp real. Face voce-la-voce prin WebRTC, dând proxy peste OpenAI Realtime API. Browser-ul nu ține niciodată o credențială de OpenAI; deschide o sesiune către backend-ul Django, care folosește Channels și WebSockets ca să medieze conexiunea și să paseze evenimentele între browser și model. Clientul primește o sesiune cu durată scurtă și scope limitat, emisă de serverul meu și pe care tot serverul meu o poate revoca. Cheia de provider rămâne într-un singur loc pe care îl controlez. (Ca să fiu corect cu statusul: OnboardingVoice e un prototip care rulează local cap-coadă, iar bucla completă în timp real o tratez ca parțial dovedită, nu testată în luptă. Arhitectura, însă, e exact cea pe care aș păstra-o.)
Pattern-ul de proxy îți cumpără patru lucruri pe care nu le poți avea cu o cheie pe client:
- Autentificare și autorizare — clientul se autentifică la tine, iar tu decizi dacă utilizatorul ăsta are voie la apelul ăsta. Rețetele Mele folosește email plus JWT cu rotație de refresh și Google Sign-In; endpoint-urile de AI stau în spatele acelei autentificări, nu în spatele unui secret comun.
- Cote per utilizator — poți limita un singur utilizator abuziv fără să atingi pe nimeni altcineva. Rețetele Mele aplică quota per utilizator exact în acest punct.
- Un singur loc de rotație — schimbi cheia pe server și fiecare client folosește instant cheia nouă. Fără release de aplicație, fără migrare.
- Observabilitate — fiecare apel trece prin codul tău, deci fiecare apel poate fi logat, măsurat și costat.
Nu bloca request-ul — pune jobul pe coadă
Apelurile AI sunt lente și pică în feluri în care un request HTTP n-ar trebui să pice. O estimare nutrițională sau o extragere de rețetă poate dura secunde; un provider te poate rate-limita sau poate da timeout. Dacă rulezi asta inline într-un request handler, ai legat un răspuns vizibil utilizatorului de un terț imprevizibil.
Așa că a doua jumătate a pattern-ului e: munca AI lungă merge pe o coadă. Rețetele Mele rulează importul de rețete printr-un worker Celery. Lipirea unui URL pornește un job asincron care rulează un cascade de extragere — întâi încearcă date structurate JSON-LD, apoi cade pe parsarea HTML-ului, apoi cade pe extragere cu AI — aterizând undeva în intervalul de 2 până la 10 secunde. Estimarea nutrițională prin Gemini e propriul ei job asincron. Telefonul primește instant un write local în Room și un job care se rezolvă mai târziu; nu stă niciodată pe un spinner așteptând un model.
Celery plus Redis apare în toate cele trei produse din același motiv. Îl folosește Studiază Biblia, îl folosește OnboardingVoice, îl folosește Rețetele Mele. Coada îți dă retry-uri, backpressure și posibilitatea de a re-pune pe coadă un job picat — adminul din Rețetele Mele poate re-rula un job care n-a aterizat. Nimic din toate astea nu e posibil dacă apelul către provider îl face direct clientul.
O gardă care rulează înaintea modelului
Punerea AI-ului pe server înseamnă și că poți pune, în fața modelului, logică la care modelul nu are niciodată drept de vot. Cel mai tăios exemplu pe care l-am construit e în Studiază Biblia.
Însoțitorul lui AI, Parakletos, are o gardă de detectare a crizelor pastorale. O verificare de cuvinte-cheie interceptează semnalele de auto-vătămare înainte de orice apel LLM și scurtcircuitează direct către informații de tip linie de urgență. Modelul nici nu e întrebat. Asta e posibil doar pentru că request-ul curge mai întâi prin stratul meu de orchestrare — există un loc în care să stai între utilizator și LLM. Dacă modelul ar fi pe client, n-ar exista un punct de strangulare de încredere care să impună o regulă de siguranță ca asta. AI server-side nu e doar o postură de securitate; e locul unde etica și gărzile tale chiar apucă să ruleze.
Prompturi versionate și observabilitate
Când prompt-ul stă pe server, devine un artefact pe care îl poți administra, în loc de un string copt într-un build.
Adminul din Rețetele Mele — construit pe django-unfold — expune prompturile AI ca înregistrări editabile, imutabile, versionate, cu comparație de cost și latență între versiuni și cu posibilitatea de rollback. Asta e răsplata centralizării, spusă pe față: pot schimba un prompt fără să livrez o aplicație nouă, pot compara cât costă versiunea 4 față de versiunea 3 și pot reveni dacă un prompt „mai bun" a devenit pe nesimțite mai scump sau mai lent. Prompt-ul e config, nu cod, și e observabil.
Asta e partea pe care echipele o sar și apoi o regretă. Un prompt care trăiește doar în capul tău, sau doar în diff-ul de git de săptămâna trecută, nu îți dă nicio cale să răspunzi la „de ce ne-a sărit factura de tokeni marți?". Versionează fiecare prompt, atașează cost și latență fiecărei versiuni și păstrează-le pe cele vechi. Studiază Biblia se sprijină pe același instinct, arhitectural, împărțind AI-ul în două aplicații Django: ai_assistant se ocupă de persistența conversațiilor, în timp ce ai_orchestrator e motorul care decide ce context recuperează, cum construiește prompt-ul și ce model cheamă. Ținerea orchestrării într-un singur loc cu nume e ceea ce o face observabilă și ușor de schimbat.
Controlul costurilor și gateway-urile multi-provider
Factura e celălalt motiv pentru care ții AI-ul pe server. Fiecare apel prin gateway-ul tău e un apel pe care îl poți număra, atribui unui utilizator și plafona. Cote per utilizator, joburi asincrone pe care le poți limita și versiuni de prompt etichetate cu cost — împreună transformă „n-avem idee cât costă asta" într-un număr pe care îl urmărești.
Gateway-ul e și locul unde stă strategia multi-provider. Studiază Biblia rulează atât pe OpenAI (GPT-4o-mini), cât și pe Anthropic, alese în interiorul orchestratorului. Clientul nu știe și nu-i pasă care provider a răspuns; a cerut serverului meu, iar serverul meu a decis. Decuplarea asta e polița de asigurare pe termen lung. Modelele sunt deprecate, prețurile se schimbă, un provider are o săptămână proastă — și fiindcă rutarea e o decizie server-side, schimbarea e o modificare de config, nu un release de client. (Pentru subiectul mai larg al proiectării unui AI care știe când să se abțină și cum să-și pună porți de calitate, am scris separat în RAG-ul care refuză să halucineze.)
Forma, dintr-o suflare
Redus la esență, pattern-ul e mic:
- Cheile și orchestrarea stau pe server, mereu.
- Clientul face cereri autentificate; serverul dă proxy către provider cu sesiuni de durată scurtă.
- Munca AI lentă rulează pe o coadă (Celery + Redis), cu retry-uri și re-enqueue.
- Gărzile și regulile de siguranță rulează înaintea modelului, în codul tău.
- Prompturile sunt config versionat, cu cost și latență atașate.
- Alegerea provider-ului e o decizie de rutare server-side, nu o dependență de client.
N-am proiectat asta de sus în jos. E ce m-au învățat, cutover după cutover, importul de rețete, onboarding-ul vocal și un însoțitor AI de studiu biblic. Dacă construiești ceva în formă de AI, cea mai ieftină decizie pe care o iei e și cea mai importantă: ține cheile departe de device.
E doar un fir dintr-o poveste mai mare despre cum construiesc 11 produse de unul singur, unde aceleași pattern-uri de backend revin iar și iar. Dacă vrei următorul articol de pattern imediat ce apare, newsletter-ul e locul unde le trimit prima dată.