乐于分享
好东西不私藏

Vercel AI SDK 接了3个项目后,我总结出这套「防翻车」指南

Vercel AI SDK 接了3个项目后,我总结出这套「防翻车」指南

你有没有遇到过这种情况:照着官方文档写了个 AI 聊天页面,useChat 一跑,消息倒是能出来,但每次回复都会闪一下空白,用户体验直接拉胯?

翻了半天 issue 才发现,不是你代码写错了,是流式响应的状态管理有个隐藏的时序问题——而官方文档压根没提。

这就是 Vercel AI SDK 的典型体验:上手极快,但坑藏得极深。今天这篇,是我用它做了 3 个项目后,把所有踩过的坑、摸出的最佳实践,一次性讲清楚。

先搞明白一件事:AI SDK 现在到底是什么版本?

很多人上来就 npm install ai,装完也不看版本号就开干。但 2026 年的 AI SDK 生态已经不是一年前的样子了。

目前的情况是:稳定版 AI SDK 5,Beta 版 AI SDK 6。如果你是新项目,建议直接上 v6 Beta;如果是老项目维护,先别急着升——v4 到 v6 的 breaking changes 能让你改到怀疑人生。

举个真实的例子:maxTokens 改成了 maxOutputTokensCoreMessage 改成了 ModelMessagegenerateObject() 被废弃,tool 调用的 args 改成了 input。这些改动分散在十几个文件里,编译器不会提醒你,只有运行时才会崩。

一个血泪教训:升级前先锁版本,逐个文件排查 API 变更,别想着一把梭。

useChat 用起来爽,但这 3 个坑你一定会踩

useChat 是 AI SDK 最核心的 React Hook,一行代码就能搞定聊天界面的状态管理。但"能用"和"好用"之间,隔着三个大坑。

坑一:消息闪烁的元凶——状态更新时序

useChat 的时候,你大概率会写出这样的代码:

'use client';
import { useChat } from '@ai-sdk/react';

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>{m.content}</div>
      ))}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
      </form>
    </div>
  );
}

看起来没毛病对吧?但实际运行时,AI 回复的第一帧会出现一个空消息气泡,然后才开始填充内容。原因是 useChat 在收到流的第一个 chunk 之前,就已经往 messages 数组里 push 了一条空的 assistant 消息。

解决方案很简单,渲染时过滤掉空内容:

{messages
  .filter((m) => m.content.trim() !== '')
  .map((m) => (
    <div key={m.id}>{m.content}</div>
  ))}

一行代码的事,但不知道原因的话能调半天。

坑二:多轮对话的内存泄漏

useChat 默认会把所有历史消息都存在内存里,每次请求都带上完整的 messages 数组发给后端。聊个 20 轮,请求体就膨胀到几十 KB,token 消耗也跟着飙升。

我的做法是在后端做消息截断:

import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';

export async function POST(req: Request) {
  const { messages } = await req.json();
  // 只保留最近 10 轮对话 + system prompt
  const recentMessages = messages.slice(-20);

  const result = await streamText({
    model: openai('gpt-4-turbo'),
    messages: recentMessages,
  });

  return result.toDataStreamResponse();
}

别在前端截断,因为 useChat 的内部状态和你手动改的 messages 会打架。后端截断是最干净的方案。

坑三:错误处理的"沉默失败"

useChat 默认的错误处理是——没有错误处理。API 返回 500,前端不报错,消息列表里也不会显示任何提示,用户只会看到"AI 不说话了"。

必须手动加上 onError 回调:

const { messages, input, handleSubmit } = useChat({
  onError: (error) => {
    toast.error('AI 开小差了,请稍后重试');
    console.error('Chat error:', error);
  },
});

这三个坑,官方文档一个都没重点提。但凡做过真实项目的人,100% 会遇到。

streamText vs generateText:选错了性能差 10 倍

AI SDK 提供了两种核心的文本生成方式:streamText(流式)和 generateText(一次性返回)。很多人觉得"流式更酷"就无脑用 streamText,但实际上选错场景会严重影响性能。

什么时候用 streamText - 聊天界面,需要逐字显示 - 长文本生成,用户需要看到进度 - 任何需要"打字机效果"的场景

候用 generateText* - 后台任务,比如批量生成摘要 - 结构化数据提取,比如从文本中提取 JSON - 不需要实时展示的场景

我在一个项目里犯过的错:用 streamText 做批量内容审核,100 条内容串行流式处理,耗时 3 分钟。改成 generateText + Promise.all 并发,15 秒搞定。

// ❌ 错误:串行流式处理批量任务
for (const item of items) {
  const result = await streamText({
    model: openai('gpt-4-turbo'),
    prompt: `审核以下内容:${item.text}`,
  });
  // 还得等流读完...
}

// ✅ 正确:并发一次性生成
const results = await Promise.all(
  items.map((item) =>
    generateText({
      model: openai('gpt-4-turbo'),
      prompt: `审核以下内容:${item.text}`,
    })
  )
);

记住:,不是给机器用的。

Provider 切换:一行代码的事,但 90% 的人架构都写错了

AI SDK 最大的卖点之一是"多 Provider 支持"——OpenAI、Anthropic、Google、DeepSeek,换模型只需要改一行代码。

理论上是这样:

// 从 GPT-4 切到 Claude
- model: openai('gpt-4-turbo'),
+ model: anthropic('claude-3-5-sonnet'),

但实际项目中,你的模型配置散落在 15 个文件里,每个 API 路由都硬编码了 provider。想切模型?得改 15 个文件,漏一个就是线上事故。

正确的做法是用 Adapter 模式做一层抽象:

// lib/ai-provider.ts
import { openai } from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';

const providers = {
  openai: (model: string) => openai(model),
  anthropic: (model: string) => anthropic(model),
} as const;

type ProviderKey = keyof typeof providers;

export function getModel(
  provider: ProviderKey = 'openai',
  model?: string
) {
  const defaultModels: Record<ProviderKey, string> = {
    openai: 'gpt-4-turbo',
    anthropic: 'claude-3-5-sonnet',
  };
  return providers[provider](model ?? defaultModels[provider]);
}

然后所有 API 路由统一调用:

import { getModel } from '@/lib/ai-provider';

const result = await streamText({
  model: getModel(), // 默认 OpenAI
  messages,
});

切模型?改一处配置,全局生效。这不是过度设计,这是基本的工程素养。

最近有篇案例:一个团队 15 个文件都直接依赖 SDK,每次升级都是噩梦。后来用 Ports and Adapters 模式重构,把 SDK 依赖收敛到 2 个文件,下次升级改动范围从 15+ 文件压缩到 2 个,28 个业务文件零变更。

结构化输出:generateObject + Zod 才是真正的杀手锏

很多人用 AI SDK 只用到了聊天功能,但其实 generateObject 才是生产环境里最值钱的 API。

传统做法:让 AI 返回 JSON,然后 JSON.parse 赌运气,解析失败了再重试。

AI SDK 的做法:用 Zod 定义 schema,generateObject 直接返回类型安全的对象。

import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

const { object } = await generateObject({
  model: openai('gpt-4-turbo'),
  schema: z.object({
  ntiment: z.enum(['positive', 'negative', 'neutral']),
    confidence: z.number().min(0).max(1),
    keywords: z.array(z.string()).max(5),
  }),
  prompt: '分析这段用户评价的情感倾向:...',
});

// object.sentiment 自动推导为 'positive' | 'negative' | 'neutral'
// object.confidence 自动推导为 number
// 写错字段名?编译期直接报错

这比运行时抓 JSON 解析错误体面多了。 尤其是把 AI 输出喂给下游系统时,类型安全等于少写一半防御代码。

我在一个内容管理项目里,用 generateObject 做文章标签提取、情感分析、摘要生成,准确率比手写 prompt + JSON.parse 高了一截,代码量反而少了 60%。

工具调用:让 AI 不只是"嘴强"

streamText 支持 tools 参数,这是构建 AI Agent 的基础能力。定义好工具的描述、参数 schema 和执行函数,模型会自己判断什么时候调用。

const result = await streamText({
  model: openai('gpt-4-turbo'),
  messages,
  tools: {
    getWeather: {
      description: '获取指定城市的天气信息',
      parameters: z.object({
        city: z.string().describe('城市名称'),
      }),
      execute: async ({ city }) => {
        const res = await fetch(`/api/weather?city=${city}`);
        return res.json();
      },
    },
  },
});

用户问"北京明天穿什么",AI 自动识别意图、填参数、调你的函数、拿结果、组织回复。整个过程对用户来说是无缝的。

但有个坑:工具调用的超时处理。如果你的工具函数调了一个慢接口(比如第三方 API),默认没有超时机制,AI 会一直等。建议给每个工具函数加上超时:

execute: async ({ city }) => {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);
  try {
    const res = await fetch(`/api/weather?city=${city}`, {
      signal: controller.signal,
    });
    return res.json();
  } catch {
    return { error: '天气服务暂时不可用' };
  } finally {
    clearTimeout(timeout);
  }
},

写在最后

Vercel AI SDK 确实是目前前端接入大模型最省心的方案——统一的 Provider 抽象、开箱即用的 React Hooks、类型安全的结构化输出,这些设计都很优雅。

但"省心"不等于"无脑用"。真正的最佳实践,从来不是照着文档抄代码,而是理解每个 API 背后的设计意图,然后根据自己的场景做取舍。

你在用 AI SDK 的过程中踩过什么坑?评论区聊聊。

这是「Hans碎碎念」的文章,欢迎关注