はじめに
前回の記事の続きです。今回は移動時間の Tool を足し、出発時刻まで判断させます。
前回は getSchedule と getWeather を順に呼び、傘が必要だと答えました。移動時間が無かったので、家を出る時刻は出していません。
Agentic AIを学びたい(Step3:予定と天気のToolを使い分ける)
今回も入力は「今日どう動けばいい?」だけです。予定が「19:00 梅田で食事」だったので、天気の次に getRoute を呼びます。自宅から梅田は 30 分です。19:00 から 30 分を引き、さらに 10 分早く出るので、18:20 になります。
getRoute の所要時間も、まだ固定のサンプルです。選んでいるのは LLM、実行しているのは execute.ts です。
呼び出しは 4 回です。1 回目が予定、2 回目が天気、3 回目が移動、4 回目が回答です。
成果物
シーケンス図
User
今日どう動けばいい?
↓
1回目 getSchedule({ date: "今日" })
19:00 梅田で食事
↓
2回目 getWeather({ location: "梅田", time: "19:00" })
18:00から雨
↓
3回目 getRoute({ origin: "自宅", destination: "梅田" })
30分
↓
4回目 choices[0].message.content
傘を持って、18:20に出る
tools
getSchedule(date)
getWeather(location, time)
getRoute(origin, destination)
1回目 tool_calls getSchedule
2回目 tool_calls getWeather
3回目 tool_calls getRoute
4回目 content 回答
ディレクトリ構成
増えたのは src/tools/route.ts です。
agentic_ai_study/
├─ src/
│ ├─ index.ts
│ ├─ env.ts
│ ├─ agent/
│ │ ├─ prompts.ts 移動と出発時刻の指示を追加
│ │ └─ runtime.ts 聞き直しの上限を5回に変更
│ ├─ llm/
│ │ ├─ openrouter.ts
│ │ └─ types.ts
│ └─ tools/
│ ├─ schedule.ts
│ ├─ weather.ts
│ ├─ route.ts getRoute の説明と実装
│ └─ execute.ts getRoute を実行先に追加
├─ .env
├─ package.json
└─ tsconfig.json
npm start
└─ tsx src/index.ts
└─ agent/runtime.ts
コード
前回から変わったファイルだけ載せます。
src/agent/prompts.ts
export const USER_PROMPT = "今日どう動けばいい?";
export const SYSTEM_PROMPT = `あなたは外出前の準備を手伝うアシスタントです。
まず予定を getSchedule で確認してください。
予定に外出先と時刻がある場合は、次に getWeather でその場所と時刻の天気を確認してください。
出発地は自宅です。目的地が分かったら getRoute で移動時間を確認してください。
一度の応答では Tool を1つだけ呼んでください。
予定も天気も移動時間も、Tool の結果が出るまで推測してはいけません。
結果が揃ったら、予定、傘の要否、家を出る時刻を日本語で短く答えてください。
出発時刻は、予定の時刻から移動時間を引き、さらに10分早く出てください。`;
src/tools/route.ts
import type { ToolDefinition } from "../llm/types.js";
export type RouteArgs = {
origin: string;
destination: string;
};
export type RouteResult = RouteArgs & {
duration: string;
};
export const routeTool: ToolDefinition = {
type: "function",
function: {
name: "getRoute",
description: "出発地から目的地までの移動時間を取得する",
parameters: {
type: "object",
properties: {
origin: {
type: "string",
description: "出発地。例: 自宅",
},
destination: {
type: "string",
description: "目的地。例: 梅田",
},
},
required: ["origin", "destination"],
},
},
};
export function getRoute(args: RouteArgs): RouteResult {
return {
origin: args.origin,
destination: args.destination,
duration: "30分",
};
}
src/tools/execute.ts
import type { ToolCall, ToolDefinition } from "../llm/types.js";
import { getRoute, routeTool, type RouteArgs } from "./route.js";
import { getSchedule, scheduleTool, type ScheduleArgs } from "./schedule.js";
import { getWeather, weatherTool, type WeatherArgs } from "./weather.js";
export const tools: ToolDefinition[] = [scheduleTool, weatherTool, routeTool];
function parseArgs<T extends object>(raw: string, keys: Array<keyof T>): T {
const parsed: unknown = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null) {
throw new Error(`Tool の引数が想定と違います: ${raw}`);
}
for (const key of keys) {
if (typeof (parsed as T)[key] !== "string") {
throw new Error(`Tool の引数が想定と違います: ${raw}`);
}
}
return parsed as T;
}
export function executeTool(call: ToolCall): unknown {
switch (call.function.name) {
case "getSchedule":
return getSchedule(parseArgs<ScheduleArgs>(call.function.arguments, ["date"]));
case "getWeather":
return getWeather(
parseArgs<WeatherArgs>(call.function.arguments, ["location", "time"]),
);
case "getRoute":
return getRoute(
parseArgs<RouteArgs>(call.function.arguments, ["origin", "destination"]),
);
default:
throw new Error(`未定義の Tool です: ${call.function.name}`);
}
}
src/agent/runtime.ts
聞き直しの上限を 5 回にしています。予定、天気、移動、回答の 4 回が入るようにするためです。
import { requireEnv } from "../env.js";
import { callOpenRouter } from "../llm/openrouter.js";
import type { AssistantReply, ChatMessage, ChatResponse } from "../llm/types.js";
import { executeTool, tools } from "../tools/execute.js";
import { SYSTEM_PROMPT, USER_PROMPT } from "./prompts.js";
function readMessage(response: ChatResponse): AssistantReply {
const message = response.choices?.[0]?.message;
if (!message) {
throw new Error("応答に choices[0].message がありません");
}
return message;
}
export async function run(): Promise<void> {
console.log("モデル:", requireEnv("MODEL_NAME"));
console.log("\n--- User Prompt ---\n");
console.log(USER_PROMPT);
const messages: ChatMessage[] = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: USER_PROMPT },
];
for (let turn = 1; turn <= 5; turn += 1) {
const message = readMessage(await callOpenRouter(messages, tools));
console.log("\n--- LLM Response ---");
console.log(JSON.stringify(message, null, 2));
console.log("\n--- Tool Calls ---");
const toolCalls = message.tool_calls ?? [];
console.log(JSON.stringify(toolCalls, null, 2));
if (toolCalls.length === 0) {
if (!message.content) {
throw new Error("応答に choices[0].message.content がありません");
}
console.log("\n--- 回答 ---");
console.log("取り出し元: choices[0].message.content\n");
console.log(message.content);
return;
}
console.log(`\n--- ${turn}回目のLLM ---`);
console.log("取り出し元: choices[0].message.tool_calls\n");
messages.push({
role: "assistant",
content: message.content ?? null,
tool_calls: toolCalls,
});
for (const call of toolCalls) {
const args = JSON.parse(call.function.arguments) as unknown;
const result = executeTool(call);
console.log(`${call.function.name}(${JSON.stringify(args)})`);
console.log("\n--- Tool Result ---");
console.log(JSON.stringify(result, null, 2));
messages.push({
role: "tool",
tool_call_id: call.id,
name: call.function.name,
content: JSON.stringify(result),
});
}
}
throw new Error("Tool 呼び出しが5回を超えたため終了しました");
}



