DEV Community

Cover image for Qwen 3.7 Plusを使ったPC操作エージェントの作り方
Akira
Akira

Posted on • Originally published at apidog.com

Qwen 3.7 Plusを使ったPC操作エージェントの作り方

Qwen 3.7 Plusは、スクリーンショットからクリックすべきピクセル座標を返すベンチマーク「ScreenSpot Pro」で79.0点を獲得しました。この能力を使うと、チャットモデルを「画面を見て、次の操作を決め、実行する」コンピューター使用エージェントとして構成できます。この記事では、Python、Qwen 3.7 Plus、Playwrightを使って、ブラウザ操作エージェントを実装します。

今すぐApidogを試す

実装するのは、スクリーンショットを取得し、Qwen 3.7 Plusに目標と一緒に渡し、clicktypescrolldoneのような構造化アクションを受け取り、Playwrightで実行するループです。モデルの背景を先に確認したい場合は、Qwen 3.7 Plusの概要をご覧ください。APIリクエスト形式は、Qwen 3.7 Plus APIガイドでマルチモーダルペイロードを扱っています。この記事では、途中でApidogを使ってモデル呼び出しも検証します。

要点

コンピューター使用エージェントは、次のループで実装できます。

  1. 画面をスクリーンショットする
  2. スクリーンショットと目標をQwen 3.7 Plusに送る
  3. モデルからJSON形式の次アクションを受け取る
  4. Playwrightなどのドライバーでアクションを実行する
  5. 新しいスクリーンショットで結果を確認する

重要なのは、モデルそのものよりも周辺実装です。具体的には、ステップ数の上限、座標スケーリング、トークンコストの制御、誤クリックを前提にしたサンドボックス化が必要です。

コンピューター使用エージェントの処理フロー

エージェントの基本構造は、以下の4ステップです。

  1. 認識: 現在の画面またはページのスクリーンショットを取得する
  2. 決定: スクリーンショットと目標をモデルに渡し、次の操作を取得する
  3. 実行: clicktypescrollなどを自動化ドライバーで実行する
  4. 確認: 次のスクリーンショットを取得し、目標に近づいたか確認する

モデルが担当するのは「決定」です。スクリーンショット取得、実行、検証、停止条件はアプリケーション側で制御します。

            <video src="https://assets.apidog.com/blog-next/2026/06/V1tXD8Bnm5DAtobB.mp4" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" loop="" autoplay="" muted="" playsinline="" preload="metadata"></video>


                    <svg xmlns="http://www.w3.org/2000/svg">
                        <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"></path>
                    </svg>





                        <svg xmlns="http://www.w3.org/2000/svg">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"></path>
                        </svg>


                        <svg xmlns="http://www.w3.org/2000/svg">
                            <rect width="7" height="22" rx="1.5" ry="1.5"></rect>
                            <rect width="7" height="22" rx="1.5" ry="1.5"></rect>
                        </svg>

                    <span>0:00</span>

                        /<span>1:26</span>


                    1×

                        <svg xmlns="http://www.w3.org/2000/svg">
                            <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"></path>
                        </svg>


                        <svg xmlns="http://www.w3.org/2000/svg">
                            <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"></path>
                        </svg>
Enter fullscreen mode Exit fullscreen mode

Qwen 3.7 Plusが適している理由

Qwen 3.7 Plusがこの用途に向いている理由は主に3つです。

  • GUI理解に強く、曖昧な説明ではなくクリック可能な座標を返せる
  • GUI操作とCLI操作を組み合わせたワークフローに使える
  • 入力トークン100万あたり0.40ドルで、多数の視覚呼び出しを行うエージェントループに使いやすい

テキスト中心のモデルとの比較は、Qwen 3.7 Plus vs Maxの比較を参照してください。

Qwen 3.7 Plusの比較画像

決定ステップ:JSONアクションを返させる

まず、モデルの出力を実行しやすい形式に制限します。

自由文で「右上のボタンを押してください」と返されると、自動化ドライバーでは扱いにくくなります。代わりに、次のようなJSONのみを許可します。

{"action": "click", "x": 100, "y": 200}
{"action": "type", "text": "hello"}
{"action": "scroll", "dy": 500}
{"action": "done", "reason": "目標を達成しました"}
Enter fullscreen mode Exit fullscreen mode

以下は、スクリーンショットをBase64化してQwen 3.7 Plusへ送り、次アクションをJSONとして取得する最小実装です。

import os
import json
import base64
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["DASHSCOPE_API_KEY"],
    base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
)

SYSTEM = """あなたはGUIエージェントです。スクリーンショットと目標を見ます。
一つのJSONアクションのみで返答してください:
{"action": "click", "x": <int>, "y": <int>}
{"action": "type", "text": "<string>"}
{"action": "scroll", "dy": <int>}
{"action": "done", "reason": "<string>"}
座標は与えられたスクリーンショットのピクセルです。"""

def next_action(goal, png_bytes):
    b64 = base64.b64encode(png_bytes).decode()

    resp = client.chat.completions.create(
        model="qwen3.7-plus",
        messages=[
            {"role": "system", "content": SYSTEM},
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": f"Goal: {goal}"},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/png;base64,{b64}"
                        },
                    },
                ],
            },
        ],
    )

    return json.loads(resp.choices[0].message.content)
Enter fullscreen mode Exit fullscreen mode

モデルIDは変更される可能性があります。デプロイ前にModel Studioのドキュメントで正確なモデルIDを確認してください。

Playwrightでブラウザ操作ループを作る

Playwrightを使うと、実際のブラウザを自動操作できます。

ここで重要なのは、スクリーンショットの解像度とブラウザのビューポートを一致させることです。たとえばビューポートを1280x800に固定し、そのままスクリーンショットを渡せば、モデルが返したx, y座標をそのままpage.mouse.click()に渡せます。

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)

    page = browser.new_page(
        viewport={"width": 1280, "height": 800}
    )

    page.goto("https://example.com")

    goal = "価格ページを開き、最も安いプランを見つける"

    for step in range(15):                 # ステップ数の上限
        shot = page.screenshot()           # 1280x800 PNG
        action = next_action(goal, shot)

        print(step, action)

        if action["action"] == "done":
            break

        if action["action"] == "click":
            page.mouse.click(action["x"], action["y"])

        elif action["action"] == "type":
            page.keyboard.type(action["text"])

        elif action["action"] == "scroll":
            page.mouse.wheel(0, action["dy"])

        page.wait_for_timeout(800)         # UIが安定するのを待つ

    browser.close()
Enter fullscreen mode Exit fullscreen mode

これで、実際にページを見ながら1ステップずつ操作するエージェントになります。

同じパターンはデスクトップアプリにも応用できます。その場合は、Playwrightの代わりにデスクトップ自動化ライブラリを使い、OSウィンドウのスクリーンショットをモデルに渡します。

JSONパース失敗に備える

実運用では、モデルがまれにJSONではなく説明文を返す可能性があります。そのため、json.loads()を直接呼ぶだけでなく、最低限のリトライ処理を入れてください。

def safe_next_action(goal, png_bytes, retries=1):
    for attempt in range(retries + 1):
        try:
            action = next_action(goal, png_bytes)

            if "action" not in action:
                raise ValueError("action field is missing")

            return action

        except Exception as e:
            if attempt == retries:
                raise

            # 実装を簡単にするため、ここでは同じ入力で再試行する
            # 本番では「JSONのみで返してください」と明示した追加プロンプトにしてもよい
            print(f"Retry because model output was invalid: {e}")
Enter fullscreen mode Exit fullscreen mode

さらに堅牢にするなら、許可されたactionのみを受け入れるバリデーションも追加します。

ALLOWED_ACTIONS = {"click", "type", "scroll", "done"}

def validate_action(action):
    name = action.get("action")

    if name not in ALLOWED_ACTIONS:
        raise ValueError(f"Unsupported action: {name}")

    if name == "click":
        if not isinstance(action.get("x"), int):
            raise ValueError("click.x must be int")
        if not isinstance(action.get("y"), int):
            raise ValueError("click.y must be int")

    if name == "type":
        if not isinstance(action.get("text"), str):
            raise ValueError("type.text must be string")

    if name == "scroll":
        if not isinstance(action.get("dy"), int):
            raise ValueError("scroll.dy must be int")

    return action
Enter fullscreen mode Exit fullscreen mode

ループ内では次のように使います。

action = validate_action(safe_next_action(goal, shot))
Enter fullscreen mode Exit fullscreen mode

座標スケーリングの扱い

スクリーンショットサイズと実際の操作対象サイズが一致していれば、座標変換は不要です。

page = browser.new_page(viewport={"width": 1280, "height": 800})
shot = page.screenshot()
Enter fullscreen mode Exit fullscreen mode

この場合、モデルが返した{"x": 640, "y": 400}は、そのまま中央付近のクリックになります。

一方、スクリーンショットを縮小してモデルに送る場合は、返された座標を元サイズに戻す必要があります。

def scale_point(x, y, screenshot_size, viewport_size):
    shot_w, shot_h = screenshot_size
    view_w, view_h = viewport_size

    return {
        "x": int(x * view_w / shot_w),
        "y": int(y * view_h / shot_h),
    }
Enter fullscreen mode Exit fullscreen mode

たとえば640x400に縮小した画像をモデルに送り、実ブラウザが1280x800なら、返された座標を2倍にしてクリックします。

コストと信頼性を制御する

コンピューター使用エージェントでは、スクリーンショットがコストの大きな部分になります。画像はトークン化されるため、幅1280pxの画像を何度も送るループでは、ステップ数に比例してコストが増えます。

コストと信頼性を改善するには、次を実装してください。

  • 画像を小さくする

    モデルが読める範囲でダウンスケールします。

  • 関連領域だけをクロップする

    モーダル、フォーム、ナビゲーションなど、判断に必要な領域だけを送ります。

  • ステップ数に上限を設ける

    例ではrange(15)で最大15ステップに制限しています。

  • 各アクション後に再確認する

    クリック後に新しいスクリーンショットを取得し、次の判断をモデルに任せます。

  • 同じアクションの繰り返しを検出する

    同じ座標クリックや同じスクロールが続く場合は停止します。

詳細は、エージェントトークンコストの削減と、エージェントワークフローの配線も参考になります。

エージェントが停止した場合の対処

よくある失敗は3つです。

1. モデルがJSONではなく文章を返す

対処方法:

  • JSONパースに失敗したら1回だけ再試行する
  • システムプロンプトで「JSONのみ」と明記する
  • アクションスキーマを検証する

2. クリック位置がずれる

対処方法:

  • スクリーンショット解像度とビューポートを一致させる
  • 縮小画像を使う場合は座標をスケーリングする
  • クリック後に画面変化がない場合、同じ座標を繰り返さず再判断する

3. 進行せずにループする

対処方法:

  • 最大ステップ数を設定する
  • 直近のアクション履歴を保持する
  • 同じ操作が続いたら停止し、人間がスクリーンショットを確認する

簡単な履歴チェックは次のように実装できます。

recent_actions = []

def is_repeating(action, recent_actions, limit=3):
    recent_actions.append(action)

    if len(recent_actions) > limit:
        recent_actions.pop(0)

    if len(recent_actions) < limit:
        return False

    return all(a == recent_actions[0] for a in recent_actions)
Enter fullscreen mode Exit fullscreen mode

ループ内では、次のように停止条件に追加します。

if is_repeating(action, recent_actions):
    print("同じアクションが繰り返されたため停止します")
    break
Enter fullscreen mode Exit fullscreen mode

安全性

コンピューター使用エージェントは、実際にクリックや入力を行います。誤操作を前提に設計してください。

最低限、以下を守る必要があります。

  • サンドボックスまたは使い捨てブラウザプロファイルで実行する
  • 本番環境にログインしたセッションでは使わない
  • 削除、送信、支払いなどの破壊的操作には人間の確認を入れる
  • 各ステップのスクリーンショット、アクション、理由をログに残す
  • 最大ステップ数とタイムアウトを必ず設定する

ログは次のような形式で残すと、後から検証しやすくなります。

import time
import json

def log_step(step, action):
    record = {
        "time": time.time(),
        "step": step,
        "action": action,
    }

    print(json.dumps(record, ensure_ascii=False))
Enter fullscreen mode Exit fullscreen mode

必要であれば、スクリーンショットもファイルに保存します。

with open(f"step-{step}.png", "wb") as f:
    f.write(shot)
Enter fullscreen mode Exit fullscreen mode

Apidogでエージェントの呼び出しをテストする

エージェントの失敗原因は、多くの場合「モデルが有効なアクションを返したか?」に集約できます。

Playwrightとつなぐ前に、ApidogでQwen 3.7 PlusへのAPI呼び出しを単体テストしてください。

確認するポイントは次のとおりです。

  • サンプルスクリーンショットを送信できるか
  • レスポンスが常にJSONか
  • actionが許可リスト内か
  • clickxyが含まれるか
  • typetextが含まれるか
  • 不正な出力が出たときに再試行できるか

ApidogでModel Studioキーを環境ごとに保存しておくと、ローカル、ステージング、本番相当の呼び出しを分けて検証できます。さらにエンドポイントをモックすれば、毎回トークンを消費せずにエージェントループ側の実装を進められます。

フルループの呼び出し順序を確認したい場合は、ApidogのAIエージェントデバッガーを使うと、どのステップで失敗したかを追跡できます。

Apidogでエージェント呼び出しをテストする

デザインからUIコードを生成する方法については、Qwen 3.7 Plusによるスクリーンショットからコードへの変換も参照してください。

エージェントの背後にあるモデル呼び出しをテストおよびデバッグするには、Apidogをダウンロードしてください。

よくある質問

コンピューター使用エージェントとは何ですか?

スクリーンショットで画面を認識し、モデルで次のアクションを決定し、自動化ドライバーで操作を実行し、目標達成まで繰り返すソフトウェアです。

Qwen 3.7 Plusは私のデスクトップを直接制御できますか?

モデルはアクションを返すだけです。実際のクリックや入力は、Playwrightやデスクトップ自動化ライブラリなどのドライバーで実行します。

各ステップのコストはどれくらいですか?

主にスクリーンショットの入力トークンに依存します。画像1枚が数千入力トークンになる可能性があるため、ダウンスケール、クロップ、ステップ上限が主なコスト削減策です。

本番環境で使えますか?

限定されたタスクで、各ステップの検証、サンドボックス化、人間の確認がある場合は現実的です。重要なシステムのオープンエンドな制御には、そのまま使わないでください。

座標をスケーリングする必要がありますか?

スクリーンショット解像度とビューポートが一致していれば不要です。異なる場合は、画像サイズと実画面サイズの比率で座標を変換してください。

結論

Qwen 3.7 Plusを使ったコンピューター使用エージェントは、スクリーンショット、モデル判断、アクション実行、検証を繰り返す短いループとして実装できます。

実装時のポイントは次のとおりです。

  • モデル出力をJSONアクションに制限する
  • スクリーンショットとビューポートを一致させる
  • ステップ数、再試行、停止条件を必ず入れる
  • 破壊的操作には人間の確認を入れる
  • Playwrightに接続する前に、Apidogでモデル呼び出しを検証する

まずは小さなブラウザ操作から始め、ログと検証を追加しながら、安全にエージェントループを拡張してください。

Top comments (0)