第 19 章:集成 AI 能力 (Vercel AI SDK)
在本书的前 18 章中,我们已经构建了一个极其坚实的 SAAS 基础。我们拥有了支付 (Stripe)、认证 (better-auth)、数据库 (Drizzle)、DevOps (GitHub Actions) 和功能发布 (Feature Flags) 的所有核心组件。现在,是时候为我们的 SAAS 注入“智...
第 19 章:集成 AI 能力 (Vercel AI SDK)
在本书的前 18 章中,我们已经构建了一个极其坚实的 SAAS 基础。我们拥有了支付 (Stripe)、认证 (better-auth)、数据库 (Drizzle)、DevOps (GitHub Actions) 和功能发布 (Feature Flags) 的所有核心组件。现在,是时候为我们的 SAAS 注入“智能”了——这正是本书副标题中“AI SAAS”的承诺。
对于 Python 开发者来说,你可能非常熟悉使用 requests 库调用 OpenAI API,或者使用 boto3 与 AWS Bedrock 交互。这些操作通常发生在 Flask 或 Django 的后端,你需要手动处理 API 请求、错误、重试,并将最终的 JSON 结果返回给前端(前端再想办法处理)。
这个流程在处理 AI 流式响应 (Streaming) 时会变得极其复杂。你如何将一个分块传输的 (chunked) HTTP 响应从 Python 后端代理到 React 前端,并实时更新 UI?这需要复杂的 WebSockets 或 Server-Sent Events (SSE) 设置。
Vercel AI SDK 就是为了解决这个问题而生的。它是一个与框架无关的(但对 Next.js 极其友好)工具包,旨在以最简单的方式在 JavaScript/TypeScript 应用中构建 AI 驱动的、流式优先的用户界面。
本章,我们将实战 Vercel AI SDK,并探讨如何构建一个可插拔的后端架构,以支持多个 AI 提供商。
19.1. [Skill 实战]:使用 Vercel AI SDK 快速实现流式响应
[Skill 实战:claude-nextjs-skills/vercel-ai-sdk/SKILL.md]
Vercel AI SDK 不是一个 AI 模型。它是一个适配器和UI 助手。它的核心价值在于提供了两个强大的 React Hook:
useChat():用于构建类似 ChatGPT 的多轮对话界面。useCompletion():用于简单的“输入-输出”式补全(例如,“总结这段文字”)。
这些 Hook 会自动为你处理所有前端的复杂性:管理消息列表(messages)、处理用户输入(input)、跟踪加载状态(isLoading)以及将流式响应实时渲染到 UI。
让我们来构建一个经典的聊天机器人界面。这需要两部分:
文件 1:src/app/api/chat/route.ts (后端 API)
首先,我们需要一个 Route Handler 来接收前端的请求,并安全地调用 AI API。Vercel AI SDK 提供了与 OpenAI、Google Gemini、Anthropic 等库的无缝集成。
// src/app/api/chat/route.ts
import { OpenAI } from 'openai';
import { OpenAIStream, StreamingTextResponse } from 'ai';
// 从 .env.local 中创建 OpenAI 客户端
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// 关键: 将 Vercel 部署切换到 Edge 运行时
// 这使得响应速度极快,因为它在全球边缘节点运行
export const runtime = 'edge';
export async function POST(req: Request) {
try {
// 从 `useChat` Hook 的请求中解析出消息
const { messages } = await req.json();
// 检查积分 (来自第 12 章)
// (伪代码:你需要一个方法来从请求中获取 userId)
// const userId = ...;
// const creditResult = await consumeCredits(userId, 1, 'AI Chat Completion');
// if (creditResult.error) {
// return new Response(JSON.stringify(creditResult), { status: 402 }); // 402 Payment Required
// }
// 1. 调用 OpenAI API
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
stream: true, // 关键:开启流式响应
messages: messages,
});
// 2. 将 OpenAI 的响应转换为 Vercel AI SDK 的流
const stream = OpenAIStream(response);
// 3. 将流式响应返回给前端
// 这会自动处理所有分块传输的复杂性
return new StreamingTextResponse(stream);
} catch (error) {
console.error('Chat API Error:', error);
return new Response(JSON.stringify({ error: 'Failed to get response' }), { status: 500 });
}
}架构解析:
runtime = 'edge':这是一个重大的性能优化。我们的聊天 API 不再是一个 Node.js Serverless Function,而是一个在全球分发的 Edge Function。这大大降低了 AI 响应的“首字节时间”(TTFB)。OpenAIStream和StreamingTextResponse:这是 Vercel AI SDK 的魔力所在。你不需要手动管理数据块,只需将 OpenAI 的原始流“管道”(pipe) 传给OpenAIStream,然后将其包装在StreamingTextResponse中返回。
文件 2:src/components/chat/ChatInterface.tsx (前端 UI)
现在是前端。在客户端组件中,我们只需要使用 useChat Hook 就可以驱动整个 UI。
// src/components/chat/ChatInterface.tsx
'use client';
import { useChat } from 'ai/react';
import { useEffect } from 'react';
import { toast } from 'sonner'; // (假设使用了 sonner 来显示提示)
export function ChatInterface() {
const { messages, input, handleInputChange, handleSubmit, isLoading, error } = useChat({
// `useChat` 会自动向这个 API 发送 POST 请求
api: '/api/chat',
// (可选) 在流式响应完成时
onFinish: (message) => {
console.log('Stream finished. Final message:', message);
// 你可以在这里触发积分消耗的“最终确认”
},
// (可选) 处理错误
onError: (err) => {
if (err.message.includes('402')) {
toast.error('Insufficient credits. Please upgrade your plan.');
} else {
toast.error('An error occurred. Please try again.');
}
}
});
return (
<div className="flex flex-col h-[500px] border rounded-lg">
{/* 消息显示区域 */}
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{messages.map((m) => (
<div key={m.id} className={`p-2 rounded-lg ${
m.role === 'user' ? 'bg-blue-100 text-right' : 'bg-gray-100'
}`}>
<span className="font-bold">
{m.role === 'user' ? 'You' : 'AI'}:
</span>
<span className="whitespace-pre-wrap">{m.content}</span>
</div>
))}
{isLoading && <div className="text-gray-500">AI is typing...</div>}
</div>
{/* 输入表单 */}
{/* `handleSubmit` 会自动处理表单提交、
添加用户消息到 `messages` 列表、
并调用 API
*/}
<form onSubmit={handleSubmit} className="flex p-2 border-t">
<input
value={input}
onChange={handleInputChange}
placeholder="Ask me anything..."
className="flex-1 p-2 border rounded-lg"
disabled={isLoading}
/>
<button type="submit" disabled={isLoading} className="ml-2 p-2 bg-blue-500 text-white rounded-lg">
Send
</button>
</form>
</div>
);
}就是这样!我们只用了两个文件,就构建了一个功能齐全、支持流式响应、能处理加载和错误状态的 AI 聊天界面。useChat Hook 就像一个状态机,为我们管理了一切。
19.2. [代码解析]:分析 AI 抽象层 (避免厂商锁定)
19.1 节中的方案非常棒,但它有一个架构缺陷:我们的 src/app/api/chat/route.ts 硬编码了 openai。
- 如果 OpenAI API 宕机了怎么办?
- 如果 Google 的
Gemini-Pro对某些任务更便宜、效果更好怎么办? - 如果我们想为
generateImage()功能集成Replicate(Stable Diffusion) 怎么办?
一个成熟的 AI SAAS 绝不能将自己锁定在单一的 AI 提供商上。我们需要一个抽象层 (Abstraction Layer)。
让我们设计一个假设的 src/lib/ai-provider.ts (或 src/ai-interface.ts)。
// src/lib/ai-provider.ts
import { OpenAI } from 'openai';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { OpenAIStream, GoogleGenerativeAIStream, StreamData } from 'ai';
import { ChatCompletionMessageParam } from 'openai/resources';
// 1. 定义一个统一的提供商接口
interface IAiChatProvider {
// 接收一个通用的消息数组,返回一个通用的可读流
createChatStream(
messages: ChatCompletionMessageParam[]
): Promise<ReadableStream<StreamData>>;
}
// 2. 实现 OpenAI 提供商
class OpenAiProvider implements IAiChatProvider {
private openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async createChatStream(messages: ChatCompletionMessageParam[]) {
const response = await this.openai.chat.completions.create({
model: 'gpt-4o-mini',
stream: true,
messages: messages,
});
return OpenAIStream(response);
}
}
// 3. 实现 Google Gemini 提供商
class GoogleGeminiProvider implements IAiChatProvider {
private genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
// (注意: Gemini 的消息格式与 OpenAI 不同,我们需要一个转换器)
private convertMessagesToGemini(messages: ChatCompletionMessageParam[]) {
// ... 转换逻辑 ...
return messages.map(m => ({ role: m.role, parts: [{ text: m.content as string }] }));
}
async createChatStream(messages: ChatCompletionMessageParam[]) {
const model = this.genAI.getGenerativeModel({ model: 'gemini-1.5-pro' });
// Gemini 需要转换消息格式
const geminiMessages = this.convertMessagesToGemini(messages);
const stream = await model.generateContentStream({
contents: geminiMessages,
});
// 使用 Vercel AI SDK 提供的 Google 流转换器
return GoogleGenerativeAIStream(stream);
}
}
// 4. (可选) 实现 Replicate 提供商 (用于图像)
// ... 类似地,可以为图像生成定义一个 `createImageStream` 接口 ...
// 5. 创建一个工厂函数来选择提供商
function getAiProvider(): IAiChatProvider {
const provider = process.env.AI_PROVIDER || 'openai';
if (provider === 'google') {
return new GoogleGeminiProvider();
}
// 默认使用 OpenAI
return new OpenAiProvider();
}
// 6. 导出我们的主 AI 接口
export const ai = {
chat: getAiProvider(),
// image: getImageProvider(), // (用于图像功能)
};联动:重构 src/app/api/chat/route.ts
现在,我们的 API 路由变得极其简洁和“干净”。它不再关心_谁_是 AI 提供商。
// src/app/api/chat/route.ts (重构后)
import { StreamingTextResponse } from 'ai';
import { ai } from '@/lib/ai-provider'; // 导入我们的抽象层
export const runtime = 'edge';
export async function POST(req: Request) {
try {
const { messages } = await req.json();
// ... (积分检查逻辑) ...
// 1. 调用抽象接口,而不是 OpenAI
// ai.chat 可能是 OpenAI 或 Google,这个 API 并不关心
const stream = await ai.chat.createChatStream(messages);
// 2. 将流返回给前端
return new StreamingTextResponse(stream);
} catch (error) {
console.error('Chat API Error:', error);
return new Response(JSON.stringify({ error: 'Failed to get response' }), { status: 500 });
}
}通过这种架构,我们现在只需要更改一个环境变量 AI_PROVIDER="google",我们的 SAAS 就可以在_不重新部署代码_的情况下 (如果使用了 Vercel Edge Config) 实时从 OpenAI 切换到 Google Gemini,从而实现了高可用性和成本控制。
第 19 章总结
在本章中,我们终于为我们的 SAAS 注入了核心的“智能”。
- Vercel AI SDK:我们利用
ai包提供的useChatHook(前端)和StreamingTextResponse(后端),以最小的代价构建了一个功能完善、支持流式响应的 AI 聊天界面。这解决了传统架构中处理流式数据的巨大痛点。 - Edge 运行时:我们将 AI API 路由部署到了
edge运行时,极大地降低了全球用户的访问延迟。 - 架构解耦 (抽象层):我们设计了一个
ai-provider.ts抽象层,它定义了一个统一的IAiChatProvider接口。这使得我们的 API 路由与具体的 AI 提供商(如 OpenAI 或 Google)解耦。 - 避免厂商锁定:这种抽象层架构使我们的 SAAS 具备了极高的灵活性,可以根据成本、性能或可用性,通过一个环境变量就动态切换底层的 AI 模型,这是构建一个健壮、可扩展 AI SAAS 的关键。