はじめに
前回の記事の続きです。今回はTool化したものを実装していきます。
前回は、予定・天気・移動時間を文章で渡し、LLMに傘と出発時刻を判断させました。
Agentic AIを学びたい(Step1:OpenRouter経由でLLMをコールする)
今回は、天気の文章をPromptから外します。ユーザーは「今日19時に梅田へ行くんだけど、傘いる?」とだけ入力します。LLMが getWeather を呼び、返ってきた予報を見てから答えます。
getWeather の予報は、まだ固定のサンプルです。18:00から雨 を返します。見ているのは、Toolの定義、引数、結果をLLMへ戻す流れです。
呼び出しは2回です。1回目でToolを呼び、その結果を2回目に渡して回答をもらいます。繰り返しのループにはしていません。
成果物
シーケンス図
User
今日19時に梅田へ行くんだけど、傘いる?
↓
1回目のLLM
choices[0].message.tool_calls
getWeather({ location: "梅田", time: "19:00" })
↓
getWeather()
{ forecast: "18:00から雨" }
↓
2回目のLLM
choices[0].message.content
傘が必要です。
POST https://openrouter.ai/api/v1/chat/completions
1回目
messages[0] system 天気が必要なら getWeather を呼ぶ
messages[1] user 今日19時に梅田へ行くんだけど、傘いる?
tools getWeather(location, time)
2回目
messages[0] system 1回目と同じ
messages[1] user 1回目と同じ
messages[2] assistant tool_calls
messages[3] tool { forecast: "18:00から雨" }
tools 1回目と同じ
1回目の判断は choices[0].message.tool_calls、2回目の回答は choices[0].message.content に入っています。
ディレクトリ構成
Step1からファイル構成は変えていません。処理は src/index.ts に足しています。
agentic_ai_study/
├─ src/
│ └─ index.ts Tool定義、getWeather、2回の呼び出し
├─ .env OPENROUTER_API_KEY と MODEL_NAME
├─ .env.example
├─ package.json npm start
└─ tsconfig.json
npm start
└─ tsx src/index.ts
コード
.env
OPENROUTER_API_KEY=xxx
MODEL_NAME=openai/gpt-4o-mini
src/index.ts
import "dotenv/config";
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
const USER_PROMPT = "今日19時に梅田へ行くんだけど、傘いる?";
const SYSTEM_PROMPT = `あなたは外出前の準備を手伝うアシスタントです。
天気が答えに必要なら、必ず getWeather を呼び出してください。
天気を推測してはいけません。
getWeather の結果が返ってから、傘が必要かを日本語で短く答えてください。`;
const tools = [
{
type: "function",
function: {
name: "getWeather",
description: "指定した場所と時刻の天気予報を取得する",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "場所。例: 梅田",
},
time: {
type: "string",
description: "確認したい時刻。例: 19:00",
},
},
required: ["location", "time"],
},
},
},
] as const;
type WeatherArgs = {
location: string;
time: string;
};
type WeatherResult = WeatherArgs & {
forecast: string;
};
type ToolCall = {
id: string;
type: "function";
function: {
name: string;
arguments: string;
};
};
type ChatMessage =
| { role: "system" | "user"; content: string }
| { role: "assistant"; content: string | null; tool_calls?: ToolCall[] }
| { role: "tool"; tool_call_id: string; name: string; content: string };
type AssistantReply = {
role?: string;
content?: string | null;
tool_calls?: ToolCall[];
};
type ChatResponse = {
choices?: Array<{
message?: AssistantReply;
}>;
};
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`${name} を .env に設定してください`);
}
return value;
}
function getWeather(args: WeatherArgs): WeatherResult {
return {
location: args.location,
time: args.time,
forecast: "18:00から雨",
};
}
function parseWeatherArgs(raw: string): WeatherArgs {
const parsed: unknown = JSON.parse(raw);
if (
typeof parsed !== "object" ||
parsed === null ||
typeof (parsed as WeatherArgs).location !== "string" ||
typeof (parsed as WeatherArgs).time !== "string"
) {
throw new Error(`getWeather の引数が想定と違います: ${raw}`);
}
return parsed as WeatherArgs;
}
async function callOpenRouter(messages: ChatMessage[]): Promise<ChatResponse> {
const response = await fetch(OPENROUTER_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${requireEnv("OPENROUTER_API_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: requireEnv("MODEL_NAME"),
messages,
tools,
provider: { require_parameters: true },
}),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`OpenRouter API error ${response.status}: ${body}`);
}
return (await response.json()) as ChatResponse;
}
function readMessage(response: ChatResponse): AssistantReply {
const message = response.choices?.[0]?.message;
if (!message) {
throw new Error("応答に choices[0].message がありません");
}
return message;
}
async function main(): 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 },
];
const first = readMessage(await callOpenRouter(messages));
const toolCalls = first.tool_calls ?? [];
console.log("\n--- 1回目のLLM ---");
console.log("取り出し元: choices[0].message.tool_calls\n");
if (toolCalls.length === 0) {
console.log("Tool は呼ばれませんでした。");
console.log(first.content ?? "");
return;
}
messages.push({
role: "assistant",
content: first.content ?? null,
tool_calls: toolCalls,
});
for (const call of toolCalls) {
const args = parseWeatherArgs(call.function.arguments);
if (call.function.name !== "getWeather") {
throw new Error(`未定義の Tool です: ${call.function.name}`);
}
const result = getWeather(args);
console.log(`${call.function.name}(${JSON.stringify(args)})`);
messages.push({
role: "tool",
tool_call_id: call.id,
name: call.function.name,
content: JSON.stringify(result),
});
console.log("\n--- Tool Result ---");
console.log(JSON.stringify(result, null, 2));
}
const second = readMessage(await callOpenRouter(messages));
if (!second.content) {
throw new Error("2回目の応答に choices[0].message.content がありません");
}
console.log("\n--- 回答 ---");
console.log("取り出し元: choices[0].message.content\n");
console.log(second.content);
}
main().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
console.error(message);
process.exitCode = 1;
});
