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を学びたい(Step5:Agent Loopで終わるまで繰り返す)

0
Posted at

はじめに

前回の記事の続きです。今回は、Tool の結果を戻して終わるまで繰り返す Agent Loop にします。

前回は getSchedule、getWeather、getRoute を順に呼び、18:20 に出ると答えました。繰り返しは、5 回までの for でした。
Agentic AIを学びたい(Step4:getRouteで出発時刻を決める)

今回変えたのは、回数を先に決めるのをやめたことです。while (true) で LLM を呼び、応答を2種類に分けます。

tool_call  Tool を実行し、結果を会話に足して続きを聞く
final      本文が返ったので、それを回答にして終わる

入力も Tool も前回と同じです。今回の実行は 4 回で、final が返って終わりました。8 回を超えても止まります。

成果物

npm start

> start
> tsx src/index.ts

モデル: openai/gpt-4o-mini

--- User Prompt ---

今日どう動けばいい?

--- LLM Response ---
{
  "role": "assistant",
  "content": null,
  "refusal": null,
  "reasoning": null,
  "tool_calls": [
    {
      "type": "function",
      "index": 0,
      "id": "call_nYOUKmOp4shDuL7z1I1wJomj",
      "function": {
        "name": "getSchedule",
        "arguments": "{\"date\":\"今日\"}"
      }
    }
  ]
}

--- Tool Calls ---
[
  {
    "type": "function",
    "index": 0,
    "id": "call_nYOUKmOp4shDuL7z1I1wJomj",
    "function": {
      "name": "getSchedule",
      "arguments": "{\"date\":\"今日\"}"
    }
  }
]

--- 1回目のLLM ---
取り出し元: choices[0].message.tool_calls

getSchedule({"date":"今日"})

--- Tool Result ---
{
  "date": "今日",
  "events": [
    {
      "time": "19:00",
      "location": "梅田",
      "title": "食事"
    }
  ]
}

--- LLM Response ---
{
  "role": "assistant",
  "content": null,
  "refusal": null,
  "reasoning": null,
  "tool_calls": [
    {
      "type": "function",
      "index": 0,
      "id": "call_ozMaR40NmvdEmTPWqpJx9gCc",
      "function": {
        "name": "getWeather",
        "arguments": "{\"location\":\"梅田\",\"time\":\"19:00\"}"
      }
    }
  ]
}

--- Tool Calls ---
[
  {
    "type": "function",
    "index": 0,
    "id": "call_ozMaR40NmvdEmTPWqpJx9gCc",
    "function": {
      "name": "getWeather",
      "arguments": "{\"location\":\"梅田\",\"time\":\"19:00\"}"
    }
  }
]

--- 2回目のLLM ---
取り出し元: choices[0].message.tool_calls

getWeather({"location":"梅田","time":"19:00"})

--- Tool Result ---
{
  "location": "梅田",
  "time": "19:00",
  "forecast": "18:00から雨"
}

--- LLM Response ---
{
  "role": "assistant",
  "content": null,
  "refusal": null,
  "reasoning": null,
  "tool_calls": [
    {
      "type": "function",
      "index": 0,
      "id": "call_8OO8gsIpUtBCV4bct4DCgYoC",
      "function": {
        "name": "getRoute",
        "arguments": "{\"origin\":\"自宅\",\"destination\":\"梅田\"}"
      }
    }
  ]
}

--- Tool Calls ---
[
  {
    "type": "function",
    "index": 0,
    "id": "call_8OO8gsIpUtBCV4bct4DCgYoC",
    "function": {
      "name": "getRoute",
      "arguments": "{\"origin\":\"自宅\",\"destination\":\"梅田\"}"
    }
  }
]

--- 3回目のLLM ---
取り出し元: choices[0].message.tool_calls

getRoute({"origin":"自宅","destination":"梅田"})

--- Tool Result ---
{
  "origin": "自宅",
  "destination": "梅田",
  "duration": "30分"
}

--- LLM Response ---
{
  "role": "assistant",
  "content": "予定は19:00に梅田での食事です。雨が予想されるため、傘が必要です。家を出る時刻は18:20です。",
  "refusal": null,
  "reasoning": null
}

--- Tool Calls ---
[]

--- 回答 ---
終了条件: tool_calls がなく、本文が返った
取り出し元: choices[0].message.content

予定は19:00に梅田での食事です。雨が予想されるため、傘が必要です。家を出る時刻は18:20です。

シーケンス図

while (true)
  LLMを呼ぶ
    ↓
  tool_call ?
    はい → Tool実行 → 結果を会話に足す → 先頭へ
    いいえ → final。本文を表示して return

終了条件
  1. type が final
  2. 8回を超えた

今回は 1 で止まっています。予定、天気、移動、回答の 4 回です。

ディレクトリ構成

新しいファイルはありません。変わったのは src/agent/runtime.ts です。

agentic_ai_study/
├─ src/
│  ├─ index.ts
│  ├─ env.ts
│  ├─ agent/
│  │  ├─ prompts.ts
│  │  └─ runtime.ts         while と final / tool_call
│  ├─ llm/
│  │  ├─ openrouter.ts
│  │  └─ types.ts
│  └─ tools/
│     ├─ schedule.ts
│     ├─ weather.ts
│     ├─ route.ts
│     └─ execute.ts
├─ .env
├─ package.json
└─ tsconfig.json
npm start
  └─ tsx src/index.ts
       └─ agent/runtime.ts
            └─ while (true)

コード

変わった src/agent/runtime.ts だけ載せます。

import { requireEnv } from "../env.js";
import { callOpenRouter } from "../llm/openrouter.js";
import type { AssistantReply, ChatMessage, ChatResponse, ToolCall } from "../llm/types.js";
import { executeTool, tools } from "../tools/execute.js";
import { SYSTEM_PROMPT, USER_PROMPT } from "./prompts.js";

const MAX_TURNS = 8;

type AgentResponse =
  | { type: "final"; content: string }
  | { type: "tool_call"; calls: ToolCall[] };

function readMessage(response: ChatResponse): AssistantReply {
  const message = response.choices?.[0]?.message;
  if (!message) {
    throw new Error("応答に choices[0].message がありません");
  }
  return message;
}

function toAgentResponse(message: AssistantReply): AgentResponse {
  const calls = message.tool_calls ?? [];
  if (calls.length > 0) {
    return { type: "tool_call", calls };
  }
  if (!message.content) {
    throw new Error("応答に tool_calls も content もありません");
  }
  return { type: "final", content: message.content };
}

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 },
  ];

  let turn = 0;

  while (true) {
    turn += 1;
    if (turn > MAX_TURNS) {
      throw new Error(`終了条件: ${MAX_TURNS}回を超えたため終了しました`);
    }

    const message = readMessage(await callOpenRouter(messages, tools));
    console.log("\n--- LLM Response ---");
    console.log(JSON.stringify(message, null, 2));
    console.log("\n--- Tool Calls ---");
    console.log(JSON.stringify(message.tool_calls ?? [], null, 2));

    const response = toAgentResponse(message);

    if (response.type === "final") {
      console.log("\n--- 回答 ---");
      console.log("終了条件: tool_calls がなく、本文が返った");
      console.log("取り出し元: choices[0].message.content\n");
      console.log(response.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: response.calls,
    });

    for (const call of response.calls) {
      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),
      });
    }
  }
}
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?