跳到主要内容

流式 Token 是无法收回的承诺

· 阅读需 10 分钟
Tian Pan
Software Engineer

模型已经向用户屏幕推送了 70% 听起来很自信的回答。接着,它即将进行的工具调用返回了错误、无结果或 429 错误。现在你必须在两种损失之间做出选择:让模型通过编造剩余部分来优雅地结束,或者在句子中间戛然而止,且没有体面的方式撤回。这两种都不是修复 —— 它们都是损害。

这是流式传输 UX 中没人考虑过成本的部分。流式传输被描绘成一种感知延迟的胜利:首个 Token 时间 (TTFT) 是核心指标,用户更早开始阅读,应用显得充满活力。但这种描绘忽略了你推送的每一个 Token 都是一种承诺。你发布了一个你还不知道是否正确的答案草稿,而你系统的后半部分还没有运行完毕。当它运行结束并产生分歧时,你的 UI 没有原生方法来撤回已经显示的内容。

这里标准的可靠性本能是“快速失败”。及早检测到错误状态,抛出错误,停止请求,让调用者决定。这种本能假设失败发生在用户看到任何内容之前。但在流式传输中,用户已经看到了前三段。现在的“快速失败”变成了用户已经处于失败状态中的响亮宣告。选择不在于失败还是不失败 —— 而是在于让失败显而易见,还是以一种用户可能察觉不到的方式失败。

“流式传输” 到底是如何推销给你的

推销 Token 流式传输的理由几乎总是延迟。即使总时长相似,400 毫秒的首个 Token 延迟加上每秒 40 个 Token 的速度,在感觉上比 4 秒的批量响应要快得多。用户得到了事情正在发生的持续信号。他们开始阅读。在产品术语中,感知速度就是实际速度。

这种说辞遗漏了两点。首先,Agent 模式稀释了这种优势。当最终答案是多个链式 LLM 调用和工具调用的结果时,只有最后一次生成是流式传输的 —— 中间步骤是不可见的等待。流式传输只在最后一跳为你争取到了渐进式输出,之前则毫无用处。其次,也是本文的主题,流式传输改变了系统的失效模式,而延迟框架从未提及这一点。你用一个干净的失败界面换取了更快的感知响应。

在批量响应中,失败的工具调用只是你的处理程序返回给客户端的一个错误代码。用户会看到一个优雅的“出错了”状态。系统从未处于表达任何内容的过程中。而在流式传输响应中,模型一直在叙述通往答案的路径,而这个答案取决于它尚未接收到的数据,失败发生在叙述开始之后。错误界面不再是响应开始时的一个清晰界限 —— 而是一个用户已经阅读了一半的段落。

流式传输无法处理的三种失败模式

第一种也是最常见的是“结束时的截断工具调用”。模型处于流传输中途,决定调用一个工具,流在工具调用完成前达到了硬限制,客户端库静默丢弃了部分工具调用。用户看到了一个指向从未执行动作的自信引导。Agent 框架知道响应是不完整的;用户只知道原本该发生的事情没发生。

第二种是“叙述后的工具失败”。模型已经流式输出了“我会为你提取最新的发票并汇总分项”,而当它尝试实际提取发票时,API 返回了 500。自然语言的承诺已经显示在屏幕上。系统无法兑现。无论客户端接下来做什么 —— 错误横幅、重试、静默停止 —— 在用户的心理模型中都会读作一种矛盾。模型承诺了发票,但发票并不存在。

第三种是 Agent 永远看不到的“缓慢降级”情况。下游服务虽然有响应但性能下降:陈旧的数据、部分结果、预示即将失败的速率限制头。模型没有原生渠道来获取人类用于感知的运行元数据。它在服务质量下降的情况下流式输出自信的内容。这种失败并不响亮,也不会停止流。它悄无声息地污染了答案。

在这三种情况下,问题是一样的:流式传输在系统组装好答案所需材料之前,就已经承诺了答案的形式。流式传输是一份关于节奏的契约。节奏跑在了真相前面。

赎回选择权的模式

坦率的解决办法是减少流式传输,并审慎地决定响应的哪些部分在何时进行流式传输。没有什么巧妙的构思能让不可逆的输出变得可逆 —— 只有设计模式能延迟不可逆性,直到你拥有足够的信息来做出承诺。

延迟流式传输直到工具调用解析完成。 最直接的模式:在规划和工具调用阶段完全不进行 Token 流式传输。显示一个明确的“正在思考”或“正在查找”指示器,完整运行工具调用,验证结果,然后开始流式传输合成后的答案。你放弃了工具调用环节的 TTFT,而这个环节原本就是流式传输价值最低的地方 —— 用户本来也不会去读工具调用计划。你在用户预期等待的地方支付延迟。你在用户预期流畅的地方进行流式传输。

带有可隐藏部分的结构化响应。 将响应视为一个 Schema,而不是一个段落。立即流式传输不依赖工具结果的部分 —— 前言、问题的重述、框架。为绑定数据的内容保留一个部分,并且仅在输入解析后才渲染它。如果工具调用失败,该部分永远不会渲染;周围的响应会优雅地忽略它。用户读到的是一个连贯的答案,只是其范围恰好限定在了实际已知的内容。这就是许多设计良好的 Agent UI 悄悄在做的事情:可见的“答案”是一个结构化对象的渲染,而不是原始的 Token 流。

用户知道并非最终结果的显式思考阶段。 在最终答案之前展示其推理过程的模型具有一种天然的宽容度:用户已经理解思考文本不是答案。思考块中的任何内容都可以是错误的、不完整的或经过修改的,而不会破坏契约 —— 契约在于后续的答案。在你的 UI 中使用相同的宽容度。一个标记清晰的“草稿”或“检查”环节是被允许出错的,因为你已经告诉用户它是暂时的。在退出该环节之前,你没有承诺任何事情。

延迟渲染与渐进式披露。 将 Token 流式传输到用户看不到的缓冲区,并在确认每个块的依赖项后分块显示。模型仍然全速流式传输。用户仍然能看到屏幕上出现的 Token。但渲染层会拦截任何后端工具调用尚未完成的块。代价是每个依赖边界会产生一个往返的感知延迟。好处是用户永远不会读到由失败调用支撑的句子。

这些都不是免费的。每一种都是用延迟换取诚实。问题不在于是否进行这种交易,而是在响应的哪个部分进行交易。

成本-延迟-真实性三角

在为了安全而缓冲与为了速度而流式传输之间存在着切实的张力,这种张力无法被轻描淡写地略过。缓冲会增加 TTFT。TTFT 的增加会提高跳出率并降低感知质量。流式传输降低了 TTFT,但当下游调用失败时,会增加用户可见的不可逆性的概率。这里不存在绝对更优的选择。

你能做的是停止将这个选择视为二元对立。大多数响应并不需要每个部分都进行流式传输。一个合理的默认方案如下:流式传输对话式的开场白和框架,在工具调用运行期间,将数据绑定部分缓冲在“思考中”指示器之后,然后在实质上安全的情况下,再流式传输综合后的答案。用户的 TTFT 计时从开场白开始。他们的耐心消耗在一个明确标识的等待中。他们阅读最终答案时是流式速率,因为当你开始流式传输时,答案已经组装完成了。

一个有用的诊断方法:对于典型响应的每个部分,询问如果支持该部分的工具调用在流式传输开始后失败了会发生什么。如果答案是“用户会读到一个自信的谎言,直到我们切断流媒体”,那么该部分就不应该进行流式传输。它应该被缓冲,直到其输入准备就绪。

流式传输 UX 是关于错误的契约

思考流式传输 UX 最清晰的方式是:它不是一个性能特性,而是一份契约。你在告诉用户:我对即将说的话非常有信心,以至于我会边想边将其呈现到你的屏幕上,并且我接受了无法撤回的约束。响应的不同部分对应着不同级别的信心。对用户问题的重述是可以安全流式传输的——你已经知道了。而依赖于你尚未运行的查询的答案则不然。

好的流式传输 UX 主要是关于你决定哪些错误是无法撤回的选择。你流式传输的每个部分,都是你为了换取速度而接受了不可逆性的部分。错误不在于流式传输本身——而在于统一地进行流式传输,就好像响应的所有部分都具有相同的风险形态一样。事实并非如此。开场白和数据是不同的承诺。它们值得拥有不同的渲染策略。

下一次干系人询问为什么你的 Agent 感觉比 Demo 慢时,诚实的回答有时是:因为 Demo 在确定能否交付之前就做出文本承诺,而你决定不这样做。Demo 的 TTFT 更好。但当调用失败时,你的方案不会撒谎。请挑选你的用户真正想要的那一个。

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