
OpenAI Agents SDK 入門 (3) - Tool ・ Handoff ・ Tracing
「OpenAI Agents SDK」の「Tool」「Handoff」「Tracing」についてまとめました。
前回
1. Tool
1-1. Tool
「Tool」は「Agent」にアクションを実行させるものです。データの取得、コード実行、外部API呼び出し、Computer useなどが含まれます。
「Agent SDK」には、次の3つの「Tool」のクラスがあります。
・Hosted tools
LLMサーバ上でAIモデルと一緒に実行されます。OpenAIは、検索機能、Web検索、「Computer use」を「Hosted tools」として提供しています。
・Function Calling
任意のPython関数をToolとして利用できます。
・Agents as tools
AgentをToolとして活用でき、Agent同士が直接引き継ぐことなく他のAgentを呼び出すことが可能になります。
1-2. Hosted tools
OpenAIResponsesModel を使用する際に、いくつかの組み込みToolが提供されています。
・WebSearchTool
Web検索を実行できるTool。
・FileSearchTool
ベクトルストアから情報を取得できるTool。
・ComputerTool
コンピューターの操作を自動化できるTool。
from agents import Agent, FileSearchTool, Runner, WebSearchTool
# Agent
agent = Agent(
name="Assistant",
tools=[ # Tool
WebSearchTool(),
FileSearchTool(
max_num_results=3,
vector_store_ids=["VECTOR_STORE_ID"],
),
],
)
async def main():
result = await Runner.run(agent, "私の好みと今日のサンフランシスコの天気を考慮すると、どのコーヒーショップに行くべきでしょうか?")
print(result.final_output)1-3. Function tools
Pythonの関数をToolとして使用できます。「Agents SDK」は「Tool」を自動的にセットアップします。
・Toolの名前はPython関数の名前になります(または任意の名前を指定可能)。
・Toolの説明は関数のドキュメント文字列(docstring)から取得されます(または任意の説明を指定可能)。
・関数の引数から自動的に入力スキーマが作成されます。
・各入力の説明は、関数のドキュメント文字列から取得されます(無効化も可能)。
Pythonの inspect モジュールを使用して関数のシグネチャを抽出し、griffe でdocstringを解析し、pydantic でスキーマを作成します。
import json
from typing_extensions import TypedDict, Any
from agents import Agent, FunctionTool, RunContextWrapper, function_tool
class Location(TypedDict):
lat: float
long: float
# Tool
@function_tool
async def fetch_weather(location: Location) -> str:
"""指定された場所の天気を取得します。
Args:
location: 天気を取得する場所。
"""
# 実際には、天気APIから天気情報を取得します。
return "sunny"
# Tool
@function_tool(name_override="fetch_data")
def read_file(ctx: RunContextWrapper[Any], path: str, directory: str | None = None) -> str:
"""ファイルの内容を読み取ります。
Args:
path: 読み取るファイルへのパス。
directory: ファイルを読み込むディレクトリ。
"""
# 実際には、ファイルシステムからファイルを読み取ります
return "<file contents>"
# Agent
agent = Agent(
name="Assistant",
tools=[fetch_weather, read_file], # Tool
)
for tool in agent.tools:
if isinstance(tool, FunctionTool):
print(tool.name)
print(tool.description)
print(json.dumps(tool.params_json_schema, indent=2))
print()1-4. Custom function tools
場合によっては、Python関数をToolとして使用したくないこともあります。その場合、FunctionTool を直接作成することができます。作成する際には、以下の情報を提供する必要があります。
・name
Toolの名前
・description
Toolの説明
・params_json_schema
引数のJSONスキーマ
・on_invoke_tool
非同期関数。コンテキストと引数をJSON文字列として受け取り、Toolの出力を文字列として返す必要があります。
from typing import Any
from pydantic import BaseModel
from agents import RunContextWrapper, FunctionTool
def do_some_work(data: str) -> str:
return "done"
class FunctionArgs(BaseModel):
username: str
age: int
async def run_function(ctx: RunContextWrapper[Any], args: str) -> str:
parsed = FunctionArgs.model_validate_json(args)
return do_some_work(data=f"{parsed.username} is {parsed.age} years old")
tool = FunctionTool(
name="process_user",
description="抽出されたユーザーデータを処理します。",
params_json_schema=FunctionArgs.model_json_schema(),
on_invoke_tool=run_function,
)1-5. Automatic Argument と Docstring Parsing
関数のシグネチャを自動的に解析してToolのスキーマを抽出し、Docstringを解析してToolや各引数の説明を取得します。以下の点に注意してください。
・シグネチャの解析は inspect モジュールを使用して行われます。型アノテーションを利用して引数の型を理解し、それに基づいて Pydantic モデルを動的に構築し、全体のスキーマを表現します。Python の基本型、Pydantic モデル、TypedDict など、多くの型をサポートしています。
・Docstringの解析には griffe を使用します。サポートされているドックストリング形式は Google、Sphinx、Numpy です。Docstringの形式は自動検出されますが、これはベストエフォートであり、function_tool を呼び出す際に明示的に設定することも可能です。また、use_docstring_infoを False に設定することで、ドックストリングの解析を無効化することもできます。
スキーマ抽出のコードは agents.function_schema. にあります。
1-6. Agents as tools
一部のワークフローでは、制御を引き渡すのではなく、中央のAgentが専門的なAgentのネットワークをオーケストレーション(調整)するようにしたい場合があります。このような場合、AgentをToolとしてモデル化することで実現できます。
from agents import Agent, Runner
import asyncio
# Spanish Agent
spanish_agent = Agent(
name="Spanish agent",
instructions="ユーザーのメッセージをスペイン語に翻訳します。",
)
# French Agent
french_agent = Agent(
name="French agent",
instructions="ユーザーのメッセージをフランス語に翻訳します。",
)
# Orchestrator Agent
orchestrator_agent = Agent(
name="orchestrator_agent",
instructions=(
"あなたは翻訳Agentです。与えられたToolを使用して翻訳を行います。"
"複数の翻訳を要求された場合は、関連するToolを呼び出します。"
),
tools=[
spanish_agent.as_tool(
tool_name="translate_to_spanish",
tool_description="ユーザーのメッセージをスペイン語に翻訳する。",
),
french_agent.as_tool(
tool_name="translate_to_french",
tool_description="ユーザーのメッセージをフランス語に翻訳する。",
),
],
)
# メイン
async def main():
result = await Runner.run(orchestrator_agent, input="スペイン語で「こんにちは、お元気ですか?」と言いましょう。")
print(result.final_output)1-7. Handling Error と Function Tool
@function_tool を使用して「Function Tool」を作成する際に、failure_error_function を渡すことができます。これは、Toolの呼び出しがクラッシュした場合にLLMにエラーレスポンスを提供する関数です。
・デフォルトでは(つまり、何も渡さなかった場合)、default_tool_error_function が実行され、LLM にエラーが発生したことを通知します。
・独自のエラー関数を渡した場合は、それが実行され、そのレスポンスが LLM に送信されます。
・明示的に None を渡した場合、Toolの呼び出しエラーはそのまま再スローされるため、ユーザーが処理する必要があります。このエラーには、モデルが無効な JSON を生成した場合の ModelBehaviorErrorや、コードがクラッシュした場合の UserError などが含まれます。
・FunctionTool オブジェクトを手動で作成する場合は、on_invoke_tool 関数内でエラーを処理する必要があります。
2. Handoff
2-1. Handoff
「Handoff」を使用すると、Agentが別のAgentにタスクを委任できます。これは、異なるAgentがそれぞれ特定の分野を専門としている場合に特に有用です。
例えば、カスタマーサポートアプリでは、注文状況、返金対応、FAQ などのタスクをそれぞれ専門的に処理するAgentが存在することがあります。
Handoffは LLM に対してToolとして表現されます。
そのため、例えば「Refund Agent」にHandoffする場合、そのToolは transfer_to_refund_agent という名前で呼び出されます。
2-2. Handoff の作成
すべてのAgentには handoffs パラメータがあり、これは Agent を直接受け取るか、カスタマイズ可能な Handoff オブジェクトを受け取ることができます。
「Agents SDK」には、Handoffを作成するための handoff() が用意されています。この関数を使用すると、Handoff先のAgentを指定できるほか、オプションでオーバーライド設定や入力フィルタを追加することも可能です。
・基本的な使い方
簡単なHandoffを作成する方法は次のとおりです。
from agents import Agent, handoff
billing_agent = Agent(name="Billing agent")
refund_agent = Agent(name="Refund agent")
triage_agent = Agent(name="Triage agent", handoffs=[billing_agent, handoff(refund_agent)])・handoff() によるHandoffのカスタマイズ
handoff() を使用すると、カスタマイズできます。
・agent
Handoffの受け渡し先となるAgent。
・tool_name_override
デフォルトでは Handoff.default_tool_name() が使用され、transfer_to_<agent_name> というTool名に解決されるが、これを上書き可能。
・tool_description_override
Handoff.default_tool_description() によるデフォルトのTool説明を上書き可能。
・on_handoff
Handoffが実行された際に呼び出されるコールバック関数。
・Handoffの開始時にデータ取得を実行する場合などに役立つ。
・この関数はAgentのコンテキストを受け取り、オプションで LLM が生成した入力も受け取ることができる。
・受け取る入力データは input_type パラメータによって制御される。
・input_type(オプション)
Handoffで受け取る入力の種類。
・input_filter
次のAgentが受け取る入力をフィルタリングするためのパラメータ。
from agents import Agent, handoff, RunContextWrapper
# Handoffのコールバック関数
def on_handoff(ctx: RunContextWrapper[None]):
print("Handoff called")
# Agent
agent = Agent(name="My agent")
# Handoff
handoff_obj = handoff(
agent=agent,
on_handoff=on_handoff,
tool_name_override="custom_handoff_tool",
tool_description_override="Custom description",
)2-3. Handoff の入力
特定の状況では、LLM がHandoffを呼び出す際にデータを提供する必要があります。
例えば、「Escalation Agent」へのHandoffを想定すると、その理由を提供することで記録を残せるようにしたい場合があります。
from pydantic import BaseModel
from agents import Agent, handoff, RunContextWrapper
class EscalationData(BaseModel):
reason: str
# Handoffのコールバック
async def on_handoff(ctx: RunContextWrapper[None], input_data: EscalationData):
print(f"Escalation agent called with reason: {input_data.reason}")
# Agent
agent = Agent(name="Escalation agent")
# Handoff
handoff_obj = handoff(
agent=agent,
on_handoff=on_handoff,
input_type=EscalationData,
)2-4. Input Filters
Handoffが発生すると、新しいAgentが会話を引き継ぎ、これまでの会話履歴全体を確認できる状態になります。この動作を変更したい場合は、input_filter を設定できます。input_filter は、HandoffInputData を受け取り、新しい HandoffInputData を返す関数です。
また、一般的なパターン(例えば、履歴からすべてのTool呼び出しを削除するなど)は、agents.extensions.handoff_filters にあらかじめ実装されています。
from agents import Agent, handoff
from agents.extensions import handoff_filters
# Agent
agent = Agent(name="FAQ agent")
# Handoff
handoff_obj = handoff(
agent=agent,
input_filter=handoff_filters.remove_all_tools,
)2-5. 推奨プロンプト
LLM がHandoffを正しく理解できるようにするため、AgentにHandoffに関する情報を含めることを推奨します。
agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX に推奨されるプレフィックスが用意されています。また、agents.extensions.handoff_prompt.prompt_with_handoff_instructions を使用すると、推奨データをプロンプトに自動的に追加できます。
from agents import Agent
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
# Agent
billing_agent = Agent(
name="Billing agent",
instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
<Fill in the rest of your prompt here>.""",
)3. Tracing
3-1. Tracing
「Agents SDK」には、Agentの実行中に発生するイベント(LLMの生成、Toolの呼び出し、Handoff、Guardrail、カスタムイベント)を包括的に記録する組み込みのTrace機能が含まれています。Tracesダッシュボードを使用すると、開発中および本番環境でワークフローをデバッグ、可視化、監視できます。
3-2. TraceとSpan
「Trace」は、「ワークフロー」の単一のエンドツーエンドの操作を表します。「Trace」は「Spans」で構成されます。「Trace」には以下のプロパティがあります。
・workflow_name
論理的なワークフローやアプリケーションの名前。
例:「コード生成」や「カスタマーサービス」など。
・trace_id
Traceの一意のID。指定しない場合は自動生成されます。
形式は trace_<32桁の英数字> である必要があります。
・group_id(オプション)
同じ会話内の複数のTraceを関連付けるためのグループID。
例えば、チャットスレッドIDを使用することができます。
・disabled
True に設定すると、Traceは記録されません。
・metadata(オプション)
Traceに付与できるメタデータ。
「Span」は、開始時間と終了時間を持つ操作を表します。「Span」には以下のプロパティがあります。
・started_at ・ ended_at
開始時刻と終了時刻のタイムスタンプ。
・trace_id
Spanが属するTraceを示すID。
・parent_id
Spanの親Spanを指すID(親Spanがある場合)。
・span_data
Spanに関する情報。
例: AgentSpanData はAgentに関する情報を含み、GenerationSpanData は LLM の生成に関する情報を含む。
3-3. デフォルトのTrace
デフォルトでは、以下の処理をTraceします。
・Runner.{run, run_sync, run_streamed}() 全体が trace() でラップ。
・Agentが実行されるたびに agent_span() でラップ。
・LLM の生成処理は generation_span() でラップ。
・Function Toolの呼び出しは、それぞれ function_span() でラップ。
・Guardrailsは guardrail_span() でラップ。
・Handoffsは handoff_span() でラップ。
デフォルトでは、Traceの名前は "Agent trace" となります。ただし、trace を使用することでこの名前を設定でき、RunConfig を使用すると名前やその他のプロパティを構成できます。
3-4. 上位レベルのTrace
場合によっては、複数回の run() 呼び出しを1つのTraceとしてまとめたいことがあります。その場合、コード全体を trace() でラップすることで実現できます。
from agents import Agent, Runner, trace
async def main():
agent = Agent(name="Joke generator", instructions="Tell funny jokes.")
with trace("Joke workflow"):
first_result = await Runner.run(agent, "Tell me a joke")
second_result = await Runner.run(agent, f"Rate this joke: {first_result.final_output}")
print(f"Joke: {first_result.final_output}")
print(f"Rating: {second_result.final_output}")3-5. Traceの作成
trace() 関数を使用してTraceを作成できます。Traceは 開始(start) と 終了(finish) が必要です。これを行う方法は2つあります。
(1) 推奨方法
trace(...) as my_trace のように コンテキストマネージャ として使用する。これにより、適切なタイミングで自動的にTraceが開始・終了されます。
(2) 手動で管理
trace.start() と trace.finish() を手動で呼び出す。
現在のTraceは Python の contextvar を使用して追跡 されるため、並行処理(concurrency)でも自動的に機能します。ただし、手動でTraceを開始・終了する場合は、start()/finish() に mark_as_current と reset_current を渡して、現在のTraceを更新する必要があります。
3-6. Spanの作成
さまざまな *_span() を使用してSpanを作成できます。
通常、Spanを手動で作成する必要はありませんが、カスタムSpan情報を追跡するための custom_span() 関数が用意されています。
・Spanは自動的に 現在のTraceの一部 となる。
・最も近い現在のSpanの下にネストされる
3-7. 機密データ
一部のSpanは、機密情報を含む可能性があります。
・generation_span() → LLM の入力/出力 を保存
・function_span() → 関数の入力/出力 を保存
これらのデータに機密情報が含まれる場合があります。
データの記録を無効にしたい場合は、RunConfig.trace_include_sensitive_data を使用して制御できます。
3-8. カスタムTraceプロセッサ
Traceの高レベルアーキテクチャは以下のようになっています。
(1) 初期化時 に、グローバルな TraceProvider を作成し、Traceの生成を管理する。
(2) TraceProvider を構成 し、BatchTraceProcessor を設定する。BatchTraceProcessor は、TraceやSpanをバッチ処理し、BackendSpanExporter に送信する。BackendSpanExporter は、SpanとTraceを OpenAI のバックエンドにバッチでエクスポートする。
デフォルトの設定を変更し、別のバックエンドにTraceを送信 したり、エクスポートの動作を変更 する方法は2つあります。
(1) add_trace_processor() を使用
追加のTraceプロセッサ を登録し、TraceやSpanを受け取るようにする。これにより、OpenAI のバックエンドに送信する + 独自の処理を追加 できる。
(2) set_trace_processors() を使用
デフォルトのプロセッサを置き換える(カスタムのTraceプロセッサを設定)。OpenAI のバックエンドにTraceを送信しなくなる ため、必要なら TracingProcessor を追加する必要がある。
3-9. 外部Traceプロセッサリスト
・Arize-Phoenix
・MLflow
・Braintrust
・Pydantic Logfire
・AgentOps
・Scorecard
・Keywords AI
・LangSmith
・Maxim AI
・Comet Opik
4. コンテキスト管理
4-1. ローカルコンテキスト
RunContextWrapper クラスと、その中の context プロパティによって表されます。動作のしくみは次の通りです。
(1) 任意の Python オブジェクトを作成します。一般的なパターンとして、dataclass や Pydantic オブジェクトを使用します。
(2) そのオブジェクトをさまざまな run メソッドに渡します。
(例:Runner.run(..., **context=whatever**))
(3) すべてのToolの呼び出し、ライフサイクルフックなどには RunContextWrapper[T] というラッパーオブジェクトが渡されます。ここで T はコンテキストオブジェクトの型を表し、wrapper.context を通じてアクセスできます。
特定のAgent実行に関わるすべてのAgent、Tool関数、ライフサイクルなどは 同じ型のコンテキストを使用する必要があります。
コンテキストの使用例としては、以下のようなものがあります。
・実行時のコンテキストデータ(例:ユーザー名 / UID などのユーザー情報)
・依存関係(例:ロガーオブジェクト、データフェッチャーなど)
・ヘルパー関数
import asyncio
from dataclasses import dataclass
from agents import Agent, RunContextWrapper, Runner, function_tool
@dataclass
class UserInfo:
name: str
uid: int
# Tool
@function_tool
async def fetch_user_age(wrapper: RunContextWrapper[UserInfo]) -> str:
return f"ユーザー {wrapper.context.name} は47歳です"
# メイン
async def main():
user_info = UserInfo(name="ジョン", uid=123)
# Agent
agent = Agent[UserInfo](
name="Assistant",
tools=[fetch_user_age],
)
# Agentの実行
result = await Runner.run(
starting_agent=agent,
input="ユーザーの年齢は何歳ですか?",
context=user_info,
)
print(result.final_output)
# メインの実行
if __name__ == "__main__":
asyncio.run(main())4-2. Agent / LLMコンテキスト
LLMが呼び出される際、参照できるデータは会話履歴のみ です。つまり、新しいデータを LLM に利用させたい場合、そのデータを 会話履歴に組み込む 必要があります。これを実現する方法はいくつかあります。
(1) Agentの指示に追加
これは「システムプロンプト」または「開発者メッセージ」とも呼ばれます。
システムプロンプトは、静的な文字列として設定することもできますし、コンテキストを受け取って動的に文字列を生成する関数として設定することも可能です。
常に有用な情報(例:ユーザー名や現在の日付)を提供するのに適した方法です。
(2) Runner.run 関数の呼び出し時に入力として追加
これは Agentの指示(instructions)を渡す方法と似ていますが、より階層が低いメッセージ を渡すのに適しています。
(3) Function Toolを介してデータを公開
オンデマンドで必要なデータを取得する場合に便利な方法 です。
LLM が特定のデータを必要としたタイミングで、Toolを呼び出して取得できます。
(4) RetrievalやWeb searchを活用
・Retrieval : ファイルやデータベースから関連データを取得
・Web Search : インターネット上の最新情報を取得