Nu este o noutate pentru nimeni că dezvoltarea software a fost schimbată permanent de instrumentele de IA pentru generarea de cod. Sunt, fără exagerare, cel mai mare salt de productivitate pe care l-am văzut în cariera mea și cred cu tărie că pot fi folosite în avantajul nostru. Dar tocmai pentru că sunt atât de eficiente, viteza pe care o oferă poate scăpa ușor de sub control.
Ceea ce am început să remarc este o schimbare subtilă în modul nostru de lucru când începem să integrăm aceste instrumente foarte puternice fără să înțelegem cum să le folosim și fără o strategie clară. Ne concentrăm tot mai mult pe fișiere izolate în loc să urmărim fluxuri end-to-end, începem fiecare implementare cu o generare de cod în loc să schițăm o soluție și amânăm deciziile de design, preferând să facem totul din mers.
Din experiența proprie, vă pot spune că efectul devine cel mai vizibil într-un sistem construit de la zero cu IA. Cerințele de la început sunt simple și vin una câte una. Doar că totul se implementează mult prea rapid, făcând dificilă anticiparea modului în care vor interacționa componentele și tipurile de edge case-uri care vor apărea dincolo de fluxurile inițiale. Rezultatul este că fiecare piesă pare corectă individual, dar sistemul pierde din coerență la nivel de ansamblu. Controlul se poate recupera, dar cu cât se acumulează mai multe straturi de inconsistență, cu atât costul crește.
Vestea bună este că aceleași tooluri care accelerează implementarea pot fi folosite și pentru a preveni problemele dacă știm cum să le ghidăm. Pentru a explora acest fenomen, mi-am structurat observațiile într-un scenariu cu trei acte: accelerarea, inconsistența și recuperarea controlului. Povestea este inspirată din experiențe reale, dar construită într-o formă fictivă, simplificată și exagerată pe alocuri pentru claritate.
Acestea fiind spuse, vă rog să vă imaginați următorul cadru:
Personajele:
Echipa: dezvoltatori software, cu acces pentru prima dată la cele mai bune tooluri de IA disponibile;
IA-ul: un partener de implementare rapid, care face exact ce i se cere;
Designul: absent din distribuție, nu apare în primele două acte.
Decorul: Un proiect nou. La început: un frontend și un backend. Pe parcurs, backendul inițial evoluează într-un microserviciu de tip orchestrator când se adaugă un nou microserviciu downstream.
Se ridică cortina.
La început, totul era simplu. Un frontend, un backend, având comunicare directă între ele. Cerințele vin vagi, cum vin la începutul oricărui proiect, dar echipa tocmai a descoperit un tool de generare de cod și este fascinant. Este promițător. Cerințele de început prind culoare extrem de repede: un formular, un endpoint, un interceptor care prinde erorile și le afișează într-un pop-up. Elegant, simplu și funcțional. În câteva minute, totul este cod funcțional.
Apoi arhitectura crește. Apare un microserviciu specializat: downstreamul. Backendul inițial devine un microserviciu de tip orchestrator: primește cereri de la frontend, apelează downstreamul, agregă răspunsurile. Acum sunt două microservicii, fiecare cu propriile endpointuri, propriul model de erori, propria structură de răspuns.
Cu timpul, cerințele devin mai complexe și fiecare serviciu ajunge să funcționeze după propriile sale reguli. Frontendul are interceptorul lui, logica lui de retry și de afișare a erorilor. Orchestratorul are retry-ul lui, cache-ul lui, modul lui de a traduce erorile. Noul microserviciu are structura lui de răspuns, timeout-urile lui. Toate sunt generate corect, fiecare în izolarea propriului fișier. Dar nimeni nu a scris: "Așa arată un flux de eroare end-to-end." Nimeni nu a decis: "Retry-ul se face într-un singur loc." Nimeni nu a stabilit: "Formatul de eroare este acesta, peste tot."
Ceea ce se întâmplă este că, atunci când folosim AI pentru implementare, fără o viziune de ansamblu, modul de lucru devine un prompt izolat într-un fișier. Această granularitate se mapează perfect pe taskuri izolate, dar prost pe responsabilități care nu aparțin unui singur fișier: error handling, retry policies, caching strategies. Sunt exact tipul de decizii pe care arhitectura software le numește cross-cutting concerns, iar un astfel de prompt cu un context limitat nu le poate rezolva. IA-ul a răspuns exact la ce i s-a cerut, dar problema este că nu i s-a cerut niciodată coerență la nivel de sistem.
Problemele apar una câte una, deghizate în buguri banale. Și exact cum a venit implementarea, așa vin de obicei și fixurile: un fișier, un context parțial, o soluție locală.
Primul semnal: pop-upul de eroare apare gol sau cu un mesaj generic. Echipa investighează. Orchestratorul propagă erorile de la downstream, dar cele două microservicii folosesc structuri diferite. Fiecare format era rezonabil în izolare. Nimeni nu verificase că sunt compatibile, pentru că nimeni nu documentase un contract comun. Se aliniază, se fixează structura și merge. Dar fixul deschide următorul strat. Acum că toate erorile ajung corect în frontend, se descoperă că retry-urile din frontend nu s-au executat niciodată pentru că interceptorul, extins de mai multe ori ca răspuns la cerințe punctuale, re-arunca anumite erori fără să păstreze structura originală. După ce se fixează interceptorul, brusc retry-urile pornesc toate, și cele din frontend, și cele din orchestrator, iar cu un singur click sistemul generează 9, 12 requesturi către downstream.
Și tot așa vă puteți imagina cum continuă povestea: dezactivezi retry-ul din frontend și descoperi că orchestratorul păstrează în cache erori vechi. Fixezi cache-ul și descoperi că timeouturile nu sunt corelate. Fiecare strat corectat expune un strat pe care nu-l știai. Fiecare investigație durează.
Cum de nimeni nu a observat mai devreme?
Cercetătorii numesc asta "automation complacency", adică tendința de a reduce vigilența când un sistem automatizat pare să funcționeze. Nu s-a verificat fluxul end-to-end, nu din neglijență, ci pentru că fiecare piesă individuală trecuse peste reviewuri ca fiind corectă. Riscul crește când cei care folosesc toolul nu au încă experiența necesară pentru a-i evalua critic outputul. Un dezvoltator experimentat poate recunoaște că o soluție este corectă local dar problematică în context. Unul la început de drum, confruntat cu un tool care pare atât de sigur pe sine, pierde discernământul. Iar odată ce problemele încep să apară, investigația devine tot mai adâncă.
Rabbit hole-ul nu are fund, pentru că nu mai trebuie să depanezi un bug, ci trebuie să depanezi lipsa unui design. Un LLM generează cod pe baza unei ferestre de context limitate, fundamental diferită de înțelegerea unui sistem. Studiile recente arată că există un decalaj semnificativ între ce așteaptă dezvoltatorii de la codul generat de IA și ce primes. Aceleași inconsistențe ar fi putut apărea și fără IA. Diferența e că IA-ul comprimă timeline-ul atât de mult încât straturile se acumulează înainte ca cineva să le observe.
Ceea ce am descris în primele două acte se poate întâmpla oricui. Nu este o poveste despre o echipă care a greșit, ci este despre un mod de lucru care devine natural când metrica cea mai importantă devine viteza de implementare, fără o pauză deliberată pentru design și revizuire. Cu fiecare zi care trece, codul acceptat fără o decizie conștientă devine mai greu de gestionat. Nu pentru că cineva a uitat rațiunea din spate, ci pentru că nu a existat niciodată una. Sistemul crește pe fundații pe care nimeni nu le-a înțeles și validat corespunzător.
Tehnicile care urmează sunt cele pe care echipa din povestea noastră le-a adoptat ca să recupereze controlul și să folosească instrumentele în avantajul lor. Dar nu trebuie să aștepți să te lovești de un zid ca să le aplici. Folosește-le când începi o implementare nouă a unui feature complex, când adaugi ceva pe un flow existent și de fiecare dată când o schimbare traversează mai mult de un serviciu sau mai mult de un layer.
Înainte de a deschide un prompt, oprește-te și privește sistemul ca pe un întreg. Stabilește fluxul complet al unui request: tot ce ar trebui să se întâmple de la clickul utilizatorului până la răspunsul afișat pe ecran. Nu codul, nu fișierele, ci fluxul de date. Din acest flux extrage contractele explicite: un singur format de eroare, peste tot. Retry doar într-un singur loc. Nu sunt decizii de implementare, sunt decizii de arhitectură. Spre exemplu dacă folosești Github Copilot, scrie-le în fișiere de tip copilot instructions sau custom instructions care descriu regulile transversale ale proiectului. Acestea devin contextul de bază pe care IA-ul îl primește la fiecare interacțiune, indiferent de prompt, indiferent de cine promptează.
Cea mai concretă lecție este, totuși, că niciun prompt nu ar trebui să existe în vid. Designul stabilit în pasul anterior trebuie reîntărit în fiecare prompt, la fiecare interacțiune cu IA-ul. Modul de promptare ar trebui schimbat de la orientat pe simptome la orientat pe specificații.
Symptom-driven:
"Pop-up-ul de eroare apare gol. Repară interceptorul."
Specification-driven:
"Refactorizează interceptorul conform contractului comun de eroare: obiect cu câmpurile message, code, source. Nu prinde erori care nu respectă schema ApiError, ci lasă-le să se propage."
Diferența nu este doar de formulare ci și de mentalitate. Un astfel de model de promptare constrânge outputul IA-ului la spațiul de design definit de om și elimină cel mai periculos mod de eșec, anume IA-ul rezolvând o problemă diferită de cea reală.
Înainte de a executa, revizuiește. Copilot oferă moduri de planificare care permit vizualizarea schimbărilor propuse înainte de generarea codului. Dacă planul include retry într-un serviciu frontend, iar specificația spune că retry-urile aparțin orchestratorului, dezalinierea este vizibilă înainte de a scrie o linie de cod.
Folosește acest pas și pentru verificarea coerenței: "Iată contractul de eroare. Iată interceptorul din frontend. Iată handlerul din orchestrator. Sunt consistente? Ce scenarii ar putea produce un comportament neașteptat?" IA-ul este excelent la acest tip de analiză, atâta timp cât îi dai toate piesele.
Planul arată intenția, dar codul generat trebuie verificat și în execuție. Testele unitare, de integrare și end-to-end sunt unele dintre cele mai eficiente metode de a valida codul generat de IA. Aceasta le poate genera rapid, dar aici apare aceeași capcană: dacă testele sunt generate pe baza implementării, ele validează ce face codul, nu ceea ce ar trebui să facă. Testele nu trebuie neapărat să vină înainte de implementare, dar principiul din Test-Driven Development rămâne valabil: testele trebuie scrise pornind de la specificații și contracte. Nevoia de design nu este eliminată, iar testele scrise corect devin gardieni ai deciziilor luate conștient.
Instrumentele de IA sunt cel mai bun partener de implementare pe care l-am avut vreodată. Dar implementarea nu este dezvoltare software, aceasta reprezintă doar o parte dintr-un proces mult mai vast. Deciziile de design, contractele între componente, responsabilitățile per layer rămân în continuare responsabilitatea noastră. Și va rămâne, indiferent cât de capabile devin toolurile.
Lecția pe care am extras-o nu este spectaculoasă. Este, de fapt, un principiu vechi reformulat pentru un context nou: planifică înainte de a construi. Desenează fluxul înainte de a deschide fișierul. Stabilește contractele înainte de a scrie promptul. Spune-i IA-ului toate regulile jocului înainte de a-l pune să joace.
[1] R. Parasuraman and D. Manzey, "Complacency and bias in human use of automation," 2010.
[2] P. Vaithilingam, Z. Tianyi and E. Glassman, "Expectation vs. Experience: Evaluating the Usability of Code Generation Tools Powered by Large Language Models.," in CHI '22, 2022.
[3] "Adding repository custom instructions for GitHub Copilot," [Online]. Available: https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/add-custom-instructions/add-repository-instructions.
[4] "Best practices for GitHub Copilot CLI," [Online]. Available: https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-best-practices.