- AI ⚙️
OpenAI Agents SDK入門|Agent・ツール・ハンドオフ
OpenAI Agents SDK(パッケージ名 openai-agents)は、LLM に「指示・ツール・引き継ぎ先」を持たせた Agent を作り、Runner で動かすための Python ライブラリです。ツールの呼び出しと結果の受け渡しの繰り返し(エージェントループ)を SDK が受け持ちます。
pip install openai-agents
export OPENAI_API_KEY=sk-...
from agents import Agent, Runner
agent = Agent(name="Assistant", instructions="あなたは親切なアシスタントです。")
result = Runner.run_sync(agent, "こんにちは")
print(result.final_output)
| 部品 | 役割 |
|---|---|
Agent |
名前・指示(instructions)・モデル・ツール・引き継ぎ先をまとめたもの |
Runner |
Agent を動かす。run(async)・run_sync・run_streamed |
@function_tool |
Python の関数をツールにする(引数の型と docstring から JSON スキーマを作る) |
handoffs |
別の Agent に会話を引き継ぐ |
input_guardrail / output_guardrail |
入力・出力を検査して、条件に当たったら止める |
| tracing | 実行の記録を OpenAI のダッシュボードに送る(既定でオン) |
確認環境:openai-agents 0.23.1(openai 3.29.0)、Python 3.11、Linux。OpenAI の API の代わりに、Responses API の形で応答を返す擬似エンドポイントを自作し、OPENAI_BASE_URL をそこに向けて、SDK が送ったリクエストの本文とヘッダーを記録しました。モデルの応答は擬似エンドポイントが決めたもので、SDK の動き(ループ・例外・送る内容)を確かめています。仕様は SDK の公式ドキュメントとソースで確かめました(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「OpenAI Agents SDK入門|Agent・ツール・ハンドオフ」
インストールと必要なもの
| 項目 | 内容 |
|---|---|
| パッケージ | openai-agents(import 名は agents) |
| Python | 3.10 以上(パッケージの Requires-Python: >=3.10) |
| 依存 | openai>=3.0.0,<4・pydantic>=2.12.2・mcp など |
| API キー | 環境変数 OPENAI_API_KEY |
キーが無いまま動かすと、最初のリクエストの前に次のエラーで止まりました。
openai.OpenAIError: Missing credentials. Please pass an `api_key`, `workload_identity`, `admin_api_key`, or set the `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` environment variable.
最小の Agent:実際に送られたリクエスト
冒頭の Runner.run_sync(agent, "こんにちは") で、SDK は POST /v1/responses(Responses API)を1回送りました。本文です。
{
"model": "gpt-5.6-luna",
"include": [],
"input": [{"content": "こんにちは", "role": "user"}],
"instructions": "あなたは親切なアシスタントです。",
"reasoning": {"effort": "none"},
"text": {"verbosity": "low"},
"tools": []
}
| 見えたこと | 内容 |
|---|---|
| モデル | 指定しないと gpt-5.6-luna。0.23.1 のソース(default_models.py)の既定値で、環境変数 OPENAI_DEFAULT_MODEL で変えられる |
| 推論設定 | 既定のモデルに合わせて reasoning.effort: none・verbosity: low が付いた |
| 指示 | instructions は system メッセージではなく、Responses API の instructions に入った |
| ヘッダー | User-Agent: Agents/Python 0.23.1、Authorization: Bearer <キー> |
モデルは Agent ごとに Agent(..., model="gpt-5.5") のように指定できます。
戻り値の RunResult から取れるものです。
print(result.final_output) # 最終的な答え(文字列 or output_type の型)
print(result.last_agent.name) # 最後に答えた Agent → Assistant
print([type(i).__name__ for i in result.new_items]) # ['MessageOutputItem']
print(result.context_wrapper.usage) # requests=1, input_tokens=50, output_tokens=10 …
Responses API そのものの使い方は、関連記事「OpenAI API Python入門|Responses APIのコピペ集」で扱っています。
function_tool:関数をツールにする
from typing import Literal
from agents import Agent, Runner, function_tool
@function_tool
def get_weather(city: str, unit: Literal["c", "f"] = "c") -> str:
"""都市の現在の天気を返す。
Args:
city: 都市名(例:Tokyo)
unit: 温度の単位。c か f
"""
return f"{city} は晴れ、21度({unit})"
agent = Agent(name="Weather", instructions="天気を答えます。", tools=[get_weather])
result = Runner.run_sync(agent, "東京の天気は?")
print(result.final_output)
SDK が作った JSON スキーマ
関数名がツール名、docstring の1行目が説明、Args: の各行が引数の説明になりました。 ドキュメントでは、docstring は griffe で読み、Google・Sphinx・NumPy の形式に対応しています。
{
"type": "function",
"name": "get_weather",
"description": "都市の現在の天気を返す。",
"strict": true,
"parameters": {
"type": "object",
"title": "get_weather_args",
"additionalProperties": false,
"required": ["city", "unit"],
"properties": {
"city": {"type": "string", "title": "City", "description": "都市名(例:Tokyo)"},
"unit": {"type": "string", "title": "Unit", "enum": ["c", "f"], "default": "c",
"description": "温度の単位。c か f"}
}
}
}
| Python の書き方 | スキーマ |
|---|---|
city: str |
"type": "string" |
Literal["c", "f"] |
"enum": ["c", "f"] |
= "c"(既定値あり) |
"default": "c"。ただし required にも入った |
docstring の Args: |
各引数の description |
既定値のある unit も required に入っています。 strict: true(Structured Outputs の厳密モード)では、すべての引数を必須にする決まりがあるためです。擬似エンドポイントから {"city": "Tokyo"} だけを返すと、関数には既定値の "c" が渡りました。
ツールを呼ぶと、リクエストは2回
| 回 | input の中身 | 応答 |
|---|---|---|
| 1回目 | user「東京の天気は?」 |
function_call(get_weather、{"city": "Tokyo"}) |
| 2回目 | user + function_call + function_call_output(Tokyo は晴れ、21度(c)) |
最終の文章 |
2回目の input は次の形でした。関数の戻り値が function_call_output として、call_id で対応づけて送られます。
[
{"content": "東京の天気は?", "role": "user"},
{"type": "function_call", "call_id": "call_3", "name": "get_weather",
"arguments": "{\"city\": \"Tokyo\"}", "id": "fc_3", "status": "completed"},
{"type": "function_call_output", "call_id": "call_3", "output": "Tokyo は晴れ、21度(c)"}
]
result.new_items は ToolCallItem → ToolCallOutputItem → MessageOutputItem の順でした。ツール呼び出しの仕組みそのものは、関連記事「Function Calling(tool use)の仕組みとPython実装」で扱っています。
ループの上限 max_turns
モデルがツールを呼び続けると、max_turns(既定 10)で止まります。 max_turns=3 で、毎回ツールを呼ぶ応答を返すと、3回送ったところで例外になりました。
from agents import MaxTurnsExceeded
try:
await Runner.run(agent, "天気", max_turns=3)
except MaxTurnsExceeded as e:
print(e) # Max turns (3) exceeded
handoffs:別の Agent に引き継ぐ
from agents import Agent, Runner, handoff
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
billing_agent = Agent(
name="Billing agent",
handoff_description="支払い・返金の質問を担当",
instructions=f"{RECOMMENDED_PROMPT_PREFIX}\n支払いと返金の質問に答えます。",
)
faq_agent = Agent(name="FAQ agent", instructions="よくある質問に答えます。")
def on_faq(ctx):
print("on_handoff: FAQ へ引き継ぎ")
triage = Agent(
name="Triage agent",
instructions="質問の内容を見て、適切な担当に引き継ぎます。",
handoffs=[billing_agent, handoff(faq_agent, tool_name_override="ask_faq", on_handoff=on_faq)],
)
result = Runner.run_sync(triage, "返金の手続きを知りたい")
print(result.last_agent.name) # Billing agent
引き継ぎはツールとしてモデルに見せる
triage の1回目のリクエストの tools です。引き継ぎ先は transfer_to_<名前> という引数なしのツールになり、handoff_description が説明の後ろに付きました。
[
{"type": "function", "name": "transfer_to_billing_agent", "strict": true,
"description": "Handoff to the Billing agent agent to handle the request. 支払い・返金の質問を担当",
"parameters": {"type": "object", "properties": {}, "required": [], "additionalProperties": false}},
{"type": "function", "name": "ask_faq", "strict": true,
"description": "Handoff to the FAQ agent agent to handle the request. ",
"parameters": {"type": "object", "properties": {}, "required": [], "additionalProperties": false}}
]
モデルが transfer_to_billing_agent を呼ぶと、2回目のリクエストは Billing agent の instructions と tools に入れ替わり、input にはそれまでの会話がすべて入りました(id・status は省略して載せています)。
[
{"content": "返金の手続きを知りたい", "role": "user"},
{"type": "function_call", "name": "transfer_to_billing_agent", "arguments": "{}", "call_id": "call_7"},
{"type": "function_call_output", "call_id": "call_7", "output": "{\"assistant\": \"Billing agent\"}"}
]
| 確かめたこと | 結果 |
|---|---|
result.last_agent.name |
Billing agent |
result.new_items |
HandoffCallItem → HandoffOutputItem → MessageOutputItem |
| 引き継ぎ先が見る履歴 | それまでの会話すべて(ドキュメントの既定どおり) |
RECOMMENDED_PROMPT_PREFIX は # System context で始まる英語の文章で、「Agents SDK というマルチエージェントの仕組みの一部であること」と引き継ぎの説明が入っています。ドキュメントでは、引き継ぎを使う Agent の指示に入れることが勧められています。
handoff() で変えられること
| 引数 | 意味(ドキュメント) |
|---|---|
tool_name_override |
ツール名を変える(既定 transfer_to_<名前>) |
tool_description_override |
ツールの説明を変える |
on_handoff |
引き継いだときに呼ぶ関数 |
input_type |
引き継ぎのツールに引数を持たせる(理由・優先度など) |
input_filter |
引き継ぎ先に渡す履歴を絞る |
is_enabled |
引き継ぎを有効にするか(関数で切り替えも可) |
handoffs と「Agent をツールとして使う」の違い
| 方式 | 会話の持ち主 | 向いている場面 |
|---|---|---|
handoffs |
引き継ぎ先に移る(last_agent が変わる) |
窓口から担当者へ回す |
agent.as_tool() |
呼んだ側が持ち続け、結果だけ受け取る | 司令役が複数の専門家に聞いてまとめる |
設計の考え方は、関連記事「AIエージェントの設計パターン一覧|5つの型の違いと選び方」で扱っています。
guardrails:入力・出力を検査して止める
from pydantic import BaseModel
from agents import (Agent, Runner, GuardrailFunctionOutput, InputGuardrailTripwireTriggered,
RunContextWrapper, input_guardrail)
class HomeworkCheck(BaseModel):
is_homework: bool
reasoning: str
guard_agent = Agent(name="Guardrail check", instructions="宿題の代行依頼かどうか判定する。",
output_type=HomeworkCheck)
@input_guardrail
async def homework_guardrail(ctx: RunContextWrapper, agent: Agent, input) -> GuardrailFunctionOutput:
result = await Runner.run(guard_agent, input, context=ctx.context)
return GuardrailFunctionOutput(output_info=result.final_output,
tripwire_triggered=result.final_output.is_homework)
agent = Agent(name="Support", instructions="質問に答えます。", input_guardrails=[homework_guardrail])
try:
await Runner.run(agent, "数学の宿題を代わりに解いて")
except InputGuardrailTripwireTriggered as e:
print(e.guardrail_result.output.output_info)
# is_homework=True reasoning='宿題の代行依頼'
例外のメッセージは Guardrail InputGuardrail triggered tripwire でした。判定役の Agent には output_type があるので、リクエストに text.format("type": "json_schema"、"name": "final_output")が付き、応答は HomeworkCheck の型で返りました。
既定は「並列」:止めても本体のリクエストは送られていた
入力ガードレールは既定で本体の Agent と並列に動きます。 止まった場合でも、送られたリクエストを数えると違いがありました。
| 書き方 | 止まったときに送られたリクエスト |
|---|---|
@input_guardrail(既定 run_in_parallel=True) |
判定役 + 本体の Support の2つ |
@input_guardrail(run_in_parallel=False) |
判定役の1つだけ |
ドキュメントでは、ブロッキング(run_in_parallel=False)にすると、ガードレールが止めた場合に本体の Agent は動かず、トークンの消費もツールの実行も起きない、と説明されています。並列の既定では、上の表のとおり本体のリクエストも送られていました。ツールで外部に書き込む Agent や、高いモデルを使う Agent では、run_in_parallel=False を選ぶと安全です。
| ガードレール | どの Agent で動くか(ドキュメント) | 例外 |
|---|---|---|
| 入力 | 最初の Agent だけ | InputGuardrailTripwireTriggered |
| 出力 | 最終の答えを出した Agent だけ(常に完了後) | OutputGuardrailTripwireTriggered |
| ツール入力・出力 | ガードを付けた function tool の呼び出しごと | ToolInputGuardrailTripwireTriggered など |
出力ガードレールは、答えに特定の語が入っていたら止める、という形で確かめました。
@output_guardrail
async def no_greeting(ctx, agent, output: str) -> GuardrailFunctionOutput:
return GuardrailFunctionOutput(output_info={"len": len(output)},
tripwire_triggered="こんにちは" in output)
# OutputGuardrailTripwireTriggered: {'len': 16} | Guardrail OutputGuardrail triggered tripwire
output_type:答えを Pydantic の型で受け取る
class Weather(BaseModel):
city: str
temperature_c: int
summary: str
agent = Agent(name="Structured", instructions="天気をJSONで返す", output_type=Weather)
r = await Runner.run(agent, "東京")
print(repr(r.final_output)) # Weather(city='Tokyo', temperature_c=21, summary='晴れ')
final_output が文字列ではなく Weather のインスタンスになりました。構造化出力の方式の違いは、関連記事「LLMの構造化出力とJSON|4方式の違いとPydantic検証の実行結果」で扱っています。
Session:会話の履歴を自動で持つ
from agents import SQLiteSession
session = SQLiteSession("user-123") # ファイル名を渡さないとメモリ上
await Runner.run(agent, "私の名前は山田です", session=session)
await Runner.run(agent, "私の名前は?", session=session)
2回目のリクエストの input には、1回目の質問と答えが前に入っていました。
[
{"content": "私の名前は山田です", "role": "user"},
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "…"}]},
{"content": "私の名前は?", "role": "user"}
]
await session.get_items() は 4件(質問2つと答え2つ)でした。
tracing:既定でオン。止め方は3通り
何も設定しないと、実行の記録(トレース)が OpenAI のトレース用エンドポイントに送られます。 送り先を擬似エンドポイントに差し替えて中身を見ると、1回の実行で POST /v1/traces/ingest が1回あり、次のものが入っていました。
| object | 中身 |
|---|---|
trace |
workflow_name(RunConfig(workflow_name=...) の値) |
trace.span |
agent(Assistant)・response(response_id と usage)など |
ヘッダーは Authorization: Bearer <OPENAI_API_KEY と同じキー> と OpenAI-Beta: traces=v1 でした。
止め方ごとの、トレースの送信回数です。
| 止め方 | 範囲 | traces/ingest |
|---|---|---|
| 何もしない | 1回 | |
RunConfig(tracing_disabled=True) |
その実行だけ | 0回 |
set_tracing_disabled(True) |
プロセス全体 | 0回 |
環境変数 OPENAI_AGENTS_DISABLE_TRACING=1 |
プロセス全体 | 0回 |
from agents import set_tracing_disabled
set_tracing_disabled(True)
ドキュメントでは、Zero Data Retention(ZDR)の組織ではトレースは使えないこと、入力・出力などの内容を記録に含めるか(trace_include_sensitive_data、既定 True)を RunConfig か環境変数 OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA で変えられることが説明されています。
OpenAI 以外のモデルだけを使い、OPENAI_API_KEY が無い場合は、実行は成功し、最後に次の1行が出ました。
OPENAI_API_KEY is not set, skipping trace export
OpenAI 以外のモデル(Chat Completions 互換)を使う
Ollama や他社の OpenAI 互換 API など、Chat Completions の形の API には OpenAIChatCompletionsModel を使います。
from openai import AsyncOpenAI
from agents import Agent, Runner, OpenAIChatCompletionsModel, set_tracing_disabled
set_tracing_disabled(True)
client = AsyncOpenAI(base_url="http://localhost:11434/v1", api_key="dummy")
agent = Agent(name="Local", instructions="短く答えます。",
model=OpenAIChatCompletionsModel(model="llama3.2", openai_client=client))
print(Runner.run_sync(agent, "こんにちは").final_output)
送られたのは POST /v1/chat/completions で、instructions は system メッセージになりました。
{"model": "llama3.2",
"messages": [{"content": "短く答えます。", "role": "system"}, {"role": "user", "content": "こんにちは"}]}
Ollama の API は、関連記事「Ollamaの使い方|ローカルLLMのコマンド一覧とAPIの呼び方」で扱っています。
よくあるエラー・例外
| エラー・例外 | 原因 | 対処 |
|---|---|---|
OpenAIError: Missing credentials … |
OPENAI_API_KEY が無い |
環境変数を設定、または AsyncOpenAI(api_key=...) を渡す |
MaxTurnsExceeded: Max turns (3) exceeded |
ツール呼び出しが終わらない | 指示・ツールを見直す、max_turns を調整 |
InputGuardrailTripwireTriggered |
入力ガードレールが止めた | e.guardrail_result.output.output_info で理由を見る |
OutputGuardrailTripwireTriggered |
出力ガードレールが止めた | 同上 |
OPENAI_API_KEY is not set, skipping trace export |
キーなしでトレースが有効 | set_tracing_disabled(True) |
上の文面は、0.23.1 で再現したものです。
よくある質問
Q. 以前の Swarm との関係は? A. ドキュメントと README では、Agents SDK は実験的なプロジェクトだった Swarm を、本番向けに作り直したものと説明されています。
Q. run と run_sync のどちらを使いますか。
A. run_sync は中で async の run を動かす同期版です。FastAPI など、すでに async で動いているアプリの中では await Runner.run(...) を使います。
Q. エージェントという考え方そのものを知りたい。 A. ワークフローとの違いや作り方の全体は、関連記事「AIエージェントとは|仕組みとワークフローとの違い・作り方入門」で扱っています。
まとめ
Agentに 指示・ツール・引き継ぎ先を持たせ、Runnerでループを回す。送られるのは Responses API@function_toolは 型ヒントと docstring からスキーマを作る。strict のため 既定値のある引数も requiredhandoffsはtransfer_to_<名前>のツール。引き継ぐと 指示とツールが入れ替わり、履歴は全部渡る- 入力ガードレールは既定で並列。止めても本体のリクエストが送られていたので、
run_in_parallel=Falseを検討 - トレースは既定でオン。
RunConfig・set_tracing_disabled・OPENAI_AGENTS_DISABLE_TRACING=1で止まる
参考・出典
確認日はいずれも 2026年10月13日です。
- OpenAI Agents SDK ドキュメント:https://openai.github.io/openai-agents-python/
- OpenAI Agents SDK ドキュメント「Handoffs」:https://openai.github.io/openai-agents-python/handoffs/
- OpenAI Agents SDK ドキュメント「Guardrails」:https://openai.github.io/openai-agents-python/guardrails/
- OpenAI Agents SDK ドキュメント「Tracing」:https://openai.github.io/openai-agents-python/tracing/
- OpenAI Agents SDK ドキュメント「Tools」:https://openai.github.io/openai-agents-python/tools/
- PyPI「openai-agents」0.23.1:https://pypi.org/project/openai-agents/
- GitHub「openai/openai-agents-python」:https://github.com/openai/openai-agents-python
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「OpenAI Agents SDK入門|Agent・ツール・ハンドオフ」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。