Vercel AI SDK 6 : AI SDK UI – チャットボット・ツールの使用方法

useChat と streamText の使用により、チャットボット・アプリケーションでツールが使用できます。AI SDK はこのコンテキストで 3 種類のツールをサポートしています。

Vercel AI SDK 6 : AI SDK UI – チャットボット・ツールの使用方法

作成 : Masashi Okumura (@classcat.com)
作成日時 : 02/20/2026
バージョン : ai@6.0.91

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

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

 

 

Vercel AI SDK 6.x : AI SDK UI – チャットボット・ツールの使用方法

useChatstreamText の使用により、チャットボット・アプリケーションでツールが使用できます。AI SDK はこのコンテキストで 3 種類のツールをサポートしています :

  1. 自動的に実行されるサーバ側ツール

  2. 自動的に実行されるクライアント側ツール

  3. 確認ダイアログのようなユーザインタラクションを必要とするツール

フローは以下のとおりです :

  1. ユーザはチャット UI にメッセージを入力します。

  2. メッセージは API route に送信されます。

  3. サーバ側 route では、streamText 呼び出しの間に言語モデルがツール呼び出しを生成します。

  4. すべてのツール呼び出しはクライアントに転送されます。

  5. サーバ側ツールは execute メソッドを使用して実行され、その結果はクライアントに転送されます。

  6. 自動的に実行されるクライアント側ツールは、onToolCall コールバックで処理されます。ツールの結果を提供するには、addToolOutput を呼び出す必要があります。

  7. ユーザインタラクションを必要とするクライアント側ツールは UI に表示できます。ツール呼び出しと結果は、最後のアシスタントメッセージの parts プロパティのツール呼び出しパーツとして利用できます。

  8. ユーザインタラクションが完了したら、addToolOutput を使用してツールの結果をチャットに追加できます。

  9. sendAutomaticallyWhen を使用して、すべてのツールの結果が利用可能になったとき、チャットが自動的に送信されるように設定できます。これはフローの別の反復処理をトリガーします。

ツール呼び出しとツール実行は型付きツールパーツとしてアシスタントメッセージに統合されます。ツールパーツは最初のツール呼び出しにあり、それからツールが実行されればそれはツールの結果になります。ツール結果は、ツール呼び出しに関するすべての情報とツール実行の結果を含みます。

 

この例では、3 つのツールを使用します :

  • getWeatherInformation: 指定された都市の天気を返す、自動的に実行されるサーバ側ツール。

  • askForConfirmation: ユーザに確認を求める、ユーザインタラクションのクライアント側ツール。

  • getLocation: ランダムな都市を返す、自動的に実行されるクライアント側ツール。

 

API ルート

Provider app/api/chat/route.ts

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

// Allow streaming responses up to 30 seconds
export const maxDuration = 30;

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: {
      // server-side tool with execute function:
      getWeatherInformation: {
        description: 'show the weather in a given city to the user',
        inputSchema: z.object({ city: z.string() }),
        execute: async ({}: { city: string }) => {
          const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy'];
          return weatherOptions[
            Math.floor(Math.random() * weatherOptions.length)
          ];
        },
      },
      // client-side tool that starts user interaction:
      askForConfirmation: {
        description: 'Ask the user for confirmation.',
        inputSchema: z.object({
          message: z.string().describe('The message to ask for confirmation.'),
        }),
      },
      // client-side tool that is automatically executed on the client:
      getLocation: {
        description:
          'Get the user location. Always ask for confirmation before using this tool.',
        inputSchema: z.object({}),
      },
    },
  });

  return result.toUIMessageStreamResponse();
}

 

クライアント側ページ

クライアント側ページは useChat フックを使用して、リアルタイムのメッセージストリーミングを備えたチャットボット・アプリケーションを作成します。ツール呼び出しは、チャット UI で型付きツールパーツとして表示されます。必ず、メッセージの parts プロパティを使用してメッセージをレンダリングしてください。

言及すべき点が 3 つあります :

  1. onToolCall コールバックは、自動的に実行される必要があるクライアント側ツールを処理するために使用されます。この例では、getLocation ツールはランダムな都市を返すクライアント側ツールです。(潜在的なデッドロックを回避するために await なしで) addToolOutput を呼び出して結果を提供できます。

  2. lastAssistantMessageIsCompleteWithToolCalls ヘルパーによる sendAutomaticallyWhen オプションは、すべてのツールの結果が利用可能になるときに自動的に送信します。

  3. アシスタントメッセージの parts 配列は、tool-askForConfirmation のような型付きの名前を持つツールパーツを含みます。クライアント側ツール askForConfirmation は UI に表示されます。それはユーザに確認を求め、ユーザが実行を承認するか拒否すると、結果を表示します。結果は、型安全性のために tool パラメータを含む addToolOutput を使用してチャットに追加されます。

app/page.tsx

'use client';

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

export default function Chat() {
  const { messages, sendMessage, addToolOutput } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),

    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,

    // run client-side tools that are automatically executed:
    async onToolCall({ toolCall }) {
      // Check if it's a dynamic tool first for proper type narrowing
      if (toolCall.dynamic) {
        return;
      }

      if (toolCall.toolName === 'getLocation') {
        const cities = ['New York', 'Los Angeles', 'Chicago', 'San Francisco'];

        // No await - avoids potential deadlocks
        addToolOutput({
          tool: 'getLocation',
          toolCallId: toolCall.toolCallId,
          output: cities[Math.floor(Math.random() * cities.length)],
        });
      }
    },
  });
  const [input, setInput] = useState('');

  return (
    <>
      {messages?.map(message => (
        <div key={message.id}>
          <strong>{`${message.role}: `}</strong>
          {message.parts.map(part => {
            switch (part.type) {
              // render text parts as simple text:
              case 'text':
                return part.text;

              // for tool parts, use the typed tool part names:
              case 'tool-askForConfirmation': {
                const callId = part.toolCallId;

                switch (part.state) {
                  case 'input-streaming':
                    return (
                      <div key={callId}>Loading confirmation request...</div>
                    );
                  case 'input-available':
                    return (
                      <div key={callId}>
                        {part.input.message}
                        <div>
                          <button
                            onClick={() =>
                              addToolOutput({
                                tool: 'askForConfirmation',
                                toolCallId: callId,
                                output: 'Yes, confirmed.',
                              })
                            }
                          >
                            Yes
                          </button>
                          <button
                            onClick={() =>
                              addToolOutput({
                                tool: 'askForConfirmation',
                                toolCallId: callId,
                                output: 'No, denied',
                              })
                            }
                          >
                            No
                          </button>
                        </div>
                      </div>
                    );
                  case 'output-available':
                    return (
                      <div key={callId}>
                        Location access allowed: {part.output}
                      </div>
                    );
                  case 'output-error':
                    return <div key={callId}>Error: {part.errorText}</div>;
                }
                break;
              }

              case 'tool-getLocation': {
                const callId = part.toolCallId;

                switch (part.state) {
                  case 'input-streaming':
                    return (
                      <div key={callId}>Preparing location request...</div>
                    );
                  case 'input-available':
                    return <div key={callId}>Getting location...</div>;
                  case 'output-available':
                    return <div key={callId}>Location: {part.output}</div>;
                  case 'output-error':
                    return (
                      <div key={callId}>
                        Error getting location: {part.errorText}
                      </div>
                    );
                }
                break;
              }

              case 'tool-getWeatherInformation': {
                const callId = part.toolCallId;

                switch (part.state) {
                  // example of pre-rendering streaming tool inputs:
                  case 'input-streaming':
                    return (
                      <pre key={callId}>{JSON.stringify(part, null, 2)}</pre>
                    );
                  case 'input-available':
                    return (
                      <div key={callId}>
                        Getting weather information for {part.input.city}...
                      </div>
                    );
                  case 'output-available':
                    return (
                      <div key={callId}>
                        Weather in {part.input.city}: {part.output}
                      </div>
                    );
                  case 'output-error':
                    return (
                      <div key={callId}>
                        Error getting weather for {part.input.city}:{' '}
                        {part.errorText}
                      </div>
                    );
                }
                break;
              }
            }
          })}
          <br />
        </div>
      ))}

      <form
        onSubmit={e => {
          e.preventDefault();
          if (input.trim()) {
            sendMessage({ text: input });
            setInput('');
          }
        }}
      >
        <input value={input} onChange={e => setInput(e.target.value)} />
      </form>
    </>
  );
}

 

エラー処理

クライアント側ツール実行中にエラーが発生する場合があります。エラーを記録する output ではなく、output-error を指定した state と errorText 値による addToolOutput メソッドを使用します。

app/page.tsx

'use client';

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

export default function Chat() {
  const { messages, sendMessage, addToolOutput } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),

    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,

    // run client-side tools that are automatically executed:
    async onToolCall({ toolCall }) {
      // Check if it's a dynamic tool first for proper type narrowing
      if (toolCall.dynamic) {
        return;
      }

      if (toolCall.toolName === 'getWeatherInformation') {
        try {
          const weather = await getWeatherInformation(toolCall.input);

          // No await - avoids potential deadlocks
          addToolOutput({
            tool: 'getWeatherInformation',
            toolCallId: toolCall.toolCallId,
            output: weather,
          });
        } catch (err) {
          addToolOutput({
            tool: 'getWeatherInformation',
            toolCallId: toolCall.toolCallId,
            state: 'output-error',
            errorText: 'Unable to get the weather information',
          });
        }
      }
    },
  });
}

 

ツール実行の承認

ツール実行の承認は、サーバ側ツールを実行する前に、ユーザの確認を求めることができます。ブラウザで実行される クライアント側ツール とは異なり、承認を伴うツールは依然としてサーバで実行されますが、ユーザ承認後となります。

以下を望む場合、ツール実行の承認を使用します :

  • 機密性の高い操作 (支払い、削除、外部 API 呼び出し) を確認する

  • 実行前にユーザにツール入力をレビューさせる

  • 人間による監視を自動化されたワークフローに追加する

ブラウザで実行する必要があるツール (UI 状態の更新、ブラウザ API へのアクセス) については、代わりにクライアント側ツールを使用してください。

 

サーバのセットアップ

ツールに needsApproval を設定することで承認を有効にします。入力に基づく動的な承認を含む、設定オプションについては Tool Execution Approval をご覧ください。

Provider app/api/chat/route.ts

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

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

  const result = streamText({
    model: openai("gpt-4o-mini"),
    messages,
    tools: {
      getWeather: tool({
        description: 'Get the weather in a location',
        inputSchema: z.object({
          city: z.string(),
        }),
        needsApproval: true,
        execute: async ({ city }) => {
          const weather = await fetchWeather(city);
          return weather;
        },
      }),
    },
  });

  return result.toUIMessageStreamResponse();
}

 

クライアント側の承認 UI

ツールが承認を必要とする場合、ツールパーツの状態は approval-requested になります。addToolApprovalResponse を使用して、承認または拒否します :

app/page.tsx

'use client';

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

export default function Chat() {
  const { messages, addToolApprovalResponse } = useChat();

  return (
    <>
      {messages.map(message => (
        <div key={message.id}>
          {message.parts.map(part => {
            if (part.type === 'tool-getWeather') {
              switch (part.state) {
                case 'approval-requested':
                  return (
                    <div key={part.toolCallId}>
                      <p>Get weather for {part.input.city}?</p>
                      <button
                        onClick={() =>
                          addToolApprovalResponse({
                            id: part.approval.id,
                            approved: true,
                          })
                        }
                      >
                        Approve
                      </button>
                      <button
                        onClick={() =>
                          addToolApprovalResponse({
                            id: part.approval.id,
                            approved: false,
                          })
                        }
                      >
                        Deny
                      </button>
                    </div>
                  );
                case 'output-available':
                  return (
                    <div key={part.toolCallId}>
                      Weather in {part.input.city}: {part.output}
                    </div>
                  );
              }
            }
            // Handle other part types...
          })}
        </div>
      ))}
    </>
  );
}

 

承認後の自動送信

lastAssistantMessageIsCompleteWithApprovalResponses を使用して、承認後に会話を自動的に継続します :

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

const { messages, addToolApprovalResponse } = useChat({
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
});

 

動的ツール

動的ツール (コンパイル時に不明な型のツール) を使用する場合、UI パーツは特定のツール型ではなく、汎用的な dynamic-tool 型を使用します :

app/page.tsx

{
  message.parts.map((part, index) => {
    switch (part.type) {
      // Static tools with specific (`tool-${toolName}`) types
      case 'tool-getWeatherInformation':
        return <WeatherDisplay part={part} />;

      // Dynamic tools use generic `dynamic-tool` type
      case 'dynamic-tool':
        return (
          <div key={index}>
            <h4>Tool: {part.toolName}</h4>
            {part.state === 'input-streaming' && (
              <pre>{JSON.stringify(part.input, null, 2)}</pre>
            )}
            {part.state === 'output-available' && (
              <pre>{JSON.stringify(part.output, null, 2)}</pre>
            )}
            {part.state === 'output-error' && (
              <div>Error: {part.errorText}</div>
            )}
          </div>
        );
    }
  });
}

動的ツールは、以下と統合する場合に有用です :

  • スキーマのない MCP (Model Context Protocol) ツール

  • 実行時にロードされるユーザ定義関数

  • 外部ツールプロバイダー

 

ツール呼び出しストリーミング

AI SDK 5.0 で、ツール呼び出しストリーミングがデフォルトで有効になり、ツール呼び出しが生成されると同時にそれらをストリーミングすることを可能にします。これは、ツール入力をリアルタイムに生成されると同時に表示することで、より良いユーザエクスペリエンスを提供します。

app/api/chat/route.ts

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),
    // toolCallStreaming is enabled by default in v5
    // ...
  });

  return result.toUIMessageStreamResponse();
}

ツール呼び出しストリーミングが有効にされると、部分的なツール呼び出しがデータストリームの一部としてストリーミングされます。それらは useChat フックを通して利用可能です。アシスタントメッセージの型付きツールパーツもまた部分的なツール呼び出しを含みます。ツールパーツの state プロパティを使用して正しい UI をレンダリングできます。

app/page.tsx

export default function Chat() {
  // ...
  return (
    <>
      {messages?.map(message => (
        <div key={message.id}>
          {message.parts.map(part => {
            switch (part.type) {
              case 'tool-askForConfirmation':
              case 'tool-getLocation':
              case 'tool-getWeatherInformation':
                switch (part.state) {
                  case 'input-streaming':
                    return <pre>{JSON.stringify(part.input, null, 2)}</pre>;
                  case 'input-available':
                    return <pre>{JSON.stringify(part.input, null, 2)}</pre>;
                  case 'output-available':
                    return <pre>{JSON.stringify(part.output, null, 2)}</pre>;
                  case 'output-error':
                    return <div>Error: {part.errorText}</div>;
                }
            }
          })}
        </div>
      ))}
    
  );
}

 

サーバ側マルチステップ呼び出し

streamText を使用して、サーバ側でマルチステップ呼び出しを使用することもできます。これは、呼び出されるツールのすべてがサーバ側で execute 関数を持つときに機能します。

Provider app/api/chat/route.ts

import { convertToModelMessages, streamText, UIMessage, 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),
    tools: {
      getWeatherInformation: {
        description: 'show the weather in a given city to the user',
        inputSchema: z.object({ city: z.string() }),
        // tool has execute function:
        execute: async ({}: { city: string }) => {
          const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy'];
          return weatherOptions[
            Math.floor(Math.random() * weatherOptions.length)
          ];
        },
      },
    },
    stopWhen: stepCountIs(5),
  });

  return result.toUIMessageStreamResponse();
}

 

以上