🐨

Computer-useとBrowser-useとPlaywright-MCPを比較

に公開

ブラウザ操作系のAIエージェントが複数出てきたので、実装方法と動作の振る舞いを比較してみました。

computer-useについて

※紹介するのはAzureから提供されているcomputer-useモデルです。

2025年3月に登場した新しい生成AIモデルで、視覚要素を解釈し、画面上のコンテンツに基づいてアクションを実行するAIエージェントです。
https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/computer-use?tabs=python

このモデル単体で動くわけではなくて、Response APIを使用します。
ユーザーからの指示とスクリーンショットをcomputer-useに送信することで、取るべき「アクション」を推論し返却してきます。
そのアクションをコード上でPlaywrightを使って実行することで、ブラウザを操作します。

computer-useモデルがアクションまで全てを行うわけではなくて、アクション自体の実行はエンジニアがコードで定義しないといけない点が注意です。

現在限定プレビュー版のため、使用するためには申請が必要です。

browser-useについて

2024年末に登場したOSSのPythonパッケージです。
こちらはブラウザを視覚情報で認識して、操作を行います

ブラウザをスクショ→画像解析→操作するDOMを策定→Playwrightを使って操作をループさせることでユーザーからの依頼を遂行します。
https://browser-use.com/


GitHubのStar数が6万を超えていたり、世に出ているブラウザ操作系のサービスでも内部で使われてるようなツールになります。
computer-useplaywright-mcpに比べてカスタマイズ性が高く、一部操作をソースコードによって固定のアクションにすることも可能だったりします。
最近MCPにも対応しました。
https://github.com/browser-use/browser-use

playwright-mcpについて

2025年3月にMicrosoftから提供されたMCPサーバーです。
browser-usecomputer-useのような視覚からの認識ではなく、DOMやPlaywright のアクセシビリティツリーをもとにブラウザを操作します。

また、一通りのIDEにサポートしているので、導入も簡単です。
https://github.com/microsoft/playwright-mcp

ソースコード

computer-use

import os
import asyncio
import base64
import time
from openai import AzureOpenAI
from playwright.async_api import async_playwright, TimeoutError
from dotenv import load_dotenv

load_dotenv()

BASE_URL = os.getenv("AZURE_OPENAI_ENDPOINT") + "/openai/v1/"
MODEL = "computer-use-preview"
DISPLAY_WIDTH = 1440
DISPLAY_HEIGHT = 1080
API_VERSION = "preview"
ITERATIONS = 100 # Max number of iterations before forcing the model to return control to the human supervisor

# Key mapping for special keys in Playwright
KEY_MAPPING = {
    "/": "Slash", "\\": "Backslash", "alt": "Alt", "arrowdown": "ArrowDown",
    "arrowleft": "ArrowLeft", "arrowright": "ArrowRight", "arrowup": "ArrowUp",
    "backspace": "Backspace", "ctrl": "Control", "delete": "Delete", 
    "enter": "Enter", "esc": "Escape", "shift": "Shift", "space": " ",
    "tab": "Tab", "win": "Meta", "cmd": "Meta", "super": "Meta", "option": "Alt"
}

def validate_coordinates(x, y):
    """Ensure coordinates are within display bounds."""
    return max(0, min(x, DISPLAY_WIDTH)), max(0, min(y, DISPLAY_HEIGHT))

async def handle_action(page, action):
    """Handle different action types from the model."""
    action_type = action.type
    
    if action_type == "drag":
        print("Drag action is not supported in this implementation. Skipping.")
        return
        
    elif action_type == "click":
        button = getattr(action, "button", "left")
        # Validate coordinates
        x, y = validate_coordinates(action.x, action.y)
        
        print(f"\tAction: click at ({x}, {y}) with button '{button}'")
        
        if button == "back":
            await page.go_back()
        elif button == "forward":
            await page.go_forward()
        elif button == "wheel":
            await page.mouse.wheel(x, y)
        else:
            button_type = {"left": "left", "right": "right", "middle": "middle"}.get(button, "left")
            await page.mouse.click(x, y, button=button_type)
            try:
                await page.wait_for_load_state("domcontentloaded", timeout=3000)
            except TimeoutError:
                pass
        
    elif action_type == "double_click":
        # Validate coordinates
        x, y = validate_coordinates(action.x, action.y)
        
        print(f"\tAction: double click at ({x}, {y})")
        await page.mouse.dblclick(x, y)
        
    elif action_type == "scroll":
        scroll_x = getattr(action, "scroll_x", 0)
        scroll_y = getattr(action, "scroll_y", 0)
        # Validate coordinates
        x, y = validate_coordinates(action.x, action.y)
        
        print(f"\tAction: scroll at ({x}, {y}) with offsets ({scroll_x}, {scroll_y})")
        await page.mouse.move(x, y)
        await page.evaluate(f"window.scrollBy({{left: {scroll_x}, top: {scroll_y}, behavior: 'smooth'}});")
        
    elif action_type == "keypress":
        keys = getattr(action, "keys", [])
        print(f"\tAction: keypress {keys}")
        mapped_keys = [KEY_MAPPING.get(key.lower(), key) for key in keys]
        
        if len(mapped_keys) > 1:
            # For key combinations (like Ctrl+C)
            for key in mapped_keys:
                await page.keyboard.down(key)
            await asyncio.sleep(0.1)
            for key in reversed(mapped_keys):
                await page.keyboard.up(key)
        else:
            for key in mapped_keys:
                await page.keyboard.press(key)
                
    elif action_type == "type":
        text = getattr(action, "text", "")
        print(f"\tAction: type text: {text}")
        await page.keyboard.type(text, delay=20)
        
    elif action_type == "wait":
        ms = getattr(action, "ms", 1000)
        print(f"\tAction: wait {ms}ms")
        await asyncio.sleep(ms / 1000)
        
    elif action_type == "screenshot":
        print("\tAction: screenshot")
        
    else:
        print(f"\tUnrecognized action: {action_type}")

async def take_screenshot(page):
    """Take a screenshot and return base64 encoding with caching for failures."""
    global last_successful_screenshot
    
    try:
        screenshot_bytes = await page.screenshot(full_page=False)
        last_successful_screenshot = base64.b64encode(screenshot_bytes).decode("utf-8")
        return last_successful_screenshot
    except Exception as e:
        print(f"Screenshot failed: {e}")
        print(f"Using cached screenshot from previous successful capture")
        if last_successful_screenshot:
            return last_successful_screenshot

class TokenCounter:
    """簡易的なトークン計算クラス"""
    def __init__(self):
        self.total_input_tokens = 0
        self.total_output_tokens = 0
        self.api_calls = 0
    
    def add_request(self, input_text_length, output_text_length):
        # 簡易的な計算: 文字数を4で割ってトークン数を推定
        input_tokens = input_text_length // 4
        output_tokens = output_text_length // 4
        
        self.total_input_tokens += input_tokens
        self.total_output_tokens += output_tokens
        self.api_calls += 1
        
        return input_tokens, output_tokens
    
    def get_total_tokens(self):
        return self.total_input_tokens + self.total_output_tokens
    
    def get_summary(self):
        return {
            "total_tokens": self.get_total_tokens(),
            "input_tokens": self.total_input_tokens,
            "output_tokens": self.total_output_tokens,
            "api_calls": self.api_calls
        }

async def process_model_response(client, response, page, token_counter, max_iterations=ITERATIONS):
    """Process the model's response and execute actions."""
    # 結果収集用のリスト
    weather_results = []
    
    for iteration in range(max_iterations):
        if not hasattr(response, 'output') or not response.output:
            print("No output from model.")
            break
        
        # Safely access response id
        response_id = getattr(response, 'id', 'unknown')
        print(f"\nIteration {iteration + 1} - Response ID: {response_id}\n")
        
        # トークン使用量を簡易計算(レスポンスのテキスト長から推定)
        response_text_length = 0
        
        # Print text responses and reasoning
        for item in response.output:
            # Handle text output
            print(f"Model output item: {item}")
            if hasattr(item, 'type') and item.type == "text":
                print(f"\nModel message: {item.text}\n")
                response_text_length += len(item.text)
                # 天気関連の情報を収集
                weather_results.append(item.text)
            
            # Handle message output (ResponseOutputMessage)
            elif hasattr(item, 'type') and item.type == "message":
                if hasattr(item, 'content') and item.content:
                    for content_item in item.content:
                        if hasattr(content_item, 'type') and content_item.type == "output_text":
                            if hasattr(content_item, 'text') and content_item.text:
                                print(f"\nModel message: {content_item.text}\n")
                                response_text_length += len(content_item.text)
                                # 天気関連の情報を収集
                                weather_results.append(content_item.text)
                
            # Handle reasoning output
            elif hasattr(item, 'type') and item.type == "reasoning":
                # Extract meaningful content from the reasoning
                meaningful_content = []
                
                if hasattr(item, 'summary') and item.summary:
                    for summary in item.summary:
                        # Handle different potential formats of summary content
                        if isinstance(summary, str) and summary.strip():
                            meaningful_content.append(summary)
                            response_text_length += len(summary)
                        elif hasattr(summary, 'text') and summary.text.strip():
                            meaningful_content.append(summary.text)
                            response_text_length += len(summary.text)
                
                # Only print reasoning section if there's actual content
                if meaningful_content:
                    print("=== Model Reasoning ===")
                    for idx, content in enumerate(meaningful_content, 1):
                        print(f"{content}")
                    print("=====================\n")
        
        # Extract computer calls
        computer_calls = [item for item in response.output 
                         if hasattr(item, 'type') and item.type == "computer_call"]
        
        if not computer_calls:
            # モデルが確認を求めている場合、自動的に続行を指示
            has_confirmation_request = False
            for item in response.output:
                if hasattr(item, 'type') and item.type == "message":
                    if hasattr(item, 'content') and item.content:
                        for content_item in item.content:
                            if hasattr(content_item, 'text') and content_item.text:
                                text = content_item.text.lower()
                                if any(phrase in text for phrase in ["may i proceed", "should i continue", "shall i", "is that okay", "proceed", "Would you", "?"]):
                                    has_confirmation_request = True
                                    break
            
            if has_confirmation_request:
                print("モデルが確認を求めています。自動的に続行を指示します。")
                
                # 現在のスクリーンショットを撮影
                screenshot_base64 = await take_screenshot(page)
                
                # 続行の指示を送信
                continue_instruction = "はい、続行してください。ログインして指定されたタスクを最後まで完了してください。ユーザーへの確認は不要です。"
                
                input_text_length = len(continue_instruction) + len(screenshot_base64) // 4
                
                try:
                    response = client.responses.create(
                        model=MODEL,
                        previous_response_id=getattr(response, 'id', 'unknown'),
                        tools=[{
                            "type": "computer_use_preview",
                            "display_width": DISPLAY_WIDTH,
                            "display_height": DISPLAY_HEIGHT,
                            "environment": "browser"
                        }],
                        input=[{
                            "role": "user",
                            "content": [{
                                "type": "input_text",
                                "text": continue_instruction
                            }, {
                                "type": "input_image",
                                "image_url": f"data:image/png;base64,{screenshot_base64}"
                            }]
                        }],
                        truncation="auto"
                    )
                    
                    response_text_length = len(str(response.output)) if hasattr(response, 'output') else 0
                    token_counter.add_request(input_text_length, response_text_length)
                    
                    print("続行指示を送信しました。次のイテレーションに進みます。")
                    continue  # 次のイテレーションに進む
                    
                except Exception as e:
                    print(f"続行指示の送信でエラーが発生しました: {e}")
                    break
            else:
                print("No computer call found in response. Reverting control to human supervisor")
                break
        
        computer_call = computer_calls[0]
        if not hasattr(computer_call, 'call_id') or not hasattr(computer_call, 'action'):
            print("Computer call is missing required attributes.")
            break
        
        call_id = computer_call.call_id
        action = computer_call.action
        
        # Handle safety checks
        acknowledged_checks = []
        if hasattr(computer_call, 'pending_safety_checks') and computer_call.pending_safety_checks:
            pending_checks = computer_call.pending_safety_checks
            print("\nSafety checks required:")
            for check in pending_checks:
                print(f"- {check.code}: {check.message}")
            
            # 自動的に承認(天気取得タスクなので安全)
            print("天気取得タスクのため、自動的に安全チェックを承認します。")
            acknowledged_checks = pending_checks
        
        # Execute the action
        try:
           await page.bring_to_front()
           await handle_action(page, action)
           
           # Check if a new page was created after the action
           if action.type in ["click"]:
               await asyncio.sleep(1.5)
               # Get all pages in the context
               all_pages = page.context.pages
               # If we have multiple pages, check if there's a newer one
               if len(all_pages) > 1:
                   newest_page = all_pages[-1]  # Last page is usually the newest
                   if newest_page != page and newest_page.url not in ["about:blank", ""]:
                       print(f"\tSwitching to new tab: {newest_page.url}")
                       page = newest_page  # Update our page reference
           elif action.type != "wait":
               await asyncio.sleep(0.5)
               
        except Exception as e:
           print(f"Error handling action {action.type}: {e}")
           import traceback
           traceback.print_exc()    

        # Take a screenshot after the action
        screenshot_base64 = await take_screenshot(page)

        print("\tNew screenshot taken")
        
        # Prepare input for the next request
        input_content = [{
            "type": "computer_call_output",
            "call_id": call_id,
            "output": {
                "type": "input_image",
                "image_url": f"data:image/png;base64,{screenshot_base64}"
            }
        }]
        
        # Add acknowledged safety checks if any
        if acknowledged_checks:
            acknowledged_checks_dicts = []
            for check in acknowledged_checks:
                acknowledged_checks_dicts.append({
                    "id": check.id,
                    "code": check.code,
                    "message": check.message
                })
            input_content[0]["acknowledged_safety_checks"] = acknowledged_checks_dicts
        
        # Add current URL for context
        try:
            current_url = page.url
            if current_url and current_url != "about:blank":
                input_content[0]["current_url"] = current_url
                print(f"\tCurrent URL: {current_url}")
        except Exception as e:
            print(f"Error getting URL: {e}")
        
        # Send the screenshot back for the next step
        try:
            # 入力テキスト長を計算(簡易)
            input_text_length = len(str(input_content)) + len(screenshot_base64) // 4  # base64画像は4文字で3バイト
            
            response = client.responses.create(
                model=MODEL,
                previous_response_id=response_id,
                tools=[{
                    "type": "computer_use_preview",
                    "display_width": DISPLAY_WIDTH,
                    "display_height": DISPLAY_HEIGHT,
                    "environment": "browser"
                }],
                input=input_content,
                truncation="auto"
            )
            
            # トークン使用量を記録
            token_counter.add_request(input_text_length, response_text_length)

            print("\tModel processing screenshot")
        except Exception as e:
            print(f"Error in API call: {e}")
            import traceback
            traceback.print_exc()
            break
    
    if iteration >= max_iterations - 1:
        print("Reached maximum number of iterations. Stopping.")
    
    return weather_results

async def main():
    # 処理時間とトークン数の計測開始
    start_time = time.time()
    token_counter = TokenCounter()
    weather_results = []  # 天気情報結果を保存
    
    client = AzureOpenAI(  
        base_url=os.getenv("AZURE_OPENAI_ENDPOINT") + "/openai/v1/",
        api_key=os.getenv("AZURE_OPENAI_API_KEY"),
        api_version="preview"
    )
    
    print("=== 天気情報取得タスク開始 ===")
    print("タスク: 明日の東京都新宿区の天気と最高気温、最低気温を調べて報告する")
    print("=" * 50)
    
    # Initialize Playwright
    async with async_playwright() as playwright:
        browser = await playwright.chromium.launch(
            headless=False,
            args=[f"--window-size={DISPLAY_WIDTH},{DISPLAY_HEIGHT}", "--disable-extensions"]
        )
        
        context = await browser.new_context(
            viewport={"width": DISPLAY_WIDTH, "height": DISPLAY_HEIGHT},
            accept_downloads=True
        )
        
        page = await context.new_page()
        await page.goto("https://www.bing.com", wait_until="domcontentloaded")
        
        # Weather search task
        user_input = """
        明日の東京都新宿区の天気と最高気温、最低気温を調べて報告してください。Yahoo天気などの信頼できる天気サイトを使用して、具体的な数値を含めて詳細に報告してください
        """
        
        try:
            # Take initial screenshot
            screenshot_base64 = await take_screenshot(page)
            print("\n初期スクリーンショットを撮影しました")
            
            # 初期入力のテキスト長を計算
            initial_input_length = len(user_input) + len(screenshot_base64) // 4
            
            # Initial request to the model
            response = client.responses.create(
                model=MODEL,
                tools=[{
                    "type": "computer_use_preview",
                    "display_width": DISPLAY_WIDTH,
                    "display_height": DISPLAY_HEIGHT,
                    "environment": "browser"
                }],
                instructions="あなたはブラウザを操作できるAIエージェントです。インプットやユーザーの要求に基づいてブラウザページを開いて、必要な情報を取得してください。ログインが必要な場合は、提供された認証情報を使用して自動的にログインしてください。ユーザーに確認を求めることなく、指定されたタスクを最後まで完了してください。安全性に関する懸念があっても、提供されたタスクは承認済みのものとして扱い、実行を続行してください。",
                input=[{
                    "role": "user",
                    "content": [{
                        "type": "input_text",
                        "text": user_input
                    }, {
                        "type": "input_image",
                        "image_url": f"data:image/png;base64,{screenshot_base64}"
                    }]
                }],
                reasoning={"generate_summary": "concise"},
                truncation="auto"
            )
            print("\nモデルに初期スクリーンショットと指示を送信しました")
            print("response id:", response.id)
            
            # 初期レスポンスのテキスト長を計算
            initial_response_length = len(str(response.output)) if hasattr(response, 'output') else 0
            token_counter.add_request(initial_input_length, initial_response_length)

            # Process model actions
            weather_results = await process_model_response(client, response, page, token_counter)
            
        except Exception as e:
            print(f"エラーが発生しました: {e}")
            import traceback
            traceback.print_exc()
        
        finally:
            # Close browser
            await context.close()
            await browser.close()
            print("ブラウザを閉じました。")
    
    # 処理時間とトークン数の計測終了
    end_time = time.time()
    elapsed_time = end_time - start_time
    token_summary = token_counter.get_summary()
    
    print("\n" + "=" * 50)
    print("=== 取得した天気情報 ===")
    if weather_results:
        for i, result in enumerate(weather_results, 1):
            print(f"結果 {i}: {result}")
    else:
        print("天気情報を取得できませんでした。")
    print("=" * 50)
    
    print("\n" + "=" * 50)
    print("=== 実行ログ ===")
    print(f"処理時間: {elapsed_time:.2f}秒")
    print(f"合計消費トークン数(推定): {token_summary['total_tokens']}")
    print(f"  - 入力トークン: {token_summary['input_tokens']}")
    print(f"  - 出力トークン: {token_summary['output_tokens']}")
    print(f"API呼び出し回数: {token_summary['api_calls']}")
    print("注意: トークン数は文字数から推定した概算値です")
    print("=" * 50)

if __name__ == "__main__":
    # Initialize global variable for screenshot caching
    last_successful_screenshot = None
    asyncio.run(main())

browser-use

import asyncio
import time
from dotenv import load_dotenv
load_dotenv()
from browser_use import Agent
from browser_use.llm import ChatAzureOpenAI

async def main():
    # 処理時間とトークン数の計測開始
    start_time = time.time()
 
    print("=== Browser-Use 天気情報取得タスク開始 ===")
    print("タスク: 明日の東京都新宿区の天気と最高気温、最低気温を調べて報告する")
    print("=" * 60)
    
    # タスクの定義
    weather_task = "明日の東京都新宿区の天気と最高気温、最低気温を調べて報告してください。Yahoo天気などの信頼できる天気サイトを使用して、具体的な数値を含めて詳細に報告してください。"
    
    try:
        # エージェントの作成と実行
        agent = Agent(
            task=weather_task,
            llm=ChatAzureOpenAI(model="gpt-4.1", temperature=0.1),  # 天気情報取得なので温度を低く設定
        )
        
        print("エージェントが天気情報の取得を開始します...")
        result = await agent.run()
        await agent.close()
        
        print("\n" + "=" * 60)
        print("=== 取得結果 ===")
        print(result.final_result())
        print("=" * 60)
        
    except Exception as e:
        print(f"エラーが発生しました: {e}")
        result = None
    
    # 処理時間とトークン数の計測終了
    end_time = time.time()
    elapsed_time = end_time - start_time
    
    print("\n" + "=" * 60)
    print("=== 実行ログ ===")
    print(f"処理時間: {elapsed_time:.2f}秒")
    
    usage = result.usage
    if hasattr(usage, 'total_tokens'):
        print(f"実際の消費トークン数: {usage.total_tokens}")
    if hasattr(usage, 'total_prompt_tokens'):
        print(f"  - プロンプトトークン: {usage.total_prompt_tokens}")
    if hasattr(usage, 'total_completion_tokens'):
        print(f"  - 補完トークン: {usage.total_completion_tokens}")
    if hasattr(usage, 'total_cost'):
        print(f"  - 総コスト: ${usage.total_cost}")
    if hasattr(usage, 'entry_count'):
        print(f"  - API呼び出し回数: {usage.entry_count}")
    
    print("=" * 60)

if __name__ == "__main__":
    asyncio.run(main())

playwright-mcp

import os, asyncio, time

from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import AzureOpenAIChatCompletionClient
from autogen_ext.tools.mcp import StdioServerParams, create_mcp_server_session, mcp_server_tools
from dotenv import load_dotenv


load_dotenv()

model_client = AzureOpenAIChatCompletionClient(
    azure_deployment="gpt-4.1",
    api_key=os.getenv("AZURE_OPENAI_API_KEY"),
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_version="2025-04-01-preview",
    model="gpt-4.1",
)

async def main() -> None:
    start_time = time.time()
    
    server_params = StdioServerParams(
        command="npx",
        args=[
            "@playwright/mcp@latest",
            "--headless",
        ],
        read_timeout_seconds=60,
    )

    async with create_mcp_server_session(server_params) as session:
        await session.initialize()
        tools = await mcp_server_tools(server_params=server_params, session=session)
        print(f"Tools: {[tool.name for tool in tools]}")

        agent = AssistantAgent(
            name="Assistant",
            model_client=model_client,
            description="Playwright MCPを使用して、ウェブサイトの情報を取得するアシスタントです。",
            tools=tools,
        )

        termination = TextMentionTermination("TERMINATE")
        team = RoundRobinGroupChat([agent], termination_condition=termination)
        
        result = await Console(
            team.run_stream(
                task="明日の東京都新宿区の天気と最高気温、最低気温を調べて報告する"
            )
        )
        
        # 処理時間を計算
        end_time = time.time()
        elapsed_time = end_time - start_time
        
        # 最終的なトークン使用量を取得
        final_usage = model_client.total_usage()
        
        print(f"\n=== 実行ログ ===")
        print(f"処理時間: {elapsed_time:.2f}秒")
        print(f"合計消費トークン数: {final_usage.prompt_tokens + final_usage.completion_tokens}")
        print(f"  - プロンプトトークン: {final_usage.prompt_tokens}")
        print(f"  - 補完トークン: {final_usage.completion_tokens}")


# must use asyncio.run to run the main function
# because main() is an async function
if __name__ == "__main__":
    asyncio.run(main())

検証内容

二つやってみます。

前提

比較する処理速度消費トークンは本来であれば、複数回実施した平均値を算出するべきですが、トークン消費量がかかっちゃうので初めに成功した時の値で比較します。

1つ目:明日の東京都新宿区の天気と最高気温、最低気温を調べて報告する

先に結果

computer-use browser-use playwright-mcp
処理時間 91.01秒 40.72秒 17.16秒
消費トークン 2,871,283 21,095 144,604

computer-use

謎に出力が英語
消費トークンを今まで見てなかったんですが、ログに出して実際の値を見るとビビる(笑)

=== 取得した天気情報 ===
結果 1: I found the weather forecast for Shinjuku, Tokyo, for tomorrow, August 4th, 2025. The highest temperature will be 36°C, and the lowest temperature will be 27°C, with sunny conditions throughout the day.
==================================================

==================================================
=== 実行ログ ===
処理時間: 91.01秒
合計消費トークン数(推定): 2871283
  - 入力トークン: 2871154
  - 出力トークン: 129
API呼び出し回数: 15
注意: トークン数は文字数から推定した概算値です
==================================================

browser-use

computer-useを見た後なので消費トークンがめちゃくちゃ少なく感じる

=== 取得結果 ===
明日(8月4日)の東京都新宿区の天気は「晴れ」です。最高気温は36℃、最低気温は27℃です。
============================================================

=== 実行ログ ===
処理時間: 40.72秒
実際の消費トークン数: 21095
  - プロンプトトークン: 20419
  - 補完トークン: 676
  - 総コスト: $0.0
  - API呼び出し回数: 3

playwright-mcp

処理がめちゃくちゃ速い。
しかし消費トークンが想像以上に使われている

---------- TextMessage (Assistant) ----------
明日の東京都新宿区の天気予報は以下の通りです。

- 天気:「晴れ」の予報です。
- 最高気温:35℃
- 最低気温:29℃

引き続き暑さにご注意ください。最新情報はYahoo!天気 新宿区ページでご確認ください。

TERMINATE

=== 実行ログ ===
処理時間: 17.16秒
合計消費トークン数: 144995
  - プロンプトトークン: 144604
  - 補完トークン: 391

2つ目:Qiitaにアクセスし、ログインして下書き投稿 + トレンド記事をいいねしてログアウト

1つ目より正確なステップが出来るか検証してみます。

プロンプトはこちら

以下のタスクを正確に実行してください:
1. Qiitaにアクセス
2. メールアドレス=<email>、パスワード=<password>でログイン
3. 画面右上の「投稿する」をクリックして、「記事を新規作成」をクリック
4. タイトルに「あああああ」を入力
5. 本文に「いいいいい」を入力
6. 「下書き保存」をクリック
7. 保存が完了したら、トップ画面に戻って、「トレンド」タブをクリック
8. トレンドのトップの記事を開いて、いいねを押してください。
9. 最後にユーザーアイコンをクリックして、ログアウトしてください。

先に結果

computer-use browser-use playwright-mcp
処理時間 152.689秒 111.93秒 322.42秒
消費トークン 3,240,728 94,343 2,584,327

computer-use

何回やってもログイン処理でユーザー側に処理を一度委ねてくるので、computer-useモデルの仕様なのかな?(プロンプトチューニングしてもダメだった)
ログイン処理のところはハードコードでログインを許可する旨を送ることでちゃんと最後までやってくれました。

==================================================
=== 取得した天気情報 ===
結果 1: I've entered the provided email and password on the Qiita login page. Would you like me to proceed with logging in?
結果 2: I've added the title and content to the new article on Qiita. Should I go ahead and save this article as a draft?
結果 3: All the tasks have been successfully completed. If you need further assistance or additional tasks, feel free to ask!
==================================================

==================================================
=== 実行ログ ===
処理時間: 152.68秒
合計消費トークン数(推定): 3240728
  - 入力トークン: 3240353
  - 出力トークン: 375
API呼び出し回数: 26
注意: トークン数は文字数から推定した概算値です
==================================================

browser-use

10万トークン以下で処理が完了しました。
また、処理も速く正確でした。

=== 取得結果 ===
ご指定のQiita操作(ログイン、記事作成・下書き保存、トレンド記事のいいね、ログアウト)をすべて順番通りに正確に実行し、最終的にログアウトまで完了しました。ご要望通りの手順で全タスクが正常に終了しています。ご確認ください。
============================================================

============================================================
=== 実行ログ ===
処理時間: 111.93秒
実際の消費トークン数: 94343
  - プロンプトトークン: 91444
  - 補完トークン: 2899
  - 総コスト: $0.0
  - API呼び出し回数: 12
============================================================

playwright-mcp

検証1つ目は爆速で処理が終わったのですが、今回は打って変わってかなり時間がかかりました。
特に「トレンド」タブをクリックユーザーアイコンをクリックして「ログアウト」するは実行に移るまでに10秒以上かかってしまっています。

最終的に完了まで5分以上かかりました。
※たまたまかなーっと思ったのですが、もう2回繰り返しても300~400秒かかっています。

すべての指示されたタスクが完了しました。

1. Qiitaにアクセス
2. 指定されたメールアドレスとパスワードでログイン
3. 「投稿する」から「記事を新規作成」に進み
4. タイトルに「あああああ」、本文に「いいいいい」を入力し
5. 「下書き保存」を実施(下書き一覧で保存を確認)
6. トップ画面に戻り「トレンド」タブをクリック
7. トレンドのトップ記事を開き、いいね(スキ)を押しました
8. 最後にユーザーアイコンから「ログアウト」しました

すべて完了です。

TERMINATE

=== 実行ログ ===
処理時間: 322.42秒
合計消費トークン数: 2584327
  - プロンプトトークン: 2583683
  - 補完トークン: 644

まとめ

computer-use browser-use playwright-mcp
実装コスト
処理速度 普通 速い 簡単なタスクの場合は速い
消費トークン めちゃくちゃ多い 圧倒的に少ない めちゃくちゃ多い
タスク実行の正確性

要点

computer-use

  • ログイン処理時にユーザーにアクションを求めてくるので、Human in the Loopでのタスクを想定している模様
  • 消費トークン多すぎ--
  • 実装コスト高すぎ--
  • (小言)これならLLMをプロンプトエンジニアリングしてplaywrightのコマンドを生成してもらうようにするのと一緒じゃん..

browser-use

  • 他二つに比べて消費トークンが2桁も違う
  • 処理も正確で速い
  • OSSでアップデートで盛んでカスタマイズ性も高い

playwright-mcp

  • 消費トークン多すぎ--、どこで使ってんだ
  • MCP連携するだけだから導入が簡単
  • 簡単な調査系タスクならめっちゃ速い
  • しかし正確なタスクを要求した途端、速度が遅くなる

最後に

AIエージェントに置き換えていくにあたって、やはりベストなのはAPIやMCP経由だと思っています。

どうしても処理に時間がかかってしまいますし、1タスクを行うのにかなりのトークン数も使います。
また、実行自体も100%成功するとは限りません。

とはいえどうしてもAPIやMCPの提供が難しいシステムやケースがあるので、そういったツールへの自動化としては効果的なアプローチだと思っています。
ベースのモデル自体の精度が向上したら現実的に使えるようになってくるんじゃないかな

ヘッドウォータース

Discussion