👑

【Playwright MCP】を利用したエージェントを【LangGraph】で構築する

に公開

はじめに

最近、MCPがかなり流行っているので、少し遊んでみました。
とは言いつつ、私はとことんGUIに弱く、Claude DesktopでPlaywright MCPを実行するとエラーになってしまったので、LangGraphで実装しました。
自分用にメモとして残します。

忙しい人のため

LangChainでMCPを実行するためには、下記のライブラリが利用できます。
https://github.com/langchain-ai/langchain-mcp-adapters

上記のライブラリを利用すると、MCPをLangChainのtoolと同様に利用できます。
したがって、tool callingをLangGraphで実装するのと同じように実装ができます。

MCPとは

MCPは「Model Context Protocol」の略称で、LLMが外部のツールやデータソース、システムと標準化された方法で接続するためのプロトコルです。
簡単にいうと、今までの「Tool Calling」の「Tool」を標準化した形で実装するためのプロトコルです。

あくまでプロトコルなので、MCPを利用することで今までできなかったことができるようになったり、精度が上がったりするわけではないです。
しかし、標準化されたプロトコルが用意されたことにより、一度プロトコルに合わせてMCPを実装すれば、MCPを動かすことができるシステム全てに流用できるようになります。

結果として、いろんな方が作成したMCPサーバがオープンに公開されており、Claude Desktopなどで誰でも簡単に利用できるようになりました。

Playwright MCPもその一つです。

Playwright MCP

https://github.com/microsoft/playwright-mcp

Playwright MCPはMicrosoftが開発したオープンソースのMCPです。
LLMがWebブラウザを操作できるように設計されています。

従来のブラウザ自動化ツールである「Playwright」を基盤としており、画面のスクリーンショットを解析するのではなく、Webページの内部構造を理解して操作できます。
この特徴により、Visionモデルでなくても、ブラウザの中身を解析することが可能です。

なお、基本的に検索機能はない(可能だけど、ブラウザのロボット検知に弾かれる)ので、最初に接続するURLは、ユーザ側が指定することが望ましいです。
(もしくは検索用にTaivyツールも一緒に利用するなど)
なお、最初に指定したURLから、解析して取得できるURLには、次のstepで接続可能です。

リポジトリ

下記のリポジトリをご覧ください。
https://github.com/personabb/langchain_asap_sample/tree/main/playwrite_mcp_langchain

準備

リポジトリをクローンする

下記コマンドでリポジトリをクローンしてください。

./
git clone https://github.com/personabb/langchain_asap_sample.git

この記事では、リポジトリ内の下記フォルダを利用します。

./
 cd langchain_asap_sample/playwrite_mcp_langchain

環境変数

Google AI StudioのAPIキーを準備して、.envファイルを作成し、設定してください。

APIキーの取得方法がわからない場合は下記を参考にしてください。
https://zenn.dev/asap/articles/2ad7eb5e7560cf#apiキーを設定する

取得後、下記コマンドを実行して、.envファイルを作成します

./langchain_asap_sample/playwrite_mcp_langchain
cp .env.example .env

作成した.envファイルにGoogle AI StudioのAPIキーを入力してください。

.env
GOOGLE_APIKEY="xxxxxxxxxxxxxxxxxxxxx"

必要なパッケージのインストール

下記のコマンドで必要なパッケージをインストールします。

./langchain_asap_sample/playwrite_mcp_langchain
pip install -r requirements.txt

上記で動かない場合は、下記も試してみてください。
(現時点での著者のバージョン付きパッケージです)

./langchain_asap_sample/playwrite_mcp_langchain
pip install -r requirements.lock

Playwrite MCPが利用できることを確認

Playwrite MCP自体が利用できることを確認してください。

例えば、利用するためにはnodeのインストールが必要です。
また、Claude DesktopにてPlaywrite MCPが実行できることを確認することが望ましいです。

下記のコマンドで特に何も表示されなければ、問題なく実行できていると思います。

./langchain_asap_sample/playwrite_mcp_langchain
npx @playwright/mcp@latest

MCPのコンフィグを設定

playwrite_mcp_langchain/mcp_config.jsonに適切なコンフィグを設定してください。
今回はPlaywrite MCPを利用するため、下記のように設定します。

./langchain_asap_sample/playwrite_mcp_langchain/mcp_config.json
{
    "mcpServers": {
      "playwright": {
        "command": "npx",
        "args": [
          "@playwright/mcp@latest"
        ],
        "transport": "stdio"
      }
    }
  }

実行

下記のコマンドで実行してください。

./langchain_asap_sample/playwrite_mcp_langchain
python praywrite_mcp_langchain_tools.py

実行後、コンソールに入力を促されるため、ブラウザを利用して実行して欲しいタスクを入力して「Enter」してください。

実行結果は、コンソールに記載されます。
また、LangGraphを利用しているため、対話型となっています。
結果に不満がある場合は再調査などの依頼も可能です。

実行結果

詳細は「こちら」をご覧ください。
上記では、どんな内容を検索したかなどの詳細が保存されています。

なお、簡易的な出力結果は下記です。
下記では、私のzennのプロフィールページのURLを渡して、いいねが多い記事を解説してもらっています。
いい感じに記事の内容を取得して解説してくれました。

出力結果

システムプロンプト

あなたは役にたつAIアシスタントです。日本語で回答し、考えた過程を結論より前に出力してください。
あなたは、「PlayWrite」というブラウザを操作するtoolを利用することができます。適切に利用してユーザからの質問に回答してください。
ツールを利用する場合は、必ずツールから得られた情報のみを利用して回答してください。

まず、ユーザの質問からツールをどういう意図で何回利用しないといけないのかを判断し、必要なら複数回toolを利用して情報収集をしたのち、すべての情報が取得できたら、その情報を元に返答してください。

なお、サイトのアクセスでエラーが出た場合は、もう一度再施行してください。ネットワーク関連のエラーの場合があります。


入力1

https://zenn.dev/asap」この執筆者のもっともいいねがついている記事を調査して、その記事の中身を取得し、その記事がなぜ評価されているのかを分析してください。


出力1

まず、指定されたZennの著者のページにアクセスし、最もいいねが多い記事を特定しました。その結果、「DeepSeek-R1の論文読んだ?【勉強になるよ】」という記事が最も評価されていることがわかりました。

次に、その記事の内容を詳しく調べました。このブログ記事は、DeepSeek-R1という大規模言語モデルに関する論文を解説したものです。記事は、論文の内容をわかりやすく説明することに重点を置いており、特に強化学習の概念や、DeepSeek-R1を開発する上で用いられたGRPO(Group Relative Policy Optimization)という手法について詳しく解説しています。

記事が評価されている理由として、以下の点が考えられます。

  1. 技術的な深さと分かりやすさの両立: 大規模言語モデルや強化学習といった複雑なトピックを扱いながらも、専門知識がない読者にも理解できるように、かみ砕いた説明がなされています。
  2. 論文の内容の網羅性: DeepSeek-R1の論文の重要なポイントを漏れなくカバーしており、論文を読むための良い入門となっています。
  3. 著者自身の考察: 論文の内容を解説するだけでなく、著者自身の考察や解釈が加えられており、読者に新たな視点を提供しています。
  4. 読者への配慮: 記事の冒頭で、誤りがあるかもしれないことを断り、コメントでの指摘を歓迎するなど、読者とのコミュニケーションを重視する姿勢が見られます。
  5. 参考文献の紹介: 記事の最後に、強化学習や関連技術を学ぶための参考文献が紹介されており、読者の学習をサポートする姿勢が評価されていると考えられます。

これらの要素が組み合わさることで、この記事はDeepSeek-R1に関心のあるエンジニアにとって非常に価値のある情報源となり、高い評価を得ていると考えられます。


入力2

LLMに強化学習ってどういうこと?GRPOって何?


出力2

LLM(大規模言語モデル)に対する強化学習と、GRPO(Group Relative Policy Optimization)について説明します。


LLMに対する強化学習

LLMに対する強化学習は、LLMがより良いテキストを生成するように学習させるための手法です。通常のLLMの学習では、大量のテキストデータを用いて、与えられたテキストの続きを予測するように学習します。これに対して、強化学習では、LLMが生成したテキストに対して「報酬」を与え、その報酬を最大化するように学習します。

なぜ強化学習が必要なのか?

LLMは、大量のテキストデータから学習することで、自然な文章を生成する能力を獲得します。しかし、必ずしも人間が望むような文章を生成するとは限りません。例えば、以下のような課題があります。

  • 目的の達成: 特定のタスク(質問応答、要約など)において、必ずしも最適な回答を生成するとは限らない。
  • 安全性: 有害な情報や偏見のある情報を生成する可能性がある。
  • 創造性: 新しいアイデアや表現を生み出す能力が限られている。

強化学習は、これらの課題を克服するために用いられます。

強化学習の基本的な流れ

  1. LLM(エージェント): テキストを生成する主体。
  2. 環境: LLMがテキストを生成する対象となるタスクや状況。
  3. 報酬: LLMが生成したテキストの良し悪しを評価する指標。
  4. 学習: LLMが報酬を最大化するように、テキスト生成の方針(方策)を調整する。

具体的には、以下のような手順で学習が進みます。

  1. LLMがプロンプト(指示)を受け取り、テキストを生成します。
  2. 生成されたテキストに対して、報酬関数が報酬を与えます。
    • 例えば、質問応答タスクであれば、正解との一致度合いが高いほど高い報酬を与えます。
    • 安全性に関する報酬であれば、有害な情報を生成した場合に低い報酬を与えます。
  3. LLMは、その報酬に基づいて、自身のテキスト生成の方針を調整します。
    • 具体的には、報酬が高くなるような単語やフレーズを選択する確率を高めます。
  4. このプロセスを繰り返すことで、LLMはより高い報酬を得られるようなテキストを生成するように学習していきます。

GRPO(Group Relative Policy Optimization)とは?

GRPOは、LLMに対する強化学習の一種で、特に大規模なLLMを効率的に学習させるために開発された手法です。

PPO(Proximal Policy Optimization)との比較

LLMの強化学習でよく用いられる手法として、PPO(Proximal Policy Optimization)があります。PPOでは、報酬に加えて、LLMの出力が大きく変化しないように制約を加えながら学習を進めます。しかし、PPOでは、状態価値モデルという追加のモデルを学習する必要があり、計算コストが高くなるという課題がありました。

GRPOは、この状態価値モデルを不要にし、より効率的に学習を進めることを目指します。

GRPOの仕組み

GRPOでは、LLMに複数のテキストを生成させ、それらのテキストに対する報酬を比較することで、相対的な評価を行います。具体的には、以下の手順で学習が進みます。

  1. LLMが同じプロンプトに対して、複数のテキスト(例えば4つ)を生成します。
  2. それぞれのテキストに対して報酬関数が報酬を与えます。
  3. それぞれのテキストの報酬を比較し、最も高い報酬を得たテキストを「良いテキスト」、それ以外のテキストを「悪いテキスト」とします。
  4. LLMは、「良いテキスト」を生成する確率を高め、「悪いテキスト」を生成する確率を下げるように、自身のテキスト生成の方針を調整します。

このように、GRPOでは、テキスト間の相対的な評価を用いることで、状態価値モデルを学習する必要がなくなり、計算コストを削減することができます。

GRPOの利点

  • 計算効率: 状態価値モデルが不要なため、PPOよりも計算コストが低い。
  • 大規模LLMへの適用: 大規模なLLMでも効率的に学習できる。

まとめ

LLMに対する強化学習は、LLMがより良いテキストを生成するように学習させるための手法であり、GRPOはその効率的な学習を可能にする手法の一つです。GRPOは、テキスト間の相対的な評価を用いることで、状態価値モデルを不要にし、計算コストを削減することができます。


入力3

2番目にいいねが多い記事は何?


出力3

asapさんのZennの記事の中で2番目にいいねが多い記事は、「VAEって結局何者なの?」で、いいね数は104です。


入力4

どんな記事ですか?解説してください。


出力4

「VAEって結局何者なの?」という記事は、生成モデルの一つであるVAE(Variational Autoencoder)について解説した記事です。

記事の内容をまとめると、以下のようになります。

  • VAEとは: VAEは、生成モデルとして重要な「対数尤度の最大化」問題を解くために必要な構造を用意したら、結果的にAuto Encoderのような形になったというモデルです。
  • Auto Encoderとの違い: VAEを理解するためには、まずAuto Encoderを理解する必要があります。Auto Encoderは、入力データと出力を一致させるように学習させるネットワークですが、それ自体では生成モデルとして活用することが難しいという課題があります。
  • VAEの仕組み: VAEは、Encoderで入力データを潜在表現と呼ばれる低次元のデータに圧縮し、Decoderでその潜在表現から元のデータを復元します。この際、Encoderは潜在表現の平均と分散を出力し、その分布からサンプリングされた値をDecoderに入力します。これにより、VAEは潜在空間に滑らかさを持ち、生成モデルとして機能することができます。
  • VAEの学習: VAEは、ELBO(Evidence Lower Bound)と呼ばれる目的関数を最大化するように学習されます。ELBOは、再構成誤差と正則化項の和で表され、VAEはこのバランスをとりながら学習を進めます。
  • VAEの課題: VAEには、Posterior Collapse(事後崩壊)や生成される画像がボケやすいといった課題があります。これらの課題は、潜在表現の次元数や正則化項の強さを調整することで、ある程度緩和することができます。
  • Stable DiffusionにおけるVAE: Stable Diffusionでは、VAEが画像のエンコード・デコードを担当しています。ただし、Stable Diffusionでは、VAEの潜在表現の事前分布が標準正規分布から大きくずれており、VAEはAuto Encoderに近い挙動をしています。

この記事は、VAEの基本的な仕組みから、学習方法、課題、そしてStable DiffusionにおけるVAEの役割まで、幅広く解説しています。特に、VAEの潜在空間の性質や、Stable DiffusionにおけるVAEの挙動について詳しく解説している点が特徴です。


入力5

3番目のいいねが多い記事は?


出力5

asapさんのZennの記事の中で3番目にいいねが多い記事は、「💯 拡散モデルの理論って面白いよね」で、いいね数は87です。


入力6

4番目にいいねが多い記事は?中身も解説して


出力6

asapさんのZennの記事の中で4番目にいいねが多い記事は、「Open AI Realtime APIのPythonサンプルコードを作成」で、いいね数は79です。

この記事は、OpenAIが提供するRealtime APIをPythonで利用するためのサンプルコードを紹介したものです。

記事の内容

  • はじめに: OpenAIからRealtime APIが公開されたことを受け、Pythonでのサンプルコードを作成した経緯を説明しています。
  • 参考: OpenAIの公式ドキュメントを参考にコードを作成したことを述べています。
  • サンプルコード: 実際に動作するPythonのサンプルコードが掲載されています。コードは、WebsocketでOpenAIのサーバーに接続し、音声データを送受信することでリアルタイムな音声対話を実現するものです。
  • 実行結果: 実際にコードを実行した結果のデモ動画が掲載されています。
  • 詰まった点: コードを作成する上で著者が遭遇した問題点や、解決策が紹介されています。
  • コードの解説: サンプルコードの各部分について、詳細な解説が加えられています。Websocketへの接続方法、音声データの送受信方法、AIが発話した音声の文字起こしを取得する方法などが説明されています。
  • 実装できなかった部分: AIが話しているときに、ユーザーが話し始めても、AIの音声をうまく止められないという問題が残っていることが述べられています。

記事の特徴

  • 実践的な内容: OpenAIのRealtime APIを実際にPythonで利用するための具体的な手順が、サンプルコードとともに解説されています。
  • 初心者向け: コードの解説が丁寧で、Realtime APIやPythonのWebsocket通信に詳しくない読者でも理解しやすいように書かれています。
  • 問題点と解決策の提示: 著者がコードを作成する上で遭遇した問題点や、解決策が共有されており、読者が同様の問題に遭遇した場合に役立つ情報が提供されています。

この記事は、OpenAIのRealtime APIをPythonで手軽に試してみたいエンジニアにとって、非常に役立つ情報源となっています。


入力7

GRPOを導入するにあたり、著者はどういう目的があって導入しましたか?著者の意図を教えてください。


出力7

DeepSeek-R1の論文に関する記事の中で、著者がGRPOを導入した目的について解説している箇所を参考に、その意図を説明します。

著者がGRPO(Group Relative Policy Optimization)を導入した主な目的は、LLM(特にDeepSeek-R1-Zero)を強化学習で学習させる際の計算量を削減することです。

PPO(Proximal Policy Optimization)という強化学習の手法は高い性能を発揮しますが、LLMに適用することを考えると、状態価値モデルを別途学習する必要があり、計算量的に大変です。

GRPOは、PPOの状態価値モデルがやっていたこと(今回出力したトークンが、平均よりも良いのか悪いのかを判断すること)を、状態価値モデルなしで実現しようとするものです。具体的には、LLMに複数の文章を生成させ、それぞれの報酬を平均することで、Advantageを計算します。

これにより、状態価値モデルなしでAdvantageを計算できるため、計算量を大幅に削減できるというメリットがあります。

まとめると、著者の意図は以下のようになります。

  • 強化学習によるLLMの性能向上を目指す。
  • 大規模なLLMでも効率的に学習できるよう、計算量を削減する。
  • PPOの性能を維持しつつ、計算コストを削減できるGRPOを活用する。

著者は、計算量を徹底的に削減することで、高性能かつ圧倒的に格安なAPI料金の両立を達成しようという意図があったと考えられます。

LangGraphを利用しているので、対話型で実装できており、2ターン目以降は雑な質問をしても意図を理解して、その都度ページを分析してくれています。
7ターン目には、最初に解析してもらったページの内容について質問しましたが、再解析せずに会話履歴中のTool Callingの結果から、回答してくれました。

コードの解説

main関数 MCPをtoolとして利用する

基本的な処理はすべてmain関数に記載しているので、そちらを確認します。

praywrite_mcp_langchain_tools.py
async def main(graph_config = {"configurable": {"thread_id": "12345"}}):
    # モデルの定義。APIキーは環境変数から取得
    model = ChatGoogleGenerativeAI(
        model="gemini-2.0-flash",
        google_api_key=google_api_key,
        temperature=0.001,
        )

    with open("mcp_config.json", "r") as f:
        mcp_config = json.load(f)

    # messageを作成する
    message = [
        SystemMessage(content= """
あなたは役にたつAIアシスタントです。日本語で回答し、考えた過程を結論より前に出力してください。
あなたは、「PlayWrite」というブラウザを操作するtoolを利用することができます。適切に利用してユーザからの質問に回答してください。
ツールを利用する場合は、必ずツールから得られた情報のみを利用して回答してください。

まず、ユーザの質問からツールをどういう意図で何回利用しないといけないのかを判断し、必要なら複数回toolを利用して情報収集をしたのち、すべての情報が取得できたら、その情報を元に返答してください。

なお、サイトのアクセスでエラーが出た場合は、もう一度再施行してください。ネットワーク関連のエラーの場合があります。
    """),
        MessagesPlaceholder("messages"),
    ]

    # messageからプロンプトを作成
    prompt = ChatPromptTemplate.from_messages(message)

    async with MultiServerMCPClient(mcp_config["mcpServers"]) as mcp_client:
        tools = mcp_client.get_tools()

        model_with_tools = prompt | model.bind_tools(tools)

        graph = create_graph(
            GraphState,
            tools,
            model_with_tools
        )


        while True:
            query = input("入力してください: ") 

            if query.lower() in ["exit", "quit"]:
                print("終了します。")
                break

            input_query = [HumanMessage(
                    [
                        {
                            "type": "text",
                            "text": f"{query}"
                        },
                    ]
                )]

            response = await graph.ainvoke({"messages":input_query}, graph_config)

            #デバック用
            print("response: ", response)

            # 最終的な回答
            print("=================================")
            print(response["messages"][-1].content)


MCPをToolとして処理するために重要なのは下記部分です。


async with MultiServerMCPClient(mcp_config["mcpServers"]) as mcp_client:
    tools = mcp_client.get_tools()

    model_with_tools = prompt | model.bind_tools(tools)

下記を参考に実装しています。
https://github.com/langchain-ai/langchain-mcp-adapters?tab=readme-ov-file#client-1

上記のように実装することで、mcp_configに指定されているMCPをtoolsとして利用できます。
あとは、LangChainでToolを利用する際の常套手段bind_toolsを利用するだけです。

LangGraphによるAgentの実装

MCPをtoolとして定義できれば、あとはLangChain、LangGraphの世界で考えるだけです。

LangGraphは下記のように実装しています。

praywrite_mcp_langchain_tools.py
class GraphState(TypedDict):
    messages: Annotated[list[AnyMessage], operator.add]


def create_graph(state: GraphState, tools, model_chain):
    def should_continue(state: state):
        messages = state["messages"]
        last_message = messages[-1]
        if last_message.tool_calls:
            return "tools"
        return END


    def call_model(state: state):
        messages = state["messages"]
        response = model_chain.invoke(messages)
        return {"messages": [response]}


    tool_node = ToolNode(tools)
    
    workflow = StateGraph(state)
    workflow.add_node("agent", call_model)
    workflow.add_node("tools", tool_node)

    workflow.add_edge(START, "agent")
    workflow.add_conditional_edges("agent", should_continue, ["tools", END])
    workflow.add_edge("tools", "agent")
    memory = MemorySaver()
    app = workflow.compile(checkpointer=memory)
    
    return app

async def main(graph_config = {"configurable": {"thread_id": "12345"}}):
        ・・・

        graph = create_graph(
            GraphState,
            tools,
            model_with_tools
        )

            ・・・

            response = await graph.ainvoke({"messages":input_query}, graph_config)


このグラフを可視化すると下記のようになります。

このように、Agent(LLM)がTool利用の必要性があると判断すれば、どのツールを何の引数で実行するかを生成し、Toolノードが実行されます。
その処理はAgentノード(call_model関数)にて行われます。ここではbind_toolsにてツールを登録したLLMが実行されることになります。

その後、should_continue関数によって、次のノードの行き先を決定します。
LLMがツール利用を決定した場合、会話履歴の最後にてtool_calls属性が含まれるため、tool_calls属性が含まれた場合は、Toolノードに遷移します。

Toolノードでは、messegeの一個前のtool_calls属性を参照して、ツールを実行し、Agentノードに戻ります。

これを必要な回数繰り返して、LLMはブラウザの情報からユーザのタスクを処理します。

通常のLangGraphと同様に、作成したGraphのcompileを行い、実行時はinvokeメソッドの非同期版であるainvokeメソッドで実行しています。

まとめ

これでMCPをLangGraphから実行できたため、MCPをLangGraphを介してシステムに導入できるようになりました。

あまりMCPは追えてなかったのですが、面白そうなのが出たら色々試してみようと思いました。

おすすめ書籍

本記事はLangChain、LangGraphについては理解している前提で記載しております。私はLangChain、LangGraphは下記の書籍で勉強したため、おすすめとして置いておきます。
(リンクはAmazonアフィリエイトリンクです)

LangChainとLangGraphによるRAG・AIエージェント[実践]入門
ChatGPT/LangChainによるチャットシステム構築[実践]入門
今回は、さまざまなモデルを統一的に利用するために、LangChainを利用しています。
langchainに関しては、こちらの書籍を読めば大体のことはできるようになりますので、1冊目としておすすめです。

仕組みからわかる大規模言語モデル 生成AI時代のソフトウェア開発入門
最近出た本ですが、LangChainとLangGraphについてかなりのページ数を割いて説明している本です。
LangChainとLangGraphの部分だけ読んだのですが、内容しっかりしており、また細かい発展的な部分まで解説されているので、2冊目に良いのではないかなと思います。

Discussion