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 – チャットボット・ツールの使用方法
useChat と streamText の使用により、チャットボット・アプリケーションでツールが使用できます。AI SDK はこのコンテキストで 3 種類のツールをサポートしています :
- 自動的に実行されるサーバ側ツール
- 自動的に実行されるクライアント側ツール
- 確認ダイアログのようなユーザインタラクションを必要とするツール
フローは以下のとおりです :
- ユーザはチャット UI にメッセージを入力します。
- メッセージは API route に送信されます。
- サーバ側 route では、streamText 呼び出しの間に言語モデルがツール呼び出しを生成します。
- すべてのツール呼び出しはクライアントに転送されます。
- サーバ側ツールは execute メソッドを使用して実行され、その結果はクライアントに転送されます。
- 自動的に実行されるクライアント側ツールは、onToolCall コールバックで処理されます。ツールの結果を提供するには、addToolOutput を呼び出す必要があります。
- ユーザインタラクションを必要とするクライアント側ツールは UI に表示できます。ツール呼び出しと結果は、最後のアシスタントメッセージの parts プロパティのツール呼び出しパーツとして利用できます。
- ユーザインタラクションが完了したら、addToolOutput を使用してツールの結果をチャットに追加できます。
- 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 つあります :
- onToolCall コールバックは、自動的に実行される必要があるクライアント側ツールを処理するために使用されます。この例では、getLocation ツールはランダムな都市を返すクライアント側ツールです。(潜在的なデッドロックを回避するために await なしで) addToolOutput を呼び出して結果を提供できます。
- lastAssistantMessageIsCompleteWithToolCalls ヘルパーによる sendAutomaticallyWhen オプションは、すべてのツールの結果が利用可能になるときに自動的に送信します。
- アシスタントメッセージの 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();
}
以上