Przejdź do głównej treści

LangChain4j Tool Memory Tutorial: Build a Java Agent With Oracle AI Database

Learn how to build a simple LangChain4j agent that can remember important notes across conversations.
13 wrz 2026  · 8 min Czytać

Eksploruj z AI

ChatGPTClaudePerplexity

In this tutorial, we'll walk through a small Java module that exposes custom tools, stores notes as JSON in Oracle AI Database, and retrieves them with full-text search.

If you're building AI agents, short-term chat history is usually not enough. Real assistants often need a small amount of durable memory so they can store important facts and retrieve them later.

This tutorial shows how to build that pattern with a minimal Java module. You'll use LangChain4j tools, a simple agent interface, and Oracle AI Database as the backing store. Instead of adding embeddings or a full retrieval pipeline, this sample keeps the design intentionally small: notes are saved as JSON documents and searched with Oracle Text.

By the end, you will understand how the module wires together:

  • A LangChain4j agent interface
  • Two custom @Tool methods
  • AJDBC repository for durable note storage
  • A console application that ties the whole flow together

What Is LangChain4j Tool-Based Memory?

LangChain4j lets you expose ordinary Java methods as tools that a model can call when it needs external capabilities. In this module, those capabilities are memory-oriented:

  • searchMemories(String question) finds relevant saved notes
  •  storeMemory(String note) writes a new durable note

That is different from standard chat history that stores chat messages in an in-memory context window.

Persistent tool-based memory decides when it needs to search or store notes, and the underlying data lives in a database.

This pattern is useful when you want an agent to remember facts like incident notes, runbook reminders, or follow-up actions without building a larger retrieval system too early. 

Persistent, database-backed memory can also be accessed concurrently by one or more agents without data contention.

Flow showing a user request reaching a LangChain4j assistant, which searches or stores durable JSON notes in Oracle AI Database before returning a concise response.

Tool-based memory lets the assistant retrieve existing notes or store new notes in a durable database.

Key Features of the LangChain4j and Oracle AI DB Module

This sample is small, but it demonstrates several important agent design ideas.

Custom LangChain4j tools

The MemoryTools class exposes two methods annotated with @Tool. That means LangChain4j can make them available to the agent without extra wrapper code.

// 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();
 }

This method does two useful things. It delegates retrieval to the repository, and it formats the results in a model-friendly way such as [M12] some note text

That makes the tool output easy for the agent to cite in its final response.

Durable memory in Oracle AI Database

Notes are stored in an agent_memories table as native JSON documents. Each row contains:

  • An identity column id
  • A JSON column named memory_doc
  • A created_at timestamp

The module initializes the schema automatically:

-- 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
 )

This keeps the storage model simple while still giving you structured data and database-managed durability.

Keyword search with Oracle Text

The repository does not generate embeddings. Instead, it extracts keywords from the user’s question, removes stop words, builds an Oracle Text expression with ACCUM, and searches the JSON document content with json_textcontains.

That is especially helpful for identifiers and operational terms like CHG2145, where exact or near-exact token matching often matters more than semantic similarity.

A minimal console workflow

The Application class creates the repository, initializes the schema, seeds a few notes, constructs the chat model, and starts a terminal loop. That makes the example easy to run and easy to adapt into another service later.

Getting Started With Your LangChain4j Memory Project

Before running the module, make sure your local environment matches the project’s requirements.

Prerequisites

You need:

  • Java 21 or later
  • Maven 3.9 or later
  • Oracle AI Database Free running locally or remotely
  • An OPENAI_API_KEY environment variable

The Maven project depends on:

  • langchain4j
  • langchain4j-open-ai
  • ojdbc11
  • JUnit and Testcontainers for integration tests

Run the sample

Set your OpenAI API key first:

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

Then run the app from the project directory:

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

The application expects exactly three arguments:

1. JDBC URL

2. Database username

3. Database password

If those are missing, Application.main() throws an IllegalArgumentException. If OPENAI_API_KEY is missing, it throws an IllegalStateException.

What happens on startup

When the app starts, it:

1. Creates a MemoryRepository

2. Initializes the database schema

3. Seeds three sample notes if the table is empty

4. Builds an OpenAiChatModel

5. Wires the model and tools into AiServices

6. Starts a CLI loop that reads user input until you type exit

That means you can try memory retrieval immediately without manually inserting rows first.

How to Build a Memory-Enabled Java Agent With LangChain4j

Now let’s walk through the module in the same order the application uses it.

Define the agent interface

LangChain4j uses a Java interface to describe the agent contract. In this project, that interface is 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);
 }

The important part is the system message. It tells the model when to use each tool:

  • Search when earlier notes might help
  • Store when the user explicitly asks the assistant to remember something

That is a good example of lightweight tool policy. The prompt is short, but it is specific enough to guide tool selection.

Expose memory operations as tools

The agent does not talk to the repository directly. Instead, it calls the MemoryTools layer.

Here is the storage method:

// 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 + ".";
 }

This is intentionally compact. A user might say:

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

If the model follows the system prompt correctly, it calls storeMemory(), the repository persists the note, and the tool returns something like:

Stored memory M4.

Later, the user can ask:

What should the next shift verify?

At that point, the agent can call searchMemories() and use the returned note to answer.

Create the repository layer

The repository is where most of the durable memory logic lives. It is still plain JDBC, which makes the design easy to inspect.

At a high level, MemoryRepository is responsible for:

  • Schema creation
  • Optional seeding
  • Note insertion
  • Keyword extraction
  • Oracle Text search
  • Fallback retrieval of recent notes when there are no useful keywords

The insertion path normalizes and validates note text before storing it:

// 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);
 	}
 }

The JSON document itself looks like this:

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

This structure is simple, but it is enough to support both persistence and text search.

Understand the search strategy

The most interesting part of the repository is the search() method:

// 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);
 }

This method follows a practical flow:

1. Tokenize the user question

2. Lowercase the tokens

3. Remove stop words such as the, what, and remember

4. Keep meaningful short tokens when they include digits

5. Build an Oracle Text expression with ACCUM

6. Search the JSON data with json_textcontains

That “digits are allowed” detail is important. It helps terms like CHG2145 survive keyword extraction, which is exactly the kind of identifier-heavy lookup many operational agents need.

Six-step flow from a user question through keyword extraction, stop-word removal, an ACCUM expression, and json_textcontains to ranked memory notes.

The search workflow converts a question into a keyword expression and returns ranked memory notes.

The SQL search query is:

-- 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

If the question does not yield useful keywords, the repository falls back to recent notes. That gives the agent a reasonable default instead of returning an empty search too early.

Wire everything together in the application

The application ties the pieces together with 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();

This is the full pattern in one place:

  • Build a chat model
  • Expose tool methods
  • Bind those tools to an interface-driven agent

After that, the console loop simply passes user input into assistant.chat(input).

Try the demo prompts

The application prints a few starter prompts:

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?

These prompts are useful because they exercise the full memory lifecycle:

  • Retrieve an existing seeded note
  • Store a new note
  • Retrieve the newly stored note later

Tips and Troubleshooting

This module is intentionally narrow, which is a strength when you are learning the pattern. Still, there are a few practical details to keep in mind.

Keep the tool instructions explicit

The system prompt tells the model to store memory only when the user explicitly asks for it. That helps reduce accidental writes. If you broaden the prompt carelessly, the model may start persisting low-value notes.

Treat this as durable note memory, not full knowledge retrieval

This repository is best for short operational notes, reminders, and identifiers. It is not yet a full retrieval-augmented generation system with embeddings, chunking, hybrid search, or ranking.

Use Oracle Text when identifiers matter

For strings like incident IDs, ticket numbers, and change request references, keyword-oriented retrieval is often a better starting point than semantic search. This sample reflects that tradeoff clearly.

Validate your database and Docker setup separately

The project’s tests are integration tests powered by Testcontainers. If Docker is not available, mvn test will fail before the repository logic runs. That is an environment issue, not necessarily an application bug.

Run the tests with:

# Run the integration tests.
 mvn test

The tests cover two important behaviors:

  • Notes can be stored and retrieved through the repository
  • Tool output includes memory IDs and relevant note text

Start simple before adding more memory features

A common mistake in agent projects is adding vector storage, conversation history, summarization, and retrieval policies all at once. This sample shows a better progression:

1. Define a concrete memory use case

2. Expose explicit tools

3. Back those tools with durable storage

4. Verify the retrieval behavior

5. Only then consider more advanced memory patterns

Keep Learning

If you want to keep building agent skills after this tutorial, these DataCamp resources are a good next step:

Conclusion

You have now seen how to build a small but useful memory-enabled agent with LangChain4j, Java, and Oracle AI Database. The core idea is simple: give the model explicit memory tools, store notes durably as JSON, and retrieve them with a straightforward search strategy.

This design is a strong foundation because it is easy to understand and easy to extend. Once this pattern is working, you can add better filtering, richer note schemas, user scoping, or even vector search if the use case demands it.

If you want to keep learning about agent design, the next good step is to compare this tool-based memory pattern with broader agent frameworks and orchestration workflows. In particular, it is worth exploring how explicit tools, retrieval patterns, and multi-step agents fit together in larger AI systems.

FAQs

What kind of memory does this tutorial implement?

This tutorial implements tool-based durable memory. Notes are stored in a database table and retrieved through explicit tool calls instead of being appended automatically to the prompt window.

Does this project use vector embeddings?

No. This sample deliberately avoids embeddings, vector search, and reranking. It uses Oracle Text with json_textcontains to search note content.

Why use tools for memory instead of keeping everything in chat history?

Tool-based memory gives you more control over what gets stored, when it gets stored, and how it gets retrieved. It also works better for durable notes that need to survive beyond a single conversation.

What do I need to run the live demo?

You need Java 21 or later, Maven 3.9 or later, access to Oracle AI Database, and an OPENAI_API_KEY in your shell.

Do the tests need Docker?

Yes. The integration tests use Testcontainers to start Oracle AI Database Free, so Docker must be available in your environment.


Anders Swanson's photo
Author
Anders Swanson
LinkedIn

As a Developer Evangelist at Oracle, I help developers build modern apps, including microservices, event-driven systems, cloud-native architectures, and more. If you're working with tools like Kubernetes, Spring Boot, or Kafka, and wondering how Oracle fits into that world, that's where I come in.My goal is to share practical, hands-on guidance that helps you build faster, ship smarter, and make the most of Oracle's database tech. If you're using JSON, event streaming, vector search, or just figuring out how to get started, I'm here to make things clearer and more developer-friendly.

Tematy
AI Agents
Java

Top DataCamp Courses

course

Wprowadzenie do Oracle SQL

4 godz.
18.3K
Doskonal umiejętności w Oracle SQL, w tym podstawy SQL, agregowanie, łączenie i dostosowywanie danych.
Zobacz szczegółyRight Arrow
Rozpocznij Kurs
Zobacz więcejRight Arrow
Powiązany

Tutorial

Spring AI and Oracle Database: Giving Memory to an AI Agent

Stop your LLM from forgetting context. Learn how to build an AI agent with episodic, semantic, and procedural memory using Spring AI and Oracle Database.
Victor Martin's photo

Victor Martin

10 min

Tutorial

LangChain's Deep Agents: A Guide With Demo Project

Learn what deep agents are, their core components, and how to build a job application assistant using LangChain's deepagents package.
Aashi Dutt's photo

Aashi Dutt

12 min

Tutorial

Building LangChain Agents to Automate Tasks in Python

A comprehensive tutorial on building multi-tool LangChain agents to automate tasks in Python using LLMs and chat models using OpenAI.
Bexruz (Bex) Tuychiev's photo

Bexruz (Bex) Tuychiev

14 min

Tutorial

Building Context-Aware Chatbots: Leveraging LangChain Framework for ChatGPT

Explore how to build context-aware chatbots using the ChatGPT and LangChain framework.
Andrea Valenzuela's photo

Andrea Valenzuela

15 min

Tutorial

LangGraph Agents Hands-On Tutorial

Master LangGraph fundamentals — state, nodes, edges, memory — and build scalable AI agents with ReAct patterns, custom tools, and persistent state management.
Vaibhav Mehra's photo

Vaibhav Mehra

code-along

Build an AI Content Research Agent with Claude Code & Oracle AI Database

Casius Sibanda Lee shows you how to build a memory-powered research agent using Claude Code and Oracle AI Database as the persistent memory core.
Casius Sibanda Lee's photo

Casius Sibanda Lee

Zobacz WięcejZobacz Więcej