Zum Hauptinhalt springen

LangChain4j-Tool-Memory-Tutorial: Einen Java-Agenten mit Oracle AI Database bauen

Lerne, wie du einen einfachen LangChain4j-Agenten entwickelst, der sich wichtige Notizen über mehrere Unterhaltungen hinweg merkt.
· 8 Min. lesen

Entdecke KI

In diesem Tutorial baust du ein kleines Java-Modul, das eigene Tools bereitstellt, Notizen als JSON in der Oracle AI Database speichert und sie per Volltextsuche wiederfindet.

Wenn du KI-Agenten baust, reicht der kurzfristige Chatverlauf meist nicht aus. Echte Assistenten brauchen oft etwas dauerhaften Speicher, um wichtige Fakten abzulegen und später wieder abzurufen.

Dieses Tutorial zeigt, wie du dieses Muster mit einem minimalen Java-Modul umsetzt. Du nutzt LangChain4j-Tools, ein schlankes Agenten-Interface und Oracle AI Database als Datenspeicher. Anstatt Embeddings oder eine vollständige Retrieval-Pipeline hinzuzufügen, bleibt das Design bewusst klein: Notizen werden als JSON-Dokumente gespeichert und mit Oracle Text durchsucht.

Am Ende weißt du, wie das Modul Folgendes zusammenführt:

  • Ein LangChain4j-Agenteninterface
  • Zwei individuelle @Tool-Methoden
  • Ein JDBC-Repository für dauerhafte Notizspeicherung
  • Eine Konsolenanwendung, die den gesamten Ablauf zusammenführt

Was ist toolbasierte LangChain4j-Memory?

LangChain4j erlaubt es dir, ganz normale Java-Methoden als Tools zu exponieren, die ein Modell bei Bedarf aufrufen kann. In diesem Modul sind diese Fähigkeiten auf Speicher ausgerichtet:

  • searchMemories(String question) findet passende gespeicherte Notizen
  • storeMemory(String note) schreibt eine neue dauerhafte Notiz

Das unterscheidet sich von einem gewöhnlichen Chatverlauf, der Nachrichten nur im In-Memory-Kontextfenster hält.

Persistente, toolbasierte Memory entscheidet eigenständig, wann gesucht oder gespeichert wird, und die Daten liegen in einer Datenbank.

Dieses Muster ist hilfreich, wenn ein Agent Fakten wie Incident-Notizen, Runbook-Erinnerungen oder Folgeaktionen behalten soll, ohne zu früh ein großes Retrieval-System aufzubauen. 

Persistenter, datenbankgestützter Speicher kann außerdem gleichzeitig von einem oder mehreren Agenten ohne Datenkonflikte genutzt werden.

Ablauf: Eine Nutzeranfrage erreicht einen LangChain4j-Assistenten, der vor der Antwort dauerhafte JSON-Notizen in der Oracle AI Database durchsucht oder speichert und dann knapp antwortet.

Toolbasierte Memory ermöglicht es dem Assistenten, bestehende Notizen abzurufen oder neue Notizen in einer dauerhaften Datenbank abzulegen.

Wichtige Features des LangChain4j- und Oracle-AI-DB-Moduls

Das Beispiel ist klein, zeigt aber mehrere zentrale Entwurfsideen für Agenten.

Eigene LangChain4j-Tools

Die Klasse MemoryTools stellt zwei Methoden mit @Tool bereit. Dadurch kann LangChain4j sie dem Agenten ohne zusätzlichen Wrappercode verfügbar machen.

// Retrieve relevant notes and format them for the assistant.
 @Tool(
     	"Search saved memory notes when earlier context might help answer the user."
 )
 public String searchMemories(String question) {
 	List<MemoryRepository.Note> matches = repository.search(question, 3);
 	if (matches.isEmpty()) {
     	return "No saved memory matched that question.";
 	}

 	StringBuilder builder = new StringBuilder();
 	for (int i = 0; i < matches.size(); i++) {
     	MemoryRepository.Note note = matches.get(i);
     	builder.append(i + 1)
             	.append(". [M")
             	.append(note.id())
             	.append("] ")
             	.append(note.text());
     	if (i + 1 < matches.size()) {
         	builder.append('\n');
     	}
 	}
 	return builder.toString();
 }

Diese Methode erledigt zwei Dinge: Sie delegiert die Suche ans Repository und formatiert die Ergebnisse modellfreundlich, etwa als [M12] some note text. 

So kann der Agent das Tool-Output leicht in seiner finalen Antwort zitieren.

Dauerhafte Memory in der Oracle AI Database

Notizen werden in einer Tabelle agent_memories als native JSON-Dokumente gespeichert. Jede Zeile enthält:

  • Eine Identitätsspalte id
  • Eine JSON-Spalte namens memory_doc
  • Einen created_at-Zeitstempel

Das Modul initialisiert das Schema automatisch:

-- Create a JSON-backed table for durable agent notes.
 CREATE TABLE IF NOT EXISTS agent_memories (
 	id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
 	memory_doc JSON NOT NULL,
 	created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
 )

Das hält das Speichermodell einfach und liefert dennoch strukturierte Daten mit Datenbank-Dauerhaftigkeit.

Schlagwortsuche mit Oracle Text

Das Repository erzeugt keine Embeddings. Stattdessen extrahiert es Schlüsselwörter aus der Frage des Nutzers, entfernt Stoppwörter, baut mit ACCUM einen Oracle Text-Ausdruck und durchsucht den JSON-Inhalt mit json_textcontains.

Das ist besonders hilfreich für Bezeichner und operationale Begriffe wie CHG2145, bei denen exakte oder nahezu exakte Token-Treffer oft wichtiger sind als semantische Ähnlichkeit.

Ein minimaler Konsolen-Workflow

Die Klasse Application erstellt das Repository, initialisiert das Schema, befüllt ein paar Notizen, konstruiert das Chatmodell und startet eine Terminalschleife. So lässt sich das Beispiel leicht ausführen und später in andere Services übernehmen.

So startest du dein LangChain4j-Memory-Projekt

Bevor du das Modul ausführst, stelle sicher, dass deine lokale Umgebung die Projektanforderungen erfüllt.

Voraussetzungen

Du brauchst:

  • Java 21 oder neuer
  • Maven 3.9 oder neuer
  • Oracle AI Database Free lokal oder remote
  • Eine Umgebungsvariable OPENAI_API_KEY

Das Maven-Projekt hängt ab von:

  • langchain4j
  • langchain4j-open-ai
  • ojdbc11
  • JUnit und Testcontainers für Integrationstests

Beispiel ausführen

Setze zuerst deinen OpenAI-API-Schlüssel:

# Set the API key used by the chat model.
 export OPENAI_API_KEY=<your key>

Dann starte die App aus dem Projektverzeichnis:

# Compile and run the sample with the database connection details.
 mvn compile exec:java \
     -Dexec.args="jdbc:oracle:thin:@localhost:1521/freepdb1 testuser testpwd"

Die Anwendung erwartet genau drei Argumente:

1. JDBC-URL

2. Datenbank-Benutzername

3. Datenbank-Passwort

Fehlen diese, wirft Application.main() eine IllegalArgumentException. Fehlt OPENAI_API_KEY, folgt eine IllegalStateException.

Was beim Start passiert

Beim Start der App werden:

1. Ein MemoryRepository erstellt

2. Das Datenbankschema initialisiert

3. Drei Beispielsnotizen befüllt, falls die Tabelle leer ist

4. Ein OpenAiChatModel gebaut

5. Modell und Tools in AiServices verdrahtet

6. Eine CLI-Schleife gestartet, die Eingaben liest, bis du exit eintippst

So kannst du Memory-Retrieval sofort testen, ohne vorher manuell Zeilen einzufügen.

Einen Java-Agenten mit Memory in LangChain4j bauen

Gehen wir das Modul nun in der Reihenfolge durch, in der die Anwendung es nutzt.

Agenteninterface definieren

LangChain4j beschreibt den Agentenvertrag über ein Java-Interface. In diesem Projekt ist das MemoryAssistant.

// Define the interface that receives each user message.
 public interface MemoryAssistant {

 	@SystemMessage("""
         	You are a simple assistant with access to durable memory notes.
         	Use searchMemories when earlier notes might help answer the user.
         	If the user explicitly asks you to remember something for later, call storeMemory.
         	Keep answers concise.
         	""")
 	@UserMessage("{{message}}")
 	String chat(@V("message") String message);
 }

Wichtig ist die Systemnachricht. Sie erklärt dem Modell, wann welches Tool zu nutzen ist:

  • Suchen, wenn frühere Notizen helfen könnten
  • Speichern, wenn der Nutzer ausdrücklich bittet, sich etwas zu merken

Das ist ein gutes Beispiel für eine schlanke Tool-Policy. Der Prompt ist kurz, aber präzise genug für die Toolauswahl.

Memory-Operationen als Tools bereitstellen

Der Agent spricht nicht direkt mit dem Repository. Stattdessen ruft er die Schicht MemoryTools auf.

Hier ist die Speichermethode:

// Save a note and return its generated memory identifier.
 @Tool(
     	"Store a new memory note when the user asks you to remember something "
             	+ "important for later."
 )
 public String storeMemory(String note) {
 	long id = repository.store(note);
 	return "Stored memory M" + id + ".";
 }

Das ist bewusst kompakt. Ein Nutzer könnte sagen:

Remember that the next shift should verify the checkout retry queue stays below 50.

Folgt das Modell der Systemanweisung, ruft es storeMemory() auf, das Repository speichert die Notiz, und das Tool liefert etwa:

Stored memory M4.

Später kann der Nutzer fragen:

What should the next shift verify?

Dann kann der Agent searchMemories() aufrufen und die gefundene Notiz zur Antwort verwenden.

Repository-Schicht erstellen

Im Repository steckt der Großteil der Logik für dauerhafte Memory. Es ist weiterhin pures JDBC, was das Design leicht nachvollziehbar macht.

Auf hoher Ebene ist MemoryRepository zuständig für:

  • Schemaerstellung
  • Optionales Seed-Daten
  • Einfügen von Notizen
  • Schlüsselwortextraktion
  • Oracle Text-Suche
  • Fallback: Abruf jüngster Notizen, wenn keine brauchbaren Schlüsselwörter vorhanden sind

Beim Einfügen wird der Notiztext normalisiert und validiert, bevor er gespeichert wird:

// Normalize the note, store its JSON document, and return the generated ID.
 public long store(String note) {
 	String normalized = normalizeNote(note);
 	Instant createdAt = Instant.now();
 	try (
         	Connection connection = dataSource.getConnection();
         	PreparedStatement statement = connection.prepareStatement(
                 	INSERT_SQL,
                 	new String[]{"id"})
 	) {
     	statement.setObject(
             	1,
                 toJsonDocument(normalized, createdAt),
             	OracleTypes.JSON
     	);
     	statement.setTimestamp(2, Timestamp.from(createdAt));
     	statement.executeUpdate();
     	try (ResultSet keys = statement.getGeneratedKeys()) {
         	if (keys.next()) {
             	return keys.getLong(1);
         	}
     	}
     	throw new IllegalStateException(
             	"No generated id returned for stored memory."
     	);
 	} catch (SQLException e) {
     	throw new RuntimeException("Failed to store memory", e);
 	}
 }

Das JSON-Dokument sieht so aus:

{
 	"noteText": "Runbook note: use section 3 to validate checkout recovery after a rollback.",
 	"createdAt": "2026-04-27T17:00:00Z"
 }

Die Struktur ist simpel, reicht aber für Persistenz und Textsuche aus.

Die Suchstrategie verstehen

Der interessanteste Teil des Repositories ist die Methode search():

// Search by extracted keywords or fall back to recent notes.
 public List<Note> search(String question, int maxResults) {
 	List<String> keywords = extractKeywords(question);
 	if (keywords.isEmpty()) {
     	return findRecent(maxResults);
 	}
 	return textSearch(buildTextExpression(keywords), maxResults);
 }

Diese Methode folgt einem pragmatischen Ablauf:

1. Die Nutzerfrage tokenisieren

2. Tokens in Kleinbuchstaben umwandeln

3. Stoppwörter wie the, what und remember entfernen

4. Bedeutungsvolle kurze Tokens mit Ziffern beibehalten

5. Einen Oracle Text-Ausdruck mit ACCUM bauen

6. Die JSON-Daten mit json_textcontains durchsuchen

Dieses Detail „Ziffern sind erlaubt“ ist wichtig. So überleben Begriffe wie CHG2145 die Keyword-Extraktion – genau die Art kennzeichnerlastiger Suche, die viele Operations-Agenten brauchen.

Sechs Schritte: von der Nutzerfrage über Keyword-Extraktion, Stoppwort-Entfernung, einen ACCUM-Ausdruck und json_textcontains zu gerankten Memory-Notizen.

Der Such-Workflow wandelt eine Frage in einen Keyword-Ausdruck um und liefert gerankte Memory-Notizen.

Die SQL-Suchabfrage lautet:

-- Rank matching notes by Oracle Text relevance.
 SELECT id,
    	JSON_VALUE(
        	memory_doc,
        	'$.noteText' RETURNING VARCHAR2(2000 CHAR)
    	) AS note_text,
    	created_at,
    	SCORE(1) AS text_score
 FROM agent_memories
 WHERE JSON_TEXTCONTAINS(memory_doc, '$', ?, 1)
 ORDER BY SCORE(1) DESC, id DESC
 FETCH FIRST ? ROWS ONLY

Ergibt die Frage keine brauchbaren Schlüsselwörter, fällt das Repository auf jüngste Notizen zurück. So hat der Agent einen sinnvollen Standard, statt zu früh eine leere Suche zurückzugeben.

Alles in der Anwendung zusammenführen

Die Anwendung verdrahtet die Bausteine mit AiServices.builder():

// Build the chat model, then attach the memory tools to the assistant.
 ChatModel chatModel = OpenAiChatModel.builder()
     	.apiKey(apiKey)
         .modelName("gpt-5-nano")
     	.build();

 MemoryAssistant assistant = AiServices.builder(MemoryAssistant.class)
     	.chatModel(chatModel)
     	.tools(new MemoryTools(repository))
     	.build();

Das Muster auf einen Blick:

  • Ein Chatmodell erstellen
  • Tool-Methoden exponieren
  • Diese Tools an einen Interface-gesteuerten Agenten binden

Anschließend leitet die Konsolenschleife die Eingaben einfach an assistant.chat(input) weiter.

Die Demo-Prompts ausprobieren

Die Anwendung druckt ein paar Startprompts:

What do we know about CHG2145?
 Remember that the next shift should verify the checkout retry queue stays below 50.
 What should the next shift verify?

Diese Prompts sind hilfreich, weil sie den kompletten Memory-Lebenszyklus abdecken:

  • Eine bestehende Seed-Notiz abrufen
  • Eine neue Notiz speichern
  • Die neu gespeicherte Notiz später abrufen

Tipps und Troubleshooting

Das Modul ist bewusst fokussiert – ideal, um das Muster zu lernen. Ein paar praktische Punkte solltest du dennoch beachten.

Tool-Anweisungen klar formulieren

Der System-Prompt weist das Modell an, nur dann zu speichern, wenn der Nutzer es ausdrücklich verlangt. Das reduziert versehentliche Schreibvorgänge. Wenn du den Prompt unbedacht erweiterst, könnte das Modell anfangen, wenig wertige Notizen zu persistieren.

Als dauerhafte Notiz-Memory nutzen, nicht als vollwertiges Knowledge-Retrieval

Dieses Repository eignet sich am besten für kurze Operationsnotizen, Erinnerungen und Bezeichner. Es ist noch kein vollständiges Retrieval-Augmented-Generation-System mit Embeddings, Chunking, Hybrid-Suche oder Ranking.

Oracle Text nutzen, wenn Bezeichner zählen

Für Strings wie Incident-IDs, Ticketnummern und Change-Request-Referenzen ist schlüsselwortorientiertes Retrieval oft der bessere Startpunkt als semantische Suche. Dieses Beispiel spiegelt diesen Trade-off klar wider.

Datenbank- und Docker-Setup separat prüfen

Die Tests des Projekts sind Integrationstests mit Testcontainers. Ist Docker nicht verfügbar, schlägt mvn test fehl, bevor die Repository-Logik läuft. Das ist ein Umgebungsproblem, kein zwingender Anwendungsfehler.

Führe die Tests so aus:

# Run the integration tests.
 mvn test

Die Tests decken zwei wichtige Verhaltensweisen ab:

  • Notizen können über das Repository gespeichert und wieder abgerufen werden
  • Tool-Ausgaben enthalten Memory-IDs und relevante Notiztexte

Einfach starten, bevor du mehr Memory-Features hinzufügst

Ein häufiger Fehler in Agentenprojekten ist, Vektorspeicher, Gesprächsverlauf, Zusammenfassungen und Retrieval-Policies gleichzeitig einzubauen. Dieses Beispiel zeigt eine bessere Reihenfolge:

1. Einen konkreten Memory-Anwendungsfall definieren

2. Explizite Tools bereitstellen

3. Diese Tools mit dauerhafter Speicherung hinterlegen

4. Das Retrieval-Verhalten verifizieren

5. Erst dann erweiterte Memory-Muster in Betracht ziehen

Weiterlernen

Wenn du nach diesem Tutorial weiter Agentenkompetenzen aufbauen willst, sind diese DataCamp-Ressourcen ein guter nächster Schritt:

Fazit

Du hast gesehen, wie du mit LangChain4j, Java und Oracle AI Database einen kleinen, aber nützlichen Agenten mit Memory baust. Der Kern ist einfach: Gib dem Modell explizite Memory-Tools, speichere Notizen dauerhaft als JSON und rufe sie mit einer geradlinigen Suchstrategie ab.

Dieses Design ist eine starke Basis, weil es leicht zu verstehen und zu erweitern ist. Sobald das Muster steht, kannst du bessere Filter, reichere Notizschemata, Nutzerscoping oder bei Bedarf auch Vektorsuche ergänzen.

Wenn du mehr über Agentendesign lernen möchtest, vergleiche dieses toolbasierte Memory-Muster als Nächstes mit größeren Agentenframeworks und Orchestrierungs-Workflows. Besonders spannend ist, wie explizite Tools, Retrieval-Muster und mehrschrittige Agenten in größeren KI-Systemen zusammenspielen.

FAQs

Welche Art von Memory implementiert dieses Tutorial?

Dieses Tutorial implementiert toolbasierte, persistente Memory. Notizen werden in einer Datenbanktabelle gespeichert und explizit über Toolaufrufe abgerufen, statt automatisch ins Prompt-Fenster angehängt zu werden.

Verwendet dieses Projekt Vektor-Embeddings?

Nein. Dieses Beispiel verzichtet bewusst auf Embeddings, Vektorsuche und Reranking. Es nutzt Oracle Text mit json_textcontains, um den Notizinhalt zu durchsuchen.

Warum Tools für Memory nutzen statt alles im Chatverlauf zu behalten?

Toolbasierte Memory gibt dir mehr Kontrolle darüber, was wann gespeichert wird und wie der Abruf funktioniert. Außerdem eignet sie sich besser für dauerhafte Notizen, die länger als eine einzelne Unterhaltung bestehen müssen.

Was brauche ich, um die Live-Demo auszuführen?

Du brauchst Java 21 oder neuer, Maven 3.9 oder neuer, Zugriff auf die Oracle AI Database und einen OPENAI_API_KEY in deiner Shell.

Brauchen die Tests Docker?

Ja. Die Integrationstests verwenden Testcontainers, um Oracle AI Database Free zu starten. Daher muss Docker in deiner Umgebung verfügbar sein.

Anders Swanson

Autor

Anders Swanson

Als Developer Evangelist bei Oracle helfe ich Entwicklerinnen und Entwicklern, moderne Apps zu bauen – von Microservices über Event-Driven-Architekturen bis hin zu Cloud-nativen Systemen und mehr. Wenn du mit Tools wie Kubernetes, Spring Boot oder Kafka arbeitest und dich fragst, wie Oracle in dieses Ökosystem passt, bin ich dein Ansprechpartner.Mein Ziel ist es, praxisnahe, hands-on Anleitungen zu teilen, mit denen du schneller entwickelst, smarter auslieferst und das Beste aus Oracles Datenbanktechnologie herausholst. Ob du mit JSON arbeitest, Event-Streaming nutzt, Vektorsuche einsetzt oder gerade erst einsteigst – ich sorge dafür, dass alles klarer und entwicklerfreundlicher wird.

Themen

Ähnlich

Tutorial

Python Datenstrukturen Tutorial