LangchainとPlaywright MCPでエージェントを実装してみた
めあて
- Playwright MCPでブラウザ操作を自動実行するエージェントを実装してみる
- langchainとlanggraphを活用
- ReAct型エージェントに要求をインプットし、MCP経由でPlaywrightを操作させる
方法① create_agent
langchainのcreate_agent関数を呼び出すだけでStateGraphを構築する方法です。
# pip install langchain langchain-mcp-adapters langchain-ollama ollama langchain-google-genai langgraph langfuse
import asyncio
import os
from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_ollama import ChatOllama
from langchain_google_genai import ChatGoogleGenerativeAI
from langfuse import get_client
from langfuse.langchain import CallbackHandler
# langfuse向け設定
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
os.environ["LANGFUSE_BASE_URL"] = "http://localhost:3000"
langfuse = get_client()
langfuse_handler = CallbackHandler()
# 生成AIモデル(エージェント推論用モデル)
# Google AI StudioのGemini と Ollamaのgpt-oss を使って検証
os.environ["GOOGLE_API_KEY"] = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
# llm = ChatOllama(model="gpt-oss")
# エージェント実装は、langchain.agentsのcreate_agentを使用します
async def main():
# MCPクライアントの生成
client = MultiServerMCPClient(
{
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest",
],
"transport": "stdio",
}
}
)
# エージェントのメイン処理ではMCPクライアントのsessionを維持します
# そうしないと、Playwrightで「ブラウザでの一連の操作」を扱えません
async with client.session("playwright") as session:
# MCPクライアントのsessionからMCPで定義されているtoolを取得します
# langchainではMCPサーバをToolと同様に利用することができます
tools = await load_mcp_tools(session)
# create_agentをつかってSteteGraphを構築します
agent = create_agent(llm, tools, debug=True)
# 実行
result = await agent.ainvoke(
{
"messages": [
{
"role": "user",
"content": "Access http://localhost:8501/ . And set browser size to Full HD. And submit chat twice. And save screenshot. Output he path for the screenshot file.",
}
]
},
config={"callbacks": [langfuse_handler]},
)
# 結果をプリント
for m in result["messages"]:
m.pretty_print()
print("--------------------------------------------")
if __name__ == "__main__":
asyncio.run(main())
実行して、langfuseのトレースから動作を確認すると、下記のようにllmとtoolを交互に繰り返して、一連の操作が実行されているのがわかります。llm(エージェント推論用モデル)の役割は、「次のツール呼び出しの内容」または「エージェント実行を終了」を決定することです。
- llm (Access http://localhost:8501/ . And set browser size to Full HD. And submit chat twice. And save screenshot. Output he path for the screenshot file.)
- tool (browser_navigate - http://localhost:8501/にアクセス)
- tool (browser_resize - 画面サイズをFullHDに設定)
- llm
- tool (browser_snapshot - 画面の状態を把握するためスナップショットを取得)
- llm
- tool (browser_type - チャットに入力)
- llm
- tool (browser_type - チャットに入力 2回目)
- llm
- tool (browser_take_screenshot - スクリーンショットの保存)
- llm (エージェントループを終了)
方法② langgraphでStateGraphを構築
# pip install langchain langchain-mcp-adapters langchain-ollama ollama langchain-google-genai langgraph langfuse
import asyncio
import os
import operator
from typing import TypedDict, Annotated
from langchain_core.messages import AnyMessage, HumanMessage, SystemMessage, ToolMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_ollama import ChatOllama
from langchain_google_genai import ChatGoogleGenerativeAI
from langfuse import get_client
from langfuse.langchain import CallbackHandler
from langgraph.graph import StateGraph, START, END
# langfuse向け設定
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
os.environ["LANGFUSE_BASE_URL"] = "http://localhost:3000"
langfuse = get_client()
langfuse_handler = CallbackHandler()
# 生成AIモデル(エージェント推論用モデル)
# Google AI StudioのGemini と Ollamaのgpt-oss を使って検証
os.environ["GOOGLE_API_KEY"] = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
# llm = ChatOllama(model="gpt-oss")
# langgraphによるエージェント実装は、概ね公式のquickstartの内容を流用しています
# https://docs.langchain.com/oss/python/langgraph/quickstart
# langgraphのノード間で共有されるstateの定義です
# 重要なのは各種Messageを格納するリストです(各種=System, Human, AI, Tool)
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
async def main():
# MCPクライアントの生成
client = MultiServerMCPClient(
{
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest",
],
"transport": "stdio",
}
}
)
# エージェントのメイン処理ではMCPクライアントのsessionを維持します
# そうしないと、Playwrightで「ブラウザでの一連の操作」を扱えません
async with client.session("playwright") as session:
# MCPクライアントのsessionからMCPで定義されているtoolを取得し、
# チャットモデルにbindします
# langchainではMCPサーバをToolと同様に利用することができます
tools = await load_mcp_tools(session)
model_with_tools = llm.bind_tools(tools)
# エージェントの推論用ノード
# チャットモデルの推論により、次に実行すべきTool、または、
# エージェントループを終了するべきか、を判断します
async def llm_call(state: dict):
"""LLM decides whether to call a tool or not"""
return {
"messages": [
await model_with_tools.ainvoke(
[
SystemMessage(
content="You are a helpful assistant tasked with performing e2e test on input."
)
]
+ state["messages"]
)
]
}
# Tool実行用ノード
async def tool_node(state: dict):
"""Performs the tool call"""
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = next(
(item for item in tools if item.name == tool_call["name"]), None
)
observation = await tool.ainvoke(tool_call["args"])
result.append(
ToolMessage(content=observation, tool_call_id=tool_call["id"])
)
return {"messages": result}
# graphの定義
workflow = StateGraph(AgentState)
workflow.add_node("llm_call", llm_call)
workflow.add_node("tool_node", tool_node)
workflow.add_edge(START, "llm_call")
workflow.add_conditional_edges(
"llm_call",
lambda state: (
"end" if not state["messages"][-1].tool_calls else "tool_node"
),
{"end": END, "tool_node": "tool_node"},
)
workflow.add_edge("tool_node", "llm_call")
app = workflow.compile()
# graph (エージェントループ) の実行
result = await app.ainvoke(
{
"messages": [
HumanMessage(
content="Access http://localhost:8501/ . And set browser size to Full HD. And submit chat twice. Finally save screenshot. Output the path for the screenshot file."
)
]
},
config={"callbacks": [langfuse_handler]},
)
# 結果をプリント
for m in result["messages"]:
m.pretty_print()
print("--------------------------------------------")
if __name__ == "__main__":
asyncio.run(main())
実行時のループのまわり方は①と違い、1回目のllmでスナップショット取得までの3つのTool実行が一気に行われましたが、それ以外のシーケンスは同じでした。
推論用モデルの能力
上記の動作確認ではgemini-2.5-flashを使用して、一連の操作はうまく動きました。
一方、生成AIのモデルをOllama上のgpt-ossに変更してみたところ、Tool実行がエラーとなり、処理を完遂できませんでいた。違いは生成AIモデルのみですので、gpt-ossが判断した「ツール呼び出しの内容」の精度がgemini-2.5-flashに比べて低い、と言えると思います。
おまけ:langfuseについて
langchainのCallbackHandlerとして適用することで、生成AIとのやり取りや、Tool実行の様子などを記録して確認することができます。
https://langfuse.com/self-hosting/deployment/docker-compose
の通り、docker composeでセルフホスト実行し、http://localhost:3000でコンソールにアクセスできます。最初は、適当にアカウントを作成して、プロジェクトを作成し、langchainのコードで使用するSECRET_KEYとPUBLIC_KEYを発行します。
おまけ:方法③ cagentでの実装
Dockerのオープンソース プロジェクトcagentを使用してエージェントを動かしてみる。
- releasesのページからダウンロード
- /usr/local/binなどに
cagentとして配置 - agent.yamlファイルを作成し
cagent run agent.yaml - これだけ
#!/usr/bin/env cagent run
version: 2
models:
local-gpt:
provider: dmr
model: ai/gpt-oss:20B-UD-Q4_K_XL
agents:
root:
model: local-gpt
description: E2E test automation agent
instruction: |
You are a professional e2e tester.
Operate playwright according to user input (test senario).
toolsets:
- type: mcp
command: npx
args:
- "@playwright/mcp@latest"
Discussion