用 Vercel AI SDK 构建 Chat 界面时,一个高频问题是:怎么判断模型这轮输出“结束了”?
表面上看这是一个状态标志的问题,实际上 AI SDK 的 UI Stream 里存在四种不同层级的“结束”,混淆它们会导致 UI 提前停止 loading、消息被截断,或者无法处理多轮工具调用。
四种“结束”
text-end
↓ 当前文本片段结束
finish-step
↓ 当前模型 Step 结束
finish
↓ 当前 assistant message 结束
HTTP Stream close
↓ 当前请求彻底结束此外,当前请求结束后,客户端仍可能根据自动续传逻辑发起下一次请求。所以还要区分第五件事:请求结束后客户端会不会再发请求。
1. text-end:当前文本片段结束
text-end 只表示当前 text part 不会再收到 text-delta。
一条 assistant message 可能包含多个文本片段和工具调用:
text-start
text-delta
text-end
tool-input-available
tool-output-available
text-start
text-delta
text-end
finish例如:
模型输出:我先查询一下天气
→ text-end
模型调用天气工具
→ tool call
→ tool result
模型继续输出:今天杭州有雨
→ 新的 text-start / text-end因此:
收到
text-end,不代表整个回答已经结束。它只代表当前这段文本结束。
2. finish-step:当前模型 Step 结束
一个 Step 通常对应一次模型调用。
Agent Loop 中,一轮用户请求可能包含多个 Step:
Step 1
模型输出工具调用
→ finish-step
执行工具
Step 2
模型读取工具结果并继续回答
→ finish-step因此:
finish-step表示一次模型调用结束,不代表整个 Agent 任务结束。
只要后续还需要执行工具或继续调用模型,就会产生新的 Step。
3. finish:当前 assistant message 结束
finish 是消息级别的结束事件。它表示当前 assistant message 的所有内容已经生成完成,包括:
- 文本片段
- 推理片段
- 工具调用
- 工具结果
- 多个模型 Step
通常收到 finish 后,服务端会关闭当前 Response Stream。
因此判断一轮服务端生成是否完成,主要看:
chunk.type === 'finish'而不是:
chunk.type === 'text-end'4. HTTP Stream close:当前请求彻底结束
底层通过 ReadableStream 判断时:
const reader = response.body!.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
// 当前 HTTP 请求彻底结束
break;
}
// 处理流数据
}当 done === true,表示当前 HTTP Response Body 已经关闭,当前请求不可能再继续发送数据。
正常流程通常是:
收到 finish
→ 服务端关闭 Stream
→ reader.read() 返回 done: true5. 当前请求结束,不代表不会发起下一次请求
需要区分两件事:
当前请求是否结束
客户端是否会再次发送请求即使当前请求已经收到 finish 并关闭,useChat 仍可能根据自动续传规则发起下一次请求。例如配置:
useChat({
sendAutomaticallyWhen: ({ messages }) => {
return shouldContinue(messages);
},
});完整流程可能是:
请求 A
→ streaming
→ finish
→ HTTP Stream close
执行 sendAutomaticallyWhen
├─ false:停止,进入 ready
└─ true:自动发起请求 B这种机制常用于:客户端工具执行完成后继续对话、用户确认工具调用后继续、多轮工具调用、自定义 Agent Loop。
所以:
finish只能证明当前请求结束,不能单独证明客户端不会再发请求。
6. useChat 中如何判断整体状态
使用 useChat 时,通常直接观察 status:
const { messages, sendMessage, status } = useChat();常见状态流转:
submitted
→ streaming
→ ready异常时:
submitted
→ streaming
→ error含义:
submitted:请求已经提交,等待响应streaming:正在接收流数据ready:当前没有正在进行的请求error:请求失败
因此 UI 中通常这样判断:
const isLoading =
status === 'submitted' ||
status === 'streaming';
const isIdle = status === 'ready';status === 'ready' 表示当前 useChat 已经没有正在执行的请求。但如果配置了自动续传,状态可能短暂回到 ready,随后再次进入 submitted → streaming。
7. 各种结束信号对照表
| 信号 | 表示什么 | 后续还能有内容吗 |
|---|---|---|
text-end | 当前文本 part 结束 | 可以 |
finish-step | 当前模型调用结束 | 可以 |
finish | 当前 assistant message 结束 | 当前请求通常不再有业务内容 |
Stream done: true | 当前 HTTP 请求关闭 | 当前请求不可以 |
status === 'ready' | 当前 useChat 没有执行中的请求 | 之后仍可由用户或自动逻辑发起新请求 |
一句话总结
text-end是文本片段结束,finish-step是一次模型调用结束,finish是当前 assistant message 结束,HTTP Stream close 是当前请求彻底结束;请求结束后是否还会发送下一次请求,由sendAutomaticallyWhen等客户端逻辑决定。