0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Agentic AIを学びたい(Step1:OpenRouter経由でLLMをコールする)

0
Posted at

はじめに

Agentic AIを作ってみたく、実装を開始していきます。今回は初回なのでOpenRouter経由でAIを呼び、その結果を元にjsonに構造化するまでを実施してみます。

予定・天気・移動時間は、こちらで文章にして渡しています。AIが自分で取りにいくところまでは、まだやっていません。呼び出しは2回ですが、1回目の回答を2回目には渡していません。それぞれ1往復で終わっています。

実装したいこと

予定や天気、移動の情報と質問を渡し、その結果を元に傘と出発時刻を判断させる。

今日の予定: 19:00 梅田で食事
天気: 18:00から雨
自宅から梅田: 電車で30分

質問: 傘が必要か、何時に家を出ればいいか

同じ内容を2回送ります。1回目は文章、2回目は次のJSONです。

needUmbrella  傘が必要か
leaveAt       出発時刻
reason        理由

成果物

実行結果.jpg

シーケンス図

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
  ↓
回答
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?