RAG peste codul sursă: ce face codul, nu de ce e așa

Codul nu minte despre ce face și tace complet despre motiv. Cum legi implementarea de commituri, review-uri și decizii printr-un lanț de identificatori.

Andrei Badulescu
Andrei Badulescu25 iulie 2026 · 16 min citit
Ilustrație abstractă: un fragment de cod legat de mesajul de commit, de discuția de review și de decizia care l-a produs

Cineva întreabă de ce serviciul de plăți reîncearcă exact de trei ori și de ce a doua încercare așteaptă opt secunde. Sistemul răspunde cu funcția. Codul e corect, e curent, se execută în producție chiar în clipa asta.

Și nu răspunde la întrebare.

Numărul trei nu e o preferință de stil. E rezultatul unui incident de acum doi ani, al unei discuții de pe un pull request și al unui compromis negociat cu procesatorul de plăți — dintre care niciunul nu apare în fișier.

Celelalte corpusuri din serie mint într-un fel sau altul: câmpurile îmbătrânesc, procedurile rămân în urmă, documentația descrie un sistem schimbat între timp. Codul sursă e singurul care nu poate minți despre ce face, fiindcă se execută, iar execuția e verificabilă. E și singurul care tace complet despre motiv. Mecanismul de bază rămâne neschimbat; ce se schimbă e că răspunsul util aproape niciodată nu stă în corpusul care pare evident.

Corpusul autoritativ care nu conține motivul

Ce e verificabil mecanic și ce nu

Într-o arhivă de documente, „adevărat” înseamnă „cineva a scris asta și nimeni nu a contrazis-o”. Într-un repository, o funcție ori compilează, ori nu; ori trece testele, ori nu; ori e apelată la rulare, ori nu. Ai un oracol pe care niciun alt corpus intern nu ți-l dă.

Acoperă însă o singură clasă de întrebări: ce se întâmplă. „De ce” și „cine a decis” nu au reprezentare în sintaxă. Un model întrebat de ce pragul e trei va produce o justificare plauzibilă, construită din numele variabilei. Nu e o halucinație clasică — fragmentul citat e real și corect. Doar că nu conține răspunsul.

„De ce” stă în trei corpusuri diferite

Motivul se împarte, previzibil, în trei locuri:

  • Mesajul de commit — de ce s-a făcut schimbarea, în cel mai bun caz. În cel mai frecvent caz, „fix” sau „update deps”.
  • Discuția de pe pull request — obiecțiile, alternativele respinse, condiția pusă de cel care a aprobat. Cel mai dens strat de raționament dintr-o echipă, și singurul în care apare explicit ce nu s-a făcut.
  • Documentul de decizie arhitecturală — contextul, opțiunile, consecințele acceptate. Când există.

Nu sunt fațete ale aceluiași document, cum erau câmpurile, proza și istoricul unui ticket. Sunt patru sisteme separate, cu drepturi de acces separate și cicluri de viață separate: codul trăiește în repository, discuția în platforma de găzduire, decizia într-un fișier Markdown pe care nimeni nu-l mai actualizează.

Legătura e un identificator, nu o suprapunere de cuvinte

Aici e diferența de arhitectură. Cele patru corpusuri nu se leagă prin similaritate — vocabularul lor abia se suprapune. Un mesaj de commit spune „reduce retry budget after incident 4412”; funcția conține MAX_ATTEMPTS = 3. Zero cuvinte comune, aceeași decizie.

Ce le leagă e un lanț de identificatori: hash-ul commitului apare în blame, numărul pull requestului în mesajul de merge, numărul incidentului în descrierea PR-ului, iar calea fișierului leagă totul de codul curent. Joncțiunea se rezolvă la ingestie, prin traversarea lanțului, nu la interogare prin similaritate.

Practic: nu construi un index peste „codul plus istoricul”. Construiește un index peste cod, un index peste raționament și un tabel de corespondență între ele.

Cod sursă, mesaj de commit, discuție de pe pull request și decizie arhitecturală, legate printr-un lanț de identificatori

Chunking-ul pe caractere rupe o structură care există deja

Frontiera sintactică e marcată de autor

Majoritatea corpusurilor cer să ghicești unde se termină o unitate de sens. Codul e inversul: are un arbore sintactic complet, derivabil determinist cu un parser, în care fiecare funcție, clasă și bloc are frontiere exacte.

A tăia la 800 de tokeni peste această structură înseamnă a arunca informația și a o înlocui cu o aproximare. Strategiile generale de chunking discută compromisul dintre dimensiune și context; pe cod, compromisul dispare, fiindcă frontiera corectă e deja scrisă în fișier.

Ce se pierde la o tăietură arbitrară

Trei lucruri, toate tăcute:

  • Semnătura se desparte de corp. Un chunk care începe la mijlocul unei funcții conține logică fără nume, fără parametri și fără tipuri. Regăsit, e ilizibil pentru model și pentru om.
  • Contextul de încapsulare dispare. O metodă ruptă de clasa care o conține pierde numele clasei, câmpurile la care se referă și, adesea, singura indicație despre domeniu.
  • Importurile rămân în alt chunk. Fragmentul folosește un simbol pe care nu-l poți rezolva: nu se vede din ce bibliotecă vine, deci nu se vede dacă e apelul standard sau wrapperul intern cu comportament diferit.

Unitatea corectă e funcția sau metoda, cu semnătura, cu antetul clasei și cu lista importurilor relevante atașate ca preambul. Fișierele scurte rămân întregi. Funcțiile foarte lungi se împart pe blocuri de nivel superior, nu pe număr de caractere.

Vecinătatea utilă e în graf, nu în text

Două funcții care se apelează una pe alta pot să nu aibă niciun cuvânt comun. Două funcții cu nume aproape identice, în module diferite, pot să nu aibă nicio legătură.

De aceea metadatele care contează nu sunt de conținut, ci de relație: cine apelează funcția, pe cine apelează ea, unde e definit fiecare simbol folosit. Un pas de expansiune pe graful de apel, aplicat după regăsirea inițială, aduce contextul de care are nevoie răspunsul — și îl aduce corect, nu prin similaritate.

Segmentare pe număr de caractere față de segmentare pe frontieră sintactică, cu semnătura și importurile păstrate

Ce măsoară de fapt benchmarkurile de căutare în cod

Înainte să alegi un model de embeddings pentru cod, merită să știi ce anume s-a măsurat ca să obții cifra din tabelul de comparație.

Docstringul folosit ca interogare de probă

CodeSearchNet, setul care a definit domeniul, e construit din aproximativ două milioane de perechi comentariu–funcție extrase din depozite publice, în șase limbaje. Interogarea de antrenare e docstringul funcției. Autorii semnalează ei înșiși limita: documentația e scrisă de aceeași persoană, în același moment cu codul, deci folosește același vocabular — spre deosebire de o interogare reală.

Pentru evaluare au construit un set separat, cu 99 de interogări în limbaj natural adnotate de experți. Detaliul care contează pentru tine e cum au fost alese: au pornit din interogări reale de căutare și au eliminat manual pe cele care erau clar cuvinte-cheie tehnice — exemplul dat de autori e numele exact al unei funcții. Adică au scos din setul de evaluare exact tipul de interogare care domină căutarea într-un repository intern.

Interogarea scurtă prăbușește tot

CoIR, benchmarkul de referință apărut ulterior, adună zece seturi de date, opt sarcini de regăsire și paisprezece limbaje de programare, cu peste două milioane de documente în corpus. Un audit publicat în 2026 de o echipă de la Ant Group, odată cu benchmarkul propriu CoREB, îi găsește însă probleme structurale: CodeSearchNet și derivatele sale reprezintă peste 85% din volumul corpusului CoIR și au servit ani la rând drept date de antrenare pentru modelele evaluate; toate cele zece seturi atribuie exact un singur document relevant per interogare, cu scor binar și fără exemple negative dificile, ceea ce reduce metricile de ordonare la un test de tip nimerit sau nu.

Rezultatul empiric care schimbă cel mai mult felul în care citești un tabel de benchmark: pe subsarcina cu interogări scurte, de tip cuvinte-cheie — aproximativ 19 tokeni, formatul cel mai apropiat de căutarea reală a unui developer — toate cele unsprezece modele evaluate coboară la nDCG@10 aproape zero, cu două ordine de mărime sub performanța pe interogări lungi. Autorii notează că nici scalarea modelului, nici adăugarea unui reranker nu închid diferența.

Citește-o ca pe o constrângere de proiectare, nu ca pe un verdict. Regimul în care modelele funcționează bine e cel al interogărilor lungi și descriptive. Regimul în care lucrează oamenii tăi e celălalt.

Ce rămâne util

Rămân bune la ordonarea relativă a modelelor pe sarcini de tip descriere lungă către cod. Nu sunt o predicție a comportamentului pe repository-ul tău, unde interogările sunt scurte, limbajul e adesea unul singur și vocabularul e intern.

Concluzia e aceeași cu cea din hybrid search și reranking, doar mai apăsată: pe cod, componenta lexicală nu e o rafinare. Un simbol e un șir exact, iar potrivirea exactă pe simbol e cea mai frecventă interogare din sistem.

Blame-ul e o capcană, nu un răspuns

Ultimul care a atins linia a rulat un formatter

Reflexul e să folosești git blame ca să afli cine a scris o linie. Într-un repository cu vechime, răspunsul e adesea persoana care a rulat un formatter pe toată baza de cod, într-un singur commit care a atins fiecare fișier.

Git are un mecanism pentru asta din versiunea 2.23: un fișier cu hash-urile commiturilor de reformatare, de obicei .git-blame-ignore-revs, plus configurarea blame.ignoreRevsFile. Blame-ul sare peste ele și arată ultima modificare cu sens.

Pentru ingestie, asta înseamnă un pas obligatoriu: dacă derivi semnale din blame fără lista de commituri ignorate, semnalele tale descriu istoria formatării, nu istoria deciziilor.

Ce expun uneltele de istoric

Datele există și sunt accesibile local, fără niciun serviciu:

1# autor, commit și moment pentru fiecare linie, în format parsabil
2git blame --line-porcelain -w -C -C path/to/file.py
3
4# istoricul unei singure funcții, nu al fișierului
5git log -L :process_payment:path/to/file.py
6
7# istoricul unui interval de linii
8git log -L 120,168:path/to/file.py
1# autor, commit și moment pentru fiecare linie, în format parsabil
2git blame --line-porcelain -w -C -C path/to/file.py
3
4# istoricul unei singure funcții, nu al fișierului
5git log -L :process_payment:path/to/file.py
6
7# istoricul unui interval de linii
8git log -L 120,168:path/to/file.py

Opțiunea -w ignoră modificările de spațiere, iar -C repetat urmărește liniile mutate sau copiate din alte fișiere — două surse frecvente de atribuire greșită. git log -L acceptă și un nume de funcție în locul intervalului de linii, deci îți dă traiectoria unei unități sintactice. E lent pe istorii lungi, fiindcă citește diferența la fiecare commit: se rulează la ingestie, nu la interogare.

„Cine știe codul ăsta” nu se citește din blame

Întrebarea pusă cel mai des despre un fragment necunoscut nu e cine l-a scris, ci pe cine întrebi acum. Sunt lucruri diferite, iar blame-ul răspunde doar la prima, și aceea prost.

Semnale mai bune, în ordinea raportului cost-beneficiu:

  • Proprietatea declarată — fișierul de proprietari din repository, dacă e întreținut. E o declarație, deci îmbătrânește; verifică dacă echipa mai există.
  • Numărul de review-uri, nu de commituri. Cine a aprobat schimbări pe fișierul ăsta în ultimul an cunoaște codul mai bine decât cine a atins ultima linie.
  • Dispersia autorilor. Un fișier cu un singur autor activ e un risc de continuitate; unul cu doisprezece autori și niciun recenzent constant e altceva.

Cunoașterea care nu se recuperează din niciun sistem — de ce s-a abandonat abordarea alternativă — se comportă la fel ca memoria tehnică din cercetare și dezvoltare: valoroasă, rar scrisă, și pierdută odată cu oamenii.

Codul mort arată identic cu codul viu

Patru feluri de cod care nu se execută

Pentru un model de embeddings, toate arată la fel ca restul: aceeași sintaxă, aceleași denumiri, aceeași densitate.

  • Cod comentat, lăsat „pentru orice eventualitate” acum trei ani.
  • Funcții fără apelanți, rămase după o refactorizare care nu a curățat.
  • Ramuri închise de un feature flag care e false în toate mediile de doi ani.
  • Cod de test și fixtures, care implementează deliberat comportamente pe care producția nu le are.

Un răspuns care citează oricare dintre ele e plauzibil, citat corect și complet greșit. Diferența față de o simplă căutare ratată e că aici cineva copiază un tipar retras.

Semnalul e execuția, nu textul

Testul care separă viul de mort nu se citește din fișier. Se citește din altă parte:

  • Acoperirea de test spune ce linii se execută măcar în integrare.
  • Telemetria de producție — urme distribuite, profilare continuă, jurnale de apel — spune ce se execută cu trafic real.
  • Graful de apel static spune ce e teoretic accesibil, ceea ce e un prag inferior util, nu o dovadă.
  • Starea configurațiilor de feature flag pe fiecare mediu spune care ramură e vie azi.

Niciunul nu e perfect. Combinația a două dintre ele e suficientă ca să marchezi un chunk drept probabil neexecutat.

Ce excluzi și ce marchezi

Excluzi la ingestie fișierele generate, dependențele vendorizate și artefactele de build: volum mare, repetitiv, care domină vecinătatea oricărei interogări.

Marchezi, nu excluzi, codul de test, cel aparent fără apelanți și ramurile închise. Un chunk marcat „fără apelanți detectați, ultima modificare acum 3 ani” e un rezultat utilizabil. Același text, prezentat curat, e o capcană — iar regulile de format ale răspunsului sunt în cum arată un răspuns bun de RAG.

Patru categorii de cod care nu se execută niciodată și semnalele externe de execuție care le separă de codul viu

Indexul nu e repository-ul

Ce rămâne pe dinafară, tăcut

Dacă te bazezi pe căutarea de cod a platformei ca strat de regăsire, limitele publicate contează, fiindcă niciuna nu produce eroare — produce absență.

Documentația GitHub pentru căutarea de cod enumeră, la momentul scrierii: codul vendorizat și cel generat sunt excluse; fișierele goale și cele peste 350 KiB sunt excluse; liniile peste 1.024 de caractere sunt trunchiate; fișierele cu mai mult de o linie peste 4.096 de octeți sunt excluse; doar fișierele codificate UTF-8 intră; doar ramura implicită e indexată; interogarea e limitată la 1.000 de caractere, iar rezultatele la 100 per căutare.

Două surprind echipele. Detectarea codului vendorizat e euristică, pe baza căii: directoare numite external sau third_party sunt excluse chiar dacă sunt cod propriu. Iar limita de 100 de rezultate face imposibilă orice sarcină de tip inventar complet.

Pentru un index propriu, limitele dispar, dar apare cealaltă problemă: codul se schimbă de mai multe ori pe zi. Mecanica e cea din prospețime, ștergeri și reindexare, cu o simplificare — ai deja un jurnal exact al modificărilor, deci reindexarea incrementală se face pe diferența dintre două commituri, nu prin rescanare.

Ce întrebare, ce mecanism

Ce se întreabăUnde stă răspunsulCe iese greșit dacă cauți doar în cod
Ce face funcția astaretrieval pe frontieră sintacticăde obicei corect
De ce pragul e 3 și nu 5mesaj de commit, discuție de PR, document de decizieo justificare inventată din numele variabilei
Unde e definit simbolul Xindex lexical, potrivire exactăo funcție cu nume asemănător din alt modul
Se mai folosește funcția astagraf de apel plus telemetrie de execuțiecod mort, citat ca API curent
Ce s-a schimbat aici și cândgit log -L pe unitatea sintacticăun rezumat al stării curente, fără istorie
Pe cine întreb despre modulul ăstaproprietate declarată plus istoric de reviewautorul commitului de reformatare

Un singur rând e un caz curat de regăsire semantică. Restul cer fie un index lexical, fie o traversare de graf, fie un apel către alt sistem — iar pragurile de la care nimic din toate astea nu se justifică sunt în când nu folosești RAG.

Măsurare și pilot

Setul de referință: decizii deja luate

Adevărul de referință există deja în repository, sub forma schimbărilor care au fost anulate. Ia douăzeci de commituri de tip revert, sau douăzeci de pull requesturi în care recenzentul a cerut o schimbare de abordare.

Pentru fiecare, formulează întrebarea așa cum ar fi pus-o cineva chiar înainte de a scrie codul greșit — „de ce nu procedăm așa aici” — și verifică dacă sistemul ar fi adus discuția, incidentul sau documentul de decizie care conțineau răspunsul. Un sistem care citează codul curent, corect, fără raționamentul care l-a produs, se numără ca eșec.

Patru indicatori

Acuratețea rutării. Din întrebări, în câte a ales sistemul mecanismul potrivit — potrivire exactă, regăsire semantică, traversare de graf sau lanțul către istoric. Dacă e slabă, restul se măsoară degeaba.

Rata de cod neexecutabil. Din fragmentele returnate ca exemplu de urmat, în câte codul era comentat, fără apelanți sau într-o ramură închisă. Ar trebui să fie zero prin construcție.

Acoperirea motivului. Din întrebările de tip „de ce”, în câte a ajuns răspunsul la o sursă de raționament, nu doar la implementare.

Precizia pe simbol. Din interogările care conțin un identificator exact, în câte definiția lui a fost pe primul loc. E metrica pe care benchmarkurile publice nu o măsoară și utilizatorii tăi o observă imediat.

Cadrul general rămâne cel din cum evaluezi un sistem RAG; ce se adaugă e că două din patru măsoară infrastructura din jurul codului, nu regăsirea în el.

Pilotul: un serviciu, doi ani de istoric

Un singur serviciu, cu limite clare și cu istoric suficient cât deciziile să fi avut timp să fie uitate. Extrage întâi lanțul de identificatori — commit către pull request către incident — și numără câte commituri ajung la o discuție cu conținut real. Dacă mesajele sunt „fix” și PR-urile trec fără comentarii, corpusul de raționament nu există, iar proiectul e unul de proces, nu de regăsire.

Livrabilul primelor șase săptămâni e o cifră: din patruzeci de întrebări reale ale echipei, în câte a ajuns răspunsul și la cod, și la motivul lui. Sub cincisprezece, problema e la lanțul de identificatori, nu la model. Restul artefactelor operaționale — proceduri, runbook-uri, jurnale de incident — au altă constrângere dominantă și sunt tratate în RAG pentru IT și operațiuni.

Întrebări frecvente

Pot folosi un model general de embeddings sau am nevoie de unul specializat pe cod?

Pentru regăsire cod-la-cod, diferența e mare și consecventă în evaluările publice: modele mici antrenate pe cod bat modele generale de câteva ori mai mari. Pe întrebări în limbaj natural, diferența se micșorează. Testează cu interogările tale scurte, nu cu cele lungi din benchmark.

Cum tratez un monorepo cu cinci limbaje?

Parsere separate pentru segmentare, index comun pentru regăsire, limbajul ca metadată filtrabilă. Segmentarea e specifică limbajului, deci nu poți avea un singur segmentator; regăsirea nu are motiv să fie separată, iar interogările traversează frecvent limbajele.

Merită să indexez și istoricul complet, sau doar starea curentă?

Doar starea curentă pentru cod, plus istoricul deciziilor ca strat separat. Un index peste toate versiunile fiecărui fișier îți dă un corpus în care aceeași funcție apare de patruzeci de ori și fiecare interogare returnează versiuni vechi ale aceluiași lucru.

Cum împiedic scurgerea de secrete în răspunsuri?

Prin scanare la ingestie, nu prin instrucțiuni în prompt. Chei și șiruri de conexiune rămân în istoric chiar după ce au fost șterse din starea curentă. Rulează un detector de secrete peste ce indexezi și tratează permisiunile ca în controlul accesului în RAG.

Ce fac dacă mesajele de commit sunt inutile?

Le tratezi ca pe un semnal absent și treci la stratul următor. Discuția de pe pull request supraviețuiește adesea unei culturi slabe de commituri, fiindcă e scrisă pentru un cititor prezent. Dacă nici acolo nu e nimic, pilotul ți-a dat deja un rezultat util: motivul nu e scris nicăieri, iar asta se rezolvă cu o convenție, nu cu un index. Există și corpusuri cu problema inversă, în care motivul e consemnat de două ori, în versiuni care se contrazic — cazul dosarelor de instanță, tratat în RAG pe dosarul de litigiu.


Începe cu lanțul de identificatori, nu cu codul. Ia un singur serviciu și numără câte commituri din ultimul an duc la o discuție cu conținut real. Cifra aceea îți spune, înainte de orice decizie de arhitectură, dacă ai un corpus de raționament sau doar un corpus de implementare.

Dacă vrei să verifici ce s-ar putea răspunde efectiv din repository-ul echipei tale, trimite un mesaj. Restul cazurilor pe corpusuri de companie sunt în hubul Cazuri de utilizare enterprise, sursele de tip documentație tehnică au propriul hub, iar imaginea completă a serviciului e în AI & RAG.

Surse

  1. CoIR: A Comprehensive Benchmark for Code Information Retrieval ModelsAssociation for Computational Linguistics, 2025
  2. CodeSearchNet Challenge: Evaluating the State of Semantic Code SearcharXiv, 2019
  3. Beyond Retrieval: A Multitask Benchmark and Model for Code SearcharXiv, 2026
  4. About GitHub Code SearchGitHub Docs, 2026
  5. git-log DocumentationGit, 2026
Distribuie
Andrei Badulescu
Despre autor

Andrei Badulescu

Fondator & Software Architect

Construiește sisteme B2B la BaseTech — ERP la comandă, platforme SaaS, agenți AI și arhitecturi programmatic SEO. Scrie despre deciziile tehnice din spatele lor: stack, trade-off-uri și ce ține la scară.

Vezi profilul autorului →
Continuă
Newsletter

Insights pentru companii
care construiesc

Articole noi despre ERP, AI, agenți și pSEO, direct pe email. Fără spam.