跳到主要内容

那个教会用户永远不要打断智能体的中断 UI

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的流式智能体上的中断按钮点击率仅为 0.4%。产品团队看到这个数字,得出的结论是该功能正按预期运作——大多数生成内容不需要中断,实现方案没问题,发布吧,继续下一个任务。而实际的解读应该是,这个中断按钮教会了你的用户不要去按它。在使用产品不到一周的时间里,他们就发现按下停止键会丢弃已生成的片段、清除上下文,并把他们丢回到一个空的输入框中。他们学到的教训是:宁愿忍受一个糟糕的回答,也不愿冒着丢失整个对话脉络的风险。

这 0.4% 不是使用信号,而是厌恶信号。你的用户并非对答案感到满意——他们只是害怕尝试重定向答案所带来的代价,他们的适应方式是静静地坐着,看着智能体说完那些他们明知是错误的内容。工程团队将“停止生成”视为模型调用的取消。而用户将其视为“重定向,而非重启”。这两种定义从未达成一致,导致产品发布了一个在长对话中悄悄剥夺用户主动权的功能。

停止不是断路器,而是对话轮次

当你为智能体构建流式传输时,工程侧自然的心理模型是请求生命周期。请求发出,Token 流式输入,用户可以在中途取消请求,SDK 关闭连接。在这种框架下,“停止”是一个干净的技术原语:中止 fetch 请求,释放推理预算,将 UI 恢复到空闲状态。随后的实现忠实于这个原语——丢弃部分缓冲区,重置输入,大功告成。

用户思考的不是请求生命周期。他们思考的是对话。模型开始回答问题。说了三句话后,用户就已经看出答案的方向不对了——对技术栈的假设错了、范畴错了、语气错了,或者语言错了。当他们点击停止时,他们的意图不是“取消 API 调用”,而是“重定向——我有新的信息,应该重新塑造你接下来的话”。这两种意图共用同一个按钮标签,却有着完全不同的正确实现方案。

混淆这两者的产品,其发布的功能语义对一方是忠诚的,对另一方则是残酷的。用户按下停止键时期待的是对话的转折,就像他们对人说“事实上,等等,让我重新表述一下”那样。但他们得到的是一个空白框。下次当他们看到生成内容偏离轨道时,他们将不再按下停止键。他们会干坐着看下去。

用户如何学会你的原语代价

用户习惯的养成既快又无情。关于“横幅盲视”(banner blindness)的大量研究表明,用户在接触几次后就会学会过滤 UI 元素,特别是当该元素始终无法提供价值或始终带来伤害时。抹除上下文的中断 UI 就属于“伤害”一类,它们被过滤掉的方式与习以为常的广告位如出一辙——只不过这里的代价不是错过一次广告展示,而是你的安全原语已被用户自身的应对行为所瓦解。

如果你知道该看什么,就能从数据中读出这一点。无论生成长度或质量投诉如何,中断点击率随时间推移始终持平,这就是典型的特征。对话长度与中断使用率之间那种奇特的负相关也是如此——长对话在每个会话中会有更多回答错误的情况,理应机械地产生更多中断,但实际上产生的往往更少,因为处于长会话中的用户在重置上下文时损失最大,教训也最深刻。极少数新用户会按一两次按钮,被“烫”到后,便加入了大多数人的行列。

捕捉这一现象的指标不是点击率,而是“中断后的留存率”指标:在按下停止键的用户中,有多大比例在 60 秒内进行了下一次对话,又有多少比例在该窗口期内放弃了会话?0.4% 这个数字汇总了两个截然不同的群体——深知代价并愿意承受的极少数高级用户,以及按过一次就再也不回来的新用户。将他们合为一个数字汇报会掩盖伤害。

重定向通道:中断实际上应该开启什么

修复方案并不隐晦。它需要重新设计按钮的含义,并体现在从 UI 到状态、再到模型上下文的每一个层面。

当用户按下停止键时,到目前为止流式传输的响应不应被丢弃。它是对话状态,而非临时缓冲区。捕捉它。UI 不应让用户回到一个空的输入框。它应该开启一个通道,标记为类似“你希望换成什么?”的内容,并在其上方显示部分响应,作为被重定向的对象。用户输入他们的修正。发送给模型的下一轮对话包含该部分响应、用户的重定向指令,以及一条告诉模型将该部分响应视为初步尝试、并需按指定方向修改的指令——而不是将其视为需要做出反应的最终答案。

这更接近人类对话处理中断思路的方式。当有人打断你时,你不会删除自己说了一半的话。你接受他们的修正,并从发生重定向的地方继续。当你给智能体模型提供结构化的上下文时,它能很好地处理这种情况;失败从来不在模型,而在于 UX 层丢弃了下一轮对话唯一需要的信息。

一些模式可以让这种重定向通道在实践中发挥作用:

  • 部分响应以略微淡化的状态呈现在重定向输入框上方,并附有类似“在此停止”的标签。用户可以阅读它、指着它,或者在修正中引用其中的部分内容。
  • 重定向输入框的占位符与常规输入框不同——是“告诉我需要修改什么”而不是“问一个问题”——因为两者的认知任务不同。
  • 下一轮助手响应被呈现为同一线程的延续,而不是对新提示的新响应。对话树将部分响应、重定向和新响应显示为一个连接的序列。用户可以看到他们没有丢失任何东西。
  • 在重定向生成期间第二次按下停止键,会再次执行同样的操作。尝试引导的代价永远不会重置为“丢失上下文”。

当产品表现出这种行为时,中断点击率会上升——有时甚至会提高一个数量级——长对话中的用户满意度也会随之提升。这种增长不是退步。而是用户终于相信这个原语能实现他们的意愿。

重构背后的架构承诺

这不只是 UI 上的微调。它要求对话状态成为事实之源(source of truth),并将请求生命周期视为其派生视图。

大多数流式智能体(streaming agent)的实现都搞反了。获取请求(fetch request)与助手消息是耦合的——消息仅作为成功流的接收端而存在。当流被取消时,消息是不完整的并会被丢弃,因为数据模型中没有位置来存放“助手这一轮对话存在但只是部分内容,且用户紧接着说了别的内容”这种状态。修复 UX 需要数据模型承认这一状态。

持久层需要支持处于“部分——已取代”状态的助手消息,并保留部分内容、中断时间戳以及指向随后用户重定向轮次的链接。流处理层需要在关闭连接之前,而不是之后,将部分缓冲区内容刷新到该持久消息中。延续提示词(continuation prompt)需要将部分响应作为结构化上下文包含在内,并与之前完成的助手轮次区分开来,以便模型知道它应该修改而不是仅做出反应。追踪流水线(trace pipeline)需要将该部分内容记录为与重定向延续相关联的一等 Span,以便调试时显示实际的对话形态,而不是之前被取消调用的空洞。

这些改动在孤立状态下都很小。但它们通常集体缺失,因为团队将中断视为传输层(transport-layer)的问题,而非对话层(conversation-layer)的问题,导致数据模型继承了实现初期所在的层级。优先构建对话模型并将传输视为细节的团队,几乎可以毫不费力地获得重定向路径(redirect lane)。先构建传输层并试图稍后改进对话模型的团队会发现,其技术栈的每一层都假设“取消意味着丢弃”,而改造则变成了一个没人预算过的、长达数周的项目。

一个能检测到无声训练的指标

如果你从此文中只能带走一样东西,请带走这个指标。中断按钮的点击率是一个掩盖伤害的虚荣指标。

用类似这样的指标来代替:在给定会话中点击停止的用户中,有多少比例会在 60 秒内发送另一条消息?在发送消息的用户中,有多少比例的下一条消息看起来像是重定向(引用之前的回复,包含纠错信号),而不是一个无关的新问题?对于那些点击停止后不再返回的用户,与从未点击停止的用户以及点击并继续的用户相比,他们的七日留存率如何?

第一个比例告诉你是停止功能在起重定向作用,还是成了流失诱因。第二个比例告诉你的延续提示词是否真的将部分内容视为上下文,还是用户无论如何都得重复自己的话。第三个比例告诉你点击停止带来的糟糕体验是否渗透到了整体的产品流失中——这是当点击率无法引起关注时,最终能引起关注的财务数字。

一致采用该指标的团队都会看到相同的规律:健康的打断体验会产生较高的“60 秒内重定向率”以及大致中性的七日留存率。而有缺陷的体验则会导致以流失为主的分布,且七日留存率低于从未尝试打断的用户。这种缺陷状态比人们预想的更常见,即使在资金充足的团队中也是如此,因为没有人设置指标来捕获它。

更深层次的教训

中断按钮是一个暴露了重大承诺的小切面。如果你的数据模型将对话视为持久存在的事物,而将传输视为填充对话的一种方式,那么中断路径就是对已保留部分上下文的系统的自然增补。如果你的数据模型将请求视为持久存在的事物,并将对话视为完成请求的副产品,那么中断路径就要求你反转持久层的极性才能添加。

这两种架构之间的选择通常是在项目的第一周,由一名并未考虑三个月后用户是否能重定向流式响应的工程师隐式做出的。这种隐式选择变成了与用户之间关于产品支持何种代理能力的契约,而契约则通过用户从系统暴露的原语中所习得的预期来执行。

一个没有重定向路径的流式智能体正在教导每一位用户:打断是危险的。0.4% 的点击率正是这一教训深入人心的体现。修复方法不是把按钮做大、做得更显眼或换个颜色。修复方法是让点击按钮能够保留用户最初使用产品时追求的东西:对话。

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