上篇聊了架构设计,这篇直接上代码——用Spring AI怎么搭记忆模块,核心增删改查怎么写,记忆从哪来。
一、模块结构
memory-api(接口层)
↑ 只依赖接口,不关心实现
│
memory-pgvector ← memory-redis ← memory-mcp
(生产推荐) (缓存方案) (给外部Agent用)
| 模块 | 说明 |
|---|---|
| memory-api | 只定义接口,不写实现。业务代码依赖它,打成jar包后各实现模块独立加载 |
| memory-pgvector | 用PostgreSQL存储,Service写业务逻辑,Mapper写SQL |
| memory-redis | 适合开发测试快速验证,或者做缓存层 |
| memory-mcp | 把记忆能力暴露给外部Agent,通过@Tool注解注册 |
二、表结构设计
三张核心表:
-- 结构化记忆:rules / facts / status
CREATE TABLE memory.structured_memory (
id BIGSERIAL PRIMARY KEY,
owner_id VARCHAR(255) NOT NULL,
memory_type VARCHAR(50) NOT NULL, -- rule / fact / status
content TEXT NOT NULL,
status_key VARCHAR(255), -- status类型专用
metadata TEXT, -- JSON格式扩展字段
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_structured_owner_type ON memory.structured_memory (owner_id, memory_type);
CREATE INDEX idx_structured_owner_status ON memory.structured_memory (owner_id, memory_type, status_key);
-- 向量记忆:语义检索
CREATE TABLE memory.vector_memory (
id BIGSERIAL PRIMARY KEY,
namespace VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
embedding TEXT, -- 向量存为TEXT,pgvector扩展支持向量运算
metadata TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_vector_namespace ON memory.vector_memory (namespace);
-- 待审核队列:HITL
CREATE TABLE memory.pending_memory (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id VARCHAR(255) NOT NULL,
kind VARCHAR(50),
payload TEXT, -- JSON格式
confidence DOUBLE PRECISION,
source_snippet TEXT, -- 来源对话片段
status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_pending_owner_status ON memory.pending_memory (owner_id, status);
关键设计:一张表存三种类型(rule/fact/status),通过 memory_type 字段区分,比拆成三张表更灵活。
三、StructuredMemoryService:结构化记忆增删改查
3.1 添加记忆
// 添加规则
structuredMemoryService.addRule(userId, "用户不喜欢被催单");
// 添加事实
structuredMemoryService.addFact(userId, "用户上周买了奶粉");
// 更新状态(key-value形式)
structuredMemoryService.updateStatus(userId, "step", "投诉处理中");
3.2 查询记忆
// 获取完整记忆快照
MemorySnapshot snapshot = structuredMemoryService.get(userId);
List<String> rules = snapshot.ruleContents(); // ["用户不喜欢被催单"]
List<String> facts = snapshot.factContents(); // ["用户上周买了奶粉"]
Map<String, String> status = snapshot.status(); // {step: "投诉处理中"}
3.3 删除记忆
// 按ID删除规则或事实
structuredMemoryService.deleteRule(userId, ruleId);
structuredMemoryService.deleteFact(userId, factId);
// 按key删除状态
structuredMemoryService.deleteStatus(userId, "step");
3.4 批量替换
// 全量替换(先删再插,同一事务)
List<String> newRules = List.of("新规则1", "新规则2");
Map<String, String> newStatus = Map.of("step", "新状态");
structuredMemoryService.replace(userId, newRules, null, newStatus);
四、记忆从哪来:用LLM从对话中提取
这是记忆模块的核心问题——不是手动调用API写入,而是让AI从对话中自动提取。
4.1 提取Prompt设计
String systemPrompt = """
你是一个记忆提炼器,从对话中提取值得长期记忆的信息。
【提取类型】
- facts:用户明确表述的事实,0-2条
- rules:用户明确要求的行为规则,0-1条
- status_updates:当前状态更新
【输出格式】
{
"facts": ["用户上周买了奶粉"],
"rules": ["用户不喜欢被催单"],
"status_updates": [{"key": "step", "value": "投诉处理中"}],
"confidence": 0.85
}
【注意事项】
- 只提取对话中明确提及的信息,不要编造
- confidence:0.0-1.0,越高越确定
- 无新信息则各字段为空
""";
// 调用
ChatResponse response = chatModel.call(new PromptTemplate(systemPrompt)
.render(Map.of("conversation", conversationText)));
// 解析结果
Map<String, Object> result = objectMapper.readValue(
response.getResult().getText(), Map.class);
Double confidence = (Double) result.get("confidence");
4.2 置信度分流
double confidence = (Double) result.get("confidence");
if (confidence >= 0.82) {
// 高置信度:自动写入
for (String fact : (List<String>) result.get("facts")) {
structuredMemoryService.addFact(userId, fact);
}
} else if (confidence >= 0.55) {
// 中置信度:待人工确认
structuredMemoryService.enqueuePending(userId, "fact",
Map.of("value", fact), confidence, snippet);
}
// < 0.55:直接跳过
4.3 置信度阈值说明
| 置信度 | 阈值 | 处理方式 |
|---|---|---|
| 高 | ≥ 0.82 | 自动写入,无需人工干预 |
| 中 | 0.55 ~ 0.82 | 入待审核队列,人工确认后写入 |
| 低 | < 0.55 | 跳过,避免噪音 |
五、HITL:人机协作的质量兜底
语义检索依赖向量相似度,但模型判断"这句话该不该被记住"的能力有限。HITL就是在这层补一个人工确认环节。
5.1 适用场景
- 高风险信息:涉及金额、承诺、投诉等
- 低置信度内容:模型拿不准的信息
- 关键规则:用户明确要求的行为偏好
5.2 代码实现
// 入队
public void enqueuePending(String ownerId, String kind,
Map<String, Object> payload,
double confidence, String sourceSnippet) {
PendingMemoryEntity pending = PendingMemoryEntity.builder()
.ownerId(ownerId)
.kind(kind)
.payload(JsonUtils.toJson(payload))
.confidence(confidence)
.sourceSnippet(sourceSnippet)
.status("pending")
.build();
pendingMemoryMapper.insert(pending);
}
// 审核通过后写入
public void approvePending(UUID pendingId) {
PendingMemoryEntity pending = pendingMemoryMapper.selectById(pendingId);
Map<String, Object> payload = JsonUtils.fromJson(pending.getPayload());
switch (pending.getKind()) {
case "fact" -> structuredMemoryService.addFact(
pending.getOwnerId(), (String) payload.get("value"));
case "rule" -> structuredMemoryService.addRule(
pending.getOwnerId(), (String) payload.get("value"));
}
pendingMemoryMapper.updateStatus(pendingId, "approved");
}
5.3 与向量检索的配合
| 内容类型 | 向量状态 | 说明 |
|---|---|---|
| 高置信度 | 已激活 | 直接写入结构化记忆,同步生成向量 |
| 待审核 | pending | 向量生成但状态为pending,审核通过后激活 |
| 审核拒绝 | 已拒绝 | 向量保留但不暴露给检索结果 |
六、VectorMemoryService:语义搜索
6.1 存储与搜索
// 存储记忆,自动生成向量并入库
vectorMemoryService.addMemory(userId, "用户上周买了奶粉");
// 语义搜索:用户说"买了啥"能找到"用户上周买了奶粉"
List<VectorMemoryRecord> results = vectorMemoryService.search("买了啥", userId, 5);
6.2 混合检索
// 语义 + 关键词 + 时间排序,生产环境标配
hybridSearch(query, userId, startTime, endTime);
混合检索权重建议:向量0.6 + 关键词0.3 + 时间0.1。
七、Redis 实现
Redis 版本通过 @ConditionalOnMissingBean 在 PG 不可用时自动降级:
// 存储
redisTemplate.opsForHash().put(key, "rules", serializeRules(rules));
redisTemplate.expire(key, Duration.ofDays(7)); // 7天过期
// 查询
Object rules = redisTemplate.opsForHash().get(key, "rules");
八、MCP 工具暴露
memory-mcp 模块把记忆能力暴露给外部Agent:
| 工具名 | 功能 |
|---|---|
memoryGetSnapshot | 获取完整记忆 |
memoryAddRule | 添加规则 |
memoryAddFact | 添加事实 |
memoryUpdateStatus | 更新状态 |
memoryVectorSearch | 语义检索 |
memoryVectorStore | 存储向量 |
九、总结
本文介绍了 Spring AI 记忆模块的核心实现:
- 模块分层:接口与实现分离,支持多存储后端
- 数据结构:结构化记忆(rule/fact/status)+ 向量记忆 + HITL队列
- 记忆提取:LLM自动提取 + 置信度分流 + 人工兜底
- 语义检索:向量相似度 + 混合检索