什么是流式输出?
流式输出使用 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 字段标识事件类型。
流程控制事件
标志新步骤开始(对话可能包含多个步骤,如思考 → 工具调用 → 最终回复):data: {"type":"start-step"}
标志当前步骤完成:data: {"type":"finish-step"}
文本事件
标志新文本块开始,包含唯一的 id 标识:data: {"type":"text-start","id":"0"}
实时传输文本内容片段(打字机效果的核心):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 的 id
delta: 文本增量内容
标志文本块传输完成:data: {"type":"text-end","id":"0"}
工具调用事件
tool-input-start - 工具输入开始
tool-input-delta - 工具输入增量
tool-input-available - 工具输入完成
tool-output-available - 工具输出可用
流结束标记
流结束时会发送特殊的 [DONE] 标记:
重要:[DONE] 是字面量文本,不是 JSON 对象。客户端应该检测到这个字符串后停止读取流。
完整事件流示例
以下是一次包含工具调用的完整流式响应示例:
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) {
// 忽略无法解析的行
}
}
}
}
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更新一次
下一步