OpenAI Agents SDK で「人類はもう生成AIに勝てないと痛感したDeep Researchの使い方」を自動化してみた

# はじめに

@zazen_inu さんの記事「[人類はもう生成AIに勝てないと痛感したDeep Researchの使い方](https://qiita.com/zazen_inu/items/5dc2ea32aa4de18c02c7)」が凄いので OpenAI Agents SDK で自動化してみました。


# 目的

- @zazen_inu さんのメソッドを楽に実行したい
- OpenAI Agents SDK で実装してみてフレームワークのクセとか使い方を理解したい

# 方法

愚直に実装します。

## 処理フロー

それぞれの Agent の背後にはテキスト生成モデルがいます。用途によってモデルの種類を決めます。賢さ、インターネットの情報の調査能力、コストなどを意識します。

```mermaid
graph TD
    A@{ shape: stadium, label: "はじめ" } --> B;
    subgraph "調査目的の推敲"
        B@{ shape: lean-r, label: "ユーザー: 背景と目的を入力"} --> C[Agent: 修正案を生成];
        C --> D@{ shape: lean-r, label: "ユーザー: 修正有無を入力"};
        D --> D1{修正判定}
        D1 -- 修正あり --> B;
    end
    subgraph "AI によるレポート生成"
        D1 -- 修正なし --> E[Agent: 背景と目的を清書];
        E --> F[Agent: 仮のレポートを作成];
        F --> G[Agent: アブストラクトを作成];
        G --> H[Agent: 詳細調査とレポート作成];
    end
    H --> I@{ shape: lean-r, label: "完成したレポートを表示"};
    I --> J@{ shape: stadium, label: "おわり" };
```

### ライブラリ(パッケージ)

主なライブラリと用途を書きます。

- [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/)
    - Agent クラスでテキスト生成エージェントを定義
    - Runner クラスで Agent を非同期で呼び出し
    - OpenAI API Platform の Trace と MLflow へトレース情報を出力
- [MLflow](https://mlflow.org/)
    - 各エージェントの動作の記録(トレース情報)
    - OpenAI API プラットフォームでも見えるけどローカルにも残したいので利用
- pytest
    - Agent の単体テスト用

### ソフトウェア構造

Python で書きます。

- Agent モジュール
    - 5 つのテキスト生成エージェントとプロンプトを定義
    - エージェントごとにモジュールを分割
- Manager モジュール
    - メイン処理を担当します
    - ユーザーとの入出力と Agent モジュールとの橋渡し
    - 呼び出しフローの管理
- main モジュール
    - 設定ファイルの読み込み
    - Manager モジュールを呼び出し
    - ツールのエントリポイント
- テストモジュール
    - 5 つのテキスト生成エージェントの単体テスト
        - 通しで実行すると一回の処理に時間もお金もかかるし

### 参考実装

公式リポジトリの「Financial Research Agent Example」を参考にしました。
特に Manager と Agent のアーキテクチャはそのまま利用させてもらってます。

https://github.com/openai/openai-agents-python/tree/main/examples/financial_research_agent

# 結果

できました〜。とっても長いのでリンクを貼ります。

## 参考出力

https://raw.githubusercontent.com/u-masao/pocket-researcher/refs/tags/v0.1.1/EXAMPLE.md

## リポジトリ(インストール方法、使い方、ソースコード)

https://github.com/u-masao/pocket-researcher/tree/v0.1.1

# 考察

- 楽になったか？
    - コピペを繰り返すのに比べて非常に楽になりました！
- OpenAI Agents SDK のクセとか使い方
    - 単純な使い方だと問題なし
    - 条件分岐の判断を LLM に任せるともっと試行錯誤がありそう
    - Human in the loop というか人間の確認の実装
        - 今回は Runner を分けたけどもっとエレガントな実装したい
- API コスト
    - ちゃんと計算してないですが、開発時の API コストは約 $1 でした
    - 参照した記事では o1-pro を推奨していましたが単価が高いのでおじけづいてます
        - トークン数の集計とか金額計算の機能をつけたら o1-pro にも挑戦してみたい
- 利用モデル
    - o1-pro はコストが怖くて試せず
    - o3-mini はゴリゴリ出力するのでいい感じ
    - gpt-4o-mini は出力が寂しくて動作確認用というレベル
- プロンプト
    - @zazen_inu さんのプロンプトを色々と修正させてもらいました
        - Agent のプロンプトはシステムプロンプトの扱いになるのでそのあたりを修正
    - gpt-4o-mini だと時々英語になるので「日本語指定」を入れました
- さらなる利便性
    - コンソールよりも Gradio とかでリッチにしたい
        - ユーザーの入力のブラッシュアップを簡素化
        - 処理の進捗状況を可視化
    - パッケージ化して PyPI から落とせるようにしたい
    - レポートが出来上がったらどこかに通知してほしい
- コードで気になるところ
    - ユーザーインタフェースの抽象化がうまくない
        - Gradio をサポートするときに考える
    - Manager から Agent の挙動に介入できない
        - モデル名を環境変数で渡すのがいまいち
        - Agent のジェネレーターを作ったほうが良いかも
        - Tools、Handoff、Guardrail とか使うことも考えると複雑になるので介入できないことにするのもアリかも
        - なるべくコードにしないで YAML にしたい気もするが・・・
    - プロンプトの管理方法
        - 個々の Agent の仕事はそれぞれに任せるなら個別モジュールで管理
        - 全体の見通しを良くするなら集中管理もあり
    - モジュール名が冗長だし長い
        - `__init__.py` をちゃんと使おう


# まとめ

@zazen_inu さんに感謝です。あと OpenAI Agents SDK は頑張れば色々とできそうと感じました。今後も色々と試してみます。


# 備考


## メインのコード

### エージェントの例

アブストラクトから論文(レポート) を生成するエージェントです。

```python:src/pocket_researcher/agents/paper_maker_from_abstract_agent.py
"""
このモジュールはアブストラクトから論文を書くエージェントを定義します。
"""

import os

from agents import Agent

# アブストラクトから、論文を書かせる
PAPER_FROM_ABSTRACT_PROMPT = """
### 役割
あなたは有能な研究者です。
### 依頼
ユーザーは論文アブストラクトを提示します。アブストラクトから本論文を完成してください。
### 条件
- 推定値で研究を作成しているため、先行研究から得られる値は代入してください。
- 先行研究などの参考文献が得られることを期待しています。信頼性が高いものを選んでください
- 新規性の検討等、論文としてのロバスト性を高めてください
- 言語は日本語とします
"""

paper_maker_from_abstract_agent = Agent(
    name="アブストラクトから論文を作るエージェント",
    instructions=PAPER_FROM_ABSTRACT_PROMPT,
    model=os.getenv("PAPER_FROM_ABSTRACT_MODEL", "o3-mini"),
)
```

### アプリケーションのメイン処理

ユーザーとのやり取り、エージェントとのやり取りを定義します。

```python:src/pocket_researcher/manager.py
import logging
import sys
import textwrap

import mlflow
from agents import Runner, gen_trace_id, trace

from pocket_researcher.agents.abstract_maker_agent import abstract_maker_agent
from pocket_researcher.agents.background_objective_maker_agent import (
    background_objective_maker_agent,
)
from pocket_researcher.agents.clear_background_and_objective_agent import (
    clear_background_and_objective_agent,
)
from pocket_researcher.agents.full_paper_maker_from_objective_agent import (
    full_paper_maker_from_objective_agent,
)
from pocket_researcher.agents.paper_maker_from_abstract_agent import (
    paper_maker_from_abstract_agent,
)

mlflow.set_experiment("PocketResearcher")
mlflow.openai.autolog()

USAGE = """
    > 考えたいことや調べたいことを入力してください。
    > 論文の形式でリサーチ結果を表示します。
"""


class PocketResearchManager:
    """
    デモ用の最小限のリサーチマネージャー。
    """

    def __init__(self) -> None:
        self.logger = logging.getLogger(__name__)
        # logging.basicConfig(level=logging.WARNING)

    async def run(self) -> None:
        print(textwrap.dedent(USAGE))

        with trace("リサーチサービス", trace_id=gen_trace_id()):
            objective_draft = (
                await self._input_background_and_objective_draft()
            )
            objective = await self._clear_background_and_objective(
                objective_draft
            )
            full_paper_temp = await self._make_full_paper_from_objective(
                objective
            )
            abstract = await self._make_abstract_from_full_paper(
                full_paper_temp
            )
            paper = await self._make_paper_from_abstract(abstract)

        return paper

    async def _read_multiple_input(self) -> str:
        """標準入力を読み取る関数"""
        print(">    (入力終了は Ctrl+D または Ctrl+Z を入力)")
        query = sys.stdin.readlines()
        return "".join(query)

    async def _input_background_and_objective_draft(self) -> str:
        """
        ユーザーから背景と目的のラフを受け取り要約と評価をする。
        標準入力を読み取り完了確認を行う。
        """
        # 終了フラグ
        conferm_background_and_objective = False

        # 終了フラグが立つまでループ
        while not conferm_background_and_objective:

            # 背景と目的のラフを入力
            print("> 考えたいことや調べたいことの背景と目的を書いてください:")
            query = await self._read_multiple_input()
            print(f"> 背景と目的:\n\n```\n{query}\n```")
            print("> 回答作成中")

            # レビュー生成
            result = await Runner.run(
                background_objective_maker_agent,
                input=query,
            )
            print(
                f"> 背景と目的と指摘事項:\n\n```\n{result.final_output}\n```"
            )

            # 人間に確認するループ
            while True:
                command = input(
                    "> この後の処理はノンストップです。\n"
                    "> 背景と目的を確定して論文を作成しますか？(yes/no):"
                ).lower()

                # 入力チェック
                if len(command) == 0 or command not in ["yes", "no"]:
                    continue

                # yes で終了フラグを立てる
                if command == "yes":
                    conferm_background_and_objective = True

                # 人間に確認するループを終了
                break

        # 結果を出力
        print(f"> 背景と目的のドラフトが確定しました。\n\n```\n{query}\n```")
        # 最後にユーザーが入力した文章を返す
        return query

    async def _clear_background_and_objective(self, draft: str) -> str:
        """
        入力されたテキストを「背景と目的」として清書する。
        """

        print("> 背景と目的を清書中")

        # 背景と目的を清書中
        result = await Runner.run(
            clear_background_and_objective_agent,
            input=draft,
        )
        print(
            f"> 背景と目的を清書しました。\n\n```\n{result.final_output}\n```"
        )

        return result.final_output

    async def _make_full_paper_from_objective(self, objective: str) -> str:
        """
        入力された「背景と目的」から仮の論文を作成する。
        """

        print("> 仮の論文を作成中")

        # 仮の論文を作成中
        result = await Runner.run(
            full_paper_maker_from_objective_agent,
            input=objective,
        )

        print(f"> 仮の論文を作成しました。\n\n```\n{result.final_output}\n```")
        return result.final_output

    async def _make_abstract_from_full_paper(self, full_paper: str) -> str:
        """
        入力された「仮の論文」からアブストラクトを作成する。
        """

        print("> アブストラクトを作成中")

        # アブストラクトを作成
        result = await Runner.run(
            abstract_maker_agent,
            input=full_paper,
        )

        print(
            f"> アブストラクトを作成しました。\n\n```\n{result.final_output}\n```"
        )
        return result.final_output

    async def _make_paper_from_abstract(self, abstract: str) -> str:
        """
        アブストラクトから論文を作成する。
        """

        print("> 論文を作成中")

        # 論文を作成中
        result = await Runner.run(
            paper_maker_from_abstract_agent,
            input=abstract,
        )

        print(f"> 論文を作成しました。\n\n```\n{result.final_output}\n```")
        return result.final_output
```

# 謝辞

### 2025/03/29 追記

参照元の記事でこの取り組みをご紹介いただきました。感謝です！！

https://qiita.com/zazen_inu/items/5dc2ea32aa4de18c02c7#%E8%BF%BD%E8%A8%9820250329

### 2025/03/30 追記

@koki_develop さんの「[Qiita デイリーいいね数ランキング【自動更新】](https://qiita.com/koki_develop/items/fa223e1fa0ab057a54bc)」でご紹介いただきました。なんと 1 位です :tada:
いいねやストックをして下さった皆さまに感謝です :relaxed:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126852/6c9d4fc4-e223-44e1-97c4-ffbe28ad8167.png)

@kai_kou さんの「[Qiita週間ストック数ランキング【自動更新】](https://qiita.com/kai_kou/items/180a91fd88dbbbd746f6)」でご紹介いただきました。なんと 11 位です :fire:

Qiita の[トレンド](https://qiita.com/trend)では、なんと 4 位に入ってます :hushed:

### 2025/03/31 追記

Qiita の[トレンド](https://qiita.com/trend)で 1 位に :cactus:

いいねを 100 個も頂きました！！

### 2025/04/02 追記

@yonaka15 さんの「[Node-REDで実現するAI論文生成フロー - Gemini 2.5 ProとOpenAI o3-miniを活用](https://qiita.com/yonaka15/items/21ba8f8ac6c0c7afa94a)」でこの記事をご紹介頂きました。感謝です！！
Node-RED で自動化していてフローを可視化できるのが大きなメリットだと思いました。

### 2025/04/03 追記

週間ユーザーランキングで8位に入りました。興味を持っていただいた皆さまのお陰です！