跳到主要内容

数据截至 (上游 commit a675d6d61c41)

第 2 章 · 提示词、动作空间与 1000×1000 坐标

这章讲什么: Fara 的系统提示词是一串不能改一个字的文本 —— 模型是照着它训出来的。这章拆开它的三段结构、18 个动作的定义,以及贯穿全局的坐标换算。


2.1 为什么提示词是承重结构

先说结论:在 Fara 里,系统提示词不是可调参数,是模型接口的一部分。

仓库里有一个测试专门守着这件事(tests/test_fara15.py:85-114):它从身份串、critical points 串、工具 schema 三个常量重新拼一遍完整提示词,然后和 Fara15Agent._get_system_message() 的产出做字节级相等断言。测试注释说得很直白 —— 模型是在那串文本上训练的。

所以这一章读法应该是"读接口文档",而不是"读可以随便改的配置"。


2.2 三段式拼装

怎么读这张图:左边三个是独立的输入源,汇到中间拼成模板,再灌入工具描述得到最终 system message。

identity 串 ┐
(你是谁、什么时候训的) │
├──> build_fara_fn_call_template ──> 模板(含 {tool_descs} 占位)
critical points 串 │ _prompts.py:94-122 │
(什么时候必须停下问人) ┘ │
v
FaraBrowserComputerUse ──> .function(JSON schema) ──> NousFnCallPrompt.preprocess_fncall_messages
_prompts.py:125-239 qwen_helpers/fncall_prompt.py:36-128

v
最终 system message(纯文本)

第一段:身份

两个身份串注册在 IDENTITY_REGISTRY(src/fara/agents/fara/_prompts.py:71-74):

key底座模型训练时间窗
fara_qwen35(默认)Qwen3.5-9B2026 年 1–4 月
fara_qwen3vlQwen3-VL-8B-Instruct2026 年 1–3 月

身份串里除了自我介绍,还有一段很实用的话:告诉模型自己的知识截止在 2026 年初,想知道之后的事必须去浏览器上查(_prompts.py:20-23)。这是在用提示词直接压制"凭记忆瞎答"这个失败模式。

第二段:critical points

这是 Fara 安全设计的核心文本(_prompts.py:45-69),三种必须停下来的处境:

Case名字规则原文要点
1Missing User Information绝不编造个人信息;能填的先填,缺的再问
2Underspecified Task当前这步做不出决定就问,能决定就别停
3Irreversible Action用户明确授权过就直接做,没授权就停下确认

值得注意的是它写得很"反过度谨慎"。每一 Case 都有一句对称的免责:比如 Case 3 明确举例 —— 任务说"填表"就填完停下再问要不要提交,任务说"填表并提交"就直接提交别问(_prompts.py:60-65)。段尾还有一句总结,强调三个条件都不满足时不要打断(_prompts.py:67-69)。

这是在解决 CUA 的一个真实痛点:训得太保守的 agent 会在每一步都问用户,实用性归零。

第三段:函数调用格式

FN_CALL_FORMAT(_prompts.py:80-91)规定了输出协议:工具描述放在 <tools></tools> 里,模型回复时把调用包在 <tool_call></tool_call> 里。这是 Qwen-Agent 的 Nous 风格模板(src/fara/qwen_helpers/fncall_prompt.py 整个文件都是从 Qwen-Agent 搬来的,文件头有出处标注)。

三段的拼接方式

# 示意,非源码
template = identity + "\n\n" + critical_points + "\n\n" + FN_CALL_FORMAT
system_text = template.format(tool_descs=json.dumps(tool_schema))

真实实现是 build_fara_fn_call_template(_prompts.py:94-122)。它还允许直接传原始字符串而不是注册表 key,条件是长度 ≥ 20 字符 —— 这个长度检查是为了区分"你传了个不认识的 key"和"你传了段自定义文本"。


2.3 动作空间:18 个动作

动作定义在 FaraBrowserComputerUse 这个只有 schema、不能真调的工具类里(_prompts.py:125-239,call() 直接抛 NotImplementedError)。它只负责生成 JSON schema 给模型看,真正的执行在 agent 的 _dispatch_action

按性质分成四组:

组别动作说明
鼠标left_clickdouble_clickright_clicktriple_clickleft_click_dragmouse_move都要 coordinate: [x, y]
键盘与滚动keytypescrollhscrollkey 收键名数组,scroll 收像素数(正=上)
浏览器导航visit_urlhistory_backweb_searchweb_search 直接拼 Bing 搜索 URL
认知与控制read_page_answer_questionpause_and_memorize_factask_user_questionwaitterminate后三个是"元动作"

四个动作值得单独说

read_page_answer_question —— 这是唯一一个会再调一次模型的动作。它把整页转成 markdown,连同问题一起发给同一个客户端,让模型自己读页面回答(fara15_agent.py:814-818src/fara/agents/computer_agent/utils.py:116-134)。markdown 超过 20000 字符会被截断。

这本质上是给"只有一只眼睛"的 agent 配了一副放大镜:截图看不清的长文本,可以走文本通道读。

pause_and_memorize_fact —— 把一句话追加到 _state.facts(fara15_agent.py:829-832)。注意:这个列表在这个仓库的代码里只写不读 —— 没有任何地方把 facts 拼回提示词。模型"记住"这件事,实际是靠这个动作的观察文本 "I memorized the following fact: ..." 留在对话历史里生效的。换句话说,记忆机制等价于"把事实复述一遍写进上下文"。

web_search —— 不接搜索引擎 API,而是直接 goto 一个 Bing 搜索 URL(fara15_agent.py:807-812)。所以搜索结果是用眼睛看的,和人一样。

visit_url 的三分支路由(fara15_agent.py:787-794)是个小巧思:

输入的 url 字符串

├─ 以 https:// http:// file:// about: 开头 ──> 直接当 URL 用
├─ 含空格 ──────────────────────────────> 当成搜索词,拼 Bing 搜索
└─ 其它 ────────────────────────────────> 前面补 "https://"

第二分支是纯粹的容错:模型偶尔会把搜索词填进 url 字段,与其报错不如猜中它的意图。

动作和参数的映射

所有动作共享一个 JSON schema,靠 action 字段区分,其余参数全是可选(_prompts.py:228-231 只把 action 列为 required)。参数含义靠 description 里的 "Required only by action=xxx" 说明。这是 Qwen 系 computer-use 工具的惯用写法 —— 比给 18 个独立函数省 token。


2.4 坐标空间:三套尺寸,一次换算

这是整个项目最容易看晕的一处,先把三套尺寸列清楚:

尺寸谁在用
真实视口1440 × 900Playwright 实际操作的像素坐标
送模型的图1440 × 896smart_resize 算出来的,保证长宽都能被 32 整除
模型的坐标空间1000 × 1000提示词里写的"屏幕分辨率",模型输出的坐标范围

为什么图要缩到 1440×896

视觉模型把图切成 patch 处理。smart_resize(src/fara/qwen_helpers/utils.py:33-63)保证三件事:长宽都是 patch_size × merge_size = 16 × 2 = 32 的倍数、总像素在 [min_pixels, max_pixels] 之间、长宽比尽量不变。

1440 除以 32 正好是 45,不用动;900 除以 32 是 28.125,四舍五入到 28 → 896。所以只有高度掉了 4 个像素。

为什么坐标空间是固定的 1000

get_computer_use_system_prompt 的这两行(_prompts.py:286-287):

display_w = display_size if display_size is not None else resized_width
display_h = display_size if display_size is not None else resized_height

agent 传的 display_sizeFARA_DISPLAY_SIZE = 1000(src/fara/agents/coord_spaces.py:15,由 fara15_agent.py:124 引入)。传了就用固定值,不传才退化成图片实际尺寸

这一行的意义:上一代 Fara-7B 走的正是"不传"那条路 —— 它把 smart_resize 算出来的 resized_width/height 直接填进工具的 display_width_px / display_height_px,当成告诉模型的分辨率(src/fara/fara_7b/_prompts.py:197-203)。于是 Fara-7B 的坐标空间随截图尺寸浮动,换个视口就得重新适配;Fara-1.5 把它钉死成 1000×1000,视口随便换。

完整的换算链路

真实视口 1440×900
│ page.screenshot()
v
PNG 1440×900 ──resize──> 1440×896 ──base64──> 模型
│ (smart_resize) │
│ │ 提示词说:"screen resolution is 1000x1000"
│ v
│ 模型输出 (x, y) ∈ [0, 1000]
│ │
└────── _proc(): x × 1440/1000, y × 900/1000 ─┘
fara15_agent.py:495-503

换算发生在 _execute_action 里、分发之前,原地改写 args["coordinate"](fara15_agent.py:710-711)。测试给了两个锚点(tests/test_fara15.py:117-121):[500, 500] → [720.0, 450.0],[1000, 1000] → [1440.0, 900.0]

滚动量也要换算,但方向不同

垂直滚动按高度比例换算,水平滚动按宽度比例(fara15_agent.py:754-785):

# 示意,非源码
pixels = int(pixels * viewport_height / 1000) # scroll
pixels = int(pixels * viewport_width / 1000) # hscroll

注意 scroll 的观察文本只报方向("I scrolled up"),hscroll 却报具体像素数 —— 两者不对称,应该是历史遗留。


2.5 一个真实的往返例子

把前面所有东西串起来,一步长什么样:

送出去的(简化):

[system] You are Fara, a computer use agent (CUA) ... trained ... on top of Qwen3.5-9B.

A critical point is a situation where we must pause ...

You are provided with function signatures within <tools></tools> XML tags:
<tools>
{"type":"function","function":{"name":"computer_use", ... "The screen's resolution is 1000x1000." ...}}
</tools>
...
[user] <图: 1440×896 的 Bing 首页截图>
Current URL: https://www.bing.com/
Here is the next screenshot. Think about what to do next.

收回来的:

The search box is in the middle of the page. I'll click it first.
<tool_call>
{"name": "computer_use", "arguments": {"action": "left_click", "coordinate": [500, 340]}}
</tool_call>

运行时做的:

  1. 切出思考文本 "The search box is ...",和 JSON 分开。
  2. thoughts 塞回 arguments(fara15_agent.py:674),这样轨迹里动作和思考是一体的。
  3. 校验 left_click 在白名单里。
  4. [500, 340] → [720.0, 306.0]
  5. env.left_click(720.0, 306.0)
  6. 生成观察 "I clicked at coordinates (720.0, 306.0)."

重点看第 6 步:回给模型的观察里写的是换算后的真实像素,不是模型自己给的 1000 空间坐标。这算是个小小的不一致 —— 模型在下一轮看到的数字和它上一轮说的对不上。


2.6 关键细节与坑

只支持一种模式。 computer_use_mode 在 agent 侧登记了三种(browser / windows / windows_core,fara15_agent.py:132-136),但提示词构造函数只接受 fara_next_browser,其余直接 NotImplementedError(_prompts.py:268-272)。注释明说:这个仓库只发浏览器动作空间。

身份和模式必须匹配。 initialize 会校验 identity 是否属于该 mode 的合法身份组,不匹配直接报错(fara15_agent.py:186-198)。传 identity=None 则跳过校验、走自动检测。

几个配置参数是纯装饰。 include_input_text_key_argsfn_call_templaterandomize 三个参数被 get_computer_use_system_prompt 接收但完全不用,docstring 里坦白说是"为了和训练侧的提示词构造器签名兼容"(_prompts.py:260-263)。

每步重建提示词是幂等的。 _get_system_message 每一步都跑一遍,但因为 display_size 固定 1000、视口固定 1440×900,产出的文本每次都一样。重建的唯一副作用是刷新 _state.mlm_width/height_allowed_actions

Fara-7B 的动作空间小一半。 上一代只有 11 个动作(src/fara/fara_7b/_prompts.py:47-59),没有 double_click/right_click/triple_click/left_click_drag/hscroll/read_page_answer_question/ask_user_question;terminate 收的是 status: success|failure 而不是 answer 文本。没有 ask_user_question 意味着上一代根本没有"停下来问人"的能力 —— 这是 1.5 代最大的能力增量之一。


2.7 代码地图

主题文件路径符号名
提示词入口src/fara/agents/fara/_prompts.pyget_computer_use_system_prompt
三段拼装src/fara/agents/fara/_prompts.pybuild_fara_fn_call_template
身份文本src/fara/agents/fara/_prompts.pyFARA_QWEN35_IDENTITYIDENTITY_REGISTRY
critical points 文本src/fara/agents/fara/_prompts.pyCRITICAL_POINTS_FARA_1_5CRITICAL_POINTS_REGISTRY
动作 schemasrc/fara/agents/fara/_prompts.pyFaraBrowserComputerUse
Nous 模板渲染src/fara/qwen_helpers/fncall_prompt.pyNousFnCallPrompt.preprocess_fncall_messages
图像缩放规则src/fara/qwen_helpers/utils.pysmart_resize
工具 schema 校验src/fara/qwen_helpers/base_tool.pyis_tool_schemaBaseTool.function
归一坐标常量src/fara/agents/coord_spaces.pyFARA_DISPLAY_SIZEQWEN35_DISPLAY_SIZE
坐标换算src/fara/agents/fara/fara15_agent.py_procproc_coordsconvert_resized_coords_to_original
动作分发src/fara/agents/fara/fara15_agent.py_dispatch_action
页面抽取(放大镜)src/fara/agents/computer_agent/utils.pyextract_from_page
上一代动作空间src/fara/fara_7b/_prompts.pyFaraComputerUse
字节一致性测试tests/test_fara15.pytest_system_prompt_byte_identical_to_training