AI

Vercel AI SDK 7入門——Node.js 22とAgent APIでAI機能を作り直す

Vercel AI SDK 7のNode.js 22必須化、ESM専用化、Agent APIを踏まえ、Next.js + FirestoreでAI機能を移行する実践手順を解説します。

2026年6月28日
Vercel AI SDKNext.jsFirestoreGitHub ActionsAIエージェント
Vercel AI SDK 7入門——Node.js 22とAgent APIでAI機能を作り直す

はじめに

AI SDKを使ったNext.jsアプリで、こんな不安はありませんか?

  • AI呼び出しのコードが増え、どの機能がどのモデルを使っているか追いにくい
  • ツール呼び出しや利用量ログを後付けで足していて、Route Handlerが太っている
  • Node.jsやパッケージ更新のタイミングで、CIだけ落ちるのが怖い

2026年6月25日、Vercelは AI SDK 7 を発表しました。公式ブログとChangelogでは、Node.js 22以上、ESM専用化、Zod v4対応、Agent API、ツール実行まわりの型改善、telemetryとtimeoutの再設計などが紹介されています。

本記事では、架空の業務支援SaaS「TaskLedger」を題材に、Next.js App Router + Firestore + GitHub ActionsでAI機能をAI SDK 7向けに整理するハンズオンを行います。

参考にした一次情報は次の通りです。

何を優先して見るべきか

AI SDK 7は「新機能が増えた」だけではなく、実行環境と設計単位を見直すリリースです。Web系エンジニアが最初に確認すべき点を整理します。

観点確認することTaskLedgerでの方針
Node.jsNode.js 22以上で動かすCIと本番ランタイムを22へ寄せる
モジュールESM importを使うrequire("ai") の自作スクリプトをなくす
スキーマZod v4対応を確認するzod/v4 importへ寄せる
エージェントAgent APIの責務を分けるまずRoute Handlerを薄くし、将来Agent化できる形にする
監査telemetryとFirestoreログを分ける技術メトリクスと業務監査ログを混ぜない

ポイントは、AI SDKの新APIを一気に全面採用することではありません。まず「AI呼び出しを業務コードから分離し、ログを残し、Node.js 22でCIが通る」状態を作るのが実務的です。

事前準備: Node.js 22と依存関係をそろえる

AI SDK 7では、まず実行環境をそろえるところから始めます。Next.jsアプリ側では、AI SDK本体、Firebase Admin SDK、スキーマ定義用のZodを使います。

yarn add ai@latest firebase-admin zod

GitHub ActionsではNode.js 22を明示します。既存プロジェクトがNode.js 20や18で動いている場合、AI機能だけでなくビルド全体への影響を見たいので、PR上で yarn build まで回します。

name: build

on:
  pull_request:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: yarn
      - run: yarn install --frozen-lockfile
      - run: yarn build

ローカルでもCIでも同じNode.jsを使うため、package.json にエンジン条件を置いておくとレビュー時に意図が伝わります。

{
  "engines": {
    "node": ">=22"
  }
}

ハンズオン1: FirestoreにAI実行台帳を作る

TaskLedgerでは、20〜30件のGitHub Issue相当のタスクをAIで棚卸しし、実装順とリスクを提案してもらいます。ただし、AIの返答だけを保存しても運用には足りません。誰が、どのプロジェクトで、どのタスク集合を対象に、どのモデル設定で実行したかを残します。

Firestoreには aiRuns コレクションを作る想定です。

フィールド用途
projectIdstring対象プロジェクト
requestedBystring実行ユーザーID
taskIdsstring[]対象タスク
modelstring利用モデルID
statusstringrunning / done / failed
summarystringAIの要約結果
createdAttimestamp実行開始
finishedAttimestamp実行終了

プロンプト全文をFirestoreに保存するかは慎重に決めます。個人情報や顧客固有の文脈が混ざる場合、入力全文ではなく、対象ID、件数、ハッシュ、出力要約だけを保存する方が扱いやすいです。

ハンズオン2: AI呼び出しをサービス関数に分離する

次に、AI SDK 7を使う処理をRoute Handlerから切り出します。ここでは generateText を使い、Firestoreから読んだタスク情報を短く整形して渡します。モデルIDは環境変数から読み、実際に使うIDはVercel AI Gatewayや各プロバイダの公式画面で管理する前提です。

// src/lib/ai/summarizeTasks.ts
import { generateText } from "ai";

type TaskInput = {
  taskId: string;
  title: string;
  status: "todo" | "doing" | "blocked" | "done";
  risk: "low" | "medium" | "high";
};

type SummarizeTasksInput = {
  projectName: string;
  tasks: TaskInput[];
};

export async function summarizeTasks(input: SummarizeTasksInput) {
  const model = process.env.AI_MODEL_ID;

  if (!model) {
    throw new Error("AI_MODEL_ID is not configured");
  }

  const taskList = input.tasks
    .map((task) => {
      return [
        `ID: ${task.taskId}`,
        `Title: ${task.title}`,
        `Status: ${task.status}`,
        `Risk: ${task.risk}`,
      ].join("\n");
    })
    .join("\n\n---\n\n");

  const result = await generateText({
    model,
    system:
      "You are a senior TypeScript project reviewer. Reply in Japanese. Focus on implementation order, blockers, and verification.",
    prompt: [
      `Project: ${input.projectName}`,
      "次のタスクを、Next.js App Router、Firestore、GitHub Actionsで実装する前提で整理してください。",
      "出力は「優先順位」「先に解消する依存関係」「CIで確認すること」の3セクションにしてください。",
      taskList,
    ].join("\n\n"),
  });

  return {
    text: result.text,
    usage: result.usage,
    finishReason: result.finishReason,
  };
}

この形にしておくと、後からAgent APIを使う場合でも、Route Handler全体を書き換えずにAI実行部分だけを差し替えられます。AI SDK 7の公式情報では、ToolLoopAgent、WorkflowAgent、HumanInTheLoopAgentのように用途別のAgent APIが用意されています。最初から複雑なエージェントに寄せるより、まず「入力整形」「AI実行」「監査ログ」を分けるのが安全です。

ハンズオン3: Route Handlerで権限確認とログ保存を行う

Route Handlerでは、AIを呼ぶ前に必ず業務認可を確認します。AI SDKやモデルは、Firestore上のプロジェクト権限を知りません。

// src/app/api/projects/[projectId]/ai-summary/route.ts
import { FieldValue } from "firebase-admin/firestore";
import { adminDb } from "@/lib/firebase/admin";
import { summarizeTasks } from "@/lib/ai/summarizeTasks";
import { requireUser } from "@/lib/auth/requireUser";
import { assertCanReadProject } from "@/lib/projects/permissions";

type Params = {
  params: Promise<{ projectId: string }>;
};

type RequestBody = {
  taskIds: string[];
};

export async function POST(request: Request, { params }: Params) {
  const user = await requireUser(request);
  const { projectId } = await params;
  const body = (await request.json()) as RequestBody;

  await assertCanReadProject(user.uid, projectId);

  const taskIds = body.taskIds.slice(0, 30);

  if (taskIds.length === 0) {
    return Response.json({ error: "taskIds is required" }, { status: 400 });
  }

  const tasksSnapshot = await adminDb
    .collection("tasks")
    .where("projectId", "==", projectId)
    .where("taskId", "in", taskIds)
    .get();

  const tasks = tasksSnapshot.docs.map((doc) => {
    const data = doc.data() as {
      taskId: string;
      title: string;
      status: "todo" | "doing" | "blocked" | "done";
      risk: "low" | "medium" | "high";
    };

    return data;
  });

  const runRef = adminDb.collection("aiRuns").doc();

  await runRef.set({
    projectId,
    requestedBy: user.uid,
    taskIds,
    model: process.env.AI_MODEL_ID ?? null,
    status: "running",
    createdAt: FieldValue.serverTimestamp(),
  });

  try {
    const result = await summarizeTasks({
      projectName: "TaskLedger",
      tasks,
    });

    await runRef.update({
      status: "done",
      summary: result.text,
      usage: result.usage ?? null,
      finishReason: result.finishReason ?? null,
      finishedAt: FieldValue.serverTimestamp(),
    });

    return Response.json({ runId: runRef.id, summary: result.text });
  } catch (error) {
    await runRef.update({
      status: "failed",
      errorMessage: error instanceof Error ? error.message : "Unknown error",
      finishedAt: FieldValue.serverTimestamp(),
    });

    return Response.json({ error: "AI summary failed" }, { status: 500 });
  }
}

Firestoreの in クエリには件数上限があります。大量のタスクを扱う場合は、30件前後のバッチに分け、最初にブロッカーだけを抽出するなど、AIに渡す情報量を制御します。

ハンズオン4: 30タスクを小さな波に分ける

AI SDK 7のAgent APIは魅力的ですが、実務ではタスクの分け方が先です。TaskLedgerでは、30件のタスクを次のように扱います。

AIには「全部を実装して」ではなく、「Waveごとのリスクを整理して」と頼む方が精度が上がります。AI SDK 7のtimeoutやtelemetryの改善は、こうした実行単位を運用で追うときに効いてきます。Firestoreの aiRuns は業務監査、VercelやOpenTelemetry側のtelemetryは技術メトリクス、と役割を分けると後から調査しやすくなります。

移行時の注意点

AI SDK 7へ上げるときは、次の順番で確認すると事故が少ないです。

  1. CIをNode.js 22へ上げ、yarn build が通るか確認する
  2. require("ai") を使う自作スクリプトをESM importへ直す
  3. Zodを使っている箇所をZod v4対応で確認する
  4. AI呼び出しをRoute Handlerからサービス関数へ分離する
  5. Firestoreに実行台帳を残し、失敗時の再実行単位を決める
  6. Agent APIは、まず1つの業務フローで小さく試す

特に注意したいのは、AI SDKのtelemetryとFirestoreログを混同しないことです。telemetryはレイテンシや実行状況を見るための技術ログ、Firestoreの aiRuns は「誰が何を実行したか」を追うための業務ログです。片方に寄せると、セキュリティレビューや障害調査で情報が足りなくなります。

まとめ

Vercel AI SDK 7は、Next.jsでAI機能を作るチームにとって、環境と設計を見直す良いタイミングです。

  • Node.js 22以上とESM importを前提にCIを整える
  • AI呼び出しをサービス関数へ分離し、Agent APIへ移行しやすくする
  • Firestoreに aiRuns を作り、業務監査ログを残す
  • 20〜30件のタスクは小さなWaveに分け、AIに依存関係とリスク整理を任せる
  • telemetry、timeout、Agent APIは、実行単位が整理されてから導入すると効果が出やすい

まずは既存のAI Route Handlerを1つ選び、Node.js 22のCI上でビルドし、Firestoreに実行台帳を残すところから始めるのがおすすめです。AI SDK 7の新機能は、その土台ができてから順に取り込むと、実装も運用も安定します。