Dodajte Plan-and-Execute Agent CLI-u, neka Agent prvo planira zatim izvršava, podržava DAG.
Prvo izdanje PaiCLI-a smo već implementirali, osnovni ReAct Agent, može korak po korak izvršavati zadatke, dok razmišlja dok radi.
Ali ovaj način ima jedan problem: kompleksni zadaci zahtevaju mnogo rundi razgovora, svaki korak zahteva poziv LLM.
Na primer zadatak „napravi Spring Boot projekat, napiši REST API, zatim pakuj i pokreni“:

Ukupno će se pozvati 5 puta LLM, svaki put čeka mrežni povrat.
- izdanje, implementiraćemo Plan-and-Execute režim: prvo neka LLM napravi kompletni plan, zatim izvršava po koracima, bez stalnog ponavljanog LLM upita.
01. Ključna ideja Plan-and-Execute
Plan-and-Execute režim dolazi iz članka „Plan-and-Solve Prompting“.

Ključna ideja je razdvajanje planiranja i izvršenja.

Prednosti ovog pristupa:
- Smanjenje LLM poziva: planira jednom, izvršava više puta
- Veća predvidljivost: unapred znate ceo proces izvršenja
- Podrška paralelnom izvršenju: prepoznaje nezavisne zadatke i obrađuje ih paralelno
- Ponovljeni pokušaji pri grešci: ako neki korak ne uspe, može se samostalno ponoviti, ne mora od početka
Cena je smanjena fleksibilnost, ako se u toku izvršenja otkrije problem u planu, treba ponovno planirati.
02. Modelovanje zadataka
Da bismo implementirali Plan-and-Execute, prvo treba definisati šta je „zadatak“.
Zašto treba modelovanje zadataka
U ReAct režimu, zadatak je implicitno sadržan u istoriji razgovora. LLM kroz čitanje istorijskih poruka zna šta treba da radi. Ali ovaj način ima dva problema:
Prvi, ekspanzija konteksta. Kompleksni zadaci zahtevaju mnogo rundi razgovora, kako istorijske poruke postaju duže, potrošnja Token-a drastično raste.
Drugi, nejasno stanje. U istoriji razgovora su mešane misaoni procesi, pozivi alata, rezultati izvršenja, teško je odjednom videti do kog koraka je zadatak stigao.
Modelovanje zadataka razdvaja „šta“ i „kako“. Faza planiranja određuje „šta“ (lista zadataka), faza izvršenja rešava „kako“ (konkretno izvršenje).
Dizajn Task klase
public class Task {
private final String id; // jedinstveni identifikator zadatka
private final String description; // opis zadatka
private final TaskType type; // tip zadatka
private TaskStatus status; // stanje izvršenja
private String result; // rezultat izvršenja
private String error; // informacija o grešci
private final List<String> dependencies; // ID-jevi zavisnosti zadatka
private final List<String> dependents; // ID-jevi zavisnih zadataka
private long startTime; // vreme početka
private long endTime; // vreme završetka
}Definisali smo 6 tipova zadataka:
PLANNING: zadatak planiranja, za analizu i odlučivanjeFILE_READ: čitanje fajla, dobijanje informacijaFILE_WRITE: pisanje fajla, slanje rezultataCOMMAND: izvršenje komande, kompajliranje, pokretanje itd.ANALYSIS: analiza rezultata, međurešenjaVERIFICATION: provera rezultata, provera tačnosti
Stanja zadatka ima 5:
PENDING: čeka izvršenjeRUNNING: izvršava seCOMPLETED: završenoFAILED: neuspešno izvršenjeSKIPPED: preskočeno (zbog neuspelih zavisnosti)
Životni ciklus zadatka
Kompletan životni ciklus zadatka od kreiranja do završetka izgleda ovako:
PENDING → RUNNING → COMPLETED/FAILED/SKIPPEDSvaka promena stanja ima odgovarajuću metodu:
public void markStarted() {
this.status = TaskStatus.RUNNING;
this.startTime = System.currentTimeMillis();
}
public void markCompleted(String result) {
this.status = TaskStatus.COMPLETED;
this.result = result;
this.endTime = System.currentTimeMillis();
}
public void markFailed(String error) {
this.status = TaskStatus.FAILED;
this.error = error;
this.endTime = System.currentTimeMillis();
}Čuvanje vremenskih oznaka ima dve namene: prvo za statistiku trajanja izvršenja, drugo za analizu uskih grla. Ako neki zadatak uvek traje dugo, možda treba optimizovati ili razložiti.
Zavisnosti
Kompleksni zadaci imaju redosled zavisnosti. Na primer „pisanje koda“ zavisi od „kreiranja projekta“, „pokretanje“ zavisi od „kompajliranja“.
Koristimo DAG (usmereni aciklični graf) za predstavljanje zavisnosti:

Svaki zadatak može deklarisati koje zadatke zavisi (dependencies), sistem automatski izračunava redosled izvršenja.
Ključna metoda zavisnosti je isExecutable:
public boolean isExecutable(Map<String, Task> allTasks) {
if (status != TaskStatus.PENDING) return false;
for (String depId : dependencies) {
Task dep = allTasks.get(depId);
if (dep == null || dep.getStatus() != TaskStatus.COMPLETED) {
return false;
}
}
return true;
}Samo kada su sve zavisnosti završene, zadatak se može izvršiti. Ova jednostavna provera osigurava ispravnost redosleda izvršenja.
Tipovi zadataka smo definisali 6:
PLANNING: zadatak planiranjaFILE_READ: čitanje fajlaFILE_WRITE: pisanje fajlaCOMMAND: izvršenje komandeANALYSIS: analiza rezultataVERIFICATION: provera rezultata

Zavisnosti
Kompleksni zadaci imaju redosled zavisnosti. Na primer „pisanje koda“ zavisi od „kreiranja projekta“, „pokretanje“ zavisi od „kompajliranja“.
Koristimo DAG (usmereni aciklični graf) za predstavljanje zavisnosti:

Svaki zadatak može deklarisati koje zadatke zavisi (dependencies), sistem automatski izračunava redosled izvršenja.
Izvršni plan
Više zadataka može činiti jedan izvršni plan:
public class ExecutionPlan {
private final String id;
private final String goal; // cilj plana
private final Map<String, Task> tasks; // svi zadaci
private final List<String> executionOrder; // redosled izvršenja
private PlanStatus status;
private String summary;
}
Algoritam topološkog sortiranja
Ključna metoda je computeExecutionOrder(), koristi algoritam topološkog sortiranja da pretvori DAG u linearni redosled izvršenja.
Osnovna ideja topološkog sortiranja:
- Pronađi sve čvorove sa ulaznim stepenom 0 (zadaci bez zavisnosti)
- Dodaj te čvorove u rezultujuću listu
- Ukloni te čvorove i njihove odlazne ivice
- Ponovi 1-3, dok se svi čvorovi ne obrade
Implementiramo pomoću DFS algoritma:
public boolean computeExecutionOrder() {
executionOrder.clear();
Set<String> visited = new HashSet<>();
Set<String> visiting = new HashSet<>();
for (Task task : tasks.values()) {
if (!visited.contains(task.getId())) {
if (!topologicalSort(task, visited, visiting)) {
return false; // postoji petlja
}
}
}
Collections.reverse(executionOrder);
return true;
}
private boolean topologicalSort(Task task, Set<String> visited, Set<String> visiting) {
String id = task.getId();
if (visiting.contains(id)) {
return false; // postoji petlja, sortiranje neuspešno
}
if (visited.contains(id)) {
return true;
}
visiting.add(id);
// rekurzivno obradi sve zavisnosti
for (String depId : getDependencies()) {
Task dep = tasks.get(depId);
if (dep != null) {
if (!topologicalSort(dep, visited, visiting)) {
return false;
}
}
}
visiting.remove(id);
visited.add(id);
executionOrder.add(id);
return true;
}Algoritam koristi dva seta za praćenje stanja: visiting su čvorovi u trenutnoj rekurzivnoj gomili, za detekciju petlji; visited su već obrađeni čvorovi, za izbegavanje ponovljenog procesiranja.
Ako se detektuje petlja (visiting.contains(id)), znači da je problem sa zavisnostima zadatka, na primer A zavisi od B, B zavisi od C, C opet zavisi od A. U ovom slučaju plan ne može da se izvrši, treba prijaviti grešku.
Upravljanje stanjem plana
Izvršni plan takođe ima svoje stanje:
CREATED: upravo kreiran, još nije počelo izvršenjeRUNNING: trenutno se izvršavaCOMPLETED: svi zadaci su završeniFAILED: neki zadaci nisu uspeliCANCELLED: otkazano
Promena stanja određuje se rezultatima izvršenja:
public void markStarted() {
this.status = PlanStatus.RUNNING;
this.startTime = System.currentTimeMillis();
}
public void markCompleted() {
this.status = PlanStatus.COMPLETED;
this.endTime = System.currentTimeMillis();
}
public boolean hasFailed() {
return tasks.values().stream()
.anyMatch(t -> t.getStatus() == TaskStatus.FAILED);
}Stanje na nivou plana omogućava korisniku da brzo razume celokupno stanje izvršenja, bez potrebe da proverava sve zadatke pojedinačno.
03. Implementacija planera
Planer je zadužen da razlaže korisničke kompleksne zadatke na izvršive planove.
public class Planner {
private final GLMClient llmClient;
public ExecutionPlan createPlan(String goal) throws IOException {
// 1. napravi planirajući prompt
List<Message> messages = Arrays.asList(
Message.system(PLANNING_PROMPT),
Message.user("Molim te da napraviš izvršni plan za sledeći zadatak:\n" + goal)
);
// 2. pozovi LLM da generiše plan
ChatResponse response = llmClient.chat(messages, null);
// 3. parsiraj JSON plan
return parsePlan(goal, response.content());
}
}
Inženjering prompt-a za planiranje
Ključno je dati LLM-u jasno uputstvo, da izda standardno formatirani plan.
Dizajn prompt-a ima nekoliko principa:
Prvi, jasno specificiraj format izlaza. Kaži LLM-u da mora izdati JSON, i dati kompletan primer.
Molim te izadaj izvršni plan u sledećem JSON formatu:
{
"summary": "Sažetak zadatka",
"tasks": [
{
"id": "task_1",
"description": "Opis zadatka",
"type": "FILE_READ",
"dependencies": []
}
]
}Drugi, definiši tipove zadataka. Nabroji sve dostupne tipove zadataka i namene, neka LLM zna koju tip kada koristiti.
Dostupni tipovi zadataka:
- FILE_READ: čitanje fajla, za dobijanje informacija
- FILE_WRITE: pisanje fajla, za slanje rezultata
- COMMAND: izvršenje Shell komande, za kompajliranje, pokretanje itd.
- ANALYSIS: analiza rezultata, za međurešenske odluke
- VERIFICATION: provera rezultata, za proveru tačnostiTreći, daj pravila ograničenja. Jasno specificiraj granularnost zadataka, način izražavanja zavisnosti itd.
Pravila:
1. Svaki zadatak mora imati jedinstveni ID (npr. task_1, task_2)
2. dependencies nabroji ID-ove zavisnih zadataka
3. Zadaci treba da budu poređani po redosledu izvršenja
4. Opisi zadataka trebaju biti konkretni i jasni
5. Kompleksni zadaci se razlažu na 5-10 podzadatakaParsiranje LLM izlaza
Nakon što LLM izda JSON, treba ga parsirati i konstruisati Task objekte. Ovde postoji jedan detalj: ID-ovi zadataka koje je generisao LLM mogu biti duplikati ili nefomatirani, treba ih ponovo mapirati.
private ExecutionPlan parsePlan(String goal, String planJson) throws IOException {
// očisti moguće markdown kodne blokove
String cleaned = planJson.replaceAll("```json\\s*", "")
.replaceAll("```\\s*", "")
.trim();
JsonNode root = mapper.readTree(cleaned);
String summary = root.path("summary").asText();
JsonNode tasksNode = root.path("tasks");
ExecutionPlan plan = new ExecutionPlan(generatePlanId(), goal);
plan.setSummary(summary);
// prvi prolaz: kreiraj zadatke, ne obrađuj zavisnosti
Map<String, String> idMapping = new HashMap<>();
int taskIndex = 1;
for (JsonNode taskNode : tasksNode) {
String originalId = taskNode.path("id").asText();
String newId = "task_" + taskIndex++;
idMapping.put(originalId, newId);
// kreiraj zadatak...
Task task = new Task(newId, description, type);
plan.addTask(task);
}
// drugi prolaz: obradi zavisnosti
// ...
}
Razlog za dva prolaza je: LLM može prvo definisati task_2, zatim task_1, ali task_2 zavisi od task_1. Prvi prolaz kreira sve zadatke, drugi prolaz uspostavlja zavisnosti, izbegava problem unaprednog referenciranja.
Ponovno planiranje
Ako se u toku izvršenja neki korak ne uspe, može se ponovno planirati na osnovu već završenog napretka:
public ExecutionPlan replan(ExecutionPlan failedPlan, String failureReason) {
// napravi kontekst: završeni zadaci + razlog neuspeha
String context = buildContext(failedPlan, failureReason);
// ponovno generiši plan
return createPlan(context);
}Tako čak i ako se sredina neuspešno završi, ne mora početi od početka, završeni zadaci se mogu zadržati.
04. PlanExecuteAgent
Sada integrišemo planer i izvršilac, implementiramo PlanExecuteAgent:
public class PlanExecuteAgent {
private final GLMClient llmClient;
private final ToolRegistry toolRegistry;
private final Planner planner;
public String run(String userInput) {
// 1. kreiraj izvršni plan
ExecutionPlan plan = planner.createPlan(userInput);
// 2. prikaži plan
System.out.println(plan.visualize());
// 3. izvrši plan
for (String taskId : plan.getExecutionOrder()) {
Task task = plan.getTask(taskId);
executeTask(task);
}
// 4. vrati rezultat
return buildResult(plan);
}
}Pametno prebacivanje režima
Jednostavni zadaci ne zahtevaju planiranje. Dodamo heurističku odluku:
private boolean shouldPlan(String input) {
// sadrži više ključnih reči aktivnosti ili duže je od 50 karaktera, zahteva planiranje
String[] keywords = {"kreiraj", "napiši", "čitaj", "izvrši", "zatim", "onda"};
int actionCount = 0;
for (String keyword : keywords) {
if (input.contains(keyword)) actionCount++;
}
return actionCount >= 3 || input.length() > 50;
}Jednostavni zadaci koriste ReAct, kompleksni zadaci koriste Plan-and-Execute, automatski biraju optimalni režim.
05. Vizuelizacija plana
Izvršni plan može se vizuelno prikazati, korisnik jasno vidi šta Agent treba da radi:

U toku izvršenja se ažurira statusna ikona u realnom vremenu: ⏳ → ▶️ → ✅/❌
06. Testiranje
Kompajlirajte i pokrenite:
mvn clean package
java -jar target/paicli-1.0-SNAPSHOT.jarUnesite /plan da uđete u plan režim, zatim unesite prompt: napravi Java projekat po imenu demo, napiši Hello klasu koja ispisuje Hello World, zatim kompajliraj i pokreni

Zatim počinje izvršenje plana.

Ovde možemo dodati interakciju, da pitamo korisnika da li ima nešto da dopuni, da li želi da promeni plan, zatim počne izvršenje plana.
Neka Codex dopuni ovaj korak.

Imamo.

Celi proces je jasno vidljiv, svaki korak se zna šta se radi.
07. Poređenje sa ReAct
Dva režima ima svoje prednosti i mane, različite scenarije primene.
| Karakteristika | ReAct | Plan-and-Execute |
|---|---|---|
| Broj LLM poziva | Više (svaki korak) | Manje (samo planiranje) |
| Brzina izvršenja | Sporo (mnogo mrežnih povrata) | Brzo (više lokalnog izvršenja) |
| Potrošnja Token-a | Visoka | Niska |
| Fleksibilnost | Visoka (bilo kada se prilagođava) | Niska (izvršava po planu) |
| Predvidljivost | Niska | Visoka |
| Oporavak od grešaka | Lako (bilo kada se menja) | Zahteva ponovno planiranje |
| Scenariji primene | Jednostavni/istraživački zadaci | Kompleksni/deterministički zadaci |
Kad koristiti ReAct
- Zadatak je jednostavan, 1-3 koraka dovoljni
- Potrebne su istraživačke operacije, nisu sigurni konkretne korake
- Korisnik želi da vidi proces razmišljanja
- Potrebna je česta ljudsko-računarska interakcija
Na primer „prikaži šta fajlove trenutni direktorijum ima“, ReAct jednim korakom završava.
Kad koristiti Plan-and-Execute
- Zadatak je kompleksan, zahteva više koraka
- Postoje jasne zavisnosti između koraka
- Teži se efikasnost izvršenja
- Potreban je predvidljiv proces izvršenja
Na primer „izgradi kompletan Web projekat, uključujući prednji-stranu, zadnji-stranu, bazu podataka, implementaciju“, Plan-and-Execute je pogodnije.
Mešovita upotreba
U stvarnom proizvodu, dva režima mogu se mešovito koristiti:
1. Koristi Plan-and-Execute za generalni plan
2. Unutar svakog zadatka koristi ReAct za izvršenje
3. Ako neki korak ne uspe, koristi ReAct za analizu razloga i odluku da li ponoviti ili ponovno planiratiOvaj mešoviti režim kombinuje efikasnost i fleksibilnost, Claude Code i Codex interno koriste sličnu arhitekturu.
08. Napredno: paralelno izvršenje
Trenutna implementacija je serijska, ali nezavisni zadaci u DAG mogu se izvršavati paralelno.
// dobavi sve izvršive zadatke (sve zavisnosti su završene)
List<Task> executableTasks = plan.getExecutableTasks();
// paralelno izvršenje
List<CompletableFuture<Void>> futures = executableTasks.stream()
.map(task -> CompletableFuture.runAsync(() -> executeTask(task)))
.toList();
// sačekaj sve da se završe
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();Na primer „kreiraj projekat“ i „napiši README“ mogu istovremeno, dodatno povećavaju efikasnost.
Mogući problemi paralelnog izvršenja
Paralelno izvršenje može naići na takve probleme:
- Sukob resursa: dva zadataka istovremeno pišu u isti fajl, dolazi do gubitka podataka.
- Haos u izlazu: logovi dva zadataka istovremeno se šalju, korisnik ne vidi koji je koji.
- Kompleksno rukovanje greškama: jedan zadatak ne uspe, šta sa ostalim koji se trenutno izvršavaju?
Mi već implementirali.
Prompt:
/plan molim te da razložiš zadatak u paralelne DAG-e:
1. pročitaj pom.xml
2. navedi fajlove u src/main/java
3. navedi fajlove u src/test/java
4. pročitaj prvih 80 linija README.md
5. konačno sumiraj gore navedeno, reci mi kako se projekat gradi, strukturu izvornog koda i testne situacije.
Zahtev: prvih 4 zadataka su međusobno nezavisni paralelno se izvršavaju, samo 1. zadatak zavisi od prethodna 4; u izvršnom logu jasno naglasi koji zadaci se izvršavaju paralelno.
Počinje paralelno izvršenje zadataka.

Kad se svi paralelni zadaci završe, počinje sumiranje.

Inteligentnije planiranje
Trenutni planer samo jednom poziva LLM, generiše kompletni plan. Ali kompleksni zadaci često zahtevaju slojevito planiranje:
Prvi sloj planiranja: odredi glavne faze
- Faza 1: postavljanje okoline
- Faza 2: razvoja ključnih funkcionalnosti
- Faza 3: testiranje i provera
Drugi sloj planiranja: detalizuje svaku fazu
- Faza 1 uključuje: instaliranje zavisnosti, konfigurisanje okoline, inicijalizacija projekta
- Faza 2 uključuje: pisanje modula A, pisanje modula B, integraciono testiranjePrednost slojevitog planiranja: visoki nivo plana je stabilan, detaljni plan niskog nivoa može se fleksibilno prilagoditi. Ako postoji problem u detaljnom planu neke faze, treba samo ponovno planirati tu fazu, ne utiče na celokupnost.
Samokorekcija plana
Plan koji LMK donese nije nužno perfektnan, može nedostajati korake, redosled je pogrešan, zavisnosti su nereasionalne. Treba nam mehanizam samo-korekcije plana.
Jedan metod je verifikacija plana: pre izvršenja, kroz pravila proveriti da li je plan razuman.
public List<String> validatePlan(ExecutionPlan plan) {
List<String> errors = new ArrayList<>();
// proveri da li postoje duplikatni ID
// proveri da li postoje zavisnosti
// proveri da li postoje ciklusne zavisnosti
// proveri da li su tipovi zadataka legalni
return errors;
}Drugi metod je povratna informacija plana: nakon nekoliko koraka izvršenja, oceni kvalitet plana, po potrebi ponovno planiraj.
if (successRate < 0.5) {
// stopa uspeha je preniska, ponovno planiraj
plan = planner.replan(plan, "stopa uspeha prethodnih zadataka je preniska");
}PaiCLI kako napisati u CV?
PaiCLI projekat (2. izdanje) | 2026.04 - 2026.06 | Agent razvoj
Opis projekta: Dodajte Plan-and-Execute mogućnost Agent CLI-u, realizujte rastavljanje zadataka, DAG upravljanje zavisnostima i topološko sortiranje izvršenje.
Tehnološki stack: Java 17, Maven, GLM-5.1 API, DAG topološko sortiranje, JSON parsiranje
Ključne odgovornosti:
- Dizajnirao Task model zadataka, implementirao 6 tipova zadataka i 5 stanja promene, podržava dvosmerno praćenje zavisnosti zadataka i DAG reprezentaciju
- Implementirao topološki sort algoritam baziran na DFS, konvertuje task DAG u linearni redosled izvršenja, automatski detektuje ciklusne zavisnosti i prijavljuje grešku, osigurava da se zadaci izvršavaju po redosledu; koristeći thread pool paralelno izvršava nezavisne višestruke zadatke, u poređenju sa serijskim izvršenjem efikasnost je značajno povećana
- Razvio Planner planer, koristeći LLM da kompleksne zadatke razloži na 5-10 izvršivih podzadataka, kroz JSON format izdaje plan, realizuje ID mapiranje i tretiranje unaprednjih referenci
- Implementirao PlanExecuteAgent, podržava automatsko prebacivanje između ReAct i Plan-and-Execute dva režima na osnovu kompleksnosti zadataka
- Integrisao JLine3 implementirao interaktivnu komandnu liniju, podržava istoriju komandi (gornje/donje strelice), Tab automatsko dopunjavanje, uređivanje linija i sintaksno isticanje, korisničko iskustvo je približno nativnom Shell-u
