はじめに
Agentic AIを作ってみたく、実装を開始していきます。今回は初回なのでOpenRouter経由でAIを呼び、その結果を元にjsonに構造化するまでを実施してみます。
予定・天気・移動時間は、こちらで文章にして渡しています。AIが自分で取りにいくところまでは、まだやっていません。呼び出しは2回ですが、1回目の回答を2回目には渡していません。それぞれ1往復で終わっています。
実装したいこと
予定や天気、移動の情報と質問を渡し、その結果を元に傘と出発時刻を判断させる。
今日の予定: 19:00 梅田で食事
天気: 18:00から雨
自宅から梅田: 電車で30分
質問: 傘が必要か、何時に家を出ればいいか
同じ内容を2回送ります。1回目は文章、2回目は次のJSONです。
needUmbrella 傘が必要か
leaveAt 出発時刻
reason 理由
成果物
シーケンス図
POST https://openrouter.ai/api/v1/chat/completions
1回目
messages[0] system 日本語で短く提案する
messages[1] user 予定・天気・移動・質問
2回目
messages[0] system 傘と出発時刻を判断する
messages[1] user 1回目と同じ
response_format needUmbrella / leaveAt / reason
回答はどちらも choices[0].message.content に入っています。2回目だけ、その文字列をJSONとして読んでいます。
ディレクトリ構成
agentic_ai_study/
├─ src/
│ └─ index.ts 問い合わせと表示
├─ .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:00 梅田で食事
天気:
18:00から雨
自宅から梅田:
電車で30分
質問:
傘が必要か、何時に家を出ればいいか教えて。`;
const TEXT_SYSTEM_PROMPT = `あなたは外出前の準備を短く提案するアシスタントです。
渡された予定、天気、移動時間だけを根拠に、傘が必要かと家を出る時刻を日本語で答えてください。
2文程度で、理由が分かるように書いてください。`;
const JSON_SYSTEM_PROMPT = `あなたは外出前の準備を判断するアシスタントです。
渡された予定、天気、移動時間だけを根拠に、傘の要否と家を出る時刻を判断してください。`;
const adviceResponseFormat = {
type: "json_schema",
json_schema: {
name: "departure_advice",
strict: true,
schema: {
type: "object",
properties: {
needUmbrella: {
type: "boolean",
description: "傘が必要なら true",
},
leaveAt: {
type: "string",
description: "家を出る時刻の目安。例: 18:20",
},
reason: {
type: "string",
description: "そう判断した理由",
},
},
required: ["needUmbrella", "leaveAt", "reason"],
additionalProperties: false,
},
},
} as const;
type Advice = {
needUmbrella: boolean;
leaveAt: string;
reason: string;
};
type ChatMessage = {
role: "system" | "user";
content: string;
};
type ChatResponse = {
choices?: Array<{
message?: {
role?: string;
content?: string | null;
};
}>;
};
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`${name} を .env に設定してください`);
}
return value;
}
async function callOpenRouter(
messages: ChatMessage[],
responseFormat?: typeof adviceResponseFormat,
): 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,
...(responseFormat
? {
response_format: responseFormat,
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 readAnswer(response: ChatResponse): string {
const content = response.choices?.[0]?.message?.content;
if (!content) {
throw new Error("応答に choices[0].message.content がありません");
}
return content;
}
function parseAdvice(content: string): Advice {
const parsed: unknown = JSON.parse(content);
if (
typeof parsed !== "object" ||
parsed === null ||
typeof (parsed as Advice).needUmbrella !== "boolean" ||
typeof (parsed as Advice).leaveAt !== "string" ||
typeof (parsed as Advice).reason !== "string"
) {
throw new Error(`Structured Output の形が想定と違います: ${content}`);
}
return parsed as Advice;
}
async function main(): Promise<void> {
console.log("モデル:", requireEnv("MODEL_NAME"));
console.log("\n--- User Prompt ---\n");
console.log(USER_PROMPT);
const textResponse = await callOpenRouter([
{ role: "system", content: TEXT_SYSTEM_PROMPT },
{ role: "user", content: USER_PROMPT },
]);
console.log("\n--- 自由文の回答 ---");
console.log("取り出し元: choices[0].message.content\n");
console.log(readAnswer(textResponse));
const jsonResponse = await callOpenRouter(
[
{ role: "system", content: JSON_SYSTEM_PROMPT },
{ role: "user", content: USER_PROMPT },
],
adviceResponseFormat,
);
const advice = parseAdvice(readAnswer(jsonResponse));
console.log("\n--- Structured Output ---");
console.log("取り出し元: choices[0].message.content を JSON として解釈\n");
console.log(JSON.stringify(advice, null, 2));
}
main().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
console.error(message);
process.exitCode = 1;
});
次回やる事
天気を Prompt に書くのをやめ、getWeather() として Tool にします。
ユーザーは「今日19時に梅田へ行くんだけど、傘いる?」とだけ入力します。LLM が天気を確認する必要があると判断し、getWeather() を呼びます。
User
↓
LLM
↓
getWeather()
↓
結果
↓
LLM
↓
回答
