NTT DATA TECH
⚾

AIエージェントが切り拓くレガシーサイト自動化:少年野球のグラウンド探しをbrowser-useとAWS Lambdaで解決する

に公開

はじめに

NTTデータの奥村です。昨今、様々な生成AIエージェントに関する取り組みが公開されています。今回は、その中でもブラウザ操作に特化したAIエージェントに関する取り組みの紹介をします。

取り組みの背景

私は都内でとある少年野球チームの運営に携わっており、特に練習用グラウンドの確保に頭を悩ませています。都内のグラウンド探しは、主に以下のような課題を抱えています。

  • 競争率の高さ: 野球ができる場所が限定的なのに対し、チーム数が多く、毎週末の確保が難しい。
  • システムの旧態依然さ: 多くの公共施設予約システムがレガシーな作りで、非常に使いにくい。
  • 自動化の壁:
    • 空き通知機能がない
    • APIが提供されておらず、プログラムからの情報取得が不可能

こうした状況のため、これまではチームの保護者が定期的にサイトを巡回する人海戦術で対応するしかありませんでした。

もちろん、SeleniumやPuppeteerといったライブラリでの自動化も考えられます。しかし、これらのスクリプトはサイトのUI変更に弱く、ボタンのIDがひとつ変わっただけで動かなくなります。その都度、エンジニアがコードを修正するのは持続的ではありません。

そこで私は「実行基盤の保守」と「操作手順の保守」を分けて考えてみることにしました。
AWS等の実行基盤はエンジニアが管理しますが、頻繁に発生するサイト側の仕様変更への追随(=操作手順の修正)は、ITスキルを持たない保護者でも行える状態にできないかと考えました。
専門知識がなくても自然言語(ふだんの言葉)で指示できるAIエージェントであれば、将来的に「プロンプトをGoogleドライブ上のテキストファイル等で管理し、保護者がそれを書き換えるだけでメンテナンス完了」という運用が可能になると考え、今回の取り組みを始めました。

構成概要

本システムは、極力コストを抑えて運用できることを重視し、以下のシンプルな構成としました。

役割 使用技術・サービス 補足
定期実行 Amazon EventBridge 毎日定時にLambdaを起動
実行基盤 AWS Lambda Pythonカスタムイメージで実行
ブラウザ操作 browser-use 自然言語で操作可能なOSSで、PlaywrightをLLM経由で制御してブラウザ操作を自動化
LLM Gemini 2.5 Flash browser-useのバックエンドとして利用
通知 Slack Incoming Webhookで結果を通知

費用の目安

本構成において、Gemini APIは無料利用枠(Free Tier)を使用しています。
また、AWS環境(EventBridge, Lambda, ECR)についても、無料利用枠を考慮しない従量課金前提で試算した場合でも、月額100円未満での運用が可能です。

構成要素の説明

AWS Lambda プログラム

カスタムイメージの実行環境を使用し、HTMLのパース等のロジックは組み込みません。「手順の実施」と「空きの判断」を生成AIに一任しています。
基盤モデルはレート制限(RPM, requests per minute)と性能のバランスから、Gemini 2.5 Flashを選択しました。
参考:Gemini APIのレート制限
なお、レート制限はリクエスト数だけでなくトークン使用量の影響も受ける場合があります。運用時は最新の公式ドキュメントを確認してください。
より軽量なモデルでは、なかなか想定通りの動作を行ってくれなかったり、Gemini 2.5 Proだとすぐにレート制限に引っかかってしまうといった課題があり、間を取ってFlashの利用に落ち着きました。
今回の仕組みで利用したLambdaのコードは下記です。

main.py
import os
import json
import asyncio
import requests
import glob
from datetime import datetime, timezone, timedelta
from browser_use import Browser
from browser_use.agent.service import Agent
from browser_use.llm.google import ChatGoogle

# --- 設定 ---
TARGET_URL = "{確認対象の施設予約サイトのURL}"

# --- プロンプト ---
# SYSTEMプロンプトは空にし、ライブラリのデフォルトの挙動を優先
SYSTEM = ""

# エージェントに実行させるタスクをステップバイステップで指示
# 【将来像】このTASKの内容を外部ファイル化し、非エンジニアが自然言語で修正できるようにする
TASK = f"""
# 命令
あなたはウェブページを操作するロボットです。以下のステップを、書かれている順番通りに、一つずつ正確に実行してください。
ステップ1: `{TARGET_URL}` を開いてください。
ステップ2: 「種目」というテキストの隣にあるドロップダウンを探し、「野球」を選択してください。
ステップ3: 「公園」というテキストの隣にあるドロップダウンを探し、「XXXXX」を選択してください。
ステップ4: 「検索」と書かれたボタンをクリックしてください。
ステップ5: ページが切り替わり、「週表示」のカレンダーが見えるようになるまで待ってください。
ステップ6: **今、画面に見えているカレンダーの範囲だけを調べてください。「次週」ボタンは絶対にクリックしないでください。**
   - まず、土曜日(青い日付)の列を探してください。
   - 次に、日曜日または祝日(赤い日付)の列を探してください。
   - それらの列の中に、数字が書かれているマスがあれば、その「日付」「時間」「書かれている数字」をすべてメモしてください。
ステップ7: メモした内容をすべて報告し、`done`アクションで終了してください。もし何も見つからなければ、「今週の表示範囲に空きはありませんでした。」と報告して終了してください。
"""

def notify_slack(title: str, content: str):
    """Slackに結果を通知する"""
    webhook_url = os.getenv("SLACK_WEBHOOK_URL")
    if not webhook_url:
        print("SLACK_WEBHOOK_URL が未設定です")
        return

    blocks = [
        {"type": "header", "text": {"type": "plain_text", "text": title}},
        {"type": "section", "text": {"type": "mrkdwn", "text": content}}
    ]
    payload = {"blocks": blocks}
    resp = requests.post(webhook_url, json=payload, timeout=10)
    resp.raise_for_status()

async def run_once() -> str:
    """AIエージェントのメイン処理"""
    # Lambdaコンテナ内のChromium実行パスを特定
    executable_path = None
    browser_path = os.environ.get("PLAYWRIGHT_BROWSERS_PATH", "/opt/playwright-browsers")
    chromium_dirs = glob.glob(f"{browser_path}/chromium-*")
    if chromium_dirs:
        chrome_path = f"{chromium_dirs[0]}/chrome-linux/chrome"
        if os.path.exists(chrome_path):
            executable_path = chrome_path
    if not executable_path:
        raise FileNotFoundError("Chromium executable not found")

    # ブラウザとLLM、エージェントを初期化
    browser = Browser(
        headless=True,
        args=["--disable-gpu", "--no-sandbox", "--disable-dev-shm-usage", "--single-process"],
        executable_path=executable_path,
        enable_default_extensions=False,
        wait_between_actions=7  # APIレート制限対策
    )
    llm = ChatGoogle(model="gemini-2.5-flash")
    agent = Agent(
        task=TASK,
        llm=llm,
        browser=browser,
        system_prompt=SYSTEM,
        use_vision=True
    )

    print("エージェントの処理を開始します...")
    history = await agent.run(max_steps=30)

    # エージェントが'done'以外で終了した場合(最大ステップ到達など)はエラー扱い
    if not history.is_done():
        final_result_text = history.final_result() or "タスク失敗(最大ステップ到達)"
        raise Exception(f"エージェントがタスクを完了できませんでした: {final_result_text}")

    print("エージェントがレポートを返しました。")
    return history.final_result() or ""

def lambda_handler(event, context):
    jst = timezone(timedelta(hours=9))
    now = datetime.now(jst).strftime("%Y-%m-%d %H:%M:%S %Z")

    try:
        report_text = asyncio.run(run_once())
        print(f"成功: {report_text}")
        notify_slack(
            "⚾ グラウンド空き状況レポート",
            f"実行時刻: {now}\n\n{report_text}"
        )
        return {"ok": True}

    except Exception as e:
        print(f"エラー: {e}")
        notify_slack(
            "🚨 グラウンド空き状況レポート:エラー発生",
            f"実行時刻: {now}\n\n処理中にエラーが発生しました。\n```\n{e}\n```"
        )
        return {"ok": False, "error": str(e)}

実装に当たり、特に工夫した3つのポイントを紹介します。

1. Lambda環境におけるChromium実行パスの特定

browser-useは、ブラウザ操作のバックエンドとしてPlaywrightというライブラリを利用しています。Playwrightは実際にブラウザ(今回はChromium)を起動し、Webページを操作する役割を担います。
今回のLambda環境では、browser-useがPlaywrightによってインストールされたChromiumの場所を自動で特定することができませんでした。その結果、browser-useは「Chromiumがない」と勘違いし、再度インストールを試みてエラーになっていました。
この問題を解決するため、DockerfileでChromiumのインストール先を固定し、そのパスをBrowser()の初期化時にexecutable_pathを明示的に指定することで、browser-useが確実にChromiumを認識できるようにしています。

2. APIレート制限を回避するための待機時間設定

今回はFree Tierでの利用ということもあり、連続して生成AIに問い合わせを行うと、すぐにRate Limitの上限に達してしまうため、Browserの初期化時にwait_between_actions=7を指定し、処理の間に7秒間の待ち時間を設けることでスロットリング対策を行っています。

3. 「空き無し」を正常に扱うための完了判定ロジック

処理が完了したかを判定するis_successful()メソッドは、エージェントが返すdoneアクションのsuccessフラグを見ています。しかし、デバッグの過程で、エージェントは「空きが見つからなかった」場合も「目的未達」と判断し、success: falseを返すことがあるとわかりました。
これはプログラムのエラーではなく、「空き無し」という正常系の結果です。
この挙動に対応するため、判定ロジックをis_successful()からis_done()に変更しました。is_done()はsuccessフラグの値に関わらず、エージェントが自らの意思でdoneアクションを実行してタスクを終了したかを判定します。
これにより、「空き無し」の正常な報告をエラーとして誤検知するのを防いでいます。

AWS Lambda 実行基盤

前述のLambdaのプログラムは、下記Dockerfileに基づき、ビルドを行いECRへカスタムイメージとして登録しておくことで、動作させることが可能です。

Dockerfile
# ベースイメージとしてAWS公式のPython 3.12 Lambdaイメージを使用
FROM public.ecr.aws/lambda/python:3.12

# Lambda関数コードとライブラリが配置される作業ディレクトリを設定
WORKDIR /var/task

# --- Playwright (Chromium) を動作させるための依存関係をインストール ---
# ヘッドレスブラウザが必要とするグラフィック関連ライブラリや日本語フォントを追加
RUN dnf -y install \
    alsa-lib atk at-spi2-atk fontconfig libXcomposite libXcursor \
    libXdamage libXext libXfixes libXi libXrandr libXScrnSaver \
    cups-libs pango gtk3 ipa-gothic-fonts \
    && dnf clean all

# --- browser-use と Playwright の設定 ---
# browser-useライブラリが実行時に設定ファイルなどを書き込むための一時ディレクトリ
ENV BROWSER_USE_CONFIG_DIR=/tmp/browseruse_config
# PlaywrightがChromiumをインストールする永続的な場所
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers

# --- アプリケーションコードとPythonライブラリのインストール ---
# app/ ディレクトリの中身をコンテナの /var/task/ にコピー
COPY app/. .

# pip自体をアップグレード
RUN pip install --upgrade pip

# requirements.txt に記載されたPythonライブラリをインストール
RUN pip install --no-cache-dir -r requirements.txt

# --- Chromiumブラウザのインストール ---
# Playwrightの機能を使って、コンテナイメージ内にChromiumを焼き付ける
RUN python -m playwright install chromium

# --- Lambda関数のエントリーポイント ---
# コンテナが起動したときに、main.py の lambda_handler 関数を実行するように指定
CMD ["main.lambda_handler"]
requirements.txt
browser-use==0.9.*
playwright==1.47.*
google-genai>=1.29,<2
requests==2.*

本取り組みの考察

ITスキルを持たない人でもメンテナンスでき、かつ極力コストがかからないようにということを意識して本システムを作ってみましたが、実際に使ってみると様々な課題が見えてきました。

動作が安定しない

生成AIを利用している以上、仕方がないのですが、前述のサンプルコードのプロンプトのようにかなり細かく操作手順を示しても、毎回同じ動作をしてくれるとは限らず、何回かに1回は処理が失敗してしまいます。理由は様々ですが、完全に表示されるまでに時間がかかるページや折りたたまれている箇所の中の操作などがある場合に、袋小路に陥りやすいです。
例えば、当初のプロンプトでは「土日祝日の空きを探してください」と大まかに指示していましたが、エージェントが勝手に「次週」ボタンをクリックしてしまい、意図しない範囲まで検索を広げてしまうことがありました。そのため、現在のプロンプトでは「今、画面に見えているカレンダーの範囲だけを調べてください。「次週」ボタンは絶対にクリックしないでください。」という強い制約を加えています。
また前述のサンプルコードのプロンプトでは細かく指示をしていませんが、アウトプットのフォーマットも、厳密に定義してもなかなか指示通りにはなりません。単純な文章で返してくるときもあれば、JSON形式で返してくるときもあり、その形式の不統一が原因で後続のSlack通知処理が失敗することが多々ありました。この問題に対し、プロンプト側で厳密なフォーマットを強制するのをやめ、エージェントからの返答をそのままテキストとして通知する方針に変更しました。「JSON」などのIT用語をプロンプトに含め、出力形式を強制するような書き方は、ITスキルを持たない人がメンテナンスするという当初の目的から外れてしまうためです。

基盤モデルの呼び出し上限に引っかかる

Free Tierの場合、Geminiはモデルによって1分当たりのレート制限(RPM)が異なり、本記事執筆時点では、Pro: 2 RPM、Flash: 15 RPMです。(数値は変更される可能性があるため、最新情報の確認をおすすめします。)そのため、AIができるだけ迷わない(=APIコール数が少なくなる)ような、詳細なプロンプトを用意する必要がありました。本当は最上位のProモデルを利用したかったのですが、1分当たりの最大呼び出し可能数が2で、さすがに利用回数が現実的でないため、採用を見送りました。Geminiの場合は、Google AI Studioの「Usage and Billing」メニューでモデルごとのRate Limitの情報を確認できます。

表示されているRPMなどの数字は、リアルタイムの情報ではなく、指定した期間での最大値が表示されています。

AWSのグローバルIPがブロックされている場合がある

この仕組みでいくつかの施設予約サイトの情報収集の自動化を試みたのですが、いくつかの施設予約サイトではAWSからの通信がブロックされていることがありました。そのため、クラウドからのアクセスが対象サイトにブロックされないか、事前の動作確認が重要だと感じました。なお、ブロックされていることは、同じプログラムをローカルPCから実行すると成功するのに対し、LambdaやEC2から実行した場合のみ接続がタイムアウトするという挙動で確認しました。これにより、サイト側でAWSのIPアドレス帯域を識別し、アクセスを制限している可能性が高いと判断しました。

現状のステータスと今後の展望

読者の皆様の中には「実際にチームでの運用は始まっているの?」「効果は出ているの?」と気になった方もいらっしゃるかもしれません。
正直に申し上げますと、まだチーム内での本格運用には至っていません。

考察で触れた「動作の不安定さ」に加え、以下の点が解決すべき課題として残っているためです。

  1. 通知内容のムラ: 生成AIからの出力が安定せず、人がパッと見て理解できる形式で通知されないことがまだあります。
  2. 通知ツールの壁: 現在は検証のためSlackを使用していますが、一般の保護者の方にとってSlackは利用経験が少ない場合があります。本来であれば保護者のグループLINEに通知すべきですが、SlackからLINE(Messaging API)への移行や、グループ内でのBot運用の検証がまだこれからの段階です。

まずはプロンプトの改善で安定性を高めつつ、通知先をLINEに変更することもこれから検討できれば良いなと考えています。

まとめ

browser-useを利用してブラウザ操作を行うAIエージェントを実際に試してみた事例を紹介させていただきました。Nova ActやGemini 2.5 Computer Useなど、ハイパースケーラーからもブラウザ操作に特化したAIエージェントに関連する発表は続々と公開されていますので、今後も引き続き注目していきたいと思います。
本記事が、皆さんの身の回りにある「ちょっとした不便」を、AIエージェントという新しい武器で解決するきっかけとなれば幸いです。
なお、過度なアクセスは操作先サイトに負荷をかける可能性がありますので、利用にあたっては十分ご注意ください。

仲間募集中です!

NTTデータ クラウド&データセンタ事業部では、以下の職種を募集しています。

  1. プライベートクラウドコンサル/エンジニア
  2. デジタルワークスペース構築/新規ソリューション開発におけるプロジェクトリーダー
  3. IT基盤(パブリッククラウド、プライベートクラウド)エンジニア
  4. パブリッククラウド/プライベートクラウドを用いた大規模プロジェクトをリードするインフラエンジニア

ソリューション紹介

  1. クラウドプロフェッショナルサービス/クラウドインテグレーションサービス/クラウドマネージドサービス/パートナークラウドサービス
  2. OpenCanvas
NTT DATA TECH
NTT DATA TECH
設定によりコメント欄が無効化されています