跳到主要内容

Session Stitching:为什么你的会话 ID 是个谎言

· 阅读需 12 分钟
Tian Pan
Software Engineer

一名用户在上午 9 点开始在她的电脑上与你的智能体谈判合同。她收到一条 Slack 消息,在午休时间切换到手机问了一个澄清问题,并在下午 4 点重新打开电脑标签页来修改草案。对她来说,这是一项任务 —— 处理一份合同的三个小时工作。对你的系统来说,这是两个设备上的三个会话,每个都有自己的 conversation-id,每个都有自己的记忆窗口,每个都呈现全新的问候并要求她重新粘贴已经讨论过两次的草案。

Bug 不在模型中。Bug 在于你的平台将“会话 (session)” —— 一个关于单一连接的传输层产物 —— 编码为上下文单位,而你的用户将“任务 (task)” —— 即合同 —— 编码为上下文单位。市面上的每个框架都悄悄地混淆了这两者,而它们之间的差距正是智能体 UX 损耗了一半的地方。

这并不是一个冷门的抱怨。一旦你开始记录任务级别的追踪(trace),你会发现很大一部分“新对话”实际上是未完成任务的延续 —— 用户放弃了手动缝合它们,转而重新开始。你称之为“参与度 (engagement)”的产品 KPI,实际上部分衡量了用户在为你的抽象缺失支付代价。

框架丢给你一个 Session-ID 并称之为记忆

打开过去两年中的任何智能体 SDK,持久化的故事都大同小异。LangGraph 要求你传递一个 thread_id;检查点(checkpointer)按线程保存图状态,与其他每个线程分开,并在你使用相同的 ID 重新调用时恢复。OpenAI Agents SDK 提供一个由会话 ID 索引的 Session (SQLiteSession("conversation_123"));同一个会话,完整的历史;新会话,一片空白。Claude Agent SDK 将会话持久化到磁盘,以便你稍后返回。Google 的 ADK 提供了一个 Resume 功能,可以接续中断的工作流运行。

这些原语在它们针对的层级上是正确的 —— 它们可靠地为单个逻辑运行持久化状态。但它们都没有定义从用户的角度来看什么是“逻辑运行”,而教程中提供的默认方案是:每个 WebSocket 连接一个会话,每个浏览器标签页一个线程,每个进程一次恢复。平台决定了边界在哪里,而边界几乎总是落在错误的地方。

当你追踪一个真实用户时,缝隙并不在框架划定的地方。用户不会因为他们的 TLS 连接超时而切换任务。他们只有在完成合同谈判并开始引导供应商入驻时才会切换任务。传输层的 session-id 和用户的心理任务边界是两件不同的事情,只是有时恰好重合,而你的产品正将连续性押在巧合之上。

Task-ID 必须是一等公民 —— 且是正交的

解决方法不是让会话存活得更久。长会话会积累无关的历史记录,如果你以后升级模型来修剪上下文,你无法决定保留什么,因为你从未记录过哪些内容属于什么。

解决方法是引入一个对用户有意义且与 session_id 正交的 task_id。会话仍然存在 —— 它们描述了一个连接、一个设备、一个进程的生命周期 —— 但它们不再是记忆的单位。任务才是记忆的单位,而会话只是恰好属于某个任务的一段活动。

具体来说:

  • 任务有一个用户理解的名称(例如“Acme 合同修订”,而不是 “conv-7e3f”)。用户显式命名它,或者你的智能体从第一轮开始建议一个名称并让用户接受或重命名。
  • 任务有一个工作集 (working set):构成其进行中状态的文档、决策、开放性问题和部分输出。这个工作集在不同会话之间是持久的,也是你构建恢复摘要的基础。
  • 会话有一个 task_id 外键。当用户在不同设备上重新连接时,平台会查找该用户的未完成任务,将其展示出来,并询问当前的会话接续哪一个任务 —— 或者允许用户显式开始一个新任务。
  • 智能体的续接 UX 是“你之前正在处理 Acme 合同修订 —— 从上次中断的地方继续吗?”并附带一段状态摘要。它不是“昨天的聊天”这种按时间顺序排列的滚动条,强迫用户重新推导他们当时在做什么。

ChatGPT Projects 是目前最接近这种正确形态的主流模式:项目是一个持久的容器,对话存在于其中,用户可以切换设备而不会丢失进度。它并不完美,但它证明了在会话之上建立任务抽象在消费级规模上是可行的。

重点并不是每个人都需要一个 Projects 克隆。重点在于你的数据模型需要在会话和任务之间建立一个显式的关联 (join),由你的团队拥有,暴露给你的评估套件,并在你的遥测数据中可见。如果你没有这种关联,你就不具备任务的连续性 —— 你有的只是侥幸。

跨设备连续性本质上是伪装的授权问题

一旦你致力于持久的、跨设备的任务,你为临时会话构建的鉴权模型就不再适用了。

当用户、设备和对话共存亡时,短期的会话绑定凭据是没问题的。契约是:此令牌授权此连接;连接丢失,令牌失效。但一个在不同设备间存续的任务需要不同的契约:此令牌授权从不同设备恢复此任务,可能没有原始 Cookie,可能是在重启后,也可能是在用户同时从另一个设备活跃连接时。

最近关于跨设备流的工作已将其推向了具体的协议领域。IETF 的 Cross-Device Flows BCP 草案正式确定了会话转移流,用户在“授权设备”上授权转移,然后在“消费设备”上使用会话,且跨边界保留状态。设备绑定会话凭据 (DBSC) 将会话 Cookie 绑定到设备持有的私钥,因此无法被静默窃取。这两点都很重要,因为它们告诉你,你的跨设备恢复将面临与鉴权协议领域一直在努力解决的相同约束 —— 只是现在你是在一个充满进行中智能体状态(包括草案合同和部分执行的工具调用)的持久任务库之上进行协商。

两个实际的后果:

  • 任务需要一个不同于会话鉴权的访问策略。谁可以恢复任务?仅限原始用户?委派的团队成员?在任何设备上,还是仅在注册设备上?你会被问到这些问题;如果你没有对它们建模,安全的默认设置是“拒绝”,而你的连续性就会消失。
  • 同一任务上的并发会话成为一个现实问题。如果电脑和手机都打开了该任务,谁的编辑获胜,智能体如何处理两路输入流,以及如何调和发散的工具调用?幼稚的答案是“最后写入者胜”;而用户信任的答案更接近于工作集上的 OT 或 CRDT 语义,这比大多数团队计划的工程量要大。即使在 UX 中承认冲突(“你的手机也在编辑这个 —— 你想接管吗?”)也比让模型产生两个发散的修订版本要好。

评估任务连续性,而非单轮质量

过去两年,Agent 的评估套件一直致力于优化单轮对话质量和单次会话的任务完成度。指标看起来很漂亮;但在生产环境中依然会发生失败,因为失败存在于会话之间的缝隙中,而不是会话内部。

最近的基准测试开始揭示这一点。MemoryArena 使用精心策划的多会话任务,其中包含相互依赖的子任务,早期会话中引入的信息会对后续会话施加潜在约束——这正是真实用户在不同设备间分散上下文时所创造的情境。据报道,即使相同的模型在单会话基准测试中表现良好,目前最先进的 Agent 记忆方法在这些任务上的成功率也低得惊人。MemoryAgentBench (ICLR 2026) 采取了类似的“一次注入,多次查询”的立场,并揭露了长程理解和冲突解决中的失败,而扁平历史评估(flat-history evals)永远无法捕捉到这些问题。

一个真实产品需要的评估准则:

  • 合成多会话追踪,包括设备切换、长达数小时或数天的空闲间隙,以及至少一条在间隙前引入、必须在间隙后召回的上下文。
  • 一个评分函数,用于对 Agent 生成的恢复总结(resumption summary)进行评分,而不只是评分最终答案。如果总结告诉用户“你正在处理 Acme 合同”,但默默遗漏了关于赔偿条款的约束,那么即使下一轮对话碰巧是正确的,Agent 也已经失败了。
  • 在模型升级时触发的回归测试,并端到端地重新运行多会话追踪。如果一次模型更换让单轮质量提高了两个百分点,但使恢复总结的保真度降低了十个百分点,这就是你无感推向生产环境的回归缺陷,因为你的评估套件从未测量过它。
  • 一种区分会话内推理错误与跨会话记忆错误的失败分类学。修复方法各不相同——前者指向提示词(prompt),后者指向检索和总结。

如果你的评估只针对单会话任务完成度评分,那么在框架抛弃你的那另一半用户体验中,你无异于在盲飞。

组织失效模式:基于聊天产品构建工作流产品

这里最常见的组织级失败是隐形的。产品团队认为他们交付的是聊天产品;而用户认为他们买的是工作流产品。用户在聊天界面之上叠加了一个工作流——长期运行的任务、跨越数日的周期、跨设备移交——而团队只有在模型升级或会话 ID 变更抹去了客户的工作集后,才能从流失访谈中了解到这一点。

一些迹象表明这种情况正在发生:

  • 你的支持队列呈现出一种周期性的模式:“当我切换到手机时,我的对话丢失了。”上次有人更改会话生成方式时,这类问题的数量可能有所增加。
  • 资深用户发送了长对话的截图,并在第一条消息中使用个人命名习惯,因为他们发明了一个你的平台拒绝提供的 task_id
  • 按会话长度排列,前 25% 的用户留存曲线接近持平,而其余 75% 的用户则不然——工作流用户找到了规避方法,而普通用户则选择了放弃。
  • 工程师们定期争论“我们是不是应该让会话永不过期?”这个问题不断出现的事实本身就是症状;病根在于会话(session)是错误的原子概念(primitive)。

架构上的认知是平淡无奇的。会话只是你的传输层为了追踪连接而需要的实现细节。任务才是你的用户真正关心的单元。将两者混淆所交付的 UX,会在用户切换标签页的一瞬间丢掉他们一半的工作成果,而你选用的框架不会将其标记为 bug,因为框架的契约止于会话边界。

在需要之前构建衔接机制

务实的操作顺序:

  1. 在本季度为你的持久层添加一个 task_id 列,即使 UI 尚未展示它。开始时将其与 session_id 进行一对一的回填。成本很低;其期权价值是其他一切的基础。
  2. 监测:记录用户在旧会话结束后 N 小时内开始新会话的情况。为了安全你可能已经在这么做了;现在为了产品也这样做。数据量会告诉你,你欠用户多少任务连续性。
  3. 发布一个最小化的延续 UX——“恢复此任务?”并附带一段 Agent 生成的一段话总结——作为功能开关向资深用户开放。观察他们重命名了什么、放弃了什么,以及在总结中纠正了什么。这些反馈就是你的评估集。
  4. 只有在有了鉴权模型后才添加跨设备恢复。不要将其硬塞进 session cookies 中;第一次发生令牌泄露时你会后悔的。
  5. 在下次更换模型之前,将跨会话评估加入你的发布准入机制。编写它们的成本是有限的;而恢复总结质量发生隐形回归的成本则是无限的。

那些做对这一点的团队不会为此大肆宣传。UX 只会感觉不那么生硬——Agent 会知道用户回来了,不会第三次索要合同草案,也不会默默假装早上的对话从未发生。没能做到这一点的团队将继续向聊天界面堆砌功能,并纳闷为什么留存率毫无波动。

无论如何,用户已经决定了工作的单元是什么。唯一的问题是你的平台是否承认这一点。

References:Let's stay in touch and Follow me for more thoughts and updates