# Claude API 本番実装ガイド：プロンプトキャッシュ・ツール使用・構造化出力・エージェントを設計する

> Claude APIとVercel AI SDK v6で本番品質のAI機能を実装する決定版ガイド。構造化出力・ツール使用・ストリーミング・エージェント・プロンプトキャッシュ・コスト最適化・可観測性・セキュリティを公式ドキュメント準拠の実コードで解説。AI Gateway経由のモデル指定とフォールバックも網羅。

- 公開日: 2026-06-22
- 著者: 友田 陽大
- タグ: Claude, Anthropic, AI SDK, TypeScript, LLM, エージェント, 構造化出力, ストリーミング, コスト最適化
- URL: https://tomodahinata.com/blog/claude-api-ai-sdk-v6-production-ai-features

## 要点

- 本番品質は地味な5原則の積み上げ。スキーマで縛る・ツール権限最小化・キャンセル可能なストリーミング・事前計測＋キャッシュ・検証ゲート
- モデルは全部Opusも全部Haikuも誤り。既定はSonnet、難工程だけOpus、分類・抽出はHaikuに振り分ける
- 構造化出力はAI SDKのgenerateObject＋Zodで型安全に縛り、AIの出力も外部入力とみなして境界で検証する
- エージェントの暴走はstopWhen: stepCountIs(n)で最大ステップ数を必ず上限設定し、不可逆な副作用は承認ゲートで止める
- プロンプトキャッシュは接頭辞の完全一致。固定内容を先頭・変動内容を後ろに置き、cacheReadInputTokensで効きを計測する

---

「PoCでLLMを呼んだら動いた。でも本番に出した瞬間、JSONが壊れて落ちた」

「ツールを使うエージェントを組んだら、無限ループでトークンを溶かした」

「ストリーミングUIを作ったが、中断もキャンセルもできず、ユーザーが固まった画面を見ている」

「コストが読めない。月末の請求が来てから初めて『高い』と気づく」

AI機能の開発を外注検討している企業も、自分で実装している開発者も、ここで止まります。「とりあえずLLMを呼ぶ」のは1日で終わります。**本番品質に上げる作業こそが本体** です。

私は経済産業大臣賞を受賞したB2B SaaSの中核エンジニアであり、国内大手放送事業者向けの社内AIプラットフォーム（5つのAIサービス・認証ハブ・音声合成・OCR×音声認識の誤字検出・生成AI考査支援）を構築してきました。そこで効いたのは派手なモデルではなく、**構造化出力・ツールの安全設計・キャンセル可能なストリーミング・プロンプトキャッシュ・検証ゲート** という地味な本番技術でした。

この記事は、それらを **Anthropic公式（docs.anthropic.com / platform.claude.com）と Vercel AI SDK v6 公式（ai-sdk.dev / vercel.com/docs）に忠実** に、TypeScriptの実コードで示します。各セクション末尾に参照した公式URLを明記しています。

> **この記事の地図**
> 1. モデル選定（Opus 4.8 / Sonnet 4.6 / Haiku 4.5 / Fable 5）
> 2. 基本：AI SDK v6 の `generateText` / `streamText` と AI Gateway のモデル指定
> 3. 構造化出力：`generateObject` / `streamObject` + Zod で型安全に
> 4. ツール使用：`tool()` 定義とマルチステップ・エージェント化
> 5. ストリーミングUX：Route Handler → `useChat`、キャンセル/中断
> 6. コスト・性能最適化：プロンプトキャッシュ・ルーティング・計測
> 7. 信頼性・可観測性：リトライ・ガードレール・ハルシネーション抑制
> 8. セキュリティ：APIキー秘匿・プロンプトインジェクション・PII

---

## まず押さえる：「一人 × 生成AI」が速くて安いのは、本番技術を省略しないから

生成AI（Claude Code）をアクセラレータにすると、一人でも企業案件のAI機能を速く・安く実装できます。ただしそれは「コードを速く書ける」からではありません。**検証ゲートを自動化し、本番品質の型を守るから** です。

- 出力は**必ずスキーマで縛り**、境界でバリデーションする（壊れたJSONを下流に流さない）
- ツールは**権限最小化**し、副作用を隔離する（暴走の被害半径を小さく）
- ストリーミングは**キャンセル可能**にする（ユーザーの離脱を止める）
- コストは**事前に計測**し、キャッシュで削る（請求書で驚かない）
- 出力は**人間 or 別モデルの検証ゲート**を通す（ハルシネーションを本番に出さない）

以降、この5原則を具体的なコードに落とします。

---

## モデル選定：知能 vs コスト vs レイテンシ

最初の設計判断はモデル選定です。Anthropic公式のモデル一覧を踏まえ、2026年6月時点の使い分けを表にします。**「全部Opus」も「全部Haiku」も間違い** です。場面ごとに最適点が違います。

| モデル | モデルID | コンテキスト | 想定用途 | 判断軸 |
|--------|---------|------------|---------|--------|
| **Claude Opus 4.8** | `claude-opus-4-8` | 1M | 長時間の自律エージェント・複雑なコード生成・難度の高い知識作業 | 最高知能。難タスクの中核 |
| **Claude Sonnet 4.6** | `claude-sonnet-4-6` | 1M | 大多数のアプリの既定。RAG・要約・対話・ツール使用 | 速度と知能のバランス最良 |
| **Claude Haiku 4.5** | `claude-haiku-4-5` | 200K | 分類・抽出・ルーティング・前処理・大量バッチ | 最速・最安。単純タスク |
| **Claude Fable 5** | `claude-fable-5` | 1M | 最難関の推論・超長期エージェント | 最も高性能。料金はOpus超 |

実務での割り当ての考え方：

- **既定は Sonnet 4.6。** 迷ったらここ。多くのB2B機能（RAG回答・社内検索・整形）はSonnetで十分な品質が出ます。
- **難しい工程だけ Opus 4.8。** 仕様書からの一括実装、深いリファクタ、長時間の自律ループ。コストはかかるが、誤りの修正コストの方が高い場面に投資します。
- **前段・大量処理は Haiku 4.5。** 「この問い合わせはどのカテゴリか」「この文書から日付を抜く」のような単純タスクは、安く速いHaikuに振る。エージェントの**サブエージェント**にもHaikuが効きます。
- **Fable 5 は明示的に選ぶときだけ。** トークナイザが変わり同じ内容でも約30%多くトークンを消費し、料金もOpus超。`thinking` パラメータの仕様も異なる（常時オン）ため、「最も難しい問題」専用と考えます。「とりあえず最新」で選ぶ対象ではありません。

> ヒント：知能の調整は **モデルの差し替えだけでなく `effort` パラメータ** でも行えます（`low`/`medium`/`high`/`xhigh`/`max`）。Opus 4.8 では `high` を既定に、コーディング/エージェントは `xhigh` が推奨です。AI SDK 経由なら `providerOptions.anthropic` でモデル個別オプションとして渡せます。

> 出典: [Models overview](https://platform.claude.com/docs/en/about-claude/models/overview) / [Effort](https://platform.claude.com/docs/en/build-with-claude/effort)

---

## 基本：AI SDK v6 と AI Gateway でモデルを呼ぶ

実装の土台は Vercel AI SDK v6（`ai` パッケージ）です。**モデルの指定方法が本番設計を左右する** ので、まずここを正します。

### 推奨：AI Gateway の `"provider/model"` 文字列

2025年8月にGAした **Vercel AI Gateway** を経由すると、`"anthropic/claude-opus-4.8"` のような `"プロバイダ/モデル"` 文字列だけでモデルを指定できます。プロバイダ個別パッケージ（`@ai-sdk/anthropic`）のインポートは不要です。

AI Gateway を使う理由（公式の利点）：

- **1キーで複数プロバイダ**。`AI_GATEWAY_API_KEY` 1本で Anthropic / Bedrock / Vertex 等にアクセス。
- **自動フォールバック**。あるプロバイダが落ちても別経路へ自動リトライ。
- **トークンに上乗せなし**（no markup）。直接利用と同じ単価。
- **支出の可視化**。プロバイダ横断でコストを監視。

```ts
// app/api/summary/route.ts
import { generateText } from "ai";

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

  const { text, usage, finishReason } = await generateText({
    // "provider/model" 文字列。@ai-sdk/anthropic の import は不要
    model: "anthropic/claude-sonnet-4.6",
    system:
      "あなたは技術記事の要約者です。事実のみを、箇条書き3点で日本語要約してください。",
    prompt: `次の記事を要約してください:\n\n${article}`,
  });

  return Response.json({ text, usage, finishReason });
}
```

認証は環境変数 `AI_GATEWAY_API_KEY` を読むだけ（Vercelデプロイ時はOIDCトークンでも可）。**APIキーをコードに書かない**ことが大前提です（詳細は後述のセキュリティ章）。

> 補足：AI Gateway のモデルスラッグは公式表記がドット区切り（`anthropic/claude-opus-4.8`）です。一方、Anthropic API を**直接**（`@ai-sdk/anthropic` や Anthropic SDK で）叩く場合のモデルIDはハイフン区切りの一次表記（`claude-opus-4-8` / `claude-sonnet-4-6` / `claude-haiku-4-5`）になります。経路によって表記が変わる点に注意してください。

### ストリーミングを既定にする

長い入力・長い出力・高い `max_tokens` を伴うリクエストは、HTTPタイムアウトを避けるため **ストリーミングを既定** にします。サーバ側は `streamText`：

```ts
import { streamText } from "ai";

const result = streamText({
  model: "anthropic/claude-sonnet-4.6",
  system: "簡潔で正確な日本語で答えてください。",
  prompt: "Reactのレンダリング最適化を3行で説明して。",
});

for await (const textPart of result.textStream) {
  process.stdout.write(textPart);
}
```

### 明示的に個別プロバイダを使いたいとき

「Anthropic直叩きにしたい」「プロンプトキャッシュの細かい指定をしたい」など明示的要件があるときだけ、個別パッケージ `@ai-sdk/anthropic` を使います。

```ts
import { createAnthropic } from "@ai-sdk/anthropic";
import { generateText } from "ai";

const anthropic = createAnthropic({
  apiKey: process.env.ANTHROPIC_API_KEY, // 直叩きの場合のみ
});

const { text } = await generateText({
  model: anthropic("claude-sonnet-4-6"), // 一次表記（ハイフン区切り）
  prompt: "...",
});
```

> 出典: [AI Gateway](https://vercel.com/docs/ai-gateway) / [AI Gateway: Text Generation](https://vercel.com/docs/ai-gateway/getting-started/text) / [AI SDK Core: generateText/streamText](https://ai-sdk.dev/docs/ai-sdk-core/generating-text)

---

## 構造化出力：壊れたJSONを本番に流さない

本番AI機能で最初に効くのが **構造化出力** です。LLMに「JSONで返して」と頼むだけでは、前後に説明文が混ざったり、フィールドが欠けたりします。AI SDK v6 はスキーマ（Zod）で出力を縛り、**SDKがパース・検証まで** やってくれます。

### `generateObject`：抽出・分類・整形

問い合わせメールから構造化データを抜く例です。`schema` に Zod を渡すと、結果の `object` が型付きで返ります。

```ts
import { generateObject } from "ai";
import { z } from "zod";

const LeadSchema = z.object({
  name: z.string().describe("問い合わせ者の氏名"),
  email: z.string().describe("連絡先メールアドレス"),
  plan: z.enum(["lite", "standard", "enterprise"]).describe("希望プラン"),
  interests: z.array(z.string()).describe("関心のある機能"),
  demoRequested: z.boolean().describe("デモ希望の有無"),
});

const { object, usage } = await generateObject({
  model: "anthropic/claude-haiku-4.5", // 抽出は安価なHaikuで十分
  schema: LeadSchema,
  prompt:
    "次の問い合わせから情報を抽出: 「田中太郎です(tanaka@example.com)。" +
    "Enterpriseを検討中で、API連携とSSOに関心あり。デモ希望です」",
});

// object は LeadSchema 型として保証される
console.log(object.plan); // "enterprise"
```

> Zodの一部制約（`min`/`max`/`minLength` 等）はClaude側のスキーマでは非対応ですが、AI SDKがそれらをクライアント側で検証してくれます。境界での検証は **AIの出力も「外部入力」とみなす** 設計の核心です。信頼せず、必ずスキーマで縛ります。

### 分類タスク：enumで選択肢を固定

「この問い合わせをどの部署に振るか」のような分類は、`enum` で選択肢を固定するのが最も堅牢です。Haikuに振ってコストを抑えます。

```ts
const { object } = await generateObject({
  model: "anthropic/claude-haiku-4.5",
  schema: z.object({
    category: z.enum(["技術サポート", "営業", "請求", "その他"]),
    urgency: z.enum(["低", "中", "高"]),
  }),
  prompt: `次の問い合わせを分類: ${inquiry}`,
});
```

### `streamObject`：生成しながら部分表示

長いレポートやリストを生成する場合、`streamObject` で **部分オブジェクトを逐次** 受け取れます。UIに「埋まっていく」体験を出せます。

```ts
import { streamObject } from "ai";
import { z } from "zod";

const { partialObjectStream } = streamObject({
  model: "anthropic/claude-sonnet-4.6",
  schema: z.object({
    title: z.string(),
    sections: z.array(z.object({ heading: z.string(), body: z.string() })),
  }),
  prompt: "新製品の発表ブログ記事の構成を生成して。",
});

for await (const partial of partialObjectStream) {
  // partial は生成途中の部分オブジェクト（型は Deep Partial）
  render(partial);
}
```

> 出典: [AI SDK Core: Generating Structured Data](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data)

---

## ツール使用：エージェント化と「暴走させない」設計

LLMに外部API・DB・計算へのアクセスを与えるのが **ツール使用（tool calling）** です。AI SDK v6 では `tool()` ヘルパで定義し、Zodで入力を縛り、`execute` で実処理を書きます。

### ツール定義

```ts
import { tool } from "ai";
import { z } from "zod";

const getOrderStatus = tool({
  description:
    "注文IDから配送ステータスを取得する。ユーザーが注文状況を尋ねたときに使う。",
  inputSchema: z.object({
    orderId: z.string().describe("注文ID（例: ORD-12345）"),
  }),
  execute: async ({ orderId }) => {
    // 権限最小化: 読み取り専用の照会APIだけを呼ぶ
    const status = await db.orders.findStatus(orderId);
    return { orderId, status }; // 戻り値はモデルのコンテキストに入る
  },
});
```

ツールの `description` は **「いつ呼ぶか」を明示** するのがコツです。「何をするか」だけでなく「ユーザーが○○を尋ねたとき」と書くと、最近のClaudeは呼び出し判断の精度が上がります。

### マルチステップ＝エージェント化（`stopWhen`）

ツールの結果を見て次のツールを呼ぶ、という反復を有効化するのが `stopWhen` です。`stepCountIs(n)` で **最大ステップ数を必ず上限設定** します。これが暴走（無限ループ・トークン溶解）を防ぐ最重要ガードです。

```ts
import { generateText, stepCountIs, tool } from "ai";

const { text, steps } = await generateText({
  model: "anthropic/claude-sonnet-4.6",
  tools: { getOrderStatus, searchKnowledgeBase },
  // 上限を必ず設ける。無限ループとコスト爆発の最初の防波堤
  stopWhen: stepCountIs(5),
  system:
    "あなたはカスタマーサポートです。ツールで事実を確認してから答えてください。",
  prompt: "注文ORD-12345はいつ届きますか？",
});
```

### ツールの安全な設計（権限最小化・副作用の隔離）

ツールはLLMが実行を要求し、**あなたのコードが実際に動かします**。だからこそ、ツールの形が安全性を決めます。

- **読み取りと書き込みを分ける。** `searchKnowledgeBase`（読み取り）は自動実行してよいが、`sendEmail` / `issueRefund`（不可逆な副作用）は人間の承認を挟む。
- **不可逆な操作はゲートする。** AI SDK の `stopWhen` でステップ境界に達したら、`steps` を検査して「副作用ツールが呼ばれていないか」を確認し、承認UIを出してから続行する設計が堅牢です。
- **入力は `execute` 内で再検証する。** Zodで型は縛れますが、`orderId` がそのユーザーのものか、といった**認可**は `execute` 内で別途チェックします。LLMの出した値を信頼してDBを引いてはいけません。

```ts
const issueRefund = tool({
  description: "返金を実行する。金額と注文IDが必要。",
  inputSchema: z.object({
    orderId: z.string(),
    amount: z.number(),
  }),
  execute: async ({ orderId, amount }, { abortSignal }) => {
    // 認可: このツールを起動したユーザーが当該注文の持ち主か再確認
    await assertOwnership(currentUserId, orderId);
    // 副作用は隔離されたサービス層経由でのみ実行
    return await refundService.execute(orderId, amount, { abortSignal });
  },
});
```

> ステップごとにモデルや `toolChoice` を切り替えたいときは `prepareStep` を使います（例: 初手だけ `toolChoice: 'required'` でツール起動を強制）。

> 出典: [AI SDK Core: Tools and Tool Calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) / [Tool use overview](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)

---

## ストリーミングUX：キャンセル可能・アクセシブルな逐次表示

「待たされている感」を消すのがストリーミングUXです。サーバの Route Handler とクライアントの `useChat` を組み合わせます。

### サーバ：Route Handler

クライアントから来たUIメッセージを `convertToModelMessages` でモデル用に変換し、`streamText` の結果を `toUIMessageStreamResponse()` で返します。

```ts
// app/api/chat/route.ts
import { convertToModelMessages, streamText, type UIMessage } from "ai";

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

  const result = streamText({
    model: "anthropic/claude-sonnet-4.6",
    system: "あなたは親切なアシスタントです。簡潔に答えてください。",
    messages: convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse();
}
```

### クライアント：`useChat`（中断・キャンセル込み）

`@ai-sdk/react` の `useChat` が、メッセージ・入力・状態（`status`）・中断（`stop`）を管理してくれます。**`status` と `stop` を必ずUIに繋ぐ** のが本番品質の分かれ目です。

```tsx
"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";

export function Chat() {
  const [input, setInput] = useState("");
  const { messages, sendMessage, status, stop } = useChat({
    transport: new DefaultChatTransport({ api: "/api/chat" }),
  });

  const isBusy = status === "submitted" || status === "streaming";

  return (
    <div>
      {/* aria-live で、追記される応答をスクリーンリーダーに伝える */}
      <div aria-live="polite">
        {messages.map((m) => (
          <article key={m.id}>
            <strong>{m.role === "user" ? "あなた" : "AI"}</strong>
            {m.parts.map((part, i) =>
              part.type === "text" ? <span key={i}>{part.text}</span> : null,
            )}
          </article>
        ))}
      </div>

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (!input.trim() || isBusy) return;
          sendMessage({ text: input });
          setInput("");
        }}
      >
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          aria-label="メッセージを入力"
        />
        {isBusy ? (
          // 生成中はキャンセルボタンに切り替える
          <button type="button" onClick={() => stop()}>
            停止
          </button>
        ) : (
          <button type="submit">送信</button>
        )}
      </form>
    </div>
  );
}
```

アクセシビリティの要点：

- 応答領域に `aria-live="polite"` を付け、逐次追記をスクリーンリーダーに伝える。
- 生成中（`streaming`）は送信ボタンを **停止ボタンに切り替え**、`stop()` で中断可能に。
- `status === "error"` の分岐を用意し、再送導線を出す。

> 出典: [AI SDK UI: Chatbot](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot) / [Streaming](https://platform.claude.com/docs/en/build-with-claude/streaming)

---

## コスト・性能最適化：プロンプトキャッシュとルーティング

「コストが読めない」を解決します。最も効くのは **プロンプトキャッシュ** と **モデルのルーティング/フォールバック** です。

### プロンプトキャッシュ：接頭辞の使い回し

プロンプトキャッシュは **接頭辞（prefix）の完全一致** です。`tools` → `system` → `messages` の順にレンダリングされるので、**安定した内容（固定のシステムプロンプト・ナレッジ）を先頭に、変動する内容（ユーザーの質問）を後ろに** 置きます。先頭に `Date.now()` やリクエストIDを差し込むと、それ以降が全てキャッシュ無効になります。

AI SDK で Anthropic のキャッシュを使うには、対象コンテンツに `providerOptions.anthropic.cacheControl` を付けます。

```ts
import { generateText } from "ai";

const { text, providerMetadata } = await generateText({
  model: "anthropic/claude-sonnet-4.6",
  messages: [
    {
      role: "system",
      content: LARGE_KNOWLEDGE_BASE, // 数千トークンの固定コンテキスト
      providerOptions: {
        anthropic: { cacheControl: { type: "ephemeral" } }, // キャッシュ境界
      },
    },
    { role: "user", content: userQuestion }, // 変動部分は後ろ。マーカー無し
  ],
});

// キャッシュの効きを必ず計測する
console.log(providerMetadata?.anthropic);
// cacheCreationInputTokens（書き込み）/ cacheReadInputTokens（読み出し）
```

AI Gateway 経由なら、`providerOptions.gateway.caching: 'auto'` でプロバイダに応じたキャッシュ戦略を自動適用させることもできます（Anthropicのように明示的なキャッシュマーカーが要るプロバイダで便利）。

```ts
const result = streamText({
  model: "anthropic/claude-sonnet-4.6",
  messages,
  providerOptions: { gateway: { caching: "auto" } },
});
```

経済性の目安：キャッシュ読み出しは基本入力単価の約0.1倍、書き込みは約1.25倍（5分TTL）。**同じ接頭辞を2回以上使うなら、ほぼ確実に得** になります。`cacheReadInputTokens` が常に0なら、`datetime.now()` の混入や非決定的なJSON順序など「サイレント無効化」を疑ってください。

### ルーティングとフォールバック（AI Gateway）

可用性とコストの両方を、`providerOptions.gateway` の `order` / `only` / `sort` で制御します。

```ts
const result = await generateText({
  model: "anthropic/claude-sonnet-4.6",
  prompt,
  providerOptions: {
    gateway: {
      order: ["bedrock", "anthropic"], // Bedrock優先、ダメならAnthropic
      sort: "cost", // コスト最小のプロバイダから試す（'ttft'/'tps'も可）
    },
  },
});
```

### トークン使用量の計測とバッチの考え方

- **使用量は毎回記録する。** `usage` / `totalUsage` をログに出し、ルート別・モデル別に集計。これが「請求書で驚かない」唯一の方法です。
- **不要な再生成を抑える。** 同一入力に対する結果は（決定性が許す範囲で）アプリ側でキャッシュする。LLMを呼ばないのが最大のコスト削減です。
- **レイテンシ非依存の大量処理はバッチ/並列で。** 分類・抽出のような独立タスクは、安価なHaikuに並列で投げる。Anthropic の Message Batches API は非同期で標準価格の50%です。

> 出典: [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) / [AI Gateway: Provider Options](https://vercel.com/docs/ai-gateway/models-and-providers/provider-options)

---

## 信頼性・可観測性：失敗を前提に設計する

本番では **失敗が起きる前提** で組みます。LLMは確率的で、外部APIは落ち、レート制限にも当たります。

### リトライ・タイムアウト・レート制限

- Anthropic SDK / AI SDK は **429・5xx を指数バックオフで自動リトライ** します（既定の再試行回数あり）。これに任せつつ、アプリ固有の上限を別途設けます。
- 長時間ツールに `abortSignal` を渡し、上流のタイムアウト/キャンセルを伝播させます（前掲の `issueRefund` 参照）。
- レート制限（429）には `retry-after` ヘッダがあり、SDKが読んで待機します。バースト時は **Haikuに退避** したり、キューイングを挟むのが現実解です。

### 可観測性：使用量・レイテンシ・失敗率

最低限、以下を構造化ログに出します。「動いているか」ではなく **「いくら・どれだけ速く・どれくらい失敗しているか」** を見える化します。

```ts
const started = performance.now();
const { text, usage, finishReason } = await generateText({
  model: "anthropic/claude-sonnet-4.6",
  prompt,
});

logger.info("llm.call", {
  route: "summary",
  model: "anthropic/claude-sonnet-4.6",
  inputTokens: usage.inputTokens,
  outputTokens: usage.outputTokens,
  finishReason, // "stop" / "length" / "tool-calls" など
  latencyMs: Math.round(performance.now() - started),
});
```

AI Gateway 自体も、プロバイダ横断の使用量・レイテンシ・支出のObservabilityを提供します。

### ガードレールとハルシネーション抑制（検証ゲート）

LLMの出力をそのまま本番に流さない。**検証ゲート** を必ず挟みます。

1. **スキーマで縛る**（前述の構造化出力）。形が壊れていたら下流に行かせない。
2. **ツールで事実を取りに行かせる**。「知識から答えるな、まず `search` を呼べ」とシステムプロンプトで明示し、根拠のない断定を抑える。
3. **重要な出力は別工程で検証**。生成と検証を分ける（生成は網羅、検証は別パスで取捨）。コードレビューや考査のように「人間 or 別モデルが確認する」工程を制度化する。

私が放送事業者向けに構築した考査支援・誤字検出は、まさにこの「**生成はAI、最終確認は検証ゲート**」の形でした。AIをアクセラレータに置き、品質保証を人間（と別モデル）の検証で担保する。これが「速い・安い」と「安全」を両立させる勘所です。

> 出典: [Errors / rate limits](https://platform.claude.com/docs/en/api/errors) / [Tool use overview](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)

---

## セキュリティ：APIキー・プロンプトインジェクション・PII

最後に、外注検討時にも必ず確認すべきセキュリティ要件です。

### APIキーの秘匿

- **キーはコードに書かない。** `AI_GATEWAY_API_KEY` / `ANTHROPIC_API_KEY` は環境変数 or シークレットマネージャに置く。リポジトリ・クライアントバンドル・ログに絶対出さない。
- **ブラウザから直接LLMを呼ばない。** 必ずサーバの Route Handler を経由させる（クライアントにキーを露出させない）。Vercelデプロイなら OIDC トークン認証も選択肢。

### プロンプトインジェクション対策

外部由来のテキスト（ユーザー入力・取得したWebページ・ツール結果）は、**指示ではなくデータとして扱う** のが原則です。

- **権限分離**：ツール経由の操作はLLMの判断だけで実行せず、不可逆な副作用は承認ゲート＋サーバ側の認可で守る（前述の `issueRefund`）。
- **信頼境界の明示**：システムプロンプト（運用者の指示）と、ユーザー/取得コンテンツ（信頼しないデータ）を混同しない。後者に「これまでの指示を無視せよ」と書かれていても、副作用ツールがサーバ側認可で守られていれば被害半径は限定されます。
- **出力の検証**：注入が成功してもスキーマ・認可・検証ゲートで止める多層防御にする。

### PIIの取り扱い

- 問い合わせフォームのPIIを必要以上にログに残さない。前掲のログ例も**トークン数とメタ情報だけ**で、本文やPIIは出していません。
- 個人データの保持は規制（GDPR/個人情報保護法）に照らして判断する。メモリ機能等に秘密情報を保存しない。

> 出典: [Tool use overview](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) / [AI Gateway: Authentication](https://vercel.com/docs/ai-gateway/authentication-and-byok/authentication)

---

## FAQ

**Q1. AI Gateway は必須ですか？個別パッケージ（`@ai-sdk/anthropic`）ではダメ？**
必須ではありません。ただし v6 の既定の推奨は AI Gateway 経由の `"provider/model"` 文字列指定です。1キーで複数プロバイダ・自動フォールバック・支出可視化が得られ、トークン単価の上乗せもありません。プロンプトキャッシュの細かい指定など**明示的な要件があるときだけ** 個別パッケージを使う、という切り分けが実務的です。

**Q2. `generateObject` と `streamObject`、`generateText`＋`Output` はどう使い分ける？**
抽出・分類・整形のように「構造化された結果が主目的」なら `generateObject` / `streamObject` が素直です。一方、テキスト生成の中で構造化出力も欲しい（ツール使用と併用するなど）場合は `generateText` / `streamText` に `Output.object()` を組み合わせる手もあります。どちらも Zod スキーマで型安全に縛れます。

**Q3. エージェントが暴走してトークンを溶かさないか不安です。**
`stopWhen: stepCountIs(n)` で**最大ステップ数を必ず上限設定**してください。加えて、不可逆な副作用ツールは承認ゲートで止め、`usage` をログして異常な消費を検知します。Opus 4.7 以降は API ネイティブの **Task Budgets**（モデルに残トークンを伝える）も使えます。

**Q4. モデルは全部 Opus 4.8 にすればいい？**
いいえ。コストとレイテンシが見合いません。既定は Sonnet 4.6、単純タスク（分類・抽出・前処理）は Haiku 4.5、難工程だけ Opus 4.8、という割り当てが最適点です。`effort` パラメータでも知能/コストを調整できます。

**Q5. 出力がたまに事実と違います（ハルシネーション）。**
モデル選定だけでは解決しません。①スキーマで縛る ②ツールで事実を取りに行かせる（「知識から答えず search を呼べ」）③重要な出力は別工程で検証する、の検証ゲートを重ねます。生成はAI、最終確認は人間 or 別モデル、という分業が効きます。

---

## まとめ：本番品質は「地味な技術」の積み上げ

「とりあえずLLMを呼ぶ」から本番品質へ上げる壁は、構造化出力・ツールの安全設計・キャンセル可能なストリーミング・プロンプトキャッシュ・検証ゲートという、地味だが効く技術の積み上げで越えられます。Claude API × AI SDK v6 は、それらを公式の型に沿って素直に実装できる組み合わせです。

私は経済産業大臣賞を受賞したB2B SaaSの中核エンジニアとして、また国内大手放送事業者向け社内AIプラットフォーム（5つのAIサービス・認証ハブ・音声合成・OCR×音声認識の誤字検出・生成AI考査支援）の構築者として、この「生成はAI、品質は検証ゲート」の設計を実戦で運用してきました。**一人 × 生成AI（Claude Code）** だからこそ、本番技術を省略せず、速く・安く・安全にAI機能を届けられます。

AI機能の開発・PoCからの本番化・既存システムへのAI統合をご検討中なら、お気軽にご相談ください。

[お問い合わせはこちら](/contact)

---

## 参考（Anthropic / Vercel AI SDK 公式）

- [Anthropic — Models overview](https://platform.claude.com/docs/en/about-claude/models/overview)
- [Anthropic — Tool use overview](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)
- [Anthropic — Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)
- [Anthropic — Streaming](https://platform.claude.com/docs/en/build-with-claude/streaming)
- [Anthropic — Effort](https://platform.claude.com/docs/en/build-with-claude/effort)
- [Vercel — AI Gateway](https://vercel.com/docs/ai-gateway)
- [Vercel — AI Gateway: Provider Options](https://vercel.com/docs/ai-gateway/models-and-providers/provider-options)
- [AI SDK Core — Generating Text](https://ai-sdk.dev/docs/ai-sdk-core/generating-text)
- [AI SDK Core — Generating Structured Data](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data)
- [AI SDK Core — Tools and Tool Calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling)
- [AI SDK UI — Chatbot (useChat)](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot)
- [AI SDK Providers — Anthropic](https://ai-sdk.dev/providers/ai-sdk-providers/anthropic)
