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との深い統合にあります——
useChatHookでストリーミングチャットが数行で実現し、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コードに変換
ブラウザローカルツールを無料で試す →