
目次
記事概要
AIエージェントにブラウザを操作させるツール『Browser Use』のユーザ割り込み機能のデモと、エージェントはユーザが割り込んだ状況をどう解釈して作業を再開するのかコードを追って内部処理を調べた。結論としてはLLMの力をアテにして何とかリカバリさせている感が強かった。
背景
『Browser Use』はAIエージェントにWebブラウザを用いた作業をさせることができるpython製のツールである。
エージェントに指示内容のプロンプトを与えてスクリプトを実行すると、あとはLLMが指示内容を達成するための手順を構築しブラウザの操作を実行する。
私はもっぱらwebの調べ物をするタスクをエージェントに依頼しているが、時々私がブラウザを操作したい場面がある。
どうしたものかと思っていたが、いつの間にかエージェントのタスク実行中にユーザが割り込みしブラウザ操作できる機能が追加されていた。(関連issue: https://github.com/browser-use/browser-use/issues/221#issuecomment-2843745594)
ユーザ割り込みの方法は、エージェントがタスク実行中に、ユーザがコンソール上でCtrl + Cをキー入力すると、割り込みをかけることができ、エージェントが一時中断状態になる。ユーザがブラウザで必要な操作を行った後、コンソール上でEnterを押すとエージェントの作業を、ユーザ操作後から開始できる。
Browser Useのユーザ割り込み機能のデモ
ユーザ割り込みのデモを動画にした。
デモシナリオとしては、「meow-noisyのgithubリポジトリに秘密の情報を公開しているURLがあるのでその秘密を取得させる」というものである1。ただし当該のURLにはreCAPTCHAが設けられておりエージェントは解けないので、その部分は人間が介入して調査を進めるというものである。
※ ⚠️ reCAPTCHAの例はあくまでエージェントがタスクを実行できない例として用意したものです。Browser Use 使用時はエージェントにアクセスさせるサイトの利用規約を遵守ください。
割り込み機能の紹介は以上。以降ではどのような処理が内部で走っているかを調べる。
コード調査: ユーザ割り込み後の状況をエージェントはどのように判断しているのか
ユーザ割り込みから復帰できることがわかったので、内部的にエージェントがどのように復帰するのかを調べた。
結論としては、割り込みがかけられている間にユーザが何をしたかエージェントは把握していなかった。作業再開時、割り込みがかけられる前の文脈、および割り込みから再開された地点のURLやページ情報から、最初に設定されたタスク達成のために何をすべきかLLMに考えさせるというものだった。
参考程度にユーザ割り込みを含むBrowser Useのシーケンス図を提示する。

以下、過去の解説記事を参考にしつつ、コードを適宜github copilotに質問しながら処理の中身を追った過程を記す。
ただしコード調査は commit id: 726dd30c824b16b40baf6d6450da4b958e2adc4f(2025/06/21) のものなので、今の実装とは異なる点があるかもしれない
Browser Use のロジックに関する基本的事項
そもそもBrowser Useがどのように動いているのかわかっていなかったので有志の技術記事で勉強した。
※括弧付きの数字は記事末尾の参考文献の番号に対応する。
- Browser Useも基本的にはLLMにタスク達成のための作業を考えさせる処理がプロンプト含めハードコードされている[1]
- ブラウザを操作するにあたっては、おおよそ3つのコンポーネントからなっている[2]
- Agent: ユーザから依頼されたタスクをどのように達成するかを考える部分。LLMを用いてControllerが行うアクションを計画する。
- Controller: Agentが計画したアクションを実行する部分。
- Browser: playwright越しにブラウザで、Controllerの命令を実行する。
- AgentはBrowserから収集したURL、タイトル、DOM、スクリーンショット(画像)などを収集しながら、タスク達成に必要な次の作業を考える[3]。作業実行はステップという単位で管理されている。
- Agentはステップの履歴を持っており、そのステップにたどり着いた文脈や、タスク実行時のエラーが起きた記録なども持っている[1]。
ユーザ割り込みのロジック
コードを覗いてユーザ割り込みがどのように行われているか確認した。
ざっくりいうと、エージェントはforループでステップを進めるが、非同期に走っているキー入力監視プロセスが割り込みをかけエージェントの中断・再開を制御するというものだった。
- エージェント
- エージェントはrun()内で step() を呼び、LLMによる作業計画とブラウザ操作を1ステップ行う。forループでstep()を繰り返し呼び、最大でmax_steps (デフォルト50回)分を実行しようとする。
- キー入力監視プロセス
- エージェントには割り込み監視のためのSignalHandlerというクラスからオブジェクトsignal_handlerを生成し、ユーザのキー入力を非同期に監視している。
- step()実行中に、Ctrl + C キーが入力されると signal_handlerの"Ctrl + C"キー操作に結びつけられているpause()が呼び出されsignal_handlerの状態がpause状態に移行する。
- ブラウザ操作の中断
- pause状態になると同時に、asyncio.CancelledErrorが投げられstep()を抜け、また、操作履歴に、「エージェントの実行をユーザによって中断された」というメッセージを追加する
- エージェントの待機
- step()から、run()のforループ先頭に戻ってきて、pause状態のif文内に入り、Enterキーの入力待ち状態に入る。
- エージェントの再開
- pause状態中にEnterキーが押されるとsignal_handlerのpause状態が解除され、再度run()のforループが回り始める
ユーザ操作後のエージェントの再開に関して
次に、ユーザ操作後にエージェントがどのように作業を再開するかをコードベースで説明する。
とはいえ、生のコードを見ないとおそらくイメージが沸かないと思うので、「pause状態中にユーザが何をしたかの情報はなく、エージェントは再開後のブラウザの状態と過去の操作履歴だけから復帰しようとしているんだな」という雰囲気だけ掴んでいただければと思う。
大きく分けて4工程あり、
- pause状態が解除されると、再度step()を実行しブラウザ操作を計画しようとする。
- ただし、pause状態中のエージェントはキーの監視以外何の処理もしていないため、ユーザが何をしたのかは全く把握していない。
- 過去の情報と今の状況だけから次のアクションをLLMに考えてもらうべくプロンプトを組む。
- step()内でLLMへ入力するプロンプトを生成するための文脈情報の収集を行う
step内の
self._message_manager.add_state_message()で、pause後のブラウザの状態を記録している。self._message_manager.add_state_message( browser_state_summary=browser_state_summary, model_output=self.state.last_model_output, result=self.state.last_result, step_info=step_info, use_vision=self.settings.use_vision, page_filtered_actions=page_filtered_actions if page_filtered_actions else None, sensitive_data=self.sensitive_data, )与えている情報のうち重要と思われるものをピックアップ
- browser_state_summaryが再開後に取得されたブラウザの状態(どのサイトを開いているか)
- model_outputがLLMが前回どんな思考・評価・次の目標・アクションを生成したか
- result=self.state.last_result で、割り込みが起きたというエラーメッセージが入力される。
プロンプトの生成
self._message_managerに、これまでのstepにおけるメッセージの記録がたまっており、self._message_manager.get_messages()でプロンプトを発行するget_messages() では本当に過去のメッセージが連結されているだけ。
def get_messages(self) -> list[BaseMessage]: """Get current message list, potentially trimmed to max tokens""" msg = [m.message for m in self.state.history.messages] # ... return msgmessageの具体的な中身までは追えていないが、github copilotに聞くと次のようなものらしい。このうちのHumanMessageやToolMessageの中に、今のブラウザの状態やブラウジング履歴が含まれているらしい。
[ # System prompt SystemMessage(content="You are a browser agent..."), # ユーザーのタスク HumanMessage(content="Go to example.com and click the login button."), # エージェントの思考・アクション提案 AIMessage(content="", tool_calls=[...]), # ツール実行結果 ToolMessage(content="Clicked login button. Page loaded."), # ...以降、ステップごとに追加されていく ]
LLMの呼び出し
- このメッセージをLLMへの入力として、
self.get_next_action(input_messages)で LLMにアクションを生成させる。
- このメッセージをLLMへの入力として、
ここまででわかった通り、LLMに与えているのは、エージェントの過去の履歴と、割り込みからの復帰後の状態のみである。LLMは渡された情報をもとにどうすれば作業達成できるかを考えタスクを再開している。
pause状態のエージェントは人間で言えば気絶しているようなものであり、ゴールと文脈情報だけでリカバリを図るのは中々LLM頼みだな、という所感。
おわりに
Browser Useのユーザ割り込み機能について調べた。
当初は「中断された文脈がわからずにエージェントが動けるわけがない。ユーザ割り込みなんて無理だろう」と思っていたので、この機能があるのは調査を進める上でありがたい。
とはいえ、割り込みからのリカバリは中々LLM依存なところがある。
したがって、ユーザサイドもBrowser Use の仕様を理解した上で割り込み後の状態を引き継がせる工夫をする必要はあると思う。
参考文献
- [1] browser-useの中の処理をみる
- https://zenn.dev/yusukeiwaki/scraps/3c73e93497315f
- この記事で紹介されている以下の記事も参考にしています
- [2] browser-useの基礎理解
- [3] Browser Use実装解説Part2