OpenAI Agents SDKと、その中にあるMCPの仕組みを利用して、自律するAIエージェントを試作してみました。
特徴としては、
- 複数のエージェントとMCPサーバー
- それらエージェント達が、1つの目的のために、MCPツールを自律的に次々と実行したり、委譲(Handoff)しあったりする
全体構成と概要
架空の「ボードゲーム部(いわゆる「ボドゲ部」)」というのを想定します。
このボドゲ部では、メンバーが、1人もしくは数人で、部が所有する多数のゲームから何か1つ選んで、遊びます。
メンバーは、遊びたいゲームを予約(使用登録)します。それは電子的に行われます。
このようにして、「ゲーム使用データ」が溜まっていきます。
ゲーム使用データ:「誰(と誰)が」「どのゲームを」「いつからいつまで」使うか
ある時、このボドゲ部にAIエージェントシステムが導入された、と想定します。
ボドゲ部AIエージェントシステムを利用する(プロンプトを投入する)のは、部のメンバー達です。
全体構成
このボドゲ部AIエージェントシステムの中のエージェントは以下の3人で、それぞれの役割を持っています。
ユーザーからのプロンプトを1次受付して、実際に対応する下記子エージェントに対応を委譲する親エージェント「1次受付」。
ユーザーからの相談に対応し、ゲーム使用データの統計を駆使して応える子エージェント「アナリスト・コンサルタント」。
ユーザーからの単純な手続き依頼や問い合わせ対応をする子エージェント「処理・問い合わせ対応係」。
子エージェント1人に対して専用のMCPサーバー1個を割り当て、子エージェントが自分の仕事をする時は、それら自分専用のMCPツールを自律して実行できるようにしました。
これら複数のエージェント達が、自律的に委譲しあい、委譲されたエージェントは自分に与えられた武器(MCPツール)を自律的に使い、ユーザーからのプロンプトに対応します。
エージェント | 役割 | 各エージェント専用 MCPサーバーのツール群 |
---|---|---|
子エージェント 「アナリスト・コンサルタント」 |
▼メンバーたちの相談対応 ・遊ぶゲームをリコメンド メンバー個々のゲーム嗜好性などを割り出した上で行う。 ・ゲーム使用統計の情報提供 ゲームの人気ランキングやメンバー個々のゲーム嗜好性など、ゲーム使用データの統計を取得して行う。 ※いずれもゲーム使用データの統計の利活用 |
・ゲーム使用データの各種統計の取得 ・メンバーマスタの読み出し ・ゲームマスタの読み出し |
子エージェント 「処理・問い合わせ対応係」 |
▼メンバーの依頼に応じて手続き ・ゲーム使用の予約 ▼メンバーからの問い合わせ対応 ・予約の確認や問い合わせに回答 ・ゲームの空きの問い合わせに回答 ※いずれもゲーム使用データのローデータの単純利用 |
・ゲーム使用の予約(ゲーム使用データの生成) ・ゲーム予約データ(ゲーム使用データのローデータ)の取得 ・ゲームの空き時間帯の取得 ・メンバーマスタの読み出し ・ゲームマスタの読み出し |
親エージェント 「1次受付」 |
ユーザープロンプトを1次受付して、その内容を見て、上記2つの子エージェントのどちらかに委譲する。 | (無し) |
ボドゲ部AIエージェントシステムを使ってみる
実際にこのボドゲ部AIエージェントシステムを使ってみることにします。
以下の2パターンを試します。
<ユーザーの判断を挟む>親エージェント→子エージェントの委譲の繰り返し
<完全自律・一気通貫>子エージェント同士の委譲
<ユーザーの判断を挟む>親エージェント→子エージェントの委譲の繰り返し
まずは、1パターン目の、
<ユーザーの判断を挟む>親エージェント→子エージェントの委譲の繰り返し
をやってみます。
ユーザーの判断を挟んで、2つの子エージェントがその前後におけるそれぞれの領分で自律的に動く様を見てみます。
「私」がプロンプト1を投入
私と山田さんの2人に合いそうなゲームを何か1つ決めて、今日の午後のテキトーな時間帯に1時間、予約入れたい。
「私」とは、AIエージェントシステムにログインしているログインユーザーで、このプロンプトを投入している者です。ボドゲ部のメンバーの1人です。
「山田さん」とは、ボドゲ部のメンバーの1人です。
プロンプト
「私と山田さんの2人に合いそうなゲームを何か1つ決めて、今日の午後のテキトーな時間帯に1時間、予約入れたい。」
から、
親エージェント「1次受付」は、
まずは「私と山田さんの2人に合いそうなゲームを何か1つ決め」る必要があると判断し、
子エージェント 「アナリスト・コンサルタント」に委譲します。
↓
委譲された子エージェント 「アナリスト・コンサルタント」は、
「私と山田さんの2人に合いそうなゲーム」を、MCPサーバーの各種統計ツールを駆使して推測し、ユーザーに提案します。
↓
ユーザーは、
その提案を了承するならば、そのままGOサインのプロンプトを投入します。
↓
遊ぶゲームが決定して、次は予約手続きなので、
親エージェント「1次受付」は、
子エージェント 「処理・問い合わせ対応係」に委譲します。
↓
委譲された子エージェント「処理・問い合わせ対応係」は、
MCPサーバーのツールを使って、空きの時間帯の確認と予約手続きを行い、予約結果をユーザーに返します。
事前:「私」のメンバーコードと現在の日時等を、各エージェントに渡してある
前半)子エージェント「アナリスト・コンサルタント」がゲームを提案するまでの自律の軌跡
順番 | 誰が | 何をした | 委譲先 |
---|---|---|---|
1 | ユーザー | <プロンプト1の投入> プロンプト 「私と山田さんの2人に合いそうなゲームを何か1つ決めて、今日の午後のテキトーな時間帯に1時間、予約入れたい。」 投入。 |
– |
2 | 親エージェント 「1次受付」 |
<子エージェント「アナリスト・コンサルタント」に委譲> まずは「私と山田さんの2人に合いそうなゲームを何か1つ決め」る必要があると判断し、子エージェント 「アナリスト・コンサルタント」に委譲。 |
子エージェント 「アナリスト・コンサルタント」 |
3 | 子エージェント 「アナリスト・コンサルタント」 |
<山田さんのメンバー情報を取得> 山田さんのメンバーコードやその他の情報を、MCPのメンバーマスタデータ取得ツールで取得。 ※全MCPツールは、メンバーもゲームも、名前ではなくコードで扱うため、これは必要。 |
– |
4 | 子エージェント 「アナリスト・コンサルタント」 |
<「私」のメンバー情報を取得> 「私」のメンバー情報を、MCPのメンバーマスタデータ取得ツールで取得。 ※事前に「私」のメンバーコードを各エージェントに渡してあるので意図は不明であるが、名前を取得したかったのかも。最終応答時に名前を使用するからか。 |
– |
5 | 子エージェント 「アナリスト・コンサルタント」 |
<「私」のゲーム使用履歴を取得> 「私」の過去4日間のゲーム使用履歴を、MCPのゲーム使用データ統計ツール群のツールを使用して取得。 |
– |
6 | 子エージェント 「アナリスト・コンサルタント」 |
<山田さんのゲーム使用履歴を取得> 山田さんの過去4日間のゲーム使用履歴を、MCPのゲーム使用データ統計ツール群のツールを使用して取得。 |
– |
7 | 子エージェント 「アナリスト・コンサルタント」 |
<ゲーム人気ランキングを取得> 過去4日間のゲーム人気ランキングを、MCPのゲーム使用データ統計ツール群のツールを使用して取得。 |
– |
8 | 子エージェント 「アナリスト・コンサルタント」 |
<ユーザーに提案するゲームの決定> これまで得られた種々の情報から、 ゲーム「電脳将棋」 をユーザーに提案することを決定。 |
– |
9 | 子エージェント 「アナリスト・コンサルタント」 |
<最終応答・ユーザーにゲームを提案> ユーザーに、ゲーム「電脳将棋」を提案する最終応答をアウトプット。 |
– |
子エージェント 「アナリスト・コンサルタント」からの最終応答。ゲームの提案と予約の促し。
中村あかりさんと山田美咲さんに合うゲームとして、「電脳将棋」を提案します。
このゲームは過去に使用されており、対戦を楽しむ要素があるため、2人で楽しむのに適しています。
明日の午後の適当な時間帯に1時間、予約を入れましょう。どの時間が良いですか?
↓ 後半開始
引き続いて「私」がプロンプト2を投入
そこはまかせるよ。
「私」(中村あかり)は、提案「電脳将棋」に対して何も異議を唱えなかったので、提案を受け入れたことになります。
子エージェント 「アナリスト・コンサルタント」から「明日の午後の適当な時間帯に1時間、予約を入れましょう。どの時間が良いですか?」と尋ねられました。
ここは敢えて、「そこはまかせるよ。」と、丸投げしてみました。
後半)子エージェント「処理・問い合わせ対応係」が予約を完了するまでの自律の軌跡
順番 | 誰が | 何をした | 委譲先 |
---|---|---|---|
1 | ユーザー | <プロンプト2の投入> プロンプト 「そこはまかせるよ。」 投入。 |
– |
2 | 親エージェント 「1次受付」 |
<子エージェント「処理・問い合わせ対応係」に委譲> ゲームも決まって、あとは予約するだけなので、子エージェント「処理・問い合わせ対応係」に委譲。 |
子エージェント 「処理・問い合わせ対応係」 |
3 | 子エージェント 「処理・問い合わせ対応係」 |
<予約するゲームの空き時間帯を取得> 「電脳将棋」の今日午後いっぱいの空き時間帯を、専用のMCPツールで取得。 |
– |
4 | 子エージェント 「処理・問い合わせ対応係」 |
<予約処理実行> 取得した「電脳将棋」の空き時間帯から、今日の14:00 ~ 15:00で、「私」(中村あかり)と山田美咲さんの2人の参加者で予約。予約のMCPツールを使用。 |
– |
5 | 子エージェント 「処理・問い合わせ対応係」 |
<最終応答・ユーザーに予約内容を提示> ユーザーに、予約した内容の最終応答をアウトプット。 |
– |
子エージェント 「処理・問い合わせ対応係」からの最終応答。ゲームの予約内容の提示。
「電脳将棋」の予約が完了しました。
### 予約内容
- **ゲーム名**: 電脳将棋
- **使用日時**: 2025年6月16日 (月) 14:00 ~ 15:00
- **参加者**: 中村あかり, 山田美咲
- **ゲーム使用番号**: G004841320
楽しんでください!
<完全自律・一気通貫>子エージェント同士の委譲
次は、2パターン目の、
<完全自律・一気通貫>子エージェント同士の委譲
をやってみます。
AIによる完全自律・一気通貫をする場合、当たり前ですが、どこかに人間の最終判断を必要とする部分があってはなりません。
「私」がプロンプトを投入
今月の部の1番人気のゲームで、
明日の午後のテキトーな時間帯に1時間、田中さんと私の2人分の予約を入れといて。
「私」とは、AIエージェントシステムにログインしているログインユーザーで、このプロンプトを投入している者です。ボドゲ部のメンバーの1人です。
「田中さん」とは、ボドゲ部のメンバーの1人です。
「今月の(ボドゲ)部の1番人気のゲーム」は「事実」であり、ユーザーの最終判断ではありません。
「テキトーな時間帯」と言ってるので、ユーザーが最終判断する権利を放棄しています。
ですので、人間の最終判断を必要とする部分はありません。
AIによる完全自律・一気通貫の条件を満たしています。
プロンプト
「今月の部の1番人気のゲームで、明日の午後のテキトーな時間帯に1時間、田中さんと私の2人分の予約を入れといて。」
から、
親エージェント「1次受付」は、
まずは「今月の(ボドゲ)部の1番人気のゲーム」を割り出す必要があると判断し、
子エージェント 「アナリスト・コンサルタント」に委譲します。
↓
委譲された子エージェント 「アナリスト・コンサルタント」は、
「今月の(ボドゲ)部の1番人気のゲーム」を、MCPサーバーの適切な統計ツールを使用して割り出します。
↓
遊ぶゲームが決定して、次は予約手続きなので、
子エージェント 「アナリスト・コンサルタント」は、
子エージェント 「処理・問い合わせ対応係」に横パス委譲します。
↓
委譲された子エージェント 「処理・問い合わせ対応係」は、
MCPサーバーのツールを使って、空きの時間帯の確認と予約手続きを行い、予約結果をユーザーに返します。
事前:「私」のメンバーコードと現在の日時等を、各エージェントに渡してある
全エージェントが協働して一気通貫で予約を完了するまでの自律の軌跡
順番 | 誰が | 何をした | 委譲先 |
---|---|---|---|
1 | ユーザー | <プロンプトの投入> プロンプト 「今月の部の1番人気のゲームで、明日の午後のテキトーな時間帯に1時間、田中さんと私の2人分の予約を入れといて。」 投入。 |
– |
2 | 親エージェント 「1次受付」 |
<子エージェント「アナリスト・コンサルタント」に委譲> まずは「今月の(ボドゲ)部の1番人気のゲーム」を割り出す必要があると判断し、子エージェント「アナリスト・コンサルタント」に委譲。 |
子エージェント 「アナリスト・コンサルタント」 |
3 | 子エージェント 「アナリスト・コンサルタント」 |
<田中さんのメンバー情報を取得> 田中さんのメンバーコードやその他の情報を、MCPのメンバーマスタデータ取得ツールで取得。 ※全MCPツールは、メンバーもゲームも、名前ではなくコードで扱うため、これは必要。 |
– |
4 | 子エージェント 「アナリスト・コンサルタント」 |
<「私」のメンバー情報を取得> 「私」のメンバー情報を、MCPのメンバーマスタデータ取得ツールで取得。 ※事前に「私」のメンバーコードを各エージェントに渡してあるので意図は不明であるが、名前を取得したかったのかも。最終応答時に名前を使用するからか。 |
– |
5 | 子エージェント 「アナリスト・コンサルタント」 |
<今月のボドゲ部の1番人気のゲームの割り出し> 今月のボドゲ部の1番人気のゲームを、適切なMCPのゲーム使用データ統計ツールを使用して取得。 それは、 ゲーム「ダンジョンダッシュ」 であった。 |
– |
6 | 子エージェント 「アナリスト・コンサルタント」 |
<子エージェント「処理・問い合わせ対応係」に横パス委譲> 遊ぶゲームが決定して、次は予約手続きなので、子エージェント 「処理・問い合わせ対応係」に横パス委譲。 |
子エージェント 「処理・問い合わせ対応係」 |
7 | 子エージェント 「処理・問い合わせ対応係」 |
<予約するゲームの空き時間帯を取得> 「ダンジョンダッシュ」の明日午後いっぱいの空き時間帯を、専用のMCPツールで取得。 |
– |
8 | 子エージェント 「処理・問い合わせ対応係」 |
<予約処理実行> 取得した「ダンジョンダッシュ」の空き時間帯から、明日の12:00 ~ 13:00で、「私」(中村あかり)と田中花子さんの2人の参加者で予約。予約のMCPツールを使用。 |
– |
9 | 子エージェント 「処理・問い合わせ対応係」 |
<最終応答・ユーザーに予約内容を提示> ユーザーに、予約した内容の最終応答をアウトプット。 |
– |
子エージェント 「処理・問い合わせ対応係」からの最終応答。ゲームの予約内容の提示。
予約が完了しました。以下が予約の詳細です。
- **ゲーム名**: ダンジョンダッシュ
- **ゲーム使用番号**(code_game_usage): G002960060
- **使用日時**: 2025年6月17日 12:00 ~ 13:00
- **参加者**: 中村あかり (あなた), 田中花子
何か他にお手伝いできることがあればお知らせください。
実装など
※エージェントがいるMCPクライアント側のみ掲載します。
MCPクライアント側(エージェントがいる側)
OpenAI Agents SDKを使用しています。
実装コードを抜粋して掲載します。
核となるのはprocess_user_prompt
関数で、ユーザープロンプトを受け付け、エージェントの最終応答を戻り値として返します。
MCPクライアント側(エージェントがいる側)実装の抜粋
MCPクライアント側(エージェントがいる側)実装の抜粋
# 環境変数の読み込み
load_dotenv()
env_vars = os.environ.copy()
# ログインユーザーのメンバーコード(ログインユーザーもボドゲ部のメンバーの1人)
user_code_member = ""
# RunResultクラスのto_input_list()の結果を保持
conversation_items = [] # OpenAI Agents SDKの履歴管理を活用
# MCPサーバー「アナリスト・コンサルタント専用」
server_sug = None
# MCPサーバー「アナリスト・コンサルタント専用」の設定
SERVER_COMMAND_SUG = "uv"
SERVER_SCRIPT_SUG = "./mcp_servers/server_sug.py"
# MCPサーバー「処理・問い合わせ対応係専用」
server_reg = None
# MCPサーバー「処理・問い合わせ対応係専用」の設定
SERVER_COMMAND_REG = "uv"
SERVER_SCRIPT_REG = "./mcp_servers/server_reg.py"
# 全エージェント内部で使用するLLM
LLM_MODEL_NAME ="gpt-4o-mini"
def initialize(user_code: str) -> bool:
"""
MCPクライアントの初期化。
"""
global user_code_member, conversation_items, server_sug, server_reg
# ログインユーザー(プロンプト投入者)のメンバーコード
user_code_member = user_code
# 会話履歴をクリア
conversation_items = []
# OpenAI APIキーの取得と設定
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEYが設定されていません。")
set_default_openai_key(api_key)
# MCPサーバー「アナリスト・コンサルタント専用」インスタンス生成
server_sug = MCPServerStdio(
params={
"command": SERVER_COMMAND_SUG,
"args": ["run", SERVER_SCRIPT_SUG],
"env": env_vars
}
)
# MCPサーバー「処理・問い合わせ対応係専用」インスタンス生成
server_reg = MCPServerStdio(
params={
"command": SERVER_COMMAND_REG,
"args": ["run", SERVER_SCRIPT_REG],
"env": env_vars
}
)
return True
def process_user_prompt(user_input: str) -> str:
"""
ユーザーのプロンプトuser_inputをAIエージェントが処理。
AIエージェントによる最終応答を返す。
UIから呼ばれることを想定し、同期関数とする。
"""
# 子エージェント「アナリスト・コンサルタント」のinstructions
instructions_sug = create_instructions_for_sug()
# 子エージェント「処理・問い合わせ対応係」のinstructions
instructions_reg = create_instructions_for_reg()
# 親エージェント「1次受付」のinstructions
instructions_main = create_instructions_for_main()
# 子エージェント「アナリスト・コンサルタント」のhandoff_description
handoff_description_sug = create_handoff_description_for_sug()
# 子エージェント「処理・問い合わせ対応係」のhandoff_description
handoff_description_reg = create_handoff_description_for_reg()
async def async_process():
global conversation_items, server_sug, server_reg
# エージェントの最終応答で、この同期関数の戻り値
ret = ""
try:
async with server_sug, server_reg:
# async with ブロックに入り、MCPサーバーとのMCPセッションが自動的に開始される
await server_sug.list_tools()
await server_reg.list_tools()
# 子エージェント「アナリスト・コンサルタント」インスタンス生成
agent_sug = Agent(
name="アナリスト・コンサルタント",
instructions=instructions_sug,
mcp_servers=[server_sug],
model=LLM_MODEL_NAME,
handoff_description=handoff_description_sug
)
# 子エージェント「処理・問い合わせ対応係」インスタンス生成
agent_reg = Agent(
name="処理・問い合わせ対応係",
instructions=instructions_reg,
mcp_servers=[server_reg],
model=LLM_MODEL_NAME,
handoff_description=handoff_description_reg
)
# 子エージェント同士で委譲しあえるように
agent_sug.handoffs = [agent_reg]
agent_reg.handoffs = [agent_sug]
# 親エージェント「1次受付」インスタンス生成
agent_main = Agent(
name="1次受付",
instructions=instructions_main,
model=LLM_MODEL_NAME,
handoffs=[agent_sug, agent_reg]
)
# 新しいユーザー入力を今までのやりとりに追加
current_input = conversation_items + [{"role": "user", "content": user_input}]
# 親エージェント「1次受付」に回答要求
result = await Runner.run(agent_main, current_input)
ret = result.final_output.strip()
# 今までのやりとりを抽出・保存(以降も会話のコンテキストとして使用)
conversation_items = result.to_input_list()
# async with ブロックを抜け、MCPサーバーとのMCPセッションが自動的に遮断される
return ret
except Exception as e:
logger.error(f"プロンプト処理中にエラー発生: {type(e).__name__}: {e}")
return f"[MCP] エラーが発生しました: {str(e)}"
try:
return asyncio.run(async_process())
except Exception as e:
logger.error(f"asyncio実行エラー: {type(e).__name__}: {e}")
return f"[MCP] 実行エラー: {str(e)}"
本稿の実装における補足説明
各エージェントのInstructionsの生成
本AIエージェントシステムでの各エージェントは、各々の仕事にそれなりの正確性が求められます。
結果、各エージェントのInstructionsは長大になりました。
そこで、各エージェントのInstructionsを生成する別関数を作りました。
create_instructions_for_~
という関数で、各エージェントの長大なInstructionsを生成しています。
そこでは、
・このエージェントに期待されていること
・このエージェントの仕事リスト
・このエージェントの各仕事の手順や注意事項
・他のエージェントに委譲すべきものは何か
といったことを記載しています。
各子エージェントのhandoff_descriptionの生成
エージェント間での委譲(handoff)を正しく行わせるためには、委譲される側の2つの子エージェントのhandoff_descriptionも、それなりにちゃんと書く必要があります。
そういうわけで、2つの子エージェントのhandoff_descriptionもそこそこ長くなってしまい、Instructions同様、2つの子エージェントのhandoff_descriptionを生成する別関数を作りました。
create_handoff_description_for_~
という関数で、2つの子エージェントのそこそこ長いhandoff_descriptionを生成しています。
そこでは、
・委譲先であるこのエージェントの役割
・委譲先であるこのエージェントの仕事リスト
といったことを記載しています。
ログインユーザーのメンバーコードと現在日時のエージェントへの渡し方
「ログインユーザー」とは、AIエージェントシステムにログインしてエージェントにプロンプトを投入する者で、ボドゲ部のメンバーの1人です。
このログインユーザーのメンバーコードをエージェントに渡して、このログインユーザーの情報を扱うことができるようにしてあげる必要があります。
また、エージェント(内部のLLM)は、「今がいつか」を知らないので、現在日時を教えてあげる必要があります。
でないと、「今月のボドゲ部の一番人気のゲーム」「明日の午後のテキトーな時間帯に予約」とエージェントに要求しても、エージェントは実のある対応をしません。
両者は、全エージェントのInstructionsに含める形で、エージェントに渡しています。
contextに含める、という考えも浮かびますが、それはできません。
OpenAI Agents SDKのrun_context.pyに「Contexts are not passed to the LLM.」と書いてあります。
しかし、ログインユーザーのメンバーコードも現在日時も、むしろ積極的にエージェント(内部のLLM)に渡す必要があります。
なので、contextは使えません。
簡潔で容易な方法として、エージェントのInstructionsに含める、というやり方しか思いつきませんでした。
ユーザーとエージェントの複数ターンに渡る会話の実現
実装上なにも考慮しないと、ユーザーとエージェントとの会話は「1ターン」で消失します。
(それは当たり前で、そもそも1ターン毎にAgentインスタンスを生成しているので。普通の実装はそうするはず。)
なので、1個前の会話を前提に発話しても、全く通じません。
そこで、エージェントと複数ターンに渡る一貫した会話をするために、OpenAI Agents SDKの実装例にならって、以下のようにしました。
global変数
# RunResultクラスのto_input_list()の結果を保持
conversation_items = [] # OpenAI Agents SDKの履歴管理を活用
def process_user_prompt関数内
# 新しいユーザー入力を今までのやりとりに追加
current_input = conversation_items + [{"role": "user", "content": user_input}]
# 親エージェント「1次受付」に回答要求
result = await Runner.run(agent_main, current_input)
# 今までのやりとりを抽出・保存(以降も会話のコンテキストとして使用)
conversation_items = result.to_input_list()
エージェントからの回答resultは、RunResultクラスのインスタンスです。
RunResultクラスには、to_input_list()
というメソッドがあります。
この戻り値は、このrun
に投入された入力と、それに対する回答、及びそこに至るまでのAgentのツール呼び出しとその結果などが入ったlistです。要するに、そのlistにはこの1ターン分のやり取りとその経緯全てが入っているわけです。
これを利用して、ユーザーとエージェントの複数ターンに渡る会話を実現しています。
Views: 0