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を学びたい(Step2:LLMにgetWeatherを呼ばせる)

0
Posted at

はじめに

前回の記事の続きです。今回はTool化したものを実装していきます。

前回は、予定・天気・移動時間を文章で渡し、LLMに傘と出発時刻を判断させました。
Agentic AIを学びたい(Step1:OpenRouter経由でLLMをコールする)

今回は、天気の文章をPromptから外します。ユーザーは「今日19時に梅田へ行くんだけど、傘いる?」とだけ入力します。LLMが getWeather を呼び、返ってきた予報を見てから答えます。

getWeather の予報は、まだ固定のサンプルです。18:00から雨 を返します。見ているのは、Toolの定義、引数、結果をLLMへ戻す流れです。

呼び出しは2回です。1回目でToolを呼び、その結果を2回目に渡して回答をもらいます。繰り返しのループにはしていません。

成果物

実行結果.png

シーケンス図

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;
});
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?