Vercel AI SDK完全ガイド【TypeScript】|generateText・streamText・Tool Calling・generateObject・Next.js統合まで徹底解説

Vercel AI SDK完全ガイド【TypeScript】|generateText・streamText・Tool Calling・generateObject・Next.js統合まで徹底解説 AI開発

Vercel AI SDKは、TypeScript/JavaScriptアプリケーションにAI機能を組み込むためのオープンソースライブラリです。Claude・OpenAI・Google Gemini・Mistralなど20以上のAIプロバイダーを統一されたAPIで扱えるのが最大の特徴で、Next.js・React・Svelte・Node.jsなど幅広い環境に対応しています。

たとえばOpenAIからClaudeに乗り換えるとき、Vercel AI SDKを使っていればプロバイダーの設定を1行変えるだけで移行できます。ストリーミング・ツール呼び出し・構造化出力・マルチモーダル入力といった最新機能がすべて同じ関数インターフェースで使えるため、複数のAIを試しながら開発するプロジェクトに特に向いています。

この記事ではVercel AI SDK(v4系)の基本から応用まで、TypeScriptの実装コードつきで体系的に解説します。Claude APIOpenAI APIを直接使う方法と比較しながら、どんな場面でVercel AI SDKが活きるかも紹介します。

「Next.jsにAIチャット機能を実装したい」「ストリーミングレスポンスをReactで表示したい」「LLMの出力をJSON型安全に受け取りたい」——そういったエンジニアの方に最適な内容です。

この記事でわかること

  • Vercel AI SDKとは何か・直接APIを使う場合との違い
  • インストール方法と各プロバイダー(Claude・OpenAI・Gemini)の設定
  • generateText / streamText によるテキスト生成
  • generateObject で型安全な構造化出力を取得する方法
  • Tool Calling(ツール呼び出し)の実装パターン
  • マルチモーダル(画像・PDF入力)の扱い方
  • Next.js App RouterでのRoute Handler・RSC・Client Component統合
  • useChat / useCompletion フックの使い方
  • エラーハンドリングとレート制限への対処法
スポンサーリンク

Vercel AI SDKとは何か

Vercel AI SDK(パッケージ名: ai)は、2023年にVercelがリリースしたTypeScript/JavaScript向けAIライブラリです。バージョン4(2024年リリース)でAPIが大幅に整理され、現在はプロダクション利用に十分な安定性を持ちます。

比較項目 Vercel AI SDK 直接APIを使う(例: Anthropic SDK)
プロバイダー切替 設定1行で切替可能 SDKごとにコードを書き直す必要あり
ストリーミング streamText() で統一API プロバイダーごとに実装が異なる
構造化出力 generateObject() + Zod で型安全 各APIのJSON modeを個別実装
Next.js統合 useChat/useCompletion等のフック標準搭載 自前でカスタムフックを書く必要あり
ツール呼び出し tools オプションで統一定義 プロバイダーごとにフォーマットが異なる
マルチモーダル content配列で統一フォーマット APIごとに異なる形式

直接SDKを使う場合は特定プロバイダーの機能を最大限に活用できますが、Vercel AI SDKを使うとプロバイダー間の差異を吸収でき、複数のモデルを比較・切替しながら開発する場合に大きな優位性があります。

インストールとプロバイダー設定

パッケージのインストール

ターミナル
# コアパッケージ
npm install ai

# Reactフック(useChat / useCompletion)を使う場合
npm install @ai-sdk/react

# 使用するプロバイダーのパッケージを追加(必要なものだけ)
npm install @ai-sdk/anthropic   # Claude (Anthropic)
npm install @ai-sdk/openai      # OpenAI (ChatGPT)
npm install @ai-sdk/google      # Google Gemini
npm install @ai-sdk/mistral     # Mistral AI

# Zod(generateObject で使用)
npm install zod

各プロバイダーの初期設定

providers.ts
import { anthropic } from "@ai-sdk/anthropic";
import { openai } from "@ai-sdk/openai";
import { google } from "@ai-sdk/google";

// 環境変数から自動取得(手動初期化不要)
// ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY

// モデルの取得
export const claudeModel = anthropic("claude-opus-4-6");
export const gpt4oModel = openai("gpt-4o");
export const geminiModel = google("gemini-1.5-pro");

// カスタム設定が必要な場合(例: baseURL変更)
import { createOpenAI } from "@ai-sdk/openai";
const customOpenAI = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: "https://api.example.com/v1", // Azure OpenAI等
});
export const azureModel = customOpenAI("gpt-4o");
.env.local
ANTHROPIC_API_KEY=sk-ant-xxxxxxxx
OPENAI_API_KEY=sk-xxxxxxxx
GOOGLE_GENERATIVE_AI_API_KEY=AIxxxxxxxx
APIキーの管理
本番環境では環境変数を必ず使い、コードにAPIキーを直接書かないでください。Next.jsではサーバーサイド専用の環境変数(NEXT_PUBLIC_プレフィックスなし)として設定し、クライアントには公開しないようにします。

generateText:テキスト生成の基本

generateText()は、プロンプトを渡してLLMからテキストを取得する最も基本的な関数です。レスポンスを一括で受け取るため、バックグラウンドでの処理(バッチ生成・要約等)に向いています。

generate-text.ts
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

// シンプルなテキスト生成
const { text, usage, finishReason } = await generateText({
  model: anthropic("claude-opus-4-6"),
  prompt: "TypeScriptのジェネリクスを初心者向けに100字で説明してください。",
});

console.log(text);
// → 「ジェネリクスは型を変数のように扱う仕組みです。...」

console.log(`使用トークン: ${usage.totalTokens}`);
console.log(`終了理由: ${finishReason}`); // "stop" | "length" | "tool-calls" 等

// システムプロンプトとメッセージ履歴を使う場合
const { text: reviewText } = await generateText({
  model: anthropic("claude-opus-4-6"),
  system: "あなたはシニアTypeScriptエンジニアです。コードレビューを簡潔に行ってください。",
  messages: [
    {
      role: "user",
      content: "const x: any = getValue(); は問題ありますか?",
    },
    {
      role: "assistant",
      content: "はい、anyは型安全性を損ないます。",
    },
    {
      role: "user",
      content: "どう修正すればよいですか?",
    },
  ],
});

console.log(reviewText);

モデルの切り替えは1行だけ

switch-model.ts
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { openai } from "@ai-sdk/openai";
import { google } from "@ai-sdk/google";

type Provider = "claude" | "gpt4o" | "gemini";

function getModel(provider: Provider) {
  switch (provider) {
    case "claude":  return anthropic("claude-opus-4-6");
    case "gpt4o":   return openai("gpt-4o");
    case "gemini":  return google("gemini-1.5-pro");
  }
}

async function summarize(text: string, provider: Provider = "claude") {
  const { text: summary } = await generateText({
    model: getModel(provider), // ← ここだけ変えれば全モデル対応
    prompt: `以下のテキストを3行で要約してください:\n\n${text}`,
  });
  return summary;
}

// 同じコードでどのモデルでも動く
const result = await summarize("長文テキスト...", "gpt4o");

streamText:ストリーミングでUXを向上させる

チャットUIのように生成中のテキストをリアルタイムで表示したい場合はstreamText()を使います。レスポンス全体の生成完了を待たずにユーザーに提示できるため、体体的な待ち時間を大幅に削減できます。

Node.js環境でのストリーミング

stream-text-node.ts
import { streamText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

const result = streamText({
  model: anthropic("claude-opus-4-6"),
  prompt: "TypeScriptのデコレータについて詳しく説明してください。",
});

// textStreamで逐次取得(AsyncIterable)
for await (const chunk of result.textStream) {
  process.stdout.write(chunk); // リアルタイムで出力
}

// 完了後に全文・使用量を取得
const fullText = await result.text;
const usage = await result.usage;
console.log(`\n\n使用トークン: ${usage.totalTokens}`);

Next.js Route HandlerでストリーミングAPIを作る

app/api/chat/route.ts
import { streamText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

export async function POST(req: Request) {
  const { messages } = await req.json() as {
    messages: { role: "user" | "assistant"; content: string }[];
  };

  const result = streamText({
    model: anthropic("claude-opus-4-6"),
    system: "あなたは親切なAIアシスタントです。日本語で回答してください。",
    messages,
    maxTokens: 1024,
  });

  // toDataStreamResponse() でVercel AI SDK形式のストリームを返す
  return result.toDataStreamResponse();
}

React Client Componentでusechatを使う

app/components/ChatUI.tsx
"use client";
// AI SDK v4: "@ai-sdk/react" から import(古いバージョンは "ai/react")
import { useChat } from "@ai-sdk/react";

export default function ChatUI() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } =
    useChat({
      api: "/api/chat", // Route HandlerのURL
    });

  return (
    <div>
      <div className="messages">
        {messages.map((m) => (
          <div key={m.id} className={`message ${m.role}`}>
            <strong>{m.role === "user" ? "あなた" : "AI"}:</strong>
            <p>{m.content}</p>
          </div>
        ))}
        {isLoading && <div className="loading">生成中...</div>}
      </div>

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="メッセージを入力..."
          disabled={isLoading}
        />
        <button type="submit" disabled={isLoading}>
          送信
        </button>
      </form>
    </div>
  );
}
useChat のポイント
useChatはメッセージ履歴・ローディング状態・エラー処理を自動管理します。ストリーミングAPIのレスポンスをそのままReact Stateに反映してくれるため、カスタムフックを自前実装する必要がありません。

generateObject:型安全な構造化出力

generateObject()はZodスキーマを渡すことで、LLMのレスポンスをTypeScriptの型として直接取得できます。JSONパース・バリデーションが自動で行われるため、プロダクション環境でのAPI連携に最適です。

generate-object.ts
import { generateObject } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

// レスポンスの型をZodスキーマで定義
const ArticleSchema = z.object({
  title: z.string().describe("SEOを意識した記事タイトル"),
  summary: z.string().max(300).describe("記事の概要(300字以内)"),
  keywords: z.array(z.string()).min(3).max(8).describe("検索キーワード"),
  difficulty: z.enum(["beginner", "intermediate", "advanced"]).describe("難易度"),
  estimatedReadTime: z.number().int().positive().describe("想定読了時間(分)"),
  sections: z.array(
    z.object({
      heading: z.string(),
      description: z.string().max(100),
    })
  ).min(3).describe("記事の目次構成"),
});

type Article = z.infer<typeof ArticleSchema>;

async function generateArticlePlan(topic: string): Promise<Article> {
  const { object } = await generateObject({
    model: anthropic("claude-opus-4-6"),
    schema: ArticleSchema,
    prompt: `以下のトピックについての技術記事の構成案を作成してください:${topic}`,
  });
  return object; // TypeScriptの型が保証される
}

const plan = await generateArticlePlan("TypeScriptのデコレータパターン");
console.log(plan.title);          // string
console.log(plan.difficulty);     // "beginner" | "intermediate" | "advanced"
console.log(plan.sections[0].heading); // string

配列の生成(output: “array”)

generate-array.ts
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const QuestionSchema = z.object({
  question: z.string(),
  options: z.array(z.string()).length(4),
  correctIndex: z.number().int().min(0).max(3),
  explanation: z.string(),
});

// 複数オブジェクトの配列を生成
const { object: questions } = await generateObject({
  model: openai("gpt-4o"),
  output: "array",    // 配列として返す
  schema: QuestionSchema,
  prompt: "TypeScriptの型システムに関する4択クイズを5問作成してください。",
});

// questions は QuestionSchema[] 型として推論される
questions.forEach((q, i) => {
  console.log(`Q${i + 1}: ${q.question}`);
  console.log(`正解: ${q.options[q.correctIndex]}`);
});

Tool Calling:LLMにツールを使わせる

Tool Calling(Function Calling)は、LLMが外部APIやデータベースを呼び出して情報を取得・処理する仕組みです。Vercel AI SDKではtoolsオプションに定義するだけで、プロバイダーごとのフォーマットの違いを意識せず実装できます。

tool-calling.ts
import { generateText, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

// ツール定義(tool() ヘルパーで型安全に作る)
const tools = {
  getWeather: tool({
    description: "指定した都市の現在の天気を取得します",
    parameters: z.object({
      city: z.string().describe("都市名(例: Tokyo)"),
      unit: z.enum(["celsius", "fahrenheit"]).default("celsius"),
    }),
    execute: async ({ city, unit }) => {
      // 実際は天気APIを呼ぶ
      return {
        city,
        temperature: unit === "celsius" ? 22 : 72,
        condition: "晴れ",
        humidity: 60,
      };
    },
  }),

  searchDatabase: tool({
    description: "社内ドキュメントDBを検索して関連情報を返します",
    parameters: z.object({
      query: z.string().describe("検索クエリ"),
      limit: z.number().int().min(1).max(10).default(3),
    }),
    execute: async ({ query, limit }) => {
      // 実際はベクトルDB等を検索する
      return {
        results: [
          { title: `${query}に関するドキュメント`, content: "関連内容..." },
        ].slice(0, limit),
      };
    },
  }),
};

// ツールを使ったテキスト生成
const { text, toolCalls, toolResults } = await generateText({
  model: anthropic("claude-opus-4-6"),
  tools,
  maxSteps: 5, // ツール呼び出しを最大5回まで繰り返す
  messages: [
    {
      role: "user",
      content: "東京の今日の天気を教えて、それに合わせた服装を提案してください。",
    },
  ],
});

console.log(text); // 天気を取得した上での服装提案
console.log("実行されたツール:", toolCalls.map((t) => t.toolName));

ツールの実行ステップをログに残す

tool-with-steps.ts
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const { steps } = await generateText({
  model: openai("gpt-4o"),
  tools: {
    calculator: tool({
      description: "数値計算を行います",
      parameters: z.object({
        expression: z.string().describe("計算式(例: 2 + 3 * 4)"),
      }),
      execute: async ({ expression }) => {
        // 安全な計算のみ(実際はeval()を使わないこと)
        return { result: 14, expression };
      },
    }),
  },
  maxSteps: 3,
  prompt: "1から10までの合計を計算してください。",
});

// 各ステップの詳細を確認
steps.forEach((step, i) => {
  console.log(`--- Step ${i + 1} ---`);
  console.log("テキスト:", step.text);
  if (step.toolCalls.length > 0) {
    console.log("ツール呼び出し:", step.toolCalls);
    console.log("ツール結果:", step.toolResults);
  }
});

マルチモーダル:画像・PDFを入力する

Vercel AI SDKはテキスト以外の入力形式を統一したAPIで扱えるのも利点です。画像URLの指定・Base64エンコード・ファイルバッファのいずれも同じcontent配列の形式で書けます。

multimodal.ts
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import * as fs from "fs";

// 画像URLを使う場合
const { text: descriptionFromUrl } = await generateText({
  model: anthropic("claude-opus-4-6"),
  messages: [
    {
      role: "user",
      content: [
        {
          type: "image",
          image: new URL("https://example.com/diagram.png"),
        },
        {
          type: "text",
          text: "この図を説明してください。技術的な観点から解説をお願いします。",
        },
      ],
    },
  ],
});

// ローカルファイルを使う場合(Base64変換は自動)
const imageBuffer = fs.readFileSync("./screenshot.png");

const { text: descriptionFromFile } = await generateText({
  model: anthropic("claude-opus-4-6"),
  messages: [
    {
      role: "user",
      content: [
        {
          type: "image",
          image: imageBuffer,        // Buffer を直接渡す
          mimeType: "image/png",
        },
        {
          type: "text",
          text: "このスクリーンショットのエラーメッセージを解析してください。",
        },
      ],
    },
  ],
});

console.log(descriptionFromFile);

Next.js App Routerとの統合パターン

パターン1:Route HandlerでストリーミングAPI

チャットUIなど、クライアントサイドからストリーミングで使う最もよく使われるパターンです。

app/api/chat/route.ts(フル実装)
import { streamText, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

export const runtime = "edge"; // Edge Runtimeで高速化(オプション)

export async function POST(req: Request) {
  const { messages, systemPrompt } = await req.json() as {
    messages: { role: "user" | "assistant"; content: string }[];
    systemPrompt?: string;
  };

  // リクエストの簡易バリデーション
  if (!Array.isArray(messages) || messages.length === 0) {
    return new Response("Invalid request", { status: 400 });
  }

  const result = streamText({
    model: anthropic("claude-opus-4-6"),
    system: systemPrompt ?? "あなたは親切なAIアシスタントです。",
    messages,
    maxTokens: 2048,
    tools: {
      // 必要に応じてツールを追加
      getCurrentTime: tool({
        description: "現在の日本時間を取得します",
        parameters: z.object({}),
        execute: async () => ({
          time: new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" }),
        }),
      }),
    },
    onFinish({ text, usage, finishReason }) {
      // 生成完了後の処理(ログ・DBへの保存等)
      console.log(`生成完了: ${usage.totalTokens}トークン, 理由: ${finishReason}`);
    },
  });

  return result.toDataStreamResponse({
    headers: {
      "X-Content-Type-Options": "nosniff",
    },
  });
}

パターン2:Server ComponentでのServer-Side生成

ページロード時に一度だけAI生成を行い、HTML込みでレスポンスする場合はServer Componentが最適です。

app/summary/page.tsx
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

interface Props {
  searchParams: { topic?: string };
}

// Server Component("use client" なし)
export default async function SummaryPage({ searchParams }: Props) {
  const topic = searchParams.topic ?? "TypeScript";

  const { text: summary } = await generateText({
    model: anthropic("claude-opus-4-6"),
    prompt: `「${topic}」について初心者向けに200字で解説してください。`,
    maxTokens: 300,
  });

  return (
    <main>
      <h1>{topic}について</h1>
      <p>{summary}</p>
    </main>
  );
}

パターン3:useCompletionで単発生成UI

useCompletionは一問一答形式(会話履歴不要)のUIに向いています。テキスト生成・要約・翻訳などシンプルなユースケースに最適です。

app/components/Summarizer.tsx
"use client";
// AI SDK v4: "@ai-sdk/react" から import(古いバージョンは "ai/react")
import { useCompletion } from "@ai-sdk/react";
import { useState } from "react";

export default function Summarizer() {
  const [inputText, setInputText] = useState("");

  const { completion, complete, isLoading, error } = useCompletion({
    api: "/api/summarize",
  });

  const handleSummarize = async () => {
    if (!inputText.trim()) return;
    await complete(inputText);
  };

  return (
    <div>
      <textarea
        value={inputText}
        onChange={(e) => setInputText(e.target.value)}
        placeholder="要約したいテキストを入力..."
        rows={6}
      />
      <button onClick={handleSummarize} disabled={isLoading}>
        {isLoading ? "要約中..." : "要約する"}
      </button>

      {error && <p className="error">エラー: {error.message}</p>}

      {completion && (
        <div className="result">
          <h3>要約結果</h3>
          <p>{completion}</p>
        </div>
      )}
    </div>
  );
}

エラーハンドリングとレート制限対応

プロダクション環境ではAPIエラーへの適切な対処が必須です。Vercel AI SDKはプロバイダーごとのエラーを統一された型で返してくれるため、共通のエラーハンドリングロジックを実装できます。

error-handling.ts
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import {
  APICallError,
  RetryError,
  NoTextGeneratedError,
} from "ai";

async function generateWithRetry(
  prompt: string,
  maxRetries: number = 3
): Promise<string> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const { text } = await generateText({
        model: anthropic("claude-opus-4-6"),
        prompt,
        maxTokens: 512,
      });
      return text;
    } catch (error) {
      if (error instanceof APICallError) {
        const statusCode = error.statusCode ?? 0;

        // レート制限(429)はリトライ
        if (statusCode === 429) {
          if (attempt < maxRetries) {
            const waitMs = Math.pow(2, attempt) * 1000; // 指数バックオフ
            console.warn(`レート制限。${waitMs}ms後にリトライ (${attempt}/${maxRetries})`);
            await new Promise((resolve) => setTimeout(resolve, waitMs));
            continue;
          }
        }

        // 認証エラー(401/403)はリトライ不要
        if (statusCode === 401 || statusCode === 403) {
          throw new Error(`API認証エラー: APIキーを確認してください (${statusCode})`);
        }

        // サーバーエラー(5xx)はリトライ
        if (statusCode >= 500 && attempt < maxRetries) {
          await new Promise((resolve) => setTimeout(resolve, 2000));
          continue;
        }

        throw new Error(`APIエラー (${statusCode}): ${error.message}`);
      }

      if (error instanceof NoTextGeneratedError) {
        throw new Error("テキストが生成されませんでした(コンテンツポリシー違反の可能性)");
      }

      if (error instanceof RetryError) {
        throw new Error(`最大リトライ回数に達しました: ${error.message}`);
      }

      throw error; // 未知のエラーはそのまま投げる
    }
  }

  throw new Error(`${maxRetries}回試行しましたが失敗しました`);
}

// 使用例
try {
  const result = await generateWithRetry("TypeScriptを説明してください");
  console.log(result);
} catch (error) {
  console.error("生成に失敗しました:", error);
}

ストリーミングのエラーハンドリング

stream-error-handling.ts
import { streamText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

// Route Handlerでのストリーミングエラー処理
export async function POST(req: Request) {
  try {
    const { prompt } = await req.json() as { prompt: string };

    const result = streamText({
      model: anthropic("claude-opus-4-6"),
      prompt,
    });

    return result.toDataStreamResponse();
  } catch (error) {
    // エラーをクライアントに返す
    return new Response(
      JSON.stringify({
        error: error instanceof Error ? error.message : "Unknown error",
      }),
      {
        status: 500,
        headers: { "Content-Type": "application/json" },
      }
    );
  }
}

実践サンプル:コードレビューボットの実装

学んだ機能を組み合わせた実践的なサンプルとして、TypeScriptコードを受け取って構造化されたレビュー結果を返すAPIを実装します。

app/api/code-review/route.ts
import { generateObject, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

const ReviewSchema = z.object({
  overallScore: z.number().int().min(0).max(100).describe("全体スコア(0-100)"),
  severity: z.enum(["critical", "major", "minor", "good"]),
  issues: z.array(
    z.object({
      category: z.enum(["type-safety", "performance", "security", "readability", "best-practice"]),
      severity: z.enum(["critical", "major", "minor"]),
      line: z.string().describe("問題のある行番号または範囲(例: 5-8)"),
      description: z.string(),
      suggestion: z.string(),
      fixedCode: z.string().optional().describe("修正後のコードスニペット"),
    })
  ),
  positives: z.array(z.string()).describe("良い点のリスト"),
  summary: z.string().max(200),
});

type ReviewResult = z.infer<typeof ReviewSchema>;

export async function POST(req: Request) {
  const { code, context } = await req.json() as {
    code: string;
    context?: string;
  };

  if (!code || typeof code !== "string") {
    return new Response("codeが必要です", { status: 400 });
  }

  try {
    const { object: review } = await generateObject({
      model: anthropic("claude-opus-4-6"),
      schema: ReviewSchema,
      system: `あなたはシニアTypeScriptエンジニアです。
コードレビューは建設的かつ具体的に行ってください。
良い点も必ず指摘してください。`,
      prompt: `以下のTypeScriptコードをレビューしてください。

${context ? `コンテキスト: ${context}\n\n` : ""}コード:
\`\`\`typescript
${code}
\`\`\``,
    });

    return Response.json(review);
  } catch (error) {
    return new Response(
      JSON.stringify({ error: "レビューに失敗しました" }),
      { status: 500, headers: { "Content-Type": "application/json" } }
    );
  }
}

まとめ

Vercel AI SDKはAI機能をTypeScriptアプリに組み込む際の「共通インターフェース」として非常に優れたライブラリです。プロバイダーの切替・ストリーミング・構造化出力・ツール呼び出しがすべて統一されたAPIで扱える点が、プロダクション開発での生産性を大きく上げます。

機能 関数/フック 主なユースケース
テキスト生成(一括) generateText() バッチ処理・要約・バックグラウンド生成
テキスト生成(逐次) streamText() チャットUI・リアルタイム表示
構造化出力 generateObject() API連携・データ抽出・フォーム入力補助
ツール呼び出し tools オプション 外部API呼び出し・DB検索・計算
チャットUI(React) useChat() マルチターン会話インターフェース
単発生成UI(React) useCompletion() 要約・翻訳・コード補完UI

まずgenerateText()streamText()の基本を押さえ、その後generateObject()でAPIを型安全にするのがおすすめの学習順序です。さらに深めたい方はプロンプトエンジニアリング完全ガイドも参照してください。各AIプロバイダーの詳細な使い方はClaude API × TypeScript入門OpenAI API × TypeScript入門で解説しています。

よくある質問

QVercel AI SDKはVercelのホスティングサービス上でしか使えませんか?

Aいいえ。パッケージ名に「Vercel」と付いていますが、Node.js・Bun・Cloudflare Workers・AWS Lambda等、どのランタイム・ホスティング環境でも利用できます。Next.js以外のフレームワーク(Express・Hono・Fastify等)でも問題なく動作します。

QgenerateObject()はすべてのモデルで使えますか?

A主要なモデル(GPT-4o・Claude 3+・Gemini 1.5+)ではサポートされています。ただし、一部の軽量モデルやローカルモデルでは対応していない場合があります。Vercel AI SDKのドキュメントでプロバイダーごとの対応状況を確認してください。また、mode: "json"mode: "tool"の2種類があり、モデルによってどちらを使うか自動で判断されます。

QstreamText()をEdge Runtime(Cloudflare Workers等)で使えますか?

Aはい。export const runtime = "edge"を設定するだけで動作します。Edge RuntimeではNode.js固有のAPIは使えませんが、streamText()自体はWeb標準のReadableStreamを使っているため問題ありません。

QuseChat()でメッセージ履歴をデータベースに保存するにはどうすれば良いですか?

ARoute HandlerのonFinishコールバックを使います。streamText({ ..., onFinish({ messages }) { await db.save(messages); } })のように記述するか、またはtoDataStreamResponse()の前に保存処理を挟むことができます。Vercel AI SDKのv4ではstreamText()のonFinishに完成したメッセージ配列が渡されるので、1回のコールバックでユーザーメッセージ+アシスタントメッセージをまとめて保存できます。

QTool CallingでLLMが同じツールを無限ループで呼び続けることはありませんか?

Aあります。そのためmaxStepsオプションで最大実行ステップ数を必ず設定してください。デフォルトは1(1回のみ)です。実用的には3〜10程度を設定することが多いです。また、各ツールに明確なdescriptionを書くことで、LLMが無駄なツール呼び出しをしにくくなります。

Q複数のモデルでA/Bテストをするにはどうすればよいですか?

Aモデルを引数で切り替えられる関数を作り、リクエストヘッダーやユーザーグループで分岐するのが定番です。例:const model = req.headers.get("x-model-variant") === "claude" ? anthropic("claude-opus-4-6") : openai("gpt-4o");Vercel AI SDKなら同じgenerateText()呼び出しのままmodelだけ変えるだけで済むため、A/Bテストの実装コストが大幅に下がります。