Skip to content

Day 3 — 会话管理 (Session)

读完这章你能获得什么:理解多用户多轮对话的管理机制——如何为每个用户保持独立的对话上下文,如何存储和清理会话数据。


一、前情提要

经过 Day 1-2,我们有了:

  • Gateway:统一消息格式 + 路由分发
  • Channel:CLI / WebChat / Webhook 多种接入方式

但现在有个尴尬的问题:

用户说了"我叫小明",然后接着问"我叫什么名字?"——系统根本不记得上一句话说了什么!

每条消息都是独立的,没有上下文。 这就像一个失忆的客服,每次你打电话去,他都问你"请问您是哪位?"。我们需要一个"记事本"来记录对话历史。


二、生活类比:银行客户档案

想象你去银行办业务:

  1. 开户:第一次来,银行给你建一份档案(Session),里面记录你的基本信息
  2. 每次来都查档案:你说"我要查余额",柜员先用你的身份证(user_id)找到你的档案
  3. 同一个人在不同网点有不同档案:你在 A 网点办的理财和 B 网点办的存款是分开记录的——同一个用户在 CLI 和 WebChat 有不同的会话
  4. 超时销户:如果你 3 年没来,银行会把你的不活跃账户清理掉——TTL 过期清理
  5. 两种存储方式
    • 小银行把档案存在柜台后面的文件柜里 → FileStorage(JSON 文件)
    • 大堂经理脑子里记几个常客的信息 → InMemoryStorage(内存,重启就忘了)

三、核心概念详解

3.1 Session 和 ChatMessage —— 档案和交易记录

Session(会话)= 一份完整的客户档案:

字段类比说明
session_id档案编号UUID 唯一标识
user_id身份证号用户标识
channel办理网点来自哪个渠道(cli/webchat 等)
messages交易记录列表对话中的所有消息
created_at开户时间会话创建时间
updated_at最后一次来的时间每次有新消息就更新

ChatMessage(聊天消息)= 档案里的每一条交易记录:

字段说明与 OpenAI API 的关系
role谁说的(user/assistant/system/tool)和 OpenAI 的 role 完全对齐
content说了什么消息正文
tool_call_id关联的工具调用 ID工具返回结果时用
tool_calls工具调用请求列表LLM 要求调用工具时填写

为什么 role 要和 OpenAI 对齐? 因为 Session 里的消息最终要发给 LLM——格式一致就可以直接用,不需要再转一次。

3.2 SessionManager —— 银行柜台

SessionManager 是管理会话的"总柜台",对外只暴露几个简单操作:

get_or_create(user_id, channel)  →  找到已有档案,或者新建一份
save(session)                    →  保存变更(添加新消息后要记得保存)
delete(session_id)               →  销户
cleanup_expired()                →  清理所有过期档案

查找流程(get_or_create 的工作方式):

为什么要"内存索引"? 查找加速。直接遍历存储里所有会话太慢了,内存里维护一个 (user_id, channel) → session_id 的映射表,O(1) 就能查到。类比:银行柜台电脑上有个检索系统,输入身份证号秒查到档案在哪。

3.3 SessionStorage —— 档案柜

存储接口是抽象的,提供两种实现:

存储类比优点缺点适合场景
InMemoryStorage柜员脑子里记快!重启就忘了开发、测试
FileStorage文件柜重启还在慢一点轻量生产环境

为什么要做成抽象接口? 因为将来你可能想换成 Redis 或数据库——只需要写一个新的 Storage 类实现接口,不用改 SessionManager 的代码。这就是"面向接口编程"的好处。

3.4 TTL 过期清理 —— 定期销户

TTL(Time To Live)= 生存时间。如果一个会话超过指定时间没有新消息,就认为它"过期"了。

为什么需要清理? 如果不清理,内存和磁盘会被废弃会话占满。类比:银行不可能永远保留所有客户的账户,不活跃的要清理。

清理是"主动"而非"自动后台":需要调用 cleanup_expired() 才会执行清理,不会在后台偷偷启动定时器。这样可以完全控制清理的时机,避免"正在处理消息时突然触发清理"的意外。


四、动手实验指南

4.1 运行示例

bash
python day3-session/example/main.py

示例会演示:

  1. 用内存存储创建会话、追加消息
  2. 用文件存储把会话持久化到磁盘
  3. 用短 TTL 触发过期清理

4.2 改一改,看看会怎样

实验 1:创建两个不同用户的会话,然后用 list_active_sessions() 查看所有活跃会话。

实验 2:把 TTL 设成 5 秒,创建会话后等 6 秒再调用 get_or_create 同一用户——观察是否创建了新会话。

实验 3:用 FileStorage 保存一个会话,找到生成的 JSON 文件打开看看里面长什么样。

4.3 运行测试

bash
pytest day3-session/session/ -v

五、常见问题 FAQ

Q1:同一个用户在 CLI 和 WebChat 的会话为什么是分开的?

A:因为场景不同。用户在 CLI 可能在调试代码,在 WebChat 可能在日常聊天——两边的对话上下文混在一起会很混乱。用 (user_id, channel) 作为联合键来隔离,就像同一个人在不同银行网点办不同业务。

Q2:messages 列表会不会无限增长?

A:在当前实现中会的。Day 7 我们会引入 ShortTermMemory 来做"窗口裁剪"——超过一定条数就把旧消息清理掉,只保留最近的 N 条。

Q3:为什么 ChatMessage 的 role 用 StrEnum 而不是普通字符串?

A:类型安全。如果你手打 "asistant"(少了个 s),普通字符串不会报错但 LLM 会不认识。用 StrEnum,IDE 会在你打错的时候立刻标红提醒。


下一章

现在消息能进来了,对话上下文也能保存了。但系统还缺一个关键角色——谁来"思考"并回复用户?

下一章 Day 4: Agent 运行时 将引入整个系统的"大脑"。