Machine Learning Mastery

Building Browser-Using AI Agents in Python

8.5内容质量

TL;DR · AI 摘要

本文介绍了如何使用 Playwright 和 LangGraph 构建能够与真实网站交互的 AI 代理,适用于没有 API 的场景。

核心要点

  • Playwright 比 Selenium 快 30-50%,适合 2026 年的新项目。
  • AI 代理可以处理 95% 的日常任务,而不仅仅是 5%。
  • 7% 的企业已在生产环境中使用浏览器代理。

结构提纲

按章节快速跳转。

  1. 本文介绍如何构建能够与真实网站交互的 AI 代理,适用于没有 API 的场景。

  2. Playwright 比 Selenium 快 30-50%,适合 2026 年的新项目。

  3. 27.7% 的企业已在生产环境中使用浏览器代理,预计到 2030 年市场将达到 503.1 亿美元。

  4. 本文将指导读者如何构建一个能够导航网站、填写表单并连接 LLM 的 AI 代理。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • 构建浏览器使用 AI 代理
    • 引言
      • AI 代理的市场前景
    • 为什么选择 Playwright
      • Playwright 的性能优势
    • 构建 AI 代理的步骤
      • 使用 Playwright 和 LangGraph

金句 / Highlights

值得收藏与分享的关键句。

#Playwright#AI 代理#Python#浏览器自动化
打开原文

使用 Python 构建能够使用浏览器的 AI 代理 - MachineLearningMastery.com

使用 Python 构建能够使用浏览器的 AI 代理

作者:

Shittu Olumide

2026 年 6 月 22 日

人工智能

0

分享

文章

在本文中,你将学习如何使用 Playwright、浏览器使用和 LangGraph 构建能够浏览和与真实网站进行交互的 AI 代理。

我们将涵盖的主题包括:

  • 为什么 Playwright 是 2026 年浏览器自动化最合适的基石,以及它与 Selenium 的区别。
  • 如何可靠地抓取动态的、由 JavaScript 渲染的页面并完成多步骤表单。
  • 如何将浏览器操作集成到 LangGraph 和浏览器使用代理中,处理反爬虫检测,管理等待和会话持久性,并在 Docker 中部署结果。

使用 Python 构建能够使用浏览器的 AI 代理

介绍

大多数 AI 代理教程都从 API 开始。它们向你展示如何调用 OpenWeather,访问 Stripe 的端点,从 GitHub 提取数据。这虽然是一个不错的起点,但当你尝试构建一个真实的东西并意识到你实际需要完成的任务没有 API 时,这个起点就显得不足了。

想想人类每天在浏览器上做什么:填写政府表格、阅读竞争对手的价格、从那些用 JavaScript 渲染保护数据的网站提取研究信息、登录那些从未听说过 OAuth 的门户。互联网上大约有 11 亿个网站。其中只有极小一部分拥有公开的 API。其余的网站只与浏览器“对话”。

一个仅限于 API 调用的代理可能只能处理人类员工每天完成任务的 5%。给这个代理一个浏览器,它的覆盖范围就接近于所有任务。这就是本文要填补的空白。

2026 年,全球 AI 代理市场价值达到 109.1 亿美元,并预计到 2030 年将达到 503.1 亿美元,其中具备浏览器功能的代理是这一增长的核心。目前,27.7% 的企业已经在生产环境中运行代理浏览器,而两年前几乎没有任何企业这样做。工具已经迅速成熟,模式也已稳定到可以正确教授的程度。

在本文结束时,你将拥有一个能够导航真实网站、填写表单、提取结构化数据,并连接到一个决定下一步该做什么的 LLM 的工作中的浏览器代理,所有操作都使用 Python 完成。

为什么选择 Playwright 而不是 Selenium

如果你五年前构建过浏览器自动化,那么你使用的是 Selenium。Selenium 仍然被广泛部署,仍然有效,并且不会消失。但就 2026 年的任何新项目而言,Playwright 是默认的选择。原因很实际,而不是理论上的。

Selenium 通过向 WebDriver 发送单独的 HTTP 请求与浏览器通信。每个操作,如点击、输入、滚动,都是一个单独的请求。Playwright 则在整个会话中使用一个持久的 WebSocket 连接。命令通过该通道传输,没有每个操作的往返成本。独立的基准测试一致显示,Playwright 在测试套件级别上比 Selenium 快 30% 到 50%,平均每个操作耗时约 290 毫秒,而 Selenium 的平均耗时约为 536 毫秒。对于可能执行数百个操作的浏览器代理来说,这种差距会累积起来。

Playwright 还自带了其自身的浏览器二进制文件。当你安装它时,你会获得预配置的 Chromium、Firefox 和 WebKit 版本,这些版本可以保证与你的 Playwright 版本兼容。无需担心驱动版本不匹配的问题,也不用因为有人更新了 Chrome 而导致 CI 管道出错。它在点击元素之前内置了自动等待功能;它会验证该元素是否可见、是否启用且没有动画。你不需要编写 time.sleep(2) 并祈祷一切顺利。

对于 AI 代理来说,Playwright 会触发真实的鼠标和键盘事件,这些事件与人类如何与浏览器交互的方式一致。那些设计用来检测自动化操作的网站会寻找合成的 DOM 点击。Playwright 的交互模型更难以与真实的人类输入区分开来。

并排的架构比较图(点击放大)

还有一个浏览器使用库,它位于上一层。Browser-use 是一个 Python 库,它为 LLM 提供一个可用的浏览器。在底层,它使用 Playwright 来控制浏览器,但 LLM 会读取页面状态并决定点击、输入和提取内容,无需使用 CSS 选择器。你只需用普通英语给它一个任务,它就能处理其余部分。在本文中,我们将同时介绍原始的 Playwright 和 browser-use,因为它们满足不同的需求:当你需要精确且可预测的控制时使用 Playwright;当你希望代理自主处理导航决策时使用 browser-use。

设置环境

你需要 Python 3.10 或更高版本、一个 OpenAI API 密钥,以及大约五分钟的时间。

步骤 1:创建虚拟环境

python -m venv browser_agent_env # macOS / Linux source browser_agent_env/bin/activate # Windows browser_agent_env\Scripts\activate

1

2

3

4

5

6

7

python

-

m

venv

browser_agent

_

env

macOS / Linux

source

browser_agent_env

/

bin

activate

Windows

\

Scripts

步骤 2:安装依赖项

pip install playwright \ browser-use \ langchain \ langchain-openai \ langgraph \ langchain-community \ python-dotenv

pip

install

playwright

browser

use

langchain

openai

langgraph

community

dotenv

步骤 3:安装浏览器二进制文件 这是大多数人容易遗漏的步骤。Playwright 需要从 Python 包之外单独下载 Chromium、Firefox 和 WebKit。安装后运行一次:

playwright install chromium

chromium

如果你想安装所有三个浏览器引擎:playwright install . 对于大多数代理工作来说,仅安装 Chromium 就足够了,而且下载体积更小。

步骤 4:存储你的 API 密钥 在你的项目目录中创建一个 .env 文件:

OPENAI_API_KEY=your_openai_api_key_here

OPENAI_API_KEY

=

your_openai_api_key_here

立即将 .env 添加到你的 .gitignore 文件中。不要提交 API 密钥。

步骤 5:验证一切是否正常运行 这是一个首次运行的脚本,它会导航到一个 URL,读取标题并保存截图。使用 example.com,这是一个由 IANA 维护的公开可用的测试域名,不会阻止你访问。

如何运行:保存为 first_run.py 并运行 python first_run.py

first_run.py

导航到一个 URL,截取屏幕截图,并提取页面标题。

先决条件:pip install playwright && playwright install chromium

运行方式:python first_run.py

import asyncio

from playwright.async_api import async_playwright

async def main():

async with async_playwright() as p:

以无头模式启动 Chromium(不显示浏览器窗口)。

如果希望在开发过程中观看其运行,请将 headless=False。

browser = await p.chromium.launch(headless=True)

浏览器上下文类似于一个全新的浏览器配置文件。

它将 cookie、存储和缓存与其他上下文隔离。

context = await browser.new_context(

viewport={"width": 1280, "height": 720},

user_agent=(

"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "

"AppleWebKit/537.36 (KHTML, like Gecko) "

"Chrome/120.0.0.0 Safari/537.36"

)

)

page = await context.new_page()

导航到 URL 并等待网络空闲。

“networkidle” 表示 500 毫秒内没有打开的网络连接。

对于更快的页面,“domcontentloaded” 就足够了。

await page.goto("https://example.com", wait_until="networkidle")

提取页面标题

title = await page.title()

print(f"页面标题: {title}")

提取 h1 标题的文本内容

h1 = await page.text_content("h1")

print(f"H1 标题: {h1}")

截取整个页面的截图并保存到磁盘

await page.screenshot(path="screenshot.png", full_page=True)

print("截图已保存到 screenshot.png")

await browser.close()

asyncio.run(main())

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

first_run.py

导航到一个 URL,截取屏幕截图,并提取页面标题。

先决条件:pip install playwright && playwright install chromium

运行方式:python first_run.py

import

asyncio

from

.

async_api

async_playwright

async

def

main

(

)

:

with

as

p

以无头模式启动 Chromium(不显示浏览器窗口)。

如果希望在开发过程中观看其运行,请将 headless=False。

await

launch

headless

True

浏览器上下文类似于一个全新的浏览器配置文件。

它将 cookie、存储和缓存与其他上下文隔离。

context

new_context

viewport

{

"width"

1280

,

"height"

720

}

user_agent

"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "

"AppleWebKit/537.36 (KHTML, like Gecko) "

"Chrome/120.0.0.0 Safari/537.36"

page

new_page

导航到 URL 并等待网络空闲。

“networkidle” 表示 500 毫秒内没有打开的网络连接。

对于更快的页面,“domcontentloaded” 就足够了。

goto

"https://example.com"

wait_until

"networkidle"

提取页面标题

title

print

f

"页面标题: {title}"

提取 h1 标题的文本内容

h1

text_content

"h1"

"H1 标题: {h1}"

截取整个页面的截图并保存到磁盘

screenshot

path

"screenshot.png"

full_page

"截图已保存到 screenshot.png"

close

run

这是它所做的事情:async_playwright() 是整个 Playwright 会话的入口点。浏览器上下文相当于打开一个全新的无痕窗口;cookie、本地存储和缓存与其他内容隔离。wait_until="networkidle" 告诉 Playwright 在页面完成所有网络活动之前,代码不会继续执行,这对于动态页面来说是最安全的等待策略。

如果它运行并保存了截图,那么您的环境工作正常。

Web 导航和抓取

你需要使用 Playwright 而不是 requests + BeautifulSoup 的原因在于 JavaScript 渲染。现代网站在页面加载后会动态生成实际内容,只提供一个 HTML 框架:React、Vue、Angular、Next.js。普通的 HTTP 请求只能获取框架内容。Playwright 运行一个真正的浏览器,因此它能看到 JavaScript 执行后人类看到的完整页面。

以下目标是 books.toscrape.com,这是一个为练习而构建的合法爬虫沙盒网站。它使用分页,使用动态类名表示评分,并且非常接近真实电子商务产品页面的结构。

如何运行:保存为 scrape_books.py 并运行 python scrape_books.py

scrape_books.py

从 books.toscrape.com 抓取书籍标题、价格和评分

这是一个为练习而构建的合法爬虫沙盒网站。

依赖项:pip install playwright && playwright install chromium

如何运行:python scrape_books.py

import asyncio import json from playwright.async_api import async_playwright

async def scrape_books(max_pages: int = 3) -> list[dict]: """跨多页从 books.toscrape.com 抓取书籍列表。 返回包含标题、价格、评分和页码的字典列表。 """ results = [] async with async_playwright() as p: browser = await p.chromium.launch(headless=True) context = await browser.new_context(viewport={"width": 1280, "height": 720}) page = await context.new_page() for page_num in range(1, max_pages + 1): url = f"https://books.toscrape.com/catalogue/page-{page_num}.html" print(f"正在抓取页面 {page_num}: {url}") await page.goto(url, wait_until="domcontentloaded")

在提取内容之前等待产品卡片可见。

这对于 JavaScript 内容较多的页面非常重要,因为内容在 HTML 加载后才加载。

timeout=10000 表示在引发错误之前最多等待 10 秒。

await page.wait_for_selector("article.product_pod", timeout=10000)

获取当前页面上的所有书籍卡片

books = await page.query_selector_all("article.product_pod") for book in books:

从 <a> 标签的 title 属性中提取标题

title_el = await book.query_selector("h3 a") title = await title_el.get_attribute("title") if title_el else "N/A"

提取价格文本

price_el = await book.query_selector(".price_color") price = await price_el.inner_text() if price_el else "N/A"

从 CSS 类名中提取星级评分。

例如 <p class="star-rating Three"> → "Three"

rating_el = await book.query_selector("p.star-rating") rating_class = await rating_el.get_attribute("class") if rating_el else "" rating = rating_class.replace("star-rating", "").strip() results.append({ "title": title, "price": price, "rating": rating, "page": page_num }) print(f"从页面 {page_num} 提取了 {len(books)} 本书") await browser.close() return results

async def main(): books = await scrape_books(max_pages=2) print(f"\n总共抓取了 {len(books)} 本书") print(json.dumps(books[:3], indent=2))

asyncio.run(main())

"https://books.toscrape.com/catalogue/page-{page_num}.html"

"正在抓取页面 {page_num}: {url}"

"domcontentloaded"

等待产品卡片可见后再进行提取。

在内容在 HTML 加载之后才加载的 JavaScript 重的页面上,这一点至关重要。

timeout=10000 表示最多等待 10 秒,否则会引发错误。

wait_for_selector

"article.product_pod"

timeout

10000

获取当前页面上的所有书籍卡片

books

query_selector_all

book

从 <a> 标签的 title 属性中提取标题

title_el

query_selector

"h3 a"

get_attribute

"title"

if

else

"N/A"

提取价格文本

price_el

".price_color"

price

inner_text

从 CSS 类名中提取星级评分。

例如 <p class="star-rating Three"> → "Three"

rating_el

"p.star-rating"

rating_class

"class"

rating

replace

"star-rating"

strip

append

"price"

"rating"

"page"

num

" 从页面 {page_num} 提取了 {len(books)} 本书"

return

"\n总共抓取的书籍数量: {len(books)}"

dumps

indent

这是做什么的:wait_for_selector() 是这里的关键调用。与其固定等待一段时间并希望内容已经加载,它会监视 DOM 并在目标元素一出现时立即继续执行,或者在超时窗口内目标元素没有出现时引发 TimeoutError。这是正确的做法:快速且明确地失败,而不是在空白页面上默默地提取数据。

星级评分的提取值得特别关注。星级评分是作为 CSS 类(star-rating Three)编码的,而不是数字。代码会从类字符串中剥离“star-rating”以获得文本值。这是你只有通过实际检查 HTML 才能知道的事情。当你将这个任务交给没有浏览器的原始 LLM 时,它无法知道类结构的外观。使用 Playwright,你可以直接检查它并准确提取。

表单填写和多步骤流程

填写表单是浏览器代理发挥价值的地方,也是大多数自动化脚本失败的地方。原因在于网页表单不仅仅是输入框和按钮。它们按顺序触发 focus、input、change 和 blur 事件。JavaScript 验证会监听这些事件。如果你通过直接在 DOM 中设置值(如旧的自动化工具经常做的那样)将值注入到输入字段中,验证监听器永远不会触发,表单就会出错。

Playwright 的 fill() 和 click() 方法会按正确的顺序触发真实的浏览器事件,这就是为什么它们可以处理会阻止低级方法的表单验证。

下面的目标是 the-internet.herokuapp.com/login,这是一个专门为自动化练习维护的公共测试网站。它接受 tomsmith / SuperSecretPassword! 作为有效凭据,并返回清晰的成功/失败消息。

如何运行:保存为 form_submit.py 并运行 python form_submit.py

form_submit.py

在公共演示站点上完成并提交一个包含多个字段的登录表单。

目标:https://the-internet.herokuapp.com/login(公共测试站点)

运行方式:python form_submit.py

import asyncio from playwright.async_api import async_playwright

async def login_and_verify(username: str, password: str) -> dict: """尝试登录到演示站点,并返回是否成功。 处理内容包括:填写输入框、点击按钮以及验证结果。""" async with async_playwright() as p: browser = await p.chromium.launch(headless=True) context = await browser.new_context() page = await context.new_page() await page.goto("https://the-internet.herokuapp.com/login")

在交互之前等待表单可见。

state="visible" 是默认值,但可以明确表达意图。

await page.wait_for_selector("#username", state="visible")

fill() 会先清空字段,然后输入值。

它依次触发 focus、input 和 change 事件。

await page.fill("#username", username) await page.fill("#password", password)

click() 触发真实的鼠标事件 -- mousedown、mouseup、click。

这会触发普通 DOM 点击无法触发的 JavaScript 监听器。

await page.click("button[type='submit']")

表单提交后等待页面稳定

await page.wait_for_load_state("networkidle")

检查哪个结果元素出现

success_el = await page.query_selector(".flash.success") error_el = await page.query_selector(".flash.error")

if success_el: message = await success_el.inner_text() result = {"success": True, "message": message.strip()} elif error_el: message = await error_el.inner_text() result = {"success": False, "message": message.strip()} else: result = {"success": False, "message": "未知结果"}

await browser.close() return result

async def main():

用于演示站点的有效凭据

result = await login_and_verify("tomsmith", "SuperSecretPassword!") print(f"有效登录: {result}")

用于验证错误处理的无效凭据

result_fail = await login_and_verify("wronguser", "wrongpass") print(f"无效登录: {result_fail}")

asyncio.run(main())

"无效登录:{result_fail}"

这是做什么的:这里的模式,fill() → click() → wait_for_load_state() → 检查结果元素,是几乎所有表单交互的模板。提交后的 wait_for_load_state("networkidle") 是非常重要的:如果没有它,你将在页面更新之前查询 DOM,从而得到提交前的状态,而不是结果。

对于具有文件上传、下拉框和复选框的更复杂表单:

文件上传

await page.set_input_files("#file-upload", "/path/to/document.pdf")

通过可见标签文本选择下拉框

await page.select_option("#country-select", label="Nigeria")

勾选复选框

await page.check("#agree-terms")

处理模态对话框(确认/警告)

page.on("dialog", lambda dialog: asyncio.ensure_future(dialog.accept()))

文件上传

set_input_files

"#file-upload"

"/path/to/document.pdf"

通过可见标签文本选择下拉框

select_option

"#country-select"

label

"Nigeria"

勾选复选框

check

"#agree-terms"

处理模态对话框(确认/警告)

"dialog"

lambda

dialog

ensure_future

accept

使用 LangChain 和 LangGraph 进行工具编排

原始的 Playwright 脚本功能强大但固定。它们只执行你编写的内容,不会做更多。一旦页面结构发生变化,或者任务需要一个脚本未预料到的决策,它们就会失效。

将 Playwright 连接到 LLM 会改变这一点。浏览器操作变成代理在决定需要时可以调用的工具。代理阅读任务,思考该做什么,调用一个工具,读取结果,然后决定下一步该做什么。这个循环可以处理固定脚本无法处理的变化。

这是从“浏览器自动化脚本”到“AI 代理”的桥梁。

如何运行:保存为 agent_tools.py,确保 OPENAI_API_KEY 在你的 .env 中,然后运行 python agent_tools.py

agent_tools.py

LangGraph 代理,包含三个浏览器工具:navigate_and_extract、fill_and_submit_form、take_screenshot

先决条件:pip install playwright langchain langchain-openai langgraph python-dotenv

安装:playwright install chromium

如何运行:python agent_tools.py

import asyncio import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langchain.tools import tool from langchain_core.messages import HumanMessage from langgraph.prebuilt import create_react_agent from playwright.async_api import async_playwright

load_dotenv()

── 共享的浏览器状态 ──────────────────────────────────────────────────────

我们在整个代理生命周期中保持一个浏览器实例存活。

在每次工具调用时创建和销毁浏览器实例会很慢且浪费资源。

_browser = None _page = None _playwright = None

async def get_page(): """返回共享的页面,如果需要的话启动浏览器。""" global _browser, _page, _playwright if _browser is None: _playwright = await async_playwright().start() _browser = await _playwright.chromium.launch(headless=True) context = await _browser.new_context(viewport={"width": 1280, "height": 720}) _page = await context.new_page() return _page

async def close_browser(): """当代理会话结束时清理浏览器资源。""" global _browser, _page, _playwright if _browser: await _browser.close() await _playwright.stop() _browser = None _page = None _playwright = None

── 浏览器工具 ─────────────────────────────────────────────────────────────

注意:这些是异步工具(async def)。LangChain 的 @tool 装饰器直接支持异步函数,

并且代理必须使用 ainvoke() 调用,这样工具调用可以在同一个事件循环中运行,而不是尝试启动第二个循环。

@tool async def navigate_and_extract(url: str) -> str: """导航到一个 URL 并返回页面的可见文本内容。使用此功能访问网站并读取其内容。 输入:一个完整的 URL 字符串,包括 https://(例如,'https://example.com')。""" page = await get_page() await page.goto(url, wait_until="domcontentloaded", timeout=15000) await page.wait_for_load_state("networkidle") content = await page.inner_text("body")

截断以避免淹没 LLM 上下文窗口

return content[:3000] if len(content) > 3000 else content

@tool async def fill_and_submit_form(selector_value_pairs: str) -> str: """填写表单字段并提交当前加载页面上的表单。 输入:一个由 'selector:value' 对组成的逗号分隔字符串,以 'submit:button_selector' 结尾。 示例:'#email:user@example.com,#password:secret,submit:button[type=submit]'""" page = await get_page() try: pairs = selector_value_pairs.split(",") submit_selector = None for pair in pairs: key, val = pair.split(":", 1) key = key.strip() val = val.strip() if key == "submit": submit_selector = val else: await page.fill(key, val) if submit_selector: await page.click(submit_selector) await page.wait_for_load_state("networkidle") return f"表单已提交。当前 URL:{page.url}" except Exception as e: return f"表单交互失败:{str(e)}"

@tool async def take_screenshot(filename: str) -> str: """对当前浏览器页面进行截图并保存到文件中。 使用此功能可以直观验证页面的当前状态。 输入:文件名字符串(例如,'result.png')。""" page = await get_page() await page.screenshot(path=filename, full_page=False) return f"截图已保存到 {filename}"

── 代理设置 ───────────────────────────────────────────────────────────────

llm = ChatOpenAI( model="gpt-4o", temperature=0, api_key=os.getenv("OPENAI_API_KEY") )

tools = [navigate_and_extract, fill_and_submit_form, take_screenshot]

create_react_agent 将 LLM、工具和 ReAct 推理循环连接在一起。

代理决定调用哪个工具,调用它,读取结果,并继续。

agent = create_react_agent(llm, tools)

── 示例 ──────────────────────────────────────────────────────────────────────

async def main(): result = await agent.ainvoke({ "messages": [HumanMessage( content=( "前往 https://example.com,读取页面内容," "然后将截图保存为 example.png" ) )] }) print(result["messages"][-1].content) await close_browser()

asyncio.run(main())

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

agent_tools.py

使用三个浏览器工具的 LangGraph 代理:navigate_and_extract、fill_and_submit_form、take_screenshot

先决条件:pip install playwright langchain langchain-openai langgraph python-dotenv

playwright install chromium

运行方式:python agent_tools.py

os

load_dotenv

langchain_openai

ChatOpenAI

tools

tool

langchain_core

messages

HumanMessage

prebuilt

create_react_agent

── 共享的浏览器状态 ──────────────────────────────────────────────────────

我们在整个代理生命周期中保持一个浏览器实例存活。

在每次工具调用时创建和销毁浏览器实例会很慢且浪费资源。

_browser

None

_page

_playwright

get_page

"返回共享的页面,如需要则启动浏览器。"

global

is

start

close_browser

"当代理会话结束时清理浏览器资源。"

stop

── 浏览器工具 ─────────────────────────────────────────────────────────────

注意:这些是异步工具(async def)。LangChain 的 @tool 装饰器支持

异步函数,代理必须使用 ainvoke() 调用,这样工具调用将在同一个事件循环中运行,

而不是尝试启动第二个事件循环。

@

navigate_and_extract

导航到一个 URL 并返回页面的可见文本内容。

使用此工具访问网站并读取其内容。

输入:包含 https:// 的完整 URL 字符串(例如,'https://example.com')。

15000

content

"body"

截断以避免淹没 LLM 上下文窗口

3000

len

fill_and_submit_form

selector_value_pairs

填充当前加载页面上的表单字段并提交表单。

输入:以逗号分隔的 'selector:value' 对字符串,以 'submit:button_selector' 结尾。

示例:'#email:user@example.com,#password:secret,submit:button[type=submit]'

try

pairs

split

","

submit_selector

pair

key

val

":"

==

"submit"

"表单已提交。当前 URL:{page.url}"

except

Exception

e

"表单交互失败:{str(e)}"

take_screenshot

filename

对当前浏览器页面进行截图并保存到文件。

使用此工具可以直观地验证页面的当前状态。

输入:文件名字符串(例如,'result.png')。

"截图已保存到 {filename}"

── 代理设置 ───────────────────────────────────────────────────────────────

llm

model

"gpt-4o"

temperature

api_key

getenv

"OPENAI_API_KEY"

create_react_agent 将 LLM、工具和 ReAct 推理循环连接在一起。

代理决定调用哪个工具,调用它,读取结果并继续。

agent

── 示例 ──────────────────────────────────────────────────────────────────────

ainvoke

"messages"

"前往 https://example.com,读取页面内容,"

"然后将截图保存为 example.png"

此操作的作用:三个使用 @tool 装饰器的函数会被注册到代理中。每个函数的文档字符串是 LLM 用来理解工具功能及其使用场景的内容。请将它们写成工作描述,而不是代码注释。共享的 _browser 和 _page 全局变量意味着浏览器在多个工具调用之间保持打开状态,这对于在同一个会话中跨越多个页面的任务至关重要。由于工具是使用 async def 定义的,因此代理是通过 ainvoke() 而不是 invoke() 被调用的,这样工具调用将在 main() 已经使用的同一个事件循环中运行。

一个垂直流程图,展示了任务请求如何通过代理进行流动(点击以放大)。图片由编辑提供。

此代码片段中的关键设计决策是共享的浏览器实例。如果每个工具调用都启动并关闭自己的浏览器,那么在调用之间将丢失所有会话状态,例如 cookie、导航历史记录以及代理已经构建的任何表单状态。在整个代理会话中保持浏览器运行可以保留这些上下文。

使用 browser-use 进行高级代理任务

使用 @tool 函数的原始 Playwright 提供了精确的控制。但代价是,你仍然需要编写选择器,仍然需要考虑页面结构,仍然需要手动处理每一个边缘情况。如果网站更改了其 HTML,你的选择器就会失效。

browser-use 采用了不同的方法。你不需要编写选择器,而是用普通的英语给代理一个任务。browser-use 在底层使用 Playwright,但 LLM 在每一步都会读取当前页面状态,并决定下一步该做什么:点击哪个元素、输入什么内容,以及何时完成任务。页面结构不会硬编码到你的代码中。代理会在运行时自行确定。

browser-use 是一个 Python 库,它为 LLM 提供了一个可用的浏览器。LLM 会读取每一页,并决定点击什么、输入什么以及提取什么。这使得它对会导致基于选择器脚本失效的网站更改具有更强的适应能力。

何时使用 browser-use 而不是原始 Playwright:

  • 如果任务是探索性的,且页面结构不可预测,使用 browser-use。
  • 如果你运行的是固定且可重复的工作流程,其中每个选择器都是已知且稳定的,原始 Playwright 更可靠且每次运行成本更低。
  • 一个 browser-use 代理在每个任务步骤中会进行多次 LLM 调用;而脚本化的 Playwright 运行则不会进行任何调用。

如何运行:保存为 browser_use_agent.py,确保 OPENAI_API_KEY 在你的 .env 文件中,然后运行 python browser_use_agent.py

browser_use_agent.py

一个浏览器使用代理,接受自然语言任务并完成任务

不需要任何 CSS 选择器或硬编码的页面结构。

先决条件:pip install browser-use playwright python-dotenv

如何运行:python browser_use_agent.py

browser_use

run_browser_task

task

将自然语言任务交给浏览器使用代理。

代理处理导航、点击和提取,而无需选择器。

temperature=0 使决策确定性更强,并减少幻觉动作

Agent 将浏览器、LLM 和任务循环组合在一起。

max_actions_per_step 限制代理在重新读取页面之前可以采取的动作数量 —— 防止在复杂页面上出现无限循环。

max_actions_per_step

run() 执行完整的任务循环:

读取页面 → 决定动作 → 执行动作 → 读取更新后的页面 → 重复

final_result() 返回代理提取的内容或结论

final_result

"任务完成但没有提取输出。"

"前往 https://books.toscrape.com 并找到第一页上最贵的三本书。返回它们的标题和价格。"

"任务: {task}\n"

output

"结果:\n{output}"

这是如何实现的:整个任务,包括导航到网站、读取页面、识别三个最高价格并提取它们,均由代理处理,而你的代码中没有使用任何 CSS 选择器。如果 books.toscrape.com 明天重新设计其价格显示方式,该脚本仍然可以正常工作。而基于选择器的爬虫则会静默失败。

max_actions_per_step=5 参数值得解释。在每一步中,代理读取页面,并可以在重新读取页面之前决定采取最多五个动作(点击、输入、滚动、导航)。保持这个值较低可以迫使代理更频繁地检查其工作,从而更早地发现错误。

处理复杂部分

在生产环境中,大多数浏览器自动化工具都会因以下三个问题而失效。每个问题都有对应的解决方案,但只有在你已经吃过亏之后,这些解决方案才会变得显而易见。

  1. 反爬虫检测

不希望被自动化的网站会通过多种方式检测自动化行为,例如检查 navigator.webdriver 属性(Playwright 默认将其设置为 true),在 JavaScript 环境中查找无头浏览器的指纹,以及分析交互模式是否过于快速或过于统一,从而判断是否为人类操作。

最重要的缓解措施是移除 webdriver 标志。除此之外,使用一个真实的用户代理字符串、标准的视口大小,以及合理的语言和时区设置,可以覆盖大多数检测方法,除非网站进行了复杂的指纹分析。

hard_parts.py -- 第1部分:反爬虫的隐身启动

先决条件:pip install playwright && playwright install chromium

运行方式:python hard_parts.py

import asyncio import json from pathlib import Path from playwright.async_api import async_playwright

async def launch_stealth_browser(playwright): """ 启动一个看起来更像真实用户会话的浏览器上下文。 包含:真实视口、用户代理、语言、时区、webdriver 标志。 注意:对于严重的反爬虫目标,可以考虑使用付费服务,如 Browserbase。 """ browser = await playwright.chromium.launch( headless=True, args=[ "--disable-blink-features=AutomationControlled", # 隐藏 webdriver 检测 "--no-sandbox", "--disable-dev-shm-usage", ] ) context = await browser.new_context( viewport={"width": 1366, "height": 768}, # 常见的桌面分辨率 user_agent=( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0.0.0 Safari/537.36" ), locale="en-US", timezone_id="America/New_York", java_script_enabled=True, )

移除 Playwright 默认注入的 'webdriver' 属性。

反爬虫系统会在浏览器的 JS 环境中检查这个属性。

await context.add_init_script( "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" ) return browser, context

hard_parts.py -- 第1部分:反爬虫的隐身启动

运行方式:python hard_parts.py

pathlib

launch_stealth_browser

启动一个看起来更像真实用户会话的浏览器上下文。

包含:真实视口、用户代理、语言、时区、webdriver 标志。

注意:对于严重的反爬虫目标,可以考虑使用付费服务,如 Browserbase。

args

"--disable-blink-features=AutomationControlled"

隐藏 webdriver 检测

"--no-sandbox"

"--disable-dev-shm-usage"

1366

768

常见的桌面分辨率

"Chrome/124.0.0.0 Safari/537.36"

locale

"en-US"

timezone_id

"America/New_York"

java_script_enabled

移除 Playwright 默认注入的 'webdriver' 属性。

反爬虫系统会在浏览器的 JS 环境中检查这个属性。

add_init_script

"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"

作用说明:add_init_script() 调用会在任何页面 JavaScript 执行之前运行,这意味着在网站的检测代码检查之前,navigator.webdriver 的覆盖就已经生效。--disable-blink-features=AutomationControlled 启动参数会在浏览器引擎级别移除另一个自动化标志。这两项更改一起处理了最常见的检测方法。

对于那些具有激进的指纹识别和 CAPTCHA 系统的网站,上述缓解措施将不够。像 Browserbase、Spidra 和 Brightdata 的 Scraping Browser 这样的服务,将 CAPTCHA 解决、住宅 IP 轮换和浏览器指纹管理作为托管基础设施来处理。

  1. 智能等待

第二种失败模式是时机问题。反射的反应是添加 time.sleep() 调用,并在出现问题时增加它们。这在两个方向上都是错误的:在慢速连接上太短,在快速连接上太长,且在调试时完全不透明。

Playwright 有四种正确的等待策略。使用与你实际等待的内容相匹配的一种:

第二部分:智能等待策略(添加到你的爬虫或代理工具中)

async def smart_wait_examples(page): """四种无需任意等待的方法,以等待正确的页面状态。"""

策略 1:等待特定元素出现在 DOM 中

当你知道确切的元素表示内容已加载时使用

await page.wait_for_selector(".product-list", state="visible", timeout=10000)

策略 2:等待特定的 API 响应

当内容来自你可以识别的 XHR/fetch 调用时使用

async with page.expect_response( lambda r: "/api/products" in r.url and r.status == 200 ) as response_info: await page.click("#load-more") response = await response_info.value print(f"API responded: {response.status}")

策略 3:等待表单提交后 URL 发生变化

当成功提交后重定向到新页面时使用

await page.wait_for_url("/dashboard", timeout=10000)

策略 4:等待 JavaScript 变量被设置

当没有视觉元素可靠地表示就绪状态时使用

await page.wait_for_function( "() => window.__dataLoaded === true", timeout=10000 )

第二部分:智能等待策略(添加到你的爬虫或代理工具中)

smart_wait_examples

四种无需任意等待的方法,以等待正确的页面状态。

策略 1:等待特定元素出现在 DOM 中

当你知道确切的元素表示内容已加载时使用

".product-list"

策略 2:等待特定的 API 响应

当内容来自你可以识别的 XHR/fetch 调用时使用

expect_response

r

"/api/products"

and

status

200

response_info

"#load-more"

response

value

"API responded: {response.status}"

策略 3:等待表单提交后 URL 发生变化

当成功提交后重定向到新页面时使用

wait_for_url

"/dashboard"

策略 4:等待 JavaScript 变量被设置

当没有视觉元素可靠地表示就绪状态时使用

wait_for_function

"() => window.__dataLoaded === true"

这是它所做的事情:每种策略都与特定的可观测事件相关联,而不是任意的时间延迟。wait_for_selector 监视 DOM。expect_response 连接到网络层。wait_for_url 监控导航。wait_for_function 在浏览器上下文中评估 JavaScript。使用最直接表明“我需要的东西现在已就绪”的那种。

  1. 会话和 Cookie 持久性

第三种失败模式是丢失会话状态。如果代理在第一步登录到某个网站,然后浏览器上下文被销毁,第二步就没有认证信息了。在每次运行时重新创建登录过程很慢,可能会触发速率限制或锁定。

解决方案是在登录后将 Cookie 保存到磁盘,并在每次后续运行开始时加载它们:

第3部分:跨运行的会话持久化

COOKIES_FILE = Path("session_cookies.json")

async def save_session(context) -> None: """登录成功后将浏览器 cookie 保存到磁盘。""" cookies = await context.cookies() COOKIES_FILE.write_text(json.dumps(cookies, indent=2)) print(f"会话已保存: 已写入 {len(cookies)} 个 cookie。")

async def load_session(context) -> bool: """在导航前加载已保存的 cookie。如果找到会话则返回 True。""" if not COOKIES_FILE.exists(): print("未找到已保存的会话。需要重新登录。") return False cookies = json.loads(COOKIES_FILE.read_text()) await context.add_cookies(cookies) print(f"会话已恢复: 已加载 {len(cookies)} 个 cookie。") return True

第3部分:跨运行的会话持久化

COOKIES_FILE

"session_cookies.json"

save_session

"登录成功后将浏览器 cookie 保存到磁盘。"

cookies

write_text

"会话已保存: 已写入 {len(cookies)} 个 cookie。"

load_session

bool

"在导航前加载已保存的 cookie。如果找到会话则返回 True。"

not

exists

"未找到已保存的会话。需要重新登录。"

loads

read_text

add_cookies

"会话已恢复: 已加载 {len(cookies)} 个 cookie。"

这是它的作用:context.cookies() 返回当前浏览器上下文中所有的 cookie,包括会话令牌和认证 cookie。将它们写入 JSON 并在下一次运行时重新加载,意味着浏览器将以已认证的状态启动。请注意,会话会过期;添加一个检查,如果保存的会话返回到登录页面的重定向,则回退到重新登录。

部署浏览器代理

让浏览器代理在本地运行是一回事。在云环境中可靠地运行它则是另一回事。

在你的笔记本电脑上运行良好的 Python 脚本和在 CI 中失败的 Python 脚本之间的主要区别是系统依赖项。Playwright 的 Chromium 浏览器需要一组共享库,这些库在大多数开发机器上都存在,但在最小的云镜像中却不存在。最干净的解决方案是使用 Docker。

Dockerfile — 构建一个包含 Playwright 所需所有内容的容器:

用于无头 Playwright 浏览器代理的 Dockerfile

构建: docker build -t browser-agent .

运行: docker run --rm -e OPENAI_API_KEY=your_key browser-agent

FROM python:3.11-slim

安装 Chromium 所需的系统依赖项

RUN apt-get update && apt-get install -y \ libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 \ libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \ libxrandr2 libgbm1 libasound2 libpangocairo-1.0-0 \ libpango-1.0-0 libcairo2 libx11-6 libxext6 libxfixes3 \ fonts-liberation wget ca-certificates \ && rm -rf /var/lib/apt/lists/*

WORKDIR /app

首先安装 Python 依赖项(缓存层 —— 仅在 requirements 变化时重新构建)

COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt

将 Playwright 浏览器二进制文件安装到镜像中

RUN playwright install chromium RUN playwright install-deps chromium

最后复制应用程序代码(此处的更改不会使 pip/playwright 层失效)

COPY . .

CMD ["python", "agent_tools.py"]

requirements.txt: playwright browser-use langchain langchain-openai langgraph python-dotenv

libgbm1

libasound2

libpangocairo

1.0

libpango

libcairo2

libx11

libxext6

libxfixes3

fonts

liberation

wget

ca

certificates

rm

rf

var

lib

lists

*

WORKDIR

app

首先安装 Python 依赖项(缓存层 -- 仅在 requirements 变化时重新构建)

COPY

requirements

txt

--

no

cache

dir

将 Playwright 浏览器二进制文件安装到镜像中

deps

最后复制应用程序代码(此处的更改不会使 pip/playwright 层失效)

CMD

"python"

"agent_tools.py"

对于需要并行运行多个浏览器会话的并发工作负载,使用 Playwright 的异步 API 与 asyncio.gather() :

使用信号量速率限制的并行抓取 # 同时最多运行 3 个浏览器会话 import asyncio from playwright.async_api import async_playwright async def scrape_url(browser, url: str, semaphore: asyncio.Semaphore) -> dict: """抓取单个 URL,遵守并发信号量限制。""" async with semaphore: context = await browser.new_context() page = await context.new_page() await page.goto(url, wait_until="domcontentloaded") title = await page.title() await context.close() # 关闭上下文(而不是浏览器)以释放资源 return {"url": url, "title": title} async def scrape_parallel(urls: list[str], max_concurrent: int = 3) -> list[dict]: """并行抓取 URL 列表,最多同时运行 max_concurrent 个会话。""" semaphore = asyncio.Semaphore(max_concurrent) # 限制并发会话数 async with async_playwright() as p: # 所有上下文共享一个浏览器实例 -- 比每个 URL 启动一个浏览器便宜得多 browser = await p.chromium.launch(headless=True) tasks = [scrape_url(browser, url, semaphore) for url in urls] results = await asyncio.gather(*tasks) await browser.close() return list(results)

使用信号量速率限制的并行抓取

同时最多运行 3 个浏览器会话

scrape_url

semaphore

"抓取单个 URL,遵守并发信号量限制。"

关闭上下文(而不是浏览器)以释放资源

"url"

scrape_parallel

urls

max_concurrent

"并行抓取 URL 列表,最多同时运行 max_concurrent 个会话。"

限制并发会话数

所有上下文共享一个浏览器实例 -- 比每个 URL 启动一个浏览器便宜得多

tasks

gather

这是它所做的事情:asyncio.Semaphore(max_concurrent) 限制了同时运行的浏览器上下文数量。如果没有它,启动 50 个并发的浏览器上下文会耗尽内存。一个浏览器进程被所有上下文共享;一个上下文成本低;一个完整的浏览器实例成本高。

在托管基础设施方面,Amazon Nova Act 于 2025 年 3 月推出,作为在 AWS 上构建浏览器代理的专用 SDK,原生集成 Playwright 用于浏览器控制。Playwright 自己的 MCP 服务器通过模型上下文协议,让 AI 助手获得完整的浏览器控制权,使用结构化的可访问性快照而不是截图,这意味着在代理对页面的理解保持高水平的同时,令牌成本保持较低。

综合起来

这是一个完整的端到端代理,它接受一个研究问题,导航到一个公共数据源,提取结构化结果,并返回一个干净的摘要。它使用了第 5 节中的浏览器工具,由 LangGraph 代理协调。

如何运行:保存为 reference_agent.py,确保 OPENAI_API_KEY 存在于你的 .env 文件中,然后运行 python reference_agent.py

reference_agent.py

完整的浏览器使用AI代理:导航、提取、总结。

目标:books.toscrape.com(公开的爬虫沙盒)

先决条件:pip install playwright langchain langchain-openai langgraph python-dotenv

playwright install chromium

如何运行:python reference_agent.py

import asyncio import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langchain.tools import tool from langchain_core.messages import HumanMessage, SystemMessage from langgraph.prebuilt import create_react_agent from playwright.async_api import async_playwright

load_dotenv()

── 浏览器状态 ─────────────────────────────────────────────────────────────

_browser = None _context = None _page = None _playwright = None

async def get_page(): global _browser, _context, _page, _playwright if _browser is None: _playwright = await async_playwright().start() _browser = await _playwright.chromium.launch(headless=True) _context = await _browser.new_context( viewport={"width": 1280, "height": 720}, user_agent=( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ) )

移除 webdriver 指纹

await _context.add_init_script( "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" ) _page = await _context.new_page() return _page

async def teardown(): global _browser, _playwright if _browser: await _browser.close() await _playwright.stop() _browser = None _playwright = None

── 工具 ─────────────────────────────────────────────────────────────────────

@tool async def navigate(url: str) -> str: """ 导航到指定的URL并返回页面的文本内容。 当你需要打开一个网站或跳转到新页面时使用。 输入:带https://前缀的完整URL。 """ page = await get_page() await page.goto(url, wait_until="domcontentloaded", timeout=20000) await page.wait_for_load_state("networkidle") content = await page.inner_text("body") return content[:4000]

@tool async def extract_structured(css_selector: str) -> str: """ 从当前页面中所有匹配CSS选择器的元素中提取文本。 当你需要从已加载的页面中提取特定元素时使用。 输入:有效的CSS选择器字符串(例如,'h3 a', '.price_color', 'article.product_pod')。 """ page = await get_page() try: await page.wait_for_selector(css_selector, timeout=5000) elements = await page.query_selector_all(css_selector) texts = [] for el in elements[:20]: # 限制最多20个元素以保持输出可控 text = await el.inner_text() texts.append(text.strip()) return "\n".join(texts) if texts else "未找到元素。" except Exception as e: return f"提取失败:{str(e)}"

@tool async def get_current_url() -> str: """ 返回浏览器当前所在的URL。 不需要输入。 """ page = await get_page() return page.url

── 代理 ─────────────────────────────────────────────────────────────────────

llm = ChatOpenAI( model="gpt-4o", temperature=0, api_key=os.getenv("OPENAI_API_KEY") )

tools = [navigate, extract_structured, get_current_url] agent = create_react_agent(llm, tools)

SYSTEM = ( "你是一个基于浏览器的研究代理。你拥有一个真实的浏览器。" "使用 navigate() 打开页面,使用 extract_structured() 提取特定元素," "使用 get_current_url() 检查当前所在位置。" "始终先导航,再提取。在最终答案中保持简洁。" )

async def run_agent(query: str) -> str: result = await agent.ainvoke({ "messages": [ SystemMessage(content=SYSTEM), HumanMessage(content=query) ] }) await teardown() return result["messages"][-1].content

── 示例 ──────────────────────────────────────────────────────────────────────

if __name__ == "__main__": query = ( "前往 https://books.toscrape.com 并提取列出的前5本书的标题和价格。" "以结构化的列表形式返回它们。" ) print(f"查询: {query}\n") answer = asyncio.run(run_agent(query)) print(f"答案:\n{answer}")

reference_agent.py

使用完整浏览器的 AI 代理:导航、提取、总结。

目标:books.toscrape.com(公开的爬虫沙盒)

运行方式:python reference_agent.py

SystemMessage

── 浏览器状态 ─────────────────────────────────────────────────────────────

_context

移除 webdriver 指纹

teardown

── 工具 ─────────────────────────────────────────────────────────────────────

navigate

将浏览器导航到一个 URL 并返回页面的文本内容。

当需要打开一个网站或跳转到新页面时使用。

输入:带有 https:// 前缀的完整 URL。

20000

4000

extract_structured

css_selector

从当前页面上所有匹配 CSS 选择器的元素中提取文本。

当需要从已加载的页面中提取特定元素时使用。

输入:有效的 CSS 选择器字符串(例如,'h3 a', '.price_color', 'article.product_pod')。

5000

elements

texts

el

限制为 20 个元素以保持输出简洁

text

"\n"

join

"No elements found."

"Extraction failed: {str(e)}"

get_current_url

"返回浏览器当前所在的 URL。不需要输入。"

── 代理 ─────────────────────────────────────────────────────────────────────

SYSTEM

"你是一个基于浏览器的研究代理。你拥有一个真正的浏览器。"

"使用 navigate() 打开页面,使用 extract_structured() 提取特定元素,"

"使用 get_current_url() 检查你当前所在的位置。"

"首先导航,然后提取。在最终答案中保持简洁。"

run_agent

query

__name__

"__main__"

"前往 https://books.toscrape.com 并提取列出的前 5 本书的标题和价格,"

"以结构化列表的形式返回。"

"Query: {query}\n"

answer

"Answer:\n{answer}"

这是做什么的:这个代理有三个清晰的工具:navigate、extract_structured 和 get_current_url,还有一个系统提示,明确告诉它何时使用每个工具。代理调用 navigate 来加载页面,使用 extract_structured 通过 CSS 选择器提取书籍标题和价格,并在最终答案中合成一个结构化列表。代理完成后,teardown() 调用会干净地关闭浏览器,确保不会留下任何僵尸 Chromium 进程。

结论

浏览器并不是自动化工程师的专用工具。它是网络的通用接口,而网络是世界上大部分实际工作进行的地方。一个能够使用浏览器的 AI 代理不需要一个维护 API 集成的合作伙伴团队。它可以到达任何人类可以到达的地方。

使这在实践中可行,而不仅仅是理论上的有趣,是工具的成熟。Playwright 处理了浏览器交互的困难部分。browser-use 消除了为探索性任务编写选择器的需要。LangGraph 为 LLM 提供了清晰的工具钩子和一个处理可变页面结构的推理循环。本文中的模式不是演示。它们是目前 51% 正在生产环境中运行 AI 代理的企业所构建的相同模式。

从爬虫示例开始。让它运行在你实际需要数据的网站上。当脚本无法预见决策时,添加代理层。当页面结构过于动态,选择器无法处理时,添加 browser-use。当需要在除你的笔记本电脑之外的其他地方运行时,使用 Docker 部署。

困难的部分不是代码。而是在每一层知道该使用哪个工具。希望本文能让这一点更加清晰。

关于此主题的更多信息

  • 使用 Pydantic AI 在 Python 中构建 AI 代理
  • 使用 LangGraph 构建 ReAct 代理:初学者指南
  • 构建一个“人工参与”的审批门...
  • 使用本地小型语言模型构建 AI 代理
  • 为长期运行的代理构建上下文修剪管道
  • 在 Python 代理中实现权限控制的工具调用