Vercel AI SDK 6.x : Next.js App Router (2) ツールによる強化

大規模言語モデル (LLM) が驚異的な生成能力を持つ一方で、離散的なタスクや外界とのやり取りには苦労します。ここでツールの出番になります。ツールは LLM が呼び出せるアクションです。

Vercel AI SDK 6.x : Getting Started – Next.js App Router (2) ツールによる強化

作成 : Masashi Okumura (@classcat.com)
作成日時 : 01/07/2026
バージョン : ai@6.0.14

* 本記事は ai-sdk.dev/docs の以下のページを参考にしています :

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

 

Vercel AI SDK 6.x : Getting Started – Next.js App Router (2) ツールによる強化

ツールでチャットボットを強化

大規模言語モデル (LLM) が驚異的な生成能力を持つ一方で、離散的なタスク (e.g. 数学) や外界とのやり取り (e.g. 天気の取得) には苦労します。ここでツール の出番になります。

ツールは LLM が呼び出せるアクションです。これらのアクションの結果は LLM にレポートされて次のレスポンスで考慮されます。

例えば、ユーザが現在の天気について質問した場合、ツールがなければ、モデルはトレーニングデータに基づいて一般的な情報を提供できるだけです。しかし天気ツールを使用すれば、最新の、場所固有の天気情報を取得して提供することができます。

単純な天気ツールを追加することでチャットボットを拡張しましょう。

 

Route ハンドラの更新

app/api/chat/route.ts ファイルを変更して新しい天気ツールを含めます :

Gateway app/api/chat/route.ts

import { streamText, UIMessage, convertToModelMessages, tool } from 'ai';
import { z } from 'zod';

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

  const result = streamText({
    model: "openai/gpt-4o-mini",
    messages: await convertToModelMessages(messages),
    tools: {
      weather: tool({
        description: 'Get the weather in a location (fahrenheit)',
        inputSchema: z.object({
          location: z.string().describe('The location to get the weather for'),
        }),
        execute: async ({ location }) => {
          const temperature = Math.round(Math.random() * (90 - 32) + 32);
          return {
            location,
            temperature,
          };
        },
      }),
    },
  });

  return result.toUIMessageStreamResponse();
}

Provider app/api/chat/route.ts

import { streamText, UIMessage, convertToModelMessages, tool } from 'ai';
import { openai } from "@ai-sdk/openai";
import { z } from 'zod';

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

  const result = streamText({
    model: openai("gpt-4o-mini"),
    messages: await convertToModelMessages(messages),
    tools: {
      weather: tool({
        description: 'Get the weather in a location (fahrenheit)',
        inputSchema: z.object({
          location: z.string().describe('The location to get the weather for'),
        }),
        execute: async ({ location }) => {
          const temperature = Math.round(Math.random() * (90 - 32) + 32);
          return {
            location,
            temperature,
          };
        },
      }),
    },
  });

  return result.toUIMessageStreamResponse();
}

この更新されたコードでは :

  1. ai パッケージから tool 関数を、スキーマ検証のために zod から z をインポートします。

  2. weather ツールで tools オブジェクトを定義します。このツールは :

    • モデルがいつそれを使用するか理解するのに役立つ説明を含みます。

    • Zod スキーマを使用して inputSchema を定義し、このツールを実行するには location 文字列が必要であることを指定します。モデルは会話のコンテキストからこの入力を抽出しようとします。抽出できない場合は不足している情報をユーザに質問します。

    • 天気データの取得をシミュレートする execute 関数を定義します (この場合はランダムな気温を返します)。これはサーバ上で実行される非同期関数なので、外部 API からリアルデータを取得できます。

これでチャットボットは、ユーザが質問した任意の場所に対する天気情報を「取得」できるようになります。モデルが天気ツールを使用する必要があると判断した場合、必要な入力でツール呼び出しを生成します。そして execute 関数は自動的に実行され、ツール出力は、tool メッセージとして messages に追加されます。

“What’s the weather in New York?” のように質問してみて、モデルが新しいツールをどのように使用するか確認してください。

UI で空のレスポンスに気づきましたか?これは、モデルがテキスト応答を生成する代わりに、ツール呼び出しを生成したからです。ツール呼び出しと続くツールの結果には、messages.parts 配列の tool-weather パートを通して、クライアントでアクセスできます。

 

UI の更新

UI でツールの呼び出しを表示するには、app/page.tsx ファイルを更新します :

app/page.tsx

'use client';

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

export default function Chat() {
  const [input, setInput] = useState('');
  const { messages, sendMessage } = useChat();
  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
      {messages.map(message => (
        <div key={message.id} className="whitespace-pre-wrap">
          {message.role === 'user' ? 'User: ' : 'AI: '}
          {message.parts.map((part, i) => {
            switch (part.type) {
              case 'text':
                return <div key={`${message.id}-${i}`}>{part.text}</div>;
              case 'tool-weather':
                return (
                  <pre key={`${message.id}-${i}`}>
                    {JSON.stringify(part, null, 2)}
                  </pre>
                );
            }
          })}
        </div>
      ))}

      <form
        onSubmit={e => {
          e.preventDefault();
          sendMessage({ text: input });
          setInput('');
        }}
      >
        <input
          className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
          value={input}
          placeholder="Say something..."
          onChange={e => setInput(e.currentTarget.value)}
        />
      </form>
    </div>
  );
}

出力例

User: 
東京の天気はどうですか?
AI: 
{
  "type": "tool-weather",
  "toolCallId": "call_1UzGuAuMWtRm3Lwu7Ta60ImS",
  "state": "output-available",
  "input": {
    "location": "Tokyo"
  },
  "output": {
    "location": "Tokyo",
    "temperature": 41
  },
  "callProviderMetadata": {
    "openai": {
      "itemId": "fc_017244d4fd21073c016959104e30448193bfa7aad906079d37"
    }
  }
}

この変更により、UI を更新して異なるメッセージ・パートの処理ができるようになります。テキストパートについては、以前のようにテキストの内容を表示します。天気ツールの呼び出しについては、ツール呼び出しとその結果の JSON 表現を表示します。

これで、天気について質問した場合、チャットインターフェイスに表示された、ツール呼び出しとその結果を確認できるでしょう。

 

マルチステップのツール呼び出しを可能にする

ツールがチャット・インターフェイスで表示されるようになった一方で、モデルは元の質問に答えるためにこの情報を使用していないことに気づいたかもしれません。これはモデルがツール呼び出しをひとたび生成すれば、技術的には生成を完了しているためです。

これを解決するために、stopWhen を使用してマルチステップ・ツール呼び出しを有効にできます。デフォルトでは、stopWhen は stepCountIs(1) に設定されています、これはツール結果がある場合には最初のステップの後で生成が停止することを意味しています。この条件を変更することで、モデルがツール結果を自動的に自身に送り返して、指定した停止条件が満たされるまで追加の生成をトリガーすることを可能にします。この場合、ユーザはモデルには生成を継続し、天気ツールの結果を使用して元の質問に答えることを望むでしょう。

 

Route ハンドラの更新

app/api/chat/route.ts ファイルを変更して stopWhen 条件を含めます :

Gateway app/api/chat/route.ts

import {
  streamText,
  UIMessage,
  convertToModelMessages,
  tool,
  stepCountIs,
} from 'ai';
import { z } from 'zod';

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

  const result = streamText({
    model: "openai/gpt-4o-mini",
    messages: await convertToModelMessages(messages),
    stopWhen: stepCountIs(5),
    tools: {
      weather: tool({
        description: 'Get the weather in a location (fahrenheit)',
        inputSchema: z.object({
          location: z.string().describe('The location to get the weather for'),
        }),
        execute: async ({ location }) => {
          const temperature = Math.round(Math.random() * (90 - 32) + 32);
          return {
            location,
            temperature,
          };
        },
      }),
    },
  });

  return result.toUIMessageStreamResponse();
}

Provider app/api/chat/route.ts

import {
  streamText,
  UIMessage,
  convertToModelMessages,
  tool,
  stepCountIs,
} from 'ai';
import { openai } from "@ai-sdk/openai";
import { z } from 'zod';

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

  const result = streamText({
    model: openai("gpt-4o-mini"),
    messages: await convertToModelMessages(messages),
    stopWhen: stepCountIs(5),
    tools: {
      weather: tool({
        description: 'Get the weather in a location (fahrenheit)',
        inputSchema: z.object({
          location: z.string().describe('The location to get the weather for'),
        }),
        execute: async ({ location }) => {
          const temperature = Math.round(Math.random() * (90 - 32) + 32);
          return {
            location,
            temperature,
          };
        },
      }),
    },
  });

  return result.toUIMessageStreamResponse();
}

この更新されたコードでは :

  1. stopWeb を、stepCountIs が 5 の場合に設定に設定すると、モデルが任意の指定された生成について 5 まで “steps” を使用することを可能にします。

  2. onStepFinish を追加してインタラクションの各ステップからの toolResults をログ記録し、モデルのツール使用を理解しやすくできます。これはつまり、前の例から toolCall と toolResult console.log ステートメントを削除することもできます。

ブラウザに戻り、適当な場所の天気について質問してください。モデルが天気ツールの結果を使用して質問に答えるのが確認できるはずです。

stopWhen: stepCountIs(5) を設定することで、モデルが任意の指定された生成について 5 まで “steps” を使用することを可能にします。これはより複雑なインタラクションを可能にし、モデルが必要に応じて数ステップにわたり情報を収集・処理することを可能にします。温度を摂氏から華氏に変換するもうひとつのツールを追加することで、これを実際に確認できます。

 

もうひとつのツールの追加

app/api/chat/route.ts ファイルを更新して、華氏から摂氏に温度を変換する新しいツールを追加します :

Gateway app/api/chat/route.ts

import {
  streamText,
  UIMessage,
  convertToModelMessages,
  tool,
  stepCountIs,
} from 'ai';
import { z } from 'zod';

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

  const result = streamText({
    model: "openai/gpt-4o-mini",
    messages: await convertToModelMessages(messages),
    stopWhen: stepCountIs(5),
    tools: {
      weather: tool({
        description: 'Get the weather in a location (fahrenheit)',
        inputSchema: z.object({
          location: z.string().describe('The location to get the weather for'),
        }),
        execute: async ({ location }) => {
          const temperature = Math.round(Math.random() * (90 - 32) + 32);
          return {
            location,
            temperature,
          };
        },
      }),
      convertFahrenheitToCelsius: tool({
        description: 'Convert a temperature in fahrenheit to celsius',
        inputSchema: z.object({
          temperature: z
            .number()
            .describe('The temperature in fahrenheit to convert'),
        }),
        execute: async ({ temperature }) => {
          const celsius = Math.round((temperature - 32) * (5 / 9));
          return {
            celsius,
          };
        },
      }),
    },
  });

  return result.toUIMessageStreamResponse();
}

 

フロントエンドの更新

app/page.tsx ファイルを更新して新しい温度変換ツールをレンダリングします :

app/page.tsx

'use client';

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

export default function Chat() {
  const [input, setInput] = useState('');
  const { messages, sendMessage } = useChat();
  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
      {messages.map(message => (
        <div key={message.id} className="whitespace-pre-wrap">
          {message.role === 'user' ? 'User: ' : 'AI: '}
          {message.parts.map((part, i) => {
            switch (part.type) {
              case 'text':
                return <div key={`${message.id}-${i}`}>{part.text}</div>;
              case 'tool-weather':
              case 'tool-convertFahrenheitToCelsius':
                return (
                  <pre key={`${message.id}-${i}`}>
                    {JSON.stringify(part, null, 2)}
                  </pre>
                );
            }
          })}
        </div>
      ))}

      <form
        onSubmit={e => {
          e.preventDefault();
          sendMessage({ text: input });
          setInput('');
        }}
      >
        <input
          className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
          value={input}
          placeholder="Say something..."
          onChange={e => setInput(e.currentTarget.value)}
        />
      </form>
    </div>
  );
}

 

以上