やっほー!国内のAI狂いだよ!✨
今日はね、成功談じゃないの。**壮絶な爆死記録**をお届けするよ!😭 最新技術を詰め込んだらどうなるか、そのリアルを見て!
みんなー!ローカルLLM、回してる?🌀
最近話題の「AIエージェント(Web操作自動化)」を、完全ローカル&無料で作りたい!って思ったことない?
私もそう思って、**Googleの最新モデル `gemma3-4b`** と、今一番ホットなライブラリ **`browser-use`** を組み合わせて、「最強のトレンド収集エージェント」を作ろうとしたんだ。
結果?
……PCの中で、Pythonのエラーログがキャンプファイヤーみたいに燃え上がったよ🔥🔥🔥
この記事は、**Windows環境で最新AIライブラリを使おうとした私が踏み抜いた「地雷の数々」**の記録だよ。これを読んで、みんなは同じ死に方をしないでね!💀
1. 夢見た「最強の構成」 💭
最初はね、こんな完璧な布陣を考えてたの。
- 🧠 Brain:
my-gemma3(orgemma3-4b) via Ollama
Google信者として外せない!軽量で賢い期待の星🌟 - 👁️ Eyes:
moondreamvia Ollama
画像認識専用の爆速モデル。 - 💪 Body:
browser-use
LangChainベースのWeb操作ライブラリ。今一番アツい! - 💅 Face:
marimo
次世代のPythonノートブック。UIがリッチで最高!
これらを組み合わせて、「ボタン一つでZennを巡回して、トレンド記事を要約してくれるエージェント」を作るはずだったんだ…。
2. 踏み抜いた地雷原(エラーログ供養) 💣
開発を始めた瞬間から、Windowsユーザーへの洗礼が止まらなかったよ!😭
💣 地雷1: Windowsのパス問題「\tmp なんてないよ!」
browser-use をインポートした瞬間、Pythonが即死。
解説: Linux/Mac前提で作られたライブラリあるある。「ルートディレクトリの /tmp に保存すればいいでしょ?」ってコードになってて、CドライブしかないWindows君が「そんな場所ない!」ってパニックに。
👉 無理やり解決: os.makedirs をモンキーパッチ(動的書き換え)して、エラーを握りつぶす暴挙に出たよ🙈
💣 地雷2: イベントループの呪い「ブラウザが起動しない」
いざ実行!と思ったら、ブラウザが立ち上がらない。
解説: これが一番厄介だった!WindowsのPython標準の非同期処理(SelectorEventLoop)だと、外部プロセス(Chrome)を制御できないの。
最近のPythonは改善されたはずなんだけど、marimo や他のライブラリとの相性で、古い設定に戻っちゃってたみたい。
👉 無理やり解決: 別スレッドを立てて、そこで強制的に ProactorEventLoop を起動するという、黒魔術みたいなコードを書く羽目に🧙♀️
💣 地雷3: 言葉が通じない「メッセージ形式の違い」
やっと動いた!と思ったら、今度はAIとエージェントが喧嘩し始めた。
解説: browser-use は独自のメッセージ形式を使ってるんだけど、LangChain経由でOllamaに渡すとき、Ollama君が「なにこの独自規格?LangChain標準のやつじゃないと読めないよ!」って拒否。
👉 無理やり解決: エージェントからAIに渡る直前で、メッセージを全変換する「通訳ラッパー」クラスを自作したよ🗣️
💣 地雷4: レシートがないと通さない「Usage情報の欠落」
これがトドメの一撃。AIが回答してるのに、エージェントがエラーを吐く。
解説: 最新の browser-use は、トークン消費量(お金)の計算に厳密すぎる!
LangChainのOllamaラッパーが返す結果には usage(トークン使用量の詳細)が含まれてないことがあるんだけど、browser-use は「レシート(usage)がない回答は受け取れません!」って門前払いするの。
👉 無理やり解決: usage: {"input_tokens": 0, ...} という偽のレシートを捏造して持たせるパッチを作成🧾
3. 最終的にどうなったか?(屍を越えて)
これだけのパッチを当てて、ついにエラーが出なくなった!
「いけー!Gemmaちゃん!」と叫んだ私の目の前に表示されたのは…
⚠️ 結果が空でした。
虚無!!!😱
エラーは出ないけど、タイムアウト(600秒待っても終わらない)か、あるいはGemmaちゃんがZennの膨大なHTML(DOM)に圧倒されて、「うーん、わかんない!」って思考停止しちゃったみたい。
4. 次はどうすればいいか考察(リベンジ計画) 🔥
今回は「完全敗北」だったけど、ただでは転ばないよ!
次回リベンジするなら、絶対にこうするべきっていう「生存戦略」が見えてきた。次に挑む勇者(私)のために残しておくね!📝
-
脱Windowsネイティブ!WSL2かDockerへ逃げる 🐧
Windowsのイベントループ問題は根深すぎる。素直にWSL2(Windows Subsystem for Linux)かDockerコンテナの中で動かせば、開発者が想定しているLinux環境になるから、エラーの半分(パス問題やプロセス起動問題)は消滅するはず!WindowsでPythonを極めるのは、茨の道すぎた…。 -
脳みそを「Gemini 3 Flash Preview」に変える 🧠✨
ローカルLLM縛りはロマンがあるけど、実用性ならクラウドAPIが最強なのは認めざるを得ない。特にGemini 3 Flash Previewは無料で使えて、コンテキストウィンドウ(記憶容量)が巨大。WebページのHTMLを丸ごと読ませても余裕だし、画像も理解できる。browser-useとの相性もバッチリだから、まずはこれで「動く体験」を作ってから、ローカルに落とし込むのが正解ルートだったかも。 -
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の管理人:今回のまとめ
今回の失敗から得られた知見はこれだね。
- Windowsは「修羅の国」: 最新のAIライブラリ(特に海外製)は、Mac/Linuxファーストで作られてる。Windows特有のパス区切り文字や、非同期処理の仕組み(EventLoop)の違いは、開発者が想定していないことが多いの。
- ライブラリの進化速度に環境が追いつかない:
browser-useは素晴らしいライブラリだけど、まだバージョン0.x系。仕様がコロコロ変わるし、LangChainやOllamaとの連携部分(インターフェース)が完全に標準化されてないから、今回のような「言葉の通じないトラブル」が起きちゃう。 - ローカルLLMの限界: HTMLのDOM構造はトークン数が膨大。クラウドのGPT-4oなら力技で読めるけど、ローカルの軽量モデルだとコンテキスト長や推論能力の限界で、情報を処理しきれずにフリーズしちゃうことがあるんだね。
でもね、こうやってエラーと格闘した時間は無駄じゃない!
次こそは、この屍を越えて、最強のエージェントを作り上げてみせるよ!🔥
とりあえず今日は…ふて寝する!おやすみ!🛌💤






