652 字
3 分钟
AgentScope 源码调研(六):Session 与 Message 为什么分开存储

Session 与 Message 都包含 session_id,却不应因此保存在同一结构中。AgentScope 的存储抽象说明,边界应由数量关系、生命周期、更新方式和查询模式决定,而不是由关联字段决定。

StorageBase 固定业务操作#

ChatService 不直接处理 SQL 表或 Redis Key,而是调用:

await storage.upsert_message(user_id, session_id, reply_msg)
await storage.update_session_state(
user_id=user_id,
agent_id=agent_id,
session_id=session_id,
state=agent.state,
)

StorageBase 因此更接近业务持久化端口。Redis 与 SQL 适配器负责序列化、主键、TTL 和事务提交,上层只依赖“保存消息快照”和“更新 Session 状态”的语义。

Session 与 Message 具有不同数据形状#

角度SessionMessage
数量每个会话一条当前记录每个会话持续产生多条
生命周期创建、更新、终止随每轮对话追加
更新替换当前 AgentState新消息插入,同一回复可更新
查询按 Session ID 读取分页、排序或按消息 ID 定位

SQL 实现因此使用 Session 当前记录和 (session_id, msg_id) 复合主键的 Message 表。消息 ID 稳定,使同一轮恢复后的 upsert_message() 更新原回复,而不是产生重复消息。

相同接口不代表相同一致性#

Redis 与 SQL 都实现 upsert_message,但底层原子性、排序和过期行为并不完全相同。ChatService 收尾时依次保存 Message、更新 Session State,再 Trim Event Log;异步屏蔽可以保证取消期间继续执行收尾,却不能把多个操作变成跨对象事务。

可能出现的中间状态包括:

  • Message 已保存,Session State 尚未更新;
  • Session State 已更新,Event 尚未清理;
  • Redis 列表与 SQL 排序对同一边界条件处理不同。

所以统一接口固定的是业务意图,不是自动获得统一事务语义。恢复路径仍需规定哪一项是权威事实,并对重复写入保持幂等。

我的判断:统一端口,显式声明一致性#

我会保留 Session 与 Message 的独立模型,通过统一存储端口约束上层调用,同时为每个实现写出顺序、幂等、事务和过期保证。

代价是业务层不能假设所有后端完全等价,测试也必须覆盖多个适配器。若部署只使用一种数据库,仍然值得保留领域端口;只有在业务语义本身完全相同的情况下才共享接口,不能为了“可替换”隐藏实际一致性差异。

关键源码路径