跳至主要内容

LangChain4j 工具化记忆教程:使用 Oracle AI Database 构建 Java Agent

学习如何构建一个简单的 LangChain4j 代理,让它能在多轮对话间记住重要备注。
· 8分钟 阅读

使用 AI 探索

在本教程中,我们将一起完成一个小型 Java 模块,它暴露自定义工具,将备注以 JSON 形式存储在Oracle AI Database 中,并通过全文搜索进行检索。

如果您在构建 AI 代理,仅靠短期的聊天历史通常不够。真正的助手往往需要少量可持久化的记忆,以便保存重要事实并在之后取回。

本教程展示如何用一个最小化的 Java 模块实现这一模式。您将使用LangChain4j 工具、一个简单的代理接口,以及 Oracle AI Database 作为后端存储。不引入向量嵌入或完整检索管道,本示例有意保持设计小而精:备注以 JSON 文档保存,并通过Oracle Text 搜索。

结束时,您将理解模块如何将以下部分连接起来:

  • 一个 LangChain4j 代理接口
  • 两个自定义 @Tool 方法
  • 一个JDBC 仓储层用于持久化存储备注
  • 一个串联整个流程的控制台应用

什么是 LangChain4j 的工具化记忆?

LangChain4j 允许您将普通的 Java 方法暴露为工具,模型在需要外部能力时可以调用它们。在本模块中,这些能力面向记忆:

  • searchMemories(String question) 查找相关的已保存备注
  •  storeMemory(String note) 写入一条新的可持久备注

这不同于仅在内存上下文窗口中保存聊天消息的标准聊天历史。

持久化的工具化记忆 会自行决定何时搜索或存储备注,底层数据保存在数据库中。

当您希望代理记住事故备注、运行手册提醒或后续行动,而又不想过早构建更大的检索系统时,这一模式很实用。

基于数据库的持久记忆还可以被一个或多个代理并发访问,而不会产生数据争用。

流程:用户请求到达 LangChain4j 助手,助手在返回简明回答前,在 Oracle AI Database 中搜索或存储持久化的 JSON 备注。

工具化记忆让助手能够在持久化数据库中检索现有备注或存储新备注。

LangChain4j 与 Oracle AI DB 模块的关键特性

这个示例虽小,但演示了多个重要的代理设计理念。

自定义 LangChain4j 工具

MemoryTools 类暴露了两个用 @Tool 注解的方法。这意味着 LangChain4j 无需额外包装代码即可将它们提供给代理使用。

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

该方法做了两件有用的事:将检索委托给仓储层,并以对模型友好的格式(如 [M12] some note text)整理结果。

这样,工具的输出就便于代理在最终回复中引用。

Oracle AI Database 中的持久记忆

备注存储在 agent_memories 表中,作为原生 JSON 文档。每行包含:

  • 标识列 id
  • 名为 memory_doc 的 JSON 列
  • 一个 created_at 时间戳

模块会自动初始化该模式:

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

这既保持了存储模型的简洁,也提供了结构化数据与数据库管理的持久性。

使用 Oracle Text 进行关键词搜索

仓储层不生成向量嵌入。它会从用户问题中提取关键词、移除停用词,使用 ACCUM 构建 Oracle Text 表达式,并通过 json_textcontains 搜索 JSON 文档内容。

这对 CHG2145 等标识符与运维术语尤其有用,这类场景往往更需要精确或近似精确的词元匹配,而非语义相似度。

一个最简控制台工作流

Application 类会创建仓储、初始化模式、预置少量备注、构建聊天模型,并启动终端循环。这样示例既易于运行,也便于日后改造成其他服务。

开始上手您的 LangChain4j 记忆项目

在运行该模块前,请确保本地环境满足项目要求。

前置条件

您需要:

  • Java 21 或更高版本
  • Maven 3.9 或更高版本
  • 本地或远程运行的 Oracle AI Database Free
  • 一个 OPENAI_API_KEY 环境变量

Maven 项目依赖:

  • langchain4j
  • langchain4j-open-ai
  • ojdbc11
  • 用于集成测试的 JUnit 和 Testcontainers

运行示例

先设置您的 OpenAI API 密钥:

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

然后在项目目录下运行应用:

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

应用需要且仅需要三个参数:

1. JDBC URL

2. 数据库用户名

3. 数据库密码

如果缺少这些参数,Application.main() 将抛出 IllegalArgumentException。若缺少 OPENAI_API_KEY,则抛出 IllegalStateException。

启动时会发生什么

应用启动后会:

1. 创建一个 MemoryRepository

2. 初始化数据库模式

3. 预置三条示例备注(若表为空)

4. 构建一个 OpenAiChatModel

5. 将模型和工具接入 AiServices

6. 启动一个 CLI 循环读取用户输入,直到您输入 exit

这意味着您无需先手工插入数据行,就能立刻尝试记忆检索。

如何用 LangChain4j 构建具备记忆的 Java 代理

现在按照应用的使用顺序,逐步了解该模块。

定义代理接口

LangChain4j 使用 Java 接口描述代理契约。本项目中的接口是 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);
 }

关键在于系统消息。它告知模型何时使用各个工具:

  • 当早期备注可能有帮助时进行搜索
  • 当用户明确要求“记住以备后用”时进行存储

这是轻量级工具策略的一个好示例。提示简短,但足以指导工具选择。

将记忆操作暴露为工具

代理不直接与仓储交互,而是调用 MemoryTools 层。

下面是存储方法:

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

这里有意保持简洁。用户可能会说:

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

如果模型遵循系统提示,它会调用 storeMemory(),仓储会持久化该备注,工具返回类似:

Stored memory M4.

之后,用户可以询问:

What should the next shift verify?

此时代理可以调用 searchMemories() 并用返回的备注作答。

创建仓储层

仓储层承载了大部分持久记忆逻辑。它仍然是朴素的 JDBC,设计易于审查。

从高层看,MemoryRepository 负责:

  • 模式创建
  • 可选的预置数据
  • 备注插入
  • 关键词提取
  • Oracle Text 搜索
  • 当没有有用关键词时回退到最近备注

插入路径会在存储前规范化并校验备注文本:

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

JSON 文档本身如下:

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

结构虽简单,却足以支持持久化与文本搜索。

理解搜索策略

仓储中最有意思的是 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);
 }

该方法遵循一条务实流程:

1. 将用户问题分词

2. 将词元转为小写

3. 去除 the、what、remember 等停用词

4. 当短词包含数字时保留它们

5. 使用 ACCUM 构建 Oracle Text 表达式

6. 使用 json_textcontains 搜索 JSON 数据

“允许数字”这一细节很重要。它有助于让 CHG2145 这样的词保留下来——这正是许多运维代理常见的、以标识符为主的查找场景所需要的。

六步流程:从用户问题到关键词提取、停用词移除、ACCUM 表达式与 json_textcontains,再到排序后的记忆备注。

搜索工作流将问题转为关键词表达式,并返回排序后的记忆备注。

SQL 搜索查询如下:

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

如果问题没有产生有用的关键词,仓储会回退到最近备注。这为代理提供了合理的默认行为,而不是过早返回空结果。

在应用中将一切串联起来

应用使用 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();

完整模式一目了然:

  • 构建聊天模型
  • 暴露工具方法
  • 将这些工具绑定到接口驱动的代理

之后,控制台循环只需将用户输入传给 assistant.chat(input)。

试试演示用提示词

应用会打印一些入门提示:

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?

这些提示很有用,因为它们覆盖了完整的记忆生命周期:

  • 检索一条已预置的备注
  • 存储一条新备注
  • 随后检索刚存储的备注

技巧与故障排查

该模块刻意聚焦狭窄场景,这在学习模式时是一种优势。但仍需注意一些实践细节。

保持工具指令的明确性

系统提示要求模型仅在用户明确提出时才存储记忆,这有助于减少意外写入。如果不加约束地扩展提示,模型可能会开始持久化低价值备注。

将其视为持久备注记忆,而非完整知识检索

该仓储最适合简短的运维备注、提醒和标识符。目前它还不是带有向量嵌入、切分、混合搜索或重排的完整 RAG 系统。

当标识符重要时使用 Oracle Text

对于事故 ID、工单号、变更请求引用等字符串,关键词取向的检索往往比语义搜索更合适。本示例清晰体现了这一取舍。

分别验证数据库与 Docker 设置

项目测试是由 Testcontainers 驱动的集成测试。如果 Docker 不可用,mvn test 会在仓储逻辑运行前失败。这是环境问题,不一定是应用缺陷。

通过以下命令运行测试:

# Run the integration tests.
 mvn test

测试覆盖两个重要行为:

  • 备注可通过仓储被存储与检索
  • 工具输出包含记忆 ID 与相关备注文本

在添加更多记忆特性前先保持简单

代理项目中的常见错误是一次性加入向量存储、会话历史、摘要以及检索策略。本示例展示了更好的演进路径:

1. 明确定义一个具体的记忆用例

2. 暴露明确的工具

3. 用持久化存储支撑这些工具

4. 验证检索行为

5. 仅在此之后再考虑更高级的记忆模式

继续学习

如果您想在本教程之后继续提升代理构建技能,这些 DataCamp 资源是不错的下一步:

结语

您已经了解了如何使用 LangChain4j、Java 和 Oracle AI Database 构建一个小而实用、具备记忆能力的代理。核心思路很简单:给模型明确的记忆工具,将备注以 JSON 持久化存储,并用直接明了的搜索策略进行检索。

这种设计是一个坚实的基础,因为它易于理解且便于扩展。一旦该模式运行良好,您可以根据需求添加更好的过滤、更丰富的备注模式、用户作用域,甚至引入向量搜索。

如果您想继续学习代理设计,下一步可以将这种工具化记忆模式与更广泛的代理框架与编排工作流进行比较。特别值得探索的是,明确的工具、检索模式与多步代理如何在更大的 AI 系统中协同配合。

FAQs

本教程实现了哪种记忆?

本教程实现的是基于工具的持久记忆。备注存储在数据库表中,并通过显式的工具调用进行检索,而非自动附加到提示窗口。

该项目是否使用了向量嵌入?

没有。该示例有意避免使用嵌入、向量搜索和重排。它使用 Oracle Text 与 json_textcontains 搜索备注内容。

为何使用工具实现记忆,而不是把所有内容放在聊天历史中?

工具化记忆让您更好地控制哪些内容被存储、何时存储以及如何检索。它也更适合需要跨会话保留的持久备注。

运行在线演示需要什么?

您需要 Java 21 或更高版本、Maven 3.9 或更高版本、可访问的 Oracle AI Database,以及在 shell 中设置 OPENAI_API_KEY。

测试是否需要 Docker?

需要。集成测试使用 Testcontainers 启动 Oracle AI Database Free,因此您的环境必须可用 Docker。

Anders Swanson

作者

Anders Swanson

作为 Oracle 的开发者布道师,我帮助开发者构建现代应用,包括微服务、事件驱动系统、云原生架构等。如果您在使用 Kubernetes、Spring Boot 或 Kafka,并想知道 Oracle 在这个生态中如何发挥作用,我正是为此而来。我的目标是分享实用、可操作的指南,帮助您构建更快、交付更稳,并充分利用 Oracle 的数据库技术。无论您在用 JSON、事件流、向量检索,还是刚开始上手,我都会尽量把事情讲清楚,让开发体验更友好。

主题