AIエージェントの作り方|Pythonで動かす最小構成を図解

AIエージェントの作り方をPython・OpenAI API・Function Callingで学ぶ記事のアイキャッチ LLM

AIエージェントは、ユーザーの依頼に文章で答えるだけでなく、目的に応じてツールを選び、実行結果を確認しながら次の行動を決める仕組みです。

前回の「AIエージェントとは?生成AI・RAGとの違いを図解」では、生成AI・RAG・AIエージェント・MCPの役割を整理しました。今回は、PythonとOpenAI Responses APIを使い、ツールを1つだけ持つ最小のAIエージェントを実際に作ります。

完成するのは、「東京の天気を確認して、服装を提案して」という依頼を受けると、モデルが天気取得ツールを選び、Python関数の結果を使って回答するプログラムです。天気データは学習用の固定値にしているため、外部の天気APIは必要ありません。

スポンサーリンク

この記事で作るもの

今回のプログラムは、次の流れで動きます。

  1. ユーザーが目標を入力する
  2. モデルが、回答にツールが必要か判断する
  3. 必要なら、呼び出す関数名と引数を返す
  4. Python側が実際の関数を実行する
  5. 実行結果をモデルへ返す
  6. モデルが最終回答を作るか、次のツールを選ぶ

Pythonで作るAIエージェントの最小ループ

ここで重要なのは、モデル自身がPython関数を直接実行しているわけではないことです。モデルは「この関数を、この引数で呼びたい」と要求し、アプリ側のPythonコードが内容を検証して実行します。

スポンサーリンク

AIエージェントの最小構成

AIエージェントを小さく分解すると、最低限必要なのは次の5要素です。

要素 役割 今回の実装
目標 達成したいことを伝える ユーザーの入力文
モデル 次の行動を判断する gpt-5.6-terra
ツール 外部情報の取得や操作を行う get_weather()
ループ 実行結果を次の判断へ渡す run_agent()
停止条件 無限実行を防ぐ 最終回答または最大5ターン

生成AIへ1回質問して回答を受け取るだけなら、処理は「入力→出力」で終わります。エージェントでは、途中にツール呼び出しと観察結果が入り、必要に応じてループします。

事前準備

必要なもの

  • Python 3.10以降
  • OpenAI APIのアカウント
  • APIキー
  • ターミナルまたはコマンドプロンプト

OpenAI APIはChatGPTの月額プランとは別に課金・利用枠が管理されます。利用前にAPI側の設定を確認してください。

作業用フォルダを作る

mkdir ai-agent-sample
cd ai-agent-sample
python -m venv .venv

仮想環境を有効化します。

macOS・Linuxの場合:

source .venv/bin/activate

Windows PowerShellの場合:

.venv\Scripts\Activate.ps1

OpenAIのPythonライブラリをインストールします。

pip install --upgrade openai

APIキーを環境変数へ設定する

macOS・Linuxの場合:

export OPENAI_API_KEY="あなたのAPIキー"

Windows PowerShellの場合:

$env:OPENAI_API_KEY="あなたのAPIキー"

APIキーをPythonファイルへ直接書いたり、GitHubへコミットしたりしないでください。OpenAIのPythonライブラリは、OPENAI_API_KEYから自動でキーを読み取ります。

Step 1:エージェントが使うツールを作る

最初に、都市名を受け取って天気を返す関数を作ります。

from typing import Any

def get_weather(city: str) -> dict[str, Any]:
    """学習用の固定データを返す。実運用では天気APIなどに置き換える。"""
    sample_data = {
        "東京": {"weather": "晴れ", "temperature_c": 31},
        "大阪": {"weather": "くもり", "temperature_c": 29},
    }
    return sample_data.get(city, {"error": f"{city}のデータはありません"})

これは通常のPython関数です。AI専用の書き方ではありません。実運用では、この関数の中身を天気API、データベース検索、社内文書検索などへ置き換えられます。

Step 2:ツールの仕様をモデルへ伝える

次に、get_weather()の名前・説明・引数をJSON Schema形式で定義します。

TOOLS = [
    {
        "type": "function",
        "name": "get_weather",
        "description": "指定された都市の現在の天気と気温を取得する。",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "都市名。例: 東京",
                }
            },
            "required": ["city"],
            "additionalProperties": False,
        },
        "strict": True,
    }
]

この定義は「モデルが使えるツールの説明書」です。strict=TrueadditionalProperties=Falseを指定すると、定義していない引数が混ざりにくくなります。

ただし、形式が正しくても値が安全とは限りません。ファイルパス、金額、宛先、SQLなどを受け取る場合は、Python側で許可範囲を検証する必要があります。

Step 3:ツールを実行する振り分け処理を作る

モデルから返された関数名に応じて、実際のPython関数を呼び出します。

def call_tool(name: str, arguments: dict) -> dict:
    if name == "get_weather":
        return get_weather(arguments["city"])
    return {"error": f"未知のツールです: {name}"}

モデルが指定した関数名をそのまま任意コードとして実行してはいけません。あらかじめ許可した名前だけを、明示的に対応付けるのが安全です。

Step 4:判断と実行を繰り返すループを作る

エージェント本体です。モデルの出力にfunction_callが含まれていればツールを実行し、結果をfunction_call_outputとして追加します。

import json
from typing import Any

from openai import OpenAI

client = OpenAI()

def run_agent(user_input: str, max_turns: int = 5) -> str:
    input_list: list[Any] = [{"role": "user", "content": user_input}]

    for _ in range(max_turns):
        response = client.responses.create(
            model="gpt-5.6-terra",
            instructions=(
                "あなたは安全重視のアシスタントです。必要なときだけツールを使い、"
                "取得できない情報を推測で補わないでください。"
            ),
            tools=TOOLS,
            input=input_list,
        )

        input_list += response.output
        tool_calls = [item for item in response.output if item.type == "function_call"]

        if not tool_calls:
            return response.output_text

        for item in tool_calls:
            arguments = json.loads(item.arguments)
            result = call_tool(item.name, arguments)
            input_list.append(
                {
                    "type": "function_call_output",
                    "call_id": item.call_id,
                    "output": json.dumps(result, ensure_ascii=False),
                }
            )

    raise RuntimeError("最大ターン数に達しました。処理を停止します。")

input_list += response.outputでは、モデルが返した出力項目を次のリクエストへ引き継ぎます。推論モデルでツールを使う場合、関数呼び出しだけでなく、返された推論項目も含めて渡す必要があります。

call_idは、どのツール要求に対する結果なのかを対応付ける識別子です。別の呼び出し結果と混ざらないよう、そのまま返します。

Step 5:プログラムを実行する

同じファイルの末尾へ次のコードを追加します。

if __name__ == "__main__":
    answer = run_agent("東京の天気を確認して、今日の服装を1文で提案してください。")
    print(answer)

ファイル名をagent.pyとして保存し、実行します。

python agent.py

モデルは、依頼へ答えるためにget_weatherが必要だと判断すると、都市名「東京」を引数にしてツール呼び出しを返します。Python側が固定データを取得し、その結果をモデルへ返すと、最終的に次のような回答が出力されます。

東京は晴れで31℃です。通気性のよい半袖に、日差し対策の帽子を合わせるとよいでしょう。

文章は実行ごとに変わる可能性がありますが、天気と気温はツールの結果に基づきます。

完成コード

from __future__ import annotations

import json
from typing import Any

from openai import OpenAI

client = OpenAI()

def get_weather(city: str) -> dict[str, Any]:
    sample_data = {
        "東京": {"weather": "晴れ", "temperature_c": 31},
        "大阪": {"weather": "くもり", "temperature_c": 29},
    }
    return sample_data.get(city, {"error": f"{city}のデータはありません"})

TOOLS = [
    {
        "type": "function",
        "name": "get_weather",
        "description": "指定された都市の現在の天気と気温を取得する。",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "都市名。例: 東京"}
            },
            "required": ["city"],
            "additionalProperties": False,
        },
        "strict": True,
    }
]

def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
    if name == "get_weather":
        return get_weather(arguments["city"])
    return {"error": f"未知のツールです: {name}"}

def run_agent(user_input: str, max_turns: int = 5) -> str:
    input_list: list[Any] = [{"role": "user", "content": user_input}]

    for _ in range(max_turns):
        response = client.responses.create(
            model="gpt-5.6-terra",
            instructions=(
                "あなたは安全重視のアシスタントです。必要なときだけツールを使い、"
                "取得できない情報を推測で補わないでください。"
            ),
            tools=TOOLS,
            input=input_list,
        )

        input_list += response.output
        tool_calls = [item for item in response.output if item.type == "function_call"]

        if not tool_calls:
            return response.output_text

        for item in tool_calls:
            arguments = json.loads(item.arguments)
            result = call_tool(item.name, arguments)
            input_list.append(
                {
                    "type": "function_call_output",
                    "call_id": item.call_id,
                    "output": json.dumps(result, ensure_ascii=False),
                }
            )

    raise RuntimeError("最大ターン数に達しました。処理を停止します。")

if __name__ == "__main__":
    answer = run_agent("東京の天気を確認して、今日の服装を1文で提案してください。")
    print(answer)

なぜこれがAIエージェントなのか

このコードは単なるAPI呼び出しより一段進んでいます。アプリ側が「必ず天気関数を呼ぶ」と固定しているのではなく、モデルが依頼を読み、ツールが必要かを判断します。

一方で、自律性は限定的です。使えるツールは1つ、最大ターン数は5回、実行できる処理も固定データの読み取りだけです。最初のエージェントは、この程度に小さく作る方が、失敗原因とコストを追跡しやすくなります。

人間の確認を入れるべき操作

今回のget_weather()は読み取り専用なので、そのまま自動実行しても影響は限定的です。しかし、次のようなツールを追加する場合は、実行直前に人間の承認を入れるべきです。

  • メールやSNSへの送信
  • WordPressなどへの公開
  • 商品の購入や有料APIの大量実行
  • ファイルやデータの削除・上書き
  • カレンダー予約の確定
  • 顧客情報や機密情報へのアクセス

実装上は、call_tool()の中で危険度を判定し、承認済みでなければapproval_requiredを返す方法があります。モデルの判断だけで不可逆な操作まで進めないことが重要です。

よくある失敗と対策

ツールを呼ばずに推測で答える

instructionsへ「取得できない情報を推測で補わない」と書き、ツール説明を具体的にします。重要な処理では、回答がどのツール結果に基づくかもログへ残します。

存在しない引数が返る

strict=TrueadditionalProperties=Falseを使います。それでも値の意味や範囲はPython側で検証してください。

何度もツールを呼び続ける

最大ターン数、最大ツール回数、タイムアウトを設定します。今回のmax_turns=5は、無限ループを避ける最低限の停止条件です。

APIキーをコードへ書いてしまう

環境変数またはシークレット管理サービスを使います。公開リポジトリへ入ったキーは、削除するだけでなく無効化・再発行が必要です。

外部から取得した文章に操作を誘導される

Webページや文書には、エージェントへ意図しない命令を実行させる文章が含まれる可能性があります。外部コンテンツは「命令」ではなく「データ」として扱い、使えるツールと操作範囲を制限します。

Responses APIとAgents SDKのどちらを使う?

今回の例では、エージェントのループが見えるようにResponses APIを直接使いました。

選択肢 向いている場面
Responses API 仕組みを理解したい、小さな独自ループを作りたい、依存を少なくしたい
Agents SDK 複数エージェント、ハンドオフ、ガードレール、トレースなどをまとめて扱いたい

最初はResponses APIで1ツールのループを作り、必要性が明確になってからAgents SDKへ広げると、何が自動化されているかを理解しやすくなります。

実運用へ広げる順番

いきなり多くのツールを追加するのではなく、次の順番がおすすめです。

  1. 読み取り専用ツールを1つ追加する
  2. ツールの入出力をログへ保存する
  3. 期待するタスクを10〜20件用意して評価する
  4. 最大ターン数・タイムアウト・予算上限を設定する
  5. 外部へ影響する処理には承認を追加する
  6. 問題がなければツールを1つずつ増やす

エージェントの価値は、ツールの数ではなく、対象タスクを安定して完了できるかで判断します。

まとめ

Pythonで動くAIエージェントの最小構成は、モデル、ツール、実行ループ、停止条件の4点に整理できます。

  • モデルは、必要なツールと引数を選ぶ
  • Python側は、許可した関数だけを検証して実行する
  • ツール結果をモデルへ返し、次の判断につなげる
  • 最終回答または最大ターン数で停止する
  • 外部へ影響する操作の前には、人間の確認を入れる

まずは読み取り専用ツール1つから始めると、AIエージェントの基本構造を安全に理解できます。次の段階では、複数ツールの追加、会話状態の保存、承認フロー、評価方法を組み合わせていきます。

参考資料

コメント

タイトルとURLをコピーしました