AI Agent intervjujska pitanja treća serija: Struktura podataka ReAct petlje, Status mašina, Tok kontrola, Map-Reduce 13 pitanja
content
01, Koju strukturnu podataka koristi ReAct petlja
ReAct petlja se sastoji od tri faze: Thought (razmišljanje), Action (delovanje), Observation (posmatranje). U kodu se to manifestira kao while petlja.
Najjednostavniji oblik:
while (iteration < MAX_ITERATIONS) {
ChatResponse response = llmClient.chat(history, tools);
if (response.hasToolCalls()) {
// Action faza
for (ToolCall call : response.toolCalls()) {
String result = executeTool(call);
history.add(Message.tool(call.id(), result));
}
// Observation faza: rezultat se dodaje u istoriju
continue;
} else {
// Thought faza: LLM donosi odluku
return response.content();
}
}Ključni dizajn odluke:
Istorijska lista
history: čuva celu razgovornu sekvencu, uključuje system prompt, korisničke poruke, asistente odgovore (uključujući tool_calls) i rezultate alata.Granica petlje:
MAX_ITERATIONSsprečava beskonačnu petlju. Ali se u praksi koristi iAgentBudgetza kontrolu potrošnje tokena.Tok kontrole: poziv alata ne vraća rezultat direktno u petlju, već se rezultat dodaje u
historypa se sledeći krug šalje LLM-u. Ovo osigurava da LLM vidi kompletnu istoriju interakcije.
Šta ako LLM vrati neispravan format
PaiCLI koristi try-catch oko JSON parsiranja. Ako se arguments ne može parsirati, vratiće se greška u formatu tool poruke, LLM će je videti u sledećem krugu i možeće ispraviti.
Ovo je best-effort pristup — ne pokušavamo da ispravimo format na silu, već prepustamo LLM-u da sam ispravi.
02, Kako se TaskCompletion status mašina implementirala
TaskCompletion je Enum status mašina koja prati stanje završetka Agent-ovog izvršenja. Tri stanja:
public enum TaskCompletion {
PENDING, // još uvek u toku
COMPLETED, // uspešno završeno
FAILED // neuspešno završeno
}Na koja mesta se koristi?
HITL odobrenje: kada Agent traži potvrdu korisnika, stanje je
PENDING. Korisnik može pritisnuti Enter za odobrenje (prelazi uCOMPLETED) ili ESC za otkaz (prelazi uFAILED).Plan revizija: kada korisnik pregleda plan, može ga odobriti ili modifikovati. Revizija se ne smatra neuspehom, samo se ažurira plan.
Izvršavanje: kada se zadatak završi, status se postavlja na osnovu uspeha.
Ključna design odluka: TaskCompletion se ne čuva u istoriji razgovora. To je interni status Agent framework-a, LLM ga ne vidi. U odgovorima se koristi za odluku o daljim koracima (npr. pri neuspehu se može zatražiti novi plan).
03, Da li paralelni zadaci utiču na istoriju razgovora
Paralelni zadaci se izvršavaju nezavisno, ali svi rezultate se upisuju u zajedničku istoriju.
Npr. Agent istovremeno čita tri fajla. Svako čitanje se vrši u paralelnoj niti, ali sve tri rezultata se nakon završetka dodaju u history.
Redosled dodavanja je važan: mora biti isti redosled kao originalni tool_calls. Inače LLM ne može da poklopi rezultat sa pozivom.
PaiCLI koristi Future.get() redom da osigura očuvanje redosleda:
List<Future<ToolResult>> futures = new ArrayList<>();
for (ToolCall call : toolCalls) {
futures.add(executor.submit(() -> executeTool(call)));
}
// sakupljanje rezultata po originalnom redosledu
for (int i = 0; i < futures.size(); i++) {
results.add(futures.get(i).get(timeout, TimeUnit.SECONDS));
}Paralelizam poboljšava performanse, ali ne menja semantiku istorije razgovora.
04, Kako Map-Reduce radi u sažimanju istorije
PaiCLI koristi Map-Reduce za sažimanje dugih istorija razgovora. Proces:
Map faza: Dugačka istorija se deli na manje segmente. Svaki segment se šalje LLM-u da se sažme.
// podela na segmente
List<Message> segments = splitIntoSegments(history, SEGMENT_SIZE);
List<String> summaries = new ArrayList<>();
for (Message segment : segments) {
String summary = llmClient.chat(segment, null).content();
summaries.add(summary);
}Reduce faza: Svi sažeci se spajaju u jedan konačan sažetak.
String finalSummary = llmClient.chat(
List.of(Message.system("Sažmi sledeće sažitke: " + String.join("\n", summaries))),
null
).content();Konačan sažetak zamenjuje originalne segmente u istoriji:
history = new ArrayList<>();
history.add(Message.system(SYSTEM_PROMPT));
history.add(Message.user("[Sažeta istorija razgovora]\n" + finalSummary));
history.add(Message.assistant("Razumem kontekst, nastavi."));Ključna prednost Map-Reduce: LLM ne mora da vredi celu istoriju odjednom, samo manje delove. To smanjuje rizik od "Lost in the Middle" fenomena.
05, Zašto sažimanje čuva samo poslednja 3 korisnička kruga
Dizajn odluka: prilikom sažimanja, čuvaju se system prompt, sažetak i poslednjih 3 korisnička kruga.
Zašto baš 3?
Empirijski određeno. Manje od 3 gubi suviše konteksta, više od 3 troši previše tokena.
Još važnije: čuvanje cele istorije bi poništilo svrhu sažimanja.
Ključni zahtev: ne sme se prekidati par tool_call i tool result. Inače LLM API će vratiti grešku "tool_call_id not found".
Zato se tačka preseka mora naći na granici user message, a ne usred tool_call.
Primer:
Original: [system, user1, assistant1(tool_call), tool1, user2, assistant2, ..., user20]
Pravilno sažimanje:
[system, user("[sažetak]"), assistant("Razumem."), user18, assistant18, user19, assistant19, user20]
Pogrešno sažimanje (prekid tool_call):
[system, user("[sažetak]"), assistant1(tool_call), ... ← greška! tool1 nedostaje06, Kada se pokreće ContextCompressor
ContextCompressor se pokreće u dva trenutka:
Na kraju svakog kruga (posle dodavanja novih poruka u istoriju): proverava da li je istorija prešla prag, ako da, pokreće sažimanje.
Pre svakog LLM poziva (u
Agent.chatiliPlanExecuteAgent.runSingleTask): dodatna provera da li se ipak mora sažimati pre slanja zahteva.
Ovaj dvoslojni pristup osigurava da se ne šalje preveliki zahtev LLM-u.
Prag je maxContextWindow * 0.9. Npr. za DeepSeek V4 (1M tokena), prag je 900k tokena.
07, Kako se menja kontekst pri promeni modela
Promena modela (npr. sa GLM-5.1 na DeepSeek V4) utiče na kontekst u više tačaka:
Veličina prozora: veći prozor znači da može stajati više istorije bez sažimanja.
Cena tokena: drugi modeli imaju druge cene, ovo utiče na budžet.
Podrška za funkcije: svi modeli podržavaju Function Calling, ali neki imaju specifična polja (npr. DeepSeek-ov
reasoning_content).Prompt Caching: modeli koji podržavaju caching mogu smanjiti troškove.
PaiCLI-jev LlmClient apstrahuje ove razlike. Svaki provider implementira svoju logiku:
public interface LlmClient {
ChatResponse chat(List<Message> messages, List<Tool> tools);
int maxContextWindow();
boolean supportsPromptCaching();
String promptCacheMode();
}Pri promeni modela, AgentBudget automatski rekonfiguriše pragove.
08, Kako se postiže thread-safe paralelno izvršenje
Paralelno izvršenje alata koristi ExecutorService:
ExecutorService executor = Executors.newFixedThreadPool(4);Ali conversationHistory nije thread-safe. Zato prilikom paralelnog izvršenja koristi se sinhronizacija:
synchronized (historyLock) {
for (ToolResult result : results) {
history.add(Message.tool(result.toolCallId(), result.content()));
}
}Ovo sprečava race condition gde bi dva niti istovremeno pisala u istoriju.
Plan Agent koristi isti ExecutorService za paralelno izvršenje zadataka nezavisnih u grafu.
09, Kako se implementiraju AbstractWorker i TaskQueue
TaskQueue je blokirajući red koji čuva čekajuće zadatke. Implementiran je na osnovu LinkedBlockingQueue:
BlockingQueue<AgentTask> queue = new LinkedBlockingQueue<>();AbstractWorker je nit koja stalno izvlači zadatke iz reda:
public void run() {
while (!Thread.interrupted()) {
try {
AgentTask task = queue.take(); // blokirajuće
processTask(task);
} catch (InterruptedException e) {
break;
}
}
}processTask je apstraktan metod, konkretne implementacije (ReActWorker, PlanWorker) ga override-uju.
Ključni dizajn:
Otkaz worker-a: kada se nit prekine,
queue.take()bacaInterruptedException, petlja se izlazi.Timeout na zadatku: svaki zadatak ima maksimalno vreme izvršenja, posle čega se automatski prekida.
Povratak rezultata: rezultat se šalje nazad kroz callback ili Future.
10, Kako se mapa mogućnosti koristi pri izboru alata
PaiCLI ne koristi mapu mogućnosti pri izboru alata. Izbor je potpuno u rukama LLM-a.
Agent samo šalje listu dostupnih alata (sa opisima i JSON Schema parametara) LLM-u. LLM odlučuje koji alat pozvati.
Ovo je osnovni princip OpenAI Function Calling protokola.
Dodatno, PaiCLI-jev system prompt sadrži uputstva o korišćenju alata:
## Korišćenje alata
Za čitanje fajlova koristi `read_file`, ne koristi `execute_command cat`.
Za kompajliranje koristi `execute_command`, ne pozivaj `javac` direktno.Ovo je "soft" uputstvo — LLM može da ga poštuje ili ne.
11, Kako se implementiraju BreakSignal i ContinueSignal
Ovo su interni signali za kontrolu toka:
public class BreakSignal extends RuntimeException {
public BreakSignal(String reason) { super(reason); }
}
public class ContinueSignal extends RuntimeException {
public ContinueSignal() { super(); }
}BreakSignal se koristi kada treba hitlo prekinuti izvršenje (npr. HITL otkaz). Nije redovna greška, već kontrola toka.
ContinueSignal se koristi kada treba preskočiti trenutni korak i nastaviti sa sledećim (npr. kod Plan revizije).
Ovi signali se hvataju na odgovarajućem nivou:
try {
executeTask();
} catch (BreakSignal e) {
// hito prekid
return TaskCompletion.FAILED;
} catch (ContinueSignal e) {
// preskoči, nastavi dalje
}12, Kako se implementiraju zapisi u dnevnik i audit log
PaiCLI koristi dva sistema za evidenciju:
Dnevnik (log): za debug i praćenje, koristi SLF4J.
Audit log: za evidenciju operacija, čuva u
~/.paicli/audit.jsonl. Svaki unos sadrži timestamp, tip operacije, detalje.
Format audit loga:
{
"timestamp": "2026-04-18T10:30:45Z",
"type": "tool_call",
"tool": "write_file",
"path": "/path/to/file.java",
"user": "username"
}Audit log se piše asinhrono da ne usporava izvršenje.
13, Kako se dizajniraju interfejsi za proširivost
PaiCLI koristi nekoliko paterna za proširivost:
Strategy pattern:
LlmClientinterfejs — svaki provider je druga strategija.Registry pattern:
ToolRegistry— alati se mogu dinamički registrovati.Template Method:
AbstractWorker— zajednički kod u baznoj klasi, specifični kod u podklasama.Chain of Responsibility:
NotificationRouter— ručno obrađuje različite tipove notifikacija.Builder pattern:
Agent.Builder— fleksibilna konstrukcija Agent objekata.
Ovi paterni omogućavaju lako dodavanje novih modela, alata ili radnih modifikacija bez menjanja postojećeg koda.
ending
Dizajn Agent strukture nije samo implementacija ReAct petlje. To je sistem koji:
- Upravlja kontekstom i memorijom
- Kontroluše tokom izvršenja
- Paralelno izvršava zadatke
- Sažima istoriju da ne prekorači limit
- Pruža proširiv interfejs
Svaki ovaj deo je ključan za stabilnog i korisnog Agent-a.
Kako napisati životopis
Naziv projekta: PaiCLI — Struktura Agent-a
Opis projekta: Od nule dizajnirana struktura Agent-a, implementirana ReAct petlja, TaskCompletion status mašina, Map-Reduce sažimanje i paralelno izvršenje zadataka.
Tehnološki stack: Java 17, ExecutorService, LinkedBlockingQueue, Enum status mašina, JSONL audit log
Ključne odgovornosti:
- Dizajnirao strukturu ReAct petlje sa
whilepetljom i kontrolom granice krozMAX_ITERATIONSiAgentBudget - Implementirao TaskCompletion status mašinu sa tri stanja (PENDING/COMPLETED/FAILED) za HITL odobrenja
- Koristeći Map-Reduce dao sredstvo za sažimanje istorije razgovora dok se ne prekorači kontekstni prozor
- Implementirao paralelno izvršenje alata kroz
ExecutorServiceuz sinhronizacijuconversationHistory - Dizajnirao AbstractWorker i TaskQueue za pozadinske zadatke sa thread-safe procesiranjem
