Open Deep Research実践ガイド:Pythonで構築する自律型マルチステップWebリサーチAIエージェント

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

深夜2時15分の504 Gateway Timeout:従来のRAGが限界を迎えるとき

午前2時15分、ワーカークラスタから504 Gateway Timeoutが頻発し、PagerDutyのアラートが鳴り響きました。ログを追跡すると、いつもの原因が判明しました。マーケティング部門が競合20社の数値データ集約を求めるあまりに広範なプロンプトを投入していたのです。単一パイプラインのRAG構成が、膨大なWebページの生データを1回のLLM呼び出しに詰め込もうとした結果、コンテキストは128kトークンを超過し、並行スクレイピングスレッドも軒並みタイムアウトしていました。

本番環境の実績が示すように、基本的なRAGやZero-shot LLMでは本格的なリサーチ(Deep Research)の課題に対応できません。複雑な問いに対して、一般的なパイプラインはベクトル検索を1〜2回実行しただけで回答を生成してしまいます。その結果、データ不足からハルシネーション(幻覚)を起こすか、実用に耐えない浅薄なレポートを出力することになります。

Open Deep Researchモデルは、この課題を根本から解決します。LLMに即座の回答を強制するのではなく、プロセスを自律ループ(Autonomous Loop)へと分解します。

  • 計画立案(Planning):大きな問いをサブ課題に分解します(例:「AIチップの比較」というテーマを、アーキテクチャ、価格、推論性能の観点に分割)。
  • 再帰的検索(Recursive Search):検索エンジン経由でWebを照会し、内容を精読したうえで、データ不足を検知すると自動的に深掘り用クエリを生成します。
  • 統合・ファクトチェック(Synthesis & Fact-checking):複数ソースを相互検証し、ノイズを除去したうえで、参照元URL付きの標準Markdownレポートを出力します。

Pythonで自前のパイプラインを構築することで、実行フローを完全に制御できます。トークン消費のハードリミット設定やメモリリークの抑止が容易になり、高額なプロプライエタリAPIに依存する必要もなくなります。

実行環境のセットアップ

Ubuntu 22.04 / 24.04サーバーでは、システムパッケージとの競合を避けるため、専用の仮想環境に依存関係を隔離することを推奨します。

1. 仮想環境(Virtual Environment)の構築

# 作業ディレクトリの作成とPython 3.11+仮想環境のアクティベーション
mkdir -p /opt/open-deep-research && cd /opt/open-deep-research
python3 -m venv venv
source venv/bin/activate

# 基本ビルドツールのアップグレード
pip install --upgrade pip setuptools wheel

2. 必要なライブラリのインストール

ここでは、APIキー不要でWeb検索を実行できるduckduckgo-search、不要なスクリプトタグや広告、ナビゲーションバーを除去してクリーンなテキストを抽出するtrafilatura、そしてOpenAI、OpenRouter、またはセルフホストのvLLM/Ollamaと互換性の高いopenai SDKを採用します。

pip install openai duckduckgo-search trafilatura pydantic tenacity rich

Deep Research Agentの実装:モジュール別の詳細

無限ループによるクォータ枯渇を防ぐため、ソースコードは堅牢なWebスクレイピングモジュール、ループ制御エージェント、テスト実行スクリプトの3つに明確に分離します。

1. 安全な検索&テキスト抽出モジュール

tools.pyは検索とテキストクローリングを担当します。一時的なネットワーク障害やレートリミットを回避するため、Exponential Backoffによるリトライ処理を組み込んでいます。

import trafilatura
from duckduckgo_search import DDGS
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=6))
def search_web(query: str, max_results: int = 3) -> list[dict]:
    """レートリミット対策のリトライ機能を備えたDuckDuckGo Web検索"""
    results = []
    with DDGS() as ddgs:
        raw_results = ddgs.text(query, max_results=max_results)
        if raw_results:
            for item in raw_results:
                results.append({
                    "title": item.get("title", ""),
                    "url": item.get("href", ""),
                    "snippet": item.get("body", "")
                })
    return results

def fetch_clean_content(url: str, max_chars: int = 4000) -> str:
    """Webページを取得して本文テキストを抽出し、コンテキスト溢れを防ぐため文字数を制限する"""
    try:
        downloaded = trafilatura.fetch_url(url, timeout=10)
        if not downloaded:
            return ""
        text = trafilatura.extract(downloaded, include_links=False, include_comments=False)
        return text[:max_chars] if text else ""
    except Exception as err:
        return f"{url} の読み込みエラー: {err}"

2. 再帰ループを制御するAgentモジュール

agent.pyはオーケストレーターとしての役割を果たします。ドキュメントを読み込むたびに、モデルは自ら「現在のデータで十分な数値と論拠が揃っているか? 不足している場合、次に追加検索すべきキーワードは何か?」を自己評価します。

import os
import json
from openai import OpenAI
from tools import search_web, fetch_clean_content

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", "sk-mock-key"))

class DeepResearchAgent:
    def __init__(self, max_iterations: int = 3):
        self.max_iterations = max_iterations
        self.collected_evidence = []

    def plan_next_step(self, topic: str, history: str) -> dict:
        """収集済みデータを評価し、次のクエリを決定する"""
        prompt = f"""あなたは専門的なディープリサーチAIです。調査テーマ: "{topic}"
これまでに収集した情報:
{history}

分析を行い、以下のJSON形式に厳密に従って回答してください:
{{
  "is_sufficient": true/false,
  "reason": "情報が十分である理由、または不足しているデータの簡潔な説明",
  "next_query": "情報不足の場合に実行する次の検索クエリ(十分な場合は空文字)"
}}"""
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            response_format={"type": "json_object"},
            messages=[{"role": "user", "content": prompt}],
            temperature=0.2
        )
        return json.loads(response.choices[0].message.content)

    def run(self, topic: str) -> str:
        print(f"[*] リサーチ開始: {topic}")
        current_query = topic

        for iteration in range(1, self.max_iterations + 1):
            print(f"\n--- ループ {iteration}/{self.max_iterations}: '{current_query}' を検索中 ---")
            search_hits = search_web(current_query, max_results=2)
            
            for hit in search_hits:
                url = hit["url"]
                print(f" -> クロール中: {url}")
                page_text = fetch_clean_content(url)
                if page_text:
                    self.collected_evidence.append({
                        "source": url,
                        "query": current_query,
                        "snippet": page_text
                    })

            # 直近の収集エビデンスの要約をAgentに提示して検討させる
            evidence_summary = "\n".join([
                f"- 情報源 [{e['source']}]: {e['snippet'][:250]}..."
                for e in self.collected_evidence[-4:]
            ])

            decision = self.plan_next_step(topic, evidence_summary)
            if decision.get("is_sufficient") or iteration == self.max_iterations:
                print(f"[*] 終了条件に到達: {decision.get('reason')}")
                break
            
            current_query = decision.get("next_query", topic)

        return self.generate_report(topic)

    def generate_report(self, topic: str) -> str:
        """収集したすべてのエビデンスを統合してテクニカルレポートを生成する"""
        print("\n[*] 最終レポートを作成中...")
        raw_data = "\n\n".join([
            f"### 情報源: {e['source']}\n{e['snippet']}"
            for e in self.collected_evidence
        ])
        
        system_prompt = "あなたはテクニカルアナリストです。提供された引用情報のみに基づき、Markdown形式で詳細な統合レポートを作成してください。各論点には必ず対応する参照元URLを付記してください。"
        user_prompt = f"テーマ: {topic}\n\n調査データ:\n{raw_data}"

        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}
            ],
            temperature=0.3
        )
        return response.choices[0].message.content

3. テスト実行用スクリプト

main.pyは入力テーマを受け取り、ワーカーを起動してMarkdownファイルに結果を出力します:

import os
from agent import DeepResearchAgent

def main():
    if not os.getenv("OPENAI_API_KEY"):
        raise ValueError("環境変数 OPENAI_API_KEY が設定されていません!")

    topic = "AIエージェント向け外部メモリのアーキテクチャ:レイテンシとトークンコストにおけるMemGPTと従来型Vector DBの比較評価"
    agent = DeepResearchAgent(max_iterations=3)
    report = agent.run(topic)

    output_file = "deep_research_report.md"
    with open(output_file, "w", encoding="utf-8") as f:
        f.write(report)
    
    print(f"\n[✓] 完了!レポートを保存しました: {output_file}")

if __name__ == "__main__":
    main()

本番環境での運用:コストとサーバーリソースのコントロール

ローカル環境でスムーズに動作しても、本番環境で安定稼働するとは限りません。毎日数百件のタスクをワーカーに処理させる場合、以下の3つの安全策を講じる必要があります:

1. 実際の実行フローの検証

スクリプトを実行し、ターミナルで詳細ログを確認します:

export OPENAI_API_KEY="sk-your-actual-api-key"
python3 main.py

ターミナルには、Agentの思考プロセスがステップごとに表示されます。たとえば、反復1でMemGPTの定義を読み取った後、反復2でレイテンシのベンチマーク表が不足していると認識し、自動的にMemGPT vs Pinecone latency benchmarkというクエリを生成して追加検索を行い、最終的にdeep_research_report.mdを出力します。

2. 3つの障害点(Failure Points)とその対策

  • 反復回数の上限設定(Iteration Cap):必ずmax_iterations = 3(最大でも4)を設定してください。1回のループで約3,000〜5,000トークンを消費します。曖昧なテーマに対してエージェントの自律判断に任せきりにすると、15〜20回もループしてしまい、わずか1時間で10〜15ドルのAPI費用を浪費するリスクがあります。
  • テキストバッファのサイズ制限:URLごとにmax_chars = 4000(約1,000トークン)の上限を設けます。一部のニュースサイトには大量のスクリプトが埋め込まれており、Wikipediaのページは2万語を超えることもあります。そうしたページを制限なしで3つクロールするだけで、最初の反復で128kのコンテキストウィンドウが溢れてしまいます。
  • Systemdによるリソース制限:nohupやtmuxによるバックグラウンド手動実行は避けてください。同一VPS上で稼働するデータベースのクラッシュを防ぐため、メモリ上限を設定したsystemdサービスとして構成します:
# /etc/systemd/system/deep-research-worker.service
[Unit]
Description=Deep Research Agent Worker
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/open-deep-research
ExecStart=/opt/open-deep-research/venv/bin/python main.py
Restart=on-failure
RestartSec=10s
MemoryMax=1.5G

[Install]
WantedBy=multi-user.target

サービスを有効化した後は、systemctl status deep-research-workerでプロセスの稼働状況を確認し、journalctl -u deep-research-worker -fでリアルタイムログを監視します。これにより、検索エンジンによるBotブロックやCAPTCHAの発生を早期に検知できます。

Share: