本节摘要:本节处理 Agent 循环里风险最高的一类动作:提交、支付、删除、发送。先给风险模型——模型输出是概率性的,不可逆动作没有重试机会,因此必须在「决策」与「行动」之间加一道确认门。然后讲 browser-use 的敏感数据机制:任务文本里只写占位符,真值经配置注入,对话历史与日志自动脱敏——凭据因此不进提示词、不进轨迹文件。再讲人工介入点的三种形态(前置审批、执行前确认、事后审计)与各自适用面,并演示用自定义动作实现一个最小确认门。第四部分讲域名白名单:限制智能体只能访问指定域名,是敏感数据机制的另一半——数据出不去,危险页面也进不来。本节收在一张五条目的信任边界自查清单,供接入任何新任务前过一遍。
第 2.1 节的循环里,模型每轮输出的动作都是概率选择的。对 fill、click 这类可逆动作,选错的代价是浪费一轮;对「提交订单」「删除全部」「发送邮件」这类不可逆动作,选错的代价没有上限。风险结构可以写成一行:
期望损失 = 触发概率(不为你控制) × 单次损失(不可逆,无上限) │ │ ▼ ▼ 治理手段:确认门、白名单、 治理手段:永远不要让 最小权限降低触发面 「高权限动作」脱离人工确认
结论是工程界的老话:概率系统的出口处要放确定性闸门。位置很明确——在「决策」与「行动」之间,也就是第 2.1 节控制流的第③步之前:
任务 ──▶ 感知 ──▶ 决策 ──▶ ┌──────────┐ ──▶ 行动 ──▶ 页面 │ 确认门 │ │ 高危动作? │ │ 是→人工确认│ └──────────┘
浏览器智能体最常见的敏感数据是登录凭据。直觉做法(把用户名密码写进 task 文本)有三宗罪:凭据进入模型的提示词(发往模型服务商);凭据进入执行历史与轨迹文件(落盘、可能被导出分享);凭据进入错误消息与日志(排障时顺手泄露)。browser-use 的敏感数据机制针对此设计:任务文本里只写占位符,真值在配置里绑定,注入发生在动作执行前的最后一刻,历史与日志里看到的是掩码。
任务文本: 「在用户名框输入 {USERNAME} …」 ← 占位符进提示词 配置: sensitive_data = {USERNAME: 真值} ← 真值留在本地 执行时: {USERNAME} → 真值,填入输入框 ← 最后一刻替换 历史日志: 「在用户名框输入 ***USERNAME***」 ← 脱敏后的形态
完整示例(靶场侧新备一个登录页 demo_login.html,结构见下表;示例演示机制本身,不针对任何真实站点):
| 元素 | 语义角色 | 可见文本或标签 |
|---|---|---|
| 页面标题 | 文档标题 | 「演示登录」 |
| 用户名输入框 | 文本框 | 关联标签「用户名」 |
| 密码输入框 | 密码框 | 关联标签「密码」 |
| 登录按钮 | 按钮 | 「登录」 |
| 登录结果区域 | 状态 | 成功后显示「欢迎回来」 |
# sensitive_login.py —— 敏感数据占位符与域名白名单(写法示意,以官方文档为准) import asyncio import os from browser_use import Agent from langchain_openai import ChatOpenAI async def main() -> None: agent = Agent( task=( "打开 http://127.0.0.1:8080/demo_login.html," "在用户名框输入 {USERNAME},在密码框输入 {PASSWORD}," "点击「登录」,然后报告页面出现的变化。" ), llm=ChatOpenAI(model="gpt-4o"), sensitive_data={ "USERNAME": os.environ.get("DEMO_USER", "demo"), "PASSWORD": os.environ.get("DEMO_PASS", ""), }, allowed_domains=["127.0.0.1"], ) result = await agent.run() print(result.final_result()) asyncio.run(main())
真值从环境变量读取,代码仓库与任务文本里都不出现明文——这与第 0.2 节「密钥用环境变量注入」是同一条纪律在不同位置的执行。参数名与掩码形态随版本可能调整,以官方文档为准;机制的存在本身(占位符、晚绑定、脱敏)是稳定面。
确认门什么时候触发、谁来批准,有三种工程形态:
| 形态 | 时点 | 谁在场 | 适用 | 代价 |
|---|---|---|---|---|
| 前置审批 | 任务开始前 | 人 | 任务本身的正当性未定 | 一次性,便宜 |
| 执行前确认 | 高危动作触发时 | 人 | 支付、删除、发送类不可逆动作 | 每次打断,随频率变贵 |
| 事后审计 | 任务完成后 | 人(异步) | 可逆但需要追责的操作 | 不打断,靠日志质量 |
浏览器智能体的常见组合:日常可逆动作直接放行(循环照转),不可逆动作挂执行前确认,全程落审计(第 11 章生产化给出日志方案)。执行前确认的最小实现是一个自定义动作(自定义动作的完整语法在 2.3 节,本节先用起来):
# confirm_gate.py —— 用自定义动作实现最小确认门(写法示意,以官方文档为准) import asyncio from browser_use import ActionResult, Agent, Controller from langchain_openai import ChatOpenAI controller = Controller() @controller.action("高危操作前的人工确认,用户拒绝则中止后续提交") def confirm_dangerous(reason: str) -> ActionResult: answer = input(f"是否允许执行该操作?原因:{reason} [y/N] ") allowed = answer.strip().lower() == "y" return ActionResult(extracted_content=f"人工{'允许' if allowed else '拒绝'}:{reason}") async def main() -> None: agent = Agent( task=( "打开 http://127.0.0.1:8080/demo_todo.html,连续添加三条待办后," "调用高危确认说明将删除全部待办,确认通过才点击「清空」按钮," "最后报告列表状态。" ), llm=ChatOpenAI(model="gpt-4o"), controller=controller, ) result = await agent.run() print(result.final_result()) asyncio.run(main())
(靶场页面如无「清空」按钮,自行在 demo_todo.html 里加一个,作为本节练习。)这个实现的要点不在 input 而在结构:确认是一个模型可调用、但由人类应答的动作——模型学会了「删除前先问」,闸门逻辑却握在代码与人手里。第 1.1 节说过的「不可逆动作先 observe 再 act」,到这里升级为「不可逆动作先确认再执行」。
占位符管住「数据怎么出去」,白名单管住「智能体能去哪」。allowed_domains 把导航限制在指定域名内:任务文本里的其他链接、页面上的跳转、弹窗广告里的域名,一律不可达。两件东西合起来才是完整的信任边界——数据出不去危险通道,危险页面也送不进来。工程意义在事故曲线上:即便模型被页面内容诱导(提示注入的浏览器形态,第 8 章详述),可达范围也封在白名单内,损失半径有界。
白名单的粒度建议:按任务给白名单,不给全局白名单——「这个任务只允许访问本站与支付页」比「允许访问一切」小得多的事故面。本地开发时用 127.0.0.1 即可;对接真实业务时,白名单应该来自任务定义文档而非现场随手敲。
接入任何新任务(尤其涉及账号、提交、支付的)之前过一遍:
| 条目 | 自查问题 |
|---|---|
| 密钥隔离 | 凭据是否只经占位符机制注入,未出现在任务文本、代码与日志? |
| 不可逆确认 | 支付、删除、发送类动作是否挂在人工确认门后? |
| 域名白名单 | 本任务的导航范围是否已显式枚举? |
| 步数与速率护栏 | 最大步数是否设定?访问频率是否在站点可接受范围? |
| 轨迹留存 | 执行历史是否落盘(脱敏后),供事后审计与回放? |
五条全绿才放任务进循环。这张表在第 8 章会被扩充进合规边界的完整框架(robots 与服务条款、速率礼仪、标识身份),在第 11 章被接进生产化的准入流程。
刹车装好了,接下来调校发动机:下一节讲自定义动作的完整语法、系统提示定制、模型选择与会话选项——把 browser-use 捏成你任务的形状,并郑重交代它的版本纪律。