Implementirajte Multi-Agent i SubAgent paralelnost, neka Agent tim trči kao duži
Naš PaiCLI Agent je već implementirao ReAct, Plan-and-Execute, Memory, RAG — jedan Agent već može da čita fajlove, pokazuje komande, pretražuje kod, pamti kontekst.
Ali hoćemo još više.
OK, danas ćemo da dovršimo Multi-Agent, da jedan svestrani Agent razložimo na više specijalizovanih uloga, koje sarađuju kako bi završile zadatak.

Kao veliki projektni tim.
Produkt menadžer je zadužen za rastavljanje zahteva, programeri za pisanje koda, testeri za prihvatanje. Svaki obavlja svoj posao, međusobno se balansiraju. Multi-Agent prati istu logiku.
01. Šta problem Multi-Agent zapravo rešava
Nemojmo odmah da kucamo kod, prvo jasno da razumemo zašto nam treba Multi-Agent.
Jezgro Multi-Agent vrednosti je uvođenje više uloga.
Planer specijalizovan za rastavljanje zadataka, izvršilac specijalizovan za rad, pregledač specijalizovan za pronalaženje grešaka. Svako radi samo jednu stvar, ali je taj posao radi temeljno i duboko.

Multi-Agent frameworki na tržištu takođe potvrđuju ovaj trend. Microsoft-ov AutoGen već ima 57.3K zvezdica, a CrewAI 49.5K, oba projekta naglašavaju „podela uloga + saradnja“.
AutoGen je više okrenut dijalogom vođenoj multi-agent saradnji — više Agenata oko istog problema vodi diskusiju, pogodno za scenarije koji zahtevaju ponovljenenu komunikaciju i pregovaranje.
CrewAI je više okrenut igranju uloga + delegiranju zadataka — prvo definiše uloge, zatim dodeljuje zadatke, pogodno za scenarije sa jasnim procesima izvršenja.
PaiCLI je odabrao pristup bliži CrewAI — definiranje uloga, dodela zadataka, pregled rezultata.

Da bismo vam jasno objasnili Multi-Agent, mi smo ovo pisali od nule, bez korišćenja LangGraph4J framework-a. Mislim da sledeći put možemo uvesti taj framework.
02. Glavno-podređeni režim + tročlana podela uloga
PaiCLI je odabrao glavno-podređenu arhitekturu (Orchestrator-SubAgent) — orkestrator je „glavni“, pod-Agenti su „podređeni“.
Orkestrator je zadužen za distribuciju zadataka i kontrolu toka, pod-Agenti samo rade svoj posao.
Zašto glavno-podređeni a ne ravnopravni režim?
Zato što u ravnopravnom režimu Agenti moraju da komuniciraju direktno. Naravno, kasnije možemo iterativno dodati novu verziju.
Glavno-podređeni režim koncentriše koordinacionu logiku u orkestrator, pod-Agenti ne komuniciraju direktno, sve poruke idu preko orkestratora. Struktura je jasna, debugging je lak.

Definicija tri uloge je vrlo jasna:
Planer (Planner) — dobija korisničke zahteve, rastavlja ih na listu izvršivih koraka, označava tipove i zavisnosti svakog koraka.
Izvršilac (Worker) — dobija opis koraka, poziva alatke da završi specifične operacije. Čitanje fajlova, pisanje fajlova, pokretanje komandi — to je posao izvršioca.
Pregledač (Reviewer) — dobija rezultate izvršenja, ocenjuje da li zadovoljavaju zahteve. Ako prođe — pušta, ako ne prođe — vraća problem da izvršilac ponovi.
Što se tiče kodne strukture, ove četiri klase svaka obavlja svoj posao:

AgentRole.java definiše enum uloga, tri vrednosti: PLANNER, WORKER, REVIEWER. Svaka uloga ima ime za prikaz i opis.
public enum AgentRole {
PLANNER("Planer", " zadužen za analizu korisničkih zadataka, izradu planova izvršenja, rastavljanje kompleksnih zadataka na izvršive podzadatke"),
WORKER("Izvršilac", "zadužen za izvršavanje specifičnih koraka zadataka, pozivanje alataka za operacije nad fajlovima, izvršavanje komandi itd."),
REVIEWER("Pregledač", "zadužen za pregled kvaliteta i tačnosti rezultata izvršenja, davanje predloga za unapređenje");
}AgentMessage.java definiše komunikacione poruke između Agenata, implementirane kao Java 17 record. Šest tipova poruka: TASK (zadatak), RESULT (rezultat), FEEDBACK (povratna informacija), APPROVAL (odobrenje), REJECTION (odbijanje), ERROR (greška).
Stanje poruke zavisi od ovih šest tipova.
public record AgentMessage(
String fromAgent,
AgentRole fromRole,
String content,
Type type
) {
public enum Type {
TASK, RESULT, FEEDBACK, APPROVAL, REJECTION, ERROR
}
}SubAgent.java je implementacija pod-Agenta, svaki pod-Agent ima nezavisnu ulogu, sistemski prompt i istoriju razgovora, ali deli LLM klijent i registry alata.
AgentOrchestrator.java je orkestrator, upravlja celim procesom saradnje — od planiranja do izvršenja do pregleda do sumiranja.
Celu arhitekturu možemo svesti na jednu rečenicu: orkestrator uzima zadatak, daje ga planeru da rastavi, izvršiocu da izvrši, pregledaču da proveri, a kad sve prođe sumira rezultat i vraća ga korisniku.
03. Komandir tima
Orkestrator je srce celog Multi-Agent sistema, sva koordinaciona logika je ovde. Prvo pogledajmo konstruktor:
public AgentOrchestrator(GLMClient llmClient, ToolRegistry toolRegistry, MemoryManager memoryManager) {
this.llmClient = llmClient;
this.toolRegistry = toolRegistry;
this.planner = new SubAgent("planner", AgentRole.PLANNER, llmClient, toolRegistry);
this.workers = List.of(
new SubAgent("worker-1", AgentRole.WORKER, llmClient, toolRegistry),
new SubAgent("worker-2", AgentRole.WORKER, llmClient, toolRegistry)
);
this.reviewer = new SubAgent("reviewer", AgentRole.REVIEWER, llmClient, toolRegistry);
this.memoryManager = memoryManager;
}Podrazumevano se kreira 1 planer, 2 izvršioca, 1 pregledač. Dva Workera se rotiraju — kad jedan radi, drugi može preuzeti sledeći korak. Ovo je priprema za paralelno izvršenje.

Ključna metoda orkestratora run() je ulaz u celu saradnju, tok ima šest faza:
- Prva faza: planiranje. Orkestrator daje korisnički zadatak planeru, planer izdaje izvršni plan u JSON formatu.
- Druga faza: parsiranje plana. Orkestrator parsira JSON u listu
ExecutionStep, uspostavlja zavisnosti između koraka. - Treća faza: izvršenje. Po redosledu zavisnosti, dodeljuje izvršive korake Worker-ima. Koraci bez zavisnosti u istom batch-u mogu ići paralelno.
- Četvrta faza: pregled. Nakon svakog izvršenog koraka, šalje se pregledaču na odobrenje. Ako prođe — označava se završenim, ako ne — vraća se sa povratnim informacijama za ponovno izvršenje.
- Peta faza: rukovanje preostalim koracima. Ako neki korak ne uspe i onemogući dalje zavisne korake, eksplicitno se upozorava korisnik da su ti koraci preskočeni.
- Šesta faza: sumiranje rezultata. Sumira se stanje i rezultat svih koraka, upisuje u memoriju, vraća se korisniku.
Kod izgleda ovako:
public String run(String userInput) {
// 1. planiranje
AgentMessage planResult = planner.execute(planMessage);
// 2. parsiranje plana
List<ExecutionStep> steps = parsePlan(planResult.content());
// 3. izvršenje + pregled
while (true) {
List<ExecutionStep> executable = getExecutableSteps(steps);
if (executable.isEmpty()) break;
// Pojedinačno serijski, više koraka paralelno
}
// 4. rukovanje preostalim + sumiranje
String finalResult = buildFinalResult(steps);
return finalResult;
}Ovde je jedan detalj vredan pažnje — upravljanje zavisnostima i određivanje izvršivih koraka.
Kako se određuju zavisnosti
Svaki korak ima grupu dependencies, koje označavaju koje korake zavisi.
Samo kada sve zavisnosti budu završene, ovaj korak se može izvršiti. Logika getExecutableSteps() je filtriranje koraka koji su „PENDING i sve su zavisnosti COMPLETED“:
List<ExecutionStep> getExecutableSteps(List<ExecutionStep> steps) {
Map<String, StepStatus> statusMap = new HashMap<>();
for (ExecutionStep step : steps) {
statusMap.put(step.id(), step.status());
}
return steps.stream()
.filter(step -> step.status() == StepStatus.PENDING)
.filter(step -> step.dependencies().stream()
.allMatch(dep -> statusMap.get(dep) == StepStatus.COMPLETED))
.toList();
}Na primer, korisnik kaže „napravi projekat, zatim napiši Controller, pa pokreni test za proveru“, planer će izdati tri koraka:
- step_1 napravi projekat (bez zavisnosti)
- step_2 napiši Controller (zavisi od step_1)
- step_3 pokreni test (zavisi od step_2).
Prvi korak se prvo izvršava, nakon završetka se izvršava drugi, itd.

Kako se radi paralelno izvršenje
Kada u istom batch-u ima više nezavisnih koraka, orkestrator će ih izvršavati paralelno.
Na primer, step_1 i step_2 nemaju zavisnost, pa će se istovremeno dodeliti dva Worker-a.
Koristili smo ExecutorService + BlockingQueue za paralelnost Worker-a. Svaki korak dobija jednog Worker-a, a nakon upotrebe vraća ga u pool.
Stream-ing output se piše u ByteArrayOutputStream, nakon batch-a se flush redosledom step_id u stdout, osiguravajući da korisnik vidi uređen proces izvršenja.
private void runBatchParallel(List<ExecutionStep> batch, ...) {
ExecutorService executor = Executors.newFixedThreadPool(parallelism);
BlockingQueue<SubAgent> workerPool = new LinkedBlockingQueue<>(workers);
Map<String, ByteArrayOutputStream> buffers = new ConcurrentHashMap<>();
for (ExecutionStep step : batch) {
// Svaki korak po jedan buffer, paralelno izvršenje
futures.add(executor.submit(() -> {
SubAgent worker = workerPool.take(); // uzmi Worker iz pool-a
// izvrši + pregled
workerPool.offer(worker); // vrati nakon upotrebe
}));
}
// flush redosledom step_id
for (ExecutionStep step : batch) {
System.out.print(buffers.get(step.id()).toString());
}
}04. Izvršioci sa svojim zaduženjima
SubAgent je laka implementacija Agent-a, svaka instanca ima nezavisnu ulogu, sistemski prompt i istoriju razgovora. Ali ne zauzima LLM klijent i registry alata — ovi su deljeni, izbjegavajući inicijalizaciju novog seta za svaki pod-Agent.
Ključni dizajn su tri sistemski prompt-a. Prompt-i različitih uloga su potpuno različiti, određujući način ponašanja.
Prompt planera
private static final String PLANNER_PROMPT = """
Ti si stručnjak za planiranje zadataka. Tvoja dužnost je da analiziraš korisničke zahteve i da ih rastaviš na jasne korake izvršenja.
Molim te izadaj izvršni plan u sledećem JSON formatu:
{
"summary": "Sažetak zadatka",
"steps": [
{
"id": "step_1",
"description": "Opis koraka,treba da bude konkretno i jasno",
"type": "FILE_READ | FILE_WRITE | COMMAND | ANALYSIS | VERIFICATION",
"dependencies": []
}
]
}
""";Planer je ograničen da izdaje samo JSON. Svaki korak mora imati id, opis, tip i zavisnosti. Jednostavni zadaci se rastavljaju na 1-3 koraka, komplikovani na 5-10 koraka.
Prompt izvšioca
private static final String WORKER_PROMPT = """
Ti si stručnjak za izvršenje zadataka. Tvoja dužnost je da pozivaš alatke kako bi izvršio konkretne operacije na osnovu datih koraka.
Dostupni alati:
1. read_file - čita sadržaj fajla
2. write_file - piše sadržaj u fajl
3. list_dir - lista sadržaj direktorijuma
4. execute_command - izvršava komandu
5. create_project - kreira projekat
6. search_code - semantičko pretraživanje koda
Ako zadatak uključuje razumevanje koda, prvo koristi search_code alat.
""";Prompt izvšioca nabraja dostupne alate i daje prioritete korišćenja — kada je u pitanju razumevanje koda, prvo koristiti search_code, ne odmah execute_command skenirajući fajl sistem.
Prompt pregledača
private static final String REVIEWER_PROMPT = """
Ti si stručnjak za kontrolu kvaliteta. Tvoja dužnost je da proveriš da li su rezultati izvršenja tačni, kompletni i visokog kvaliteta.
Molim te izadaj rezultate provere u JSON formatu:
{
"approved": true ili false,
"summary": "Sažetak provere",
"issues": ["problem1", "problem2"],
"suggestions": ["predlog1", "predlog2"]
}
""";Pregledač je takođe ograničen da izdaje JSON, approved true znači puštanje, false znači vraćanje uz listu problema i predloge unapređenja.

Još jedan dizajn detalj — samo izvršilac poziva alatke, planer i pregledač se bave samo analizom i odlukama. U kodu je to kontrolisano kroz shouldUseTools():
private boolean shouldUseTools() {
return role == AgentRole.WORKER;
}To znači da planer i pregledač neće generisati pozive alata, njihovi izlazi su čist tekst (JSON format). Dizajn ima prednost jasne podele odgovornosti — planer ne dira alatke, pregledač ne dira alatke, samo izvršilac radi.
Što je jasnija podela, manje grešaka.
Možda se pitate, zašto planer ne može istovremeno da i obavi posao?
Zato što kad planer pozove alatke, on više nije „planer“, postaje „hibridna uloga“ koja i planira i izvršava.
Problem hibridnih uloga je što lako može da zapadne u izvršne detalje u fazi planiranja, što dovodi do toga da plan nije dovoljno visokog nivoa, nije dovoljno kompletn. Kao što produkt menadžer napiše i kod dok piše dokument zahteva.
Upravljanje istorijom razgovora
Svaki SubAgent održava nezavisnu istoriju razgovora, ali nakon završetka svakog nezavisnog zadatka čisti istoriju (zadržavajući sistemski prompt):
public void clearHistory() {
GLMClient.Message systemMsg = conversationHistory.get(0);
conversationHistory.clear();
conversationHistory.add(systemMsg);
}Zašto?
Zato što je svaki korak nezavisni zadatak, kontekst razgovora iz prethodnog koraka ne pomaže sledećem, naprotiv ometa procenu modela.
Zadržavanje sistemskog prompt-a je dovoljno — uloga ne sme biti izgubljena.
Orkestrator nakon svakog izvršenog koraka poziva worker.clearHistory() i reviewer.clearHistory(), osiguravajući da svaki korak krene sa čistim stanjem.
05. Posao pregledača
Pregledač je najvrednija uloga u Multi-Agent sistemu. Bez njega, kvalitet rezultata izvršenja zavisi isključivo od svesti modela; uz njega, svaki korak ima kontrolora, ako ne prođe vraća se na ponovno izvršenje.

Proces pregleda je u metodi orkestratora runStep(), ključna logika ima tri koraka:
Prvi korak, nakon što izvršilac završi korak, orkestrator daje originalni zadatak i rezultat izvršenja pregledaču:
AgentMessage reviewResult = reviewer.review(step.description(), result.content(), out);Drugi korak, parsira se odobrenje pregledača. U JSON izlazu pregledača postoji approved polje, true znači puštanje, false znači vraćanje:
boolean approved = parseReviewApproval(reviewResult.content());Treći korak, ako nije prošlo, izvlače se problemi, uz povratne informacije se šalje natrag izvršiocu da ponovi.
Konzervativna strategija parsiranja odobrenja
Implementacija parseReviewApproval() ima jednu dizajnersku odluku vrednu pažnje — kad se izlaz pregledača ne može parsirati, podrazumeva se „nije prošlo“:
boolean parseReviewApproval(String reviewContent) {
if (reviewContent == null || reviewContent.isEmpty()) {
return false; // prazan sadržaj, podrazumeva se nije prošlo
}
try {
JsonNode root = mapper.readTree(cleaned);
JsonNode approvedNode = root.path("approved");
if (approvedNode.isMissingNode() || approvedNode.isNull()) {
return false; // nedostaje approved polje, podrazumeva se nije prošlo
}
return approvedNode.asBoolean(false);
} catch (Exception e) {
// JSON parsiranje nije uspelo: mora istovremeno imati pozitivne ključne reči i bez negativnih ključnih reči za smatanje da je prošlo
String lower = reviewContent.toLowerCase();
boolean hasNegative = lower.contains("nije prošlo") || lower.contains("ne prošlo");
boolean hasPositive = lower.contains("prošlo") || lower.contains("odobreno");
if (hasNegative) return false;
if (!hasPositive) return false; // nema ni pozitivnih ni negativnih, konzervativno se smatra da nije prošlo
return true;
}
}Zašto konzervativna strategija?
Zato što je cena puštanja problema mnogo veća od cene ponovnog izvršenja ispravnog rezultata.
Jedan red koda koji prođe greškom može dovesti do toga da svi sledeći koraci krenu pogrešno; jedan ispravan rezultat koji se još jednom proveri najviše potroši dodatne token. Biramo manje od dva zla.

Mehhanizam ponovnog pokušaja
Kada pregled ne prođe, orkestrator će dozvoliti izvršiocu da ponovi uz povratne informacije, najviše 2 puta:
while (!approved && retries < MAX_RETRIES_PER_STEP) {
retries++;
String feedbackContext = context + "\n\nPrethodni rezultat izvršenja je odbačen uz sledeći razlog:\n" + issues;
AgentMessage retryResult = worker.executeWithContext(taskMsg, feedbackContext, out);
AgentMessage retryReview = reviewer.review(step.description(), retryResult.content(), out);
approved = parseReviewApproval(retryReview.content());
}Prilikom ponovnog pokušaja se ubacuje razlog odbijanja, tako da izvršilac zna gde je pogrešio i kako da ispravi.
Ovo je kao kod Code Review-a kada vam reviewer ostavi komentar, vi ispravite i ponovo podnesite PR.
U stvarnom radu, efekat ovakvog povratnog ponovnog pokušaja je prilično očigledan.
Ako ne prođe ni posle 2 pokušaja, zadržava se trenutni rezultat, ne iskoristi se više resursa na uporno ponavljanje.
Prenos konteksta
Još jedan lako previđen ali važan detalj — dok Worker izvršava svaki korak, orkestrator ubacuje kontekst „završenih zavisnih koraka“.
Tako Worker zna šta su prethodni koraci uradili, šta su proizveli, ne mora da pogađa.
private String buildStepContext(List<ExecutionStep> steps, ExecutionStep currentStep) {
StringBuilder context = new StringBuilder();
context.append("Kontekst celog zadatka:\n");
for (ExecutionStep step : steps) {
if (step.status() == StepStatus.COMPLETED && currentStep.dependencies().contains(step.id())) {
context.append("Završeni zavisni korak [").append(step.id()).append("]: ")
.append(step.description()).append("\n");
String preview = step.result().length() > 500
? step.result().substring(0, 500) + "..."
: step.result();
context.append("Rezultat:").append(preview).append("\n");
}
}
return context.toString();
}Obratite pažnju na skraćivanje rezultata, ako je duži od 500 karaktera uzima samo prvih 500 i dodaje tri tačke.
Zašto?
Zato što kontekstni prozor Worker-a je ograničen, ako je rezultat prethodnog koraka poseban dug (npr. pročitao je veliki fajl), ako se sve stavi bice eksplodiralo token.
500 karaktera je dovoljno da Worker razume šta je prethodni korak uradio, a ne zauzima previše kontekstnog prostora.
06. Pokrenimo i probajmo
Kod smo objasnili, hajde da vidimo kako se pokreće.
Kompajliranje
mvn clean packageNakon uspešnog kompajliranja će se generisati paicli-1.0-SNAPSHOT.jar u target/ direktorijumu.
Pokretanje
java -jar target/paicli-1.0-SNAPSHOT.jarNakon pokretanja ćete videti PaiCLI v5.0.0 Banner i informacije.
Ulazak u Multi-Agent režim
Nakon unosa /team, sledeći zadatak će ići kroz Multi-Agent mod saradnje:
👤 Ti: /team
👤 Ti: Napravi Spring Boot projekat, napiši HelloController, zatim proveri strukturu projekta
Može i sve u jednoj komandi:
👤 Ti: /team napravi Java projekat po imenu demoapp, zatim pročitaj pom.xml, na kraju proveri strukturu projektaPrilikom izvršenja ćete videti tri uloge redom:
📋 Prva faza: planiranje
🧑💼 Planer analizira zadatak...
📋 Izvršni plan
⏳ [step_1] Kreiraj strukturu demoapp projekta (zavisnost: nema)
⏳ [step_2] Pročitaj sadržaj demoapp/pom.xml (zavisnost: step_1)
⏳ [step_3] Proveri strukturu projekta i Maven konfiguraciju (zavisnost: step_2)
⚡ Druga faza: izvršenje
🛠️ worker-1 izvršava korak [step_1]: Kreiranje strukture demoapp projekta
🔍 reviewer pregleda rezultat koraka [step_1]...
✅ Korak [step_1] pregled prošao
🛠️ worker-2 izvršava korak [step_2]: Čitanje sadržaja demoapp/pom.xml
🔍 reviewer pregleda rezultat koraka [step_2]...
✅ Korak [step_2] pregled prošao
🛠️ worker-1 izvršava korak [step_3]: Provera strukture projekta i Maven konfiguracije
🔍 reviewer pregleda rezultat koraka [step_3]...
✅ Korak [step_3] pregled prošao



Tri uloge rade svoje poslove, ne ometaju se, svaki korak ima pregled.


Konačno savršeno rešeno.
Povratak na podrazumevani režim
Nakon izvršenja Multi-Agent zadatka, automatski se vraća na podrazumevani ReAct režim. Nema potrebe za ručnim prebacivanjem.

Ostale komande
PaiCLI takođe podržava /plan (Plan-and-Execute režim), /memory (pregled stanja memorije), /index (indeksiranje koda), /search (semantičko pretraživanje), /graph (graf relacija koda) itd. Može se kombinovati.
Jednostavan zadatak se rešava sa ReAct, više koraka sa zavisnostima sa Plan, više koraka podeljeno sa Multi-Agent.
Na primer „pomozni mi pročitati README.md“, dovoljan je ReAct;
„napravi projekat, napiši kod, pokreni test“, Plan je bolji;
„napravi projekat, napiši kod, pokreni test, svaki korak treba provere“, to je domena Multi-Agent-a.
Deljenje memorije i alata
Još jedna stvar koju treba napomenuti — Multi-Agent režim i ReAct režim dele istu memoriju i ToolRegistry.
To znači da činjenice sačuvane sa /save u ReAct režimu, Multi-Agent Worker-i mogu da pronađu kroz pretragu memorije. Indeks koda uspostavljen sa /index, Worker pozivom search_code može da pronađe.
07. Za CV
Naziv projekta: PaiCLI - Java Agent CLI
Opis projekta: Multi-Agent baziran na glavno-podreženoj arhitekturi, realizuje podelu uloga i saradnju planera, izvšioca, pregledača.
Ključne odgovornosti:
Dizajnirao i implementirao
Multi-Agentglavno-podređenu arhitekturu saradnje, rešio usko grlo single Agent-a pri kompleksnim zadacima, može se aktivirati komandom/team, podržava3vrste uloga: Planner / Worker / Reviewer.Baziran na
BlockingQueue + ExecutorServiceimplementirao paralelni izvršni engine Worker pod-Agenata, rešio problem niske efikasnosti serijskog izvršenja višekoraknih zadataka, i rad napred po redosledu zavisnosti.Realizovao
6tipova AgentMessage poruka, mehanizam pregleda povratnih informacija i automatskog ponavljanja, osigurao da su rezultati multi-Agent izvršenja pregledljivi i mogu se vratiti.Baziran na
ProcessBuilder + Future + timeout kontrolarefaktorisao Shell putanju izvršenja, rešio problem dugotrajnog blokiranjaexecute_commandšto utiče na glavni tok, i završio stvarnu/teamend-to-end verifikaciju sa119testova koje su sve prošle.
