具体例で学ぶAIエージェントのオーケストレーション: ルーチンとハンドオフ
はじめに
エージェント同士の連携やタスクの切り替えが必要になるとき、どのように管理すればよいのでしょうか。
言語モデルを活用したアプリケーションでは、1つの大規模言語モデル(LLM)に全責任を委ねる単純な構成であっても、多岐にわたるプロンプトやアクションを必要とする場合、コードや設計が急激に複雑化する懸念があります。そこで注目されるのが、エージェント同士をルーチンと呼ばれる手順で定義し、必要に応じて別のエージェントへハンドオフ(引き継ぎ)できる形でオーケストレーションを行う方法です。
この記事では、ルーチンとハンドオフの概念を整理しながら、複数のエージェントを効率よく制御する実装パターンを紹介します。コード例を丁寧に交えながら解説し、最後にはそれらのアイデアを組み合わせたサンプルリポジトリ「Swarm」に言及します。
要約
ルーチン: システムプロンプトと使用ツール(関数など)のセットで、タスクや会話の進行手順をまとめたもの
ハンドオフ: 会話やタスク処理の途中で、あるエージェントから別のエージェントに切り替える方法
複数のエージェント: ルーチンをエージェント単位で定義し、ユーザーの要求に合わせて動的に切り替えたり、引き継ぎを行う
実装方法: OpenAIの関数呼び出し(function calling)機能やシステムプロンプトを活用し、ループでツール呼び出しを処理する。最後に結果をモデル側に返してやり取りする
これらの流れを押さえることで、エージェントのオーケストレーションを比較的シンプルかつ堅牢に実現できます。
ルーチンとは
「ルーチン」は、自然言語で表された指示(システムプロンプト)と、それを実行するためのツール(関数群やAPIなど)をまとめた概念です。
日常用語の「手順書」や「マニュアル」に近いイメージで、特定のタスクを一連の流れで実行していきます。
たとえばカスタマーサポートのシナリオの場合:
はじめにユーザーの問題点をヒアリング
可能な解決策を提示
条件を満たせば返金を実行
といった流れをルーチンとして定義できます。
単一のルーチン例
system_message = (
"You are a customer support agent for ACME Inc. "
"Always answer in a sentence or less. "
"Follow the following routine with the user: "
"1. First, ask probing questions and understand the user's problem deeper. "
" - unless the user has already provided a reason. "
"2. Propose a fix (make one up). "
"3. ONLY if not satisfied, offer a refund. "
"4. If accepted, search for the ID and then execute refund."
)
def look_up_item(search_query):
"""Use to find item ID.
Search query can be a description or keywords.
"""
# 実際にはデータベース検索などを行う想定
return "item_132612938"
def execute_refund(item_id, reason="not provided"):
print("Summary:", item_id, reason)
return "success"
上記のように、ルーチン(system_message)とツール(look_up_itemやexecute_refund)がセットになっている構成です。
LLM(言語モデル)がシステムプロンプトに書かれた手順(ルーチン)を読み取り、その指示に合わせてツール呼び出しを行います。
ルーチンの実行とコード例
ルーチンを実行するには、ユーザーからの入力メッセージとシステムプロンプトを含むメッセージ一式をLLMに投げかけ、結果を得るループを組みます。以下は簡単な例です。
from openai import OpenAI
client = OpenAI()
def run_full_turn(system_message, messages):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "system", "content": system_message}] + messages,
)
message = response.choices[0].message
messages.append(message)
if message.content:
print("Assistant:", message.content)
return message
messages = []
while True:
user_input = input("User: ")
messages.append({"role": "user", "content": user_input})
run_full_turn(system_message, messages)
ここではまだ関数呼び出し(function calling)をハンドリングしていませんが、会話を重ねる形でルーチン(システムプロンプトに書かれた「手順」)を実行しています。
コードでのツール呼び出し
OpenAIの機能では、エージェントが「関数を呼び出す」という形でツールを使うように定義できます。たとえば execute_refund や look_up_item をLLMが呼び出す場合、引数に適切なデータを与えて実行し、その結果をLLMに返す仕組みが取れます。
関数とスキーマの対応づけ
import inspect
import json
def function_to_schema(func) -> dict:
"""Pythonの関数を、OpenAIのfunction calling向けJSONスキーマに変換する."""
type_map = {
str: "string",
int: "integer",
float: "number",
bool: "boolean",
list: "array",
dict: "object",
type(None): "null",
}
signature = inspect.signature(func)
parameters = {}
for param in signature.parameters.values():
param_type = type_map.get(param.annotation, "string")
parameters[param.name] = {"type": param_type}
required = [
param.name
for param in signature.parameters.values()
if param.default == inspect._empty
]
return {
"type": "function",
"function": {
"name": func.__name__,
"description": (func.__doc__ or "").strip(),
"parameters": {
"type": "object",
"properties": parameters,
"required": required,
},
},
}
関数の署名(パラメータ名や型アノテーションなど)を自動的にJSONスキーマに変換し、OpenAIに「ツール」として登録できるようにします。
実行フェーズ
tools = [execute_refund, look_up_item]
tool_schemas = [function_to_schema(t) for t in tools]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Look up the black boot."}],
tools=tool_schemas,
)
message = response.choices[0].message
ここでモデルが関数呼び出しを行った場合、message.tool_calls という形で、どの関数が呼ばれたか・引数は何かを取得できます。その結果に基づいてPython側で実際の関数を呼び出し、結果を再度LLMに返す、というループを作ればOKです。
ハンドオフ
ここまでは「1つのルーチンを使い続ける」例でした。しかし、ユーザーの要求が変わった場合、あるエージェント(ルーチン)で対応しきれなくなることがあります。
たとえば、「商品を購入したい」と言ったあとで「やっぱり返金したい」となる場合、販売担当エージェントから返金担当エージェントへの切り替え(ハンドオフ)が必要です。
電話で「ちょっと部署を変わりますね」と言われて転送されるイメージをそのままエージェントに適用したものがハンドオフです。
エージェントのクラス設計
複数のルーチンを扱う方法として、ルーチンをまとめて1つの「エージェントクラス」と見立てるのが便利です。以下のように最低限の情報を持たせます。
from typing import Optional, List
from pydantic import BaseModel
class Agent(BaseModel):
name: str = "Agent"
model: str = "gpt-4o-mini"
instructions: str = "You are a helpful Agent"
tools: list = []
name: エージェントの名前
model: 利用する言語モデル
instructions: システムプロンプトに相当する文字列(ルーチンの内容)
tools: エージェントが使える関数やAPIなど
あとは、単純な「会話 + 関数呼び出し処理」をする関数を用意し、上記の Agent インスタンスを引数に渡せばいいのです。
複数エージェントの連携
ユーザーのリクエスト内容に応じて、販売エージェント(Sales Agent)と返金エージェント(Refund Agent)を切り替える例を見てみましょう。
def execute_refund(item_name):
return "success"
refund_agent = Agent(
name="Refund Agent",
instructions="You are a refund agent. Help the user with refunds.",
tools=[execute_refund],
)
def place_order(item_name):
return "success"
sales_agent = Agent(
name="Sales Assistant",
instructions="You are a sales assistant. Sell the user a product.",
tools=[place_order],
)
messages = []
user_query = "Place an order for a black boot."
print("User:", user_query)
messages.append({"role": "user", "content": user_query})
# 販売エージェントで処理
run_result = run_full_turn(sales_agent, messages)
messages.extend(run_result)
user_query = "Actually, I want a refund."
print("User:", user_query)
messages.append({"role": "user", "content": user_query})
# 返金エージェントで処理
run_result = run_full_turn(refund_agent, messages)
messages.extend(run_result)
ユーザーが「黒いブーツを注文してほしい」と言ったら販売エージェントが対応し、注文後に「返金してほしい」と言われれば返金エージェントに渡す、というシンプルな構造です。
ハンドオフをモデル自身に判断させる
上記例ではハンドオフを「コード上で人間が明示的に切り替える」形ですが、さらにLLMに「適切なタイミングで別エージェントに転送する」という機能を持たせると、より高度なオーケストレーションが可能です。
ハンドオフ用の関数をあらかじめツールとして登録しておき、モデルが transfer_to_refunds() のような関数を呼び出したら、実際に返金エージェントに切り替えるイメージです。
def transfer_to_refunds():
return refund_agent # エージェントインスタンスを返す
refund_agent = Agent(
name="Refund Agent",
instructions="You are a refund agent. ...",
tools=[execute_refund]
)
sales_agent = Agent(
name="Sales Agent",
instructions="You are a sales agent. ...",
tools=[place_order, transfer_to_refunds]
)
モデルが「このユーザーは返金を求めている」と判断したら transfer_to_refunds() を呼び出す。その関数が返した refund_agent を次の処理で使うことで、ハンドオフを実現できます。
単一エージェント vs 複数エージェントの比較表
アプローチ 特徴 メリット デメリット 単一エージェント すべてのロジック(ルーチン)を1つのエージェント(システムプロンプト)に書き込み、ツールも全て同じ領域で使う ・実装が単純・学習コストが低い ・指示が肥大化しがち・分岐ロジックが膨大になる 複数エージェント 販売、返金、トリアージなど担当ごとにエージェントを定義し、必要に応じてハンドオフする ・各エージェントが専門特化できる・メンテナンスが容易・大規模運用に向いている ・エージェント間連携の設計が必要・どのタイミングでハンドオフするかを工夫する必要がある
まとめ
エージェントを複数に分割し、ルーチンとハンドオフによってオーケストレーションを行うアプローチは、LLM活用の新しい可能性を広げます
特に、ユーザーの要求が多岐にわたる大規模なシステムでは、エージェントを役割別に設計し、適切なタイミングでハンドオフすることでコードの見通しを良くし、スケールしやすい構成を得られます。興味のある方は、サンプルリポジトリ「Swarm」を参考にしてみてください