OpenAI Agents SDK の Tools を調べてみた

# はじめに

OpenAI Agents SDK が 2025 年 3 月 11 日にリリースされました。この記事では、[公式ドキュメント](https://openai.github.io/openai-agents-python/)を参考にして実際に動作を確認しながら、特に重要なポイントや興味深い点を深掘りしていきます。

検証に使用したライブラリのバージョンは openai-agents==0.0.6 です。


## OpenAI Agents SDK の紹介

別のページでご紹介します。

https://qiita.com/osamasao/items/a9f2dffa1f310c5ed8b7#openai-agents-sdk-%E3%81%AE%E7%B4%B9%E4%BB%8B


## Tools の紹介

Tools はエージェントがアクションを実行できるようにするものです。データの取得、コードの実行、外部 API の呼び出し、さらにはコンピューターの使用などが含まれます。OpenAI Agent SDK には、3 種類のツールがあります。

- 関数呼び出し(Function calling): 
    - ローカルの関数や機能の存在を LLM に伝えて、必要に応じて実行のための引数を生成させて実行する
- ツールとしてのエージェント(Agents as tools)
    - Agents をツールとして使用することで LLM をツールとして呼び出せる
    - LLM への問い合わせは Function calling 形式
- ホスト型ツール(hosted): OpenAI API のサーバー側で提供される機能を利用
    - [File search](https://platform.openai.com/docs/guides/tools-file-search): OpenAI へアップロードした文書のベクトル検索
    - [Web search](https://platform.openai.com/docs/guides/tools-web-search?api-mode=chat): インターネット検索
    - [Computer use](https://platform.openai.com/docs/guides/tools-computer-use): ローカルのブラウザ等を制御するためのコマンドを生成
    

関数呼び出し(Function calling)とツールとしてのエージェント(Agents as tools)は、OpenAI の [Function calling](https://platform.openai.com/docs/guides/function-calling) を呼び出すインタフェースです。Python 関数のデコレーターを使って Function calling のメタ情報を渡し、ローカルで実行すべき関数と引数を生成します。



## Function calling 用のインタフェース

[OpenAI Function calling](https://platform.openai.com/docs/guides/function-calling) は、モデルに対してローカル環境に存在する関数の概要を伝えて、必要に応じてモデル側から関数を使うリクエストを受けるインタフェースです。ローカル側の関数を定義することで、外部ツールや API と連携し、OpenAI モデルがその結果を活用できるようになります。質問応答、データ抽出、タスク自動化などが可能です。

いくつかのインタフェースがあります。

- Python の関数に function_tool デコレーターをつけるだけ
- FunctionTool インスタンスを作成して利用
- Agents を Tool として利用(Agents as tools)

Agents as tools は Handoff と似ています。Handoff は仕事を移譲しますが、Agents as tools は仕事を移譲せずに結果だけを受け取るのが大きな違いです。

### function_tool デコレーター

Agents のドキュメントの最初のサンプルコードに function_tool があります。ちょっとアレンジしてみました。

デコレーターの引数の description_override はオプションの引数です。必須ではありません。docstring から読み取らせることも可能です。

```python:src/tools.py
import datetime

from agents import Agent, Runner, function_tool

@function_tool(description_override="指定の都市の天気を返す関数です")
def get_weather(city: str) -> str:
    return f"{city} の天気は雪です"


@function_tool(description_override="現在時刻を返す関数です")
def get_datetime(city: str) -> str:
    return str(datetime.datetime.now())


agent = Agent(
    name="俳句エージェント",
    instructions="常に俳句で応答してね",
    model="o3-mini",
    tools=[get_weather, get_datetime],
)

result = Runner.run_sync(agent, "沖縄の天気知ってる？")
print(result.final_output + "\n")

result = Runner.run_sync(agent, "昨日って何月何日だったっけ？")
print(result.final_output + "\n")
```

以下のように実行します。

```bash
$ python src/tools.py
雪の島に  
冬の息吹あり  
沖縄かな

夜露染み  
昨日は静かに  
三月二十三日
```

OpenAI プラットフォーム の Trace を確認すると、該当の API リクエストの Functions に以下のエントリがあります。

現在時刻の取得関数の情報です。

```json:get_datetime()
{
  "name": "get_datetime",
  "description": "現在時刻を返す関数です",
  "strict": true,
  "parameters": {
    "properties": {
      "city": {
        "title": "City",
        "type": "string"
      }
    },
    "required": [
      "city"
    ],
    "title": "get_datetime_args",
    "type": "object",
    "additionalProperties": false
  }
}
```

指定の都市の天気を返す関数の情報です。

```json:get_weather()
{
  "name": "get_datetime",
  "description": "現在時刻を返す関数です",
  "strict": true,
  "parameters": {
    "properties": {
      "city": {
        "title": "City",
        "type": "string"
      }
    },
    "required": [
      "city"
    ],
    "title": "get_datetime_args",
    "type": "object",
    "additionalProperties": false
  }
}
```

デコレーターの function_tool() の引数で関数の情報を変更することができます。

別の例です。

```python:src/tools_function_tool.py
import json
from pprint import pprint

from agents import Agent, FunctionTool, RunContextWrapper, function_tool
from typing_extensions import Any, TypedDict


class Location(TypedDict):
    lat: float
    long: float


@function_tool
async def fetch_weather(location: Location) -> str:
    """与えられたロケーションの天気を取得

    Args:
        location: 天気を取得したいロケーション
    """
    # 外部 API などで外部の天気を取得したつもり
    weather = "濃霧"
    return weather


@function_tool(name_override="fetch_data")
def read_file(
    ctx: RunContextWrapper[Any], path: str, directory: str | None = None
) -> str:
    """ファイルの中身を読む

    Args:
        path: ファイル名
        directory: ファイルのディレクトリ名
    """
    # 例えばこんな感じでファイルを読んだつもり
    # import json
    # from pathlib import Path
    # contents = json.load(open(Path(directory) / path, "r"))
    contents = """{"diary": "今日は豚肉と野菜で炒めものを作ったよ"}"""
    return contents


agent = Agent(
    name="アシスタント",
    tools=[fetch_weather, read_file],
)

for tool in agent.tools:
    if isinstance(tool, FunctionTool):
        print(f"## tool name: {tool.name}")
        pprint(tool)
        print(
            json.dumps(tool.params_json_schema, indent=2, ensure_ascii=False)
        )
        print()

```

実行結果です。description に関数の docstring を反映しているので LLM に理解しやすい書き方にするのが良さそうです。


```
## tool name: fetch_weather
FunctionTool(name='fetch_weather',
             description='与えられたロケーションの天気を取得',
             params_json_schema={'$defs': {'Location': (別途表示のため省略),
                                 'required': ['location'],
                                 'title': 'fetch_weather_args',
                                 'type': 'object'},
             on_invoke_tool=<function function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool at 0x737387bfc7c0>,
             strict_json_schema=True)
```


```json:fetch_weather の FunctionTool.params_json_schema
{
  "$defs": {
    "Location": {
      "properties": {
        "lat": {
          "title": "Lat",
          "type": "number"
        },
        "long": {
          "title": "Long",
          "type": "number"
        }
      },
      "required": [
        "lat",
        "long"
      ],
      "title": "Location",
      "type": "object",
      "additionalProperties": false
    }
  },
  "properties": {
    "location": {
      "description": "天気を取得したいロケーション",
      "properties": {
        "lat": {
          "title": "Lat",
          "type": "number"
        },
        "long": {
          "title": "Long",
          "type": "number"
        }
      },
      "required": [
        "lat",
        "long"
      ],
      "title": "Location",
      "type": "object",
      "additionalProperties": false
    }
  },
  "required": [
    "location"
  ],
  "title": "fetch_weather_args",
  "type": "object",
  "additionalProperties": false
}
```


ローカルファイルを読む関数の FunctionTool です。


```
FunctionTool(name='fetch_data',
             description='ファイルの中身を読む',
             params_json_schema=(別途表示のため省略),
             on_invoke_tool=<function function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool at 0x737387bfd760>,
             strict_json_schema=True)
```

OpenAI Function calling でリクエスト時に利用する引数のスキーマです。関数の docstring を反映しています。


```json:read_file の FunctionTool.params_json_schema
{
  "properties": {
    "path": {
      "description": "ファイル名",
      "title": "Path",
      "type": "string"
    },
    "directory": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "description": "ファイルのディレクトリ名",
      "title": "Directory"
    }
  },
  "required": [
    "path",
    "directory"
  ],
  "title": "fetch_data_args",
  "type": "object",
  "additionalProperties": false
}
```


### FunctionTool クラスの利用

FunctionTool クラスからインスタンスを作ります。function_tool デコレーターは利用しません。


```python:src/tools_custom.py
from pprint import pprint
from typing import Any

from agents import FunctionTool, RunContextWrapper
from pydantic import BaseModel


def do_some_work(data: str) -> str:
    return "完了"


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} は {parsed.age} 歳です。")


tool = FunctionTool(
    name="処理する担当者",
    description="抽出されたユーザーを処理します",
    params_json_schema=FunctionArgs.model_json_schema(),
    on_invoke_tool=run_function,
)

pprint(tool)

```

```
$ python -m src.tools_custom
FunctionTool(name='処理する担当者',
             description='抽出されたユーザーを処理します',
             params_json_schema={'properties': {'age': {'title': 'Age',
                                                        'type': 'integer'},
                                                'username': {'title': 'Username',
                                                             'type': 'string'}},
                                 'required': ['username', 'age'],
                                 'title': 'FunctionArgs',
                                 'type': 'object'},
             on_invoke_tool=<function run_function at 0x75de4e3b5f80>,
             strict_json_schema=True)
```

function_tool デコレーターを使うよりも FunctionTool クラスを使うほうがインタフェースの分離が良いかもしれません。できることは同じようなので、お好みで使い分けかなとも思います。params_json_schema が異様にシンプルになってるのがちょっと心配です。

お気に入りの FunctionTool をストックすると使い回せて良いかもしれません。


### Agents as tools


次は Agent を tools として利用する方法です。普通に Agent を定義しておいて、あとから tools に変換します。この機能により LLM をバックエンドにした tools が定義できます。高機能な Agent を tools にすることもできます。

:::note info
Agent.as_tool() の tool_name は利用可能な文字に制約があります。英数大文字小文字、ハイフン、アンダースコアが利用可能です。OpenAI API の Function calling の仕様によるものだと思います。日本語にしたら以下のようなエラーが発生しました。

> openai.BadRequestError: Error code: 400 - {'error': {'message': "Invalid 'tools[0].name': string does not match pattern. Expected a string that matches the pattern '^[a-zA-Z0-9_-]+$'.", 'type': 'invalid_request_error', 'param': 'tools[0].name', 'code': 'invalid_value'}}
:::



```python:src/tools_agents_as_tools.py 
import asyncio
from pprint import pprint

from agents import Agent, Runner

spanish_agent = Agent(
    name="スペイン語エージェント",
    instructions="ユーザーのメッセージをスペイン語にして",
)

french_agent = Agent(
    name="フランス語エージェント",
    instructions="ユーザーのメッセージをフランス語にして",
)

orchestrator_agent = Agent(
    name="オーケストレーターエージェント",
    instructions=(
        "あなたは翻訳エージェントです。"
        "与えられた tools を使って翻訳してください。"
        "複数の翻訳を求められたら関連する tools を呼び出してください。"
    ),
    tools=[
        spanish_agent.as_tool(
            tool_name="translate_to_spanish",
            tool_description="ユーザーのメッセージをスペイン語に翻訳します",
        ),
        french_agent.as_tool(
            tool_name="translate_to_franch",
            tool_description="ユーザーのメッセージをフランス語に翻訳します",
        ),
    ],
)


async def main():
    result = await Runner.run(
        orchestrator_agent,
        input="'こんにちは、あるいはこんばんは' をスペイン語とフランス語と英語で言うと？",
    )
    print(result.final_output)
    pprint(orchestrator_agent)


if __name__ == "__main__":
    asyncio.run(main())
```

実行結果です。

```
$ python src/tools_agents_as_tools.py
- **スペイン語**: Hola, o buenas noches.
- **フランス語**: Bonjour ou bonsoir
- **英語**: Hello, or good evening
```

オーケストレーター Agent の文字列表現です。

```
Agent(name='オーケストレーターエージェント',
      instructions='あなたは翻訳エージェントです。与えられた tools を使って翻訳してください。複数の翻訳を求められたら関連する '
                   'tools を呼び出してください。',
      handoff_description=None,
      handoffs=[],
      model=None,
      model_settings=ModelSettings(temperature=None,
                                   top_p=None,
                                   frequency_penalty=None,
                                   presence_penalty=None,
                                   tool_choice=None,
                                   parallel_tool_calls=False,
                                   truncation=None,
                                   max_tokens=None),
      tools=[FunctionTool(name='translate_to_spanish',
                          description='ユーザーのメッセージをスペイン語に翻訳します',
                          params_json_schema={'additionalProperties': False,
                                              'properties': {'input': {'title': 'Input',
                                                                       'type': 'string'}},
                                              'required': ['input'],
                                              'title': 'translate_to_spanish_args',
                                              'type': 'object'},
                          on_invoke_tool=<function function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool at 0x76382d510900>,
                          strict_json_schema=True),
             FunctionTool(name='translate_to_franch',
                          description='ユーザーのメッセージをフランス語に翻訳します',
                          params_json_schema={'additionalProperties': False,
                                              'properties': {'input': {'title': 'Input',
                                                                       'type': 'string'}},
                                              'required': ['input'],
                                              'title': 'translate_to_franch_args',
                                              'type': 'object'},
                          on_invoke_tool=<function function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool at 0x76382d511c60>,
                          strict_json_schema=True)],
      input_guardrails=[],
      output_guardrails=[],
      output_type=None,
      hooks=None,
      tool_use_behavior='run_llm_again')
```

OpenAI プラットフォームの Trace 情報です。スペイン語エージェントとフランス語エージェントが順番に呼ばれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126852/b4c19c08-5498-4fcf-9500-c69092cd24a9.png)

フランス語エージェントの動作詳細です。メッセージのなかで翻訳したい箇所だけを切り出された状態で「フランス語エージェント」が処理している様子がわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126852/059532bb-12c4-4cd1-92ce-cc77cfae24c5.png)



## Hosted tools

OpenAI API のサービス側で提供される機能を利用した tools です。2025年3月25日時点では、以下が利用可能です。

- [Web search](https://platform.openai.com/docs/guides/tools-web-search?api-mode=chat)
    - インターネット検索
    - 内部的に LLM 用のヘッドレスブラウザが動いているらしい
- [Computer use](https://platform.openai.com/docs/guides/tools-computer-use)
    - ローカルのブラウザ等を制御するためのコマンドを生成
    - 安全に利用するためには、LLM 用のブラウザ環境やサンドボックスを構築する必要がある
- [File search](https://platform.openai.com/docs/guides/tools-file-search)
    - OpenAI へアップロードした文書のベクトル検索
    - 事前に文書をアップロードする必要あり

手軽に利用できる Web search を試してみます。Computer use と File search は大掛かりになるので、今回はパスして別の記事で書こうと思います。

:::note warn
Computer use は、ローカルコンピューターを動かす必要があるので情報漏えいや外部への攻撃などを前提にした安全な環境を作ることが重要です。エージェントの開発環境には API KEY や認証情報等の機密情報があることが多いですし、踏み台として他のサイトを攻撃したりすれば責任を問われます。安全なサンドボックスの作り方は、開発動作環境からの絶縁のレベルやかけられるコストに応じて様々な方法が考えられます。(あぁ、仕事っぽくて萎える・・・)
:::

:::note
File search は、文書を OpenAI にアップロードする必要があります。手軽に RAG や検索 DB が組めるのは良いですが、OpenAI に依存し過ぎで実際の構成で使いにくい印象があります(個人の乾燥です)。機会があったら調べて見ようかなという温度感です。
:::


### Web search の利用

WebSearchTool クラスのインスタンスを tools に登録して使います。コンテキスト長を選ぶと検索結果から取得する情報量がかわります。

```python:src/tools_websearch.py
from pprint import pprint

from agents import Agent, Runner, WebSearchTool

qiita_search = Agent(
    name="Qiita から情報を取得するエージェント",
    tools=[WebSearchTool(search_context_size="high")],
)

result = Runner.run_sync(
    qiita_search,
    "Qiita で OpenAI Agents SDK について書いている記事を5件教えて",
)

print(result.final_output)
pprint(qiita_search)
```

実行結果です。いい感じに Qiita の記事だけを検索できました。

```
$ python -m src.tools_websearch
QiitaでOpenAI Agents SDKに関する記事を5件ご紹介します。

1. **15分でわかる！AIエージェント開発の最新フレームワーク OpenAI Agents SDK #Python**  
   OpenAI Agents SDKのコア概念であるエージェント、ハンドオフ、ガードレール、トレーシングについて解説し、Pythonでの実装例を紹介しています。 ([qiita.com](https://qiita.com/Kumacchiino/items/51a8ffee98eeb4f8d0c6?utm_source=openai))

2. **OpenAIの新ツール 『Responses API』 & 『Agents SDK』 でAIエージェント開発が進化！💡 #自動化**  
   OpenAIが発表したResponses APIとAgents SDKの詳細な解説と、プロフェッショナルなサンプルコードを紹介しています。 ([qiita.com](https://qiita.com/robonikki/items/104605a03b569c18580a?utm_source=openai))

3. **OpenAI Agents SDKをDatabricksで動かしてみる #エージェント**  
   OpenAI Agents SDKをDatabricks上で実行する方法を解説し、Hello Worldサンプルや引き継ぎサンプル、関数サンプルを紹介しています。 ([qiita.com](https://qiita.com/taka_yayoi/items/3161baddac7c745dcef9?utm_source=openai))

4. **🚀 OpenAI Agents SDKでOllama, Claude, Geminiのモデルを利用する #claude**  
   OpenAI Agents SDKでOllama、Claude、Geminiなどのモデルを利用する方法を解説し、統一API設計や複数モデルの統合について説明しています。 ([qiita.com](https://qiita.com/kitfactory/items/5a169e66b7205f83b555?utm_source=openai))

5. **OpenAIのAgent SDKで遊ぶ。３人寄れば文殊の知恵？ #ChatGPT**  
   OpenAIのAgent SDKを使って、複数のエージェントが協力してアイデアを出し合い、最適なアイデアを選ぶシステムを構築する方法を紹介しています。 ([qiita.com](https://qiita.com/garyo/items/18f3ad6b4f434f4835d3?utm_source=openai))

これらの記事は、OpenAI Agents SDKの活用方法や実装例を詳しく解説しており、AIエージェント開発の参考になるでしょう。 
```

Agent の文字列表現です。シンプルですね。instructions すらありません。


```
Agent(name='Qiita から情報を取得するエージェント',
      instructions=None,
      handoff_description=None,
      handoffs=[],
      model=None,
      model_settings=ModelSettings(temperature=None,
                                   top_p=None,
                                   frequency_penalty=None,
                                   presence_penalty=None,
                                   tool_choice=None,
                                   parallel_tool_calls=False,
                                   truncation=None,
                                   max_tokens=None),
      tools=[WebSearchTool(user_location=None, search_context_size='high')],
      input_guardrails=[],
      output_guardrails=[],
      output_type=None,
      hooks=None,
      tool_use_behavior='run_llm_again')

```



## エラーハンドリング

Function Tools でのエラーハンドリングです。Hosted は対象外のようです。

### function_tool デコレーターの場合

failure_error_function を渡すことができます。これは、ツール呼び出しがクラッシュした場合に LLM にエラー応答を提供する関数です。

- デフォルトでは (つまり、何も渡さない場合)
    - エラーが発生したことを LLM に通知する default_tool_error_function() が実行される
- 独自のエラー関数を渡す
    - default_tool_error_function() の代わりにその関数が実行され、応答が LLM に送信される
- 明示的に None を渡す
    - ツール呼び出しエラーが再度発生し処理が必要になります
        - モデルが無効な JSON を生成した場合は ModelBehaviorError
        - コードがクラッシュした場合は UserError

### FunctionTool の場合

on_invoke_tool 関数内でエラーを処理する必要があります。


## まとめ

OpenAI Agents SDK の Tools は、エージェントに多様なアクション実行能力を与える強力な機能です。データの取得からコード実行、外部 API 連携、さらにはインターネット検索やブラウザ操作まで、コードで実現可能なあらゆるタスクをエージェントに委ねることができます。これにより、例えば、IFTTT などのホームオートメーションサービスと連携させ、音声による家電操作（Alexaのような機能）を実現することも可能です。

Tools は、そのバックエンドの仕組みによって大きく2つに分類できます。

- バックエンドが LLM の Function calling なもの
    - function_tool デコレーター: 手軽
    - FunctionTool クラス: 柔軟
    - Agent を tools に変換: バックエンドに LLM や Tools が利用可能
- バックエンドが OpenAI サービスに依存するもの
    - WebSearch: 手軽にインターネット検索が可能
    - FileSearch: OpenAI Assistants のベクトルストレージが利用可能
    - ComputerTool: サンドボックスのコンピューターを操作可能

使い方としては、オーケストレーション Agent を代表にして、他の Agent や Tools がサブタスクを行うような仕組みが合いそうだなと思いました。お気に入りの Agent や Tools をストックしておくとできることが増えそうです。
今後、一般的で汎用的な Agent や Tools を集めた便利なライブラリが登場するような予感がします。ライブラリ、事例、ドキュメントなども含めて OpenAI Agents SDK のエコシステムが充実するのが楽しみです。

最後までお読みいただきありがとうございました！