Analiza pitanja sa intervjua za PaiSmart RAG: prava pitanja iz prvog kruga Tencent QQ
Sigurno ste i sami primetili da se ova godina mnogo razlikuje od prošle — bilo da idete na intervju za Java back-end ili za drugu poziciju, AI je svuda prisutan.
U nastavku delim neka stvarna iskustva sa intervjua, koja mogu poslužiti kao referenca onima koji jure letnju praksu u velikim kompanijama.

Tekst je prilično gust — vežite pojaseve, krećemo.
Ispitivanje projekta
01. Imate 5 minuta, opišite ukupan tok RAG projekta i navedite 1–2 teške tačke dizajna
Ukupan tok PaiSmart-a je ovakav: nakon što korisnik otpremi dokument, sistem prvo vrši sečenje dokumenta na delove, dugački dokument se seče na nekoliko chunk-ova, za svaki chunk generiše se odgovarajući vektor (embedding), a zatim se zajedno sa originalnim tekstom skladišti u ElasticSearch.
Kada korisnik postavi pitanje, sistem pitanje pretvara u vektor, u ES-u izvršava hibridnu pretragu (vektorska pretraga + BM25 pretraga ključnih reči), prisjeća relevantne chunk-ove i ubacuje ih u prompt, predaje ih velikom modelu na generisanje odgovora, a zatim preko WebSocket-a strujno vraća front-end-u.

Dve vredne teške tačke:
Prva je strategija sečenja na delove. Sečenje dokumenata se ne radi nasumice — ako se seče prekratko, gubi se kontekst, ako predugo, prekoračuje se ograničenje token-a embedding modela i razvodnjava se semantika.
PaiSmart koristi sečenje na fiksnu veličinu uz overlap (preklapanje), čime se osigurava da susedni chunk-ovi imaju izvesno preklapanje sadržaja i izbegava se slučaj da ključna informacija završi tačno na granici sečenja.
Druga je upravljanje kontekstom razgovora. Kod višekrugnog razgovora istoriju treba uneti u prompt, ali kako kontekst raste, prekoračuje se context window velikog modela. Kako zadržati dovoljno konteksta, a istovremeno kontrolisati broj token-a, vrlo je praktičan inženjerski problem u RAG projektima.
02. Osim sečenja na delove pri otpremanju, da li imate i nastavak od tačke prekida?
Jezgro nastavka od tačke prekida je: veliku datoteku iseći na više delova (chunk), svaki deo pojedinačno otpremiti, nakon uspešnog otpremanja zabeležiti status tog dela, a ako se otpremanje prekine, sledeći put otpremiti samo preostale delove i na kraju ih na serveru spojiti.

PaiSmart koristi Redis Bitmap za čuvanje statusa otpremanja delova. Svaki deo odgovara jednom bit-u u Bitmap-u — uspešno otpremanje se postavlja na 1, neposlato je 0.
Pre nego što front-end pokrene otpremanje, prvo proveri Bitmap, pa zna koji su delovi već otpremljeni, a koji nisu, i šalje samo ono što nedostaje.
Zašto ovakav dizajn?
Jer u stvarnim scenarijima, kada korisnik otpremi veliki dokument (npr. PDF, Word), mrežna treperenja ili osvežavanje stranice mogu prekinuti otpremanje. Ako svaki put krene iz početka, korisničko iskustvo je loše, a i resursi servera se troše uzalud.

Nastavak od tačke prekida, uz Bitmap za praćenje napretka, je jeftin, jednostavan za implementaciju i vrlo efikasan.
Zanimljivo je da se operacija spajanja obavlja u MinIO-u, nakon što se svi delovi otpreme.
Nakon spajanja, PaiSmart odmah pokreće naknadni proces analize dokumenta i upis delova u bazu — čitav lanac je asinhron.
03. Pošto ste koristili Bitmap za čuvanje indeksa delova, kako je Bitmap implementiran u Redis-u na donjem nivou? Koja su još scenariji upotrebe?
Redis Bitmap je suštinski omotač oko tipa String.
String se u Redis-u čuva kao niz bajtova, a operacije Bitmap-a (SETBIT, GETBIT, BITCOUNT itd.) zapravo su operacije nad tim nizom bajtova na nivou bit-a. Svaki bajt ima 8 bit-a, jedan String od 512 MB može da čuva 2^32 bit-a, odnosno oko 4,2 milijarde pozicija — prostorna efikasnost je izuzetno visoka.
Konkretan primer: izvršavanje SETBIT mykey 7 1 postavlja sedmi bit niza bajtova ispod ključa mykey na 1. Na donjem nivou to je jedna prosta operacija nad bit-om.

Uobičajeni scenariji upotrebe Bitmap-a:
- Prijava korisnika (check-in): userId kao offset, svaki dan jedan ključ, prijava je SETBIT, a za statistiku dana prijave u mesecu koristi se BITCOUNT.
- Statistika aktivnih korisnika: beleži se svakog dana koji su korisnici aktivni; presek za više dana (BITOP AND) daje kontinualno aktivne korisnike.
- Bloom filter: iako Redis od 4.0 ima nativni Bloom Filter, samoimplementacija preko Bitmap-a je takođe čest pristup.
- Napredak otpremanja delova: upravo ono što PaiSmart radi — beleži se koji su delovi otpremljeni, čime se omogućava nastavak od tačke prekida.
04. Hibridna pretraga koristi embedding model + IK analyzer, zašto takav dizajn?
Prednost vektorske pretrage je semantičko poklapanje — pronalazi sadržaj „sličnog značenja", ali je kod preciznog poklapanja ključnih reči nestabilna, posebno kod proprijetarnih imena, naziva proizvoda, skraćenica — embedding model ih ne mora dobro uhvatiti.

BM25 je model statistike učestalosti reči — gleda samo da li se reč pojavi i koliko puta, uopšte ne razume semantiku, pa će za pitanje „kako poboljšati performanse sistema" i za pitanje „kako optimizovati efikasnost sistema" BM25 verovatno misliti da su to dve potpuno različite stvari.
Kombinacijom obe, nadopunjuju se. Vektorska pretraga zadužena je za semantičko prisećanje, IK analyzer uz BM25 za precizno poklapanje ključnih reči, a na kraju se pomoću RRF (Reciprocal Rank Fusion) ili ponderisanja spajaju oba toka rezultata i dobija konačni redosled.
IK analyzer je alatka za segmentaciju reči optimizovana za kineski, specijalno obrađuje problem segmentacije kineskih reči (kiniski nema prirodne razmake, pa direktno sečenje po znaku ili korišćenje engleskog analyzer-a daje loše rezultate).
Nakon segmentacije IK analyzer-om, „pretraživanje pojačano generisanjem" seče se na „pretraživanje" „pojačano" „generisanjem", i BM25 onda radi normalno.
05. Šta su algoritmi BM25 i KNN i kako se konkretno implementiraju?
BM25 (Best Matching 25) je klasični algoritam iz oblasti pronalaženja informacija, poboljšana verzija TF-IDF-a.
Njegova jezgro misao je: što se neka reč češće pojavljuje u dokumentu (učestalost reči TF), to je važnija za taj dokument; ali ako se ta reč javlja svuda u svim dokumentima (niska inverzna učestalost dokumenta IDF), njena težina se umanjuje.
BM25 na to dodaje normalizaciju dužine dokumenta, kako se dugi dokumenti ne bi prirodno izdvajali samo zato što se reč u njima češće pojavljuje.
BM25 formula za ocenu otprilike izgleda ovako:
gde je
KNN (K-Nearest Neighbors, K najbližih suseda) u vektorskoj pretrazi označava pronalaženje K vektora koji su najbliži vektoru upita.
Nakon što dokumente embedding model pretvori u višedimenzionalne vektore i unese ih u vektorski indeks, i upit se pri pretragi pretvara u vektor, te se u indeksu pronalazi K dokumenata sa najvišom kosinusnom sličnošću.
ElasticSearch od verzije 8.x nativno podržava tip dense_vector i knn upit, pa se vektorska pretraga može raditi direktno u ES-u, bez potrebe za posebnim ubacivanjem vektorske baze podataka poput Milvus-a.

U ES mapping-u se definiše polje dense_vector, prilikom indeksiranja dokumenta upisuje se rezultat embedding-a u to polje, a pri upitu se pomoću knn upita navode query_vector i vrednost k.
06. Šta ste uzeli u obzir kod skladištenja delova dokumenata?
Skladištenje delova zahteva razmatranje nekoliko dimenzija:
Veličina dela. Prevelika (npr. 100 token-a) znači da svaki chunk nema potpun kontekst, pa pronađeni fragmenti ne mogu samostalno da čine rečenicu i velikom modelu je teško da na osnovu njih da dobar odgovor.
Prevelika (npr. 2000 token-a) prelazi ulazno ograničenje embedding modela i razvodnjava semantiku, što smanjuje preciznost pretrage.

PaiSmart koristi veličinu dela od oko 512 token-a — to je empirijski primerena vrednost.
Overlap (preklapanje). Između dva uzastopna chunk-a zadržava se izvesno preklapanje (npr. 50 token-a), da se izbegne slučaj da ključna informacija bude tačno presečena.
U pasusu „…prema tome, jezgro prednost ovog rešenja je…", ako „prema tome" završi prvi chunk, a „jezgro prednost" počinje sledeći chunk, bez overlap-a oba chunk-a su nepotpuna.
Metapodaci. Pri skladištenju svakog chunk-a prate se metapodaci kao što su ID izvornog dokumenta, redni broj dela, originalno ime datoteke — s jedne strane radi sledivosti (da se korisniku kaže odakle odgovor dolazi), s druge strane u pojedinim scenarijima treba po redosledu vratiti i susedne chunk-ove (sliding window priziv).
Izbor skladišta. Originalni tekst chunk-a i vektor čuvaju se u ES-u, kontekst razgovora u Redis-u, a sama datoteka u skladištu objekata (npr. MinIO ili skladište u oblaku) — svako sa svojom ulogom.
07. Zašto WebSocket?
RAG projekat koristi WebSocket prvenstveno da bi se ostvario strujni izlaz (Streaming).
Veliki model ne generiše odgovor odjednom, već izbacuje ga token po token; ako se čeka kraj generisanja pa se onda vraća, korisnik čeka dugo, što daje loše iskustvo.

WebSocket podržava aktivno slanje sa strane servera, pa se svaki token koji veliki model generiše u realnom vremenu može proslediti front-end-u. Korisnik to vidi kao tekst koji se „kuca" znak po znak — to je danas standardni način interakcije u glavnim AI proizvodima za razgovor.
Za razliku od HTTP SSE (Server-Sent Events), WebSocket je full-duplex: front-end može u svakom trenutku slati poruke serveru (npr. prekinuti generisanje), što je fleksibilnije.
Naravno, ako je potrebno samo jednosmerno slanje, SSE je takođe dobar izbor, a implementacija je jednostavnija.
Na kom protokolu počiva WebSocket?
WebSocket protokol počiva na TCP-u, ali proces rukovanja pri uspostavljanju veze pozajmljuje se od HTTP/1.1. Konkretno:
- Klijent šalje HTTP zahtev sa zaglavljima
Upgrade: websocketiConnection: Upgrade. - Server odgovara sa 101 Switching Protocols, čime prihvata nadogradnju.
- Nakon toga ta TCP veza više ne koristi HTTP protokol, već WebSocket protokol okvira, čime se ostvaruje full-duplex komunikacija.
Podrazumevani portovi za WebSocket su 80 (ws://) i 443 (wss://, šifrovana verzija), isto kao HTTP/HTTPS, što olakšava prolaz kroz firewall.

08. Za čuvanje konteksta razgovora koristite Redis — koji konkretno tip?
Kontekst razgovora je uređena lista poruka, gde svaka poruka ima polja uloga (user/assistant) i sadržaj (content).
Na osnovu te osobine, Redis tip List je najprikladniji izbor.
Konkretan pristup: kao ključ se koristi userId + sessionId, svaka poruka se serijalizuje u JSON string, a zatim se pomoću RPUSH dodaje na kraj liste.

Pri čitanju se pomoću LRANGE key 0 -1 uzmu sve poruke, ili LRANGE key -20 -1 za poslednjih 10 rundi (20 poruka, jer jedna runda sadrži po jednu user i jednu assistant poruku).
Zašto ne String?
Jer kod String-a svaki put treba pročitati, deserijalizovati, izmeniti, ponovo serijalizovati i upisati — komplikovano, a postoji i rizik od konkurentnosti.
Zašto ne Hash?
Hash je pogodan za strukturirane parove ključ-vrednost, ali lista poruka je uređena, a Hash ne garantuje redosled.
Zašto ne ZSet?
ZSet je uređeni skup, može kao score koristiti vremensku oznaku da garantuje redosled, funkcionalno može da se izvede, ali je složeniji od List-a — za taj scenario je pomalo preteran.
Zato je List najprirodniji izbor: uređen, podržava operacije na oba kraja (što olakšava odsecanje najstarijih poruka) i jednostavan za upotrebu.
09. Šta kada kontekst razgovora postane predug? Osim uzimanja poslednjih 10 rundi, postoje li drugi metodi?
Uzimanje poslednjih N rundi je najjednostavniji pristup kliznog prozora, jeftin za implementaciju, ali sa očiglednim nedostacima: ako je korisnik u 1. rundi pomenuo ključnu informaciju, a u 11. rundi pita nešto srodno, sadržaj 1. runde je već odbačen i model ne zna šta je ranije rečeno.

Naprednija rešenja su:
Rešenje 1: Sažimanje rezimeom (Summarization). Kada kontekst prekorači prag, umesto da se prosto odbace stare poruke, veliki model sažima prethodni razgovor u jedan rezime; rezime + nekoliko poslednjih punih rundi razgovora zajedno čine kontekst. Tako se zadržava suština istorije, a istovremeno kontroliše broj token-a. Nedostatak je dodatni poziv velikom modelu, što povećava latenciju i trošak.
Rešenje 2: Ocena važnosti (Importance Scoring). Istorijske poruke se ocenjuju prema važnosti, uz prioritet za zadržavanje važnijih, umesto prostog odsecanja po vremenskom redosledu. Na primer, kada korisnik izričito kaže „zapamti da se zovem Wang Er", to bi trebalo da bude važnije od običnog ćaskanja.
Rešenje 3: Eksterna memorija (External Memory). Važne informacije (npr. preferencije korisnika, ključne činjenice) čuvaju se posebno u „bazi memorije"; pri svakom razgovoru se prvo iz nje prizivaju relevantne informacije i ubacuju u prompt, slično kao kod RAG-a. MemGPT i Memory modul iz LangChain-a imaju takve implementacije.
Rešenje 4: Dinamički budžet token-a. Prema složenosti pitanja dinamički se dodeljuje dužina konteksta. Za jednostavna pitanja manje konteksta i brži odgovor; za složena pitanja više konteksta i duži lanac zaključivanja.
Završna reč
Uvek kažem — projekat na biografiji mora da „izdrži batine".
Nije u tome da svako pitanje mora savršeno da se odgovori, već u tome da pokažete svoj tok tehničkog razmišljanja — zašto ste tako dizajnirali, da li ste razmišljali o alternativama, kako ste pri nailasku na problem vršili dijagnostiku.
Projekat je oslonjen na razmišljanje, a razmišljanje na akumulaciju.
Zato i stalno preporučujem da RAG projekat radite duboko i temeljno, umesto da samo propustite kroz tok i stanete.
Još jedna stvar: nakon intervjua, bez obzira na ishod, vredi rekapitulirati pitanja koja su vam postavljena.
Takav je intervju — jedno iskustvo sa intervjua može vas naterati da celu tehničku oblast prođete od početka do kraja, što je znatno efikasnije nego kad sami čujete knjigu zatvorene.
Samo napred, srećno.
