AI应用后端工程(03):回答输出一半就断了:AI 流式接口如何定义成功与失败?

目录

[0.为什么状态码是 200,回答仍然会失败?](#0.为什么状态码是 200,回答仍然会失败?)

1.连接关闭,为什么也不能说明回答完成?

2.已经显示的半段回答,该不该保存?

3.输出了一半,再自动重试不就好了?

4.后端应该怎样定义"一次成功生成"?


Hello,大家好,我是 1M55!👋

前两篇我们聊了流式输出,也聊了用户点击"停止生成"后,请求会怎样结束。这次换一个更棘手的情况:用户没有点停止,回答却自己停在了半句话。

假设用户问:"帮我分析这段代码为什么会超时。"页面已经显示:

这段代码的主要问题在于,每次请求都会重新创建......

然后就没有下文了。

从用户角度看,这次回答显然没有完成。但如果打开开发者工具,可能会发现请求的 HTTP 状态码是 200。后端日志里也可能只记着"接口返回成功"。

问题来了:一段已经发出去、却没有正常结束的回答,到底算什么?

这个问题看起来是异常处理,实际会牵出流式协议、重试、会话数据和系统状态的设计。

0.为什么状态码是 200,回答仍然会失败?

普通接口通常先完成业务处理,再一次性返回结果。如果业务处理失败,后端还有机会返回 500,告诉调用方这次请求没有成功。

流式接口的时间顺序不同。

后端先发出响应头,随后不断发送响应体。用户看到第一段文字时,这次 HTTP 响应已经开始了。此时模型服务如果突然断开、后端读取上游时抛出异常,或者连接在网络中途断掉,后端通常已经不能把发出去的 200 改成 500。

所以,HTTP 状态码只能说明流式响应成功开始,不能单独证明整段回答成功完成。 ASGI 的响应规范将响应开始和后续响应体定义为不同的发送阶段,这正是问题的根源。

把"接口返回了 200"直接记成"模型回答成功",会让监控数据从一开始就失真:用户看见半句话,系统却统计了一次成功请求。

1.连接关闭,为什么也不能说明回答完成?

那我们换个判断办法:等连接关闭再算完成,行不行?

也不行。正常回答结束后,连接会关闭;模型服务故障、浏览器断网、代理超时,也都可能导致连接关闭。关闭是传输现象,完成是业务结论。

因此,流式接口需要一个明确的结束信号。例如后端可以约定:

  • 持续发送内容事件,表示"回答仍在生成";
  • 发送完成事件,表示"回答已正常结束";
  • 发送错误事件,表示"已确认发生错误"。

如果前端收到了一部分内容,却始终没收到完成事件,随后连接断开,它只能将这次回答视为未确认完成。此时前端未必能判断故障究竟发生在模型服务、后端,还是自己与后端之间的网络。

不同模型接口的结束协议并不相同,不能把某个接口的结束标记套用到所有流上。例如 DeepSeek 的 Responses API 流式文档区分了 response.completed、`response.incomplete` 和 response.failed。如果后端还要向浏览器转发一层 SSE,就需要把上游的终态准确转换成自己对前端承诺的终态。

还有一种情况尤其容易遗漏:后端确实准备了错误事件,但浏览器连接已经断了,这个事件根本送不到用户手里。所以系统内部也必须记录最终状态,不能把"通知前端"当作唯一的错误处理。

2.已经显示的半段回答,该不该保存?

假设用户已经看到了 300 字,模型随后出错。后端现在有两个事实:

一是这 300 字确实生成过,用户也可能已经读到;二是它并不是一条完整回答。

如果数据库只保存 content,不保存生成状态,下次用户打开历史记录,这 300 字看起来就和正常回答没有区别。更麻烦的是,下一轮请求可能把这段残缺回答作为完整的 assistant 消息送进模型上下文。

这时问题已经从"页面显示不完整"变成了"后续对话依据不可靠"。

比较稳妥的做法是把内容 和状态分开保存。一次生成可以处于"生成中""已完成""已中断""已失败"等状态;已接收的内容可以保留,但只有收到明确的完成信号后,状态才能进入"已完成"。

"已中断"和"已失败"也值得区分。用户主动停止、浏览器连接断开、模型服务报错,最终都可能留下半段文字,但它们的原因、后续提示和监控含义并不一样。

至于半段回答是否进入下一轮上下文,应当由产品和后端明确约定。我更倾向于默认不把它当成一条正常完成的 assistant 回答;如果产品希望保留并继续使用,也应该让用户看得出它曾经中断。

3.输出了一半,再自动重试不就好了?

这是最容易想到、也最容易做错的一步。

假设第一次已经输出:

这段代码的问题在于数据库连接没有及时释放......

后端重新调用模型后,第二次可能从头写一遍,也可能换一个分析角度。把两次输出直接拼起来,用户就可能看到重复段落、句子断裂,甚至前后结论矛盾。

模型生成也不是按字符位置读取文件。你不能因为已经收到 300 字,就假定新请求能够从第 301 个字准确继续。重新请求通常意味着重新生成,不能默认等于断点续写。

即使前端尚未收到任何文字,重试也要谨慎:后端可能已经把请求送到模型服务,只是响应还没传回来。此时重试可能形成第二次模型调用。分布式系统里,"连接断了"并不等于"对方没有执行";Stripe 对请求中途失败和幂等性的分析把这种不确定性解释得很清楚。它讨论的是支付 API,具体机制不能直接照搬到模型生成,但"调用方无法仅凭断连确定对方是否执行"这一点同样适用。

因此,重试至少要分清两个阶段:

还没有向用户输出内容时,后端可以根据错误类型、重试次数和业务成本,考虑有限重试。即便如此,也要承认上游调用是否已经产生消耗,未必能从一次断连中确认。

已经向用户输出内容后,通常更合适的处理是结束本次生成、标记为中断,并由用户明确选择"重新生成"。新的回答作为一次新的生成展示,而不是悄悄接到旧回答后面。

如果业务真的需要"断线后接着看",那就要设计另一套能力:服务端保存已经产生的事件,给事件编号,并在客户端重连时重放尚未收到的内容。这能解决已经生成的内容如何重新送达 ,但仍不自动解决已经停止的模型生成如何从原位置继续 。SSE 的 id 与重连机制可以帮助传输层识别事件位置,具体能否恢复,还取决于服务端是否保留了可重放的数据。MDN 的 SSE 说明介绍了事件 ID 和重连字段的作用。

4.后端应该怎样定义"一次成功生成"?

写到这里,可以给这次请求一个比 HTTP 200 更准确的成功条件:

后端确认上游正常结束,已处理完预期内容,并向自己的流写出了约定的完成事件,才将生成状态记为完成。

这个条件也有边界。后端成功写出完成事件,并不能百分之百证明浏览器最终收到了它。因此要分开看两个视角:

  • 后端视角:上游有没有正常结束?本地是否成功处理并发送了终态?
  • 前端视角:有没有实际收到完成事件?如果没有,页面就不应把回答展示为"已完成"。

对应地,监控也不能只统计接口状态码。至少应该知道:请求有没有开始输出、输出了多少内容、上游如何结束、下游是否断开、最终记录成了什么状态。这样用户反馈"回答到一半停了"时,才能沿着一次请求查清楚它停在哪里。

流式输出让回答更早出现在屏幕上,也把"成功"从一个瞬间变成了一段过程。开始输出是一次进展,正常结束才是一次完成。 后端只有把这两个时刻区分开,才能正确处理半段回答、重试和会话历史。

我是 1M55,我们下篇见!

相关推荐
AI 编程助手GPT3 小时前
GPT 排的日志时间线差一小时?用 zoneinfo 复现夏令时回拨
人工智能·python·gpt
Thanks_ks3 小时前
【第 003 讲】全面认识 Python:语言起源与设计哲学 | 行业地位与流行趋势 | 技术优势与应用领域 | 技术边界与工程局限
人工智能·python·数据分析·编程语言·python 入门·web 开发·python 之禅
用户790860057264 小时前
给 AI 装上耳朵:离线语音识别与唤醒,国内环境踩坑实录
python
师夷长技以自愈4 小时前
第 5 讲:Function Calling 做强制结构化输出——把「交卷」伪装成 tool
面试
用户790860057264 小时前
我让 AI 自己上了趟网:browser-use 的三个致命坑和 CDP 混合方案
python
安枳杦4 小时前
把一个 FastAPI 网页塞进 macOS 的 Dock:Tauri 2 + PyInstaller 实战
python
像风一样自由20204 小时前
3.Dockerfile如何构建一个Python算法镜像
开发语言·人工智能·python·大模型
Dmz丶4 小时前
前端转 AI 100 天 Day 36:Prompt 工程入门——从“感觉对了就行”到方法论
python
日日大王学编程5 小时前
AI 时代后端方向新人的未来规划与看法
人工智能·后端·python·ai·golang