山东大学软件学院项目实训-基于语言大模型的智能居家养老健康守护系统-个人博客(五)
AI 对话记录从文件存储迁移到 PostgreSQL 的实战记录
为什么要改?
我们的智慧养老系统有 5 个 AI Agent(情感陪伴、健康陪诊、健康干预、用药审核、家属辅诊),每个 Agent 都支持多轮对话。原来的对话记录存储方案是把每个会话序列化成一个 JSON 文件,按 Agent 类型分目录存放:
./chat-sessions/
├── companion/ # 情感陪伴
│ ├── abc123.json
│ └── def456.json
└── medication-safety/ # 用药审核
└── ghi789.json
跑了一段时间后暴露出一个严重 bug:健康陪诊、健康干预、家属辅诊这三个 Agent 的会话无法删除,也无法通过 sessionId 查询到。
原因出在 ChatSessionServiceImpl 的 findSessionFile 方法:
private Path findSessionFile(String sessionId) {
String[] subDirs = {"medication-safety", "companion"}; // 只写了2个!
for (String sub : subDirs) {
Path file = Path.of(storageDir, sub, sessionId + ".json");
if (Files.exists(file)) {
return file;
}
}
return null;
}
硬编码了只搜索 2 个目录,后面新增的 3 个 Agent(health-companion、health-intervention、family-assist)的会话文件根本找不到。
与其继续修补文件方案,不如直接迁到数据库。
做了什么
1. 设计数据库表
两张表:chat_session(会话主表)+ chat_message(消息明细表)。
CREATE TABLE chat_session (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL UNIQUE,
agent_type VARCHAR(32) NOT NULL,
mode VARCHAR(32),
title VARCHAR(200),
elderly_id BIGINT,
family_id BIGINT,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
CREATE TABLE chat_message (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
role VARCHAR(16) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT now(),
CONSTRAINT fk_msg_session FOREIGN KEY (session_id)
REFERENCES chat_session(session_id) ON DELETE CASCADE
);
关键设计点:
session_id用 UUID 字符串做唯一键,而不是自增 id,因为前端已经在用这个值做关联- 消息表通过外键关联会话,设置
ON DELETE CASCADE——删会话时自动清理所有消息 - 新增
elderly_id和family_id字段,为后续"查看某个老人的所有对话记录"做准备 - 索引覆盖了主要查询路径:按 agent_type 列表、按 session_id 查消息、按时间排序
2. 改造实体类
原来的 ChatSession 和 ChatMessage 是纯 POJO,没有任何持久化注解。改造后加上 MyBatis-Plus 注解:
ChatSession.java 变更:
- 新增
Long id(数据库自增主键) - 新增
Long elderlyId、Long familyId messages字段标记@TableField(exist = false),不映射到表列timestamp字段统一改名createdAt
ChatMessage.java 变更:
- 新增
Long id(数据库自增主键) - 新增
String sessionId(关联字段) - 原来的
LocalDateTime timestamp改名为createdAt
3. 重写 ChatSessionServiceImpl
这是改动最大的文件。原来 200 行的文件 I/O 代码,替换为 100 行的数据库操作。
创建会话——直接 insert:
public ChatSession createSession(String agentType, String mode, Long elderlyId, Long familyId) {
ChatSession session = new ChatSession();
session.setSessionId(UUID.randomUUID().toString().replace("-", ""));
session.setAgentType(agentType);
session.setMode(mode);
session.setElderlyId(elderlyId);
session.setFamilyId(familyId);
chatSessionMapper.insert(session);
return session;
}
查询会话——一次查 session + 一次查 messages:
public ChatSession getSession(String sessionId) {
ChatSession session = chatSessionMapper.selectOne(
new LambdaQueryWrapper<ChatSession>()
.eq(ChatSession::getSessionId, sessionId)
);
if (session == null) {
throw new BusinessException(404, "会话不存在: " + sessionId);
}
List<ChatMessage> messages = chatMessageMapper.selectList(
new LambdaQueryWrapper<ChatMessage>()
.eq(ChatMessage::getSessionId, sessionId)
.orderByAsc(ChatMessage::getCreatedAt)
);
session.setMessages(messages);
return session;
}
保存会话——增量插入新消息,不全量覆盖:
public ChatSession saveSession(ChatSession session) {
// 更新标题和时间
dbSession.setTitle(session.getTitle());
dbSession.setUpdatedAt(LocalDateTime.now());
chatSessionMapper.updateById(dbSession);
// 只插入新增的消息
long existingCount = chatMessageMapper.selectCount(...);
List<ChatMessage> newMessages = allMessages.subList((int) existingCount, allMessages.size());
for (ChatMessage msg : newMessages) {
msg.setSessionId(session.getSessionId());
chatMessageMapper.insert(msg);
}
return session;
}
删除会话——一行搞定,不再有目录遍历问题:
public void deleteSession(String sessionId) {
chatMessageMapper.delete(
new LambdaQueryWrapper<ChatMessage>().eq(ChatMessage::getSessionId, sessionId)
);
chatSessionMapper.delete(
new LambdaQueryWrapper<ChatSession>().eq(ChatSession::getSessionId, sessionId)
);
}
4. 适配 5 个 Agent Service
接口签名从 createSession(String agentType, String mode) 改为 createSession(String agentType, String mode, Long elderlyId, Long familyId)。
5 个 Agent 的 resolveSession 方法都需要传入 elderlyId:
// 以健康陪诊为例
session = chatSessionService.createSession("health-companion", null, request.getElderlyId(), null);
// 家属辅诊还要传 familyId
session = chatSessionService.createSession("family-assist", null, request.getElderlyId(), request.getFamilyId());
5. 清理旧配置
删除 application.yml 中的文件存储路径配置:
# 已删除
chat:
session:
storage-dir: ./chat-sessions
踩过的坑
坑 1:字段名 timestamp 和数据库保留字冲突
原来 ChatMessage 有个字段叫 timestamp,这在 PostgreSQL 中是类型关键字。虽然加反引号可以规避,但不如直接改名为 createdAt,和项目其他表保持一致。改名前需要确认没有任何代码直接调用 getTimestamp()。
坑 2:增量保存的前提假设
增量保存靠"数据库已有消息数"对比"内存中消息列表长度"来判断哪些是新增的。这个方案的前提是消息只增不改。如果未来要做消息编辑或撤回,这套逻辑就不适用了。当前场景下够用。
坑 3:事务注解不要忘
saveSession 涉及"更新 session + 批量插入 messages",必须加 @Transactional。不然中间出错会导致 session 标题更新了但消息没插进去。
改造前后对比
| 维度 | 文件存储(改前) | 数据库存储(改后) |
|---|---|---|
| 删除会话 | 5 个 Agent 中有 3 个失效 | 全部正常 |
| 查询方式 | 遍历目录、解析文件名 | SQL 按索引查询 |
| 按用户筛选 | 不支持 | 支持(elderly_id 索引) |
| 并发安全 | 文件锁(未实现) | 数据库事务保证 |
| 部署依赖 | 需要可写磁盘路径 | 只依赖数据库连接 |
| 数据一致性 | 无保证 | CASCADE + 事务 |
部署步骤
- 在 PostgreSQL 中执行
sql/chat_session_tables.sql - 重新打包部署
- 旧的
./chat-sessions/目录可以删除(历史数据不迁移,新对话会写入数据库)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)