// Retrieve relevant notes and format them for the assistant.@Tool("Search saved memory notes when earlier context might help answer the user.")publicStringsearchMemories(String question){List<MemoryRepository.Note> matches = repository.search(question,3);if(matches.isEmpty()){return"No saved memory matched that question.";}StringBuilder builder =newStringBuilder();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.CREATETABLEIFNOTEXISTS agent_memories (
id NUMBER GENERATED ALWAYS ASIDENTITYPRIMARYKEY,
memory_doc JSON NOTNULL,
created_at TIMESTAMPDEFAULTCURRENT_TIMESTAMPNOTNULL)技术支持
这既保持了存储模型的简洁,也提供了结构化数据与数据库管理的持久性。
使用 Oracle Text 进行关键词搜索
仓储层不生成向量嵌入。它会从用户问题中提取关键词、移除停用词,使用 ACCUM 构建 Oracle Text 表达式,并通过 json_textcontains 搜索 JSON 文档内容。
# Set the API key used by the chat model.exportOPENAI_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"技术支持
// Define the interface that receives each user message.publicinterfaceMemoryAssistant{@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}}")Stringchat(@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.")publicStringstoreMemory(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.技术支持
// Normalize the note, store its JSON document, and return the generated ID.publiclongstore(String note){String normalized =normalizeNote(note);Instant createdAt =Instant.now();try(Connection connection = dataSource.getConnection();PreparedStatement statement = connection.prepareStatement(INSERT_SQL,newString[]{"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);}}thrownewIllegalStateException("No generated id returned for stored memory.");}catch(SQLException e){thrownewRuntimeException("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.publicList<Note>search(String question,int maxResults){List<String> keywords =extractKeywords(question);if(keywords.isEmpty()){returnfindRecent(maxResults);}returntextSearch(buildTextExpression(keywords), maxResults);}技术支持
// 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(newMemoryTools(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?技术支持
在本教程中,我们将一起完成一个小型 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 DB 模块的关键特性
这个示例虽小,但演示了多个重要的代理设计理念。
自定义 LangChain4j 工具
MemoryTools类暴露了两个用@Tool注解的方法。这意味着LangChain4j无需额外包装代码即可将它们提供给代理使用。该方法做了两件有用的事:将检索委托给仓储层,并以对模型友好的格式(如
[M12] some note text)整理结果。这样,工具的输出就便于代理在最终回复中引用。
Oracle AI Database 中的持久记忆
备注存储在
agent_memories表中,作为原生 JSON 文档。每行包含:idmemory_doc的 JSON 列created_at时间戳模块会自动初始化该模式:
这既保持了存储模型的简洁,也提供了结构化数据与数据库管理的持久性。
使用 Oracle Text 进行关键词搜索
仓储层不生成向量嵌入。它会从用户问题中提取关键词、移除停用词,使用
ACCUM构建Oracle Text表达式,并通过json_textcontains搜索 JSON 文档内容。这对
CHG2145等标识符与运维术语尤其有用,这类场景往往更需要精确或近似精确的词元匹配,而非语义相似度。一个最简控制台工作流
Application类会创建仓储、初始化模式、预置少量备注、构建聊天模型,并启动终端循环。这样示例既易于运行,也便于日后改造成其他服务。开始上手您的 LangChain4j 记忆项目
在运行该模块前,请确保本地环境满足项目要求。
前置条件
您需要:
OPENAI_API_KEY环境变量Maven 项目依赖:
langchain4jlangchain4j-open-aiojdbc11Testcontainers运行示例
先设置您的 OpenAI API 密钥:
然后在项目目录下运行应用:
应用需要且仅需要三个参数:
1.
JDBCURL2. 数据库用户名
3. 数据库密码
如果缺少这些参数,
Application.main()将抛出IllegalArgumentException。若缺少OPENAI_API_KEY,则抛出IllegalStateException。启动时会发生什么
应用启动后会:
1. 创建一个
MemoryRepository2. 初始化数据库模式
3. 预置三条示例备注(若表为空)
4. 构建一个
OpenAiChatModel5. 将模型和工具接入
AiServices6. 启动一个 CLI 循环读取用户输入,直到您输入
exit这意味着您无需先手工插入数据行,就能立刻尝试记忆检索。
如何用 LangChain4j 构建具备记忆的 Java 代理
现在按照应用的使用顺序,逐步了解该模块。
定义代理接口
LangChain4j使用 Java 接口描述代理契约。本项目中的接口是MemoryAssistant。关键在于系统消息。它告知模型何时使用各个工具:
这是轻量级工具策略的一个好示例。提示简短,但足以指导工具选择。
将记忆操作暴露为工具
代理不直接与仓储交互,而是调用
MemoryTools层。下面是存储方法:
这里有意保持简洁。用户可能会说:
如果模型遵循系统提示,它会调用
storeMemory(),仓储会持久化该备注,工具返回类似:之后,用户可以询问:
此时代理可以调用
searchMemories()并用返回的备注作答。创建仓储层
仓储层承载了大部分持久记忆逻辑。它仍然是朴素的
JDBC,设计易于审查。从高层看,
MemoryRepository负责:Oracle Text搜索插入路径会在存储前规范化并校验备注文本:
JSON 文档本身如下:
结构虽简单,却足以支持持久化与文本搜索。
理解搜索策略
仓储中最有意思的是
search()方法:该方法遵循一条务实流程:
1. 将用户问题分词
2. 将词元转为小写
3. 去除
the、what、remember等停用词4. 当短词包含数字时保留它们
5. 使用
ACCUM构建Oracle Text表达式6. 使用
json_textcontains搜索 JSON 数据“允许数字”这一细节很重要。它有助于让
CHG2145这样的词保留下来——这正是许多运维代理常见的、以标识符为主的查找场景所需要的。搜索工作流将问题转为关键词表达式,并返回排序后的记忆备注。
SQL 搜索查询如下:
如果问题没有产生有用的关键词,仓储会回退到最近备注。这为代理提供了合理的默认行为,而不是过早返回空结果。
在应用中将一切串联起来
应用使用
AiServices.builder()将各部分连接:完整模式一目了然:
之后,控制台循环只需将用户输入传给
assistant.chat(input)。试试演示用提示词
应用会打印一些入门提示:
这些提示很有用,因为它们覆盖了完整的记忆生命周期:
技巧与故障排查
该模块刻意聚焦狭窄场景,这在学习模式时是一种优势。但仍需注意一些实践细节。
保持工具指令的明确性
系统提示要求模型仅在用户明确提出时才存储记忆,这有助于减少意外写入。如果不加约束地扩展提示,模型可能会开始持久化低价值备注。
将其视为持久备注记忆,而非完整知识检索
该仓储最适合简短的运维备注、提醒和标识符。目前它还不是带有向量嵌入、切分、混合搜索或重排的完整 RAG 系统。
当标识符重要时使用 Oracle Text
对于事故 ID、工单号、变更请求引用等字符串,关键词取向的检索往往比语义搜索更合适。本示例清晰体现了这一取舍。
分别验证数据库与 Docker 设置
项目测试是由
Testcontainers驱动的集成测试。如果 Docker 不可用,mvn test会在仓储逻辑运行前失败。这是环境问题,不一定是应用缺陷。通过以下命令运行测试:
测试覆盖两个重要行为:
在添加更多记忆特性前先保持简单
代理项目中的常见错误是一次性加入向量存储、会话历史、摘要以及检索策略。本示例展示了更好的演进路径:
1. 明确定义一个具体的记忆用例
2. 暴露明确的工具
3. 用持久化存储支撑这些工具
4. 验证检索行为
5. 仅在此之后再考虑更高级的记忆模式
继续学习
如果您想在本教程之后继续提升代理构建技能,这些 DataCamp 资源是不错的下一步:
结语
您已经了解了如何使用
LangChain4j、Java 和 Oracle AI Database 构建一个小而实用、具备记忆能力的代理。核心思路很简单:给模型明确的记忆工具,将备注以 JSON 持久化存储,并用直接明了的搜索策略进行检索。这种设计是一个坚实的基础,因为它易于理解且便于扩展。一旦该模式运行良好,您可以根据需求添加更好的过滤、更丰富的备注模式、用户作用域,甚至引入向量搜索。
如果您想继续学习代理设计,下一步可以将这种工具化记忆模式与更广泛的代理框架与编排工作流进行比较。特别值得探索的是,明确的工具、检索模式与多步代理如何在更大的 AI 系统中协同配合。