Dodajte RAG Agentu pomoću SQLite + Embedding, od sada razumeće izvorni kod projekta za sekund
U ovoj epizodi ćemo instalirati RAG Agentu, da Agent može direktno čitati našu kodnu bazu.
Evo konkretnog scenarija, pitam „Kako MemoryManager kompresuje kontekst“. Agent bez RAG može samo da nagađa na osnovu trening podataka, pogodi se — to je sreća.
Nakon instaliranja RAG-a, Agent će prvo otići u kodnu bazu i izvući ContextCompressor.compressIfNeeded, pogleda Map-Reduce implementaciju, zatim odgovori na osnovu ovog pravog koda.
Arhitektura celog RAG modula prikazana je ispod.

01, Ukupni dizajn RAG-a
RAG vam sigurno nije stran, reći ću ga jednom rečenicom.
Vektorizuj bazu znanja i trajno sačuvaj u vektorsku bazu podataka, pri pretrazi pronađi najrelevantnije fragmente po semantičkoj sličnosti, zatim ih zajedno sa pitanjem prosledi LLM-u.
U scenariju koda, tri problema se ne mogu zaobići.
Prvi je kako seći. Kod nije kao dokument, ako sečeš po broju karaktera, dobićeš mnogo šuma. Najsigurnija metoda je seći po strukturnim karakteristikama — na nivou fajla, klase, metoda, pri pretrazi se slažu po granularnosti.
Drugi je gde sačuvati. Produkioni okruženje obično koristi Milvus, Pinecone, ElasticSearch ove specijalizovane vektorske biblioteke. Ali mi smo CLI alat, ovo je suviše teško.
Zato sam ovde izabrao SQLite.
Treći je kako da pretraga bude tačna. Čista vektorska pretraga je prijateljska prirodnom jeziku, ali ne nužno identifikatorima koda.
Zato smo ovde napravili hibridnu pretragu — semantička osnova, dodatna težina ključnim rečima, zatim dodatak po tipu chunk-a. method blok ima viši prioritet od file bloka, jer kada korisnik pita „kako je implementirano“, dati mu telo metoda je mnogo korisnije nego dati mu ceo fajl.
Na primer, pretraži „mesto gde se procesuiraje prijava korisnika“, može da locira LoginService.authenticate.
Celi RAG modul je podeljen na 10 klasa, dole objašnjavam komad po komad.
CodeChunk — model podataka bloka koda
CodeChunker — AST sečenje
EmbeddingClient — klijent za vektorizaciju
VectorStore — SQLite vektorsko skladištenje
CodeAnalyzer — AST analiza relacija
CodeRelation — model relacija
CodeIndex — ulaz indeksa
CodeRetriever — ulaz pretrage
RagQueryTokenizer — tokenizacija upita
SearchResultFormatter — formatiranje rezultata02, AST analiza
Sečenje koda je korak u RAG-u koji se najčešće podcenjuje. Ako dobro sečeš, pretraga je tačna; ako sečeš grubo, kasnije ni koje dodatno težinje ne mogu da spas situaciju.
Java fajlove i ne-Java fajlove treba tretirati odvojeno.
Java ide AST-om, seče se po klasama i metodama; ne-Java (npr. Markdown, yaml) seče se po veličini karaktera, svaki segment se kontroliše ispod 2000 karaktera.

Za Java deo koristim JavaParser, osnovna logika u CodeChunker-u je ovakva:
public List<CodeChunk> chunkFile(Path filePath) throws IOException {
String content = Files.readString(filePath);
// ne-Java fajlovi: segmenti po veličini
if (!relativePath.endsWith(".java")) {
return chunkLargeText(relativePath, content);
}
// Java fajlovi: AST analiza i sečenje
return chunkJavaFile(filePath, content);
}JavaParser može da postavi nivo jezika na JAVA_17, text block, record, sealed class ove nove sintakse se normalno analiziraju.
Ako naiđe na grešku u sintaksi, može automatski da se vrati na sečenje po veličini, neće zbog jednog fajla koji se nije uspeo analizirati izgubiti ceo blok koda.
Kada ne-Java fajlovi premašuju 2000 karaktera, generiše se jedan chunk, uz isto početne i krajnje brojeve redova. Rezultati pretrage mogu direktno da skaču na odgovarajući red, ne treba sekundarno lociranje.
Java strana čuva po jedan primerak na nivou klase i na nivou metoda.
Nivou klase samo čuva deklaraciju klase i prvih 5 redova (polja, potpisi su dovoljni informacije), ne treba da stavlja stotine redova klase; nivou metoda izvlači kompletno telo metoda, poseban blok.
// chunk na nivou klase
chunks.add(CodeChunk.classChunk(
filePath.toString(), className,
classHeader, classStart, classEnd));
// chunk na nivou metoda
chunks.add(CodeChunk.methodChunk(
filePath.toString(),
className + "." + methodSignature,
methodContent, methodStart, methodEnd));CodeChunk koristi record, osim glavnog sadržaja, nosi putanju fajla, tip bloka, naziv, početne i krajnje brojeve redova.
Metod toEmbeddingText će ovo spojiti u [method:Agent.run] public String run(...) ovaj format pa onda računa vektor, da model odmah vidi koja je to klasa i koji metod.
CodeIndex je ulaz celog procesa indeksiranja, obuhvata „obilazak fajlova → sečenje → vektorizacija → persistencija“.
Spolja samo jedna linija codeIndex.index("/path/to/project") može da pokrene.
Obilazak koristi Files.walkFileTree, node_modules, target, .git, build ove direktorijume se preskaču, tipovi fajlova se biraju samo standardne sufikse izvornog koda.
03, Embedding
Nakon sečanja blokova treba generisati vektore.
EmbeddingClient podržava dva načina.
Podrazumevano ide Ollama lokalni model, besplatno, radi i bez interneta, lokalno instaliraj Ollama i povuci nomic-embed-text može da počneš.

Ako mašina ne može, prebaci se na udaljeni API — Zhipu, Ali Qianwen oba imaju Embedging modele.
Prebacivanje ne zahteva promenu koda, dovoljno je podesiti promenljive okruženja:
export EMBEDDING_PROVIDER=ollama
export EMBEDDING_MODEL=nomic-embed-text:latest
export EMBEDDING_BASE_URL=http://localhost:11434Prebacivanje na Zhipu:
export EMBEDDING_PROVIDER=glm
export EMBEDDING_API_KEY=your_key_hereMetod embed distribuira po provider-u — Ollama ide /api/embeddings, OpenAI kompatibilni ide /embeddings, telo zahteva i analiza odgovora su interno obrađeni, spolja samo prosleđuje tekst, uzima vektor.
public float[] embed(String text) throws IOException {
String input = text.length() > MAX_INPUT_CHARS
? text.substring(0, MAX_INPUT_CHARS) : text;
return switch (provider.toLowerCase()) {
case "ollama" -> embedOllama(input);
case "openai", "zhipu", "glm" -> embedOpenAICompatible(input);
default -> embedOllama(input);
};
}MAX_INPUT_CHARS je postavljen na 2000, tekst gust kineski otprilike odgovara 4000~6000 token-a, dati modelu sa 8192 konteksta je više nego dovoljno.
Deo koji prelazi se direktno seče, da API ne bi bacao greške.
Format odgovora dva providera nije isti.
Ollama stavlja vektor u polje embedding, to je niz array; OpenAI kompatibilan format stavlja u data[0].embedding.
U klijentu se unifikovano pretvara u float[], gornji sloj ne mora da zna ko je provider dole.
HTTP timeout je prilično opušten, 30 sekundi za konekciju, 120 sekundi za čitanje. Ollama prvo učitavanje modela je prilično sporo, udaljeni API obično vrati za par sekundi, 120 sekundi pokriva obe strane.
04, Vektorsko skladištenje
Gde čuvati vektore, ovo sam se najduže mučio.
Milvus, Weaviate su suviše teški. Jedan CLI alat, korisnik mora prvo da podigne docker, podesi port, još instalira hrpu SDK-a da bi mogao da radi, ko to može da podnese.
Na kraju sam izabrao SQLite.

Vektor se stavlja u TEXT polje u obliku JSON niza, pri pretrazi se čita ceo u memoriju, red po red se računa kosinusna sličnost, sortira se i uzima TopK.
Mogli biste se zapitati: sve u memoriju, može to da podnese?
Ja sam sam probao — obični lični projekti imaju stotinjak do par hiljada blokova koda, 768-dimenzionalni vektori, 1000 blokova otprilike 3MB memorije, jedno pretraga traje desetak milisekundi.
Ova veličina uopšte ne zahteva specijalizovanu vektorsku biblioteku, kad jednom ne izdrži, onda se može promeniti.
public List<SearchResult> search(float[] queryEmbedding, int topK) throws SQLException {
String sql = "SELECT ... FROM code_chunks WHERE project_path = ?";
List<SearchResult> candidates = new ArrayList<>();
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, projectPath);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
float[] embedding = jsonToEmbedding(rs.getString("embedding_json"));
double similarity = cosineSimilarity(queryEmbedding, embedding);
candidates.add(new SearchResult(..., similarity));
}
}
}
candidates.sort((a, b) -> Double.compare(b.similarity(), a.similarity()));
return candidates.size() > topK
? new ArrayList<>(candidates.subList(0, topK)) : candidates;
}Pored vektorske pretrage, VectorStore je odmah uradio i pretragu ključnim rečima i upit relacionog grafa.
Ključne reči ide kroz LIKE, specijalno za tačno pogadanje naziva klasa/metoda; relacioni graf ima posebnu tabelu, čuva extends, implements, imports, calls, contains ove pet relacija, kasnije pozivni lanac se oslanja na njega.
Batch umetanje ima zaštitu transakcije.
Prvo se isključi autoCommit, na kraju se batch odjednom submit, ako u međuvremenu pukne, rollback.
Indeksi treba da se prave pošteno.
project_path, file_path, chunk_type ove česte dimenzije su pokrivene, from_name i to_name u relacionoj tabeli su takođe dodat. SQLite je lagan, ali bez indeksa na hiljadama redova LIKE može da zakači.
Serijalizacija vektora koristi Jackson, float niz se direktno pretvara u JSON string i stavlja u TEXT polje.
Mogli biste pitati zašto ne koristim BLOB, razlog je jednostavan — JSON je lakši za debugiranje.
Otvoriš alat za vizualizaciju baze podataka i odmah vidiš red po red vektorske vrednosti, za lociranje problema ne treba da pišeš skriptu za deserijalizaciju.
Kosinusna sličnost nije koristila nikakve treće strane biblioteke, ručno napisana petlja, tačkasti proizvod podeljen sa dve module, desetak redova je gotovo.
Fajl baze podataka se podrazumevano stavlja u ~/.paicli/rag/codebase.db, indeksi svih projekata dele ovaj jedan fajl, razlikuju se po project_path.
Umesto jedan po projektu, upravljanje bi bilo komplikovanije. Ako želiš da promeniš lokaciju, dovoljno je dati paicli.rag.dir sistemsko svojstvo.
05, Hibridna pretraga
Čista vektorska pretraga ima jedan stari problem — veoma osetljiva je na sinonime i semantički srodne izraze, ali na tačne identifikatore koda nije nužno tačna.
Na primer, pretraži „run metoda Agent-a“, vektorska pretraga ti može vratiti hrpu blokova sa kontekstom „Agent“, „run“, ali baš onaj metod ne.
CodeRetriever-eva hybridSearch radi tri stvari da nadoknadi ovu manjkavost.

Prva, semantička pretraga kao osnova. Vektorizuje upit, računa kosinusnu sličnost sa svim vektorima u bazi, prvo izvlači najsemantički relevantne.
Druga, dodata težina ključnim rečima. Upit seče koristeći jieba, izdvaja „Agent“, „run“, „ReAct“ ove ključne reči koda, zatim ide kroz biblioteku sa LIKE-om.
Pogođeni rezultati dobijaju različite bodovanja po gde su pogođeni — naziv klase/metoda +0.3, putanja fajla +0.1, sadržaj +0.1. Što je ključnija pozicija gde je pogođeno, bodovi su teži, slično kao BM25 dodavanje težine u ES-u, samo je lakše.
Treća, dodatak po tipu. method blok +0.15, class blok +0.1, file blok ne dobija. Razlog je jednostavan, kada korisnik pita „kako je implementirano“, dati telo metoda je mnogo korisnije nego dati ceo fajl.
// dodatak po tipu koda
double typeBoost = switch (r.chunkType()) {
case "method" -> 0.15;
case "class" -> 0.10;
default -> 0.0;
};RagQueryTokenizer takođe zaslužuje da se spomene dve reči.
On koristi jieba za kinesku segmentaciju, uz to čuva ASCII identifikatore (engleski u nazivima klasa, metoda).
Nakon segmentacije se filtriraju jednokarakterne i „kako“, „kako bi“, „malo“ ove beskorisne stop reči.
Na taj način, „kako se procesuiraje prijava korisnika“ ovakvi prirodnojezički upiti, „ReAct“, „Agent“, „MemoryManager“ ovi čisti engleski identifikatori se takođe zadržavaju.
Još jedan mali mehanizam se zove nagrada za dvostruki pogodak.
Ako isti blok pogode i semantička pretraga i pretraga ključnim rečima, dodatno +0.1. Ekvivalentno više dimenzija međusobno potvrđuju, naravno da bodovi trebaju da budu jedan nivo viši. Ova nagrada se daje samo jednom, ne akumulira se, da određeni blok ne bi dominirao tabelom zato što je pogodio više ključnih reči.
Konačno, još jedan deduplikacija po istom fajlu, svaki fajlo najviše zadržava 2 reda. Inače ako naiđeš na posebno veliki fajl, može da zauzme celu stranicu rezultata, diversity se gubi.
private List<SearchResult> limitPerFile(List<SearchResult> sorted, int topK, int maxPerFile) {
List<SearchResult> result = new ArrayList<>();
Map<String, Integer> fileCount = new HashMap<>();
for (SearchResult r : sorted) {
int count = fileCount.getOrDefault(r.filePath(), 0);
if (count < maxPerFile) {
result.add(r);
fileCount.put(r.filePath(), count + 1);
if (result.size() >= topK) break;
}
}
return result;
}06, Relacioni graf koda
Pronaći blok koda je samo prvi korak.
Da bi zaista razumeo jedan projekat, mora da zna „ova klasa koga nasleđuje, koje interfejse implementira, njene metode koga pozivaju“.
CodeAnalyzer koristi JavaParser za AST obilazak, pet relacija se procesira zajedno: extends (nasleđivanje klasa), implements (implementacija interfejsa), imports (zavisnost uvoza, samo ne-JDK), contains (klasa sadrži metode), calls (poziv metoda, pojednostavljena verzija beleži samo naziv metoda).
private void extractClassRelations(String filePath, CompilationUnit cu, List<CodeRelation> relations) {
cu.findAll(ClassOrInterfaceDeclaration.class).forEach(clazz -> {
String className = clazz.getNameAsString();
// extends relacija
clazz.getExtendedTypes().forEach(ext -> {
relations.add(new CodeRelation(
filePath, className, null, ext.getNameAsString(), "extends"));
});
// contains relacija: klasa sadrži metode
clazz.getMethods().forEach(method -> {
relations.add(new CodeRelation(
filePath, className, filePath,
className + "." + method.getNameAsString(), "contains"));
});
// calls relacija: pozivi metoda
clazz.findAll(MethodCallExpr.class).forEach(call -> {
Optional<MethodDeclaration> parentMethod = findParentMethod(call);
if (parentMethod.isPresent()) {
String caller = className + "." + parentMethod.get().getNameAsString();
relations.add(new CodeRelation(
filePath, caller, null, call.getNameAsString(), "calls"));
}
});
});
}Kod imports dela je dodata filtracija, beleže se samo oni koji ne počinju sa java. i javax..
calls deo je malo teži.
Kada JavaParser naiđe na MethodCallExpr čvor, mora da se vrati uz rekurziju da pronađe kojoj metodi pripada. U CodeAnalyzer-u sam napisao findParentMethod, penje se uz AST roditeljske čvorove, kad naiđe na MethodDeclaration staje.
Bez ovog koraka, samo znaš „neko je pozvao chat“, ali ne možeš da kažeš da li je Agent.run pozvao ili PlanExecuteAgent.executeTask.
Izvučene relacije se sve stavljaju u SQLite tabelu code_relations. Kroz CLI komandu /graph može da se pretražuje. Na primer /graph Agent:
Agent ├── contains --> Agent.run
Agent └── extends --> BaseAgent
Agent.run ├── calls --> chat
Agent.run ├── calls --> executeToolJednim pogledom razumeš: Agent nasleđuje BaseAgent, ima run metod, run interno poziva chat i executeTool.

07, Integrisanje u Agent
RAG modul je napisan, ali Agent neće sam od sebe da ga koristi.
U ToolRegistry-u treba registrovati search_code alat, reci LLM-u — za pitanja vezana za kodnu bazu, pozovi ovaj.
tools.put("search_code", new Tool(
"search_code",
"Semantička pretraga kodne baze, pronalazi relevantne blokove koda na osnovu opisa prirodnim jezikom",
createParameters(
new Param("query", "string", "Opis upita prirodnim jezikom, npr.'implementacija prijave korisnika'", true),
new Param("top_k", "integer", "Broj rezultata koje treba vratiti (podrazumevano 5)", false)
),
args -> {
String query = args.get("query");
int topK = ...;
try (CodeRetriever retriever = new CodeRetriever(projectPath)) {
List<VectorStore.SearchResult> results = retriever.hybridSearch(query, topK);
return SearchResultFormatter.formatForTool(query, results);
}
}
));U isto vreme ažurirao sam i sistemski prompt Agent-a, eksplicitno sam rekao LLM-u:
Ako korisnik postavlja pitanja vezana za kodnu bazu (npr. „Šta ovaj klasa radi“, „Gde se koristi određena funkcija“),
molim te da prvo koristiš search_code alat da pronađeš relevantni kod, zatim odgovori na osnovu rezultata pretrage.Ovde postoji jedan detalj gde je lako napastiti — projectPath u ToolRegistry-u podrazumevano uzima user.dir, ali korisnik je možda koristio /index da indeksira drugi direktorijum.
Ako se ne sinhronizuje, alat i dalje pretražuje staru putanju, rezultati su prazni. Zato u Main-u nakon što se /index izvrši, odmah se sinhronizuje indeksirana putanja sa ToolRegistry-om, osigurava se da se dve strane poklapaju.
SearchResultFormatter organizuje rezultate pretrage u oblik čovek može da čita.
Ima dva izlazna moda: formatForCli se koristi za komandnu liniju, sa emoji-om i uvlačenjem; formatForTool se koristi za LLM, kompaktniji, uključuje rezime pretrage plus fragmente koda sa brojevima redova.
U rezimeu će se reći modelu „koji je najrelevantniji ulaz“, „rezultati su koncentrisani na koje fajlove“, „koje ključne reči su korišćene za sortiranje“, da LLM može brzo da proceni koje redove treba da gleda.
PlanExecute Agent-ov izvršni prompt je takođe dodatno vođen, koraci u planiranju zadacâ koji uključuju razumevanje koda će automatski pokrenuti pretragu. Na taj način, bilo ReAct ili Plan-and-Execute, oba moda mogu da razumeju kodnu bazu.
08, CLI praksa
RUP ovu hrpu funkcija konačno se predaju korisniku kroz tri CLI komande.
Prva je /index, pravi indeks za kodnu bazu. Pri izvršenju obilazi projektni direktorijum (node_modules, target, .git ove se automatski preskaču), svaki fajl seče, vektorizuje, stavlja u SQLite.

Druga je /search, prirodnojezična pretraga koda. Ne treba pamćenje naziva klasa, naziva fajlova, slično kao pitanje kolegi.

Treća je /graph, proverava relacioni graf klasa ili metoda.
👤 Ti: /graph Agent
🕸️ Pretraga relacija klasa: Agent
📋 Pronađeno 5 relacija:
Agent ├── contains --> Agent.run
Agent ├── contains --> Agent.clearHistory
Agent └── extends --> BaseAgent
Agent.run ├── calls --> chat
Agent.run ├── calls --> executeTool/index takođe podržava navođenje putanje, npr. /index /Users/xxx/my-project, može indeksirati kodnu bazu bilo kog direktorijuma.
Proces indeksiranja svakih 10 fajlova jednom štampa napredak, ako jedan fajl ne uspe da se parsira samo se upozorava, ne prekida ceo proces.
Konačno se ispiše jedan red statistike — koliko blokova koda, koliko relacija, jednim pogledom znaš kvalitet ovog indeksiranja.
Šta ako se kodna baza ažurira?
Ponovo izvrši /index.
CodeIndex će prvo očistiti stare podatke zatim upisati nove, osigurava se da vektorska baza i kodna baza uvek budu poravnate, neće se desiti „kod je već izmenjen, pretraga i dalje vraća staru verziju“ ovakve natprirodne fenomene.
/search ako nije napravljen indeks prijateljski će te upozoriti „Kodna baza nije indeksirana, molim te prvo koristi /index komandu“, neće direktno baciti exception u lice. Ako se proces pretrage desi greška takođe će se uhvatiti exception i logovati, CLI neće odmah pasti.
Svaka dnevna upotreba je uglavnom: prvo jednom /index napravi indeks, uobičajeno ako imaš pitanje /search prirodnojezično pretražiš, ako želiš da vidiš arhitekturu /graph proveri relacije. Agent mod je jednostavniji, pitanja direktno baci njemu, iza automatski poziva search_code, čak ni /search ne mora ručno da kucaš.
pom.xml-u ovoj epizodi su dodata tri zavisnosti: sqlite-jdbc za vektorsku persistenciju, javaparser-core za AST analizu, jieba-analysis za kinesku segmentaciju. Plus jackson-databind i okhttp koje su već postojale u ranijim epizodama, ceo RAG modul spoljne zavisnosti je kontrolišu ispod 5, ostaje konzistentan sa prethodnom laganom pozicijom.
ending
Nakon četiri epizode, PaiCLI se od jednog ReAct Agent-a koji samo korak po korak hoda, postepeno evoluirao u alat koji može da planira, pamti, čak i čita kodnu bazu.
Sav kod je otvorenog koda na GitHub-u, četvrta epizoda je dodala 10 RAG-related klasa, ukupna količina koda dosegla je oko 1700 redova.

Prati tutorial jednom, lokalno možeš da pokreneš sopstveni Agent CLI.
Ako primetiš da pretraga nije tačna, verovatno je problem u granularnosti indeksa ili segmentaciji upita. Možeš prvo pokušati da podesiš veličinu MAX_CHUNK_CHARS, ili dodaš nekoliko stop reči vezanih za biznis u RagQueryTokenizer, u većini scenā će biti efikasno.
Sledeća epizoda — Multi-Agent saradnja.
Jedan Agent ne može da se snađe, napravićemo tim, oni koji planiraju planiraju, oni koji izvršavaju izvršavaju, oni koji pregledavaju pregledavaju, podelimo rad.
Omotnica za životopis: Projekat PaiCLI
Naziv projekta: PaiCLI — Alat otvorenog koda Java Agent CLI
Opis projekta: Izgraditi alat Agent komandne linije sličan Claude Code od nule, podržava ReAct zaključivanje, Plan-and-Execute planiranje zadataka, Memory sistem memorije, RAG pretragu kodne baze.
Tehnološki stack: Java 17, JavaParser, SQLite, Jieba segmentacija, OkHttp, Jackson, JUnit 5, JLine3
Ključne odgovornosti:
- Na osnovu JavaParser AST-a implementirati višenivojsko sečenje koda (fajl/klasa/metoda), ne-Java fajlove segmentira po veličini, značajno poboljšanje recall-a pretrage
- Unificirano pakovanje lokalnih Ollama modela i OpenAI kompatibilnih udaljenih API-ja, glatko promeniti provider putem promenljivih okruženja, podržava automatsko secanje teksta da bi se sprečilo prekoračenje API limita
- Na osnovu SQLite-a implementirati lako vektorsko skladištenje, vektori se persistiraju u JSON niz, računanje kosinusne sličnosti u memoriji, pretraga koda hiljade redova na projektu traje < 100ms
- Implementirati strategiju hibridne pretrage: semantička pretraga kao osnova + jieba segmentacija+dodata težina + dodatak po tipu koda + ograničenje po fajlu, tačnost Top5 dostiže proizvodni nivo
- Na osnovu AST-a izvuci relacioni graf koda (extends/implements/imports/calls/contains), podržava pretragu pozivnog lanca klasa kroz prirodni jezik
- RAG upakovati kao search_code alat i registrovati u Agent alate, kroz LLM sistemski prompt voditi automatsko pokretanje pretrage, ReAct i Plan-and-Execute oba moda podržavaju razumevanje kodne baze
