微软 AI Agent 入门课第 15 课:browser-use 接入与任务描述
你手上有个站点,没有开放 API,页面结构还三天两头动一次。写死 CSS selector 的脚本每周都要修,改到后来你已经记不清哪几行是为了绕弹窗、哪几行是为了绕 A/B 改版。ai-agents-for-beginners 的第 15 课就是冲这个场景来的:让模型看着页面决定下一步点哪里,同时保留一条确定性的浏览器控制通道。
这一课在仓库里的目录是 15-browser-use/,README 标题是「Building Computer Use Agents (CUA)」,只有一个 notebook:15-browser-use/15-browser-user.ipynb(注意文件名是 browser-user 而不是目录名的 browser-use,找文件的时候容易愣一下)。示例任务是打开 Airbnb、搜索斯德哥尔摩、把房源价格抽成结构化数据、挑出最便宜的一条。
先说前置条件,这段别跳
README 的 Prerequisites 一节写明四件事:Python 3.12+、已配好的 Azure OpenAI deployment、本机装了 Chrome 或 Chromium、装好 Playwright 依赖,另外要求你对 async Python 有基本了解。notebook 的 kernelspec 元数据里记的也是一个 3.12 的虚拟环境。
有一个坑值得单独点出来:仓库根目录的 requirements.txt 里没有 browser_use,也没有 playwright。这一课的依赖是在课内单独装的。README 的 Setup 一节给的是:
pip install browser_use playwright python-dotenv
playwright install chromium
而 notebook 前两个代码 cell 装的是:
%pip install browser_use langchain-openai playwright
!playwright install chromium
两处不一致:README 装了 python-dotenv、notebook 没装,但 notebook 里确实 from dotenv import load_dotenv 并调用了 load_dotenv();反过来 notebook 装了 langchain-openai,可导入那一格的注释却写着「Changed from langchain_openai」,实际用的是 from browser_use import Agent, Browser, ChatAzureOpenAI。也就是说 langchain-openai 在当前代码里没有被 import。按 README 那条命令装,再补 python-dotenv,是更贴合 notebook 实际 import 的做法。这两处以仓库最新内容为准,课程更新后可能就对齐了。
环境变量按 README 的 Setup 一节:
AZURE_OPENAI_ENDPOINT=...
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=...
# Optional: defaults to the latest API version when omitted
AZURE_OPENAI_API_VERSION=...
notebook 里 ChatAzureOpenAI 只显式传了 model=azure_openai_deployment,端点、密钥、API 版本由库自己从环境变量读——这一点是代码注释里写明的。密钥写进 .env,别写进 notebook 单元格,也别让它进模型上下文。
接入链路:CDP 是这一课的关节
README 的 Architecture Overview 把流程写成四步:Chrome 带 CDP 启动,让 Playwright 和 Browser-Use 共用同一个浏览器会话;Browser-Use 的 agent 负责开放式导航(开页面、关弹窗、搜城市);再用 Pydantic schema 从当前页抽结构化数据;最后由 Python 逻辑比价。
对应到 notebook,这条链路是这么落地的。第一步是自己起一个带远程调试的 Chrome,start_chrome_with_cdp() 拼出来的命令行参数是:
cmd = [
chrome_exe,
f'--remote-debugging-port={port}',
f'--user-data-dir={user_data_dir}',
'--no-first-run',
'--no-default-browser-check',
'about:blank',
]
user_data_dir 来自 tempfile.mkdtemp(prefix='chrome_cdp_'),即每次跑都是一份全新的临时 profile,不碰你日常浏览器的登录态。函数签名上 port 的默认值是 9222——这是仓库当前代码里的默认值,随版本可能变动。
第二步,Playwright 通过 playwright.chromium.connect_over_cdp(cdp_url) 连上去。注意 main() 里这一步的注释标的是 optional - for custom Playwright actions:抽价这条主链路并不依赖它,它是留给你写确定性操作的口子。
第三步才是 Browser-Use。cell 里的类这样构造浏览器对象:
self.browser = Browser(
cdp_url=cdp_url,
keep_alive=True # ✅ This keeps the browser open!
)
keep_alive=True 这一行有代码注释说明用途:防止 Agent 跑完之后浏览器被关掉。因为后面还要拿同一个页面做抽取,浏览器一关就没得抽了。
这里有个读代码时必须看清的地方:AirbnbSearchAgent 这个类在 notebook 里被定义了两次。靠前那一版用的是 Browser(playwright_browser=playwright_browser),截图走 await self.browser.get_current_page();靠后那一版改成了 Browser(cdp_url=..., keep_alive=True),取页面改成 pages = await self.browser.get_pages() 再取 pages[0]。顺序执行的话后一版覆盖前一版。你要是照着靠前那格改,跑的其实是靠后那格的逻辑。
任务描述怎么写
这一课真正能迁移到你自己项目里的东西,是那段 task 字符串:
search_agent = Agent(
task=(
"Navigate to https://www.airbnb.com. "
"Close any pop-ups, cookie banners, or login prompts if they appear. "
"Search for 'Stockholm, Sweden' in the search box. "
"Wait for the search results page to fully load with listing cards visible."
),
llm=self.llm,
browser=self.browser,
use_vision=True # Critical: enables screenshot analysis
)
拆开看它写了四件事:一个明确的起始 URL;一句预先声明的干扰项处置(弹窗、cookie 横幅、登录提示,且用 if they appear 说明可能不出现);一个精确到控件的动作(在 search box 里搜 'Stockholm, Sweden');以及一个可判定的收尾条件(等结果页加载完、listing 卡片可见)。最后这句是最容易被漏掉的——不给终止条件,agent 不知道什么时候算做完。
use_vision=True 旁边的注释写的是 enables screenshot analysis,也就是让模型能看截图。
抽取那一步没有走 agent,而是直接对页面调用:
search_results = await page.extract_content(
prompt=extraction_prompt,
structured_output=SearchResult,
llm=self.llm
)
extraction_prompt 是一段多行字符串,里面写了要排除的东西(DO NOT include "Experiences" or other non-home listings)、每条要抽的字段、URL 的形状约束(应以 https://www.airbnb.com/rooms/ 开头),以及「只收你能清楚看见价格的条目」。约束写在 prompt 里,形状写在 Pydantic 模型里,两边配合。
SearchResult 的字段有 location、total_listings_found、listings、cheapest_listing、average_price、price_range;元素类型 AirbnbListing 有 title、price_per_night(float)、currency(default="SEK")、rating 和 url(后两个是 Optional)。每个字段的 Field(description=...) 都写了一句人话,这些描述会成为模型抽取时的依据,不是注释。
边界:仓库自己划的,和读代码能看出来的
跨平台。 start_chrome_with_cdp() 里的 chrome_paths 列出的候选是 macOS 的 Chrome 应用包路径、/usr/bin/google-chrome、/usr/bin/chromium-browser,以及裸名 'chrome' 和 'chromium'——'chrome' 那一行的行内注释就是 # Windows/PATH。列表里没有任何 Windows 绝对路径,Windows 侧只能靠裸名从 PATH 解析。筛选逻辑是 if os.path.exists(path) or path in ['chrome', 'chromium'],命中后再用 --version 拉一个子进程试探,全都失败则抛 RuntimeError('❌ Chrome not found. Please install Chrome or Chromium.')。所以 Windows 上跑这一课,先确认命令行里能直接敲出 chrome。
README 与 notebook 的一处对不上。 README 的架构第 4 步写的是 Python 逻辑比较抽出来的房源、给出最便宜的那条;但 notebook 里 cheapest_listing、average_price、price_range 都是 SearchResult 的字段,extraction_prompt 也明确要求模型 identify lowest / calculate average / determine range,展示环节直接读 result.cheapest_listing。Python 侧真正做的事是 sorted(result.listings, key=lambda x: x.price_per_night),用于渲染表格。换句话说,「谁最便宜」在当前代码里是模型算的。你要是在意这个结论的确定性,把比价挪回 Python 是一行的事。
有几个模型定义没被用上。 notebook 里另外定义了 ListingInfo、BookingDates、BookingResult 三个 Pydantic 模型,但主流程走的是 SearchResult:ListingInfo 和 BookingDates 只出现在 BookingResult 的字段类型上,而 BookingResult 本身在当前代码里没有被任何流程用到——这一组的 docstring 写的是订单信息(BookingResult 是 “Complete booking result information”,字段有 check_in、check_out、total_price),至于为什么留在文件里,仓库没有说明。另外有一格 markdown 说 Temperature: 0.3 用于保持稳定,可 ChatAzureOpenAI(...) 的实际调用里并没有 temperature 参数。以仓库最新代码为准。
安全护栏是这一课 README 花了最大篇幅的部分。 Safety Guardrails 一节的要点包括:把 agent 限制在专用浏览器 profile 或沙箱里、只放开任务必需的域名;把「观察」和「动作」分开,提交表单、发消息、下单、删数据、改账户设置前必须有显式批准;密钥、支付信息、会话 cookie、原始个人数据不进模型上下文,认证交给用户自己完成;把页面内容当作不可信输入——网页上可能写着给 agent 看的指令,要求它改目标、泄露数据、关掉防护或跳到别的站,agent 应当无视;风险步骤前用代码做确定性校验(当前 URL、页面标题、选中项、价格、收款方);给动作数、重试数、标签页数和时长设上限,页面状态不明确时停下而不是继续点;留证据要留动作摘要、时间戳、URL、元素描述和截图引用,而不是把整页内容都存下来。README 给这个示例定的安全默认值是:搜索和抽价可以自动做,登录、联系房东、下单必须单独走用户批准。
README 末尾还提到了 Microsoft 365 Copilot 里的 Project Opal,用来对照一个企业级的 computer use agent 长什么样。README 在可用性说明里标注它属于 Frontier 早期访问计划、需要管理员完成配置,并且明说这是 experimental 的 Frontier 功能、能力可能随时间变化——这是仓库文档自述,别当成稳定能力去规划。
怎么确认自己配对了
代码里已经写了三个可以直接借用的判定点,分别卡在连接层、页面层和数据层。
第一个是 CDP 就绪探测:start_chrome_with_cdp() 起完进程后会轮询 http://localhost:{port}/json/version,拿到 HTTP 200 才算就绪,否则 process.terminate() 并抛 RuntimeError('❌ Chrome failed to start with CDP')。你完全可以在跑 notebook 前先手动起 Chrome,然后在浏览器里打开这个地址看看有没有返回内容——这一步跑通,说明 CDP 这一环是好的。
第二个是页面可达性:抽取前那段
pages = await self.browser.get_pages()
if not pages:
raise RuntimeError("No pages available after Agent run. Browser might have closed.")
如果这里抛错,问题多半出在浏览器提前关了,回头看 keep_alive 那一行。
第三个是数据层:extract_content 的返回要能装进 SearchResult,Pydantic 会替你校验类型。price_per_night 是 float,rating 和 url 是 Optional——缺 rating 不会炸,缺价格会。
另外 take_screenshot() 把截图转 base64 直接渲染在 notebook 里,README 的最佳实践里也写了「迭代时多截图,失败更好查」。真在调导航步骤,截图比读日志直观。
顺带一提,示例里 agent 跑完之后有一句 await asyncio.sleep(3) 等结果加载——那是仓库示例里的取值,不是什么推荐值,换个站点就得换。
以上代码片段均取自仓库,相关说明按仓库代码中的接口语义整理,未经实测,以仓库最新代码为准。这一课把浏览器交给了模型驱动,真要往生产上挪,README 那七条护栏比 notebook 本身更值得先读一遍。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。
本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。