跳至主要内容

Spring AI 与 Oracle 数据库:为 AI 代理添“记忆”

别再让您的 LLM 忘掉上下文。学习如何使用 Spring AI 和 Oracle 数据库,为 AI 代理构建情景、语义和程序性记忆。
· 10分钟 阅读

使用 AI 探索

所有 LLM 都有同样的问题:对话一结束就把一切忘光,有时甚至在长对话中就开始遗忘。花二十分钟解释项目设置、约束条件和您的偏好,它就能给出很准确的回答。关掉标签页、开启新会话,它又像初次见面一样打招呼。所有那些上下文,都没了。

如果您想构建一个 AI 代理——能真正记住上下文、了解您领域知识的系统——就需要给它“记忆”。是那种实用的记忆,能确实记住您说过的话,并能查到您教给它的事实。

这是一份我为此目的构建的 POC。三种记忆、一个数据库、很少的代码。完整源码在 GitHub 上可用。

使用 Spring AI 的 AI 代理架构

Mermaid diagram 1

图 1. 使用 Spring AI 和 Oracle AI Database 26ai 的端到端代理记忆架构

技术栈:

  • Spring Boot 3.5.11 + Spring AI 1.1.2 作为后端
  • Ollama 用于本地运行的聊天推理(qwen2.5)
  • Oracle AI Database 26ai 承载三类记忆存储,使用混合向量索引(向量 + 关键词搜索,结合互惠排序融合 RRF)做语义检索,并加载 ONNX 模型(all-MiniLM-L12-v2)在库内生成嵌入
  • Streamlit 快速搭建 Web UI(约 100 行 Python)
  • Java 21,Gradle 8.14

三种 AI 代理记忆类型

谈到代理,通常会涉及情景记忆、语义记忆、程序性记忆和工作记忆。

工作记忆就是 LLM 的上下文窗口;当前请求的活动“草稿本”。它不持久化,所以无需构建。我实现了另外三种:

情景记忆:持久化聊天历史

情景记忆 就是聊天历史。代理会记住您在对话早些时候说过的话。第 1 条消息“我叫 Victor”,到了第 50 条它仍然知道您的名字。这些以行的形式存储在关系表中。

The agent remembers context from earlier in the conversation (episodic memory).

图 2. 情景记忆即聊天历史

语义记忆:集成 RAG

语义记忆 是领域知识。您向代理提供事实(产品文档、公司政策等),它在回答问题时检索相关内容。

这就是 RAG(检索增强生成):文本被转换为密集向量(嵌入),将语义映射到几何空间,使语义相近的文本彼此靠近。

但纯向量搜索也有盲点;它擅长捕捉含义,却不擅长精确词项,比如订单 ID 或产品编码。

因此此 POC 使用 Oracle 的混合向量索引,并行运行向量相似度搜索与关键词(词汇)搜索,然后使用互惠排序融合(RRF)将两份结果列表融合。

在查询时,混合搜索会找到按语义或精确词项匹配的文档,并将前几条结果注入 LLM 提示词。嵌入由 ONNX 模型(all-MiniLM-L12-v2,384 维)在数据库内直接计算,无需外部嵌入 API 调用。下面将详述其工作方式。

The agent answers a policy question using RAG-retrieved documents (semantic memory).

图 3. 语义记忆即领域知识

程序性记忆:LLM 工具调用

程序性记忆 是“如何做”,即代理会执行的分步工作流:查询订单、发起退货、升级至支持。在 Spring AI 中,这些是带有 @Tool 注解的方法,LLM 在判断需要采取动作而非仅给出答案时即可调用。

The agent lists orders and looks up order details via tool calls (procedural memory).

图 4. 程序性记忆的实际应用:代理通过工具调用查询实时订单数据并返回与任务相关的结果。

Mermaid diagram 2

图 5. 通过 AgentController 的请求流转,将来自 Oracle AI Database 各表的情景、语义与程序性记忆合并后,再用 Ollama 生成响应。

两张表都在同一个 Oracle 数据库里。没有 Pinecone。没有 Redis。没有第二个数据库。一个连接池、一套凭据、一个监控对象。

实现 Spring AI 工具

程序性记忆以 @Tool 注解的方法实现,位于查询真实数据库表的 Spring 组件中。下面是两个具有代表性的简化方法;完整类包含六个工具(见 AgentTools.java):

@Tool(description = "Look up the status of a customer order by its order ID. " +
        "Returns the current status including shipping information.")
public String lookupOrderStatus(
        @ToolParam(description = "The order ID to look up, e.g. ORD-1001") String orderId) {
    // Fetches order from DB via JPA, returns formatted status string
}

@Tool(description = "Initiate a product return for a given order. " +
        "Validates the order exists, checks that it is in DELIVERED status, " +
        "and verifies the return is within the 30-day return window.")
public String initiateReturn(
        @ToolParam(description = "The order ID to return") String orderId,
        @ToolParam(description = "The reason for the return") String reason) {
    // Validates order exists, checks DELIVERED status and 30-day window, updates status via JPA
}

@Tool 的描述告诉 LLM 何时 使用各方法,而 @ToolParam 描述参数。当用户说“我想退货订单 ORD-1001”时,LLM 会阅读工具描述,判断 initiateReturn 是正确流程,从对话中提取参数,调用方法,并把结果纳入回复中。

另外四个工具是 getCurrentDateTime(从数据库获取当前日期/时间)、listOrders、escalateToSupport 和 listSupportTickets。它们遵循相同模式:通过 JPA 或 JDBC 查询数据库。LLM 决定何时行动;Java 方法定义如何行动。

构建 Spring Boot 控制器

控制器把一切串起来:两个 advisor、六个工具、一个 ChatClient。核心代码如下,为清晰起见做了简化(完整版包含输入校验、错误处理和会话管理端点。见 AgentController.java):

@RestController
@RequestMapping("/api/v1/agent")
public class AgentController {

    public AgentController(ChatClient.Builder builder,
                           JdbcChatMemoryRepository chatMemoryRepository,
                           JdbcTemplate jdbcTemplate,
                           AgentTools agentTools) {
        // Builds a ChatClient with:
        //   - MessageChatMemoryAdvisor (episodic: last 100 messages per conversation)
        //   - RetrievalAugmentationAdvisor + OracleHybridDocumentRetriever (semantic: hybrid search)
        //   - AgentTools via .defaultTools() (procedural: 6 @Tool methods)
        //   - System prompt defining the agent persona and tool usage rules
    }

    @PostMapping("/chat")
    public ResponseEntity<String> chat(
            @RequestBody String message,
            @RequestHeader("X-Conversation-Id") String conversationId) {
        // Sends message to ChatClient with conversation ID, returns LLM response
    }

    @PostMapping("/knowledge")
    public ResponseEntity<String> addKnowledge(@RequestBody String content) {
        // Inserts text into POLICY_DOCS table via JDBC (hybrid index handles embedding)
    }
}

两个端点、两个 advisor、六个工具、一个 ChatClient。下面分别拆解三类记忆。

用 Spring Advisors 管理聊天记忆

Spring AI 的 advisor 模式是“魔法”所在。Advisor 会拦截每次对 LLM 的调用,能在请求发出前修改提示词,并在响应返回时处理结果。

MessageChatMemoryAdvisor 负责情景记忆。每次调用 LLM 前,它会从 SPRING_AI_CHAT_MEMORY 表加载当前会话最近 100 条消息并前置到提示词中。响应返回后,它会保存这次新的问答交换。会话由 X-Conversation-Id 头标识:ID 不同,记忆不同。

用文档检索器实现 RAG

RetrievalAugmentationAdvisor 负责语义记忆。每次调用 LLM 前,自定义的 OracleHybridDocumentRetriever 会调用 DBMS_HYBRID_VECTOR.SEARCH,并行运行向量相似度搜索与 Oracle Text 关键词搜索,再用互惠排序融合(RRF)合并结果。

前 5 个匹配文档会通过自定义 ContextualQueryAugmenter 注入到提示词中作为上下文;LLM 被告知这些文档仅用于政策相关问题,而不用于动作请求或对话上下文。这样可避免 RAG 上下文覆盖聊天历史或抑制工具调用。

为何选混合而非纯向量搜索?密集嵌入能捕捉含义:关于“退货政策”的查询会匹配到“退款”“换货”的文档,即使没有出现完全相同的词。

但它对精确词项较弱:“ORD-1001”的查询会变差,因为嵌入模型编码语义而非关键词。混合搜索两者兼顾:向量侧负责语义,关键词侧负责精确匹配,RRF 则按名次而非试图归一化不相容的分数来合并两份结果列表。

注册代理工具

AgentTools 负责程序性记忆。调用 .defaultTools(agentTools) 会注册该组件中所有六个带 @Tool 注解的方法。每个请求中,LLM 会连同用户消息一起接收工具描述。如果任务需要动作而非仅检索知识,LLM 会调用合适的工具,拿到结果并将其编织进回答。Spring AI 会自动处理工具调用协议。

三类记忆在每次请求中同时运行。代理既能记住您说过的话、查到相关知识,也知道如何执行任务。

Mermaid diagram 3

图 6 端到端请求序列:将聊天历史、混合 RAG 上下文和可选工具调用组合,生成并持久化最终响应。

创建 RAG 知识端点

/knowledge 端点很简单:POST 一段文本,即通过 JDBC 插入到 POLICY_DOCS 表。混合向量索引会自动使用库内ONNX 模型 进行嵌入,无需外部嵌入 API。下次有人问相关问题时,混合搜索就会命中它。

为 Oracle AI 数据库预置数据

一个 DataSeeder(Spring CommandLineRunner)会在启动时向数据库填充 8 条演示订单与 12 篇政策文档(退货、运输、支持、质保、支付、取消、换货、国际运输、隐私、促销、产品保证及批量订单政策)。策略从 policies.json 资源文件加载,通过 JDBC 插入 POLICY_DOCS 表。订单使用相对日期,确保 30 天退货窗逻辑在演示中始终有效。Seeder 会检查现有计数,避免重启时重复插入。

升级语义记忆:混合搜索

这个 POC 的首个版本使用了 Spring AI 的 QuestionAnswerAdvisor 搭配 OracleVectorStore:纯向量相似度搜索,加余弦阈值。对干净、表述良好的政策问题效果不错。但在精确词项和拼写错误上就崩了。比如“order ORD-1001”的查询会被拿去跟政策文档做语义匹配,这就不合逻辑。拼成“retrun polcy”的问题也会因为嵌入模型不识别拼写错误而失分。

创建 Oracle 混合向量索引

Oracle 26ai 提供 DBMS_HYBRID_VECTOR.SEARCH,一个同时并行运行向量相似度与 Oracle Text 关键词搜索并融合结果的单一 PL/SQL 调用。

关键在于互惠排序融合(RRF):不再尝试把有界的余弦相似度(0-1)与无界的 BM25 关键词得分做归一化,而是按各自结果列表中的名次来排名。

某文档若在向量结果里排第 1、在关键词结果里排第 3,则合并名次会同时体现两种信号。

设置步骤是一份一次性的 SQL 脚本:将 ONNX 嵌入模型加载到 Oracle AI Database 中,并创建一个 混合索引:

/SQL
-- Load the ONNX model for in-database embeddings
BEGIN
  DBMS_VECTOR.LOAD_ONNX_MODEL(
    directory  => 'DM_DUMP',
    file_name  => 'all_MiniLM_L12_v2.onnx',
    model_name => 'ALL_MINILM_L12_V2'
  );
END;
/

-- Create a hybrid index: vector similarity + Oracle Text keyword search
CREATE HYBRID VECTOR INDEX POLICY_HYBRID_IDX
ON POLICY_DOCS(content)
PARAMETERS('MODEL ALL_MINILM_L12_V2 VECTOR_IDXTYPE HNSW');

索引创建后,在插入行时会自动计算嵌入——无需外部嵌入 API 调用。

Spring AI 检索集成

Spring AI 的 QuestionAnswerAdvisor 只封装了 VectorStore.similaritySearch(),也就是纯向量搜索,别无他物。要使用混合搜索,我改用了 RetrievalAugmentationAdvisor,这是模块化方案:它接受自定义 DocumentRetriever 和自定义 QueryAugmenter 来控制如何把检索到的文档注入提示词。

自定义的 OracleHybridDocumentRetriever 实现了 DocumentRetriever,通过 JDBC 调用 DBMS_HYBRID_VECTOR.SEARCH,并传入一个 JSON 参数,指定混合索引、打分器(RRF)及关键词匹配:

public List<Document> retrieve(Query query) {
    // Builds a JSON spec with hybrid index name, RRF scorer, vector + text search clauses
    // Calls DBMS_HYBRID_VECTOR.SEARCH via JDBC, parses JSON results into List<Document>
}

这在检索环节完全绕过了 OracleVectorStore。text.contains 子句使用普通关键词 OR 匹配(超过 2 个字符的词),由 Oracle Text 内置词干提取处理词形变化。

为何混合搜索能改进 AI 代理

代理需要精准的检索来做出良好决策。如果语义记忆返回了错误或低置信度的政策文档,LLM 可能会臆造工具参数,或错过本该调用的工具。

混合搜索意味着更高置信度的上下文,也就带来更好的自主决策;当语义与精确词项同时匹配时,代理更不易误引政策或漏掉相关文档。

Spring AI 的数据库配置

配置集中在一个 application.yaml 中。关键设计:Hibernate 自动创建 JPA 表(ddl-auto: update),Spring AI 自动创建聊天记忆表(initialize-schema: always),POLICY_DOCS 表及混合向量索引由一次性 SQL 脚本(setup-hybrid-search.sql)创建,Oracle UCP 在三类记忆中共享同一个连接池。

无需 Flyway,无需自定义 @Configuration 类。Spring AI 的自动配置会检测到 Oracle JDBC 驱动并完成装配。详见仓库中的 application.yaml。

构建 Streamlit Web UI

Streamlit 前端向后端发送消息并渲染响应。核心代码如下(完整应用包含快速开始按钮和新建会话功能。见 app.py):

def send_message(prompt, url):
    # POSTs plain text to /api/v1/agent/chat with X-Conversation-Id header
    # Renders user message and assistant response in Streamlit chat UI

if prompt := st.chat_input("Type a message..."):
    send_message(prompt, backend_url)

它为每个会话生成一个 UUID 作为会话 ID,向后端发送纯文本并渲染响应。就是这么简单。

在本地运行 AI 代理 POC

简要步骤:启动 Oracle DB 容器,加载 ONNX 模型并创建混合索引(一次性设置),安装 Ollama 并拉取聊天模型(qwen2.5),使用 local 配置文件运行 Spring Boot 后端,并可选启动 Streamlit UI。嵌入由 ONNX 模型在数据库内处理,无需 Ollama 的嵌入模型。完整安装说明请见 repo README。

用 curl 快速测试:

curl -X POST http://localhost:8080/api/v1/agent/chat \
  -H "Content-Type: text/plain" \
  -H "X-Conversation-Id: test-1" \
  -d "What orders do I have?"

结语

此方案是用 Spring AI 和 Oracle 数据库构建真实代理的强力默认选项,因为它把所有记忆层整合到一套可运维栈中:情景上下文、语义检索与程序性动作。Spring AI 提供干净的抽象(Advisors + @Tool),使代理行为保持可读、可测试;而 Oracle 在同一系统中处理事务性数据与混合检索,借助库内嵌入与 RRF 排名,既提高语义又兼顾精确词项。

结果很务实:更少的活动部件、更少的集成失败、更低的延迟、更易的生产运维。无需把聊天历史、向量搜索、关键词搜索和业务表分散在不同系统中拼接,您可以基于一个平台,同时获得高质量检索与可靠的工具执行。对于要交付代理功能的团队而言,这种简单性、准确性与可控性的组合,很难被超越。

想进一步了解 AI 代理及其工作方式,推荐查看 DataCamp 的 AI Agent Fundamentals 技能路径。

FAQs

什么是代理记忆?为何重要?

代理记忆使基于 LLM 的系统能够在请求之间持久化上下文,从而记住过往对话、检索领域知识,并一致地执行工作流。

语义记忆需要单独的向量数据库吗?

不需要。在本教程中,Oracle AI Database 在同一系统中存储关系数据、聊天历史与混合检索索引。

情景、语义与程序性记忆有何区别?

情景记忆存储会话历史,语义记忆存储可检索的知识,程序性记忆通过工具定义可执行的动作。

为何使用混合检索而非纯向量搜索?

混合检索结合了语义含义与精确关键词匹配,能同时改进模糊问题与精确 ID/术语的检索效果。

该架构是否可直接用于生产环境?

这是一个很好的基础,但生产环境仍需加入认证、授权、可观测性和压测。

Victor Martin

作者

Victor Martin

我是一名后端架构师与技术负责人,拥有十余年构建可扩展、云原生系统的经验。专注于基于 Java、Kotlin 和现代云平台的分布式架构,侧重领域驱动设计、事件驱动系统以及务实的软件设计。
 
我的工作重点在于设计具备弹性的后端平台并改进工程实践,同时也在不断探索如何利用 AI 提升开发效率与系统设计质量。

主题