用 Playwright MCP 和 Ollama 搭一个更稳的浏览器自动化 Agent

简介: 本文分享了一种基于AI的自适应网页自动化方案:用Ollama本地运行Qwen3/Phi4等轻量模型,结合Playwright MCP与LangGraph构建浏览器Agent。AI通过可访问性快照理解页面,自动选择鲁棒定位器(如role/text),无需硬编码CSS,显著提升面对页面改版的稳定性。含完整环境配置、工具封装、状态管理与避坑指南。

前阵子在做项目的时候遇到一个挺烦人的问题:需要定期从某个内部系统抓数据,但那个系统三天两头改页面结构,写死的 CSS 选择器动不动就失效。改脚本改到怀疑人生。

后来试了试让 AI 来干这活——用 Ollama 跑本地模型,结合 Playwright 操作浏览器。折腾了一段时间,踩了不少坑,今天把比较稳的一套方案整理出来。

为什么选这个组合
先说 Playwright MCP。MCP(Model Context Protocol)是 Anthropic 去年推的一个开放标准,说白了就是让 AI 能直接调用外部工具。Playwright MCP 是这个协议的一个服务器实现,把 Playwright 的浏览器操作能力封装成了一套标准工具接口。

和传统写死脚本的方式比,最大的区别在于:AI 不是靠猜选择器来操作页面,而是通过结构化的可访问性快照来理解页面。页面结构变了,AI 能自己调整,不用你手动改代码。

Ollama 这边就不多介绍了,本地跑 LLM 最省事的方案。模型的话,实测下来 qwen3:4b 或者 phi4-mini 这种规模的就够用。你要是任务复杂,可以上 7B 甚至 14B 的,但说实话大部分浏览器自动化场景用不着那么大。

环境准备
先把东西装上:

安装 Playwright MCP

npm install -g @playwright/mcp

安装浏览器驱动

npx playwright install chromium

安装 Python 依赖

pip install ollama langgraph playwright

拉模型(选一个就行)

ollama pull qwen3:4b

或者

ollama pull phi4-mini
有一点要注意:Ollama 必须跑在后台。ollama serve 默认监听 http://localhost:11434,不用改什么配置。

核心思路:把浏览器操作变成工具
整个 Agent 的核心逻辑其实不复杂:你把 Playwright 的常用操作封装成一个个工具函数,LLM 根据用户指令决定调用哪些工具、按什么顺序调。

我用的方案是 LangGraph + Ollama。LangGraph 负责控制 Agent 的执行流程,Ollama 负责推理决策。

先封装浏览器工具:

browser_tools.py

from playwright.sync_api import sync_playwright
import base64

_playwright = None
_browser = None
_page = None

def start_browser():
global _playwright, _browser, _page
_playwright = sync_playwright().start()
_browser = _playwright.chromium.launch(headless=False) # 调试时建议显示浏览器
_page = _browser.new_page()
return"浏览器已启动"

def goto_url(url: str) -> str:
_page.goto(url, timeout=30000)
returnf"已打开 {_page.title()}"

def fill_input(selector: str, text: str) -> str:
_page.fill(selector, text)
returnf"已在 {selector} 输入: {text}"

def click_button(selector: str) -> str:
_page.click(selector)
returnf"已点击 {selector}"

def get_page_text() -> str:
return _page.inner_text("body")[:3000] # 截断防止 token 爆炸

def take_screenshot() -> str:
b64 = base64.b64encode(_page.screenshot()).decode("utf-8")
return b64

def close_browser():
global _playwright
_browser.close()
_playwright.stop()
return"浏览器已关闭"
这些函数看着简单,但每个都藏着坑。后面会细说怎么让它们更稳。

搭建 Agent
有了工具,下一步是让 LLM 学会用它们。我这里用 LangGraph 搭了一个简单的 ReAct 风格的 Agent:

browser_agent.py

import json
import ollama
from typing import TypedDict
from langgraph.graph import StateGraph, END
from browser_tools import *

MODEL = "qwen3:4b"

TOOLS_DESC = """
可用工具:

  1. goto_url(url) - 打开网页
  2. fill_input(selector, text) - 在输入框填入内容
  3. click_button(selector) - 点击按钮
  4. get_page_text() - 获取当前页面文本
  5. take_screenshot() - 截图(返回 base64)
  6. close_browser() - 关闭浏览器

当你需要操作浏览器时,输出 JSON 格式的步骤列表:
{"steps": [{"tool": "工具名", "args": {...}}, ...]}
如果用户问题不需要浏览器,直接回答。
"""

class BrowserState(TypedDict):
user_input: str
steps: list
result: str

def agent_node(state: BrowserState):
prompt = f"{TOOLS_DESC}\n\n用户指令:{state['user_input']}\n请输出操作步骤:"
response = ollama.chat(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
format="json"# 强制 JSON 输出
)
data = json.loads(response["message"]["content"])
return {"steps": data.get("steps", [])}

def execute_node(state: BrowserState):
results = []
for step in state["steps"]:
tool = step["tool"]
args = step.get("args", {})
if tool == "goto_url":
results.append(goto_url(args))
elif tool == "fill_input":
results.append(fill_input(
args))
elif tool == "click_button":
results.append(click_button(**args))

    # ... 其他工具类似
return {"result": "\n".join(results)}

构建图

graph = StateGraph(BrowserState)
graph.add_node("agent", agent_node)
graph.add_node("execute", execute_node)
graph.add_edge("agent", "execute")
graph.add_edge("execute", END)
graph.set_entry_point("agent")

app = graph.compile()
跑起来大概是这样的:

result = app.invoke({"user_input": "打开百度,搜索 Playwright 教程,截图返回"})
print(result["result"])
人工智能技术学习交流群
伙伴们,对AI测试、大模型评测、质量保障感兴趣吗?我们建了一个 「人工智能测试开发交流群」,专门用来探讨相关技术、分享资料、互通有无。无论你是正在实践还是好奇探索,都欢迎扫码加入,一起抱团成长!期待与你交流!👇

图片
让 Agent 更稳的几个关键点
上面这套能跑,但离“稳”还差得远。下面是我踩坑之后总结的几个关键优化。

  1. 选择器要够鲁棒
    这是最大的坑。LLM 生成的 selector 经常是精确匹配的 CSS 选择器,页面稍微一变就完蛋。

Playwright 本身支持 基于角色的定位器(get_by_role)和 基于文本的定位器(get_by_text),这些比 CSS 选择器稳定得多。但问题是 LLM 默认不太会用。

我的做法是在工具描述里显式告诉 LLM 优先用什么:

TOOLS_DESC = """
填表时,优先使用以下方式定位元素(按优先级):

  1. placeholder 文本:input[placeholder="搜索"]
  2. 角色+名称:role="button", name="登录"
  3. aria-label:[aria-label="关闭"]
  4. 最后才用 class 或 id
    """
    实测下来,加了这段提示之后 selector 失效的概率降了一大截。

  5. 超时和重试不能省
    Playwright 默认的操作超时是 30 秒,但网络慢的时候经常不够。我的做法是把超时设到 60 秒,然后封装一层带重试的调用:

def robust_click(selector: str, retries: int = 3):
for i in range(retries):
try:
_page.click(selector, timeout=60000)
return f"已点击 {selector}"
except Exception as e:
if i == retries - 1:
return f"点击失败: {str(e)}"

        # 等待一下再试
        time.sleep(2 ** i)  # 指数退避

重试的时候用指数退避(1s、2s、4s),别一失败就立刻重试,那样大概率还是失败。

  1. 状态管理要清晰
    多轮对话场景下,浏览器状态容易乱。我遇到过 Agent 以为浏览器还开着,实际上早就关了的尴尬情况。

解决办法是在 State 里记录浏览器状态:

class BrowserState(TypedDict):
user_input: str
browser_running: bool # 新增
steps: list
result: str
每次执行工具前先检查状态,该启动的启动,该关闭的关闭。

  1. 日志要能救命
    Agent 跑起来像个黑盒,出错了根本不知道是哪一步的问题。

我加了个简单的日志装饰器:

import logging
logging.basicConfig(level=logging.INFO)

def log_tool(func):
def wrapper(args, **kwargs):
logging.info(f"调用 {func.name},参数: {kwargs}")
try:
result = func(
args, **kwargs)
logging.info(f"{func.name} 成功")
return result
except Exception as e:
logging.error(f"{func.name} 失败: {str(e)}")
raise
return wrapper
每个工具函数都加上 @log_tool,出问题了看日志就知道卡在哪。

  1. 模型选择有讲究
    不同模型对 function calling 的支持程度不一样。我试过几个:

qwen3:4b:中文支持好,指令理解准确,性价比高
phi4-mini:function calling 比较稳,但需要自己写 Modelfile
llama3.2:英文场景表现不错,中文稍微差点
如果你主要处理中文网页,qwen3:4b 是不错的选择。任务复杂的话可以上 qwen3:7b 或者 deepseek-r1:7b。

实际跑起来的效果
拿一个实际场景测试:让 Agent 去某招聘网站搜索“Python 工程师”,然后把前 10 条结果的职位名和公司名整理成表格。

传统脚本大概要写 50-80 行代码,而且页面一改就废。用这套 Agent,指令就一句话:

“打开 xx 招聘网,搜索 Python 工程师,把前 10 条结果的职位和公司整理成表格”

Agent 会自己决定:先打开首页 → 找到搜索框输入关键词 → 点击搜索 → 等待结果加载 → 提取数据 → 整理输出。

第一次跑可能不太顺,但把失败的 selector 反馈给 LLM 之后,第二次基本就能成。

说几个坑
最后说几个我踩过的坑,省得你再踩一遍。

Ollama 的 JSON 模式有时候不听话。加了 format="json" 之后偶尔还是会输出带 markdown 的文本,解析就挂了。我的处理方式是在解析失败的时候再调一次 Ollama,prompt 里加一句“只输出 JSON,不要其他内容”。

headless 模式调试很痛苦。一开始我开了 headless=True,出错了根本不知道浏览器里发生了什么。建议开发阶段先开界面模式(headless=False),跑通了再关掉。

MCP 服务器的端口冲突。如果你同时跑多个 MCP 服务,注意端口别冲突。默认是 8931,可以自己指定。

大页面内容截断要小心。get_page_text() 我截到 3000 字符,但有些页面光导航栏就不止这个数。可以根据任务类型动态调整截断长度,或者用 take_screenshot() 配合视觉模型来分析。

小结
这套方案的核心就三样东西:Playwright 当手,Ollama + 小模型当脑,LangGraph 当神经。和传统写死脚本的方式比,最大的优势是自适应能力强——页面结构变了,AI 能自己调整,不用你每次都改代码。

当然它也不是银弹。复杂交互(比如拖拽、画布操作)目前还不太行,模型太小的话多步任务也容易翻车。但对于表单填写、数据抓取、自动化测试这类场景,已经足够用了.

相关文章
|
1月前
|
人工智能 自然语言处理 JavaScript
别再手动“点鼠标”了!保姆级教程:让AI教你用Playwright解放双手
本文介绍2026年零代码浏览器自动化新范式:无需编程,仅靠自然语言即可驱动Playwright完成登录、导出、截图等操作。详解三大方案——录制生成(零门槛)、MCP语音指挥(最直观)、AI测试代理(专业级),助你10分钟上手,把重复劳动交给AI,专注真正有价值的事。
|
1月前
|
人工智能 JSON 测试技术
保姆级教程:从零手搓一个 Agent Skill,让AI变成你的专属助手
本文详解AI工程化新范式——Agent Skill:将隐性经验封装为可复用、可复现、可进化的标准化流程。通过实战手搓邮件Skill,揭示其三层渐进加载机制与落地路径,助你告别低效对话,迈向流程驱动的AI协作新时代。
146Echarts - 矩形树图(Basic Treemap)
146Echarts - 矩形树图(Basic Treemap)
738 0
|
API 网络安全 网络虚拟化
Google Play 上架总结(二)Google账户关联详解
Google Play 上架总结(二)Google账户关联详解
2515 0
|
1月前
|
人工智能 JavaScript 测试技术
推荐一款开源工具:让 AI Agent 替你做 iOS、Android 自动化测试,能平替 Appium?
Appium统治移动端自动化测试12年,但脚本复杂、维护成本高。新开源工具agent-device(Callstack出品)开启范式革命:专为AI Agent设计,通过语义化元素引用(@e1)、全链路证据采集、探索→回放机制,让AI“看懂”界面自主操作,非Appium替代品,而是下一代智能测试基础设施。
279 0
|
17天前
|
人工智能 自然语言处理 测试技术
不用写一行代码的测试时代来了:2026年AI测试智能体搭建全指南
本文探讨2026年AI测试智能体带来的范式革命:从“写脚本”迈向“说人话”。无需编码,仅凭自然语言指令即可完成端到端测试;AI自动理解意图、定位元素、执行操作并智能断言。涵盖Harness、Autonoma、qpilot等主流方案对比与实操指南,并揭示落地避坑要点与人机协同新趋势。
|
10月前
|
设计模式 前端开发 测试技术
告别脆弱:构建稳定UI自动化测试的3个核心策略
告别脆弱:构建稳定UI自动化测试的3个核心策略
887 113
|
1月前
|
人工智能 JavaScript 架构师
从零开始学 OpenCode:10 分钟在终端里跑起你的 AI 编程助手
OpenCode 是开源免费的终端优先AI编程智能体,支持75+模型(Claude/GPT/Ollama等),采用Plan/Build双模式——先规划再编码,保障准确性。本地优先、隐私安全,3分钟即可上手,让想法秒变可靠代码。
|
9月前
|
监控 前端开发 测试技术
Playwright为什么老是跑不稳?12个坑踩完我终于懂了!
周五下班前,测试全绿、CI顺畅,才是理想状态。若Playwright测试常慢、失败、截图冗余,说明需优化。本文12条实战建议:用例按风险分层、稳定定位、去sleep、复用登录态、API准备数据、合理mock、精准视觉回归、按需trace、控制并发、封装业务流、追踪不稳用例、标准化报告。让发版安心,告别焦虑。
|
3月前
|
人工智能 前端开发 测试技术
Qoder Skills 完全指南:从零开始,让 AI 按你的标准执行
文章内容基于作者个人技术实践与独立思考,旨在分享经验,仅代表个人观点。
2969 3

热门文章

最新文章