TypeScript AI SDK開発:2026年Vercel AI SDKで本番級AIアプリを構築する7つのキーパターン

前端工程

AIアプリ開発、なぜいつもこんなに辛いのか?

LLM APIの呼び出し自体は難しくありませんが、本番級AIアプリケーションの構築には至る所に落とし穴があります:ストリーミング出力がフロントエンドでチカチカし、Server ComponentsとAIストリーミングレスポンスの統合に悩み、Edge Runtimeの制限で依存関係の半分が使えず、Tool Callingのエラー処理にはベストプラクティスがほぼありません……本番デプロイ後のレイテンシ、安定性、コスト問題は言うまでもありません。

Vercel AI SDKは2026年にバージョン4.xに進化し、ストリーミングレンダリングからツール呼び出しまで完全なソリューションを提供しています。本記事では7つのキーパターンをまとめ、デモレベルから本番レベルへレベルアップします。


Vercel AI SDK コアアーキテクチャ

モジュール 責務 キーAPI
AI Core 統一マルチモデル呼び出しインタフェース generateText(), streamText(), generateObject()
AI SDK UI フロントエンドストリーミングレンダリングHook useChat(), useCompletion(), useObject()
AI SDK RSC Server Components統合 streamUI(), createAI()
Tool Calling 関数呼び出しとツールオーケストレーション tools, execute, maxSteps

コア依存バージョン:

{
  "ai": "^4.2.0",
  "@ai-sdk/openai": "^1.3.0",
  "@ai-sdk/anthropic": "^1.2.0",
  "next": "^15.2.0",
  "react": "^19.0.0",
  "zod": "^3.24.0"
}

問題の深掘り:AIアプリの7つのコアチャレンジ

チャレンジ 従来のアプローチ AI SDKアプローチ 利点
ストリーミングレンダリング 手動SSE解析 useChat() Hook 自動再接続、状態管理
型安全性 any型 Zod Schema コンパイル時検証
Server統合 API Route + useEffect RSC + streamUI() ゼロクライアントJS
Edgeデプロイ Node.js Runtime Edge Runtime グローバル低レイテンシ
ツール呼び出し 手動JSON解析 tools + execute 自動オーケストレーション
エラー回復 try/catch maxSteps + リトライ 自動リトライチェーン
コスト制御 なし トークンカウント + キャッシュ 精密な課金

パターン1:ストリーミングチャットUI

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

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai('gpt-4o'),
    system: 'あなたは専門的な技術アシスタントです。簡潔で正確に回答してください。',
    messages,
    maxTokens: 4096,
    temperature: 0.7,
  });

  return result.toDataStreamResponse();
}
// app/chat/page.tsx
'use client';

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

export default function ChatPage() {
  const { messages, input, handleInputChange, handleSubmit, isLoading, error, reload } = useChat({
    api: '/api/chat',
  });

  return (
    <div className="flex flex-col h-screen max-w-3xl mx-auto p-4">
      <div className="flex-1 overflow-y-auto space-y-4 mb-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`p-4 rounded-lg ${
              message.role === 'user'
                ? 'bg-blue-500 text-white ml-auto max-w-[80%]'
                : 'bg-gray-100 dark:bg-gray-800 mr-auto max-w-[80%]'
            }`}
          >
            <div className="whitespace-pre-wrap">{message.content}</div>
          </div>
        ))}
        {isLoading && <div className="animate-pulse p-4">考え中...</div>}
      </div>

      {error && (
        <div className="mb-2 p-2 bg-red-100 text-red-700 rounded text-sm">
          エラー: {error.message}
          <button onClick={() => reload()} className="ml-2 underline">リトライ</button>
        </div>
      )}

      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="質問を入力..."
          className="flex-1 p-3 border rounded-lg"
          disabled={isLoading}
        />
        <button type="submit" disabled={isLoading || !input.trim()}
          className="px-6 py-3 bg-blue-500 text-white rounded-lg disabled:opacity-50">
          送信
        </button>
      </form>
    </div>
  );
}

パターン2:Server Componentsストリーミングレンダリング

// app/api/generate/route.ts
import { openai } from '@ai-sdk/openai';
import { streamUI } from 'ai/rsc';
import { z } from 'zod';

async function WeatherComponent({ city }: { city: string }) {
  const weatherData = {
    city,
    temperature: 22 + Math.floor(Math.random() * 10),
    condition: ['晴れ', '曇り', '小雨'][Math.floor(Math.random() * 3)],
    humidity: 45 + Math.floor(Math.random() * 30),
  };

  return (
    <div className="p-4 bg-gradient-to-r from-blue-50 to-cyan-50 rounded-lg border">
      <h3 className="font-bold text-lg">{weatherData.city} の天気</h3>
      <div className="flex gap-4 mt-2">
        <span className="text-3xl">{weatherData.temperature}°C</span>
        <div>
          <div>{weatherData.condition}</div>
          <div className="text-sm text-gray-500">湿度 {weatherData.humidity}%</div>
        </div>
      </div>
    </div>
  );
}

export async function POST(req: Request) {
  const { prompt } = await req.json();

  const result = streamUI({
    model: openai('gpt-4o'),
    prompt,
    tools: {
      showWeather: {
        description: '指定した都市の天気情報を表示',
        parameters: z.object({ city: z.string().describe('都市名') }),
        generate: async ({ city }) => <WeatherComponent city={city} />,
      },
    },
  });

  return result.toUIStreamResponse();
}

パターン3:Tool Calling

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

const tools = {
  searchProducts: {
    description: '商品データベースを検索',
    parameters: z.object({
      query: z.string().describe('検索キーワード'),
      category: z.string().optional().describe('商品カテゴリ'),
      maxPrice: z.number().optional().describe('最高価格'),
    }),
    execute: async ({ query, category, maxPrice }) => {
      const products = [
        { id: 1, name: 'メカニカルキーボード Pro', price: 599, category: '周辺機器' },
        { id: 2, name: '4Kモニター', price: 2999, category: 'ディスプレイ' },
        { id: 3, name: 'ワイヤレスマウス', price: 199, category: '周辺機器' },
      ];
      let results = products.filter(p => p.name.includes(query) || p.category === category);
      if (maxPrice) results = results.filter(p => p.price <= maxPrice);
      return results;
    },
  },
  calculateTotal: {
    description: '注文合計を計算',
    parameters: z.object({
      items: z.array(z.object({ productId: z.number(), quantity: z.number(), unitPrice: z.number() })),
      discount: z.number().optional().describe('割引率'),
    }),
    execute: async ({ items, discount = 0 }) => {
      const subtotal = items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
      const total = subtotal - subtotal * (discount / 100);
      return { subtotal: subtotal.toFixed(2), total: total.toFixed(2) };
    },
  },
};

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: openai('gpt-4o'),
    system: 'あなたはECアシスタントです。',
    messages,
    tools,
    maxSteps: 5,
  });
  return result.toDataStreamResponse();
}

パターン4:構造化出力

import { z } from 'zod';

export const analysisSchema = z.object({
  summary: z.string().describe('記事の要約'),
  sentiment: z.enum(['positive', 'neutral', 'negative']).describe('感情'),
  keywords: z.array(z.string()).describe('キーワード'),
  categories: z.array(z.object({ name: z.string(), confidence: z.number() })),
  entities: z.array(z.object({
    name: z.string(),
    type: z.enum(['person', 'organization', 'location', 'product', 'event']),
    mentions: z.number(),
  })),
});

パターン5:Edge Runtimeデプロイ

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

export const runtime = 'edge';

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: openai('gpt-4o-mini'),
    system: '簡潔に回答してください。',
    messages,
    maxTokens: 2048,
  });
  return result.toDataStreamResponse();
}

パターン6:マルチモデル切り替え

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

export const models = {
  'gpt-4o': openai('gpt-4o'),
  'gpt-4o-mini': openai('gpt-4o-mini'),
  'claude-sonnet-4-20250514': anthropic('claude-sonnet-4-20250514'),
  'gemini-2.0-flash': google('gemini-2.0-flash'),
} as const;

export type ModelId = keyof typeof models;
export function getModel(id: ModelId) { return models[id]; }

パターン7:ミドルウェアと認証

import { NextRequest, NextResponse } from 'next/server';

const rateLimitMap = new Map<string, { count: number; resetAt: number }>();

export function middleware(req: NextRequest) {
  if (req.nextUrl.pathname.startsWith('/api/')) {
    const ip = req.headers.get('x-forwarded-for') || 'unknown';
    const now = Date.now();
    const record = rateLimitMap.get(ip);
    if (!record || now > record.resetAt) {
      rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 });
    } else if (record.count >= 20) {
      return NextResponse.json({ error: 'リクエスト制限超過' }, { status: 429 });
    } else {
      record.count++;
    }
  }
  return NextResponse.next();
}

export const config = { matcher: '/api/:path*' };

よくある落とし穴ガイド

落とし穴1:useChatストリーミング出力のちらつき

React 19の並行レンダリングにより、ストリーミング出力時にUIがちらつく場合があります。

解決策onFinishコールバックで最終状態を同期、whitespace-pre-wrapを使用、key={message.id}を使用。

落とし穴2:Edge Runtime依存関係の非互換性

多くのnpmパッケージがNode.js APIを使用しており、Edge Runtimeで動作しません。

解決策:Edge互換性を確認、非互換の依存関係はNode.js Runtimeにフォールバック。

落とし穴3:Tool Callingの無限ループ

maxStepsが大きすぎると、ツール呼び出しがループする可能性があります。

解決策maxStepsを3-5に設定、executeに終了条件を追加。

落とし穴4:streamUIコンポーネントのシリアライズ失敗

RSCストリーミングコンポーネントはシリアライズ可能である必要があります。

解決策:クロージャで外部変数をキャプチャしない、すべてのデータをpropsで渡す。

落とし穴5:generateObject Schema不一致

モデル出力がZod Schema定義と一致せず、パースに失敗します。

解決策:Schemaフィールドに詳細なdescribe()を追加、enumを使用。


エラートラブルシューティング

# エラーメッセージ 原因 解決方法
1 AI_APICallError: 429 Rate limit exceeded APIレート制限超過 指数バックオフリトライを追加
2 AI_InvalidPromptError: messages must not be empty messages配列が空 送信前にバリデーション
3 AI_NoOutputSpecifiedError 出力方法が未指定 toDataStreamResponse()を呼び出し
4 AI_ToolExecutionError ツールexecuteで例外 try/catchを追加
5 AI_JSONParseError 無効なJSON出力 generateObjectを使用
6 EdgeRuntime: Dynamic require of "fs" EdgeでNode.js API使用 Runtimeを切り替え
7 Hydration mismatch SSR/クライアントの不一致 クライアント状態に依存しない
8 AI_InvalidArgumentError: schema validation failed Zod Schema検証失敗 Schema定義を確認
9 AI_ReadonlyStreamError: stream already consumed ストリームの二重消費 各ストリームを1回のみ読み取り
10 Maximum call stack size exceeded maxStepsの無限再帰 maxStepsを下げる

高度な最適化

1. ストリーミングレスポンスキャッシュ

import { cache } from 'react';

export const generateAnalysis = cache(async (text: string) => {
  const { object } = await generateObject({
    model: openai('gpt-4o-mini'),
    schema: analysisSchema,
    prompt: text,
  });
  return object;
});

2. トークン使用量追跡

const result = streamText({
  model: openai('gpt-4o'),
  messages,
  onFinish: ({ usage }) => {
    console.log(`Tokens: prompt=${usage.promptTokens}, completion=${usage.completionTokens}`);
  },
});

3. ストリーミングUIローディングスケルトン

const result = streamUI({
  model: openai('gpt-4o'),
  prompt,
  loading: (
    <div className="animate-pulse space-y-3 p-4">
      <div className="h-4 bg-gray-200 rounded w-3/4" />
      <div className="h-32 bg-gray-200 rounded" />
    </div>
  ),
});

比較分析

次元 Vercel AI SDK LangChain.js OpenAI SDK LlamaIndex.ts
ストリーミングUI ネイティブReact Hook 手動統合 手動統合 手動統合
Server Components ネイティブ対応 未対応 未対応 未対応
マルチモデル Provider抽象 Callback抽象 OpenAIのみ Provider抽象
Tool Calling 宣言型 チェーン型 手動 宣言型
構造化出力 Zod Schema Zod/Dynamic JSON Mode Zod Schema
Edge Runtime ネイティブ対応 部分互換 互換 部分互換
バンドルサイズ ~30KB ~200KB+ ~50KB ~150KB+
学習曲線
Next.js統合 深い統合 アダプタ必要 アダプタ必要 アダプタ必要

まとめ:Vercel AI SDKは2026年にNext.js AIアプリを構築する最良の選択です。7つのキーパターンは、ストリーミングレンダリング、Server Components統合、Tool CallingからEdgeデプロイまで完全なチェーンをカバーしています。コアの利点はReact/Next.jsとの深い統合にあります——useChat Hookでストリーミングチャットが数行で実現し、streamUIでAIが直接Reactコンポーネントを生成し、Zod Schemaで構造化出力が型安全になります。パターン1から始め、徐々にTool CallingとRSCを導入し、最後にEdgeデプロイとキャッシュ戦略を最適化することをお勧めします。


オンラインツールおすすめ

  • JSONフォーマッター:/ja/json/format — AIモデルレスポンスとTool Callingパラメータのフォーマット
  • Base64エンコード/デコード:/ja/encode/base64 — APIキーとトークンエンコーディングの処理
  • Curl to Code:/ja/dev/curl-to-code — AI APIデバッグcurlをTypeScriptコードに変換

ブラウザローカルツールを無料で試す →

#TypeScript#Vercel AI SDK#Next.js#流式UI#大模型#AI应用#Server Components#Edge Runtime