Appearance
27|会话持久化:状态怎么存
上一篇把工具接进来了。这一篇解决多轮会话的状态存储。
第 8 篇建立了 SessionState,但那是在内存里。进程重启后状态就丢了,多 Worker 时每个进程有自己的内存,同一个会话的两次请求可能落到不同进程,第二轮看不到第一轮的上下文。本篇讲会话状态怎么持久化、怎么在多 Worker 间共享。
从内存到持久存储
单进程本地调试时,内存里的 SessionState 能工作。问题是两个:进程重启后 SESSIONS 字典清空;多 Worker 时每个进程有自己的字典。

第一步是把状态存到磁盘或数据库。SQLite 对早期项目够用,单文件、不用起服务:
python
import sqlite3, json
conn = sqlite3.connect("sessions.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
state_json TEXT,
updated_at TEXT
)
""")
def save_session(state: SessionState) -> None:
record = {"session_id": state.session_id, "messages": state.messages, ...}
conn.execute(
"REPLACE INTO sessions (session_id, state_json, updated_at) VALUES (?, ?, ?)",
(state.session_id, json.dumps(record, ensure_ascii=False), now()),
)
conn.commit()
def load_session(session_id: str) -> SessionState:
row = conn.execute("SELECT state_json FROM sessions WHERE session_id = ?", (session_id,)).fetchone()
if not row:
return SessionState(session_id, topic="unknown")
return SessionState.from_dict(json.loads(row[0]))多 Worker 共享时换 Redis,读写快、天然支持并发:
python
import redis
r = redis.Redis(host="localhost", port=6379, db=0)
def save_session(state):
r.set(f"session:{state.session_id}", json.dumps(state.to_dict()))
def load_session(session_id):
data = r.get(f"session:{session_id}")
return SessionState.from_dict(json.loads(data)) if data else SessionState(session_id, "unknown")有状态和无状态的取舍
会话存储有个根本取舍:有状态还是无状态。
有状态流程保留进度,能快速续跑——排查到一半被打断,恢复后接着查。但带来状态不一致风险:多个 Worker 同时改一份状态,后写覆盖先写。无状态流程便于复盘审计,每次请求自带完整上下文,但长任务效率低。
ops-assistant 用混合模式:会话状态(messages、turn_count、summary)持久化便于续跑,但每次请求的完整 trace 单独存到对象存储或日志系统,不进会话状态。trace 用于审计,会话状态用于续跑,两者分离。
竞态处理
多 Worker 共享会话时必须处理竞态。两个 Worker 同时加载同一份状态,各自修改后写回,后写的覆盖先写的。

text
Worker A: 加载 session-001 → 修改 → 保存
Worker B: 加载 session-001(此时还没被 A 改)→ 修改 → 保存(覆盖 A 的修改)两种解法。
悲观锁。修改前先抢锁,抢到才能改:
python
def update_session_with_lock(session_id, modifier):
lock_key = f"lock:session:{session_id}"
token = r.set(lock_key, "locked", nx=True, ex=30)
if not token:
raise ConcurrentUpdateError(f"会话 {session_id} 正在被修改")
try:
state = load_session(session_id)
new_state = modifier(state)
save_session(new_state)
finally:
r.delete(lock_key)乐观锁。不抢锁,保存时检查版本号,版本不对就重试:
python
def update_session_optimistic(session_id, expected_version, new_state):
pipe = r.pipeline()
pipe.watch(f"session:{session_id}")
current = pipe.get(f"session:{session_id}")
if json.loads(current)["version"] != expected_version:
pipe.unwatch()
return False # 版本不对,重试
pipe.multi()
new_state.version = expected_version + 1
pipe.set(f"session:{session_id}", json.dumps(new_state.to_dict()))
pipe.execute()
return True悲观锁适合写冲突多的场景,乐观锁适合读多写少。运维排查一个会话同时被多个 Worker 改的情况不多,乐观锁通常够用。
存什么、不存什么
Session 持久化的最大敌人不是存不进去,是存了太多。
很多系统把每一轮的完整消息列表都存进数据库,很快达到存储上限。内存中保留完整状态,磁盘/Redis 上只存摘要 + 最近几轮 + 关键 checkpoint。完整 trace 单独存到对象存储或日志系统,不占用高频访问的 Session 存储。
Session 里不能存不可序列化对象。数据库连接、文件句柄、线程锁无法 JSON 序列化,存进 Redis/SQLite 会失败。更隐蔽的是:某些对象看起来可序列化,但反序列化后状态不对(Pydantic 模型含 datetime 对象,不同版本解析行为不同)。Session 里只存纯数据(dict、list、str、int、float、bool),连接和句柄用时重建。
落盘频率
会话落盘不是每次请求都写一次磁盘。频繁写盘引入 I/O 延迟和并发问题。
内存中维护状态,每几轮或会话结束时批量落盘;关键操作(写操作工具执行前)强制同步落盘,保证崩溃后可恢复。续跑时从最近 checkpoint 恢复,丢失最近几轮可以接受的话,落盘频率可以低一些。
Session 过期策略要考虑正在进行中的排查。固定 TTL(1 小时)可能导致用户正在排查时 Session 被清除。活跃 Session(最近 5 分钟有更新)自动续期,不活跃的按 TTL 过期,续期上限 4 小时防无限续期。
持久化之后
会话状态能持久化了,多 Worker 能共享,进程重启能恢复。agent 现在能跨进程稳定工作。
下一篇讲最终产出:把排查过程组织成带证据的报告,以及支撑报告的 trace 和审计。