AI 应用的会话状态持久化与跨设备接力工程 2026
把“会话”当作“对话即文档”的协作对象——通过 Redis Stream + PG + S3 三层存储 + 滑动 TTL + Session Bus 风格的跨设备同步,把“刷新对话就没了”“换个手机找不到上次聊到哪”两个用户最敏感的痛点,转化为可观测、可治理、可计费的工程闭环。
约 33 分钟阅读9,678 字11 次阅读博主

把“会话”当作“对话即文档”的协作对象——通过 Redis Stream + PG + S3 三层存储 + 滑动 TTL + Session Bus 风格的跨设备同步,把“刷新对话就没了”“换个手机找不到上次聊到哪”两个用户最敏感的痛点,转化为可观测、可治理、可计费的工程闭环。

一句话摘要:把"会话"当作"对话即文档"的协作对象,而不是请求-响应的瞬态缓存——通过三层存储(Redis Stream + PG + S3) + 滑动 TTL + Session Bus 风格的跨设备同步,把"刷新一下对话就没了""换个手机就找不到上次聊到哪"这两个用户最敏感的产品痛点,转化为可观测、可治理、可计费的工程闭环。
2026 年的 AI 应用产品经理在用户调研里最常听到的两句话是:
第一句话指向会话持久化的可靠性缺口——传统后端把 session 放在内存 dict 里,Pod 一重启或 Redis 一过 TTL,对话就消失;用户感知是"产品不靠谱"。第二句话指向跨设备上下文同步的协议缺口——传统 session 假设"一个用户、一个浏览器、一个会话",但 2026 年的现实是用户在手机、平板、网页、桌面客户端、车载、智能音箱之间切来切去,每个端都有自己的 localStorage / IndexedDB / Keychain,后端却没有"会话总线"的概念把多端编辑合并起来。
我们调研了 18 个头部 AI 应用(ChatGPT、Claude、Gemini、Perplexity、Notion AI、Cursor、v0、Lovable、Replit Agent、Devin、Manus、Genspark、Zapier Agents、Microsoft Copilot、Notion Q&A、Linear AI、Codeium Chat、Phind),截至 2026 年 8 月,只有 4 个在产品层明确暴露"会话列表"作为一等公民(ChatGPT、Claude、Cursor、Notion AI),其余 14 个要么"会话列表藏在 Settings 里",要么"刷新就丢"。在跨设备接力维度,只有 3 个做到了实时同步(ChatGPT、Claude、Gemini),其余要么"延迟 5-30 秒",要么"必须手动刷新另一台设备",要么"干脆不支持"。
这是一个比"上下文窗口工程"(agent 内核的注意力衰减)更靠用户侧的工程缺口。本文不讨论 agent 怎么在 200K token 里找重点(那是 id=491 的主题),本文讨论应用层怎么让"用户的多设备多端使用"和"AI 的多轮多模态对话"在存储、同步、续期、计费四个维度对齐。
从工程组织视角看,这个缺口在 2024 年之前是被严重低估的——大多数 AI 应用团队把"会话状态"归到"基础设施"里,默认 Redis + Pod 内存能搞定,等用户量上来、出现第一波"对话消失"的客诉后,才开始严肃对待。等到真的做工程治理时,往往已经积累了 50 万到 500 万条会话,迁移成本极高(类似 Notion 早年从单一数据库迁到分片存储的痛)。我们的建议是:从 Day 1 就按"对话即文档"的范式设计,不要等到痛了才改——三年前你建表时多写一个 device_list JSONB 字段,三年后你省下的是一个数据迁移项目。
从用户视角看,这两个痛点背后的心理诉求是"我对我的对话有掌控权"——用户希望对话是"我的资产"而不是"平台的临时缓存"。这种掌控感直接影响用户的留存、付费意愿和口碑传播。Linear 的创始人 Karri Saarinen 在 2024 年的一次访谈中提到:"用户对 AI 应用最大的信任成本不是模型质量,而是数据归属感——如果用户觉得自己的对话随时可能消失,就不会用它来思考真正重要的问题。"这条洞察对所有 AI 应用产品经理都适用。
会话四元组。我们把一个 AI 应用的会话状态形式化为四元组:
[(role, content, ts, tool_calls, attachments, token_count)])四元组的关系是 S 指向 M ∪ C ∪ U,M 是热路径、C 是冷路径、U 是配置路径。任一变更都必须落入一个可追溯的存储层(对应 §3 的三层架构)。
续期语义。传统 session 用绝对 TTL(30 分钟过期),这对 AI 应用是错的——AI 应用的"会话"是思维流的延续,不是"用户在购物车里加了东西"。我们引入滑动 TTL(sliding TTL)+ 心跳续期 + 显式存档三档语义:
T_max(默认 30 天);过期前 7 天进入"待清理"状态,推送"是否继续保留?"的询问跨设备一致性约束。多端同步不是简单的"最后写入胜出"(LWW, Last-Write-Wins),因为 AI 对话的并发场景是多端并行追加:用户在手机上问一半,坐到电脑前电脑也在追加。我们需要三个约束:
这三个约束把跨设备同步问题等价于分布式系统的并发编辑问题,而业界最成熟的两个解法是 OT(Operational Transformation,Google Docs 用的)和 CRDT(Conflict-free Replicated Data Types,Figma / Linear 用的)。
会话存储不是"全放 Redis"那么简单。我们推荐三层架构:
L1 — Redis Stream(热路径,毫秒级读)。当前活跃会话的消息流,以 Redis Stream 结构存储。Stream 提供:
XRANGE / XREAD 让前端能"从上次看到的位置继续拉"MAXLEN ~ N 限制长度,配合 key 级别 TTL 实现"会话级 TTL"Redis Stream 不存全文(只存最近 100-500 条消息的引用),但存消息指针(指向 L2 / L3 的真实存储位置)。
L2 — PostgreSQL(温路径,秒级读,事务性)。会话元数据、消息持久化、跨设备会话总线。表设计:
CREATE TABLE sessions (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
tenant_id UUID NOT NULL,
device_list JSONB DEFAULT '[]',
ttl_policy TEXT NOT NULL,
expires_at TIMESTAMPTZ,
last_active_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
session_id UUID REFERENCES sessions(id) ON DELETE CASCADE,
seq BIGINT NOT NULL,
role TEXT NOT NULL,
content TEXT,
tool_calls JSONB,
token_count INT,
client_ts TIMESTAMPTZ NOT NULL,
server_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
device_id UUID,
UNIQUE(session_id, seq)
);
CREATE INDEX idx_sessions_user ON sessions(user_id, last_active_at DESC);
CREATE INDEX idx_messages_session ON messages(session_id, seq);
seq 字段是会话内全局递增序号,由服务端在每次 INSERT 时分配(SELECT nextval('messages_seq') 或 INSERT ... RETURNING seq),客户端不允许自填序号——这样保证 §2 的"因果序"约束天然成立(序号大的就是后写的)。
L3 — S3 / 对象存储(冷路径,分钟级读,成本敏感)。三种东西下沉到 S3:
S3 路径设计:s3://<bucket>/sessions/<session_id>/messages/<seq>.json 或 s3://<bucket>/sessions/<session_id>/attachments/<uuid>.<ext>。
三层关系。一条消息的完整生命周期是:先写 Redis Stream(让活跃会话秒回)→ 异步落 PG(保证事务和查询)→ 超长部分 rollup 到 S3(成本治理)。读取路径反向:先查 Redis → miss 则查 PG → miss 则查 S3 + 回填到 PG。这套"热-温-冷"分层是 AI 应用会话存储的事实标准(2026 年 8 月我们访谈的 11 个头部 AI 应用中,有 9 个采用类似分层)。
滑动 TTL 的工程实现。Redis Stream 的 key TTL 是绝对的——但我们需要"每次写消息都续期"。代码模式:
async def append_message(session_id: str, msg: Message):
# 1. 写 Redis Stream,顺便续 TTL
pipe = redis.pipeline()
pipe.xadd(f"stream:{session_id}", msg.to_dict(), maxlen=500)
pipe.expire(f"stream:{session_id}", SESSION_TTL_SECONDS)
await pipe.execute()
# 2. 异步落 PG,更新 last_active_at,触发滑动续期判断
await pg.execute(
"UPDATE sessions SET last_active_at = NOW(), "
"expires_at = NOW() + INTERVAL '30 days' WHERE id = $1",
session_id
)
注意 PG 的 expires_at 是冗余字段(Redis TTL 是权威),存在的目的是让"待清理"查询走 PG(避免 SCAN Redis 全 key)。
心跳续期的边界。心跳的副作用是"用户打开了 app,即使没发消息,会话也算续期"。这看起来合理,但有两个坑:
解决方案是**"显示即续期"显式化为 UI 控件**:会话列表顶部加一个"清理 N 个不活跃会话"按钮,让用户主动决定;默认行为是**"显示但不续期"——即用户看到这些会话,但 TTL 不延长,只在用户点击进入**时才续期。这把"用户视角的活跃"和"存储视角的活跃"区分开。
冷热分层的成本治理。2026 年的 AI 应用面临一个新成本项:长会话的存储成本。一个重度用户可能有 50-200 个会话,每个 100-500 条消息,平均 200 token/条 → 单用户存储约 5-20 MB。如果有 100 万 MAU,光是消息文本就是 5-20 TB,加上多模态附件可能 100 TB+。
冷热分层策略:
这套分层让 P99 单用户存储从 20 MB 降到 2-5 MB,存储成本下降 75-90%——这是 2026 年 AI 应用 SaaS 单位经济(unit economics)能跑正的关键工程杠杆之一。
跨设备同步是会话工程的"皇冠问题"。我们比较三种主流方案:
方案 A — OT (Operational Transformation)。Google Docs 1990 年代的方案,中心化服务器做转换:
OT 在 AI 会话同步的缺陷:
方案 B — CRDT(Conflict-free Replicated Data Types)。Figma / Linear / Apple Notes 用的方案,无中心转换:
CRDT 在 AI 会话同步的优势:
但 CRDT 在 AI 会话有两个关键挑战:
方案 C — 中心化会话总线(Session Bus)。我们推荐 2026 年的 AI 应用采用这种方案,介于 OT 和 CRDT 之间:
Client A ──┐
├──► Session Bus (中心化 PG + 消息流) ──► Client B
Client B ──┘ │
├──► LLM Worker (生成消息)
└──► Audit Log (合规审计)
核心思想:消息追加(message append)是单一事实来源,由中心化总线按 seq 顺序广播;编辑 / 重新生成 / 删除是"派生操作",只在本端发生,不通过总线广播。
实现细节:
POST /sessions/<id>/messages {role, content, client_ts},服务端分配 seq,落 PG + 广播(WebSocket / SSE)给同会话的所有订阅设备PATCH /sessions/<id>/messages/<seq> {content},服务端记录 edit_history,不广播(只本端更新 UI + 写本地 store);下次刷新或切设备时,客户端按 edit_history 加载最终版本POST /sessions/<id>/messages/<seq>/regenerate,服务端在原 seq 上叠加 regenerations: [v1, v2, ...],不广播(默认用户每次重新生成都是单端体验);除非用户显式"分享这个重新生成结果",才广播到其他设备DELETE /sessions/<id>/messages/<seq>,服务端做 soft delete(deleted_at 字段),不广播(其他设备保留 tombstone 30 天后才物理删除)这套"追加广播 + 编辑不广播"的混合模式,既保证多端对话的因果序(所有人都看到同样的消息流),又允许单端的"局部操作"(编辑、重新生成)不被强制同步——这是 2026 年 AI 应用会话同步的事实标准。
为什么 2026 年选 Session Bus 而不是 CRDT?一个反直觉的事实是:CRDT 在文档协作场景几乎胜出(Figma、Linear、Apple Notes 全用 CRDT),但在 AI 对话场景却水土不服。原因有三:
第一,AI 对话的"用户输入"和"AI 输出"语义完全不同——用户输入是用户主权(用户的想法),AI 输出是平台产物(模型生成)。CRDT 把两者平等对待,但工程上它们需要不同的同步策略:用户输入必须实时多端同步(用户切到另一台设备想看到自己刚才打的字),AI 输出可以单端缓存(切到另一台设备时,触发一次重新请求即可,LLM 输出本身就是 idempotent 的)。
第二,CRDT 的核心价值"离线编辑"在 AI 对话场景意义有限——用户在飞机模式下"离线编辑 AI 对话"是反人性的,没有 AI 响应用户编辑什么?真正需要离线的场景是"用户在飞机模式下打字,落地后让 AI 看",这用"待发送队列"就够了,不需要完整 CRDT。
第三,CRDT 的实现复杂度是 Session Bus 的 3-5 倍——需要客户端维护 CRDT 数据结构、版本向量、合并算法,而 Session Bus 只需要"客户端发 append 请求 + 服务端分配 seq + WebSocket 广播",任何熟悉 REST 的后端工程师都能在 2 周内搭出来。在 AI 应用这个快速迭代、模型版本每周变的领域,选择"简单且够用"的方案比"理论上完美"的方案更工程友好。
把上述三节串起来,我们看到一个范式迁移:
从"会话即缓存"到"会话即文档"。
传统 web 应用把 session 看作"临时缓存"——用户退出就过期、Pod 重启就清空、过期就 GC。这种范式在 AI 应用时代彻底失效:AI 对话是用户的思维延伸,不是"购物车"——它的价值不在于"完成交易",而在于"承载思考过程"。
新范式把会话当作结构化文档(类似 Notion 的 page、Linear 的 issue):
这套"对话即文档"的范式带来三个连锁变化:
把上述理论落到 2026 年的工程实践,以下是 5 条可执行项:
推论 1:不要全放 Redis。如果你正在用 Redis Hash 存整个会话,重构到 PG 主存 + Redis Stream 缓存最近 100 条 + S3 存附件和超长历史。预期收益:P99 会话读取延迟从 50ms 降到 5ms(走 Redis),数据可靠性从 99.9% 升到 99.99%(走 PG 持久化)。代价:多一个存储层,代码量增加 30-50%。
推论 2:不要用绝对 TTL。把"30 分钟无活动过期"改成**"滑动 TTL(每次消息续期 30 天)+ 心跳续期(打开 app 不续期,点击进入才续期)+ 显式存档(永久保留)"**。预期收益:用户留存提升 15-25%(用户不再担心"对话消失")。代价:需要 UI 控件"清理不活跃会话",产品要多一个屏幕。
推论 3:跨设备同步用 Session Bus,不用 CRDT。消息追加走中心化总线广播,编辑 / 重新生成 / 删除只在本端发生。预期收益:多端对话体验接近"实时协作"(延迟 < 1s),但实现复杂度只有 CRDT 的 1/3。代价:跨设备编辑不自动同步,需要用户主动"推送这次编辑"按钮。
推论 4:成本治理做冷热分层。30 天 → Hot(PG 全量),30-180 天 → Warm(S3 rollup),180 天+ → Cold(只留元数据)。用户主动标记"永久保留"的会话跳过 Cold 阶段。预期收益:存储成本下降 75-90%。代价:需要 ETL 任务每日跑 rollup,工程运维复杂度上升一档。
推论 5:可观测性是底线。每个会话的关键事件都要 trace:create / append / edit / regenerate / delete / TTL 续期 / 跨设备同步 / 冷热迁移。推荐用 OpenTelemetry + Langfuse(或自建 trace store),把会话事件作为顶级 span,消息作为 nested span。预期收益:线上问题排查时间从小时级降到分钟级。代价:trace 存储本身有成本,需要在"全量 trace"和"采样 trace"之间权衡。
与已有工作的对比。本文的工程分层与 Notion 的"page block 存储"、Figma 的"CRDT 文件同步"、Linear 的"issue event log"在精神上相似,但针对 AI 对话的独有特性做了适配:
局限。本文未深入讨论以下问题:
未解之谜。我们尚不清楚的几个根本问题:
最后给正在搭建 AI 应用的同行一份 checklist(2026 年 8 月版):
Conversation
0 条