DEV Community

noear
noear

Posted on

Breathing Soul into AI: How to Implement a Self-Reflecting Long-Term Memory System in Java AI

In the world of AI Agents, there is a famous saying: "An Agent without memory is just a sophisticated function; an Agent with memory has a soul."

However, implementing a memory system in the Java ecosystem that truly possesses "self-evolving" capabilities is no easy feat. This article provides a deep dive into how to build a long-term memory component capable of extraction, consolidation, pruning, and retrieval based on the Solon AI framework and inspired by the core concepts of the MemSkill paper.

I. The Challenge of Long-Term Memory in Agents

Currently, memory implementation in most AI applications is overly simplistic: they just stuff recent conversation logs into the context window. This approach faces three major hurdles:

  1. Context Overflow: LLM context windows are expensive and finite. "Stream-of-consciousness" memory hits the limit very quickly.
  2. Cognitive Noise: Irrelevant information in historical logs interferes with the AI's judgment on current tasks.
  3. Cognitive Conflict: When a user's status changes (e.g., "I used to like Java, but now I prefer Go"), simple retrieval (RAG) often pulls up outdated information, leading to logical confusion.

The Core Philosophy of MemSkill: An Agent should not just passively store and retrieve data. Like a human, it should actively summarize facts, correct errors, and upscale fragmented information into a structured Mental Model.

II. Thinking and Design: Mental Model Self-Evolution

The MemSkill we are designing follows four core actions:

  • Extract: Identify key facts from the conversation and assign importance.
  • Consolidate: Compress multiple low-level facts into high-level user preference summaries.
  • Prune: Identify and delete outdated or incorrect memories.
  • Search: Use semantic search to retrieve the most relevant context.

Architecture Design

To ensure production-grade reliability, we utilize a Two-Tier Storage Architecture:

  • Redis (KV Storage): Leverages TTL mechanisms to physically expire memories based on their Importance.
  • Vector/Lucene (Semantic Index): Provides multi-tenant isolated semantic retrieval capabilities.

III. Code Implementation: Building the MemSkill Core

1. The Memory Search Provider (MemSearchProvider)

First, we define an abstraction layer that can interface with either a distributed Vector Database or a local Lucene index.

public interface MemSearchProvider {
    /** Semantic/Fuzzy Search */
    List<MemSearchResult> search(String userId, String query, int limit);
    /** Get high-value "hot" memories for profile injection */
    List<MemSearchResult> getHotMemories(String userId, int limit);
    /** Sync Index */
    void updateIndex(String userId, String key, String fact, int importance, String time);
    /** Remove Index */
    void removeIndex(String userId, String key);
}
Enter fullscreen mode Exit fullscreen mode

2. The Core Skill Class: MemSkill

This is the "mental processor" of the Agent. It provides tools for the AI to call and dynamically injects the user profile via getInstruction before the dialogue begins.

public class MemSkill extends AbsSkill {
    private final RedisClient redis;
    private final String userId;
    private final MemSearchProvider searchProvider;

    @Override
    public String getInstruction(Prompt prompt) {
        // Dynamically load the top 5 core cognitive fragments
        List<MemSearchResult> hot = searchProvider.getHotMemories(userId, 5);
        String mentalModel = formatModel(hot);

        return "### Long-Term Memory Self-Evolution Guide\n" +
               "1. **Core Mental Model**: This is your existing knowledge of the user:\n" + mentalModel +
               "\n2. **Cognitive Evolution Principles**:\n" +
               "   - **Temporal Priority**: In case of conflict, trust the most recent timestamp.\n" +
               "   - **Active Correction**: If the current status conflicts with the model, you MUST update via `mem_extract` or `mem_prune`.";
    }

    @ToolMapping(name = "mem_extract", description = "Extract facts or preferences into the mental model")
    public String extract(@Param("key") String key, @Param("fact") String fact, @Param("importance") int importance) {
        // 1. Retrieve old memory for reflection
        String oldJson = redis.getBucket().get(getFinalKey(key));

        // 2. Dynamic TTL: Core summaries (>=10) last forever; common facts last 7-30 days
        int ttl = calculateTTL(importance);

        // 3. Sync to Redis and Search Provider
        saveMemory(key, fact, importance, ttl);

        return "[Operation Successful] Mental model updated. If cognitive differences are found, please reflect this evolution in your response.";
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Local Implementation: Lucene Adapter

For many Java developers, deploying a full vector database is too costly. We implemented a Lucene-based MemSearchProvider that supports multi-tenant isolated semantic matching on local disk or in memory.

public class MemSearchProviderLuceneImpl implements MemSearchProvider, AutoCloseable {
    private final IndexWriter writer;

    @Override
    public List<MemSearchResult> search(String userId, String query, int limit) {
        // Implementation via BooleanQuery (user_id = X AND content LIKE query)
        BooleanQuery.Builder mainQuery = new BooleanQuery.Builder();
        mainQuery.add(new TermQuery(new Term("user_id", userId)), BooleanClause.Occur.MUST);
        // ... Add QueryParser for the query string

        // Sort by relevance (or importance) and return
        TopDocs topDocs = searcher.search(mainQuery.build(), limit);
        // ... Map to MemSearchResult
    }
}
Enter fullscreen mode Exit fullscreen mode

IV. The Elegance of This Implementation

  1. Cognitive Reflection (Reflexion): During mem_extract, the system feeds the old memory fragment back to the AI. This triggers an "awareness" mechanism in the Agent, allowing it to say: "I remember you mentioned A before, but now you seem to prefer B. I have updated my understanding for you."
  2. Importance-Driven Lifecycle: Not all memories are created equal. By controlling TTL through importance, we achieve a natural sedimentation from "ephemeral memory" to "long-term experience."
  3. Designer Role Injection: Through getInstruction, we turn the mental model into the Agent's "pre-conscious awareness," which is more efficient and human-like than simple Tool Call retrieval.

V. Conclusion

In the Solon AI ecosystem, the implementation of solon-ai-skill-memory proves that long-term memory is not just about data stacking, but about cognitive management. Through this self-evolving solution based on Lucene/Redis, Java developers can easily build Agents that never forget and are capable of deep self-reflection.

Appendix: Full Implementation Reference

package org.noear.solon.ai.skills.memory;

import org.noear.redisx.RedisClient;
import org.noear.snack4.ONode;
import org.noear.solon.Utils;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.chat.skill.AbsSkill;
import org.noear.solon.ai.chat.prompt.Prompt;
import org.noear.solon.annotation.Param;
import org.noear.solon.core.util.Assert;
import org.noear.solon.lang.Preview;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * MemSkill: Long-term memory skill based on a self-evolving mental model.
 *
 * Follows core MemSkill principles: Extract, Consolidate, Prune, Search.
 * Guides the Agent's cognitive updates via Designer instructions.
 */
public class MemSkill extends AbsSkill {
    private static final Logger LOG = LoggerFactory.getLogger(MemSkill.class);
    private static final String BASE_PREFIX = "ai:memskill:";

    private final RedisClient redis;
    private final String userId;
    private final MemSearchProvider searchProvider;

    public MemSkill(RedisClient redis, String userId, MemSearchProvider searchProvider) {
        this.redis = redis;
        this.userId = userId;
        this.searchProvider = searchProvider;
    }

    @Override
    public String name() { return "mem_skill"; }

    @Override
    public String description() {
        return "Long-term memory expert: Manages extraction, evolution, conflict resolution, and deep retrieval of user mental models.";
    }

    @Override
    public String getInstruction(Prompt prompt) {
        String mentalModel = "";
        if (searchProvider != null) {
            List<MemSearchResult> hot = searchProvider.getHotMemories(userId, 5);
            if (!hot.isEmpty()) {
                StringBuilder sb = new StringBuilder();
                for (MemSearchResult r : hot) {
                    sb.append(String.format("- %s: %s (Time: %s)\n", r.key, r.content, r.time));
                }
                mentalModel = sb.toString();
            }
        }

        return "### Long-Term Memory Self-Evolution Guide (Current System Time: " + getNow() + ")\n" +
                "You have the ability to autonomously manage and evolve the user's mental model. Please follow these principles:\n" +
                "1. **Core Mental Model**: This is your existing knowledge of the current user. Base your conversation on this:\n" +
                (Assert.isEmpty(mentalModel) ? "- (Mental model under construction, please ask questions to learn about the user)" : mentalModel) +
                "\n\n2. **Cognitive Evolution Principles**:\n" +
                "   - **Temporal Priority**: If records conflict, the one with the most recent timestamp prevails.\n" +
                "   - **Active Correction**: If the user's current status conflicts with the model above, you MUST update via `mem_extract` or remove via `mem_prune`.\n" +
                "   - **Inductive Upscaling**: When the cognitive base becomes redundant, proactively use `mem_consolidate` to summarize low-level facts into high-level preferences.";
    }

    private String getFinalKey(String key) { return BASE_PREFIX + userId + ":" + key; }

    private String getNow() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); }

    @ToolMapping(name = "mem_extract",
            description = "Store facts, preferences, or progress into the user mental model. If the Key exists, the system returns the old record for reflection.")
    public String extract(@Param("key") String key,
                          @Param("fact") String fact,
                          @Param("importance") int importance) {
        try {
            String finalKey = getFinalKey(key);
            String oldJson = redis.getBucket().get(finalKey);
            String now = getNow();

            StringBuilder feedback = new StringBuilder("[Operation Successful] Mental model updated.");
            if (Utils.isNotEmpty(oldJson)) {
                ONode old = ONode.ofJson(oldJson);
                feedback.append("\n[Cognitive Contrast] Historical record found:")
                        .append("\n- Old Content: ").append(old.get("content").getString())
                        .append("\n- Old Time: ").append(old.get("time").getString())
                        .append("\nPlease compare the differences. If a change occurred, reflect this cognitive evolution in subsequent dialogue.");
            }

            Map<String, Object> data = new HashMap<>();
            data.put("content", fact);
            data.put("time", now);
            data.put("importance", importance);

            int ttl;
            if (importance >= 10) ttl = -1; 
            else if (importance >= 5) ttl = 2592000; // 30 days
            else ttl = 604800; // 7 days

            redis.getBucket().store(finalKey, ONode.serialize(data), ttl);

            if (searchProvider != null) {
                searchProvider.updateIndex(userId, key, fact, importance, now);
            }

            return feedback.toString();
        } catch (Exception e) {
            LOG.error("MemSkill extract error", e);
            return "Storage exception.";
        }
    }

    @ToolMapping(name = "mem_search",
            description = "Semantic Retrieval: Find relevant memory fragments in the mental model via natural language to retrieve background information.")
    public String search(@Param("query") String query) {
        if (searchProvider == null) return "Search provider not configured.";

        List<MemSearchResult> results = searchProvider.search(userId, query, 3);
        if (results.isEmpty()) return "No relevant cognitive fragments found.";

        StringBuilder sb = new StringBuilder("Matched cognitive references (Prefer the most recent timestamps):\n");
        for (MemSearchResult res : results) {
            sb.append(String.format("- [%s] (Key: %s): %s\n",
                    Utils.isNotEmpty(res.time) ? res.time : "Unknown Time", res.key, res.content));
        }
        return sb.toString();
    }

    @ToolMapping(name = "mem_recall", description = "Exact Recall: Get full details of a cognitive entry via Key.")
    public String recall(@Param("key") String key) {
        try {
            String val = redis.getBucket().get(getFinalKey(key));
            if (Utils.isEmpty(val)) return "Cognitive entry [" + key + "] not found.";
            ONode node = ONode.ofJson(val);
            return String.format("[Cognitive Details] Content: %s | Time: %s | Importance: %s",
                    node.get("content").getString(), node.get("time").getString(), node.get("importance").getString());
        } catch (Exception e) { return "Read exception."; }
    }

    @ToolMapping(name = "mem_consolidate",
            description = "Cognitive Consolidation: Merge multiple low-level fact fragments into a high-level preference model and prune redundant fragments.")
    public String consolidate(@Param("keys_to_merge") List<String> oldKeys,
                              @Param("new_key") String newKey,
                              @Param("evolved_insight") String insight) {
        String fact = "[Evolved Insight] " + insight;
        extract(newKey, fact, 10); 

        for (String k : oldKeys) {
            prune(k);
        }

        return "[Cognitive Evolution Successful] Fragments summarized into core insights; redundant records removed.";
    }

    @ToolMapping(name = "mem_prune", description = "Cognitive Correction: Delete incorrect, duplicate, or outdated knowledge.")
    public String prune(@Param("key") String key) {
        redis.getBucket().remove(getFinalKey(key));
        if (searchProvider != null) searchProvider.removeIndex(userId, key);
        return "Key cleared from model: " + key;
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)