browser-use 的两层 skills:技能目录与技能服务各管什么
本文基于 browser-use 仓库 commit f0aa3a8(2026-07-27)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/browser-use/browser-use 最新代码与文档为准。
在 browser-use 里,「skills」是两个撞名的概念:仓库根目录 skills/ 下那几个目录是纯文本操作手册,读它的是你的编码代理;browser_use/skills/ 这个 Python 包是运行时服务,它把远端定义好的能力拉下来、转成参数模型、注册成 agent 可调用的动作。 两者代码零耦合,解决的问题也不重叠。把它们混为一谈,最常见的后果是你以为装了个 SKILL.md 就能让 Agent(skills=[...]) 生效,或者反过来,配了 API key 却发现你的编码代理还是不知道怎么开浏览器。
这篇就把两层分开讲清楚,顺带说明第二层里那个容易被忽略的细节:cookie 是从你本地浏览器会话里取出来、跟着参数一起发到远端执行的。
一、先看目录结构,两层根本不在一处
在仓库里 ls 一下就能看出来。根目录 skills/ 下面是六个目录:browser-use、cloud、open-source、qa、remote-browser、x402。每个目录里有一份 SKILL.md,有几个还带 references/ 子目录放分主题的参考文档(比如 skills/open-source/references/ 里按 agent.md、browser.md、tools.md、models.md 这样切开)。这些文件里一行 Python 代码都没有,全是自然语言指令加命令示例。
而 browser_use/skills/ 是一个正常的 Python 包:service.py、views.py、utils.py、install.py、browser_use.py、README.md,另外还有一个 browser-use/ 子目录里放着一份 SKILL.md。
注意最后这个细节:包里那份 browser_use/skills/browser-use/SKILL.md 和根目录的 skills/browser-use/SKILL.md 内容逐字一致。原因在 pyproject.toml 里能找到 —— 打包时会收 browser_use/skills/**/*.md。也就是说包内那份是为了随 wheel 分发而存在的副本,browser_use/skills/browser_use.py 里的 skill_text() 就是先去读它:
def skill_text() -> str:
"""Return the canonical Browser Use skill."""
skill_path = Path(__file__).resolve().parent / 'browser-use' / 'SKILL.md'
if skill_path.exists():
return skill_path.read_text(encoding='utf-8')
这就是唯一的交叉点 —— 一个安装器,恰好住在 Python 包里,负责把第一层的文本文件搬到你的编码代理能读到的位置。除此之外两层互不知情。
二、第一层:写给编码代理读的操作手册
skills/browser-use/SKILL.md 的 frontmatter 只有 name 和 description 两个键,描述是「Direct browser control via CDP for web interaction: automation, scraping, testing, screenshots, and site/app work.」。正文结构很直白:什么时候不该用它、怎么调用、本地 Chrome 出问题怎么办、远端浏览器、页面操作流程、录屏、交互技巧索引、设计取舍、坑、域名技能。
值得单独拎出来的是它开头那节 When Not to Use:如果一个普通 HTTP 请求就能读到的公开页面、API、文档,就别开浏览器,用 curl 或者你自己的抓取工具;只有当任务需要交互(点击、输入、导航)、需要用户的登录态、需要 JS 渲染,或者页面有机器人防护时才升级到浏览器。一份技能文档把「不要用我」写在最前面,这个取向本身就值得学 —— 它是在替模型省掉一整条昂贵路径。
调用方式是 CLI 加 heredoc:
browser-use <<'PY'
print(page_info())
PY
文档里明确写了辅助函数是预导入的,首次导航用 new_tab(url) 而不是 goto_url(url),导航后调 wait_for_load(),当前标签页失效或是内部页时调 ensure_real_tab()。定位元素优先走无障碍树而不是截图:cdp("Accessibility.getFullAXTree")["nodes"] 里每个元素都有 role、name 和 backendDOMNodeId,再用 cdp("DOM.getBoxModel", backendNodeId=n) 拿盒模型算中心点,交给 click_at_xy(x, y),最后用一次针对性的 js(...) 或 page_info() 验证点中了。它在「设计取舍」一节解释了为什么默认坐标点击:CDP 鼠标事件在合成器层面穿透 iframe、shadow DOM 和跨域边界。
环境变量层面它暴露了几个开关:BH_DOMAIN_SKILLS=1 打开域名技能(默认关),打开后要先读 $BH_AGENT_WORKSPACE/domain-skills/<site>/ 下的文件再自己发明做法;BH_RECORD=1 或 BH_RECORD=0 为单个进程覆盖录屏偏好;连接模型走默认守护进程、BU_NAME、BU_CDP_URL、BU_CDP_WS 或 start_remote_daemon(...)。任务特定的辅助函数要求写进 $BH_AGENT_WORKSPACE/agent_helpers.py,核心辅助函数保持精简。
另外五个目录是不同用途的手册,分工靠 description 里的路由说明写死。skills/open-source/SKILL.md 是一张查表:想问 Agent 参数读 references/agent.md,问自定义工具读 references/tools.md,问 MCP server 与 skills 集成读 references/integrations.md;它的描述里直接写了「不要用它处理 Cloud API/SDK 用法 —— 那个用 cloud 技能;不要用它做 CLI 直接驱动浏览器 —— 那个用 browser-use 技能」。
另一个能看出设计意图的细节是权限声明。六份手册里只有 browser-use 那份的 frontmatter 是光板的 name 加 description,其余五份都额外写了 allowed-tools,而且给的口子宽窄差得很远:cloud 和 open-source 这两份本质上是查表,就只给 Read;qa 要真跑浏览器还要派子任务,给的是 Bash, Read, Task;x402 要在你项目里写配置和代码,给到 Bash, Read, Write, Edit;remote-browser 反而收得最紧,只给 Bash(browser-use:*),也就是除了这一个命令什么都不许跑。权限面是跟着这份手册真要干的活裁的,不是统一发一套 —— 这个思路值得抄:技能文档本身就是权限申请书,能少要就少要。
装载靠 browser_use/skills/install.py。它的入口在 CLI 里挂成 browser-use skill,两个子命令:show 把技能文本打到标准输出,install 写文件。目标目录是一张常量表,键是 agents、claude、codex、copilot、cursor、gemini、opencode,前六个都拼成 ~/.<助手名>/skills/browser-use/SKILL.md,opencode 走 XDG_CONFIG_HOME(缺省 ~/.config)下的 opencode/skills/browser-use/SKILL.md,并且额外把 ~/.config/opencode/... 这条旧路径也加进候选清单做兼容。--target 默认值是 all,也就是一口气全写。--path 可以指定自定义目录或直接给一个 SKILL.md 路径。--force 这个参数的帮助文本写得很坦白:只为兼容而保留,因为 install 默认就覆盖已有的 SKILL.md。
技能文本从哪来也有一层回退:install 默认会先跑 uv tool install --python 3.12 --upgrade --force browser-use(找不到 uv 就直接报错让你先装 uv,可以用 --no-install 跳过这步),然后去找 browser-harness 可执行文件 —— 先 shutil.which,再看 ~/.local/bin 下的 browser-harness 和 browser-harness.exe;找到了就执行 browser-harness skill 拿文本,找不到就退回读包内那份 SKILL.md。
从 CLI 拿到的文本还要过一道改名。browser_use/skills/browser_use.py 里的 as_browser_use_skill() 会重写 frontmatter 的 name 与 description,把正文标题 # browser-harness/# Browser Harness 换成 # Browser Use,然后用一条带否定前视的正则 (?<!/)browser-harness 把正文里的品牌名全替换掉 —— 前面带斜杠的不换,这样 github.com/browser-use/browser-harness/... 这类仓库链接能原样保留。这个函数存在本身就说明了一件事:这层技能的正典源头是另一个叫 browser-harness 的工程,browser-use 这边做的是身份对齐后的分发。
三、第二层:把远端能力注册成 agent 的一个动作
browser_use/skills/service.py 里的 SkillService 是完全另一回事。它的构造函数收 skill_ids 和可选的 api_key,key 缺省从 BROWSER_USE_API_KEY 读,读不到直接抛 ValueError。也就是说这一层不是本地文件驱动的,它需要一个账号。
async_init() 做的事是拉取加缓存。客户端是 AsyncBrowserUse,调 list_skills(page_size=..., page_number=..., is_enabled=True)。这里有两条不同的分支值得记住:
skill_ids 里出现 '*' 时走通配符模式,只取第一页、最多 100 条,代码注释写的理由是「避免 LLM 工具过载」;如果这一页确实满了,会打一条 warning 提示你想要更多就显式写 ID。给显式 ID 时反而会翻页,一直翻到请求的 ID 全部找齐,或者某页返回条数不足一页为止,同时有 max_pages = 5 的安全上限,撞上限也会 warning。拉回来的条目还要过一道 skill.status == 'finished' 的过滤,请求了却没找到的 ID 会被单独 warning 列出来。
拿到的 SDK 对象经 Skill.from_skill_response() 转成 browser_use/skills/views.py 里的 Skill 模型,字段是 id、title、description、parameters、output_schema。真正把这层和模型接上的是两个转换方法:parameters_pydantic(exclude_cookies=...) 把参数 schema 列表变成一个 pydantic 模型类,output_type_pydantic 在有输出 schema 时把它也变成模型。转换逻辑在 utils.py 的 convert_parameters_to_pydantic() 里,把类型字符串映射成 Python 类型:string 到 str,number 到 float,boolean 到 bool,object 到 dict[str, Any],array 到 list[Any],cookie 也按 str 处理;required 没写时默认视为必填。
接下来是这一层最该看懂的一段。browser_use/agent/service.py 里的 _register_skills_as_actions() 会遍历所有技能,给每个技能算一个 slug 当动作名(_get_skill_slug 把标题去标点、转小写、空格和连字符换下划线,文档字符串里的例子是 [Cloned] Github Stars Tracker 变成 cloned_github_stars_tracker;重名时追加 skill.id[:4]),动作描述拼成技能描述加上带引号的标题,参数模型则用 skill.parameters_pydantic(exclude_cookies=True),最后走 self.tools.registry.action(...) 注册。这个注册动作发生在 run() 启动阶段、browser_session.start() 之后。
注意那个 exclude_cookies=True:注册给模型看的参数表里是不含 cookie 参数的,模型根本不知道有这回事。 cookie 由处理函数在执行时补:它先 await self.browser_session.cookies() 从当前浏览器会话取,execute_skill() 里再把 cookie 列表拍平成 name -> value 的字典填进参数。缺了必填的 cookie 就抛 MissingCookieException,异常自带 cookie_name 和 cookie_description 两个属性,动作层把它翻成 Missing cookies (名字): 说明 这样的错误串回给模型,让模型知道该引导用户去登录哪个站点。
参数在发出去之前会用技能自己的 pydantic 模型校验一遍,ValidationError 会被拼成逐字段的可读错误再抛 ValueError。远端调用失败时 execute_skill 不抛异常,而是返回一个 success=False 的响应对象。整条链路最终落回 ActionResult:成功就填 extracted_content,失败就填 error。
用户侧的入口就一行。Agent 同时接受 skill_ids 和 skills 两个参数,后者在源码里被注释成前者的别名,传了就以它为准;两个都传会直接 raise ValueError,报错文本还明说了让你用 skills。仓库里 examples/models/skills.py 给了最小示例,任务是问 browser-use 仓库有多少 star,技能位置传的是一个 UUID:
agent = Agent(
task='How many stars does the browser-use repo have?',
flash_mode=True,
skills=['502af156-2a75-4b4e-816d-b2dc138b6647'],
)
一个 UUID —— 这就是两层最直观的区别。第一层的技能标识是文件路径,第二层的技能标识是远端资源 ID。
四、两层对照
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 技能手册目录 | 用自然语言告诉编码代理该怎么开浏览器、什么时候别开、卡住去读哪份参考 | skills/browser-use/SKILL.md 等六个目录 | 让 Claude Code、Cursor 这类代理帮你操作浏览器时 |
| 手册分发副本 | 随 wheel 一起分发的同一份文本,供 skill_text() 回退读取 | browser_use/skills/browser-use/SKILL.md | 只装了包、没克隆仓库时 |
| 技能安装器 | 把手册写进各家助手的技能目录,必要时先升级 CLI 再从 browser-harness skill 取正典文本 | browser_use/skills/install.py | 执行 browser-use skill install 时 |
| 身份对齐层 | 重写 frontmatter 与正文品牌名,保留仓库链接不动 | browser_use/skills/browser_use.py | 排查装出来的 SKILL.md 为何与上游措辞不同时 |
| 技能拉取服务 | 凭 API key 从远端列出技能、按 ID 或通配符筛选并缓存 | browser_use/skills/service.py | 给 Agent(skills=[...]) 传 UUID 时 |
| 技能模型与转换 | 把参数 schema 转成 pydantic 模型,处理 cookie 类型参数与输出 schema | browser_use/skills/views.py、browser_use/skills/utils.py | 参数校验报错、想知道类型怎么映射时 |
| 动作注册与执行 | 给每个技能起动作名、藏掉 cookie 参数、执行时补 cookie 并把结果转成 ActionResult | browser_use/agent/service.py | 模型调了技能却拿到 Missing cookies 时 |
五、边界与代价
第一层放弃的是确定性。它是提示词,不是代码 —— 模型可以不读、读错、或者读了不照做。手册里那些「首次导航用 new_tab」「导航后 wait_for_load()」的规矩没有任何运行时强制,写得再清楚也只是概率上更容易被遵守。它也不做版本协商:install 默认覆盖已有的 SKILL.md,所以你在装出来的文件里手写的补充,下一次安装就没了 —— 手册自己给的出口是把任务特定的东西写到 agent_helpers.py,而不是改 SKILL.md。
第二层放弃的是自持。它需要 BROWSER_USE_API_KEY,技能定义在远端而不在你的仓库里,这意味着技能内容变了你的代码不会有任何 diff,回放和归因都得靠日志。它也不管技能本身怎么写、怎么测 —— 从这个包的视角看,一个技能就是一份参数 schema 加一次远端调用。通配符 '*' 更是明摆着的取舍:只拿第一页、上限 100 条,宁可漏也不把模型的工具表撑爆。
安全边界要说得更直白些。这类工具驱动的是真实浏览器,很可能带着你的真实登录态。第二层还会把浏览器里的 cookie 从会话取出、随参数发往远端执行 —— 这条数据流是你在读 Agent(skills=[...]) 这一行时看不见的,它藏在 exclude_cookies=True 后面。凡是涉及公司内网系统、支付、邮箱、社交账号的场景,都要先想清楚这个外泄面能不能接受,再想功能。
另外几件它明确不管:目标站点的使用条款要你自己判断,验证码和反自动化机制会拦你(手册的取向是遇到登录墙就停下来问用户,只在 Chrome 已登录时自动走 SSO,密码、多因素、授权确认、账号选择有歧义时一律停),账号被判为异常的风险由你承担;云端浏览器在停止或超时之前一直计费,手册要求任务做完直接问用户是否关闭,别默默留着。这些都不是缺陷,是它把边界写在了脸上 —— 而绕过风控这件事,不在任何一层的职责范围里。
六、上手与避坑清单
- 想让编码代理会用浏览器,装的是第一层。 会踩的原因是两层同名,容易以为配了 key 就万事俱备。做法:跑
browser-use skill install,然后到对应助手的~/.<助手名>/skills/browser-use/下确认SKILL.md真落地了,再让代理读一遍。 - 不想让它顺手升级你的 CLI,先加
--no-install。 会踩的原因是install默认会跑一次uv tool install --python 3.12 --upgrade --force browser-use,在锁了版本的机器上这是个意外变更。做法:--no-install跳过升级、直接用现有命令或包内文本。 - 不要在装出来的
SKILL.md里手写补充。 会踩的原因是默认覆盖,且--force只是个兼容用的空参数,没有「不覆盖」这个选项。做法:把项目专属的东西放进$BH_AGENT_WORKSPACE/agent_helpers.py,手册本身当只读。 --target all会往目标表里的每个助手目录都写一份。 会踩的原因是默认值就是all,而目标表有七个键、还额外带一条 opencode 旧路径,你可能只用其中一个。做法:显式写--target claude之类,或者用--path指到你自己的目录。- 第二层的
skills=收的是 UUID,不是名字也不是文件路径。 会踩的原因是被第一层的目录名带偏。做法:传远端技能 ID;写错或技能状态不是finished时不会报错中断,只会在日志里 warning「未找到或不可用」,所以启动阶段务必看一眼日志有没有这条。 skills和skill_ids只能传一个。 会踩的原因是文档里两个都出现过,容易图省事都写上。做法:统一用skills,另一个当历史别名看。- 别用
'*'上生产。 会踩的原因是它只取第一页、上限 100 条,还会把这一批全注册成动作,工具表膨胀直接影响模型的选择质量。做法:显式列出这次任务真需要的几个 ID,理由和给 agent 设计工具时控制工具数量是同一个。 - 看到
Missing cookies (...)别去改参数。 会踩的原因是这个错误看着像参数漏传,其实 cookie 参数根本没暴露给模型。做法:去让用户在浏览器里把对应站点登录好,让会话里真有那个 cookie,再重试。 - 技能注册发生在浏览器会话启动之后。 会踩的原因是它要从会话取 cookie,所以在
run()之前去查动作表是查不到技能动作的。做法:调试时把断点或日志放在启动之后,别在构造Agent那一刻找。
收束
这两层的分工可以压成一句话:第一层解决「代理知不知道该怎么做」,第二层解决「代理有没有一个现成的动作可以直接调」。前者的产物是上下文,后者的产物是工具表里的一项加一次远端调用。
站内另外三篇 —— Pi 的技能装载路径、ECC 的技能检索机制、superpowers 的技能文件契约 —— 讲的都是「文件式技能怎么被发现和加载」这一层的不同实现,可以横着对比;本篇的重点是 browser-use 这一个仓库里同名两层的切分,尤其是第二层那种「远端可执行技能被注册成 agent 动作」的另一种含义,那已经更接近动作参数怎么校验的话题。项目采用 MIT 许可证,上面每条都能自己打开核。
接下来该读哪个文件,取决于你要解决哪层的问题:卡在「代理不会开浏览器」就把 skills/browser-use/SKILL.md 整篇读完,它的密度足够当一份 CDP 操作备忘;卡在「装到哪去了」读 browser_use/skills/install.py 的目标目录常量表;卡在「技能怎么变成动作」就顺着 browser_use/skills/service.py 的 execute_skill 和 browser_use/agent/service.py 的 _register_skills_as_actions 各读一遍,cookie 那条隐式数据流看一次就再也忘不掉了。
本篇属于一个把开源浏览器操作 Agent 项目 browser-use逐层拆开讲的系列,整体地图见 browser-use 是什么;沿着这条线往下,还可以看 拆 browser-use 的遥测与观测 和 读 browser-use 的 beta 支线。