什么是流式输出?
流式输出使用 Server-Sent Events (SSE) 协议,实时传输 AI 生成的内容,提供类似 ChatGPT 的打字机效果。启用流式输出
设置stream: true:
{
"model": "anthropic/claude-3-7-sonnet-20250219",
"messages": [...],
"stream": true
}
SSE 事件类型
花卷智能体 API 的流式响应使用标准 SSE 格式,所有事件都以data: 开头,后跟 JSON 对象。
重要:所有事件都只包含
data: 行,没有 event: 行。每个事件的 type 字段标识事件类型。流程控制事件
start - 流开始
start - 流开始
标志整个流式响应开始:
SSE 事件
data: {"type":"start"}
start-step - 步骤开始
start-step - 步骤开始
标志新步骤开始(对话可能包含多个步骤,如思考 → 工具调用 → 最终回复):
SSE 事件
data: {"type":"start-step"}
finish-step - 步骤完成
finish-step - 步骤完成
标志当前步骤完成:
SSE 事件
data: {"type":"finish-step"}
finish - 流结束
finish - 流结束
标志整个流式响应完成:
SSE 事件
data: {"type":"finish"}
文本事件
text-start - 文本块开始
text-start - 文本块开始
标志新文本块开始,包含唯一的
id 标识:SSE 事件
data: {"type":"text-start","id":"0"}
text-delta - 文本增量
text-delta - 文本增量
实时传输文本内容片段(打字机效果的核心):字段说明:
SSE 事件示例
data: {"type":"text-delta","id":"0","delta":"你"}
data: {"type":"text-delta","id":"0","delta":"好"}
data: {"type":"text-delta","id":"0","delta":","}
data: {"type":"text-delta","id":"0","delta":"请问"}
data: {"type":"text-delta","id":"0","delta":"有什么"}
data: {"type":"text-delta","id":"0","delta":"可以"}
data: {"type":"text-delta","id":"0","delta":"帮"}
data: {"type":"text-delta","id":"0","delta":"您的"}
data: {"type":"text-delta","id":"0","delta":"?"}
id: 文本块标识符,对应text-start的 iddelta: 文本增量内容
text-end - 文本块结束
text-end - 文本块结束
标志文本块传输完成:
SSE 事件
data: {"type":"text-end","id":"0"}
工具调用事件
tool-input-start - 工具输入开始
tool-input-start - 工具输入开始
标志 AI 开始调用工具,包含工具名称和调用 ID:字段说明:
SSE 事件
data: {
"type":"tool-input-start",
"toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T",
"toolName":"zhipin_reply_generator"
}
toolCallId: 唯一的工具调用 IDtoolName: 被调用的工具名称
tool-input-delta - 工具输入增量
tool-input-delta - 工具输入增量
实时传输工具输入参数的 JSON 字符串片段:字段说明:
SSE 事件示例
data: {"type":"tool-input-delta","toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T","inputTextDelta":""}
data: {"type":"tool-input-delta","toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T","inputTextDelta":"{\"candi"}
data: {"type":"tool-input-delta","toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T","inputTextDelta":"date_messag"}
data: {"type":"tool-input-delta","toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T","inputTextDelta":"e\": \"你们薪资待遇"}
data: {"type":"tool-input-delta","toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T","inputTextDelta":"怎么样?\""}
data: {"type":"tool-input-delta","toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T","inputTextDelta":", "}
data: {"type":"tool-input-delta","toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T","inputTextDelta":"\"incl"}
data: {"type":"tool-input-delta","toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T","inputTextDelta":"ude_stats"}
data: {"type":"tool-input-delta","toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T","inputTextDelta":"\": false}"}
inputTextDelta: 输入参数 JSON 字符串的增量片段
tool-input-available - 工具输入完成
tool-input-available - 工具输入完成
工具输入参数传输完成,包含完整的解析后参数对象:字段说明:
SSE 事件
data: {
"type":"tool-input-available",
"toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T",
"toolName":"zhipin_reply_generator",
"input":{
"candidate_message":"你们薪资待遇怎么样?",
"include_stats":false
}
}
input: 完整的工具输入参数对象(已解析的 JSON)
此时服务端开始执行工具,客户端可以显示”正在调用工具…”等加载提示。
tool-output-available - 工具输出可用
tool-output-available - 工具输出可用
工具执行完成,返回输出结果:字段说明:
SSE 事件
data: {
"type":"tool-output-available",
"toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T",
"output":{
"reply":"基本薪资是24元一小时,每周工作20到40小时,最少要上3天班。工资是T+7结算,比如你周一上的班,下周一就发钱。门店还给办五险一金,买东西有员工折扣。你现在方便去沪亭北路那家店面试吗,他们最近在招人。",
"replyType":"salary_inquiry",
"reasoningText":"消息直接询问薪资待遇,符合salary_inquiry的定义。没有涉及其他敏感话题或具体细节。",
"candidateMessage":"你们薪资待遇怎么样?",
"historyCount":0
}
}
output: 工具执行结果对象(结构取决于具体工具)
工具调用完成后,AI 通常会继续生成文本回复(新的
start-step → text-start → text-delta…)。流结束标记
流结束时会发送特殊的[DONE] 标记:
SSE 流结束标记
data: [DONE]
重要:
[DONE] 是字面量文本,不是 JSON 对象。客户端应该检测到这个字符串后停止读取流。完整事件流示例
以下是一次包含工具调用的完整流式响应示例:完整 SSE 事件流
data: {"type":"start"}
data: {"type":"start-step"}
data: {"type":"text-start","id":"0"}
data: {"type":"text-delta","id":"0","delta":"I need to generate"}
data: {"type":"text-delta","id":"0","delta":" a response..."}
data: {"type":"text-end","id":"0"}
data: {"type":"tool-input-start","toolCallId":"toolu_01ABC","toolName":"zhipin_reply_generator"}
data: {"type":"tool-input-delta","toolCallId":"toolu_01ABC","inputTextDelta":"{\"candi"}
data: {"type":"tool-input-delta","toolCallId":"toolu_01ABC","inputTextDelta":"date_message\":\"...\""}
data: {"type":"tool-input-available","toolCallId":"toolu_01ABC","toolName":"zhipin_reply_generator","input":{...}}
data: {"type":"tool-output-available","toolCallId":"toolu_01ABC","output":{...}}
data: {"type":"finish-step"}
data: {"type":"start-step"}
data: {"type":"text-start","id":"0"}
data: {"type":"text-delta","id":"0","delta":"基本薪资是24元..."}
data: {"type":"text-end","id":"0"}
data: {"type":"finish-step"}
data: {"type":"finish"}
data: [DONE]
事件流程理解:
- 开始流 (
start) - 第一步:AI 思考过程 (
start-step→ 思考文本 → 工具调用 →finish-step) - 第二步:基于工具结果生成最终回复 (
start-step→ 回复文本 →finish-step) - 结束流 (
finish→[DONE])
客户端实现
const response = await fetch("https://huajune.duliday.com/api/v1/chat", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "anthropic/claude-3-7-sonnet-20250219",
messages: [{ role: "user", content: "写一首关于春天的诗" }],
stream: true,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
// 检查流结束标记
if (data === "[DONE]") {
console.log("\n流已结束");
break;
}
try {
const event = JSON.parse(data);
// 处理文本增量事件
if (event.type === "text-delta") {
process.stdout.write(event.delta); // 实时输出文本
}
// 处理工具调用事件
if (event.type === "tool-input-available") {
console.log(`\n[工具调用] ${event.toolName}`);
console.log("输入参数:", event.input);
}
if (event.type === "tool-output-available") {
console.log("输出结果:", event.output);
}
} catch (e) {
// 忽略无法解析的行
}
}
}
}
import requests
import json
url = "https://huajune.duliday.com/api/v1/chat"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"model": "anthropic/claude-3-7-sonnet-20250219",
"messages": [
{"role": "user", "content": "写一首关于春天的诗"}
],
"stream": True
}
with requests.post(url, json=payload, headers=headers, stream=True) as response:
for line in response.iter_lines():
if line:
decoded = line.decode('utf-8')
if decoded.startswith('data: '):
data = decoded[6:]
# 检查流结束标记
if data == '[DONE]':
print("\n流已结束")
break
try:
event = json.loads(data)
# 处理文本增量事件
if event['type'] == 'text-delta':
print(event['delta'], end='', flush=True)
# 处理工具调用事件
if event['type'] == 'tool-input-available':
print(f"\n[工具调用] {event['toolName']}")
print(f"输入参数: {event['input']}")
if event['type'] == 'tool-output-available':
print(f"输出结果: {event['output']}")
except json.JSONDecodeError:
pass # 忽略无法解析的行
React 组件示例
使用 React 实现流式聊天界面:React 流式聊天组件
import { useState } from "react";
function ChatBox() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [streaming, setStreaming] = useState(false);
const sendMessage = async () => {
if (!input.trim()) return;
const userMessage = { role: "user", content: input };
setMessages((prev) => [...prev, userMessage]);
setInput("");
setStreaming(true);
let assistantMessage = { role: "assistant", content: "" };
setMessages((prev) => [...prev, assistantMessage]);
const response = await fetch("https://huajune.duliday.com/api/v1/chat", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "anthropic/claude-3-7-sonnet-20250219",
messages: messages.concat([userMessage]),
stream: true,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
// 检查流结束标记
if (data === "[DONE]") {
break;
}
try {
const event = JSON.parse(data);
// 处理文本增量事件
if (event.type === "text-delta") {
setMessages((prev) => {
const newMessages = [...prev];
newMessages[newMessages.length - 1].content += event.delta;
return newMessages;
});
}
// 处理工具调用(可选)
if (event.type === "tool-input-available") {
console.log(`工具调用: ${event.toolName}`, event.input);
}
} catch (e) {
// 忽略无法解析的行
}
}
}
}
setStreaming(false);
};
return (
<div>
<div className="messages">
{messages.map((msg, i) => (
<div key={i} className={msg.role}>
{msg.content}
</div>
))}
</div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && sendMessage()}
disabled={streaming}
placeholder={streaming ? "AI 正在回复..." : "输入消息..."}
/>
</div>
);
}
关键改动:
- 使用
event.type === "text-delta"而非event.type === "text.delta" - 可选地处理
tool-input-available事件以显示工具调用状态 - 添加占位符文本以反映流式状态
响应头
流式响应包含以下特殊头:Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache
X-Accel-Buffering: no
X-Correlation-Id: req_abc123
X-Message-Pruned: true // 如果启用了消息剪裁
X-Tools-Skipped: tool1,tool2 // 如果有工具被跳过
最佳实践
错误处理
错误处理
监听
error 事件并向用户展示友好的错误信息:if (data.type === 'error') {
console.error('流式错误:', data.message);
// 向用户显示错误提示
}
连接超时
连接超时
设置合理的超时时间,处理网络中断:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60秒超时
const response = await fetch(url, {
signal: controller.signal,
// ...其他选项
});
clearTimeout(timeoutId);
性能优化
性能优化
批量更新 UI,避免频繁渲染:
let buffer = '';
let updateTimer = null;
// 收到数据时
buffer += data.delta;
clearTimeout(updateTimer);
updateTimer = setTimeout(() => {
// 批量更新 UI
setContent(prev => prev + buffer);
buffer = '';
}, 50); // 每50ms更新一次
下一步
文本对话
了解基础文本对话功能
错误处理
学习如何处理各种错误

