数据库规模的有状态对话:每个生产聊天功能都需要的会话存储架构
大多数工程师在生产中而非设计评审时发现他们的会话架构是错误的。演示运行良好:你用五条消息进行了测试,对话历史可以放入内存,LLM 也能连贯地响应。然后你上线了,在第一批千个并发会话与第一次部署滚动之间,用户开始遇到上下文遗忘、部分响应或会话无故重置的问题。让聊天功能原型设计变得简单的内存模式,正是使其在运营中变得脆弱的根源。
这并不是一个微妙的架构错误。对话状态与请求状态有着本质区别。请求状态只存活数毫秒;对话状态必须能够在 Pod 重启、水平扩展、部署周期和移动网络中断中存活——持续数分钟、数小时或数天。建立在错误抽象上的系统会产生可靠性债务,随着对话长度增长和用户负载增加而累积。
内存失败分类
当对话历史仅存活在应用进程内 存中时,它继承了进程的生命周期。这意味着以下任何情况都会销毁它:Pod 崩溃、自动伸缩缩减、部署滚动 Pod、无服务器函数超时。无状态基础设施是有其充分理由的规范——但对话历史是状态,需要如此对待。
第二种失败模式更为微妙:上下文窗口溢出。当你无限制地将原始消息历史附加到每个 LLM 请求时,最终会触达模型的令牌限制。这可能以 API 错误的形式硬性失败,或以静默截断的形式软性失败——LLM 在不通知你的情况下丢弃较旧的消息,用户得到的响应似乎遗忘了早期上下文。这两种失败模式在使用短测试对话的暂存环境中都不可见。
第三种失败模式与直觉相悖:过多的上下文会降低质量。天真地持久化一切并将完整历史塞入每个提示的团队发现,事实回忆任务的准确率下降至 40-45%——比根本没有记忆更糟——因为语义上遥远的旧轮次污染了上下文。完整历史检索每次对话消耗约 26,000 个令牌;智能检索相关过去交流只消耗约 1,800 个令牌,且产生更好的结果。更多上下文并不总是更好的上下文。
最后是竞争条件类别:前端状态管理漏洞导致两个并发 POST 请求发送到同一会话——一个带有完整上下文,一个仅带有最新消息——造成非确定性上下文丢失。这种失败看起来像是间歇性模型混乱,但实际上是一个分布式状态问题。
分层存储模式
处理所有这些失败模式的生产解决方案是分层存储:热内存用于活跃上下文,持久存储用于耐久性,归档存储用于成本控制。
热层是 Redis。它以与会话超时对齐 的 TTL 保存活跃会话的最后 20-50 条消息。Redis 提供亚毫秒级读写,这很重要,因为上下文组装在每个 LLM 请求的关键路径上。键结构:{session_id}:{user_id} 映射到有界消息栈,以及一个单独的 {user_id}:summary 键用于存储较旧轮次的累积摘要。
持久层是文档存储或关系数据库——DynamoDB、PostgreSQL 和 MongoDB 都是常见选择。这是真相来源。当 Redis 驱逐会话或 Pod 冷重启时,系统从这里重新加载。延迟为 10-50ms,对于冷启动上下文组装来说是可接受的。
冷层是对象存储或归档数据库。30-90 天以上的会话以最低存储成本存活在这里。DynamoDB 的 TTL 功能可以在 90 天后自动过期消息项目,保持持久层精简。
当请求到来且 Redis 键缺失(缓存未命中)时,系统从持久层加载最近 N 条消息加上运行摘要,预热 Redis,然后继续处理。LLM 始终只看到重建的有界上下文,而非不断增长的原始记录。
压缩:防止无限增长
即使有分层存储,你也需要一个策略来处理对话积累时发生的情况。天真的答案——永远保留一切并全部加载——会产生前面描述的令牌成本和质量下降问题。生产答案是压缩:系统性地减少旧上下文,同时保留有意义的信息。
最常见的方法是批量摘要化。当对话积累 20 条新消息时,后台进程触发对该批次的 LLM 摘要化,将摘要写入持久层,并从活跃窗口中删除原始消息。新请求加载运行摘要加上当前工作批次。摘要本身可以随增长分层重新摘要化。这种方法使用更小、更便宜的模型(如 GPT-4o-mini 或等效模型)进行摘要化步骤,保持低成本。
更复杂的系统使用令牌计数阈值而非消息计数。当组装的上下文超过可配置的令牌预算时,压缩管道按积极程度顺序运行:
- 折叠冗长的工具调用结果(无需 LLM,最小信息损失)
- 摘要化最旧的片段(LLM 调用,适度信息损失,保留意图)
- 应用滑动窗口只保留最近 N 轮(硬截止)
- 作为最后手段截断最旧组(最大信息损失)
一个困扰团队的约束:你不能删除工具调用消息而不删除其配对的工具结果,反之亦然。LLM API 会拒绝带有孤立函数调用的序列。压缩逻辑必须将这些视为原子单元,否则会产生看起来像模型失败的 API 错误。
记忆形成是摘要化的更具选择性的替代方案。系统不是摘要化所有内容,而是提取持久事实——偏好、名称、决策、约束——并将其存储在结构化键值或图存储中。在未来请求时,这些提取的事实作为紧凑系统提示块注入,而非作为对话历史重放。基准比较显示,与单独摘要化相比,记忆形成实现了 80-90% 的令牌减少和 26% 的响应质量提升,因为事实被精确召回而非通过多层压缩转述。
重要的数据模型细节
大规模生产中使用的 DynamoDB 设计展示了你最终会遇到的数据建模约束。每项 400 KB 的硬限制排除了将所有消息存储为单个对话项目中嵌套列表的天真模式。任何非平凡的对话都会超过此限制。
正确的模型使用垂直分区:对话元数据(用户 ID、标题、创建时间戳、设置)存储在一个项目类中;每条单独的消息存储在其自己的项目中。这意味着写入"hi"恰好消耗一个写入容量单元,不管总对话大小如何。消息项目使用 ULID(通用唯一词典顺序标识符)作为排序键——一个 26 字符的词典顺序可排序标识符,无需二级索引即可对消息序列进行高效范围查询。
架构:
Conversation: PK=<UserID> SK=CONV#<ConversationID>
Message: PK=<UserID> SK=CHAT#<ConversationID>#MSG#<ULID>
这支持的访问模式:列出用户的所有对话(按 PK 前缀查询),加载会话最近 20 条消息(按 PK + SK 范围查询,逆序,限制 20),追加新消息(放置项目)。全主键访问,无扫描。
对于 PostgreSQL 用户:类似模式是一个 conversations 表和一个带外键的 messages 表,按 (user_id, conversation_id, created_at) 索引。OpenAI 的 ChatGPT 会话存储在单个 PostgreSQL 主库上运行,约有 50 个地理分布的读副本,以低两位数毫秒 p99 读延迟服务数亿用户。工作负载是强读偏向的——每写入一条消息有多次上下文查找——这自然映射到单主库加副本扇出。不需要奇特的分布式数据库。
跨设备会话 连续性
大多数生产实现能很好地处理单会话、单设备模型。当用户在对话中途切换设备、关闭移动应用后重新打开,或同时从台式机和手机访问同一会话时,架构会崩溃。
根本问题是会话状态通常绑定到连接,而非用户身份。当连接断开并重连时,服务器要么(a)没有会话记录,需要用户重新开始,要么(b)有陈旧上下文,不反映间隙期间发生的事情。
解决方案是基于频道的会话架构:状态存活在以用户身份而非连接 ID 为键的持久频道中。任何订阅该频道的设备都接收相同的消息历史。设备在连接和断开时订阅和取消订阅;频道独立持久存在。
跨设备的具体模式:
- 在频道历史中存储完整的 AI 响应,而非单个流式令牌。当新设备重新连接时,它从频道历史中高效重放。
- 跟踪设备存在。当没有设备连接时,暂停任何正在运行的代理生成,以避免在没有人阅读的输出上浪费令牌。
- 用持久层中的权威真相来源处理冲突状态——频道是数据库的视图,而非其替代品。
在 2025 年评估的 AI 平台中,37 个中有 32 个没有有意义的多设备扇出能力。这是一个常被跳过的功能,随着移动使用增长会成为重大支持负担。
何时添加哪个层
并非每个聊天功能都需要所有这些。粗略的决策矩阵:
内存会话状态已足够的情况:对话简短(少于 10 轮),会话不需要在服务器重启后存活,你在原型开发或早期访问阶段。
添加 Redis + 持久存储的情况:你遇到了第一次导致用户对话丢失的部署,会话需要在 Pod 循环中存活,或你服务超过几百个并发用户。
添加压缩的情况:平均对话长度超过 20-30 轮,上下文组装的令牌成本在你的账单中可测量,或你在长对话中观察到质量下降。
添加向量检索的情况:你需要非线性记忆(检索相关的,而非最近的),会话跨越有空闲期的数天或数周,或你构建的个人助理需要从数月前召回事实。
添加每对话工作流耐久性的情况:对话运行数小时,跨越外部集成(发送电子邮件、数据库写入),并需要进行中步骤的精确一次语义。复杂性成本显著;只有当对话连续性是核心产品保证时才值得。
大多数团队遗漏的架构缝隙
压缩策略和数据模型是实现细节。真正的架构决策是会话状态在你的系统中的位置以及谁拥有加载和水化它的契约。
将上下文管理嵌入请求处理器的团队——在同一个调用 LLM API 的函数内组装上下文——最终会遇到上下文逻辑散布在功能中、重复的压缩代码,以及没有干净方式独立测试组装逻辑的问题。提取 SessionContextLoader 抽象的团队——一个负责给定会话 ID 加载有界上下文的单一组件——最终拥有可测试、可替换的上下文策略。
会话存储架构值得在遇到扩展瓶颈之前设计,因为改 造它意味着重写存储层和每个触及对话历史的地方。与大多数基础设施债务不同,这个债务表现为用户可见的产品缺陷——遗忘的上下文——而非静默的性能回归。当用户报告它时,重构是紧迫的。
好消息是这些模式已经成熟。带有 Redis 热缓存和 PostgreSQL 或 DynamoDB 持久化的分层存储处理耐久性问题。批量摘要化或记忆形成处理令牌成本问题。ULID 键垂直分区处理数据模型问题。这些都不是新颖的;它们是你已经知道的分布式系统构建块,应用于一个恰好涉及语言模型的状态问题。
