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を学びたい(Step4:getRouteで出発時刻を決める)

0
Posted at

はじめに

前回の記事の続きです。今回は移動時間の 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 回目が回答です。

成果物

image.png

image.png

image.png

image.png

シーケンス図

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回を超えたため終了しました");
}
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?