关于 ai-sdk-ui
ai-sdk-ui 是使用 Vercel AI SDK v5 构建 React 界面的参考技能,涵盖聊天、补全和结构化对象 Hook。内容还包括流式响应、Next.js 模式、迁移与故障排查。
主要功能
- 01useChat、useCompletion 和 useObject 指南
- 02聊天与补全的流式模式
- 03工具调用展示与文件附件
- 04聊天记录和消息状态管理
- 05Next.js App Router 与 Pages Router 示例
- 06v4 到 v5 迁移及故障排查
适用场景
- 01构建支持流式响应的 React 聊天界面
- 02在 UI 中加入 AI 补全或结构化输出
- 03在聊天中展示工具调用并附加文件
- 04将 UI Hook 从 AI SDK v4 迁移到 v5
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jackspace/ claudeskillz --skill ai-sdk-ui安装后在 Claude Code 中输入/ai-sdk-ui确认已加载。
附带文件README.mdSKILL.json
详细介绍
AI SDK UI - 前端 React Hooks
借助 Vercel AI SDK v5 为 AI 驱动的用户界面提供前端 React hooks。
版本:AI SDK v5.0.76+(稳定版) 框架:React 18+、Next.js 14+ 最后更新:2025-10-22
快速入门(5 分钟)
安装
npm install ai @ai-sdk/openai
基本聊天组件(v5)
// app/chat/page.tsx
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function Chat() {
const { messages, sendMessage, isLoading } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};
return (
<div>
<div>
{messages.map(m => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
/>
</form>
</div>
);
}
API 路由(Next.js App Router)
// app/api/chat/route.ts
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-4-turbo'),
messages,
});
return result.toDataStreamResponse();
}
结果:仅需约 10 行前端代码,即可获得一个功能完备的聊天界面,并支持 AI 响应流式传输。
useChat Hook - 完整参考
基本用法(v5 模式)
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function ChatComponent() {
const { messages, sendMessage, isLoading, error } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ content: input });
setInput('');
};
return (
<div className="flex flex-col h-screen">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4">
{messages.map(message => (
<div
key={message.id}
className={message.role === 'user' ? 'text-right' : 'text-left'}
>
<div className="inline-block p-2 rounded bg-gray-100">
{message.content}
</div>
</div>
))}
{isLoading && <div className="text-gray-500">AI is thinking...</div>}
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="w-full p-2 border rounded"
/>
</form>
{/* Error */}
{error && <div className="text-red-500 p-4">{error.message}</div>}
</div>
);
}
完整 API 参考
const {
// Messages
messages, // Message[] - Chat history
setMessages, // (messages: Message[]) => void - Update messages
// Actions
sendMessage, // (message: { content: string }) => void - Send message (v5)
reload, // () => void - Reload last response
stop, // () => void - Stop current generation
// State
isLoading, // boolean - Is AI responding?
error, // Error | undefined - Error if any
// Data
data, // any[] - Custom data from stream
metadata, // object - Response metadata
} = useChat({
// Required
api: '/api/chat', // API endpoint
// Optional
id: 'chat-1', // Chat ID for persistence
initialMessages: [], // Initial messages (controlled mode)
// Callbacks
onFinish: (message, options) => {}, // Called when response completes
onError: (error) => {}, // Called on error
// Configuration
headers: {}, // Custom headers
body: {}, // Additional body data
credentials: 'same-origin', // Fetch credentials
// Streaming
streamProtocol: 'data', // 'data' | 'text' (default: 'data')
});
v4 → v5 破坏性变更
重要:v5 中的 useChat 不再管理输入状态!
v4(旧版 - 请勿使用):
const { messages, input, handleInputChange, handleSubmit, append } = useChat();
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
v5(新版 - 正确用法):
const { messages, sendMessage } = useChat();
const [input, setInput] = useState('');
<form onSubmit={(e) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
}}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
v5 变更摘要:
- 移除输入管理:不再提供
input、handleInputChange和handleSubmit append()→sendMessage():用于发送消息的新方法- 移除
onResponse:改用onFinish initialMessages→ 受控模式:使用messages属性进行全面控制- 移除
maxSteps:仅在服务端处理
完整迁移指南请参阅 references/use-chat-migration.md。
在 UI 中调用工具
当 API 使用工具时,useChat 会自动处理消息流中的工具调用:
'use client';
import { useChat } from 'ai/react';
export default function ChatWithTools() {
const { messages } = useChat({ api: '/api/chat' });
return (
<div>
{messages.map(message => (
<div key={message.id}>
{/* Text content */}
{message.content && <p>{message.content}</p>}
{/* Tool invocations */}
{message.toolInvocations?.map((tool, idx) => (
<div key={idx} className="bg-blue-50 p-2 rounded my-2">
<div className="font-bold">Tool: {tool.toolName}</div>
<div className="text-sm">
<strong>Args:</strong> {JSON.stringify(tool.args, null, 2)}
</div>
{tool.result && (
<div className="text-sm">
<strong>Result:</strong> {JSON.stringify(tool.result, null, 2)}
</div>
)}
</div>
))}
</div>
))}
</div>
);
}
文件附件
随消息上传文件(图像、PDF 等):
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function ChatWithAttachments() {
const { messages, sendMessage, isLoading } = useChat({ api: '/api/chat' });
const [input, setInput] = useState('');
const [files, setFiles] = useState<FileList | null>(null);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({
content: input,
experimental_attachments: files
? Array.from(files).map(file => ({
name: file.name,
contentType: file.type,
url: URL.createObjectURL(file),
}))
: undefined,
});
setInput('');
setFiles(null);
};
return (
<div>
{/* Messages */}
{messages.map(m => (
<div key={m.id}>
{m.content}
{m.experimental_attachments?.map((att, idx) => (
<div key={idx}>
<img src={att.url} alt={att.name} />
</div>
))}
</div>
))}
{/* Input */}
<form onSubmit={handleSubmit}>
<input
type="file"
multiple
onChange={(e) => setFiles(e.target.files)}
accept="image/*"
/>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit" disabled={isLoading}>Send</button>
</form>
</div>
);
}
消息持久化
将聊天记录保存到 localStorage 并从中加载:
'use client';
import { useChat } from 'ai/react';
import { useEffect } from 'react';
export default function PersistentChat() {
const chatId = 'my-chat-1';
const { messages, setMessages, sendMessage } = useChat({
api: '/api/chat',
id: chatId,
initialMessages: loadMessages(chatId),
});
// Save messages whenever they change
useEffect(() => {
saveMessages(chatId, messages);
}, [messages, chatId]);
return (
<div>
{messages.map(m => (
<div key={m.id}>{m.role}: {m.content}</div>
))}
{/* Input form... */}
</div>
);
}
// Helper functions
function loadMessages(chatId: string) {
const stored = localStorage.getItem(`chat-${chatId}`);
return stored ? JSON.parse(stored) : [];
}
function saveMessages(chatId: string, messages: any[]) {
localStorage.setItem(`chat-${chatId}`, JSON.stringify(messages));
}
useCompletion Hook - 完整参考
基本用法
'use client';
import { useCompletion } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function Completion() {
const { completion, complete, isLoading, error } = useCompletion({
api: '/api/completion',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
complete(input);
setInput('');
};
return (
<div>
<form onSubmit={handleSubmit}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Enter a prompt..."
rows={4}
className="w-full p-2 border rounded"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Generating...' : 'Generate'}
</button>
</form>
{completion && (
<div className="mt-4 p-4 bg-gray-50 rounded">
<h3>Result:</h3>
<p>{completion}</p>
</div>
)}
{error && <div className="text-red-500">{error.message}</div>}
</div>
);
}
完整 API 参考
const {
completion, // string - Current completion text
complete, // (prompt: string) => void - Trigger completion
setCompletion, // (completion: string) => void - Update completion
isLoading, // boolean - Is generating?
error, // Error | undefined - Error if any
stop, // () => void - Stop generation
} = useCompletion({
api: '/api/completion',
id: 'completion-1',
// Callbacks
onFinish: (prompt, completion) => {},
onError: (error) => {},
// Configuration
headers: {},
body: {},
});
useCompletion 的 API 路由
// app/api/completion/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: openai('gpt-3.5-turbo'),
prompt,
maxOutputTokens: 500,
});
return result.toDataStreamResponse();
}
useObject Hook - 完整参考
基本用法
借助实时更新,以流式方式传输结构化数据(例如表单、JSON 对象):
'use client';
import { useObject } from 'ai/react';
import { z } from 'zod';
const recipeSchema = z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(z.string()),
instructions: z.array(z.string()),
}),
});
export default function RecipeGenerator() {
const { object, submit, isLoading, error } = useObject({
api: '/api/recipe',
schema: recipeSchema,
});
return (
<div>
<button onClick={() => submit('pasta carbonara')} disabled={isLoading}>
Generate Recipe
</button>
{isLoading && <div>Generating recipe...</div>}
{object?.recipe && (
<div className="mt-4">
<h2 className="text-2xl font-bold">{object.recipe.name}</h2>
<h3 className="text-xl mt-4">Ingredients:</h3>
<ul>
{object.recipe.ingredients?.map((ingredient, idx) => (
<li key={idx}>{ingredient}</li>
))}
</ul>
<h3 className="text-xl mt-4">Instructions:</h3>
<ol>
{object.recipe.instructions?.map((step, idx) => (
<li key={idx}>{step}</li>
))}
</ol>
</div>
)}
{error && <div className="text-red-500">{error.message}</div>}
</div>
);
}
完整 API 参考
const {
object, // Partial<T> - Partial object (updates as stream progresses)
submit, // (input: string) => void - Trigger generation
isLoading, // boolean - Is generating?
error, // Error | undefined - Error if any
stop, // () => void - Stop generation
} = useObject({
api: '/api/object',
schema: zodSchema, // Zod schema
// Callbacks
onFinish: (object) => {},
onError: (error) => {},
});
useObject 的 API 路由
// app/api/recipe/route.ts
import { streamObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamObject({
model: openai('gpt-4'),
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(z.string()),
instructions: z.array(z.string()),
}),
}),
prompt: `Generate a recipe for ${prompt}`,
});
return result.toTextStreamResponse();
}
Next.js 集成
App Router 完整示例
目录结构:
app/
├── api/
│ └── chat/
│ └── route.ts # Chat API endpoint
├── chat/
│ └── page.tsx # Chat page
└── layout.tsx
聊天页面:
// app/chat/page.tsx
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent, useRef, useEffect } from 'react';
export default function ChatPage() {
const { messages, sendMessage, isLoading, error } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ content: input });
setInput('');
};
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map(message => (
<div
key={message.id}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[70%] p-3 rounded-lg ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-900'
}`}
>
{message.content}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-200 p-3 rounded-lg">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-100"></div>
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-200"></div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border-t border-red-200 text-red-700">
Error: {error.message}
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="flex space-x-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Send
</button>
</div>
</form>
</div>
);
}
API 路由:
// app/api/chat/route.ts
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-4-turbo'),
messages,
system: 'You are a helpful AI assistant.',
maxOutputTokens: 1000,
});
return result.toDataStreamResponse();
}
Pages Router 完整示例
目录结构:
pages/
├── api/
│ └── chat.ts # Chat API endpoint
└── chat.tsx # Chat page
聊天页面:
// pages/chat.tsx
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function ChatPage() {
const { messages, sendMessage, isLoading } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">AI Chat</h1>
<div className="border rounded p-4 h-96 overflow-y-auto mb-4">
{messages.map(m => (
<div key={m.id} className="mb-4">
<strong>{m.role === 'user' ? 'You' : 'AI'}:</strong> {m.content}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex space-x-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="flex-1 p-2 border rounded"
/>
<button
type="submit"
disabled={isLoading}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Send
</button>
</form>
</div>
);
}
API 路由:
// pages/api/chat.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { messages } = req.body;
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
});
// Pages Router uses pipeDataStreamToResponse
return result.pipeDataStreamToResponse(res);
}
主要区别:App Router 使用 toDataStreamResponse(),Pages Router 使用 pipeDataStreamToResponse()。
常见 UI 错误及解决方案
完整文档请参阅 references/top-ui-errors.md。快速参考:
1. useChat 无法解析流
错误:SyntaxError: Unexpected token in JSON at position X
原因:API 路由未返回正确的流格式。
解决方案:
// ✅ CORRECT
return result.toDataStreamResponse();
// ❌ WRONG
return new Response(result.textStream);
2. useChat 无响应
原因:API 路由未能正确进行流式传输。
解决方案:
// App Router - use toDataStreamResponse()
export async function POST(req: Request) {
const result = streamText({ /* ... */ });
return result.toDataStreamResponse(); // ✅
}
// Pages Router - use pipeDataStreamToResponse()
export default async function handler(req, res) {
const result = streamText({ /* ... */ });
return result.pipeDataStreamToResponse(res); // ✅
}
3. 部署后无法进行流式传输
原因:部署平台对响应进行了缓冲。
解决方案:Vercel 会自动检测流式传输。其他平台可能需要配置。
4. useChat 中的 body 值过时
原因:body 选项仅在首次渲染时捕获。
解决方案:
// ❌ WRONG - body captured once
const { userId } = useUser();
const { messages } = useChat({
body: { userId }, // Stale!
});
// ✅ CORRECT - use controlled mode
const { userId } = useUser();
const { messages, sendMessage } = useChat();
sendMessage({
content: input,
data: { userId }, // Fresh on each send
});
5. React 更新深度超出最大值
原因:useEffect 中出现无限循环。
解决方案:
// ❌ WRONG
useEffect(() => {
saveMessages(messages);
}, [messages, saveMessages]); // saveMessages triggers re-render!
// ✅ CORRECT
useEffect(() => {
saveMessages(messages);
}, [messages]); // Only depend on messages
其他 7 个常见错误请参阅 references/top-ui-errors.md。
流式传输最佳实践
性能
始终使用流式传输,以获得更好的用户体验:
// ✅ GOOD - Streaming (shows tokens as they arrive)
const { messages } = useChat({ api: '/api/chat' });
// ❌ BAD - Non-streaming (user waits for full response)
const response = await fetch('/api/chat', { method: 'POST' });
用户体验模式
显示加载状态:
{isLoading && <div>AI is typing...</div>}
提供停止按钮:
{isLoading && <button onClick={stop}>Stop</button>}
自动滚动到最新消息:
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
加载期间禁用输入:
<input disabled={isLoading} />
全面的最佳实践请参阅 references/streaming-patterns.md。
何时使用此 Skill
适用于以下场景:
- 构建 React 聊天界面
- 在 UI 中实现 AI 补全
- 将 AI 响应流式传输到前端
- 构建 Next.js AI 应用
- 管理聊天消息状态
- 在 UI 中显示工具调用
- 管理 AI 文件附件
- 从 v4 迁移到 v5(UI hooks)
- 遇到 useChat/useCompletion 错误
不适用场景:
- 需要后端 AI 功能 → 改用 ai-sdk-core
- 构建非 React 前端(Svelte、Vue)→ 查看官方文档
- 需要生成式 UI / RSC → 请参阅 https://ai-sdk.dev/docs/ai-sdk-rsc
- 构建原生应用 → 需要使用其他 SDK
相关技能:
- ai-sdk-core - 后端文本生成、结构化输出、工具、Agent
- 组合使用两者,构建完整的全栈 AI 应用
软件包版本
必需:
{
"dependencies": {
"ai": "^5.0.76",
"@ai-sdk/openai": "^2.0.53",
"react": "^18.2.0",
"zod": "^3.23.8"
}
}
Next.js:
{
"dependencies": {
"next": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
版本说明:
- AI SDK v5.0.76+(稳定版)
- React 18+(支持 React 19)
- 推荐使用 Next.js 14+(13.4+ 也可用)
- 使用 Zod 3.23.8+ 进行架构验证
官方文档链接
核心 UI Hooks:
- AI SDK UI 概览:https://ai-sdk.dev/docs/ai-sdk-ui/overview
- useChat:https://ai-sdk.dev/docs/ai-sdk-ui/chatbot
- useCompletion:https://ai-sdk.dev/docs/ai-sdk-ui/completion
- useObject:https://ai-sdk.dev/docs/ai-sdk-ui/object-generation
进阶主题(仅链接):
- 生成式 UI(RSC):https://ai-sdk.dev/docs/ai-sdk-rsc/overview
- 流协议:https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocols
- 消息元数据:https://ai-sdk.dev/docs/ai-sdk-ui/message-metadata
Next.js 集成:
- Next.js App Router:https://ai-sdk.dev/docs/getting-started/nextjs-app-router
- Next.js Pages Router:https://ai-sdk.dev/docs/getting-started/nextjs-pages-router
迁移与故障排查:
- v4→v5 迁移:https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0
- 故障排查:https://ai-sdk.dev/docs/troubleshooting
- 常见问题:https://ai-sdk.dev/docs/troubleshooting/common-issues
Vercel 部署:
- Vercel Functions:https://vercel.com/docs/functions
- 在 Vercel 上进行流式传输:https://vercel.com/docs/functions/streaming
模板
此技能在 templates/ 中包含以下模板:
- use-chat-basic.tsx - 使用手动输入的基础聊天(v5 模式)
- use-chat-tools.tsx - 带工具调用 UI 渲染的聊天
- use-chat-attachments.tsx - 支持文件附件
- use-completion-basic.tsx - 基础文本补全
- use-object-streaming.tsx - 流式传输结构化数据
- nextjs-chat-app-router.tsx - Next.js App Router 完整示例
- nextjs-chat-pages-router.tsx - Next.js Pages Router 完整示例
- nextjs-api-route.ts - 适用于 App Router 和 Pages Router 的 API 路由
- message-persistence.tsx - 保存/加载聊天记录
- custom-message-renderer.tsx - 使用 Markdown 的自定义消息组件
- package.json - 依赖项模板
参考文档
请参阅 references/:
- use-chat-migration.md - 完整的 v4→v5 迁移指南
- streaming-patterns.md - UI 流式传输最佳实践
- top-ui-errors.md - 12 个常见 UI 错误及解决方案
- nextjs-integration.md - Next.js 设置模式
- links-to-official-docs.md - 分类整理的官方文档链接
生产环境测试:WordPress Auditor(https://wordpress-auditor.webfonts.workers.dev) 最后更新:2025-10-22