用户输入“其实,我的意思是五十,而不是十五”,点击最后一条消息上的铅笔图标并进行编辑。UI 表现得非常出色:它向用户展示修改后的消息,淡出旧消息,将助手过时的回复变为带删除线的“幽灵”状态,并呈现一段流畅的对话,读起来就像最初的错误从未发生过一样。用户心满意足地发送了下一轮对话。而智能体却用“十五”进行了回答。
Bug 不在模型身上。模型准确地接收了服务器发送的内容,而服务器发送的是:原始消息、原始助手回复、撤回动作、修改后的消息以及新的请求——所有内容按顺序拼接在一起,实时发送。用户在进行一场已经编辑过的对话。而智能体在进行一场从未被编辑过的对话。两份对话记录在第三轮开始分叉,此后再未统一,之后的每一轮对话都在为这一差距支付“利息”。
UI 是树,存储是日志,而没人告诉模型
当团队构建聊天产品时,有两个设计决策是在不同的房间里完成的。前端团队将对话设计为用户导航的对象:一串气泡,每个都可编辑、可重试,每个都有一个小小的“重新生成”按钮,静默地分支出一个同级回复。这就是一棵树。每一次编辑都会创建一个分支。每一次重新生成都会创建一个同级节点。UI 将树中的一条路径投影为可见对话,并将剩余部分隐藏在交互入口之后,如果用户愿意,可以重新进入这些路径。
后端团队则基于不同的直觉,将对话建模为事件日志。每条用户消息都是一次追加(append)。每条助手回复也是一次追加。每一次编辑同样也是一次追加——被记录为 message.v2 并排在 message.v1 之后,而不是对 message.v1 的修改。对于审计、流式处理、数据分析和回放来说,这是正确的选择。但对于 Prompt 构建来说,这是错误的选择,而且直到模型开始回答用户根本不记得问过的问题时,才有人察觉到这一点。
这种差距是无声的。前端保持了用户心理模型的一致性。后端保证了数据仓库的真实性。中间的 Prompt 组装器——即那个将对话转换为 messages 数组的函数——按照时间顺序拼接日志,因为 Schema 就是这样提供数据的,而模型则尽职尽责地根据修改前后的时间线合成一个连贯的回复。大模型的流畅性使得这种失败难以察觉:不太流畅的模型会因为矛盾的上下文产生胡言乱语,Bug 在第一周就会暴露。而尖端模型生成的回复读起来很正确,只是回答了错误的问题。
从业者不断重新发现的失败模式
这种 Bug 有多种形态。第一种形态是未生效的编辑 。用户将“十五”修改为“五十”,UI 接受了,但助手的下一轮回复仍引用“十五”,就像用户坚持要用这个词一样。用户再次编辑,语气愈发强硬。智能体再次引用,表现得愈发自信。双方都在根据各自看到的输入进行正确的推理。只是输入内容并不一致。
第二种形态是幽灵助手消息 。用户在第三轮点击了“重新生成”,因为之前的回复不对。UI 替换了该回复。日志追加了一个同级节点。在第四轮中,模型的上下文包含原始助手回复、重新生成的回复以及用户第四轮的后续。由于后续内容与重新生成的回复一致,却与原始回复矛盾,模型会选择一个锚点进行回答,通常是较早的那个,因为它更接近用户的原始表述。对话现在引用了一个用户从未见过的回复。
第三种形态是未回滚的回滚 。用户使用了 UI 的“返回到第二轮并从此继续”功能,重写了第三轮,并基于新分支又进行了四轮对话。服务器将其建模为追加,日志中仍然保留着旧的第三至第七轮。如果 Prompt 组装器遍历的是日志而非树,模型就会看到全部十二轮对话:用户在平行宇宙中的分身继续了那个被放弃的线程,而智能体将两个时间线视为一体进行推理。
第四种形态是隐形 Token 税 。即便模型的回复保持连贯——可能是因为最新的用户消息足够清晰,足以覆盖之前冲突的轮次——但每一条保留下来的编辑前消息都在为后续的每一次调用支付 Token 租金,而且是永久性的,尽管用户认为他们已经删除了这些内容。在大规模运行时,这是一项可衡量的支出项,但由于没人意识到它的存在,也就没人将其归因于此。
为什么“只需遍历活跃分支”是一个知易行难的答案
一旦诊断明确,修复方案听起来就很显而易见:遍历树,而不是日志。将用户在屏幕上能看到的消息发送给模型,而不是数据库恰好拥有的消息。难点在于将“用户能看到的消息”转变为一个真实的、可查询的服务器端概念,而非前端的一种隐式计算。
这意味着服务器需要将对话树视为一等公民(first-class object)。每条消息都有一个父指针。一次编辑会产生原始用户消息的一个同级节点,并被标记为活跃同级节点。一次重新生成会产生原始助手回复的一个同级节点,并被标记为活跃。一次回滚将对话的“头节点”(head)重新指向树中较早的节点,将放弃的分支作为数据保留,但不作为实时上下文。然后,Prompt 组装器从根节点遍历到头节点,在每个分叉点遵循活跃同级节点指针,并准确地将该序列输出为 messages 数组。
这是严肃对待树结构的逻辑分支聊天系统所采用的架构:每条消息都是一个带有父节点的节点,编辑和重新生成是分叉而非覆盖,用户看到的线性视图是每个分叉点选择一个分支的投影。日志在底层被保留,用于审计、回放和分析。模型看到的是投影。这两个视图最终在“正在进行什么样的对话”上达成了一致。
从日志迁移到树并非没有成本。现有对话需要重新解析:旧的 message.v1 / message.v2 对变成了父子或同级关系,而“活跃”指针需要从没人明确记录的前端行为中推断出来。流式传输、部分回复以及多个标签页的并发编辑都变成了树的状态转移而非简单的追加,这意味着同步模型会变得更加复杂。但这都不能成为逃避这项工作的理由——对于那些将树结构追溯到成熟产品中的团队来说,应当预期需要一个季度的时间来进行细致的迁移,而不是一个下午就能搞定的。
将编辑视为显式状态,而非无声的重写 这种修复方案还有一个更微妙的版本,它不需要进行完整的树结构迁移。这个方案值得一提,因为它能以更小的改动为你带来大部分价值:将编辑视为告知模型的一项信息,而不是让模型去推测的事情。
当用户编辑之前的消息时,提示词组装器(prompt assembler)既可以假装原始内容从未发生过(即上述的树遍历方法),也可以在对话记录中保留这两个轮次,并注入一条系统级的注释:“用户在第三轮将其消息从 X 修改为 Y;请将 Y 视为当前请求,并将 X 视为已废弃。” 这样,模型就拥有了显式推理更正信息所需的数据,而不是从一个它并不知道是矛盾的冲突中进行合成。
这种方法更加繁琐——它增加了注释所需的 token,并且依赖于模型遵循“已废弃”指令。但它有一个巨大的优势:它在上下文本身中保留了审计轨迹。当编辑是为了澄清而非反驳时,这非常有用。“我的意思是 API 端点,而不是数据库表”是模型可以利用的一种细化信息;假装用户从未说过“数据库表”会丢失信息。正确的选择取决于产品,而这里的原则是显式地做出决定,而不是直接发布提示词组装器偶然产生的任何行为。
能捕捉到该问题的评估与无法捕捉到的仪表盘 标准的评估套件(eval suite)无法捕捉到这种“对话树 vs 日志”的 bug,因为每个评估案例都是单一的线性记录。模型接收一个序列并生成响应,响应根据预期答案进行评分,而流水线中没有任何环节会测试当用户认为发送的序列与模型实际收到的序列不一致时会发生什么。
要捕捉这一点,需要围绕编辑和重新生成(regenerate)事件设计对抗性的多轮对话场景。一个现成的例子:用户要求使用参数 X 进行计算,助手回答,用户编辑原始轮次改为使用参数 Y,然后用户发送后续消息。评估根据智能体(agent)使用的是 Y 还是 X 来对其响应进行评分。另一个例子:用户重新生成了一个响应,然后提出了一个仅针对该重新生成版本才有意义的后续问题。评估将判定智能体是回答了用户实际提出的问题,还是对这两个响应进行了某种幻觉式的合成。
仪表盘方面则更难。直觉通常是将“单次对话的编辑次数”记录为指标并观察其趋势,但你真正需要的指标是“后续助手轮次引用了编辑前内容的编辑次数”,这是一种质量评估,需要 LLM-as-judge 流水线或由智能体自身进行的结构化标注(例如:“我正根据用户在 {turn_id} 中修改后的请求进行回答”)。对话中出现编辑是正常的。编辑前的内容持续出现在编辑后的响应中才是 bug。
第三个值得构建的工具是对话记录回放工具(transcript-replay tool),它可以根据请求向用户展示模型在特定轮次中看到的准确 messages 数组。这是一种虽不华丽但非常有用的内部工具,当客户第一次投诉“机器人忽略了我的更正”时,它的价值就会体现出来——团队可以回放对话,查看提示词中冲突的上下文,然后修复组装器,或者向客户(或团队内部)解释实际发生了什么。如果没有这个工具,每一份此类报告都是不可证伪的,产品团队最终会陷入关于模型质量的争论,而 bug 其实存在于他们自己的数据层中。
对话是用户正在导航的一棵树 这一切背后的架构认知是:聊天对话并非事件日志。它是一棵用户正在导航的树,而导航选择——编辑、重新生成、回滚、分支——是第一类状态转换(first-class state transitions),而非元数据。那些将 UI 构建为树状结构但将存储构建为日志形式的团队,实际上发布了一个与其用户正在进行的对话完全脱节的智能体,而且随着产品迭代不断增加新的修改方式,这种差距会进一步扩大。
修复在技术上并不困难。修复的关键在于组织架构:前端负责设计对话交互方式的人员和后端负责设计持久化模型的人员,必须对“什么是对话?”这个问题达成一致。如果他们没能达成一致,中间的提示词组装器就会变成这种分歧泄露到模型上下文窗口的地方,而症状则表现为一个自信地回答了用户以为已经撤回的问题的智能体。
如果你在 2026 年发布聊天产品,请在用户发现之前审计这种差距。随机抽取一段包含编辑行为的对话,回放组装器为下一轮构建的提示词,并检查模型看到的对话是否与用户认为的一致。如果这两个记录存在偏差,问题就不再是用户是否注意到了——而是已经有多少比例的用户因此放弃了你的产品。
会员专享
余下内容仅对会员开放。 会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
— 完整文章,包含未公开存档的部分 — 可落地的工作框架,附带权衡与决策依据 — 新文章抢先看,先于公开发布 登录以继续阅读→ 随时取消 · 一次订阅,畅读全部