跳到主要内容

数据截至 (上游 commit a675d6d61c41)

第 6 章 · 巧妙之处、边界与横向对比

这章讲什么: 前五章把机器拆完了。这章只回答一个问题 —— 读完这个项目,你应该带走什么。


6.1 巧妙之处(可借鉴的技术)

① 把提示词当接口锁死,用测试守着

妙在哪: 对一个在特定提示词上做过 SFT 的模型,提示词改一个字符就可能掉点。但提示词是散文,天然容易被"顺手优化"。Fara 的做法是写一个测试,从三个源常量重新拼出完整提示词,和运行时产出做字节级相等断言(tests/test_fara15.py:85-114)。

怎么抄: 任何依赖精确提示词的系统,都应该有这么一个测试。它把"别乱改"这个口头约定变成了 CI 里的红灯。

② 固定归一坐标空间,把换算留在运行时

妙在哪: 上一代 Fara-7B 告诉模型的分辨率是截图缩放后的实际尺寸 —— 它把 smart_resize 算出来的 resized_width/height 直接填进工具配置(src/fara/fara_7b/_prompts.py:197-203),视口一改模型就要重新适配。Fara-1.5 把它钉成固定的 1000×1000(src/fara/agents/coord_spaces.py:15),换算全部交给运行时的 _proc(src/fara/agents/fara/fara15_agent.py:495-503)。

怎么抄: 模型输出的任何"物理量",都应该在一个和部署环境解耦的抽象单位里表达。一个常量换来的是模型和环境的完全解耦。

③ 动作白名单从渲染后的提示词里反向提取

妙在哪: extract_allowed_actions(fara15_agent.py:61-66)用正则从已经拼好的系统提示词文本里抠出 "enum": [...],而不是直接读 schema 常量。

这样一来,"模型被告知能用什么"和"运行时允许执行什么"物理上是同一份数据,结构上不可能漂移。改了 schema 忘了改校验列表这种经典 bug 被根除了。

怎么抄: 当两个地方必须保持一致时,让其中一个从另一个派生,而不是靠人维护两份。

④ 把不可见的事件伪造成可见的画面

这是 Fara 里最有"设计品味"的一条,而且出现了三次:

场景做法位置
Ctrl+F 查找浏览器原生查找栏够不着 → 在页内 JS 画一个假的,自动聚焦src/fara/environments/playwright/find_overlay.js
文件下载完成下载是不可见事件 → 跳转到一个 data URL 页面显示"已保存到 X"playwright_controller.py:412-417
滚动位置截图看不出"下面还有多少" → 文字化成 "scrolled to 34% of page"environment.py:677-692

统一的思路: agent 的世界只有截图。任何它需要知道但截图里没有的信息,要么画进截图里,要么写进文字观察里 —— 而不是新增一个它得学会调用的 API。

怎么抄: 给感知受限的 agent 加能力时,优先扩展它已有的感知通道,而不是扩展它的动作空间。前者不需要模型重新学习。

⑤ 三级解析容错,最后一级是"体面地失败"

模型输出解析的三级降级(fara15_agent.py:440-467):

json.loads ──失败──> ast.literal_eval ──失败──> 按配置二选一
(标准 JSON) (吃单引号等变体) ├─ 抛异常
└─ 当成 terminate,
原文当答案

妙在第三级。 批量评测时,一条轨迹解析失败不该炸掉整个进程。把它转成"一次失败但格式合法的终止",整批任务能继续跑,这条也会被如实记成失败。

⑥ 便宜的探测放在昂贵的恢复前面

页面恢复的第一步不是 reload,而是 page.evaluate("1")(playwright_controller.py:111-115)。一次几毫秒的表达式求值就能分辨"页面真死"和"刚才那个错误是假警报",避免了没必要的整页重载。

怎么抄: 任何自动恢复流程,第一步都应该是最便宜的状态确认

⑦ 执行与打分彻底分离

exec_hash() / eval_hash() 双 hash 路由(webeval/src/webeval/benchmark.py:131-143)让"跑一次轨迹、用 N 种裁判配置反复打分"成为默认能力,而不是需要额外搭脚手架的特殊操作。

怎么抄: 昂贵不可复现的步骤(跑真实网站)和便宜可复现的步骤(打分)之间,应该有一个持久化的边界。

⑧ 不是所有 LLM 判断都值得投票

裁判的 Step 8 里,"结果对不对"跑 N 次多数投票,"有没有越界"只跑 1 次。源码注释直说:后者是有明确依据的确定性判断,投票买不到多少东西(webeval/src/webeval/rubric_agent/mm_rubric_agent.py:3501-3505)。

怎么抄: self-consistency 不是免费的。只在模型确实容易分歧的判断上用。


6.2 边界与局限(诚实清单)

刻意不做的

不做什么依据
多标签页single_tab_mode 默认 True,新页接管后关掉旧页;动作空间里没有"切换标签页"
浏览器之外的桌面computer_use_mode 只接受 fara_next_browser,windows 模式直接 NotImplementedError(_prompts.py:268-272)
一步多动作每轮只解析一个 <tool_call>,没有并行工具调用
可访问性树 / DOM 定位这是整个项目的立身之本 —— 全靠视觉
沙箱README 明确建议在沙箱环境里跑、要盯着执行(README.md:225),沙箱本身要靠 Magentic-UI 提供

能力缺口

跨进程续跑不成立。 Agent.save_state / load_state 钩子在基类里(src/fara/core/agent.py:144-164),Fara15Agent 没有覆盖。磁盘上有完整事件流,但重建不出模型的 chat_history。所以"停下来问用户"只能在同一个进程内完成。

步数耗尽被标成 COMPLETE。 SolverStatus.MAX_ROUNDS 存在但从未被赋值(fara15_agent.py:349-352)。下游只能靠"最后一个动作是不是 terminate"来区分,评测侧确实这么补了(webtailbench.py:406-413),但任何直接读轨迹状态的人都会被误导。

facts 只写不读。 pause_and_memorize_fact 把事实存进 _state.facts(fara15_agent.py:829-832),但没有任何地方把它拼回提示词。记忆实际是靠观察文本留在对话历史里生效的 —— 也就是说,一旦这条观察被截图裁剪逻辑挤出上下文,这个"记忆"就没了

save_screenshots 配置项无效。 声明了、CLI 赋值了、从未被读(见 §1.8)。截图实际只看 output_dir

get_visible_text 必然抛错。WebSurfer.getVisibleText(),而页内脚本的全局名是 MultimodalWebSurfer、且没导出这个函数(见 §3.7)。死代码,但埋着雷。

结构性风险

视觉定位没有兜底。 模型报的坐标错了(点在按钮边缘、页面在截图后发生了重排),没有任何验证机制。对比之下,DOM 系的 agent 至少能报"选择器没匹配到"。这是纯视觉路线的固有代价。

时序全靠固定 sleep。 每个非停机动作之后统一调一次 env.wait_for_load()(fara15_agent.py:724-726),它在等到 load state 之后再无条件睡 extra_sleep_time(environment.py:641-642;这个字段默认 3.0 秒,定义在 BrowserEnvironmentConfig,src/fara/environments/computer.py:157)。同类固定等待还有两处:弹窗捕获等 1 秒(playwright_controller.py:513)、验证码解完再等 3 秒(environment.py:279)。慢网站会截到加载中的图,快网站白等。

两套 agent 实现并存。 src/fara/agents/fara/(1.5)和 src/fara/fara_7b/(7B)是两份平行代码,各有一套 message 类型、prompt 构造、控制器。共享的只有 qwen_helpers/。维护成本翻倍。

webeval 落后于运行时。 见 §5.7,README 自己承认了。


6.3 横向对比

同为浏览器 agent:感知路线的分野

维度Farabrowser-useStagehand
感知纯截图DOM 树 + 可交互元素索引DOM + 视觉辅助
定位方式模型直接报像素坐标模型选元素编号自然语言 → 选择器
对模型的要求必须是视觉模型,且要专门训过定位通用模型即可通用模型即可
定位失败表现静默点错地方报"索引不存在"报"选择器没匹配"
token 成本图片 token(固定)DOM 文本(随页面复杂度爆炸)中间
换视口/换设备归一坐标,零成本不受影响不受影响

取舍一句话: Fara 用"必须专门训模型"换来"不受页面结构复杂度影响";DOM 系用"页面越复杂上下文越贵"换来"任何通用模型都能用"。

同为 computer-use:范围的分野

维度FaraOSWorld
操作对象只有浏览器整个 Linux 桌面(含终端、文件管理器、办公软件)
环境实现Playwright 直连虚拟机 + 截图/输入注入
定位归一坐标依实现而定
定位是仓库重点吗是,主体就是运行时否,主体是任务集与验证脚本

Fara 的动作空间里 visit_url / history_back / web_search 这类浏览器专属动作占了很大比重 —— 这不是简化版的桌面 agent,是为浏览器专门设计的动作空间。

同为 agent 评测:裁判形态的分野

维度Fara / webevalinspect-aiopenai-evals
打分对象多模态轨迹(截图序列)文本 TaskState文本补全
裁判复杂度九步流水线、双模型、多数投票可组合的 scorer模板化的 model-graded eval
输出信号过程分 + 结果分两个独立信号单一 score(可自定义)单一 score
打分可复跑双 hash 路由,跑一次评多次有 log 复用有缓存

Fara 独有的那一条是过程/结果分离。 文本任务里"过程"没什么好评的,但对 agent 来说"步步正确但最后一步点错"和"瞎点碰对了"是完全不同的两种失败/成功,必须分开报。

代际对比:Fara-7B → Fara-1.5

维度Fara-7B(src/fara/fara_7b/)Fara-1.5(src/fara/agents/fara/)
动作数1118
坐标空间随截图尺寸浮动固定 1000×1000
停下来问用户不支持(没有 ask_user_question)支持,且写进了提示词
critical points只在通用 fn-call 模板里有一段泛泛的话(qwen_helpers/fncall_prompt.py:145-160)三 Case 结构化定义,是提示词第二段
terminate 参数status: success|failureanswer: <文本>
读页面回答问题有(read_page_answer_question)
轨迹记录自己管截图和日志统一走 RunContext / DataPoint
环境抽象直接持有 BrowserBBEnvironment 抽象基类

最有信息量的一行是**「停下来问用户」**:上一代根本没有这个动作,所以"critical point"在 7B 上只能表现为"停下不做";1.5 代把它升级成了"停下来问",这才让 README 里说的"多轮 rollout + 用户模拟器训练"(README.md:135README.md:143)有落点。


6.4 全局代码地图

按"我想干什么"组织,便于直接跳源码。

想看主流程

我想……打开
看 CLI 怎么起src/fara/run_fara.pymainrun_fara15_agent
看一步怎么走完src/fara/agents/fara/fara15_agent.pyFara15Agent.run
看提示词怎么拼src/fara/agents/fara/_prompts.pyget_computer_use_system_prompt
看动作怎么执行src/fara/agents/fara/fara15_agent.py_dispatch_action
看动作怎么落到浏览器src/fara/environments/playwright/environment.pyPlaywrightEnvironment

想改东西

我想……打开注意
加一个动作_prompts.pyFaraBrowserComputerUse.parameters + fara15_agent.py_dispatch_action模型没训过新动作,加了也不会用
换视口尺寸run_fara.pyPlaywrightEnvironment(...) 参数归一坐标会自动跟着换算
改上下文里保留几张图Fara15AgentConfig.max_n_images默认 3
开 token 预算保险丝Fara15AgentConfig.image_budget_token_cap默认 0 = 关闭
让 agent 别停下来问Fara15AgentConfig.auto_user_reply=True只适合评测,会注入假授权
让解析失败不炸Fara15AgentConfig.terminate_on_parse_error=True批量跑时应该开
调动作后的固定等待BrowserEnvironmentConfig.extra_sleep_time(computer.py:157)默认 3.0 秒;调小会更容易截到加载中的画面

想读数据

我想……打开
理解轨迹 JSONsrc/fara/core/data_point.pyDataPointSolverLog
把事件流还原成步骤src/fara/core/data_point.pySolverLog.stepsget_step_summaries
读磁盘上的轨迹src/fara/core/data_point_io.pyDataPointReader.read

想做评测

我想……打开
理解打分流程webeval/src/webeval/rubric_agent/mm_rubric_agent.py_generate_reply
接一个新 benchmarkwebeval/src/webeval/benchmark.pyBenchmark
复现 WebTailBenchwebeval/scripts/webtailbench.py(脚本)
只重新打分webeval/scripts/verify_trajectories.py(脚本)
看复现说明docs/eval_reproducibility.md——

想验证理解

我想……打开
确认提示词长什么样tests/test_fara15.pytest_system_prompt_byte_identical_to_training
确认坐标换算tests/test_fara15.pytest_proc_coords_scales_display_space_to_viewport
确认动作全集tests/test_fara15.pyBROWSER_ACTIONS
看一条真实轨迹webeval/data/example_trajectory/一个完整的 AllTrails 任务

6.5 一页纸总结

Fara 是什么: 一个纯视觉的浏览器 computer-use agent 运行时,加一套给这类 agent 打分的评测框架。

它赌的是什么: 赌"小模型 + 专门训练的视觉定位能力"能打赢"大模型 + DOM 文本"。README 报的数字(9B 在 Online-Mind2Web 上 63.4%,README.md:178)是这个赌注的成绩单。

代码里最值得学的: 不是 agent 循环本身(那部分很朴素),而是围绕"感知受限的 agent"做的一系列观察设计 —— 归一坐标、伪造的查找栏、文字化的滚动位置、data URL 形式的下载回执。这些设计的共同点是:不给 agent 加需要学习的新能力,而是把世界改造成它已经看得懂的样子。

用之前要知道的: 这是 research preview(README.md:225),要沙箱、要盯着、别碰敏感数据;webeval 那半边还停在上一代。