Spring AI 实战:把内存换成 PgVector + Redis,让 RAG 服务重启不丢数据
英文标题:Spring AI 实战:把内存换成 PgVector + Redis,让 RAG 服务重启不丢数据
在基于 Spring AI 构建物流智能客服系统时,默认的内存存储(SimpleVectorStore 与 InMemoryChatMemory)会导致服务一旦重启,知识库向量和会话上下文全部丢失。本文深入探讨了生产级持久化改造方案,通过引入 PostgreSQL 的 pgvector 扩展实现向量知识库持久化,并结合 Redis 存储多轮对话记忆。该改造免去了服务重启后重复向量化带来的 Token 消耗与计算开销,同时保障了用户上下文的连贯性,为 Spring 生态开发者落地高可用大模型应用提供了清晰的工程实践参考。
中文
前言:从“能跑”到“能一直跑”
前三篇结束时,我的物流智能客服系统已经能对话、能查知识库、能调工具。但在本地跑通和真正上生产之间,往往隔着一个致命的痛点:只要服务一重启,所有数据全没了。
在开发初期,为了快速验证链路,通常会采用 Spring AI 默认提供的内存实现:
- 向量检索使用
SimpleVectorStore,数据存放在 JVM 堆内存中; - 会话记忆使用
InMemoryChatMemory,上下文同样驻留于内存。
这意味着只要应用发版、重启或发生节点漂移,知识库文档就必须全部重新做一次 Embedding 并写入,既消耗大量 API Token,又拖慢冷启动时间;同时,用户正在进行的物流咨询上下文也会立即归零,造成客服“失忆”。
要让 RAG 服务从“玩具阶段”走向企业级生产,必须实现状态持久化。本文将带大家将内存存储替换为 PostgreSQL (pgvector) 向量数据库与 Redis 对话记忆库。
架构选型与分工
在持久化改造中,我们明确拆分两类数据的生命周期与读写特点:
| 组件 | 对应场景 | 核心诉求 | 选型 |
|---|---|---|---|
| 向量存储 (VectorStore) | 物流知识库(退换货政策、运费规则等) | 高维向量近似搜索 (ANN)、持久化、低频写入高频检索 | PostgreSQL + pgvector |
| 会话记忆 (ChatMemory) | 智能客服与用户的多轮对话上下文 | 高频低延迟读写、TTL 过期自动清理、分布式共享 | Redis |
第一步:环境搭建与依赖引入
1. 启动 PgVector 与 Redis 容器
通过 docker-compose.yml 快速拉起包含向量扩展的 PostgreSQL 实例以及 Redis 服务:
yaml version: '3.8' services: pgvector: image: pgvector/pgvector:pg16 container_name: logistics-pgvector environment: POSTGRES_DB: logistics_ai POSTGRES_USER: postgres POSTGRES_PASSWORD: password123 ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data
redis: image: redis:7-alpine container_name: logistics-redis ports: - "6379:6379" command: redis-server --appendonly yes volumes: - redisdata:/data
volumes: pgdata: redisdata:
2. 引入 Maven 依赖
在 pom.xml 中移除旧的内存向量库依赖,引入 Spring AI PgVector Starter 与 Spring Data Redis:
xml org.springframework.ai spring-ai-pgvector-store-spring-boot-starter
<!-- Spring Boot Redis Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
第二步:向量库改造(PgVector)
1. 基础配置
在 application.yml 中配置 PostgreSQL 连接及 PgVectorStore 的建表参数:
yaml spring: datasource: url: jdbc:postgresql://localhost:5432/logistics_ai username: postgres password: password123 ai: vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 1536 # 与使用的 Embedding 模型维度一致(如 text-embedding-3-small) table-name: vector_store_logistics initialize-schema: true # 启动时自动建表与向量索引
2. 向量知识库入库逻辑
由于底层改为了持久化表,我们只需在首次启动或知识库更新时进行导入,无需每次随应用启动重复构建:
java @Component public class KnowledgeBaseLoader implements CommandLineRunner {
@Autowired
private VectorStore vectorStore;
@Override
public void run(String... args) {
// 检查是否已有数据,避免重复导入
List<Document> existing = vectorStore.similaritySearch("退换货政策", 1);
if (existing.isEmpty()) {
List<Document> docs = List.of(
new Document("极速达物流退货规则:用户签收后7日内可申请无理由退货,生鲜快件除外。", Map.of("category", "refund")),
new Document("运费计算标准:首重1kg按12元计费,续重每公斤加收5元。", Map.of("category", "shipping"))
);
vectorStore.add(docs);
}
}
}
第三步:会话记忆持久化(Redis)
为了支持分布式环境下的会话保持,我们将 ChatMemory 的底层存取委托给 Redis。
1. 自定义 RedisChatMemory
基于 Spring Data Redis 实现对话历史的序列化与持久存储:
java @Configuration public class ChatMemoryConfig {
@Bean
public ChatMemory chatMemory(StringRedisTemplate redisTemplate) {
return new ChatMemory() {
private final String PREFIX = "chat:memory:";
@Override
public void add(String conversationId, List<Message> messages) {
// 将新消息序列化后存入 Redis List,并刷新 TTL
String key = PREFIX + conversationId;
for (Message msg : messages) {
redisTemplate.opsForList().rightPush(key, serialize(msg));
}
redisTemplate.expire(key, Duration.ofDays(7));
}
@Override
public List<Message> get(String conversationId, int lastN) {
String key = PREFIX + conversationId;
List<String> raw = redisTemplate.opsForList().range(key, -lastN, -1);
return raw == null ? List.of() : raw.stream().map(this::deserialize).toList();
}
@Override
public void clear(String conversationId) {
redisTemplate.delete(PREFIX + conversationId);
}
// 序列化与反序列化逻辑省略
private String serialize(Message msg) { ... }
private Message deserialize(String json) { ... }
};
}
}
2. 挂载到 ChatClient Advisor
在构建 ChatClient 时,注册 MessageChatMemoryAdvisor,使每轮交互自动绑定该会话的持久化上下文:
java @RestController @RequestMapping("/api/chat") public class LogisticsChatController {
private final ChatClient chatClient;
public LogisticsChatController(ChatClient.Builder builder, VectorStore vectorStore, ChatMemory chatMemory) {
this.chatClient = builder
.defaultAdvisors(
new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults().withTopK(3)),
new MessageChatMemoryAdvisor(chatMemory)
)
.build();
}
@GetMapping
public String chat(@RequestParam String conversationId, @RequestParam String message) {
return chatClient.prompt()
.user(message)
.advisors(advisorSpec -> advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId))
.call()
.content();
}
}
第四步:重启验证
完成代码改造后,我们执行以下流程测试持久化可靠性:
- 多轮问答建立状态:向接口发送会话
conv-1001,告知客服“我买了一箱苹果,昨天刚签收,想退货”。AI 结合知识库回答生鲜不适用7天无理由退货。 - 硬重启应用:直接终止 Java 进程并重新启动服务。
- 追问验证:使用相同
conversationId发送“那我买的衣服可以退吗?” - 结果比对:服务没有重新加载向量库,且能立刻结合前置上下文给出正确回复,证明 PgVector 检索与 Redis 会话上下文在重启后全部生效。
总结
将内存方案替换为 PgVector + Redis,是 Spring AI 应用从技术验证走向生产部署的关键一步:
- PgVector 依托成熟的关系型数据库体系,既满足了高维向量检索需求,又降低了独立维护专用向量数据库的运维门槛;
- Redis 负责多轮会话状态共享,不仅能优雅应对应用重启与弹性扩缩容,还可以通过 TTL 机制控制内存占用。
至此,物流智能客服系统具备了真正“一直跑”的基础稳定性。
English / 原文
Please wait...