Kratkoročno sećanje + dugoročno sećanje + Context kompresija, Agent sa Memory više ne zaboravlja.
Nakon što smo završili PaiCLI 2. broj Plan-Execute, jedan problem me stalno muči——sećanje Agent-a je kao zlatna ribica, pričajući priča zaboravlja šta je ranije govorio.
Kažeš mu "sviadji da koristiš JDK 17", obriši razgovor još jedan krug, on opet glupo generiše JDK 8 kôd.
Ovaj broj rešimo ovaj problem, dodaj Agent-u kompletan Memory sistem.

Celi Memory podeljen je u tri dela.
Kratkoročno sećanje, upravlja tekući kontekst razgovora——šta je korisnik rekao, šta je alat vratio, koje je odluke Agent pravio. LLM po sebi je bez stanja, svaki zahtev je nezavisan, ne seća se šta se prethodnom krugu pričalo. Kratkoročno sećanje je umesto njega "upamti", sledeći put kad unosiše zajedno sa istorijskim porukama.

Ali kratkoročno sećanje ima jednu fatalnu manu——kad sesija zatvori, sve je nestalo. Sledeći put kad pokreneš Agent, on uopšte ne zna tvoje ranije preference, koji tech stack projekat koristi, kakve ugovore stil kôda ima.
Zato treba **dugoročno sećanje`, ove cross-sesije ključne informacije perzistira na disk, bez obzira koliko sesija prozora otvoriš, Agent može se setiti.
Onda problem opet dolazi, kontekst prozor je ograničen. Claude Opus 4.6 default 200K, čini se veliko, ali kratkoročno plus dugoročno sećanje sve zajedno gurni, ne samo troši token, nerelevantne informacije će ometati LLM-ovu procenu.

Zato treba kontekst kompresija——kad dostigne prag radi sumiranje, zadrži ključne informacije, baci suvišne detalje. Tako LLM ima dovoljno prostora da razmišlja, sve što u ruke drži je koristan kontekst.
01, kompletn dizajn Memory-a
Prvo pogledajmo celu sliku.

MemoryManager je fasada celog sistema, dole upravlja pet komponenti: ConversationMemory, LongTermMemory, ContextCompressor, TokenBudget, MemoryRetriever. Agent ne mora da brige o ovim detaljima, spolja izlaže samo dve operacije——sačuvaj poruku, uzmi sećanje. Prilikom čuvanja automatski upravlja budžet, prilikom uzimanja automatski pretražuje relevantan kontekst.
02, osnovna jedinica sećanja
Sećanje nije samo gomila stringova u listu. Agent mora da zna koju tip svakog sećanje je——razgovor ili činjenica?
Kad je nastalo——koristi se za vremensko degradiranje.
Koliko token zauzima——koristi se za budžet menadžment. Ima li dodatne informacije——npr. ovaj činjenica iz kog projekta dolazi.
Zato svaka sećanje unosa izgleda ovako:
public class MemoryEntry {
private final String id;
private final String content;
private final MemoryType type;
private final Instant timestamp;
private final Map<String, String> metadata;
private final int tokenCount;
public enum MemoryType {
CONVERSATION, // razgovorno sećanje
FACT, // činjenično sećanje (preference korisnika, informacije projekta)
SUMMARY, // sumirano sećanje
TOOL_RESULT // rezultat izvršenja alata
}
}CONVERSATION je razgovorna poruka, FACT je preference korisnika, informacije projekta ove ključne činjenice, SUMMARY je kompresovani sažetak, TOOL_RESULT je rezultat izvršenja alata.
TOOL_RESULT je zato što je posebno podeljen, zato što rezultat alata obično je veoma dug (npr. čitanje fajla može vratiti stotine linija), ali prilikom pretrage treba znati "šta komandu ranije izvršeno". Označi se, prilikom kompresije može agresivnije seći rezultat alata, razgovorni sadržaj više zadržati semantiku.
token procena takođe treba metoda, budžet menadžment sve na njoj oslanja:
public static int estimateTokens(String text) {
if (text == null || text.isEmpty()) return 0;
long chineseChars = text.chars()
.filter(c -> c > 0x4E00 && c < 0x9FFF).count();
long otherChars = text.length() - chineseChars;
return (int) Math.ceil(chineseChars / 1.5 + otherChars / 4.0);
}Kineski oko 1.5 znak po token, engleski oko 4 znaka po token. Precizno računanje treba koristiti tokenizer.
04, kratkoročno sećanje
Kratkoročno sećanje radi dve stvari: sačuvaj poruku i automatska eliminacija.
token budžet je ograničen, kad se mnogo priča uvijek će se napuniti. U ovom trenutku najjednostavnija strategija je eliminacija najstarije poruke——isto kao operativni sistem zamena stranice, memorije nedovoljno najduže ne korištenu stranu zameni. Razgovor je sekventualan, najstarija poruka obično najmanje važna, FIFO dovoljno.
public class ConversationMemory implements Memory {
private final LinkedHashMap<String, MemoryEntry> entries;
private final int maxTokens;
private final AtomicInteger currentTokens;
private final List<MemoryEntry> compressedSummaries;
@Override
public void store(MemoryEntry entry) {
entries.put(entry.getId(), entry);
currentTokens.addAndGet(entry.getTokenCount());
// Kad prelazi budžet automatski eliminiše najstariji unos
while (currentTokens.get() > maxTokens && entries.size() > 1) {
evictOldest();
}
}
}Eliminisane poruke neće direktno baciti, već staviti u compressedSummaries listu, čekaju kasnije kompresovati u sažetak——informacije još uvek postoje, samo u kompaktnijem obliku.
getUsageRatio() vraća trenutni token korisćenje, preko 80% kad MemoryManager automatski aktivira ContextCompressor da radi kompresiju.
05, dugoročno sećanje
Kratkoročno sećanje prati sesiju, kad razgovor zatvori nestaje. Ali neke informacije su cross-sesije potrebne——"korisnik sviadji JDK 17", "projekat koristi Maven gradnju"——ove stvari moraju sačuvati na disk, sledeći put kad pokreneš Agent može koristiti.
public class LongTermMemory implements Memory {
private static final String STORAGE_DIR = ".paicli/memory";
private static final String STORAGE_FILE = "long_term_memory.json";
public LongTermMemory() {
// Pri pokretanju učitaj sa diska
loadFromDisk();
}
@Override
public void store(MemoryEntry entry) {
// Provera duplikata: sadržina potpuno ista preskače
Optional<Map.Entry<String, MemoryEntry>> existing = entries.entrySet().stream()
.filter(e -> e.getValue().getContent().equals(entry.getContent()))
.findFirst();
if (existing.isPresent()) return;
entries.put(entry.getId(), entry);
tokenCounter.addAndGet(entry.getTokenCount());
saveToDisk(); // Svaki put nakon čuvanja perzistira
}
}Ovde ima tri dizajn tačke vredno pomenuti.
Prvo je automatska deduplikacija, korisnik kontinuirano tri puta kaže "sviadji da koristim Java", dugoročno sećanje neće sačuvati tri puta, sadržina istog direktno preskače.
Drugo je trenutna perzistencija, svaki store poziva saveToDisk, upisuje ~/.paicli/memory/long_term_memory.json. Putanja čuvanja podržava tri konfiguracije——default putanja, JVM parametar -Dpaicli.memory.dir=/path, promenljiva okruženja PAICLI_MEMORY_DIR, pogodno za test i proizvodnu okruženju izolaciju.
Treće je učitavanje pri pokretanju, Agent prilikom pokretanja automatski učitava prethodno sećanje. Prilikom učitavanja zadržava originalni timestamp, ovo je vrlo važno——ako timestamp bude preklopljen trenutnim vremenom, kasnije prilikom vremenske degradacije pretrage, staro sećanje neće biti ispravno degradirano.
Pretraga ovaj deo, koristi jieba tokenizer da radi kineski prioritet lagan match. "Projekat tech stack" može ispravno seći "projekat" "tech" dve reči, stopa pogodaka je mnogo viša od običnog string contains.
final class MemoryQueryTokenizer {
private static final JiebaSegmenter SEGMENTER = new JiebaSegmenter();
static Set<String> tokenize(String query) {
LinkedHashSet<String> tokens = new LinkedHashSet<>();
List<String> words = SEGMENTER.sentenceProcess(
query.toLowerCase(Locale.ROOT).trim());
for (String word : words) {
String trimmed = word.trim();
// Filter jedan znak i pure interpunkcija
if (trimmed.length() >= 2 && !isPunctuation(trimmed)) {
tokens.add(trimmed);
}
}
return tokens;
}
}jieba u Java ekosistemu smatra se prilično zrela kineska segmentacija biblioteka. Nakon segmentacije takođe filter jednog znaka i pure interpunkcije——kineski "de" "le" "shi" ovaj jedan znak gotovo nema pretragu vrednost, čuvati samo donosi šum.
search metoda poziva tokenizer, istovremeno match sadržina i metadata:
public List<MemoryEntry> search(String query, int limit) {
Set<String> queryTokens = MemoryQueryTokenizer.tokenize(query);
return entries.values().stream()
.filter(entry -> {
if (MemoryQueryTokenizer.matches(entry.getContent(), queryTokens)) {
return true;
}
return entry.getMetadata().values().stream()
.anyMatch(value -> MemoryQueryTokenizer.matches(value, queryTokens));
})
.limit(limit)
.collect(Collectors.toList());
}MemoryRetriever-ov computeRelevanceScore takođe koristi jieba da računa ključne reči match stepen.
Ispreno rečeno, jieba segmentacija plus ključne reči match u poređenju sa vektorskom pretragom još uvek zaostaje. "Java framework" match ne može "Spring Boot", ova semantička razumevanje treba Embedding da se reši, stavljamo u četvrti broj.
06, kontekst kompresija
Ovaj deo je u celom Memory sistemu ja mislim najzanimljiviji.
Kratkoročno sećanje puno, stare poruke treba eliminisati, ali ključne informacije ne mogu biti izgubljene. Šta raditi? Kompresovati u sažetak, onda injektuj nazad.
Kompresiona strategija koristi Map-Reduce, isto kao obrada velikog dokumenta misao:

Map faza: stare poruke podeli svaku 5 u grupu, svaka grupa nezavisno poziva LLM generiše sažetak. Grupna obrada je mnogo bolja od odjednom baciti desetine LLM-u, kvalitet sažetka model obradio kratak tekst je očigledno viši.
Reduce faza: više fragmentnih sažetaka spaja u konačni sažetak. Samo jedan direkt koristi, više ne poziva jednom LLM.
public String compress(ConversationMemory memory) {
List<MemoryEntry> allEntries = memory.getAll();
// Podela: stare poruke vs nedavne poruke (mora kopirati, zato što kasnije će clear bottom-layer kolekciju)
int splitPoint = allEntries.size() - retainRecentRounds;
List<MemoryEntry> oldEntries = new ArrayList<>(allEntries.subList(0, splitPoint));
List<MemoryEntry> recentEntries = new ArrayList<>(allEntries.subList(splitPoint, allEntries.size()));
// Map faza
List<String> chunkSummaries = mapPhase(oldEntries);
// Reduce faza
String finalSummary = chunkSummaries.size() == 1
? chunkSummaries.get(0)
: reducePhase(chunkSummaries);
// Obriši staro sećanje, injektuj sažetak, zadrži nedavno sećanje
memory.clear();
memory.store(new MemoryEntry(
"summary-" + UUID.randomUUID().toString().substring(0, 8),
"[istorijski razgovor sažetak] " + finalSummary,
MemoryEntry.MemoryType.SUMMARY, null,
MemoryEntry.estimateTokens(finalSummary)
));
for (MemoryEntry entry : recentEntries) memory.store(entry);
return finalSummary;
}retainRecentRounds ovaj parametar je vrlo ključan——nedavne 3 krug poruke ne učestvuju u kompresiji, originalno zadrži. Upravo pričano sadržaj obično je core kontekst tekućeg zadatka, kompresija može izgubiti ključne detalje.
ContextCompressor još radi jednu stvar: ekstrakcija činjenica. Kad se razgovor završi, pozovi extractFacts da LLM iz razgovora izvadi ključne informacije——preference korisnika, konfiguraciju projekta, važne odluke——automatski stavi u dugoročno sećanje.
public List<String> extractFacts(List<MemoryEntry> entries,
LongTermMemory longTermMemory) {
// Koristi LLM iz razgovora ekstraktuje ključne činjenice
String prompt = String.format(EXTRACT_FACTS_PROMPT, conversation);
GLMClient.ChatResponse response = llmClient.chat(messages, List.of());
// Parsiraj činjenice, jednu po jednu stavi u dugoročno sećanje
for (String fact : facts) {
longTermMemory.store(new MemoryEntry(..., MemoryType.FACT, ...));
}
return facts;
}Npr. korisnik rekao "sviadji da koristim Spring Boot", sledeći put kad pokreneš Agent, ova preferenca je u dugoročnom sećanju čeka, ne treba ponoviti.

07, Token budžet menadžment
Ova stvar nije jednostavan brojač, već dodělač budžeta.
Kontekst prozor model-a je samo toliko veliki, GLM-5.1 je 200K. Ali ovih 200K ne može sve dati istoriji razgovora, system prompt zauzima jedan deo, definicija alata zauzima jedan deo, model generiše odgovor takođe treba ostaviti prostor.
public class TokenBudget {
private final int contextWindow; // 200000
private final int reservedForSystem; // 500
private final int reservedForTools; // 800
private final int reservedForResponse; // 2000
public int getAvailableForConversation() {
return contextWindow - reservedForSystem
- reservedForTools - reservedForResponse;
// 200000 - 500 - 800 - 2000 = 196700
}
public boolean needsCompression(ConversationMemory memory) {
return memory.getTokenCount()
> getAvailableForConversation() * 0.8;
}
}Istorija razgovora prelazi 80% budžeta aktivira kompresiju. Ostavlja 20% maržu zato što token procena po sebi nije precizna (mi koristimo procenu broja znakova, nije pravi tokenizer), previše čvrsto lako se prevrće.
TokenBudget takođe po sredi beleži svaki LLM poziv token potrošnju, prilikom debug vrlo koristan:
public void recordUsage(int inputTokens, int outputTokens) {
totalInputTokens += inputTokens;
totalOutputTokens += outputTokens;
llmCallCount++;
}
public String getUsageReport() {
return String.format(
"Token statistika: poziv %d puta | ukupno ulaz: %d | ukupno izlaz: %d",
llmCallCount, totalInputTokens, totalOutputTokens);
}U CLI unesi /memory možeš videti kompletn status izveštaj.
08, pretraga sećanja
Kratkoročno i dugoročno sećanje postoje, ali Agent prilikom obrade novog unosa, ne može 100 sećanja sve LLM-u baciti——većina sa tekućim zadatkom uopšte nije povezana, čisto troši token.
MemoryRetriever radi iz dva sloja sećanja najrelevantnije unove izvlači.
public List<MemoryEntry> retrieve(String query, int limit) {
List<ScoredEntry> scored = new ArrayList<>();
// Iz kratkoročnog sećanja pretraži
for (MemoryEntry entry : shortTermMemory.getAll()) {
double score = computeRelevanceScore(entry, query);
if (score > 0) scored.add(new ScoredEntry(entry, score, true));
}
// Iz dugoročnog sećanja pretraži (težina ×1.2, zato što je više rafiniran)
for (MemoryEntry entry : longTermMemory.getAll()) {
double score = computeRelevanceScore(entry, query) * 1.2;
if (score > 0) scored.add(new ScoredEntry(entry, score, false));
}
return scored.stream()
.sorted(Comparator.comparingDouble(ScoredEntry::score).reversed())
.limit(limit)
.map(ScoredEntry::entry)
.collect(Collectors.toList());
}Kalkulacija relevantnosti razmatra tri dimenzije: ključne reči match——posle segmentacije upita reč po reč match, što više hitova viši skor; vremenska degradacija——unutar 24 sata od 1.0 linearno degradira do 0.5, pre tri dana stvari težina naravno je niža; izvor težina——dugoročno sećanje unosi su prošli kroz ekstrakciju i rafinaciju, gustina informacije je viša, daje 1.2× težinu.
Rezultat pretrage kroz buildContextForQuery spaja u tekst, injektuje u LLM ulaz. Agent strana promena je mala, prilikom gradnje poruke nosi kontekst sećanja.

09, integracija u Agent
Komponente su sve napisale, sledeće ih povežemo u Agent i PlanExecuteAgent. Core promena u Agent.run metodi:
public String run(String userInput) {
// 1. Sačuvaj u kratkoročno sećanje
memoryManager.addUserMessage(userInput);
// 2. Pretraži relevantno dugoročno sećanje, injektuj u system prompt
String memoryContext = memoryManager.buildContextForQuery(userInput, 500);
updateSystemPromptWithMemory(memoryContext);
// 3. Dodaj korisnički unos u istoriju (zadrži originalni tekst, ne zagađuj user message)
conversationHistory.add(GLMClient.Message.user(userInput));
// ... ReAct loop ...
}Korisnički unos ulazi, prvo sačuvaj kratkoročno sećanje, onda iz dugoročnog sećanja izvuci relevantan kontekst, injektuj u system prompt, onda normalno ide ReAct loop.
Ovde ima jedan detalj——kontekst sećanja je injektovan u system prompt, nije spojen u user message. Ako staviš u user message, LLM može sadržinu sećanja shvatiti kao korisničku instrukciju da izvrši, to će stvarati haos. Stavi u system prompt, model će ga koristiti kao pozadinsku informaciju referencu, neće se mešati sa korisničkim unosom.
private void updateSystemPromptWithMemory(String memoryContext) {
if (memoryContext == null || memoryContext.isEmpty()) {
// Obnovi originalni system prompt
conversationHistory.set(0, GLMClient.Message.system(SYSTEM_PROMPT));
} else {
String enrichedPrompt = SYSTEM_PROMPT + "\n" + memoryContext;
conversationHistory.set(0, GLMClient.Message.system(enrichedPrompt));
}
}Kada nema relevantnog sećanja obnavlja originalni system prompt, ima sećanje dodaje na kraj. Čisto i lepo.
Rezultat izvršenja alata takođe sinhronizuje čuva u sećanje:
// Nakon izvršenja alata
memoryManager.addToolResult(toolName, toolResult);Pomoćnik odgovor takođe čuva:
// Nakon Agent odgovora
memoryManager.addAssistantMessage(response.content());
memoryManager.recordTokenUsage(response.inputTokens(),
response.outputTokens());
}Kada korisnik unese /clear obriše razgovor, nije jednostavno grubo sve obriši——prvo ekstraktuj ključne činjenice u dugoročno sećanje, onda obriši:
public void clearHistory() {
memoryManager.extractAndSaveFacts(); // Prvo sačuvaj
// Onda obriši
conversationHistory.clear();
conversationHistory.add(systemMsg);
memoryManager.getShortTermMemory().clear();
}PlanExecuteAgent takođe radi sličnu integraciju. Prilikom izvršenja svakog Task injektuj dugoročno sećanje kontekst, LLM može referisati prethodne informacije projekta da donese odluku. Nakon završetka celog plana, automatski ekstraktuj ključne činjenice u dugoročno sećanje, ne čekaš korisnik da ručno /clear.
CLI takođe dodao dve komande: /memory pregled kompletnog statusa Memory sistema, /save činjenica sadržina ručno sačuvaj ključne činjenice u dugoročno sećanje.
10, MemoryManager
Konačno pričajmo o MemoryManager, Agent samo sa njim radi, dole pet komponenti kako saradjuju, Agent uopšte ne mora brinuti.
public class MemoryManager {
private final ConversationMemory shortTermMemory;
private final LongTermMemory longTermMemory;
private final ContextCompressor compressor;
private final MemoryRetriever retriever;
private final TokenBudget tokenBudget;
// Sačuvaj korisničku poruku
public void addUserMessage(String content) {
shortTermMemory.store(entry);
compressIfNeeded(); // Automatski proveri da li treba kompresovati
}
// Pretraži relevantno sećanje
public String buildContextForQuery(String query, int maxTokens) {
return retriever.buildContextForQuery(query, maxTokens);
}
// Sistemski status
public String getSystemStatus() {
return shortTermMemory.getStatusSummary() + "\n"
+ longTermMemory.getStatusSummary() + "\n"
+ tokenBudget.getUsageReport();
}
}compressIfNeeded je ključ automatske aktivacije kompresije——svaki put nakon čuvanja poruke proveri token korisćenje, preko 80% poziva ContextCompressor. Agent kôd samo čuva poruku, kompresovati-ne, kako kompresovati, sve su MemoryManager interne stvari.
Prednost fasade modula je ovde. Ako nema MemoryManager, Agent mora sam upravljati kratkoročno sećanje, dugoročno sećanje, trenutak kompresije, token budžet......kôd će se izmestišiti u kaos.
extractAndSaveFacts takođe vredno pominjanje. Kad se razgovor završi (korisnik unese /clear ili direktno izađe), on će automatski iz tekućeg razgovora ekstraktovati ključne činjenice sačuvati u dugoročno sećanje, ceo proces ne traži korisničku operaciju. Naravno, koristi /save činjenica sadržina ručno sačuvaj može, neke informacije korisnik sam zna važne, aktivno čuvaj je pouzdanije.
11, pokreni vidi efekt
Kôd je završen, treba pokrenuti verifikovati.

Prvo pokretanje sećanje je prazno, koristeći koristeći će postepeno akumulirati.

Prompt: pomozi mi da napravim Java projekat myapp

Prompt: JDK može ostati 17, seti se

Unesi /clear, ekstraktuj ključne činjenice u dugoročno sećanje.

Prompt: pomozi mi da napavim još jedan projekat

Vidiš? Nakon čišćenja razgovora, Agent još uvek seća da sviadji JDK 17. Pretraga automatski izvadi ovu činjenicu iz dugoročnog sećanja, injektovala u kontekst, Agent-ova odluka direktno je postala tačnija.
Još pogledaj /memory komandu izlaz:

Koliko je kratkoročno sećanje potrošilo token, koliko činjenica dugoročno sećanje sačuvalo, koliko puta LLM pozvao, koliko potrošeno token——sve ovde.
Ručno čuvaj činjenice takođe može, /save projekat koristiMaven gradnju:

Sledeći put kad razgovaraš, Agent zna da koristiš Maven, neće glupo generisati Gradle konfiguraciju.
Konačno, sećanje sistema Agent tri stvari radi: šta setiti, šta zaboraviti, šta se setiti. Kratkoročno sećanje zaduženo "seti", kontekst kompresija zaduženo "zaborav kad treba", dugoročno plus pretraga zaduženo "kad je ključno može setiti".
Sledeći broj integriše RAG pretragu, Agent može razumeti ceo kôd baz.
Projekat kôd je ažuriran do: github.com/itwanger/paicli
PaiCLI kako da napišeš na CV?
PaiCLI projekat (3. broj) | 2026.04 - 2026.05 | nezavisan razvojač
Opis projekta: dodaj Memory sistemu Agent CLI, realizuj kratkoročno sećanje automatsko menadžment, dugoročno sećanje perzistenciju, Map-Reduce kontekst kompresiju i Token budžet kontrolu.
Core odgovornosti:
- Dizajnirao Memory sistem, realizovao 4 tipa sećanja (razgovor/činjenica/sažetak/rezultat alata); realizovao kratkoročno sećanje, kad prelazi token prag automatski eliminira najstariji unos, zadržava kompresovani sažetak čeka reinjekciju kontekst
- Razvio dugoročno sećanje, podržava JSON fajl perzistenciju i Agent pokretanje automatsko učitavanje, realizovao sadržinu deduplikaciju i ključne reči pretragu, cross-sesiju zadržava preference korisnika i informacije projekta
- Realizovao kontekst kompresor, koristi Map-Reduce strategiju segmentno sažetak spaja, zadržava nedavne 3 krug kompletne poruke ne učestvuju u kompresiji, i podržava automatsku ekstrakciju ključnih činjenica u dugoročno sećanje
- Realizovao budžet menadžment i sećanje pretragu, baziran na jieba segmentaciju+ključne reči match+vremenska degradacija+izvor težina sortiraj rezultat pretrage
