AI Agent 流式响应全解析:从用户输入到实时渲染的 12 个步骤

4 阅读10分钟

AI Agent 流式响应全解析:从用户输入到实时渲染的 12 个步骤

为什么 ChatGPT 的回答是逐字显示的?本文带你从零开始,完整拆解 AI Agent 流式响应的技术实现,包括数据流、SSE 协议、客户端渲染等核心知识点,代码示例拿来即用!

一、整体数据流架构

1.1 完整架构图

┌─────────────────────────────────────────────────────────────────────────┐
│                              用户浏览器                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────────┐  │
│  │  UI 组件      │  │  useChat Hook │  │   Message 渲染器              │  │
│  │ (输入框/上传) │  │  (状态管理)   │  │ (文本/图片/工具/思考)         │  │
│  └──────┬───────┘  └──────┬───────┘  └──────────────────────────────┘  │
└─────────┼──────────────────┼───────────────────────────────────────────┘
          │                  │
          │ 1. 用户输入       │ 2. 流式数据到达
          │ (文本/图片)       │ (SSE 数据流)
          ▼                  ▼
┌─────────┴──────────────────┴───────────────────────────────────────────┐
│                          HTTP 请求层                                   │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │  Content-Type: application/json                                   │  │
│  │  Body: { messages: [ {...}, {...} ] }                            │  │
│  └──────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                       Next.js API Route Handler                         │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  app/api/chat/route.ts                                            │  │
│  │                                                                   │  │
│  │  1. 接收请求 → 解析消息                                           │  │
│  │  2. 处理图片 → 转换为 base64                                      │  │
│  │  3. 调用 streamText → 启动流式响应                                │  │
│  │  4. 返回 toUIMessageStreamResponse()                             │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                      AI SDK streamText 核心                            │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  convertToModelMessages() → 转换消息格式                          │  │
│  │  streamText() → 启动流式生成                                      │  │
│  │  ├── 处理文本流 (text delta)                                      │  │
│  │  ├── 处理工具调用 (tool call)                                     │  │
│  │  ├── 处理工具结果 (tool result)                                   │  │
│  │  └── 处理推理过程 (reasoning)                                     │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                      LLM Provider (Claude/GPT)                          │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  AI Gateway → Provider API                                        │  │
│  │                                                                   │  │
│  │  请求格式:                                                         │  │
│  │  {                                                                │  │
│  │    model: "anthropic/claude-sonnet-4.6",                         │  │
│  │    messages: [...],                                               │  │
│  │    tools: [...],                                                  │  │
│  │    stream: true                                                   │  │
│  │  }                                                                │  │
│  │                                                                   │  │
│  │  响应格式 (SSE 流):                                               │  │
│  │  data: {"type":"text_delta","delta":{"text":"你好"}}             │  │
│  │  data: {"type":"content_block_delta","delta":{"text":",我是"}}   │  │
│  │  data: {"type":"content_block_stop"}                              │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                        工具执行层 (可选)                                │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  当 LLM 决定调用工具时:                                           │  │
│  │                                                                   │  │
│  │  1. 解析工具调用参数                                              │  │
│  │  2. 执行工具 (fetch / API 调用)                                   │  │
│  │  3. 获取工具结果                                                  │  │
│  │  4. 将结果注入 LLM 上下文                                         │  │
│  │  5. 继续流式生成                                                  │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                    SSE 流式响应层 (Server-Sent Events)                  │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  Content-Type: text/event-stream                                  │  │
│  │  Cache-Control: no-cache                                          │  │
│  │  Connection: keep-alive                                           │  │
│  │                                                                   │  │
│  │  流式数据块:                                                       │  │
│  │  data: 0:"你好"                                                    │  │
│  │  data: 0:",我是"                                                  │  │
│  │  data: 0:"AI助手"                                                  │  │
│  │  data: 1:{"type":"tool-call",...}                                 │  │
│  │  data: d:{"finishReason":"stop"}                                  │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                        客户端接收与渲染                                 │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  1. DefaultChatTransport 解析 SSE 流                              │  │
│  │  2. useChat 更新 messages 状态                                     │  │
│  │  3. Message 组件根据 parts 类型渲染:                               │  │
│  │     - text: 渲染文本                                               │  │
│  │     - image: 渲染图片                                              │  │
│  │     - tool-call: 渲染工具调用卡片                                  │  │
│  │     - tool-result: 渲染工具结果                                    │  │
│  │     - reasoning: 渲染思考过程                                      │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘

二、完整流程详解(12 个步骤)

步骤 1:用户在浏览器输入内容

场景 A:纯文本输入

// 用户在输入框输入
用户输入: "北京今天天气怎么样?"

// UI 组件捕获输入
const [input, setInput] = useState('');

function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
  setInput(e.target.value);
}

// 用户点击发送按钮
async function handleSend() {
  if (!input.trim()) return;

  // useChat hook 处理发送
  append({
    role: 'user',
    content: input,
  });

  setInput('');
}

场景 B:图片 + 文本输入

// 用户上传图片并输入文字
用户输入: "这张图片里是什么?"
用户上传: image.png (2.3MB)

// 图片处理流程
async function handleImageUpload(file: File) {
  // 1. 读取文件
  const reader = new FileReader();
  reader.readAsDataURL(file);

  reader.onload = () => {
    const base64 = reader.result as string;

    // 2. 构建多模态消息
    append({
      role: 'user',
      content: [
        { type: 'text', text: "这张图片里是什么?" },
        { type: 'image', image: base64 },
      ],
    });
  };
}

步骤 2:useChat Hook 构建请求

// useChat 内部处理
import { useChat } from '@ai-sdk/react';

function ChatComponent() {
  const { messages, input, handleInputChange, handleSubmit, status } = useChat({
    api: '/api/chat',  // API 端点
    transport: new DefaultChatTransport({ api: '/api/chat' }),
  });

  // messages 状态示例
  // [
  //   { id: '1', role: 'user', content: '北京今天天气怎么样?' },
  //   { id: '2', role: 'assistant', content: '我来帮您查询...', parts: [...] },
  // ]

  return (
    <div>
      {/* 消息列表 */}
      {messages.map(message => (
        <Message key={message.id} message={message} />
      ))}

      {/* 输入框 */}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
        <button disabled={status === 'streaming'}>发送</button>
      </form>
    </div>
  );
}

步骤 3:HTTP 请求发送到服务器

// 实际发送的 HTTP 请求
POST /api/chat HTTP/1.1
Host: localhost:3000
Content-Type: application/json

{
  "messages": [
    {
      "id": "msg_1",
      "role": "user",
      "content": "北京今天天气怎么样?"
    }
  ]
}

带图片的请求:

{
  "messages": [
    {
      "id": "msg_1",
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "这张图片里是什么?"
        },
        {
          "type": "image",
          "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
        }
      ]
    }
  ]
}

步骤 4:API Route Handler 处理请求

// app/api/chat/route.ts
import { streamText, convertToModelMessages } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { toUIMessageStreamResponse } from '@ai-sdk/react';

export async function POST(req: Request) {
  // ========== 1. 解析请求 ==========
  const { messages } = await req.json();

  // ========== 2. 转换消息格式 ==========
  // 将 UIMessage 格式转换为 ModelMessage 格式
  const modelMessages = await convertToModelMessages(messages);

  // ========== 3. 定义工具 ==========
  const tools = {
    getWeather: {
      description: '获取指定城市的天气信息',
      parameters: {
        type: 'object',
        properties: {
          city: { type: 'string', description: '城市名称' },
        },
        required: ['city'],
      },
      execute: async ({ city }: { city: string }) => {
        const response = await fetch(
          `https://api.weather.com/v1/current?city=${encodeURIComponent(city)}`
        );
        return response.json();
      },
    },
  };

  // ========== 4. 启动流式生成 ==========
  const result = await streamText({
    model: anthropic('claude-sonnet-4-6-20250514'),
    messages: modelMessages,
    tools: [tools.getWeather],
    maxSteps: 5,  // 最多 5 步推理(包括工具调用)
  });

  // ========== 5. 返回流式响应 ==========
  return toUIMessageStreamResponse(result.toDataStream());
}

步骤 5:AI SDK streamText 内部处理

// streamText 内部简化流程
async function streamText(params) {
  const { model, messages, tools, maxSteps } = params;

  let currentMessages = [...messages];
  let stepCount = 0;

  // ========== 循环处理多步推理 ==========
  while (stepCount < maxSteps) {
    // 1. 调用 LLM API
    const llmResponse = await callLLM({
      model,
      messages: currentMessages,
      tools,
      stream: true,  // 启用流式
    });

    // 2. 返回流式响应生成器
    return createStreamGenerator({
      async *[Symbol.asyncIterator]() {
        // 3. 逐个处理 LLM 返回的流式数据块
        for await (const chunk of llmResponse) {
          // 处理文本增量
          if (chunk.type === 'text_delta') {
            yield { type: 'text-delta', content: chunk.delta.text };
          }

          // 处理工具调用
          if (chunk.type === 'tool_call') {
            yield { type: 'tool-call', toolCall: chunk.toolCall };

            // 执行工具
            const toolResult = await tools[chunk.toolCall.name].execute(
              chunk.toolCall.args
            );

            // 更新消息历史
            currentMessages.push({
              role: 'assistant',
              content: chunk.toolCallText,
            });
            currentMessages.push({
              role: 'tool',
              toolCallId: chunk.toolCall.id,
              content: JSON.stringify(toolResult),
            });

            // 继续下一轮 LLM 调用
            stepCount++;
            continue;
          }

          // 处理思考过程
          if (chunk.type === 'reasoning') {
            yield { type: 'reasoning', content: chunk.reasoning };
          }
        }

        // 4. 流结束
        yield { type: 'finish', finishReason: 'stop' };
      },
    });
  }
}

步骤 6:LLM Provider 返回 SSE 流

Claude API 返回的 SSE 流示例:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

event: message_start
data: {"type":"message_start","message":{"id":"msg_123","type":"message","role":"assistant","content":[],"model":"claude-sonnet-4-6-20250514","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":15,"output_tokens":0}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"我"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"来"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"帮"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"您"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"查询"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"北京"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"的"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"天气"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"信息"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"。"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_123","name":"getWeather","input":{"city":"北京"}}}

event: content_block_stop
data: {"type":"content_block_stop","index":1}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":15}}

event: message_stop
data: {"type":"message_stop"}

步骤 7:工具执行(如果需要)

// 当 LLM 返回工具调用时
const toolCall = {
  id: 'toolu_123',
  name: 'getWeather',
  input: { city: '北京' },
};

// 执行工具
async function executeTool(toolCall) {
  const { name, input } = toolCall;

  switch (name) {
    case 'getWeather':
      // 调用天气 API
      const response = await fetch(
        `https://api.weather.com/v1/current?city=${encodeURIComponent(input.city)}`
      );
      const weatherData = await response.json();

      return {
        city: '北京',
        temperature: 22,
        condition: '晴',
        humidity: 45,
        wind: '东南风 3级',
        timestamp: new Date().toISOString(),
      };
  }
}

// 获取工具结果
const toolResult = await executeTool(toolCall);
// {
//   city: '北京',
//   temperature: 22,
//   condition: '晴',
//   humidity: 45,
//   wind: '东南风 3级',
//   timestamp: '2025-03-25T10:30:00.000Z'
// }

步骤 8:将工具结果注入 LLM 继续生成

// 更新消息历史
const updatedMessages = [
  ...currentMessages,
  {
    role: 'assistant',
    content: `<tool_calls>[{"id":"toolu_123","name":"getWeather","args":{"city":"北京"}}]</tool_calls>`,
  },
  {
    role: 'tool',
    tool_call_id: 'toolu_123',
    content: JSON.stringify(toolResult),
  },
];

// 继续调用 LLM
const continuedResponse = await streamText({
  model: anthropic('claude-sonnet-4-6-20250514'),
  messages: updatedMessages,
});

// LLM 继续流式输出最终回答
// "根据查询结果,北京今天天气晴朗,气温 22°C,湿度 45%,东南风 3 级。"

步骤 9:toUIMessageStreamResponse 转换为客户端可读格式

// AI SDK 将 LLM 流转换为 UIMessage 流
async function* toUIMessageStreamResponse(stream) {
  for await (const chunk of stream) {
    // 文本增量
    if (chunk.type === 'text-delta') {
      yield `0:${JSON.stringify(chunk.content)}\n`;
    }

    // 工具调用
    if (chunk.type === 'tool-call') {
      yield `1:${JSON.stringify({
        type: 'tool-call',
        toolCallId: chunk.toolCall.id,
        toolName: chunk.toolCall.name,
        args: chunk.toolCall.args,
      })}\n`;
    }

    // 工具结果
    if (chunk.type === 'tool-result') {
      yield `1:${JSON.stringify({
        type: 'tool-result',
        toolCallId: chunk.toolCallId,
        result: chunk.result,
      })}\n`;
    }

    // 思考过程
    if (chunk.type === 'reasoning') {
      yield `1:${JSON.stringify({
        type: 'reasoning',
        reasoning: chunk.content,
      })}\n`;
    }

    // 流结束
    if (chunk.type === 'finish') {
      yield `d:${JSON.stringify({
        finishReason: chunk.finishReason,
        usage: chunk.usage,
      })}\n`;
    }
  }
}

实际返回给客户端的 SSE 流:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

data: 0:"我"

data: 0:"来"

data: 0:"帮"

data: 0:"您"

data: 0:"查询"

data: 0:"北京"

data: 0:"的"

data: 0:"天气"

data: 0:"信息"

data: 0:"。"

data: 1:{"type":"tool-call","toolCallId":"toolu_123","toolName":"getWeather","args":{"city":"北京"}}

data: 1:{"type":"tool-result","toolCallId":"toolu_123","result":{"city":"北京","temperature":22,"condition":"晴","humidity":45,"wind":"东南风 3级"}}

data: 0:"根据"

data: 0:"查询"

data: 0:"结果"

data: 0:","

data: 0:"北京"

data: 0:"今天"

data: 0:"天气"

data: 0:"晴"

data: 0:"朗"

data: 0:","

data: 0:"气温"

data: 0:"22"

data: 0:"°"

data: 0:"C"

data: 0:"

data: d:{"finishReason":"stop","usage":{"promptTokens":25,"completionTokens":35}}

步骤 10:客户端接收并渲染流式响应

// DefaultChatTransport 内部处理
class DefaultChatTransport {
  async* streamMessages(messages: CoreMessage[]) {
    // 发送 POST 请求
    const response = await fetch(this.api, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ messages }),
    });

    // 解析 SSE 流
    const reader = response.body!.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });

      // 处理 SSE 数据行
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';

      for (const line of lines) {
        if (!line.startsWith('data: ')) continue;

        const data = line.slice(6);
        const [type, content] = data.split(':');

        if (type === '0') {
          // 文本增量
          yield { type: 'text-delta', content: JSON.parse(content) };
        } else if (type === '1') {
          // 结构化数据(工具调用、思考等)
          yield JSON.parse(content);
        } else if (type === 'd') {
          // 流结束
          yield JSON.parse(content);
        }
      }
    }
  }
}

步骤 11:useChat 更新状态并触发渲染

// useChat 内部状态更新
function useChat({ api, transport }) {
  const [messages, setMessages] = useState<UIMessage[]>([]);
  const [status, setStatus] = useState<'idle' | 'streaming'>('idle');

  const append = async (message: UIMessage) => {
    // 1. 添加用户消息
    setMessages(prev => [...prev, message]);

    // 2. 创建空的助手消息
    const assistantMessage: UIMessage = {
      id: generateId(),
      role: 'assistant',
      content: '',
      parts: [],
    };
    setMessages(prev => [...prev, assistantMessage]);

    setStatus('streaming');

    // 3. 接收流式数据
    const stream = transport.streamMessages(messages);

    for await (const chunk of stream) {
      // 更新助手消息
      setMessages(prev => prev.map(msg => {
        if (msg.id !== assistantMessage.id) return msg;

        if (chunk.type === 'text-delta') {
          return {
            ...msg,
            content: msg.content + chunk.content,
            parts: [...msg.parts, { type: 'text', text: chunk.content }],
          };
        }

        if (chunk.type === 'tool-call') {
          return {
            ...msg,
            parts: [...msg.parts, {
              type: 'tool-call',
              toolCallId: chunk.toolCallId,
              toolName: chunk.toolName,
              args: chunk.args,
            }],
          };
        }

        if (chunk.type === 'tool-result') {
          return {
            ...msg,
            parts: [...msg.parts, {
              type: 'tool-result',
              toolCallId: chunk.toolCallId,
              result: chunk.result,
            }],
          };
        }

        if (chunk.type === 'reasoning') {
          return {
            ...msg,
            parts: [...msg.parts, {
              type: 'reasoning',
              reasoning: chunk.reasoning,
            }],
          };
        }

        return msg;
      }));
    }

    setStatus('idle');
  };

  return { messages, append, status, ... };
}

步骤 12:Message 组件根据 parts 渲染

// Message 组件渲染逻辑
function Message({ message }: { message: UIMessage }) {
  return (
    <div className="message">
      <div className="message-header">
        <span className="role">{message.role}</span>
      </div>

      <div className="message-content">
        {message.parts.map((part, index) => {
          switch (part.type) {
            case 'text':
              // 渲染文本
              return <TextPart key={index} text={part.text} />;

            case 'image':
              // 渲染图片
              return <ImagePart key={index} image={part.image} />;

            case 'tool-call':
              // 渲染工具调用卡片
              return (
                <ToolCallCard
                  key={index}
                  toolName={part.toolName}
                  args={part.args}
                />
              );

            case 'tool-result':
              // 渲染工具结果
              return (
                <ToolResult
                  key={index}
                  result={part.result}
                />
              );

            case 'reasoning':
              // 渲染思考过程(可折叠)
              return (
                <ReasoningBlock
                  key={index}
                  reasoning={part.reasoning}
                />
              );

            default:
              return null;
          }
        })}
      </div>
    </div>
  );
}

三、完整时间线示例

时间轴从用户输入到最终显示:

0ms    ┌─────────────────────────────────────────────┐
       │ 用户在输入框输入: "北京今天天气怎么样?"    │
       └─────────────────────────────────────────────┘
        │
        ▼
50ms   ┌─────────────────────────────────────────────┐
       │ useChat 拦截输入,构建 messages 数组          │
       │ [{ role: 'user', content: '...' }]          │
       └─────────────────────────────────────────────┘
        │
        ▼
100ms  ┌─────────────────────────────────────────────┐
       │ 发送 POST /api/chat 请求                     │
       └─────────────────────────────────────────────┘
        │
        ▼
150ms  ┌─────────────────────────────────────────────┐
       │ API Route 接收请求,调用 streamText         │
       └─────────────────────────────────────────────┘
        │
        ▼
200ms  ┌─────────────────────────────────────────────┐
       │ AI SDK 调用 Claude API (stream: true)       │
       └─────────────────────────────────────────────┘
        │
        ▼
250ms  ┌─────────────────────────────────────────────┐
       │ Claude 返回第一个 SSE 数据块: "我"          │
       └─────────────────────────────────────────────┘
        │
        ▼
300ms  ┌─────────────────────────────────────────────┐
       │ 客户端收到 "我",立即渲染到屏幕              │
       │ 用户看到: "我"                              │
       └─────────────────────────────────────────────┘
        │
        ▼
350ms  ┌─────────────────────────────────────────────┐
       │ Claude 返回: "来"                           │
       │ 用户看到: "我来"                            │
       └─────────────────────────────────────────────┘
        │
        ▼
400ms  ┌─────────────────────────────────────────────┐
       │ Claude 返回: "帮您查询天气信息。"           │
       │ 用户看到: "我来帮您查询天气信息。"          │
       └─────────────────────────────────────────────┘
        │
        ▼
450ms  ┌─────────────────────────────────────────────┐
       │ Claude 返回工具调用: getWeather(city="北京") │
       │ 渲染工具调用卡片                            │
       └─────────────────────────────────────────────┘
        │
        ▼
500ms  ┌─────────────────────────────────────────────┐
       │ 服务器执行 getWeather 工具                   │
       │ fetch('https://api.weather.com/...')        │
       └─────────────────────────────────────────────┘
        │
        ▼
800ms  ┌─────────────────────────────────────────────┐
       │ 工具返回结果: { temp: 22, condition: "晴" } │
       │ 渲染工具结果卡片                            │
       └─────────────────────────────────────────────┘
        │
        ▼
850ms  ┌─────────────────────────────────────────────┐
       │ 将工具结果注入 LLM,继续生成                 │
       └─────────────────────────────────────────────┘
        │
        ▼
900ms  ┌─────────────────────────────────────────────┐
       │ Claude 返回: "根据查询结果,"               │
       │ 用户看到: "...根据查询结果,"                │
       └─────────────────────────────────────────────┘
        │
        ▼
950ms  ┌─────────────────────────────────────────────┐
       │ Claude 返回: "北京今天天气晴朗,"           │
       └─────────────────────────────────────────────┘
        │
        ▼
1000ms ┌─────────────────────────────────────────────┐
       │ Claude 返回: "气温 22°C,湿度 45%。"        │
       └─────────────────────────────────────────────┘
        │
        ▼
1050ms ┌─────────────────────────────────────────────┐
       │ 流结束标记: finishReason: "stop"            │
       └─────────────────────────────────────────────┘
        │
        ▼
1100ms ┌─────────────────────────────────────────────┐
       │ 完整消息渲染完成                            │
       │ 用户看到完整的对话和工具调用过程            │
       └─────────────────────────────────────────────┘

总耗时: 约 1.1 秒
用户体验: 实时看到文字逐字出现,流畅自然

四、多模态输入处理(图片)

// 当用户上传图片时的处理流程

// 1. 客户端:转换为 base64
async function handleImageUpload(file: File) {
  const base64 = await fileToBase64(file);

  append({
    role: 'user',
    content: [
      { type: 'text', text: '这张图片里是什么?' },
      { type: 'image', image: base64 },
    ],
  });
}

// 2. API Route:处理多模态消息
export async function POST(req: Request) {
  const { messages } = await req.json();

  // 转换消息格式(AI SDK 自动处理图片)
  const modelMessages = await convertToModelMessages(messages);
  // 结果:
  // [
  //   {
  //     role: 'user',
  //     content: [
  //       { type: 'text', text: '这张图片里是什么?' },
  //       { type: 'image', image: { type: 'base64', data: '...' } },
  //     ],
  //   },
  // ]

  // 3. 调用 LLM(支持多模态的模型)
  const result = await streamText({
    model: anthropic('claude-sonnet-4-6-20250514'),  // 支持图片
    messages: modelMessages,
  });

  return toUIMessageStreamResponse(result.toDataStream());
}

// 4. LLM 处理图片并流式返回描述
// 返回的流可能包含:
// data: 0:"这张"
// data: 0:"图片"
// data: 0:"显示"
// data: 0:"的是"
// data: 0:"一只"
// data: 0:"可爱"
// data: 0:"的"
// data: 0:"猫咪"
// data: 0:"。"

五、关键技术点总结

技术点说明实现方式
SSE 流式传输服务器主动推送数据Server-Sent Events 协议
消息格式转换UIMessage ↔ ModelMessageconvertToModelMessages()
多模态支持文本 + 图片 + 音频content 数组,多种 type
工具调用流程LLM → 工具 → 结果 → LLMmaxSteps 控制循环次数
状态管理客户端消息状态useChat Hook + React state
增量渲染逐字显示响应parts 数组累积更新
错误处理流中断、工具失败try/catch + 重试机制

六、SSE (Server-Sent Events) 协议详解

6.1 SSE 基本格式

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

data: 消息内容1

data: 消息内容2

event: 自定义事件名
data: 事件数据

id: 消息ID
data: 带ID的消息

retry: 3000
data: 重试间隔(毫秒)

6.2 SSE vs WebSocket

特性SSEWebSocket
方向单向(服务器→客户端)双向
协议HTTP独立协议(ws://)
自动重连原生支持需手动实现
浏览器支持广泛广泛
数据格式纯文本二进制/文本

6.3 SSE 在 AI 应用中的优势

  1. 简单易用:基于 HTTP,无需额外协议
  2. 自动重连:连接断开自动重新建立
  3. 文本友好:适合流式文本传输
  4. 防火墙友好:基于 HTTP,穿透性好

七、性能优化建议

7.1 客户端优化

// 1. 使用虚拟滚动处理大量消息
import { useVirtualizer } from '@tanstack/react-virtual';

// 2. 防抖处理用户输入
import { debounce } from 'lodash';

const debouncedSend = debounce(handleSend, 300);

// 3. 取消未完成的请求
const controller = new AbortController();
fetch('/api/chat', { signal: controller.signal });

7.2 服务端优化

// 1. 设置合理超时
const result = await streamText({
  // ...
  timeout: 30000,  // 30秒超时
});

// 2. 使用缓存
const cachedResponse = await cache.get(cacheKey);
if (cachedResponse) return cachedResponse;

// 3. 限流保护
import { rateLimit } from '@/lib/rate-limit';

const { success } = await rateLimit.limit(userId);
if (!success) return new Response('Too Many Requests', { status: 429 });

八、总结

本文详细解析了 AI Agent 从用户输入到流式响应的完整技术流程,核心要点包括:

  1. 完整数据流:用户输入 → useChat → API Route → AI SDK → LLM → 工具执行 → SSE 流 → 客户端渲染
  2. SSE 流式传输:使用 Server-Sent Events 实现实时数据推送
  3. 消息格式转换:UIMessage 和 ModelMessage 之间的转换
  4. 多模态支持:文本、图片等多种输入类型的处理
  5. 增量渲染:通过 parts 数组实现逐字显示
  6. 工具调用集成:流式响应中无缝集成工具调用

流式响应是现代 AI 应用的核心技术,通过 SSE 协议和增量渲染,为用户提供流畅自然的交互体验。


本文基于 Vercel AI SDK v6 和 Next.js 16 编写,如有更新请以官方文档为准。