> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wolian.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# 流式输出

> 实现打字机效果的实时对话体验

## 什么是流式输出？

流式输出使用 Server-Sent Events (SSE) 协议，实时传输 AI 生成的内容，提供类似 ChatGPT 的打字机效果。

## 启用流式输出

设置 `stream: true`：

```json theme={null}
{
  "model": "anthropic/claude-3-7-sonnet-20250219",
  "messages": [...],
  "stream": true
}
```

## SSE 事件类型

花卷智能体 API 的流式响应使用标准 SSE 格式，所有事件都以 `data:` 开头，后跟 JSON 对象。

<Note>
  **重要**：所有事件都只包含 `data:` 行，没有 `event:` 行。每个事件的 `type` 字段标识事件类型。
</Note>

### 流程控制事件

<AccordionGroup>
  <Accordion title="start - 流开始" icon="play">
    标志整个流式响应开始：

    ```json SSE 事件 icon="play" theme={null}
    data: {"type":"start"}
    ```
  </Accordion>

  <Accordion title="start-step - 步骤开始" icon="forward">
    标志新步骤开始（对话可能包含多个步骤，如思考 → 工具调用 → 最终回复）：

    ```json SSE 事件 icon="forward" theme={null}
    data: {"type":"start-step"}
    ```
  </Accordion>

  <Accordion title="finish-step - 步骤完成" icon="stop">
    标志当前步骤完成：

    ```json SSE 事件 icon="stop" theme={null}
    data: {"type":"finish-step"}
    ```
  </Accordion>

  <Accordion title="finish - 流结束" icon="flag-checkered">
    标志整个流式响应完成：

    ```json SSE 事件 icon="flag-checkered" theme={null}
    data: {"type":"finish"}
    ```
  </Accordion>
</AccordionGroup>

### 文本事件

<AccordionGroup>
  <Accordion title="text-start - 文本块开始" icon="font">
    标志新文本块开始，包含唯一的 `id` 标识：

    ```json SSE 事件 highlight={1} icon="font" theme={null}
    data: {"type":"text-start","id":"0"}
    ```
  </Accordion>

  <Accordion title="text-delta - 文本增量" icon="font">
    实时传输文本内容片段（打字机效果的核心）：

    ```json SSE 事件示例 lines icon="font" theme={null}
    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`: 文本增量内容
  </Accordion>

  <Accordion title="text-end - 文本块结束" icon="font">
    标志文本块传输完成：

    ```json SSE 事件 highlight={1} icon="font" theme={null}
    data: {"type":"text-end","id":"0"}
    ```
  </Accordion>
</AccordionGroup>

### 工具调用事件

<AccordionGroup>
  <Accordion title="tool-input-start - 工具输入开始" icon="wrench">
    标志 AI 开始调用工具，包含工具名称和调用 ID：

    ```json SSE 事件 highlight={1-3} icon="wrench" theme={null}
    data: {
      "type":"tool-input-start",
      "toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T",
      "toolName":"zhipin_reply_generator"
    }
    ```

    **字段说明**：

    * `toolCallId`: 唯一的工具调用 ID
    * `toolName`: 被调用的工具名称
  </Accordion>

  <Accordion title="tool-input-delta - 工具输入增量" icon="wrench">
    实时传输工具输入参数的 JSON 字符串片段：

    ```json SSE 事件示例 lines icon="wrench" theme={null}
    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 字符串的增量片段
  </Accordion>

  <Accordion title="tool-input-available - 工具输入完成" icon="wrench">
    工具输入参数传输完成，包含完整的解析后参数对象：

    ```json SSE 事件 highlight={4-7} lines icon="wrench" theme={null}
    data: {
      "type":"tool-input-available",
      "toolCallId":"toolu_01DqbvTck8QYggZvyt9ioB5T",
      "toolName":"zhipin_reply_generator",
      "input":{
        "candidate_message":"你们薪资待遇怎么样？",
        "include_stats":false
      }
    }
    ```

    **字段说明**：

    * `input`: 完整的工具输入参数对象（已解析的 JSON）

    <Tip>
      此时服务端开始执行工具，客户端可以显示"正在调用工具..."等加载提示。
    </Tip>
  </Accordion>

  <Accordion title="tool-output-available - 工具输出可用" icon="wrench">
    工具执行完成，返回输出结果：

    ```json SSE 事件 highlight={4-9} lines expandable icon="wrench" theme={null}
    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`: 工具执行结果对象（结构取决于具体工具）

    <Note>
      工具调用完成后，AI 通常会继续生成文本回复（新的 `start-step` → `text-start` → `text-delta`...）。
    </Note>
  </Accordion>
</AccordionGroup>

### 流结束标记

流结束时会发送特殊的 `[DONE]` 标记：

```json SSE 流结束标记 icon="flag-checkered" theme={null}
data: [DONE]
```

<Warning>
  **重要**：`[DONE]` 是字面量文本，不是 JSON 对象。客户端应该检测到这个字符串后停止读取流。
</Warning>

### 完整事件流示例

以下是一次包含工具调用的完整流式响应示例：

```json 完整 SSE 事件流 lines expandable icon="list" theme={null}
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]
```

<Tip>
  **事件流程理解**：

  1. 开始流 (`start`)
  2. 第一步：AI 思考过程 (`start-step` → 思考文本 → 工具调用 → `finish-step`)
  3. 第二步：基于工具结果生成最终回复 (`start-step` → 回复文本 → `finish-step`)
  4. 结束流 (`finish` → `[DONE]`)
</Tip>

## 客户端实现

<CodeGroup>
  ```javascript JavaScript icon="js" theme={null}
  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) {
          // 忽略无法解析的行
        }
      }
    }
  }
  ```

  ```python Python icon="python" theme={null}
  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  # 忽略无法解析的行
  ```
</CodeGroup>

## React 组件示例

使用 React 实现流式聊天界面：

```jsx React 流式聊天组件 lines expandable icon="react" theme={null}
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>
  );
}
```

<Note>
  **关键改动**：

  * 使用 `event.type === "text-delta"` 而非 `event.type === "text.delta"`
  * 可选地处理 `tool-input-available` 事件以显示工具调用状态
  * 添加占位符文本以反映流式状态
</Note>

## 响应头

流式响应包含以下特殊头：

```
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  // 如果有工具被跳过
```

## 最佳实践

<AccordionGroup>
  <Accordion title="错误处理">
    监听 `error` 事件并向用户展示友好的错误信息：

    ```javascript theme={null}
    if (data.type === 'error') {
      console.error('流式错误:', data.message);
      // 向用户显示错误提示
    }
    ```
  </Accordion>

  <Accordion title="连接超时">
    设置合理的超时时间，处理网络中断：

    ```javascript theme={null}
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 60000); // 60秒超时

    const response = await fetch(url, {
      signal: controller.signal,
      // ...其他选项
    });

    clearTimeout(timeoutId);
    ```
  </Accordion>

  <Accordion title="性能优化">
    批量更新 UI，避免频繁渲染：

    ```javascript theme={null}
    let buffer = '';
    let updateTimer = null;

    // 收到数据时
    buffer += data.delta;

    clearTimeout(updateTimer);
    updateTimer = setTimeout(() => {
      // 批量更新 UI
      setContent(prev => prev + buffer);
      buffer = '';
    }, 50); // 每50ms更新一次
    ```
  </Accordion>
</AccordionGroup>

## 下一步

<CardGroup cols={2}>
  <Card title="文本对话" icon="message" href="/features/text-chat">
    了解基础文本对话功能
  </Card>

  <Card title="错误处理" icon="triangle-exclamation" href="/features/error-handling">
    学习如何处理各种错误
  </Card>
</CardGroup>
