AI SDK 前端集成与流式响应实践
Vercel AI SDK、OpenAI API 流式调用、SSE 处理和 AI 聊天界面的前端架构设计。
2024–2026 年,前端工程师最重要的新技能是将 LLM 集成到产品中。这不是调 API 那么简单,而是涉及流式渲染、错误回退、Token 管理和用户体验设计。
AI 前端架构全景
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Chat UI │────▶│ API Route │────▶│ LLM Provider│
│ (React) │◀────│ (Server) │◀────│ OpenAI/等 │
│ 流式渲染 │ SSE │ 流式转发 │ SSE │ │
└─────────────┘ └──────────────┘ └─────────────┘
核心原则:API Key 永远不暴露到客户端。
Vercel AI SDK 快速集成
// app/api/chat/route.ts — Server Route
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages,
system: "你是一个前端技术助手。",
});
return result.toDataStreamResponse();
}
// components/Chat.tsx — Client Component
"use client";
import { useChat } from "ai/react";
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({
api: "/api/chat",
});
return (
<div>
{messages.map((m) => (
<div key={m.id} className={m.role}>
{m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
disabled={isLoading}
/>
</form>
</div>
);
}
流式响应的底层原理
Client Server LLM
│── POST /api/chat ──▶│ │
│ │── stream request ──────▶│
│ │◀── chunk 1 ────────────│
│◀── SSE: data chunk ──│ │
│ │◀── chunk 2 ────────────│
│◀── SSE: data chunk ──│ │
│ │◀── [DONE] ──────────────│
│◀── SSE: [DONE] ─────│ │
SSE(Server-Sent Events)格式:
data: {"text": "你好"}\n\n
data: {"text": ",我是"}\n\n
data: [DONE]\n\n
流式 Markdown 渲染
AI 输出 Markdown 时,流式渲染的挑战是不完整的 Markdown 语法:
function StreamingMarkdown({ content }: { content: string }) {
// 策略 1:逐字追加,最后统一渲染
const [displayContent, setDisplayContent] = useState("");
useEffect(() => {
// 防抖:每 50ms 更新一次渲染,避免频繁 re-render
const timer = setTimeout(() => setDisplayContent(content), 50);
return () => clearTimeout(timer);
}, [content]);
return <MarkdownRenderer content={displayContent} />;
}
错误处理与回退
const { messages, error, reload, stop } = useChat({
api: "/api/chat",
onError: (err) => {
if (err.message.includes("rate_limit")) {
toast.error("请求过于频繁,请稍后再试");
} else if (err.message.includes("context_length")) {
toast.error("对话过长,请开启新对话");
}
},
});
必须设计的回退场景:
- API 超时 → 显示重试按钮
- Rate Limit → 排队提示
- 内容审核拦截 → 友好提示
- 网络断开 → 保留已生成内容 + 重连
Token 管理与成本控制
// Server 端 Token 计数
import { encode } from "gpt-tokenizer";
function trimMessages(messages: Message[], maxTokens: number) {
let total = 0;
const trimmed = [];
for (let i = messages.length - 1; i >= 0; i--) {
const tokens = encode(messages[i].content).length;
if (total + tokens > maxTokens) break;
trimmed.unshift(messages[i]);
total += tokens;
}
return trimmed;
}