Chatbot 如何知道 AI 回复结束了?—— [DONE] 与 Streaming 生命周期学习总结

系列专栏:从零开发 AI Chatbot(KK Chat)
项目版本:KK Chat v1.2.0

目录


前言

上一篇文章中,我学习了 delta.content,理解了 AI Streaming 为什么能够不断返回新增内容,并逐步拼接成完整回复。

整个过程中,浏览器会持续收到:

data: {...}

data: {...}

data: {...}

每解析一个数据块,就会更新一次聊天界面。

但是,一个新的问题出现了:

浏览器怎么知道 AI 已经回答完了?

总不能一直等待服务器继续发送数据。

因此,在 Streaming 中,还需要一种机制告诉客户端:

“这次回复已经结束,可以停止读取了。”

今天学习的内容,就是 Streaming 生命周期(Streaming Lifecycle) 与 [DONE] 结束标记。


一、为什么需要结束标记?

假设 AI 正在回答:

请介绍 Python。

浏览器不断收到:

Python
  │
  ▼
是一种
  │
  ▼
编程语言
  │
  ▼
。

如果没有结束标记,浏览器根本不知道:

还有没有下一段?

因此,程序可能一直处于等待状态:

等待
  │
  ▼
等待
  │
  ▼
继续等待

这会导致:

  • Loading 不会关闭;
  • 发送按钮不会恢复;
  • 整个聊天界面一直处于“AI 正在思考……”状态。

所以,必须有一个明确的结束信号。


二、什么是 [DONE]?

在 OpenAI 兼容接口中,Streaming 结束时,服务器通常会发送:

data: [DONE]

注意:它不是:

{
  "done": true
}

而是:

[DONE]

一个特殊字符串。

它表示:

本次 Streaming 已经结束。

不是“还有一个 JSON”,而是“真正结束”。


三、Streaming 生命周期

完整流程如下:

用户发送消息
      │
      ▼
Fetch 请求
      │
      ▼
SSE 建立连接
      │
      ▼
Streaming 开始
      │
      ▼
不断收到 delta.content
      │
      ▼
收到 [DONE]
      │
      ▼
结束读取
      │
      ▼
关闭 Loading
      │
      ▼
恢复发送按钮
      │
      ▼
等待下一次聊天

这就是一次 AI 回复的完整生命周期。


四、为什么不能一直读取?

ReadableStream 通常通过下面的方式读取:

const reader = response.body.getReader();

理论上,可以一直调用:

reader.read();

但是,当 Streaming 已经结束以后,继续读取没有意义。

因此,收到:

[DONE]

以后,程序应该:

结束循环
  │
  ▼
释放资源
  │
  ▼
恢复界面

五、KK Chat 中如何处理 [DONE]

整个流程如下:

ReadableStream
      │
      ▼
TextDecoder
      │
      ▼
Buffer
      │
      ▼
JSON.parse()
      │
      ▼
delta.content
      │
      ▼
更新页面

如果解析到:

[DONE]

则执行:

停止读取
  │
  ▼
reader.releaseLock()
  │
  ▼
Loading = false
  │
  ▼
按钮恢复

这样,用户才能继续下一次聊天。


六、实践内容

今天主要完成了以下内容:

  • 学习 Streaming 生命周期;
  • 理解 [DONE] 的作用;
  • 学习如何判断 Streaming 已结束;
  • 理解结束后为什么要恢复 Loading;
  • 梳理 Streaming 请求的完整生命周期。

整个流程进一步完善为:

DeepSeek
   │
   ▼
Streaming
   │
   ▼
SSE
   │
   ▼
ReadableStream
   │
   ▼
TextDecoder
   │
   ▼
Buffer
   │
   ▼
JSON.parse()
   │
   ▼
delta.content
   │
   ▼
[DONE]
   │
   ▼
恢复页面状态

七、遇到的问题

1. 为什么有时候没有收到 [DONE]?

原因:

并不是所有 AI 服务都采用相同的结束协议。

有些服务使用 [DONE];有些则直接关闭连接;还有一些会返回自己的结束字段。

解决方案:

根据具体 API 文档判断结束方式,而不要假设所有模型都使用 [DONE]。


2. 收到 [DONE] 后为什么还要关闭 Loading?

原因:

[DONE] 只是告诉程序:

AI 已经停止生成。

真正恢复页面,还需要:

  • Loading = false;
  • 恢复发送按钮;
  • 更新停止按钮状态;
  • 释放 Reader。

3. [DONE] 和 done = true 是一样的吗?

不是。

二者分别来自不同层级:

标记所属层级含义
[DONE]AI 服务协议中的结束标记(应用层)表示本次 AI Streaming 回复结束
done = trueReadableStream 读取结果(浏览器流层)表示当前流没有更多数据可读

在很多实现中,两者都会出现,但含义不同,也不能混为一谈。


八、今天的收获

今天最大的收获,是理解了:

Streaming 不只是不断接收数据,还需要有一个完整的生命周期。

整个过程包括:

  • 建立连接;
  • 持续接收;
  • 增量解析;
  • 收到结束信号;
  • 清理资源;
  • 恢复界面。

其中:

[DONE] 或其他结束机制,就是整个 Streaming 生命周期的重要终点。


总结

今天学习了 Streaming 生命周期 与 [DONE] 结束标记。

理解了浏览器不仅要持续读取 AI 回复,还需要正确识别回复已经结束,并完成资源释放和界面恢复。

通过今天的学习,我进一步完善了对 AI Streaming 工作机制的理解,也为后续学习完整的 Streaming 架构打下了基础。


KJ

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐