browser-use 为什么要备 8 份系统提示词而不是 1 份
本文基于 browser-use 仓库 commit f0aa3a8(2026-07-27)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/browser-use/browser-use 最新代码与文档为准。
一个 Agent 的系统提示词,一旦它要同时伺候多个模型家族和多档输出格式,就不可能是一份文件。 browser-use 的 browser_use/agent/system_prompts/ 目录里躺着 8 个 .md 文件,同一个浏览器 Agent,同一套动作,提示词分了 8 份。这个目录的 __init__.py 里只有一行注释,没有任何代码,说明这些文件不是被 Python 组装出来的,而是各自独立维护的整份文本。
这件事值得看的地方不在”提示词写得好不好”,而在于分叉的依据是什么。你自己做 Agent 时同样会撞上这个岔路口:要不要为不同模型准备不同提示词,切分标准放在哪一层。
一、这个目录解决的是什么问题
先摆事实。browser_use/agent/system_prompts/ 下的 8 份文件按体量分成两类:
system_prompt.md、system_prompt_no_thinking.md、system_prompt_anthropic_flash.md 三份都在 240 行以上,是完整版;system_prompt_flash.md、system_prompt_flash_anthropic.md、system_prompt_browser_use.md、system_prompt_browser_use_flash.md、system_prompt_browser_use_no_thinking.md 五份都在 31 行以内,是极简版。
完整版里有什么?打开 system_prompt.md 能看到成段的结构化标签:<input> 定义每一步喂进来的六类输入,<browser_state> 教模型怎么读 [index]<tagname attribute=value /> 这种带缩进的元素树,<browser_rules> 里二十七条硬规则,<planning> 讲什么复杂度才输出 plan_update,<task_completion_rules> 里嵌了一整块 <pre_done_verification>——调 done 且 success=true 之前必须重读用户请求、逐条核对数量与筛选条件、确认表单真的提交了、确认每个 URL 和数值都在工具输出里逐字出现过。后面还有 <reasoning_rules>、<examples>、<critical_reminders>、<error_recovery>。
极简版是另一个物种。system_prompt_flash.md 一共 16 行,<browser_state> 被压成一句话:元素格式 [index]<type>text</type>,只有带索引的可交互,缩进表示父子,*[ 表示新元素。<file_system> 压成一段连排的文字。示例、推理规则、错误恢复全部消失,只留下输出格式和两句底线:调 done 前的自检,以及不许用训练知识补缺、页面上没有就明说。
这里的取舍很直白:模型越强或者提示词预算越紧,教学性内容越可以砍。但砍到什么程度才算见底,8 份文件给出的答案并不一致:数据接地那一条(只报页面与工具输出里见到的数据、不许用训练知识补缺、绝不编造)在 8 份里一份不缺,措辞几乎逐字相同;done 前自检那一段则只出现在 5 份里,三份给微调模型的文件连它也省掉了。所以”底线”在这个项目里其实分了两级——一条是无论换什么模型都要重申的,另一条被认为可以交给模型本身的训练来承担。你自己拆提示词时值得先把这两级分清:哪条规则是模型再强也会犯的错,哪条是换个模型就不必再唠叨。
二、选哪一份不是配置项,是三个开关算出来的
选文件的逻辑在 browser_use/agent/prompts.py 的 SystemPrompt._load_prompt_template(),是一条从上到下的 if-elif 链,顺序本身就是优先级:
第一优先级是 is_browser_use_model。它在 browser_use/agent/service.py 里的判定条件是模型名里包含 browser-use/ 这个字符串。命中之后再按 flash_mode / use_thinking 落到 system_prompt_browser_use_flash.md、system_prompt_browser_use.md、system_prompt_browser_use_no_thinking.md 三份之一。这三份都是十几行的极简文本,而且带一段其它文件都没有的 <constraint_enforcement>:告诉模型凡是出现 do NOT、never、avoid、skip、only X 的指令都是硬约束,每次动作前先自查是否违反。给微调过的模型少讲流程、多压约束,这个取向在文件里看得很清楚。
第二、第三优先级都跟 Anthropic 模型有关,下一节单独说。
之后才是通用分支:flash_mode 为真取 system_prompt_flash.md,use_thinking 为真取 system_prompt.md,都不满足取 system_prompt_no_thinking.md。
模板读进来之后会走一次 self.prompt_template.format(max_actions=self.max_actions_per_step),所以文件里的 {max_actions} 是占位符,而 JSON 示例里所有花括号都写成了 {{ }} 双写。最后包成 SystemMessage(content=prompt, cache=True)。
关键的一点:flash_mode 不只是换文件。browser_use/agent/views.py 里的 type_with_custom_actions_flash_mode() 会从输出 schema 里删掉 thinking、evaluation_previous_goal、next_goal,并弹出 current_plan_item 和 plan_update,required 只剩 memory 和 action。service.py 里还有一句配套处理:flash_mode 为真时直接把 enable_planning 置为 False,注释写得很实在——计划字段已经被从 schema 里剥掉了,规划在结构上不可能发生。
提示词和输出契约是同一个开关的两个面。这也是这个目录必须分文件的根本原因:提示词里教模型填 thinking,schema 里却没有这个字段,两边一定要同步。
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 完整思考版提示词 | 全套输入说明、浏览器规则、规划、done 前自检、推理规则、示例 | browser_use/agent/system_prompts/system_prompt.md | 默认路径(use_thinking 为真且非 flash) |
| 无思考完整版 | 同上但输出不含 thinking 字段 | system_prompts/system_prompt_no_thinking.md | 显式把 use_thinking 关掉 |
| flash 极简版 | 只保留元素格式、文件系统、动作规则、输出格式与两条底线 | system_prompts/system_prompt_flash.md | flash_mode=True 且非 Anthropic |
| Anthropic flash 变体 | 改成让模型调 AgentOutput 工具,并要求 memory 字段排在 action 之前 | system_prompts/system_prompt_flash_anthropic.md | flash_mode + ChatAnthropic |
| Anthropic 4.5 的长 flash 版 | 篇幅接近完整版,保留 <browser_rules> 等大段内容 | system_prompts/system_prompt_anthropic_flash.md | flash_mode + 模型名匹配 Opus/Haiku 4.5 |
| 微调模型三件套 | 十几行,核心是 <constraint_enforcement> 加输出格式 | system_prompts/system_prompt_browser_use.md 及 _flash / _no_thinking 两份 | 模型名含 browser-use/ |
| 选取逻辑 | if-elif 链,决定上面到底读哪个文件 | browser_use/agent/prompts.py 的 SystemPrompt._load_prompt_template | 想换默认提示词或加自己的分支 |
| 输出 schema 裁剪 | 按模式删字段,和提示词配对 | browser_use/agent/views.py 的 type_with_custom_actions_flash_mode | flash 下发现字段消失 |
| 动作序列守卫 | 提示词里”页面变更动作放最后”的代码兜底 | browser_use/tools/registry/views.py 的 terminates_sequence、agent/service.py 的 multi_act | 多动作链被截断时 |
三、为什么给 Claude 4.5 的 flash 提示词反而更长
这条分支最反直觉,也最有信息量。
prompts.py 顶部有个小函数 _is_anthropic_4_5_model(model_name),做的事就是把模型名转小写,看它是否同时包含 opus 或 haiku、以及 4.5 或 4-5。命中并且 flash_mode 为真,选的是 system_prompt_anthropic_flash.md——这份文件 247 行,跟完整版几乎一样厚,一点也不”flash”。
函数上方的注释给了理由:这两个模型需要足够长的提示词才能命中缓存。也就是说,这里的长度不是为了多教模型几句,而是为了让系统消息够到缓存的下限,否则每一步都按未缓存计费与延迟走。别忘了 SystemPrompt 最后那句 cache=True,缓存是这条链路默认打开的。各家服务商的缓存规则不同且会调整,具体条件以官方最新说明为准。
再往下一档,flash_mode 为真且 is_anthropic(判定方式是 isinstance(self.llm, ChatAnthropic))时选 system_prompt_flash_anthropic.md。它和通用 flash 版的差别不在篇幅,在输出通道:通用版要求”响应一段合法 JSON”,Anthropic 版要求”调用 AgentOutput 工具并传入以下 schema”,还额外加了一句”始终把 memory 字段放在 action 字段之前”。
同一份任务描述,三种落地方式:纯 JSON、工具调用、以及为了缓存刻意加长。这就是分文件的真实驱动力——不是内容取向不同,而是模型接口和计费机制不同。顺带一提,service.py 里还有一处同类分叉:模型名以 claude-sonnet 开头时会自动把 llm_screenshot_size 设成 1400x850,连截图尺寸都按模型家族调过。
关于”提示词该分几层、哪些内容属于模型无关的公共层”,我在 Pi 的系统提示词是怎么组织的 里拆过另一个项目的做法,那篇讲的是单一提示词内部的分块;本篇讲的是同一职责被复制成多份文件之后的选取与漂移问题。而 flash 模式砍掉 thinking 字段这件事,本质上是输出约束的取舍,那条线在 Agent 输出约束到底该收多紧 里有更一般的讨论。
四、提示词里的规则,代码里有没有兜底
看过 system_prompt.md 的 <efficiency_guidelines> 会注意到一段分类:navigate、search、go_back、switch、evaluate 属于”总会改变页面”,必须放在动作列表最后,后面的动作会被自动跳过;input、scroll、find_text、extract、search_page、find_elements 和文件操作属于”可以安全串联”。
这不是只写在提示词里劝模型自觉。browser_use/tools/registry/service.py 的 action 注册器接受一个 terminates_sequence 参数,browser_use/tools/service.py 里 search、navigate、go_back、switch、evaluate 这几个动作注册时都传了 terminates_sequence=True。agent/service.py 的 multi_act() 文档字符串把机制写得很清楚:两层保护防止对着过期 DOM 执行动作——静态标记会让后续排队动作直接中止,运行时还会在每个动作后比对当前 URL 与焦点目标,一旦变化就清空剩余队列。仓库里也有对应测试文件校验这些标记。
顺手记一个细节:那段文档字符串列举的是 navigate、search、go_back、switch 四个,而 evaluate 在代码里同样标了 terminates_sequence=True。注释与实现之间的这种小落差,恰好是同一条规则写在多处时的典型代价,也是你回仓库核事实而不是照抄注释的理由。
对你的意义很直接:提示词里的每一条”你应该”,最好都有一处代码上的”你做不到”。 模型漏读一条规则是常态,串行动作打在已经跳转的页面上却是真实损失。规则重要到不能靠模型自觉,就把它挪到执行层。这个取舍我在 Agent 硬编码流程还是交给模型 里按场景拆过,本篇只补一个实证:这个项目的做法是提示词讲清、代码兜住,两边都写。
五、边界与代价:这套分法放弃了什么
放弃了单一事实源。 8 份文件不是一份主文件加 7 个 diff,是 8 份独立文本,内容漂移已经能看到。把 system_prompt.md 和 system_prompt_no_thinking.md 摆一起 diff,会发现后者对元素格式的描述停留在 [index]<type>text</type>,没有前者的树形 XML 说明,也没有 |SCROLL|、|SHADOW(open)| 这些前缀说明;search_page 和 find_elements 那三条使用建议在 no_thinking 版里整段缺失;“步数预算用到 75% 就重估能否完成”那段也只在完整版里有。它们描述的是同一个运行时,得到的信息量并不相同。维护成本就长在这儿。
放弃了配置的直观性。 选哪份提示词没有暴露成一个显式参数,是三个布尔量加模型名字符串匹配算出来的。模型名匹配尤其脆——'browser-use/' in model.lower()、opus 配 4.5 或 4-5、startswith('claude-sonnet'),只要你走网关、改别名、加前缀,命中与否就可能翻转。相关风险我在 模型别名带来的隐性风险 里单独写过。
它明确不管的事。 提示词只描述输入格式、动作纪律和输出契约,不负责判断这个任务该不该做。哪些站点允许自动化访问、哪些操作触及账号条款,这些边界在提示词外面。
几件必须如实说的风险。 这类 Agent 驱动的是真实浏览器:
- 你若复用带登录态的浏览器数据目录,模型就是在操作你的真实账号,误点提交、误发消息、误改设置都会真的生效,且大多不可撤销。
- 完整版提示词里有一句”CAPTCHA 由浏览器自动处理,遇到就继续任务”。仓库里确实有
browser_use/browser/watchdogs/captcha_watchdog.py和wait_if_captcha_solving(),但agent/service.py里当判定被验证码挡住时给出的提示是引导改用云端浏览器(提到隐身指纹与代理轮换)。所以这句提示词的前提依赖你的运行环境,别把它读成”任何配置下验证码都会被解决”。至于目标站的反自动化机制该不该绕,那是使用条款问题,不是技术问题——本文不讨论绕过手段。 - 页面文本会整段进模型上下文。你打开的后台、订单页、邮箱里的内容都可能被发给模型服务商,这一面在做敏感系统时要提前划线。
- 高频自动访问可能让账号或 IP 被判为异常。
六、上手与避坑清单
改提示词别直接改仓库文件。 会踩是因为文件名和选取链是耦合的,你改了 system_prompt.md,一旦 flash_mode 或模型家族变化,读到的是另一份文件,改动看起来”没生效”。Agent 的构造参数里有 override_system_message 和 extend_system_message,前者整体替换、后者拼在末尾(prompt += f'\n{extend_system_message}'),把你的私有规则放这里,跟上游文件解耦。
手写模板时注意花括号。 会踩是因为模板走 format(max_actions=...),你在自定义模板里写裸的 { 会直接抛异常。照着仓库文件的写法把 JSON 示例里的花括号双写成 {{ }},只留 {max_actions} 一个真占位符。
flash 模式下不要指望日志里有推理过程。 会踩是因为 thinking、evaluation_previous_goal、next_goal 被从 schema 里删掉了,不是模型偷懒不写。要看这些字段就别开 flash;反过来,你为它们写的可观测性逻辑要能容忍字段缺失。
用 ChatBrowserUse 时留意 flash 是自动打开的。 会踩是因为 service.py 里有一句:llm.provider == 'browser-use' 时直接把 flash_mode 设为真,随后 enable_planning 也被置为 False。你以为规划开着,其实计划字段根本不在 schema 里。
多动作链要按页面变更排序。 会踩是因为 terminates_sequence 加运行时 URL 比对是双层生效的,把 navigate 放在中间,它后面的动作会被静默丢弃,日志上像是”动作没执行”。把改变页面的动作放最后,或者干脆一步一个动作,用步数换确定性。
跨模型迁移时先确认读到了哪份提示词。 会踩是因为换个网关前缀就可能改变命中的分支,同一份代码在两个模型上表现不同,容易误判成”这个模型能力不行”。定位办法是直接构造 SystemPrompt 看 get_system_message() 的内容长度和首行,比猜快得多。多模型混搭的一般性坑我在 多模型混搭的代价 里整理过。
收尾
这个目录给出的现实是:提示词工程到了工程化阶段,切分维度不是”任务类型”,而是模型接口形态、输出 schema 档位、以及缓存计费这类和内容无关的机制约束。你要做的判断只有两个——哪些内容是所有版本都不能砍的底线(这个项目在 8 份文件里一份不落地重申的只有”禁止编造数据”这一条,done 前自检退了一级,给微调模型的三份就没有),以及哪些规则重要到必须下沉进代码。
接下来该读哪个文件,给三个入口:browser_use/agent/prompts.py 看选取链和消息拼装;browser_use/agent/views.py 看三种输出 schema 怎么裁;browser_use/tools/service.py 看动作是怎么注册的,提示词里提到的每个动作名都能在这里对上。这个项目采用 MIT 许可证,仓库在 https://github.com/browser-use/browser-use ,上面每一处路径你都可以当场打开核对。
本篇属于一个把开源浏览器操作 Agent 项目 browser-use逐层拆开讲的系列,整体地图见 browser-use 是什么;沿着这条线往下,还可以看 browser-use 的 Agent 循环拆解 和 browser-use 的 DOM 序列化。