Skip to main content

什么是流式输出?

流式输出使用 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 字段标识事件类型。

流程控制事件

标志整个流式响应开始:
SSE 事件
data: {"type":"start"}
标志新步骤开始(对话可能包含多个步骤,如思考 → 工具调用 → 最终回复):
SSE 事件
data: {"type":"start-step"}
标志当前步骤完成:
SSE 事件
data: {"type":"finish-step"}
标志整个流式响应完成:
SSE 事件
data: {"type":"finish"}

文本事件

标志新文本块开始,包含唯一的 id 标识:
SSE 事件
data: {"type":"text-start","id":"0"}
实时传输文本内容片段(打字机效果的核心):
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 的 id
  • delta: 文本增量内容
标志文本块传输完成:
SSE 事件
data: {"type":"text-end","id":"0"}

工具调用事件

标志 AI 开始调用工具,包含工具名称和调用 ID:
SSE 事件
data: {
  "type":"tool-input-start",
  "toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T",
  "toolName":"zhipin_reply_generator"
}
字段说明
  • toolCallId: 唯一的工具调用 ID
  • toolName: 被调用的工具名称
实时传输工具输入参数的 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 字符串的增量片段
工具输入参数传输完成,包含完整的解析后参数对象:
SSE 事件
data: {
  "type":"tool-input-available",
  "toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T",
  "toolName":"zhipin_reply_generator",
  "input":{
    "candidate_message":"你们薪资待遇怎么样?",
    "include_stats":false
  }
}
字段说明
  • input: 完整的工具输入参数对象(已解析的 JSON)
此时服务端开始执行工具,客户端可以显示”正在调用工具…”等加载提示。
工具执行完成,返回输出结果:
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-steptext-starttext-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]
事件流程理解
  1. 开始流 (start)
  2. 第一步:AI 思考过程 (start-step → 思考文本 → 工具调用 → finish-step)
  3. 第二步:基于工具结果生成最终回复 (start-step → 回复文本 → finish-step)
  4. 结束流 (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 实现流式聊天界面:
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更新一次

下一步