1. 项目缘起:当AI Agent需要记住“一切”
最近在折腾一个AI Agent项目,遇到了一个非常典型的问题:Agent的“记忆力”太差了。这里的记忆力,不是指大模型本身的上下文长度,而是指Agent在长期运行、与用户多次交互、执行复杂任务过程中,对自身状态、历史决策、用户偏好、任务中间结果等信息的持久化存储与精准召回能力。
想象一个场景,你让Agent帮你分析上个月的销售数据,它吭哧吭哧跑了一遍,给出了结论。第二天,你问它:“对比一下这个月和上个月的趋势?”如果它完全忘了昨天做了什么、用了哪些数据、得出了什么结论,那就只能从头再来。这不仅浪费算力(Token),更破坏了交互的连贯性和智能体应有的“人格感”。一个健壮的、可投入生产的AI Agent,必须拥有一个可靠、高效且结构化的记忆系统。
这就是“三位一体记忆体”概念的由来。它不是一个单一的技术点,而是一个系统性的工程架构,旨在解决Agent记忆的持久化、结构化和高效检索三大核心问题。而我的选择,是回归一个久经考验的“老兵”——Oracle Database。
为什么是Oracle DB?在如今云原生、NoSQL大行其道的背景下,这个选择似乎有些“复古”。但经过一番权衡,我发现对于企业级AI Agent应用,Oracle DB提供了难以替代的优势:无与伦比的SQL能力处理复杂关系型记忆、绝佳的稳定性和事务一致性保障记忆不丢失、成熟的生态工具链(如SQLcl)便于集成与运维。更重要的是,我想探索一条将经典企业级数据基础设施与前沿AI Agent架构深度结合的道路。
本文将详细拆解我如何利用Oracle DB,为AI Agent构建起“工作记忆”、“情景记忆”、“语义记忆”三位一体的记忆体,并集成MCP(Model Context Protocol)协议,让Agent的记忆能力变得可插拔、可扩展。这不是一个简单的技术堆砌,而是一次从想法到落地工程的完整实践。
2. 解构“三位一体”:AI Agent需要什么样的记忆?
在深入技术细节之前,我们必须先厘清AI Agent记忆系统的设计目标。直接照搬数据库表结构是行不通的,我们需要对记忆进行建模。
2.1 记忆的分类与数据模型设计
借鉴认知心理学和现有Agent框架(如LangChain的AgentExecutor、AutoGen的GroupChat),我将Agent记忆分为三个层次,对应数据库中的不同实体和关系:
1. 工作记忆 (Working Memory)
- 是什么:Agent单次会话或单个任务执行周期内的临时状态。相当于人类的“短时记忆”。
- 数据特点:高频更新、生命周期短、结构相对简单。
- Oracle DB实现:
- 表设计:
AGENT_SESSION表。核心字段包括SESSION_ID(主键)、AGENT_ID、USER_ID、CREATED_AT、LAST_ACTIVITY_AT、CONTEXT_HASH(当前会话上下文摘要)。 - 关联表:
SESSION_MESSAGES表。记录该会话中的所有消息(用户输入、Agent思考、工具调用结果)。这是记忆检索的主要素材。字段包括MESSAGE_ID、SESSION_ID、ROLE(user/assistant/tool)、CONTENT、TOOL_CALL_ID、TIMESTAMP。这里利用Oracle的CLOB类型存储可能很长的消息内容。 - 策略:工作记忆通常与会话绑定。可以设置定时任务,清理超过一定时间(如30分钟)无活动的会话及其消息,避免数据无限膨胀。
- 表设计:
2. 情景记忆 (Episodic Memory)
- 是什么:对过去发生的具体事件、任务执行结果的记录。相当于“自传体记忆”,回答了“之前发生了什么”的问题。
- 数据特点:按事件组织、包含丰富元数据(时间、参与者、结果状态)、需要支持基于内容的模糊检索。
- Oracle DB实现:
- 表设计:
AGENT_EPISODE表。核心字段包括EPISODE_ID(主键)、AGENT_ID、TASK_TYPE(如“data_analysis”, “report_generation”)、TASK_DESCRIPTION、STATUS(success/failed/partial)、START_TIME、END_TIME、SUMMARY(由LLM生成的本次任务摘要)。 - 关键挑战与方案:如何从冗长的
SESSION_MESSAGES中提炼出可供快速检索的情景?这里我引入了向量检索的混合方案。- 结构化摘要:任务结束时,触发一个LLM调用,将本次任务的关键决策、使用的主要工具、最终结论生成一个文本摘要,存入
SUMMARY字段。 - 向量化存储:使用Oracle的
VECTOR数据类型(19c以后版本支持)或通过PL/SQL调用外部AI服务,将SUMMARY和TASK_DESCRIPTION转换为向量,存储在EPISODE_VECTOR列中。 - 检索:当需要寻找类似历史任务时,先将当前问题转换为向量,然后利用Oracle的
VECTOR_DISTANCE函数进行相似度搜索,快速定位相关历史情景。这比单纯的SQLLIKE查询要强大和精准得多。
- 结构化摘要:任务结束时,触发一个LLM调用,将本次任务的关键决策、使用的主要工具、最终结论生成一个文本摘要,存入
- 表设计:
3. 语义记忆 (Semantic Memory)
- 是什么:Agent学到的通用知识、事实、用户偏好、领域规则等。相当于“常识”或“长期知识”。
- 数据特点:相对稳定、结构化程度高、需要版本管理和有效性验证。
- Oracle DB实现:
- 表设计:这是一个更灵活的结构。我创建了
AGENT_KNOWLEDGE表,包含KNOWLEDGE_ID、AGENT_ID、CATEGORY(如“user_preference”, “domain_rule”, “api_schema”)、KEY、VALUE(CLOB)、VECTOR(可选)、EFFECTIVE_FROM、EFFECTIVE_TO。 - 应用:例如,可以存储“用户A更喜欢图表用柱状图而非饼图”、“某API的响应结构说明”、“项目Y的代码规范”等。通过
CATEGORY和KEY可以快速查询。对于非结构化的知识条目,同样可以采用向量化存储与检索。 - 版本控制:利用
EFFECTIVE_FROM和EFFECTIVE_TO实现简单的知识版本管理,确保Agent在不同时间点使用的知识是准确的。
- 表设计:这是一个更灵活的结构。我创建了
注意:直接存储原始对话消息(尤其是包含大量Token的LLM响应)会导致数据库迅速膨胀。一个重要的实践是摘要与向量化双轨制。对于需要精确回溯的步骤(如工具调用的参数),存储原始消息;对于需要语义搜索的记忆,存储由LLM生成的精炼摘要及其向量。这需要在存储成本和检索精度间取得平衡。
2.2 记忆的关联与图谱构建
单一的记忆条目价值有限,记忆之间的关联更能体现智能。例如,一次成功的数据分析任务(情景记忆)可能用到了某个特定的用户偏好(语义记忆),并产生了一系列中间步骤(工作记忆)。
我在数据库中通过外键关联和图谱化视图来建立这种连接。
SESSION_MESSAGES.SESSION_ID->AGENT_SESSION.SESSION_IDAGENT_EPISODE可以关联到触发它的原始SESSION_ID。- 在
AGENT_KNOWLEDGE中,可以通过REF_EPISODE_ID字段关联到产生这条知识的具体任务。
更进一步,可以创建一个物化视图或应用程序层的缓存,构建一个轻量的“记忆图谱”,快速回答诸如“用户A在处理销售数据时通常喜欢用什么方法?”这类复杂查询。Oracle的高级SQL分析函数(如LISTAGG, 递归查询CONNECT BY或RECURSIVE WITH)在这里能派上大用场。
3. 工程落地:Oracle SQLcl与MCP Server的桥梁
设计好了数据模型,下一步就是让AI Agent能够方便地读写这个记忆库。让Agent直接通过JDBC/ODBC连接Oracle显然不现实,也破坏了架构的清晰度。我的方案是:构建一个MCP(Model Context Protocol) Server,作为Agent与Oracle记忆体之间的专用桥梁。
3.1 为什么选择MCP?
MCP是一种新兴的协议,它允许AI模型(或Agent)通过标准化的方式发现、调用外部工具和资源。对于记忆体来说,MCP化带来了巨大好处:
- 解耦:记忆体的实现细节(Oracle、PostgreSQL、甚至向量数据库)对Agent透明。Agent只通过MCP协议定义的标准接口(
tools)与记忆交互。 - 可发现性:支持MCP的AI客户端(如Claude Code、Cursor)可以自动发现记忆服务器提供的工具,无需硬编码。
- 标准化:提供了统一的错误处理、资源描述和调用方式。
3.2 使用Oracle SQLcl构建MCP Server核心
Oracle SQLcl是一个基于Java的轻量级命令行工具,但它更是一个功能强大的脚本执行环境,支持JavaScript、Python等语言。我选择用JavaScript (Nashorn引擎)在SQLcl中快速原型化MCP Server的核心逻辑。
步骤一:环境准备与基础连接首先,确保已安装Oracle SQLcl。然后,编写一个基础的JavaScript文件(memory_mcp.js),建立数据库连接池。SQLcl内置了JDBC,连接非常方便。
// memory_mcp.js - 核心框架 var DBUtil = Java.type('oracle.dbtools.raptor.newscriptrunner.db.DBUtil'); var conn = DBUtil.getConnection(); // 获取当前SQLcl会话的连接 // 简单的连接池模拟(生产环境建议使用更成熟的池化技术) function getConnection() { // 这里简化处理,实际应考虑连接的重用和管理 return conn; }步骤二:实现MCP工具函数根据之前的设计,我们需要实现几个核心的MCP工具(Tool)。每个工具对应一个JavaScript函数,并通过SQLcl的script命令暴露为可调用接口。
例如,实现一个query_episodic_memory工具:
// 查询情景记忆 function queryEpisodicMemory(agentId, queryText, limit) { var conn = getConnection(); var stmt = null; var rs = null; try { // 1. 先将查询文本向量化(这里需要调用外部向量化服务,如OpenAI API或本地模型) // 为简化示例,假设有一个函数getVectorEmbedding(text) var queryVector = getVectorEmbedding(queryText); // 返回一个数组或序列化字符串 // 2. 使用Oracle VECTOR相似度搜索 (假设向量已存储在EPISODE_VECTOR列) var sql = ` SELECT e.EPISODE_ID, e.TASK_DESCRIPTION, e.SUMMARY, e.STATUS, VECTOR_DISTANCE(e.EPISODE_VECTOR, :1, COSINE) as similarity FROM AGENT_EPISODE e WHERE e.AGENT_ID = :2 ORDER BY similarity ASC FETCH FIRST :3 ROWS ONLY `; stmt = conn.prepareStatement(sql); // 绑定参数(需要根据实际向量存储格式调整) stmt.setObject(1, queryVector); stmt.setString(2, agentId); stmt.setInt(3, limit || 5); rs = stmt.executeQuery(); var results = []; while (rs.next()) { results.push({ episode_id: rs.getString("EPISODE_ID"), description: rs.getString("TASK_DESCRIPTION"), summary: rs.getString("SUMMARY"), status: rs.getString("STATUS"), similarity: rs.getFloat("similarity") }); } return JSON.stringify(results); } catch (e) { return JSON.stringify({error: e.message}); } finally { if (rs) rs.close(); if (stmt) stmt.close(); } } // 将函数注册为SQLcl脚本命令 script.registerFunction("queryEpisodicMemory", queryEpisodicMemory);步骤三:封装为MCP Server单纯的SQLcl脚本还不够,我们需要一个遵循MCP协议的HTTP/SSE服务器。这里,我使用Node.js(或Python的FastAPI)来构建一个轻量的HTTP服务器,这个服务器的后端逻辑通过子进程调用SQLcl并执行上述JavaScript脚本来实现。
// Node.js MCP Server 示例 (app.js) const express = require('express'); const { exec } = require('child_process'); const app = express(); app.use(express.json()); // MCP 标准的 /tools 端点,公布可用的工具 app.get('/tools', (req, res) => { res.json({ tools: [ { name: "query_episodic_memory", description: "根据任务描述语义搜索历史任务情景", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, query: { type: "string" }, limit: { type: "integer", default: 5 } }, required: ["agent_id", "query"] } }, // ... 其他工具定义,如 save_working_memory, retrieve_knowledge 等 ] }); }); // MCP 标准的 /tools/call 端点,调用具体工具 app.post('/tools/call', (req, res) => { const { name, arguments } = req.body; if (name === 'query_episodic_memory') { const { agent_id, query, limit } = arguments; // 关键:调用SQLcl执行JS函数 const sqlclCmd = `sql -script memory_mcp.js -cmd "queryEpisodicMemory('${agent_id}', '${query}', ${limit})"`; exec(sqlclCmd, (error, stdout, stderr) => { if (error) { res.json({ error: { message: stderr } }); return; } // 解析SQLcl的输出(通常是JSON字符串) try { const result = JSON.parse(stdout.trim()); res.json({ content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }); } catch (e) { res.json({ content: [{ type: "text", text: stdout }] }); } }); } else { res.status(404).json({ error: { message: `Tool ${name} not found` } }); } }); app.listen(3000, () => console.log('MCP Memory Server running on port 3000'));这样,一个基于Oracle SQLcl和Node.js的MCP记忆服务器就搭建起来了。AI Agent(如通过Codex配置了该MCP Server的Claude Code)就可以直接调用query_episodic_memory等工具来访问记忆。
3.3 踩坑实录:SQLcl集成与性能优化
这个过程并非一帆风顺,有几个关键的坑需要避开:
坑1:SQLcl进程生命周期与性能每次工具调用都启动一个新的SQLcl进程开销巨大,无法满足低延迟要求。解决方案是采用连接池+常驻进程模式。
- 修改Node.js服务器,使用
spawn启动一个常驻的SQLcl子进程,并通过标准输入输出(stdin/stdout)与其进行交互。这需要编写一个更复杂的通信协议(例如,每行一个JSON命令)。 - 在SQLcl的JavaScript脚本中,初始化一个真正的数据库连接池(例如使用Oracle UCP),并在整个进程生命周期内维护它。
坑2:向量化服务的集成与延迟在SQL层做向量相似度计算虽然方便,但向量生成(调用OpenAI等Embedding API)是网络IO密集型操作,放在数据库调用链中会阻塞整个请求。
- 解决方案:异步向量化与预处理。
- 当保存新的情景记忆(
AGENT_EPISODE)时,不要同步调用Embedding API。 - 将需要向量化的文本(如
SUMMARY)放入一个消息队列(如Oracle AQ、RabbitMQ)。 - 由一个独立的向量化Worker消费队列,生成向量后,再异步更新回数据库的
EPISODE_VECTOR字段。 - 在向量更新前,记忆仍可通过其他字段(如时间、任务类型)被检索,只是缺少了语义搜索能力。这实现了最终一致性。
- 当保存新的情景记忆(
坑3:MCP协议细节与错误处理MCP协议要求特定的响应格式。不规范的响应会导致AI客户端无法解析。
- 解决方案:严格遵循MCP协议文档定义
/tools和/tools/call端点的输入输出格式。错误时,返回{ error: { message: “...” } }格式。为每个工具函数实现完善的异常捕获,将数据库错误、网络错误等转化为用户友好的消息。
4. 在AI Agent框架中集成MCP记忆体
记忆服务器Ready了,下一步就是让Agent能用上它。我以目前较流行的LangChain框架为例,展示集成过程。
4.1 将MCP Server封装为LangChain Tool
LangChain支持自定义Tool。我们需要创建一个Tool,其_run方法内部去调用我们刚构建的MCP Server的HTTP接口。
import requests import json from langchain.tools import BaseTool from typing import Type, Optional from pydantic import BaseModel, Field class MCPMemoryQueryInput(BaseModel): """查询情景记忆的输入参数.""" agent_id: str = Field(description="智能体ID") query: str = Field(description="用于搜索记忆的自然语言查询") limit: Optional[int] = Field(default=5, description="返回结果的最大数量") class QueryEpisodicMemoryTool(BaseTool): name = "query_episodic_memory" description = "根据自然语言描述,语义搜索智能体过去执行过的类似任务(情景记忆)。" args_schema: Type[BaseModel] = MCPMemoryQueryInput mcp_server_url: str = "http://localhost:3000" def _run(self, agent_id: str, query: str, limit: int = 5) -> str: """执行工具调用.""" try: response = requests.post( f"{self.mcp_server_url}/tools/call", json={ "name": self.name, "arguments": { "agent_id": agent_id, "query": query, "limit": limit } }, timeout=10 ) response.raise_for_status() result = response.json() if 'error' in result: return f"查询记忆失败:{result['error']['message']}" # MCP返回的内容在content字段中 memory_text = result.get('content', [{}])[0].get('text', '{}') memories = json.loads(memory_text) if isinstance(memories, list): formatted = "\n".join([f"- [{m['status']}] {m['description']} (相似度: {1-m['similarity']:.2f})" for m in memories]) return f"找到{len(memories)}条相关历史任务:\n{formatted}" else: return str(memories) except requests.exceptions.RequestException as e: return f"网络请求失败:{e}" except json.JSONDecodeError as e: return f"解析响应失败:{e}" async def _arun(self, *args, **kwargs): """异步版本(可选)。""" raise NotImplementedError("此工具暂不支持异步调用")4.2 在Agent执行流程中注入记忆查询
有了Tool,下一步就是在Agent的思考循环中,在合适的时机调用它。这通常发生在Agent需要做决策或规划时。
from langchain.agents import AgentExecutor, create_react_agent from langchain.prompts import PromptTemplate from langchain_community.llms import OpenAI # 示例,可用其他LLM # 1. 初始化LLM llm = OpenAI(temperature=0, model_name="gpt-4") # 2. 准备工具列表,包含我们的记忆工具和其他功能工具 tools = [QueryEpisodicMemoryTool(), ...你的其他工具...] # 3. 创建自定义Prompt,引导Agent在规划时考虑历史记忆 prompt_template = """ 你是一个拥有记忆能力的AI助手。在回答用户问题或执行任务前,你可以查询自己的记忆库,参考过去的经验。 你有权使用以下工具: {tools} 使用工具时,请严格按照以下格式: Thought: 你需要思考现在要做什么 Action: 工具名 Action Input: 工具的输入参数(必须是有效的JSON字符串) 当你获得工具观察结果后,继续: Observation: 工具返回的结果 ...(这个Thought/Action/Observation循环可以重复多次) 开始!如果你认为查询记忆有助于当前任务,请首先使用`query_episodic_memory`工具。 历史对话: {chat_history} 当前用户输入:{input} {agent_scratchpad} """ prompt = PromptTemplate.from_template(prompt_template) # 4. 创建Agent并执行 agent = create_react_agent(llm, tools, prompt) agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True) # 执行一个任务 result = agent_executor.invoke({ "input": "帮我分析一下本季度和上一季度的销售数据差异,并给出可视化建议。", "chat_history": "", # 这里可以传入持久化的工作记忆 "agent_id": "sales_analyst_agent_001" })在这个流程中,当Agent收到“分析销售数据差异”的任务时,Thought过程可能会触发:“我应该先看看以前是怎么做销售数据分析的”。于是它会调用query_episodic_memory工具,传入agent_id和query:“销售数据分析”。MCP Server会从Oracle DB中返回相似的历史任务。Agent在Observation中看到这些历史记录后,就能借鉴过去的成功经验(比如用了哪些数据表、生成了哪种图表),从而更高效、更一致地完成当前任务。
4.3 记忆的写入:何时保存?保存什么?
记忆的读取很重要,写入同样关键。盲目保存所有对话会塞满数据库。我的策略是事件驱动的记忆写入:
- 任务结束时保存情景记忆:当Agent完成一个明确的任务(如“生成报告”、“分析数据”)并成功时,触发一个
save_episode的MCP工具调用。这个工具会将当前会话的关键消息、最终结论、任务类型等,通过SQLcl写入AGENT_EPISODE表,并异步触发向量化流程。 - 显式知识沉淀:当用户给出明确的指示,如“记住,我以后都想要PDF格式的报告”,Agent应调用
save_knowledge工具,将这条偏好作为语义记忆存入AGENT_KNOWLEDGE表。 - 工作记忆的自动暂存:工作记忆(会话消息)的保存可以更自动化。可以设置一个后台进程,定时或在会话空闲时,将活跃会话的消息快照同步到
SESSION_MESSAGES表。也可以只在会话中发生重要工具调用或决策点时进行快照。
5. 进阶优化与生产级考量
将原型推进到生产环境,还需要解决更多问题。
5.1 记忆的检索优化:超越向量搜索
单一的向量相似度搜索并非万能。在实践中,我采用了**混合检索(Hybrid Search)**策略:
- 关键词过滤:先利用Oracle的全文检索(
CONTEXT索引)或普通WHERE子句,基于AGENT_ID、TASK_TYPE、时间范围等硬性条件进行第一轮筛选,缩小数据集。 - 向量精排:对筛选后的结果,再进行向量相似度计算和排序。
- 元数据加权:在最终排序时,综合考虑相似度、任务成功状态(
STATUS='success'的优先)、任务新鲜度(最近发生的任务可能更相关)等因素,给出一个综合排名。
这可以通过在Oracle中编写一个更复杂的PL/SQL函数或存储过程来实现,一次性返回混合检索的结果。
5.2 记忆的压缩、蒸馏与遗忘机制
无限增长的记忆是不可持续的。我们需要“遗忘”机制。
- 工作记忆自动清理:基于
LAST_ACTIVITY_AT字段,定期归档或删除老旧会话。 - 情景记忆摘要化:对于非常久远的情景记忆,可以只保留
SUMMARY摘要和关键元数据,删除其关联的详细SESSION_MESSAGES记录,释放空间。 - 语义记忆的版本合并:对于语义记忆,当同一
KEY下的知识被多次更新后,可以尝试使用LLM对多个版本进行“蒸馏”,合并成一个更精炼、更通用的版本,删除旧版本。
5.3 监控、调试与可观测性
一个黑盒的记忆系统是危险的。必须增加可观测性。
- 审计日志:在Oracle中创建
MEMORY_ACCESS_LOG表,记录每一次记忆的读写操作(AGENT_ID,OPERATION,TARGET_ID,TIMESTAMP,RESULT_COUNT等)。这对于调试Agent的异常行为和优化检索策略至关重要。 - 性能监控:监控MCP Server的接口响应时间、SQLcl进程的资源消耗、关键查询语句的执行计划。Oracle的AWR/ASH报告可以帮助定位数据库层面的瓶颈。
- 记忆质量评估:定期抽样检查,看Agent检索到的记忆是否真的对当前任务有帮助。可以设计一些自动化测试用例,或者通过人工评审来评估。
构建AI Agent的记忆体,从想法到落地工程,是一个充满挑战但也极具成就感的过程。选择Oracle DB作为基石,看中的是其强大的数据管理能力和在企业环境中的可靠性。通过MCP协议进行封装,则为这套记忆系统赋予了现代化的、标准化的接口,使其能够灵活接入不同的AI Agent生态。
这套“三位一体”的架构——工作记忆维持会话流,情景记忆提供案例参考,语义记忆存储常识偏好——在实践中显著提升了我所开发Agent的连贯性、准确性和用户体验。它不再是一个“金鱼脑”的对话机器人,而是一个真正能够积累经验、持续学习的数字助手。
当然,这套架构仍在演进中。下一步,我计划探索如何利用Oracle的Graph特性来更自然地表示记忆间的复杂关系,以及如何将记忆的检索与生成过程更深度地融合到LLM的推理链条中。工程的世界没有银弹,但每一次扎实的架构选择和技术深耕,都能让我们的AI应用离真正的智能更近一步。