对话状态不仅仅是一个聊天数组:面向生产环境的多轮会话设计
大多数多轮 LLM 应用将对话历史存储为消息数组。这在演示(demo)中表现良好。但在生产环境中,它会以需要数天才能诊断出的方式崩溃,因为这些故障看起来更像是模型的问题,而非基础设施的问题。
用户在对话中途断开连接,并重新连接到不同的服务器实例——会话消失了。智能体(agent)在处理复杂任务时进入第 47 轮,载荷悄无声息地超过了上下文窗口——没有报错,只有错误的回答。产品经理问道:“我们可以让用户从第 3 步开始尝试不同的方法吗?”——而工程侧的回答是“不,按照我们的构建方式不行”。这些都不是极端情况,而是将对话状态视为瞬态数组(transient array)而非一等资源(first-class resource)的必然结果。
这篇文章将探讨对话状态究竟需要是什么样子,生产平台如何对其建模,以及在会话持续数小时并跨越多个后端实例时,如何通过 API 设计决策来保持会话的可靠性。
为什么数组模式会失 效
消息数组的吸引力在于它直接映射到 LLM API 所接收的内容:一个 {role, content} 对象列表。发送数组,获取响应,追加内容,循环往复。在出问题之前,这看起来非常完美。
上下文耗尽是第一个失败点。 在 temperature=0 的情况下处理长任务时,你的智能体在几十轮内都能做出正确的决策——然后载荷超过了模型的上下文窗口限制。你认要么遇到了硬性的 API 错误,或者更糟的是,提供商悄悄地截断了较旧的消息。当截断发生时,模型会开始对其在会话早期“做出”的、但现在已从其上下文中消失的决策产生幻觉。你的日志中没有任何迹象表明发生了这种情况。
注意力衰减是一个较慢的失败过程。 对主要前沿模型的研究表明,在达到任何上下文限制之前,多轮对话的平均性能会下降 39%。这种机制被称为“迷失在中间”(loss in the middle):模型会强烈关注其上下文的开头和结尾,而忽视中间的轮次。到第 20 或 30 轮时,早期的约束和决策对模型来说实际上是不可见的,尽管它们仍在数组中。
无状态性扼杀了重新连接。 一个存在于客户端内存中——或者更糟,从本地存储在客户端重建的——简单消息数组无法在服务器重启、网络分区或用户在中途切换设备时存活。每次重新连接都要从零开始。对于简单的聊天机器人,用户可以忍受这一点。但对于处理长达数小时任务的智能体来说,这是不可接受的。
分叉在架构上是不可能的。 用户自然希望探索替代方案:“如果我改用另一种方法呢?” 使用数组,没有分支点——你要么丢失原始路径,要么维护多个完整的历史副本,这既昂贵又不一致。
会话状态作为一种资源
解决方法是停止将对话状态视为一个载荷参数(payload parameter),而开始将其视为具有自身标识、生命周期和操作的服务器端资源。
一个会话资源承载的内容远不止消息数组。它起码需要:
- 一个在重新连接后依然存在的稳定标识符
- 一个执行状态机(空闲、运行中、等待工具结果、需要人工批准)
- 完整的消息历史,追加写入且写入后不可变
- Token 记账,让应用层能够对上下文管理做出显式的决策
- 父会话引用,用以支持分叉
- 每条消息和状态转换的时间戳
关键洞察是不可变的追加式历史(immutable append-only history)。一旦消息写入会话,它就永远不会改变。新的交互会产生新的消息。这实现了“时间旅行”语义:给定历史中的任何一点,你都可以从该检查点开始重新播放。它还防止了一类因重试逻辑使用略有不同的内容重写某一轮而导致的 Bug。
线程锁模式
当会话处于活动运行状态时——模型正在生成内容,或工具调用正在进行中——新的输入需要被拒绝或排队。OpenAI 的 Assistants API 将此正式定义为线程锁(Thread lock):在活动的 Run 期间,不能追加新消息,也不能创建新的 Run。这防止了两个并发输入破坏对话因果顺序的竞态条件。
这种模式可以直接转化为你自己的实现:会话资源有一个 locked_by 字段,获取当前 Run 的 ID,并且会话上的所有写操作在进行前都会检查该字段。这与数据库设计中的乐观锁(optimistic lock)模式相同,只是应用于对话状态。
恢复、分叉与回滚
一等会话状态支持三种数组模型无法清晰支持的操作。
恢复(Resumption) 意味着任何应用实例只要有 ID 就可以继续会话。会话状态存储在共享存储(Redis、Postgres 或托管的等效服务)中,而不是服务器内存中。当用户重新连接时,他们通过 ID 检索会话。无论哪个实例处理请求,模型看到的历史记录都是一致的。这是你在进行水平扩展时唯一可行的架构。
分叉(Forking) 会创建一个新会话,其父会话是截止到特定消息偏移量的现有会话。分叉继承到该点为止的所有历史,然后产生分歧。原始会话和分叉会话共享不可变的前缀;只有产生分歧的消息是新的。与深拷贝完整历史相比,这减少了一半的存储成本。更重要的是,它使“从第 5 轮尝试不同的方法”成为一等产品功能,而不是工程上的不可能。关于对话分支的研究显示,在探索性工作流中,用户感知的质量提升了 43%,上下文大小减少了 58%。
回滚(Rollback) 是分叉的一种退化情况:从早期的检查点创建一个分叉,然后丢弃原始分支。对于智能体来说,这对于错误恢复至关重要——当模型在 12 步任务的第 7 步做出错误决策时,你需要回退到第 6 步,而不会丢失之前的步骤。
序列化契约
当对话状态成为一种资源时,序列化就变成了一种契约,而不仅仅是一个实现细节。具体而言,这意味着:
每个会话必须能够导出为可移植的 JSON 格式。该格式包括所有消息(角色、内容、类型、时间戳)、会话在每次转换时的执行状态,以及版本元数据,以便使用者能够检测架构 (schema) 的变化。智能体 (Agent) 定义——系统提示词、工具规范和模型参数——是分开存储的,并在加载时重新实例化。凭证永远不会进入序列化状态。
可移植性要求产生了一个实际的后果:如果你想将一个会话从一个供应商移动到另一个供应商(例如,用 Claude 进行推理,用 GPT-4o 追求速度),你需要一个两个供应商都能读取的规范化消息架构。目前,这意味着需要手动在特定供应商的格式之间进行转换,但消息架构正在向通用结构汇聚。
version 字段的重要性超乎想象。当基础模型更新改变了它对工具调用 (tool call) 结果的解析方式时,更新前创建的会话在恢复时可能会表现得有所不同。在会话中存储模型版本和 API 架构版本让你能在恢复时检测到这一点,从而选择警告用户或将会话路由到兼容的端点。
该使用哪种存储层级
会话状态的正确存储方式取决于你的恢复 SLA(服务水平协议)。
内存缓存 (Redis) 为过去一小时内创建的会话提供亚毫秒级的读取,这是交互式应用的主要场景。除非配置了持久化,否则它们在重启时会丢失数据,因此应将其视为热存储层而非持久化存储。
关系型或文档数据库是持久化层。超过 Redis TTL(生存时间)的会话将被冷存储并按需获取。只增不减的消息结构非常适合文档存储;Postgres 的 JSONB 列或 MongoDB 在这里表现良好。
工作流引擎 (Temporal, Prefect) 是长时运行、容错的智能体任务的正确抽象。工作流封装了对话循环;状态持久化和重试由引擎处理。对于聊天机器人来说这有些大材小用,但对于长达数小时的自主任务来说非常合适,因为服务器重启不应中止正在进行的工作。
避免在单个应用服务器上使用内存状态。 这需要在负载均衡器上使用粘性会话 (sticky sessions),使水平扩展变得痛苦,并保证在部署或崩溃时丢失数据。
Token 预算问题
在服务器端存储完整的会话并不能解决上下文窗口问题——它只是将管理责任从客户端移交到了服务器。你的会话层需要显式的 token 预算逻辑。
最简单的方法是基于阈值的总结:当累积的 token 数量超过可配置的阈值(例如模型上下文窗口的 80%)时,会话层会自动为较旧的对话轮次创建一个压缩总结,并在发送给模型的活跃上下文中用该总结替换这些轮次。原始消息仍保留在存储中以供审计和重播;模型只是看不见它们。
更复杂的方法是利用语义相关性,在上下文紧张时决定包含哪些旧轮次。与其进行严格的时间顺序截断,会话层会检索与当前查询最相关的轮次。这需要为每一轮对话维护嵌入 (embeddings),虽然会增加延迟,但能显著提高模型在需要对早期上下文进行推理的任务中的表现。
核心设计原则:token 预算的决策属于会话层,而不是模型的职责。 模型无法告诉你它即将忘记重要信息。应用层必须对进入上下文的内容做出明确选择,并将这些选择呈现给开发者。
API 设计
将这些原则转化为 REST API,会话资源应暴露以下接口:
POST /sessions— 创建新会话,可选初始系统上下文GET /sessions/{id}— 获取会话元数据和当前状态POST /sessions/{id}/messages— 追加用户消息(如果会话已锁定则拒绝)POST /sessions/{id}/runs— 触发模型轮次,返回运行 ID (run ID)GET /sessions/{id}/runs/{run_id}— 轮询运行状态并流式传输输出POST /sessions/{id}/fork?at_message={message_id}— 从指定消息创建分支POST /sessions/{id}/checkpoint— 为回滚创建显式的命名检查点GET /sessions/{id}/export— 序列化完整的会话状态以实现可移植性
会话应采用软删除而非硬删除。硬删除一个分支所依赖的会话会损坏该分支的历史记录。保留策略和归档是与用户可见的删除操作分开进行的。
这在实践中改变了什么
从数组到资源的转变改变的不只是 API 的形状。它改变了你可以调试的内容、你的用户可以执行的操作,以及你的基础设施必须保证的事项。
可移植且可重播的会话意味着你可以在本地针对新的提示词版本重放失败的生产环境会话,以便在部署前验证修复效果。具有显式分支支持的会话意味着像“探索不同方法”这样的产品功能变得易于构建。具有 token 预算管理的会话意味着模型在第 50 轮时的表现与第 2 轮时一样可预测。
数组模型能带你走得比想象中更远。对于会话很少超过 20 轮、重新连接不常见且用户不期望有分支的聊天机器人来说,它是没问题的。但一旦你进入智能体工作流——长达数小时的任务、工具调用流水线、寿命超过浏览器标签页的工作流——其局限性的叠加速度会比你预期的快得多。从一开始就将会话状态设计为一种资源,你就能在规模化暴露弊端时避免痛苦的重构。
- https://platform.openai.com/docs/guides/conversation-state
- https://temporal.io/blog/building-a-persistent-conversational-ai-chatbot-with-temporal
- https://openreview.net/pdf?id=VKGTGGcwl6
- https://arxiv.org/abs/2512.13914
- https://github.com/microsoft/semantic-kernel/blob/main/docs/decisions/0048-agent-chat-serialization.md
- https://mem0.ai/blog/llm-chat-history-summarization-guide-2025
