主题
Day 3 — Session 设计文档
设计目标:实现多用户多轮对话的会话管理,支持可插拔存储后端和 TTL 自动过期清理。
一、核心接口设计
1.1 ChatMessage
| 字段 | 类型 | 说明 |
|---|---|---|
| role | MessageRole | user / assistant / system / tool |
| content | str | 消息正文 |
| tool_call_id | Optional[str] | 工具调用关联 ID |
| tool_calls | Optional[List] | LLM 返回的工具调用请求 |
为什么 role 和 OpenAI API 对齐?
- Session 中的消息最终要发给 LLM
- 格式一致 = 零转换成本
- 避免中间层格式映射的 bug
1.2 Session
| 字段 | 类型 | 说明 |
|---|---|---|
| session_id | str | UUID 唯一标识 |
| user_id | str | 用户标识 |
| channel | str | 渠道名称 |
| messages | List[ChatMessage] | 对话消息列表 |
| created_at | datetime | 创建时间 |
| updated_at | datetime | 最后更新时间 |
| metadata | Dict | 扩展元信息 |
1.3 SessionStorage(抽象接口)
| 方法 | 签名 | 说明 |
|---|---|---|
| save | async save(session: Session) | 持久化会话 |
| load | async load(session_id: str) -> Optional[Session] | 加载会话 |
| delete | async delete(session_id: str) | 删除会话 |
| list_all | async list_all() -> List[str] | 列出所有会话 ID |
1.4 SessionManager
| 方法 | 签名 | 说明 |
|---|---|---|
| get_or_create | async get_or_create(user_id, channel) -> Session | 查找或创建 |
| save | async save(session: Session) | 保存会话 |
| delete | async delete(session_id: str) | 删除会话 |
| cleanup_expired | async cleanup_expired() | 清理过期会话 |
| list_active_sessions | list_active_sessions() -> List[Dict] | 列出活跃会话 |
二、关键流程图
会话查找/创建流程
过期判断逻辑
python
def is_expired(session, ttl_seconds):
elapsed = now - session.updated_at
return elapsed.total_seconds() > ttl_seconds三、设计决策与权衡
决策 1:用 (user_id, channel) 作为会话路由键
| 方案 | 优点 | 缺点 |
|---|---|---|
| user_id + channel(选择) | 同一用户在不同渠道有独立上下文 | 换渠道时历史不共享 |
| 仅 user_id | 所有渠道共享一个会话 | 不同渠道的对话混在一起 |
| 每次请求新建 | 最简单 | 完全无上下文,失去多轮对话能力 |
选择理由:不同渠道的使用场景通常不同(CLI 调试 vs WebChat 正式对话),分开管理更合理。
决策 2:内存索引 + 外部存储
SessionManager 内部维护 Dict[(user_id, channel), session_id] 索引表:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 内存索引(选择) | O(1) 查找,最快 | 重启后需要从 storage 重建 |
| 每次查 storage | 无需内存索引 | O(n) 遍历,慢 |
权衡:教学阶段优先性能和简单性。生产环境中可以用 Redis 做分布式索引。
决策 3:TTL 手动触发 vs 后台定时器
| 方案 | 优点 | 缺点 |
|---|---|---|
| 手动调用 cleanup(选择) | 完全可控,无并发问题 | 需要调用方主动执行 |
| 后台定时器 | 全自动 | 可能在不合适的时机触发,并发复杂 |
决策 4:InMemoryStorage vs FileStorage
两种实现各有场景:
| 场景 | 推荐 |
|---|---|
| 单元测试 | InMemoryStorage(快、无副作用) |
| 开发调试 | 都行 |
| 需要重启后恢复 | FileStorage |
四、与前序章节的集成点
- 依赖 Day 1:ChatMessage 的 role 枚举借鉴了 GatewayMessage 的 StrEnum 方案
- 依赖 Day 2:Session 的
channel字段对应 Channel 的类型名 - 被 Day 4 依赖:AgentRuntime 通过 Session 获取对话历史
- 被 Day 7 依赖:ShortTermMemory 在 Session.messages 基础上做窗口裁剪
五、与真实生产系统的对比
| 维度 | miniOpenClaw | 生产级实现 |
|---|---|---|
| 存储 | 内存 / JSON 文件 | Redis(快)+ PostgreSQL(持久化) |
| 索引 | 进程内 dict | Redis Hash |
| TTL | 手动清理 | Redis 原生 TTL / 数据库定时任务 |
| 并发安全 | 单进程无锁 | 分布式锁 / 乐观锁 |
| 消息压缩 | 无 | 长对话摘要压缩 |