数据截至 (上游 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-9B | 2026 年 1–4 月 |
fara_qwen3vl | Qwen3-VL-8B-Instruct | 2026 年 1–3 月 |
身份串里除了自我介绍,还有一段很实用的话:告诉模型自己的知识截止在 2026 年初,想知道之后的事必须去浏览器上查(_prompts.py:20-23)。这是在用提示词直接压制"凭记忆瞎答"这个失败模式。
第二段:critical points
这是 Fara 安全设计的核心文本(_prompts.py:45-69),三种必须停下来的处境:
| Case | 名字 | 规则原文要点 |
|---|---|---|
| 1 | Missing User Information | 绝不编造个人信息;能填的先填,缺的再问 |
| 2 | Underspecified Task | 当前这步做不出决定就问,能决定就别停 |
| 3 | Irreversible 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_click、double_click、right_click、triple_click、left_click_drag、mouse_move | 都要 coordinate: [x, y] |
| 键盘与滚动 | key、type、scroll、hscroll | key 收键名数组,scroll 收像素数(正=上) |
| 浏览器导航 | visit_url、history_back、web_search | web_search 直接拼 Bing 搜索 URL |
| 认知与控制 | read_page_answer_question、pause_and_memorize_fact、ask_user_question、wait、terminate | 后三个是"元动作" |
四个动作值得单独说
read_page_answer_question —— 这是唯一一个会再调一次模型的动作。它把整页转成 markdown,连同问题一起发给同一个客户端,让模型自己读页面回答(fara15_agent.py:814-818 → src/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 × 900 | Playwright 实际操作的像素坐标 |
| 送模型的图 | 1440 × 896 | smart_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_size 是 FARA_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>
运行时做的:
- 切出思考文本
"The search box is ...",和 JSON 分开。 - 把
thoughts塞回 arguments(fara15_agent.py:674),这样轨迹里动作和思考是一体的。 - 校验
left_click在白名单里。 [500, 340] → [720.0, 306.0]。env.left_click(720.0, 306.0)。- 生成观察
"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_args、fn_call_template、randomize 三个参数被 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.py | get_computer_use_system_prompt |
| 三段拼装 | src/fara/agents/fara/_prompts.py | build_fara_fn_call_template |
| 身份文本 | src/fara/agents/fara/_prompts.py | FARA_QWEN35_IDENTITY、IDENTITY_REGISTRY |
| critical points 文本 | src/fara/agents/fara/_prompts.py | CRITICAL_POINTS_FARA_1_5、CRITICAL_POINTS_REGISTRY |
| 动作 schema | src/fara/agents/fara/_prompts.py | FaraBrowserComputerUse |
| Nous 模板渲染 | src/fara/qwen_helpers/fncall_prompt.py | NousFnCallPrompt.preprocess_fncall_messages |
| 图像缩放规则 | src/fara/qwen_helpers/utils.py | smart_resize |
| 工具 schema 校验 | src/fara/qwen_helpers/base_tool.py | is_tool_schema、BaseTool.function |
| 归一坐标常量 | src/fara/agents/coord_spaces.py | FARA_DISPLAY_SIZE、QWEN35_DISPLAY_SIZE |
| 坐标换算 | src/fara/agents/fara/fara15_agent.py | _proc、proc_coords、convert_resized_coords_to_original |
| 动作分发 | src/fara/agents/fara/fara15_agent.py | _dispatch_action |
| 页面抽取(放大镜) | src/fara/agents/computer_agent/utils.py | extract_from_page |
| 上一代动作空间 | src/fara/fara_7b/_prompts.py | FaraComputerUse |
| 字节一致性测试 | tests/test_fara15.py | test_system_prompt_byte_identical_to_training |