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 ↔ ModelMessage | convertToModelMessages() |
| 多模态支持 | 文本 + 图片 + 音频 | content 数组,多种 type |
| 工具调用流程 | LLM → 工具 → 结果 → LLM | maxSteps 控制循环次数 |
| 状态管理 | 客户端消息状态 | 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
| 特性 | SSE | WebSocket |
|---|---|---|
| 方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP | 独立协议(ws://) |
| 自动重连 | 原生支持 | 需手动实现 |
| 浏览器支持 | 广泛 | 广泛 |
| 数据格式 | 纯文本 | 二进制/文本 |
6.3 SSE 在 AI 应用中的优势
- 简单易用:基于 HTTP,无需额外协议
- 自动重连:连接断开自动重新建立
- 文本友好:适合流式文本传输
- 防火墙友好:基于 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 从用户输入到流式响应的完整技术流程,核心要点包括:
- 完整数据流:用户输入 → useChat → API Route → AI SDK → LLM → 工具执行 → SSE 流 → 客户端渲染
- SSE 流式传输:使用 Server-Sent Events 实现实时数据推送
- 消息格式转换:UIMessage 和 ModelMessage 之间的转换
- 多模态支持:文本、图片等多种输入类型的处理
- 增量渲染:通过 parts 数组实现逐字显示
- 工具调用集成:流式响应中无缝集成工具调用
流式响应是现代 AI 应用的核心技术,通过 SSE 协议和增量渲染,为用户提供流畅自然的交互体验。
本文基于 Vercel AI SDK v6 和 Next.js 16 编写,如有更新请以官方文档为准。