400 linija Java koda ručno AI Agent, ReAct petlja + Tool Call, pokrenuo sam
Iskreno, neko vreme već koristim Claude Code, Qoder CLI i Codex za AI programiranje, uvek me je zanimalo kako ovo zapravo radi.
Izgledaju kao jednostavne komandne linije, ali iza toga mogu da razumeju moje zahteve, pozivaju različite alate, čitaju i pišu fajlove, izvršavaju komande, čak mogu sami menjati kod.

Kao princip ovo radi?
Danas ćemo od nule napraviti najjednostavniji Agent CLI u Javi. Može da konfiguriše GLM-5.1 API Key, može da primi tvoj unos, ima Agent Loop, može Tool Call, može programirati ili izbacivati sadržaj na osnovu konteksta.
Jedna super uprošćena verzija Claude Code-a, oko 400 linija koda može da se pokrene.

01, Osnovni princip Agent-a
Pre nego što počnemo sa kodiranjem, razumimo šta je Agent.
Agent se sastoji od tri stvari: zaključivanje (Reasoning), delovanje (Acting), posmatranje (Observing). Ova tri u petlji čine poznati ReAct mod.

Proces je ovakav:
- Unesiš zadatak
- LLM razmišlja: šta treba da uradim? treba li pozvati alat?
- Ako treba alat, LLM izlazi sa zahtevom za poziv alata
- Agent izvršava alat, dobija rezultat
- Rezultat se hrani LLM-u, nastavlja razmišljati
- Ponavlja se 2-5, dok LLM ne smatra da je zadatak završen
- Izlazi se konačan rezultat
Ova petlja je duša Agent-a. Claude Code, Qoder CLI, OpenClaw, u suštini su sve različite implementacije ove petlje.
02, Struktura projekta
Koristićemo Java 17 + Maven, bez zavisnosti od bilo kojeg Agent framework-a, sve od nule ručno.
paicli/
├── pom.xml
├── .env
└── src/main/java/com/paicli/
├── cli/Main.java # Ulazna klasa
├── agent/Agent.java # Srce Agent-a (ReAct petlja)
├── llm/GLMClient.java # GLM-5.1 API klijent
└── tool/ToolRegistry.java # Registru alata
Maven zavisnosti
Potrebne su nam ove zavisnosti:
- Jackson: za JSON obradu
- OkHttp: za slanje HTTP zahteva
- SLF4J: jednostavno logovanje
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>03, GLM-5.1 API klijent
Agent mora razumeti naše prompt-e, mora moći da pozove veliki model. Zato prvo treba da omotačimo GLMClient, podržava obični razgovor i pozivanje alata.
public class GLMClient {
private static final String API_URL =
"https://open.bigmodel.cn/api/paas/v4/chat/completions";
private static final String MODEL = "glm-5.1";
private final String apiKey;
private final OkHttpClient httpClient;
public GLMClient(String apiKey) {
this.apiKey = apiKey;
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.build();
}
}Srce je metod chat, prima istoriju poruka i listu alata, vraća LLM odgovor.
Format poruka
GLM-5.1 API je kompatibilan sa OpenAI formatom, poruke imaju četiri uloge:
system: sistemski prompt, definiše identitet i sposobnosti Agent-auser: korisnički unosassistant: odgovor pomoćnika, može sadržati tekst ili poziv alatatool: rezultat izvršenja alata
public record Message(String role, String content,
List<ToolCall> toolCalls, String toolCallId) {
public static Message system(String content) {
return new Message("system", content, null, null);
}
public static Message user(String content) {
return new Message("user", content, null, null);
}
public static Message assistant(String content) {
return new Message("assistant", content, null, null);
}
public static Message tool(String toolCallId, String content) {
return new Message("tool", content, null, toolCallId);
}
}Format definicije alata
Da bi LLM zna koji alati su dostupni, treba ih definisati u specifičnom formatu:
public record Tool(String name, String description, JsonNode parameters) {}parameters je JSON Schema, opisuje koje parametre alat zahteva, koji su tipovi parametara. Npr. definicija parametara alata write_file:
{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Putanja fajla"
},
"content": {
"type": "string",
"description": "Sadržaj fajla"
}
},
"required": ["path", "content"]
}LLM će na osnovu ove definicije, kada treba, generisati ispravne parametre.
Kompletan metod chat
public ChatResponse chat(List<Message> messages, List<Tool> tools)
throws IOException {
// konstruiše se telo zahteva
ObjectNode requestBody = mapper.createObjectNode();
requestBody.put("model", MODEL);
// dodaje se istorija poruka
ArrayNode messagesArray = requestBody.putArray("messages");
for (Message msg : messages) {
ObjectNode msgNode = messagesArray.addObject();
msgNode.put("role", msg.role());
msgNode.put("content", msg.content());
// ako postoje pozivi alata, serializuje se tool_calls
if (msg.toolCalls() != null && !msg.toolCalls().isEmpty()) {
ArrayNode toolCallsArray = msgNode.putArray("tool_calls");
for (ToolCall tc : msg.toolCalls()) {
ObjectNode tcNode = toolCallsArray.addObject();
tcNode.put("id", tc.id());
tcNode.put("type", "function");
ObjectNode functionNode = tcNode.putObject("function");
functionNode.put("name", tc.function().name());
functionNode.put("arguments", tc.function().arguments());
}
}
// ako je rezultat alata, dodaje se tool_call_id
if (msg.toolCallId() != null) {
msgNode.put("tool_call_id", msg.toolCallId());
}
}
// dodaju se definicije alata
if (tools != null && !tools.isEmpty()) {
ArrayNode toolsArray = requestBody.putArray("tools");
for (Tool tool : tools) {
ObjectNode toolNode = toolsArray.addObject();
toolNode.put("type", "function");
ObjectNode functionNode = toolNode.putObject("function");
functionNode.put("name", tool.name());
functionNode.put("description", tool.description());
functionNode.set("parameters", tool.parameters());
}
}
// šalje se HTTP zahtev
RequestBody body = RequestBody.create(
requestBody.toString(),
MediaType.parse("application/json")
);
Request request = new Request.Builder()
.url(API_URL)
.header("Authorization", "Bearer " + apiKey)
.post(body)
.build();
// parsira se odgovor
try (Response response = httpClient.newCall(request).execute()) {
String responseBody = response.body().string();
JsonNode root = mapper.readTree(responseBody);
// izvlače se sadržaj poruka, pozivi alata, token korišćenje, itd.
// ...
}
}Ovde postoji jedna ključna tačka: poziv alata.
Kada LLM odluči da pozove alat, vraća niz tool_calls, sadrži naziv alata i parametre. Agent nakon izvršenja alata rezultat treba vratiti LLM-u kao poruku uloge tool, tako LLM može nastaviti razmišljati.
Ovaj povratni proces je srce ReAct mod-a.
LLM ne izvršava zadatak direktno, već kroz poziv alata "deluje", zatim posmatra rezultat delovanja, pa odlučuje šta sledeće. Ovaj razdvajanje omogućava LLM-u da se fokusira na razmišljanje, alat se fokusira na izvršenje, svaki ima svoju ulogu.
Zašto GLM-5.1
Izbor GLM-5.1 ima nekoliko razloga:
Prvi, dobra podrška za poziv alata. GLM-5.1 podrška za Function Calling je vrlo stabilna, može tačno razumeti definiciju alata, generisati ispravne parametre.
Drugi, ja sam max član. Hahaha.
Naravno, ovaj framework lako se proširuje na druge modele. Dok model podržava OpenAI format API-a, promena API_URL i MODEL je dovoljna.
04, Registru alata
Agent mora moći da radi nešto korisno, treba mu set alata. Implementiraćemo nekoliko osnovnih alata:
read_file: čitanje fajlawrite_file: pisanje fajlalist_dir: listanje direktorijumaexecute_command: izvršenje Shell komandecreate_project: kreiranje strukture projekta
public class ToolRegistry {
private final Map<String, Tool> tools = new HashMap<>();
public ToolRegistry() {
registerFileTools();
registerShellTools();
registerCodeTools();
}
}Definicija alata
Svaki alat sadrži četiri dela: naziv, opis, definiciju parametara, logiku izvršenja.
public record Tool(
String name,
String description,
JsonNode parameters,
ToolExecutor executor
) {}
public interface ToolExecutor {
String execute(Map<String, String> args);
}Opis i definicija parametara se šalju LLM-u, da LLM zna kada treba koristiti ovaj alat, koje parametre treba. Logika izvršenja je stvarni Java kod, zadužen za završetak zadatka.
Alati za rad sa fajlovima
Rad sa fajlovima je najosnovnija sposobnost.
read_file omogućava Agent-u da vidi kod, write_file omogućava Agent-u da piše kod, list_dir omogućava Agent-u da pregleda strukturu projekta.
private void registerFileTools() {
// read_file alat
tools.put("read_file", new Tool(
"read_file",
"Čita sadržaj fajla, za pregled koda, konfiguracionih fajlova, itd.",
createParameters(new Param("path", "string", "Putanja fajla", true)),
args -> {
String path = args.get("path");
try {
String content = Files.readString(Path.of(path));
return "Sadržaj fajla:\n" + content;
} catch (Exception e) {
return "Čitanje fajla nije uspelo: " + e.getMessage();
}
}
));
}Obrati pažnju na opis alata, treba jasno napisati namenu. LLM na osnovu opisa odlučuje kada pozvati ovaj alat. Što je opis jasniji, to je LLM-ova odluka tačnija.
Alati Shell komandi
execute_command je najmoćniji alat, Agent-u omogućava da izvrši bilo koju Shell komandu. Takođe je najopasniji, u stvarnom proizvodu treba dodati kontrolu dozvola.
tools.put("execute_command", new Tool(
"execute_command",
"Izvršava Shell komandu, za kompajliranje, pokretanje, Git operacije, itd.",
createParameters(new Param("command", "string", "Komanda za izvršavanje", true)),
args -> {
String command = args.get("command");
try {
ProcessBuilder pb = new ProcessBuilder("bash", "-c", command);
pb.redirectErrorStream(true);
Process process = pb.start();
// čitanje izlaza komande
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
int exitCode = process.waitFor();
return String.format("Komanda je završena (exit code: %d)\n%s",
exitCode, output);
} catch (Exception e) {
return "Izvršavanje komande nije uspelo: " + e.getMessage();
}
}
));Ovde se ProcessBuilder koristi za izvršenje komande, hvata standardni izlaz i izlaznu grešku. Agent može na osnovu izlaznog koda proceniti da li je komanda uspešna, na osnovu izlaza odlučiti šta sledeće.
Svaki alat sadrži tri dela: naziv, opis, definiciju parametara, logiku izvršenja. Opis i definicija parametara se šalju LLM-u, da LLM zna kada treba koristiti ovaj alat.
Npr. definicija alata write_file:
tools.put("write_file", new Tool(
"write_file",
"Piše sadržaj u fajl",
createParameters(
new Param("path", "string", "Putanja fajla", true),
new Param("content", "string", "Sadržaj fajla", true)
),
args -> {
String path = args.get("path");
String content = args.get("content");
Files.writeString(Path.of(path), content);
return "Fajl je upisan: " + path;
}
));
Rezultat izvršenja alata se vraća LLM-u kao tool poruka, LLM na osnovu rezultata odlučuje o sledećem koraku.
Generisanje definicije parametara
Parametri alata se definišu preko JSON Schema, napisali smo pomoćnu metodu createParameters:
private JsonNode createParameters(Param... params) {
ObjectNode parameters = mapper.createObjectNode();
parameters.put("type", "object");
ObjectNode properties = parameters.putObject("properties");
ArrayNode required = parameters.putArray("required");
for (Param param : params) {
ObjectNode prop = properties.putObject(param.name());
prop.put("type", param.type());
prop.put("description", param.description());
if (param.required()) {
required.add(param.name());
}
}
return parameters;
}Ovaj metod prima promenljiv broj parametara, svaki parametar sadrži naziv, tip, opis, da li je obavezan. Generisani JSON Schema odgovara OpenAI specifikaciji poziva funkcija, GLM-5.1 ga može ispravno parsirati.
Dinamička registracija alata
Trenutni alati su hardcoded, u praksi se mogu učiniti dinamičkim. Npr. učitati iz konfiguracionog fajla, iz plugin sistema, čak Agent sam može definisati alate.
Dinamička registracija se svodi na jedinstven interfejs:
public interface ToolProvider {
List<Tool> getTools();
}Različite implementacije mogu učitavati alate iz različitih izvora:
// učitavanje iz JSON konfiguracionog fajla
public class JsonToolProvider implements ToolProvider {
private final String configPath;
@Override
public List<Tool> getTools() {
// parsiranje JSON, kreiranje Tool instanci
}
}
// učitavanje iz skeniranja anotacija
public class AnnotationToolProvider implements ToolProvider {
private final String packageName;
@Override
public List<Tool> getTools() {
// skeniranje @Tool anotacije, kreiranje Tool instanci
}
}Ovaj dizajn čini sistem alata proširivim. Claude Code-ov Skills sistem je u suštini jedna implementacija dinamičke registracije alata.
05, Srce Agent-a: ReAct petlja
Sada dolazimo do najvažnijeg dela — klase Agent. Implementira ReAct petlju, koordinira LLM pozive i izvršenje alata.
public class Agent {
private final GLMClient llmClient;
private final ToolRegistry toolRegistry;
private final List<Message> conversationHistory;
private static final int MAX_ITERATIONS = 10;
public Agent(String apiKey) {
this.llmClient = new GLMClient(apiKey);
this.toolRegistry = new ToolRegistry();
this.conversationHistory = new ArrayList<>();
// dodaje se sistemski prompt
conversationHistory.add(Message.system(SYSTEM_PROMPT));
}
}Sistemski prompt govori LLM-u ko je, šta može, koji alati su dostupni:
private static final String SYSTEM_PROMPT = """
Ti je inteligentni pomoćnik za programiranje, možeš pomoći korisnicima da obave različite zadatke.
Možeš koristiti sledeće alate za završetak zadataka:
1. read_file - čita sadržaj fajla
2. write_file - piše sadržaj fajla
3. list_dir - lista sadržaj direktorijuma
4. execute_command - izvršava Shell komandu
5. create_project - kreira novu strukturu projekta
Kada treba raditi sa fajlovima, izvršavati komande ili kreirati projekat, koristi poziv alata.
Nakon korišćenja alata, na osnovu rezultata nastavi razmišljati o sledećem koraku.
Molim te odgovaraj korisniku na srpskom.
""";
Glavna logika ReAct petlje:
public String run(String userInput) {
// dodaje se korisnički unos
conversationHistory.add(Message.user(userInput));
int iteration = 0;
while (iteration < MAX_ITERATIONS) {
iteration++;
// poziva se LLM
ChatResponse response = llmClient.chat(
conversationHistory,
toolRegistry.getToolDefinitions()
);
// ako postoje pozivi alata
if (response.hasToolCalls()) {
// evidentira se asistent poruka
conversationHistory.add(
Message.assistant(response.content(), response.toolCalls())
);
// izvršavaju se svi pozivi alata
for (ToolCall toolCall : response.toolCalls()) {
String result = toolRegistry.executeTool(
toolCall.function().name(),
toolCall.function().arguments()
);
// evidentira se rezultat alata
conversationHistory.add(
Message.tool(toolCall.id(), result)
);
}
// nastavlja se petlja, LLM nastavlja razmišljati na osnovu rezultata
continue;
} else {
// nema poziva alata, zadatak je završen
conversationHistory.add(
Message.assistant(response.content())
);
return response.content();
}
}
return "Dostignuta je gornja granica broja iteracija";
}Ključ ove petlje: dok LLM još uvek traži poziv alata, petlja se nastavlja. Samo kada LLM direktno odgovori sadržajem (bez tool_calls), smatra se da je zadatak završen.
Zašto je potrebna petlja
Možeš pitati: zašto ne može jednim pozivom završiti? Zašto je potrebna petlja?
Zato što složeni zadaci često zahtevaju više koraka. Npr. "napravi Spring Boot projekat i napiši HelloController":
- Prvo pozovi
create_projectza kreiranje strukture projekta - Zatim pozovi
write_fileza pisanjepom.xml - Zatim pozovi
write_fileza pisanje glavne klase - Zatim pozovi
write_fileza pisanje Controller-a - Konačno pozovi
execute_commandza kompajliranje i verifikaciju
Svaki korak zavisi od prethodnog rezultata. LLM treba na osnovu prethodnog rezultata da odluči šta sledeće, to zahteva petlju.
Prevencija beskonačne petlje
Petlja mora imati uslov završetka, inače može ući u beskonačnu petlju. Postavili smo MAX_ITERATIONS = 10, preko se prisilno izlazi.
U stvarnom proizvodu, potrebni su i drugi mehanizmi zaštite:
- Vremensko ograničenje: jedan zadatak ne sme trajati duže od N sekundi
- Token ograničenje: ukupna potrošnja Token ne sme preći gornju granicu
- Korisnička potvrda: pre opasnih operacija pitati korisnika
- Detekcija stanja: detektovati da li se petlja zaglavila u ponavljajućem režimu
Upravljanje kontekstom
Istoria razgovora conversationHistory je "memorija" Agent-a. Sadrži kompletnu razgovornu proceduru, LLM može razumeti kontekst zadatka.
Ali kontekst ne može neograničeno rasti. Kada istorija postane preduga, treba:
- Sažimanje: stari razgovor se sažima u sažetak
- Odsecanje: odbacuje se najstariji razgovor
- Vektorizacija: istorija se čuva u vektorskoj bazi, pretražuje se po potrebi
Claude Code-ovo upravljanje kontekstom je vrlo komplikovano, može raditi sa stotinama hiljada reči koda. Naša uprošćena verzija koristi jednostavnu listu, ali princip je isti.
Obrada grešaka
U stvarnom izvršenju ima mnogo grešaka: mrežni timeout, API ograničenje, neuspeh izvršenja alata, itd. Potrebna je kompletna obrada grešaka:
try {
ChatResponse response = llmClient.chat(conversationHistory, tools);
// ...
} catch (IOException e) {
// mrežna greška, može se pokušati ponovo
if (retryCount < MAX_RETRIES) {
retryCount++;
continue;
}
return "Mrežna greška: " + e.getMessage();
} catch (Exception e) {
// druge greške, vraća se poruka o grešci
return "Greška pri izvršenju: " + e.getMessage();
}Poruka o grešci takođe se šalje LLM-u, da LLM zna šta se desilo, ako može dati predlog za popravku.
06, Interaktivni CLI
Konačno, treba nam interaktivna komandna linija da korisnik može unositi naredbe i videti rezultate.
public class Main {
public static void main(String[] args) {
printBanner();
// učitava se API Key
String apiKey = loadApiKey();
if (apiKey == null || apiKey.isEmpty()) {
System.err.println("❌ Greška: GLM_API_KEY nije pronađen");
System.exit(1);
}
// kreira se Agent
Agent agent = new Agent(apiKey);
// interaktivna petlja
Scanner scanner = new Scanner(System.in);
System.out.println("💡 Savet: ukucaj 'clear' za brisanje istorije, 'exit' za izlaz\n");
while (true) {
System.out.print("👤 Ti: ");
String input = scanner.nextLine().trim();
if (input.isEmpty()) continue;
if (input.equalsIgnoreCase("exit")) break;
if (input.equalsIgnoreCase("clear")) {
agent.clearHistory();
System.out.println("🗑️ Istorija je obrisana\n");
continue;
}
// izvršava se Agent
String response = agent.run(input);
System.out.println("🤖 Agent: " + response + "\n");
}
}
}Dizajn korisničkog iskustva
Iako je CLI jednostavan, korisničko iskustvo je vrlo važno. Dodali smo nekoliko detalja:
Pokretni banner: program izgleda profesionalnije
private static void printBanner() {
System.out.println("""
╔══════════════════════════════════════════════════════════╗
║ ██████╗ █████╗ ██╗ ██████╗██╗ ██╗ ║
║ ██╔══██╗██╔══██╗██║ ██╔════╝██║ ██║ ║
║ ██████╔╝███████║██║ ██║ ██║ ██║ ║
║ ██╔═══╝ ██╔══██║██║ ██║ ██║ ██║ ║
║ ██║ ██║ ██║███████╗╚██████╗███████╗██║ ║
║ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝╚══════╝╚═╝ ║
║ Jednostavan Java Agent CLI v1.0.0 ║
╚══════════════════════════════════════════════════════════╝
""");
}Emoji indikatori: izlaz je čitljiviji
👤 Ti:označava korisnički unos🤖 Agent:označava Agent-ov odgovor🔧 Izvršavam alat:označava da se poziva alat📊 Token korišćenje:prikazuje potrošnju resursa🗑️✅❌itd. označavaju status
Pomoćne informacije: pri pokretanju se prikazuju dostupne komande
💡 Savet:
- unesi svoje pitanje ili zadatak
- ukucaj 'clear' za brisanje istorije razgovora
- ukucaj 'exit' ili 'quit' za izlazOvi mali detalji čine korišćenje CLI-a ugodnijim.

API Key se učitava iz .env fajla ili promenljive okruženja:
private static String loadApiKey() {
// prvo pokušava da pročita .env iz trenutnog direktorijuma
File envFile = new File(".env");
if (envFile.exists()) {
return readApiKeyFromFile(envFile);
}
// zatim pokušava da pročita iz promenljive okruženja
return System.getenv("GLM_API_KEY");
}07, Pokretanje i testiranje
Kompajliranje i pakovanje:
mvn clean packageKreiranje .env fajla:
GLM_API_KEY=your_api_key_herePokretanje:
java -jar target/paicli-1.0-SNAPSHOT.jar
Probajte ove komande:
Primer 1: Kreiranje projekta
👤 Ti: kreiraj Java projekat demo
🤔 Razmišljam...
🔧 Izvršavam alat: create_project
parametri: {"name":"demo","type":"java"}
rezultat: Projekat je kreiran: demo (tip: java)
📊 Token korišćenje: ulaz=156, izlaz=89
🤖 Agent: Uspešno je kreiran Java projekat "demo", sadrži osnovnu Maven strukturu.
Agent prepoznaje tvoju nameru da kreiraš projekat, poziva create_project alat, prosledi ime projekta i tip. Nakon uspešnog izvršenja alata, Agent ti daje potvrdu.

Primer 2: Višekorakni zadatak
Probajmo još jedan kompletniji:
👤 Ti: u demo/src/main/java/com/example kreiraj Hello.java,
izlazi Hello World
🤔 Razmišljam...
🔧 Izvršavam alat: write_file
parametri: {
"path": "demo/src/main/java/com/example/Hello.java",
"content": "package com.example;\n\npublic class Hello {\n public static void main(String[] args) {\n System.out.println(\"Hello World\");\n }\n}"
}
rezultat: Fajl je upisan: demo/src/main/java/com/example/Hello.java
📊 Token korišćenje: ulaz=312, izlaz=156
🤖 Agent: Hello.java fajl je kreiran, sadrži main metod koji ispisuje "Hello World".Ovaj zadatak zahteva samo jedan korak, ali Agent automatski procesira putanju i ime paketa. Ne moraš mu govoriti kako tačno da napiše, samo opiši zahtev.

Primer 3: Čitanje i izmena
Probajmo pročitati fajl pa izmeniti:
👤 Ti: pročitaj sadržaj demo/src/main/java/com/example/Hello.java,
zatim izlaz promeni u "Hello PaiCLI"
🤔 Razmišljam...
🔧 Izvršavam alat: read_file
parametri: {"path": "demo/src/main/java/com/example/Hello.java"}
rezultat: Sadržaj fajla:
package com.example;
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
🔧 Izvršavam alat: write_file
parametri: {
"path": "demo/src/main/java/com/example/Hello.java",
"content": "package com.example;\n\npublic class Hello {\n public static void main(String[] args) {\n System.out.println(\"Hello PaiCLI\");\n }\n}"
}
rezultat: Fajl je upisan: demo/src/main/java/com/example/Hello.java
📊 Token korišćenje: ulaz=523, izlaz=234
🤖 Agent: Izlaz je promenjen iz "Hello World" u "Hello PaiCLI".
Ovaj zadatak ima dva koraka: prvo čitanje, zatim izmena. Agent automatski planira redosled izvršenja, prvo poziva read_file da dobije sadržaj, zatim write_file da upiše izmenjeni sadržaj.
Primer 4: Izvršenje komande
Probajmo izvršiti Shell komandu:
👤 Ti: uđi u demo direktorijum, kompajliraj i pokreni Hello.java
🤔 Razmišljam...
🔧 Izvršavam alat: execute_command
parametri: {"command": "cd demo && javac src/main/java/com/example/Hello.java"}
rezultat: Komanda je završena (exit code: 0)
🔧 Izvršavam alat: execute_command
parametri: {"command": "cd demo && java -cp src/main/java com.example.Hello"}
rezultat: Komanda je završena (exit code: 0)
Hello PaiCLI
📊 Token korišćenje: ulaz=678, izlaz=312
🤖 Agent: Kompajliranje je uspešno, izlaz pri pokretanju: Hello PaiCLIAgent automatski razbija zadatak: prvo kompajlira, zatim pokreće. I može na osnovu izlaza komande proceniti rezultat.

08, Dublje razumevanje ReAct mod-a
Naš PaiCLI implementirao je najosnovniju ReAct petlju, ali ReAct ima mnogo varijacija i poboljšanja. Razumevanje ovoga je ključno za pravljenje jačeg Agent-a.
Tri moda ReAct-a
Osnovni ReAct: ovo što smo implementirali. LLM svaki korak izlazi Thought + Action, zatim izvršava Action, posmatra rezultat, nastavlja petlju.
Thought: Korisnik hoće da kreiram Java projekat, trebam koristiti create_project alat
Action: create_project({"name": "demo", "type": "java"})
Observation: Projekat je kreiranPlan-and-Execute: prvo se LLM-u daje da napravi kompletan plan, zatim se izvršavaju koraci po red. Pogodno za složene višekorake zadatke.
Plan:
1. Kreirati strukturu projekta
2. Napisati pom.xml
3. Napisati glavnu klasu
4. Napisati test klasu
Zatim se izvršava svaki korak po redu, bez daljeg ponavljanog upita LLM-u.Ovaj mod je efikasniji, jer smanjuje broj LLM poziva. Ali nedostatak mu je manja fleksibilnost, ako jedan korak ne uspe, treba ponovno planirati.
Reflexion: na osnovu ReAct-a dodaje samo-refleksiju. Agent pregleda sopstvene radnje, sumira uspešna iskustva i neuspešne lekcije, čuva u memoriji. Sledeći put kada sretne sličan zadatak, može bolje uraditi.
Thought: Prošli put pri kreiranju projekta nisam proverio da li direktorijum postoji, moguće da je prepisao postojeći fajl. Sledeći put prvo treba list_dir proveriti.Ovaj mod daje Agent-u sposobnost učenja, što više koristi, to je pametniji.
Evolucija Tool Use-a
Naš poziv alata je sinhroni: pozove alat, čeka rezultat, nastavlja dalje. Ali u stvarnosti mnogi alati su asinhroni.
Asinhroni poziv alata: npr. kompajliranje velikog projekta može trajati nekoliko minuta, Agent ne treba da čeka. Može poslati zadatak i raditi nešto drugo, čekati rezultate pa nastaviti.
// slanje asinhronog zadatka
String taskId = tool.executeAsync(args);
// radi nešto drugo...
// proverava rezultate
while (!tool.isComplete(taskId)) {
Thread.sleep(1000);
}
String result = tool.getResult(taskId);Paralelni poziv alata: neki zadaci mogu paralelno da se izvrše. Npr. istovremeno čitanje više fajlova, istovremeno izvršenje više testova.
List<CompletableFuture<String>> futures = toolCalls.stream()
.map(tc -> CompletableFuture.supplyAsync(
() -> executeTool(tc)
))
.toList();
// čeka se završetak svih alata
List<String> results = futures.stream()
.map(CompletableFuture::join)
.toList();LLM može istovremeno zatražiti više poziva alata, Agent paralelno izvršava, na kraju rezultate zajedno vraća. Ovo znatno poboljšava efikasnost.
Napredno inženjeringstvo konteksta
Naše upravljanje kontekst je vrlo jednostavno, samo lista poruka. Ali pravi Agent zahteva komplikovanije inženjeringstvo konteksta.
Slojeviti kontekst: kontekst se deli na više slojeva:
- Sistemski sloj: identitet, sposobnosti, ograničenja Agent-a
- Sloj sesije: istorija trenutnog razgovora
- Sloj zadatka: informacije relevantne za trenutni zadatak
- Sloj alata: definicije i primeri alata
Različiti slojevi imaju različitu učestalost ažuriranja i strategiju zadržavanja. Sistemski sloj se skoro ne menja, sloj zadatka se često ažurira.
Vektorska pretraga: kada je kontekst predugač, može se čuvati u vektorskoj bazi. Po potrebi se prema upitu pretražuju relevantni odlomci.
// čuvaj u vektorskoj bazi
vectorStore.add("Fajl:Hello.java", embedding(content));
// pretraga relevantnog konteksta
List<String> relevant = vectorStore.search(
embedding("Gde je klasa Hello definisana?"),
topK: 3
);Ovo omogućava Agent-u da radi sa masovnim informacijama, npr. celom kôdnom bazom.
Strukturirani izlaz: prisiljava LLM da izlazi u specifičnom formatu, olakšava parsiranje.
{
"thought": "Treba da kreiram projekat",
"action": {
"tool": "create_project",
"args": {"name": "demo", "type": "java"}
},
"confidence": 0.95
}Koristi JSON Schema za ograničenje izlaza, pouzdanije je od parsiranja prirodnog jezika.
Bezbednost i sandbox
Naš Agent direktno izvršava Shell komande, u proizvodnom okruženju je vrlo opasno. Pravi Agent treba sigurnosne mehanizme.
Kontrola dozvola: različite operacije zahtevaju različite nivoe dozvola.
- Nivo 1: samo operacije čitanja (read_file, list_dir)
- Nivo 2: operacije pisanja (write_file)
- Nivo 3: operacije izvršenja (execute_command)
Korisnik može za različite zadatke postaviti nivo dozvole, sprečava se nepravilna operacija.
Izvršenje u sandbox-u: opasne operacije se izvršavaju u izolovanom okruženju.
// izvršavanje komande u Docker kontejneru
ProcessBuilder pb = new ProcessBuilder(
"docker", "run", "--rm", "-v", workDir + ":/work",
"sandbox-image", "bash", "-c", command
);Čak i ako komanda ima problem, neće uticati na domaćin mašinu.
Ljudska potvrda: pre opasne operacije pitati korisnika.
🤖 Agent: Izvršiću komandu: rm -rf /, potvrdi? [y/N]
👤 Ti: n
🤖 Agent: Operacija je otkazanaClaude Code ima ovaj mehanizam, pre izvršenja rm i sličnih opasnih komandi zahteva potvrdu.
Od PaiCLI do proizvodnog Agent-a
Naših 400 linija koda je početna tačka, da bi se pretvorilo u proizvod, treba:
Posmatljivost: čuvati komplet izvršni dnevnik, olakšava otklanjanje grešaka i reviziju.
logger.info("[Agent] početak zadatka: {}", task);
logger.info("[Agent] poziv alata: {} parametri: {}", toolName, args);
logger.info("[Agent] rezultat alata: {}", result);Persistencija: istorija razgovora i stanje izvršenja se čuvaju u bazi podataka, podržava se nastavak prekidene tačke.
Više korisnika: podržava više korisnika istovremeno, sesije su izolovane.
Sistem dodataka: dinamičko učitavanje alata, korisnik može sam proširiti.
Veb interfejs: pored CLI, treba pružiti i Web UI, prijatniju interakciju.
Ovo su smerovi u kojima PaiCLI trenutno razvija. Od jednostavne CLI do kompletnog proizvoda, potreban je veliki inženjerski posao, ali osnovni princip se ne menja: ReAct petlja + poziv alata + upravljanje kontekstom.
Razumevanje ovog srca, zatim bilo koji Agent framework može brzo da se nauči. Jer sve varijante su nadogradnje i optimizacije na ovoj osnovi.
09, Ideje za proširenje
Ovaj osnovni framework može se proširiti u više smerova:
Dodavanje alata: može se integrisati više API-ja, npr. pretraga, baza podataka, Git operacije, itd.
Poboljšanje konteksta: trenutni kontekst je prosta lista, može se implementirati pametnije sažimanje i učitavanje konteksta.
Multi-Agent saradnja: može se dizajnirati više Agent-a, svaki zadužen za različitu oblast, saradjuju kroz poruke.
Grafički interfejs: može se koristiti JavaFX ili Swing za jednostavno GUI, iskustvo bi bilo bolje.

ending
Od nule implementirati Agent CLI, najveća korist je razumevanje ReAct mod-a.
Zaključivanje, delovanje, posmatranje, ova tri u petlji, Agent može završiti složene zadatke. LLM je zadužen za razmišljanje, alat je zadužen za izvršenje, Agent framework je zadužen za koordinaciju.
Ovih 400 linija koda koje smo napisali, iako jednostavne, sadrže sve ključne elemente Agent-a:
- LLM poziv
- registracija i izvršenje alata
- ReAct petlja
- upravljanje kontekstom
[Razumevanje principa je važnije od znati uputstvo.]
Kada razumiješ kako Agent radi, kada koristiš Claude Code, Qoder CLI, OpenClaw, možeš bolje razumeti njihovo ponašanje, i možeš ih bolje kontrolisati.

Kod je objavljen na GitHub-u: github.com/itwanger/paicli, dobrodošli da zvezdate i otvorite PR. Ako imate pitanja, možete komentarisati u komentarima, ja ću se truditi da odgovorim.
Nadam se da je ovaj članak koristan, vidimo se sledeći put.
Kako napisati životopis
PaiCLI Agent projekat (1. izdanje) | 2025.01 - 2025.01 | Agent razvoj
Opis projekta: Od nule implementiran Java Agent CLI, podržava ReAct i Tool Call, može na osnovu prirodnih jezičkih instrukcija obavljati operacije nad fajlovima, generisanje koda i izvršenje komandi.
Tehnološki stack: Java 17, Maven, OkHttp, Jackson, GLM-5.1
Ključne odgovornosti:
- Na osnovu Qoder CLI/Codex/Claude Code od nule sagrađen Agent CLI projekat, implementiran osnovni ReAct (razmišljanje-delovanje-posmatranje) krug, podržava do 10 krugova složenih zadataka
- Koristeći OkHttp omotan GLM-5.1 Coding Plan klijent, implementirana podrška za Tool Call poruke serijski i parsiranje, može obrađivati četiri uloge poruka system/user/assistant/tool
- Dizajniran ToolRegistry registar alata, implementirani pet ugrađenih alata read_file, write_file, list_dir, execute_command, create_project, podržava JSON Schema definiciju parametara
- Implementiran interaktivni CLI interfejs, podržava automatsko učitavanje API Key iz .env fajla i promenljive okruženja, pruža komande clear, exit za upravljanje istorijom razgovora
