跳到主要内容

对话分支作为一等公民:为什么线性线程迫使用户不断杀死并重启对话

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的聊天产品需要分支功能的最明显信号,往往也是最容易被忽略的:用户不断地将旧对话复制粘贴到新的会话中。他们不是在更换供应商,也不是因为无聊。他们是在尝试询问“如果我早点反驳那个假设会怎样?”,同时又不想丢失花费了 40 轮对话建立的上下文。线性线程只为他们提供了两个选项——要么覆盖下一条消息并丢失原始回复,要么开始新聊天并丢失前置内容。于是,他们利用剪贴板发明了第三种方案。

每当用户这样做时,你的产品就在通过这种变通方法泄露一个功能需求。这个变通方法很糟糕:它剥离了消息元数据,破坏了工具调用(tool-call)的联动,丢失了文件附件,并产生了不再映射到连贯任务的孤立线程。但它之所以存在,是因为另一种选择——放弃花费 30 分钟建立的上下文——更糟糕。对话在结构上是一棵树,而 UI 却坚持它是一个列表。用户只能手动填补这个鸿沟。

将分支作为一等公民,意味着像版本控制系统处理分支那样对待发散:将其视为一种保存历史、支持并行探索并允许合并回来的正常操作。OpenAI 在 2025 年末在 ChatGPT 中以“在新聊天中分支”的形式发布了此功能。Claude Code 将对话存储为消息的 DAG(有向无环图),其中编辑会创建分支而非覆盖。LangGraph 检查点将 fork_at(checkpoint_id) 作为原语暴露。这种模式正在趋同,因为线性线程的抽象一直是有损的——只是经过几年的使用数据,这种损失才变得不可否认。

线性线程迫使的三种失败模式

线性聊天 UI 将三种截然不同的用户意图压缩到了同一个 UI 动作中。当用户想要改变方向时,他们会编辑最后一条消息。但“改变方向”隐藏了至少三种不同的需求,每种需求都值得一个不同的状态转换。

第一种是纠偏(course correction):用户认为模型理解有误并想要重新陈述。原来的回复不再需要;覆盖是可以的。第二种是替代探索(alternative exploration):用户得到了一个合理的答案,但想看看不同的表述会产生什么结果——两者都有价值。第三种是回滚到分支点(rollback to a fork point):用户意识到十轮对话前他们应该给出不同的约束,现在想在这些约束下重试后续的整个对话,同时保留原始分支作为参考。

在线性线程中,这三者看起来完全一样:用户点击消息上的编辑并重写它。系统无法区分“丢弃剩余部分”和“两者都保留”。大多数产品默认选择丢弃,因为保留会产生线性 UI 无法表示的导航问题。想要“两者都保留”语义的用户如果不离开产品就无法实现。

这种成本表现为重复劳动。进行对比分析的研究人员会打开同一个聊天的四个浏览器标签页。测试语气变化的营销人员会开启新的会话,并重新将简报粘贴到每个会话中。调试多步骤计划的工程师要求模型“回到第三步”,然后眼睁睁地看着模型编造之前的上下文,因为模型引用的对话已经被更改了。分支模式从底层涌现——虽然笨拙、摩擦力大,且模型无法提供帮助,因为每个分支都存在于不同的会话中。

分支时复制(Copy-on-Branch)是正确的状态模型

大多数团队在第一次尝试时犯的错误是将分支视为对话的深拷贝。在用户开始频繁使用分支之前,这还能应付,但随后存储成本和更新语义就会成为问题。一个拥有 60 轮对话的用户如果在第 50 条消息处创建了 5 个分支,不应该为 300 条消息的存储付费,也不应该在共享前缀之间出现任何不一致的情况。

正确的模型是带有结构共享的分支时复制(copy-on-branch with structural sharing):消息是不可变的,分支是指向 DAG 的指针,共享前缀在磁盘上只存在一份。这正是使 Git 具备可扩展性的核心洞察。分支不是树的副本;它是指向某个提交(commit)的新引用(ref),而提交是追加式图中的内容寻址节点。转化为聊天场景:每条消息都是一个带有父级指针的节点,分支是叶子引用,用户看到的“对话”是在读取时从根节点到叶子节点重建的一条路径。

这使得几个原本昂贵的操作变得廉价。分叉(Forking)的时间复杂度是 O(1)——你只需分配一个指向分叉点消息的新叶子引用。切换分支只是指针更改,而非副本制作。对比两个分支变成了路径之间的树差异对比(tree-diff),这对于向用户展示“对话是在这里分叉的”非常有用。垃圾回收变成了可达性分析:没有叶子引用指向的消息是可以删除的,但绝不能静默删除——它们是某人的历史。

一个不明显的益处是,这种模型使模型在各个分支中的视图保持一致。在每个分支中,共享前缀是完全相同的字节序列,因此 KV 缓存(KV cache)保持热状态。如果你使用前缀缓存来处理流量,只要用户停留在最近的分叉点附近,推理时的分支成本几乎为零。天真的深拷贝实现会丧失这一点——尽管前缀字节在技术上是相同的,但缓存键不同,因此每个分支在第一轮对话时都要支付冷启动税。

UI 问题比存储问题更难

将存储视为 DAG(有向无环图)已是成熟的解决方案。但向用户展示 DAG 则不然。对当前对话分支现状的坦率评估是,几乎每个实现都准确把握了存储模型,却在 UI 上处理得一塌糊涂。ChatGPT 的“在新对话中分支”规避了可视化问题,通过将分支推送到侧边栏的独立顶层条目中,以牺牲视觉上的父子关系为代价,维持了线性线程的假象。Claude Code 的分支导航功能尚可,但隐藏在一个大多数用户从未发现的小切换开关后面。像 tldraw 的分支对话模板和 Canvas Chat 这样的工具则走向了另一个极端,将树状结构在画布上进行空间布局,这非常适合探索,但对于心理模型仍停留在垂直滚动“聊天”的用户来说,会感到迷失方向。

目前还没有定论,但一些模式正在显现。行内分支指示器 —— 在消息上显示“1/3”的小部件,表明它有替代兄弟节点,并带有左右切换的箭头 —— 在单个分叉点只有两三个分支时效果很好。当分支超过五个,或者分支本身又有分支时,它们的扩展性就很差。侧边栏树状视图的扩展性更好,但会与主聊天争夺注意力,且往往被忽略。空间画布对高级用户的扩展性最好,但需要完全放弃聊天界面的直觉,这比大多数团队愿意做的 UX 赌注要大得多。

务实的折中方案是:默认采用行内指示器;为在单次会话中创建了三个以上分支的用户提供树状视图抽屉;并为分支成为主线的情况提供一个明确的“将此分支提升为新的顶层对话”的逃生通道。无论你发布什么,都要明白一点:在观察用户实际使用后,你肯定会重做一遍。分支功能会揭示线性 UI 所掩盖的工作流模式,而这些模式与你的设计师在白板上画出的内容并不相符。

合并是难点 —— 在 1.0 版本中跳过它

没有合并的分支是有用的。没有分支的合并是逻辑不通的。因此,一旦发布了分支功能,自然而然的下一个需求就是:“我能否将分支 A 的结论和分支 B 的数据结合到第三个线程中?”答案最终应该是肯定的,但实现起来比分支要难得多,大多数团队应该有意识地推迟它。

核心问题在于对话消息是不满足交换律的。两个并行分支包含的助手响应都引用了彼此的前缀 —— 分支 A 的第 52 条消息是以模型未看到分支 B 的第 51 条消息为前提的,反之亦然。简单地交错消息进行合并会产生一个“科学怪人”式的线程,模型无法从中进行有意义的延续。模型会看到不一致的自我引用,要么臆造出一个统一的历史,要么发出一条困惑的响应要求澄清。

可行的方法都涉及某种形式的合成,而非简单的拼接。基于摘要的合并:让模型生成分支 B 结论的摘要,将该摘要作为系统或用户消息注入分支 A,并从那里继续。选择性提取:让用户从分支 B 中挑选特定消息,作为引用的上下文复制到分支 A 中,并带有明确的框架说明(“从一个并行的探索中,模型得出结论……”)。带有合成功能的三路合并:将两个分支的结论都展示给模型,并要求它生成一个合成结果作为新的一轮对话。这些都不是 Git 意义上的真正合并;它们都是包装在合并 UI 下的受控上下文注入。

Forky 和 ContextBranch 正在实验语义化的三路合并,但这项技术仍处于研究阶段。对于大多数团队来说,正确的决策是发布不带合并的分支功能,观察用户如何通过在分支间复制粘贴来模拟合并,并让这些工作流来引导真正需要的合并原语。通常,它仅仅是“将这条消息复制到那个分支” —— 这比真正的合并功能要小得多。

昨天就该实现这一功能的预兆

诊断性问题不是“用户想要分支吗?” —— 如果你问,他们会说想,但他们对大多数功能都会说想。诊断性指标是:用户有多少次使用明显来自另一个对话的文本来开启新对话? 那些以“早些时候你说过……”或“在我们之前的聊天中,我们确定了……”开头的粘贴块,就是用户因为产品在会话内不提供分支操作,而跨会话边界手动模拟的分支操作。

监控这一点很简单。寻找那些首轮用户发言超过一定长度阈值(比如 500 个 token)且包含对模型第二人称称呼的新对话。寻找与同一用户最近的其他会话共享不顺眼专有名词或命名实体的会话。寻找首条消息包含“接续”、“正如我们讨论的”或“在之前的对话中”等短语的会话。每一个这样的例子都是一个漏洞 —— 一个你的产品几乎支持但实际并未支持的工作流。

另一个信号是先编辑后后悔。用户编辑了之前的消息,对话覆盖了原始分支,几分钟内,用户又将看起来像之前的助手响应的内容作为上下文粘贴回聊天中。他们正在凭记忆重建丢失的分支。如果这种情况频繁发生,说明你的编辑消息 UX 正在摧毁用户的劳动成果,并要求他们手动重建。

将这两个信号视为对话模型中的高优先级 Bug,而不是功能需求。用户正在做正确的事 —— 通过他们看重的上下文探索不同的路径 —— 而产品却在强迫他们以错误的方式去做。解决方案不是更好的编辑对话框或更智能的“你确定吗?”警告。而是承认对话不是一个列表,并重构数据模型,以便 UI 最终能够展示用户一直试图实现的操作。

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