跟大模型对话,出错是常态,不是异常。网络抖一下、生成超时、后端限流、用户中途点了停——这些每天都在发生。可我见过太多对话界面,一出错就甩用户一句红字「请求失败」,然后没了。气泡卡在半截,用户一脸懵:我刚才那句还算数吗?要重发吗?
我把项目里这块从头梳了一遍,分享下「出错态」该怎么设计。
先分清三种「没说完」
它们 UI 完全不一样,混在一起处理是大忌:
| 情况 | 触发 | 该给的反馈 |
|---|---|---|
| 用户主动停止 | 点了停止按钮 | 保留已生成内容,标「已停止」,给「继续」 |
| 网络/超时中断 | 连接断、读超时 | 保留半截,标「中断」,给「重试」 |
| 后端报错 | 4xx/5xx、限流 | 不留半截垃圾,整条标失败,给「重新生成」 |
第三种我特意不保留半截文本。报错时那点残缺内容往往是脏的,留着误导人,不如干净地标失败。
中断后,半截内容怎么处理
流式读到一半连接断了,我不会把已经吐出来的字删掉——用户都看到了,删了像见鬼。我的做法是把气泡定格在断点,底部加一行浅灰小字「回复中断」,旁边一个「重试」按钮。
try {
for await (const chunk of stream) appendToken(chunk);
} catch (err) {
if (err.name === 'AbortError') markBubble('stopped'); // 用户停的
else markBubble('interrupted', { retry: true }); // 意外断的
}
重试我做的是「重新生成这一条」,不是把用户的问题重发一遍——用户的话还在,重发等于刷出两条一样的提问,乱。
超时别用一个固定值
我一开始全局设 30 秒超时,结果长回复经常被自己的超时砍掉。后来改成「只要还在持续吐 token,就不算超时;连续 X 秒一个字都没来,才判超时」。等于把超时从「总时长」改成「无响应时长」,误杀少了一大半。
自动重试要克制
我加过一次「失败自动重试 3 次」,翻车了。后端限流时,自动重试等于火上浇油,越重越限。后来改成:网络类错误自动重试一次(带退避),业务类错误(限流、内容审核拦截)绝不自动重试,直接交给用户决定。
一个还没做好的地方
重试时的「上下文一致性」我处理得不干净。对话历史中间有一条失败了,用户重新生成,理论上后面的消息上下文都该跟着变,但我现在只重生成了那一条,后续没联动。算是个已知的半成品,先标在这。
后端这摊我偷懒了
错误态我抠得这么细,但模型本身没自己部署。用了个零代码就能配对话智能体、还能发布成带流式的 API 的平台,abort、错误码这些前端都接得上,我专心做交互。
模型层走**讯飞**的 MaaS,API 现成调,不用自建推理服务。
你们的重试 UI 是怎么设计的?尤其「重试时上下文怎么对齐」,有成熟做法的评论区指点下 🙏