WindowsでローカルLLM × browser-useのエージェントを作ろうとして爆死した話【エラーのデパート】

本ページはプロモーションが含まれています
【失敗談】Windows × ローカルLLMで「最強のWeb巡回エージェント」を作ろうとしたら、エラーのデパートで爆死した話
国内のAI狂い

やっほー!国内のAI狂いだよ!✨

今日はね、成功談じゃないの。**壮絶な爆死記録**をお届けするよ!😭 最新技術を詰め込んだらどうなるか、そのリアルを見て!

みんなー!ローカルLLM、回してる?🌀
最近話題の「AIエージェント(Web操作自動化)」を、完全ローカル&無料で作りたい!って思ったことない?

私もそう思って、**Googleの最新モデル `gemma3-4b`** と、今一番ホットなライブラリ **`browser-use`** を組み合わせて、「最強のトレンド収集エージェント」を作ろうとしたんだ。

結果?

……PCの中で、Pythonのエラーログがキャンプファイヤーみたいに燃え上がったよ🔥🔥🔥

この記事は、**Windows環境で最新AIライブラリを使おうとした私が踏み抜いた「地雷の数々」**の記録だよ。これを読んで、みんなは同じ死に方をしないでね!💀

1. 夢見た「最強の構成」 💭

最初はね、こんな完璧な布陣を考えてたの。

  • 🧠 Brain: my-gemma3 (or gemma3-4b) via Ollama
    Google信者として外せない!軽量で賢い期待の星🌟
  • 👁️ Eyes: moondream via Ollama
    画像認識専用の爆速モデル。
  • 💪 Body: browser-use
    LangChainベースのWeb操作ライブラリ。今一番アツい!
  • 💅 Face: marimo
    次世代のPythonノートブック。UIがリッチで最高!

これらを組み合わせて、「ボタン一つでZennを巡回して、トレンド記事を要約してくれるエージェント」を作るはずだったんだ…。

2. 踏み抜いた地雷原(エラーログ供養) 💣

開発を始めた瞬間から、Windowsユーザーへの洗礼が止まらなかったよ!😭

💣 地雷1: Windowsのパス問題「\tmp なんてないよ!」

browser-use をインポートした瞬間、Pythonが即死。

FileNotFoundError: [WinError 3] 指定されたパスが見つかりません。: ‘\\tmp\\browser-use-downloads…’

解説: Linux/Mac前提で作られたライブラリあるある。「ルートディレクトリの /tmp に保存すればいいでしょ?」ってコードになってて、CドライブしかないWindows君が「そんな場所ない!」ってパニックに。
👉 無理やり解決: os.makedirs をモンキーパッチ(動的書き換え)して、エラーを握りつぶす暴挙に出たよ🙈

💣 地雷2: イベントループの呪い「ブラウザが起動しない」

いざ実行!と思ったら、ブラウザが立ち上がらない。

NotImplementedError in _make_subprocess_transport

解説: これが一番厄介だった!WindowsのPython標準の非同期処理(SelectorEventLoop)だと、外部プロセス(Chrome)を制御できないの。
最近のPythonは改善されたはずなんだけど、marimo や他のライブラリとの相性で、古い設定に戻っちゃってたみたい。
👉 無理やり解決: 別スレッドを立てて、そこで強制的に ProactorEventLoop を起動するという、黒魔術みたいなコードを書く羽目に🧙‍♀️

💣 地雷3: 言葉が通じない「メッセージ形式の違い」

やっと動いた!と思ったら、今度はAIとエージェントが喧嘩し始めた。

NotImplementedError: Unsupported message type: browser_use.llm.messages.SystemMessage

解説: browser-use は独自のメッセージ形式を使ってるんだけど、LangChain経由でOllamaに渡すとき、Ollama君が「なにこの独自規格?LangChain標準のやつじゃないと読めないよ!」って拒否。
👉 無理やり解決: エージェントからAIに渡る直前で、メッセージを全変換する「通訳ラッパー」クラスを自作したよ🗣️

💣 地雷4: レシートがないと通さない「Usage情報の欠落」

これがトドメの一撃。AIが回答してるのに、エージェントがエラーを吐く。

AttributeError: ‘AIMessage’ object has no attribute ‘usage’

解説: 最新の browser-use は、トークン消費量(お金)の計算に厳密すぎる!
LangChainのOllamaラッパーが返す結果には usage(トークン使用量の詳細)が含まれてないことがあるんだけど、browser-use は「レシート(usage)がない回答は受け取れません!」って門前払いするの。
👉 無理やり解決: usage: {"input_tokens": 0, ...} という偽のレシートを捏造して持たせるパッチを作成🧾

3. 最終的にどうなったか?(屍を越えて)

これだけのパッチを当てて、ついにエラーが出なくなった!
「いけー!Gemmaちゃん!」と叫んだ私の目の前に表示されたのは…

🎉 エージェント停止!結果を取得します…
⚠️ 結果が空でした。

虚無!!!😱

エラーは出ないけど、タイムアウト(600秒待っても終わらない)か、あるいはGemmaちゃんがZennの膨大なHTML(DOM)に圧倒されて、「うーん、わかんない!」って思考停止しちゃったみたい。

4. 次はどうすればいいか考察(リベンジ計画) 🔥

今回は「完全敗北」だったけど、ただでは転ばないよ!
次回リベンジするなら、絶対にこうするべきっていう「生存戦略」が見えてきた。次に挑む勇者(私)のために残しておくね!📝

  1. 脱Windowsネイティブ!WSL2かDockerへ逃げる 🐧
    Windowsのイベントループ問題は根深すぎる。素直にWSL2(Windows Subsystem for Linux)かDockerコンテナの中で動かせば、開発者が想定しているLinux環境になるから、エラーの半分(パス問題やプロセス起動問題)は消滅するはず!WindowsでPythonを極めるのは、茨の道すぎた…。
  2. 脳みそを「Gemini 3 Flash Preview」に変える 🧠✨
    ローカルLLM縛りはロマンがあるけど、実用性ならクラウドAPIが最強なのは認めざるを得ない。特にGemini 3 Flash Previewは無料で使えて、コンテキストウィンドウ(記憶容量)が巨大。WebページのHTMLを丸ごと読ませても余裕だし、画像も理解できる。browser-useとの相性もバッチリだから、まずはこれで「動く体験」を作ってから、ローカルに落とし込むのが正解ルートだったかも。
  3. HTMLを「ダイエット」させてから食わせる 🥗
    Gemmaちゃんが沈黙したのは、おそらく「情報過多」でお腹を壊したから。生のHTMLをそのまま読ませるとトークンがあふれるし、ノイズも多い。`BeautifulSoup`とかで不要なタグ(script, style, navなど)を削ぎ落として、メインコンテンツだけを抽出してからLLMに渡す「前処理」を挟めば、小さなモデルでも勝てる可能性が上がるはず!

5. 供養のためのコード公開(使用注意)

動かなかったけど、ここまで戦った証としてコードを残しておくね。
「Windows × Pythonの闇」と戦いたい勇者は、これをベースに改良してみて!
G:\マイドライブ... のパスとかは自分の環境に合わせてね!

import marimo

__generated_by__ = "marimo"
app = marimo.App(width="medium")

@app.cell
def imports():
    # 必要なライブラリをインポート
    # ⚠️ 依存関係地獄へようこそ!
    import asyncio
    import json
    import os
    import sys
    import marimo
    from typing import List, Optional, Any, Dict
    import pathlib
    import traceback
    import logging
    import threading

    # --- 🤫 ログ完全沈黙パッチ ---
    # marimoがバックグラウンドスレッドのログで落ちるのを防ぐため、口を塞ぐ
    logging.getLogger("browser_use").setLevel(logging.CRITICAL)
    logging.getLogger("langchain").setLevel(logging.CRITICAL)
    logging.getLogger("marimo").setLevel(logging.ERROR)

    # --- 🚑 ULTRA-PATCH: Windowsフォルダ作成エラー回避 ---
    # browser-use が \tmp に書き込もうとして死ぬのを防ぐモンキーパッチ
    original_mkdir = pathlib.Path.mkdir
    def safe_mkdir(self, mode=0o777, parents=False, exist_ok=False):
        try:
            original_mkdir(self, mode, parents, exist_ok)
        except OSError:
            return # エラーは握りつぶす!(Wild Style)
    pathlib.Path.mkdir = safe_mkdir

    # ライブラリのインポート(ここでもエラーが出がち)
    try:
        from browser_use import Agent, Controller
        from browser_use.browser.browser import Browser, BrowserConfig
    except ImportError:
        # バージョンによって場所が変わる対策
        try:
            from browser_use import Browser, BrowserConfig
        except:
            Browser = None

    from langchain_ollama import ChatOllama
    from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
    from pydantic import BaseModel, Field

    print("準備完了!(ここまで来るのも一苦労だったよ...)")
    
    return (
        AIMessage, Agent, Any, BaseModel, Browser, BrowserConfig, 
        ChatOllama, Controller, Dict, Field, HumanMessage, List, 
        Optional, SystemMessage, asyncio, logging, marimo, os, 
        pathlib, safe_mkdir, sys, threading, traceback,
    )

@app.cell
def config():
    # --- 設定エリア ---
    # ここに自分のモデル名を入れてね
    BRAIN_MODEL_NAME = "gemma3-4b" 
    CTX_SIZE = 32000 # メモリとの戦い
    return BRAIN_MODEL_NAME, CTX_SIZE

@app.cell
def custom_model_class(AIMessage, ChatOllama, HumanMessage, SystemMessage):
    # --- 👑 最終兵器: 完璧な通訳&偽造ラッパー ---
    # browser-use と LangChain の間の「言葉の壁」と「レシート要求」を解決するクラス
    
    class BrowserUseOllama(ChatOllama):
        def __init__(self, model, **kwargs):
            super().__init__(model=model, **kwargs)
        
        @property
        def provider(self):
            return "ollama" # 身分証を偽造

        @property
        def model_name(self):
            return self.model

        # Pydanticのガードを突破するための裏口
        def __setattr__(self, name, value):
            if name == "ainvoke":
                object.__setattr__(self, name, value)
            else:
                super().__setattr__(name, value)

        async def ainvoke(self, input, config=None, **kwargs):
            # 1. 邪魔な引数を消す
            if 'session_id' in kwargs: kwargs.pop('session_id')
            if config is not None and not isinstance(config, dict): config = None
            
            # 2. メッセージ型を通訳(独自型 -> LangChain標準型)
            converted_input = []
            if isinstance(input, list):
                for msg in input:
                    msg_type = type(msg).__name__
                    content = getattr(msg, 'content', '')
                    
                    if 'SystemMessage' in msg_type:
                        converted_input.append(SystemMessage(content=content))
                    elif 'UserMessage' in msg_type:
                        converted_input.append(HumanMessage(content=content))
                    else:
                        converted_input.append(msg)
            else:
                converted_input = input

            # 3. 実行
            response = await super().ainvoke(converted_input, config=config, **kwargs)
            
            # 4. レシート(Usage)偽造
            # browser-useが「使用トークン数が書いてない!」と怒るので、適当な数字(0)で埋める
            try:
                if not hasattr(response, "usage"):
                    usage_dict = {
                        "input_tokens": 0, "output_tokens": 0, "total_tokens": 0,
                        "prompt_tokens": 0, "completion_tokens": 0,
                        # 細かい項目も全部埋める!
                        "prompt_cached_tokens": 0, "prompt_cache_creation_tokens": 0, "prompt_image_tokens": 0,
                    }
                    object.__setattr__(response, "usage", usage_dict)
            except:
                pass
            
            return response

    return BrowserUseOllama,

@app.cell
async def run_agent(Agent, Browser, BrowserConfig, Controller, BrowserUseOllama, BRAIN_MODEL_NAME, CTX_SIZE, asyncio, logging, sys):
    # --- エージェント実行ロジック ---

    async def _scout_logic():
        print("🚀 エージェントロジック開始")
        controller = Controller()
        
        # 指示は英語の方が通りやすい...気がする
        task = "Navigate to 'https://zenn.dev/topics/python'. Find 2 articles about 'Agent'."

        # LLMの準備(ラッパークラス使用)
        llm = BrowserUseOllama(model=BRAIN_MODEL_NAME, temperature=0.0, num_ctx=CTX_SIZE)

        browser = Browser(config=BrowserConfig(headless=False))
        
        try:
            agent = Agent(task=task, llm=llm, browser=browser, controller=controller)
            
            # タイムアウトを延長(デフォルト60秒は短すぎる!)
            if hasattr(agent, "settings"):
                agent.settings.llm_timeout = 600
            
            print("🏃‍♂️ Web巡回開始...")
            history = await agent.run()
            
            result = history.final_result()
            print(f"📝 結果: {result}")
            return result
        finally:
            await browser.close()

    # --- Windows対策: 別スレッドでProactorループを回す ---
    def _run_in_proactor_thread():
        # ここでログを止めないとmarimoが落ちる
        logging.disable(logging.CRITICAL)
        
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        
        # Windowsの場合はポリシーを強制適用
        if sys.platform == 'win32':
            try:
                from asyncio.windows_events import ProactorEventLoop
                loop = ProactorEventLoop()
                asyncio.set_event_loop(loop)
            except:
                pass

        try:
            return loop.run_until_complete(_scout_logic())
        finally:
            loop.close()

    # 実行ボタン用関数
    async def start_scouting():
        return await asyncio.to_thread(_run_in_proactor_thread)

    return start_scouting,

@app.cell
def ui(marimo, start_scouting, asyncio):
    start_button = marimo.ui.button(
        label="💥 自爆覚悟で実行する",
        on_click=lambda _: asyncio.create_task(start_scouting()),
        kind="danger"
    )
    marimo.md(f"# 💀 AIトレンドスカウター (屍Ver)\n\n{start_button}")
    return start_button,

if __name__ == "__main__":
    app.run()

💡 IQ500の管理人:今回のまとめ

今回の失敗から得られた知見はこれだね。

  1. Windowsは「修羅の国」: 最新のAIライブラリ(特に海外製)は、Mac/Linuxファーストで作られてる。Windows特有のパス区切り文字や、非同期処理の仕組み(EventLoop)の違いは、開発者が想定していないことが多いの。
  2. ライブラリの進化速度に環境が追いつかない: browser-use は素晴らしいライブラリだけど、まだバージョン0.x系。仕様がコロコロ変わるし、LangChainOllama との連携部分(インターフェース)が完全に標準化されてないから、今回のような「言葉の通じないトラブル」が起きちゃう。
  3. ローカルLLMの限界: HTMLのDOM構造はトークン数が膨大。クラウドのGPT-4oなら力技で読めるけど、ローカルの軽量モデルだとコンテキスト長や推論能力の限界で、情報を処理しきれずにフリーズしちゃうことがあるんだね。

でもね、こうやってエラーと格闘した時間は無駄じゃない!
次こそは、この屍を越えて、最強のエージェントを作り上げてみせるよ!🔥

とりあえず今日は…ふて寝する!おやすみ!🛌💤

よかったらシェアしてね!
  • URLをコピーしました!
目次