標註網頁瀏覽智慧體:從 WebArena 軌跡到人工評測
如何用 Potato 的網頁智慧體軌跡展示來評測自主網頁瀏覽智慧體,包含逐步截圖、SVG 覆蓋層和逐步標註方案。
把智慧體的軌跡轉換成 Potato 的網頁智慧體格式,用 web_agent_trace 檢視器展示,讓每張截圖都帶上標明智慧體點選位置的覆蓋層,再用 trajectory_eval 方案給各個步驟打分。
評判一個網頁瀏覽智慧體,看的是它針對頁面所採取的動作,所以評測需要每一步的兩個部分:智慧體看到的頁面狀態,以及它在這個狀態上執行的動作。只有動作日誌撐不起這個判斷。"點選了元素 product-101"並不能告訴評審者那是不是頁面上最便宜的筆記本,而一張沒有標記的截圖會讓人只能猜測十幾個連結裡哪一個才是目標。
本指南為 WebArena 軌跡搭建這套評測。同一條路徑也適用於 VisualWebArena、Mind2Web、Anthropic Computer Use 日誌和原始瀏覽器錄製,因為它們都由同一個轉換器覆蓋。完整的配置參考見源文件。
你需要什麼
pip install potato-annotation你還需要軌跡本身。對 WebArena 來說,這意味著每個回合的一組截圖加一份 JSON 動作日誌。VisualWebArena 記錄的結構相同,另外帶有視覺定位資訊;原始瀏覽器錄製帶的則是視口和滑鼠路徑資料。
原始 WebArena 軌跡裡有什麼
一個 WebArena 回合包含任務描述、動作序列,以及每張截圖的路徑:
{
"trace_id": "wa_001",
"task": "Find the cheapest laptop on the electronics store and add it to the cart",
"website": "shopping",
"steps": [
{
"step": 0,
"url": "http://shop.example.com/",
"action_type": "click",
"action_target": "Electronics category link",
"element_id": "nav-electronics",
"coordinates": [245, 82],
"screenshot": "screenshots/wa_001_step_00.png"
},
{
"step": 1,
"url": "http://shop.example.com/electronics",
"action_type": "click",
"action_target": "Laptops subcategory",
"element_id": "cat-laptops",
"coordinates": [180, 310],
"screenshot": "screenshots/wa_001_step_01.png"
}
],
"success": true
}轉換軌跡
Potato 的檢視器讀取的是一種規範化的步驟格式,而不是各框架各自的格式,所以原始回合要先經過轉換器:
python -m potato.trace_converter -i traces.json -f web_agent -o data/web_traces.jsonl這會為每個回合寫出一個實例,每一步帶有 step_index、screenshot_url、action_type、目標 element 及其邊界框、coordinates、可選的 mouse_path,以及智慧體自己的 thought 和 observation(前提是源日誌記錄了它們)。把 -f web_agent 換成 --auto-detect,轉換器就會自行識別格式;當一個目錄裡存放著來自多個框架的軌跡時,就該用這個選項。
項目配置
這個檢視器是一種展示類型,而不是標註類型,所以它放在 instance_display.fields 下面,標註方案與它分開。最值得先定下來的設定是截圖尺寸:900 畫素寬既能讓整頁桌面版頁面保持可讀,又不會把標註控制元件擠出螢幕底部。
annotation_task_name: "WebArena Agent Evaluation"
task_dir: "."
data_files:
- "data/web_traces.jsonl"
item_properties:
id_key: id
text_key: task_description
output_annotation_dir: "output/"
export_annotation_format: "jsonl"
instance_display:
layout:
direction: vertical
gap: 16px
fields:
- key: task_description
type: text
label: "Task"
- key: steps
type: web_agent_trace
label: "Agent Browsing Trace"
display_options:
show_overlays: true
show_filmstrip: true
show_thought: true
show_observation: true
show_element_info: true
screenshot_max_width: 900
screenshot_max_height: 600
filmstrip_size: 80
annotation_schemes:
- annotation_type: radio
name: task_success
description: "Did the agent complete the task?"
labels:
- "Success"
- "Partial success"
- "Failure"
label_requirement:
required: true
- annotation_type: radio
name: task_efficiency
description: "Was the agent's path to the goal efficient?"
labels:
- "Optimal path"
- "Reasonable but not optimal"
- "Inefficient, unnecessary steps"
- "Completely wrong direction"
label_requirement:
required: true
- annotation_type: trajectory_eval
name: step_evaluation
description: "Judge each action against the task"
steps_key: steps
step_text_key: action_type
correctness_options:
- correct
- acceptable
- incorrect
- unnecessary
error_types:
- name: wrong_element_clicked
- name: wrong_page_navigated_to
- name: missed_better_option
- name: incorrect_form_input
- name: premature_completion
- name: unnecessary_navigation
- name: failed_to_scroll
- name: wrong_page_section
- name: other
severities:
- name: minor
weight: -1
- name: major
weight: -5
- name: critical
weight: -10
show_score: true
max_score: 100
user_config:
allow_all_users: true一個 trajectory_eval 方案就覆蓋了整個逐步評估。它把每一步渲染成一張卡片,在每張卡片上收集正確性、錯誤類型、嚴重程度和一段文字理由,再根據嚴重程度的權重算出軌跡級別的品質分。上面的權重讓一條軌跡每出現一個 major 錯誤扣五分,critical 錯誤扣十分。這些權重要慎重設定,因為它們決定了一條有一次嚴重失敗的軌跡與一條有四次小錯的軌跡誰排在前面。
標註者看到的介面

當前步驟的截圖佔據主面板,上面繪製著 SVG 覆蓋層。每種覆蓋層標出該步驟的不同部分,顏色是固定的:
| 覆蓋層 | 標出的內容 |
|---|---|
| 帶十字準星的紅色圓圈 | 點選座標 |
| 黃色高亮 | 智慧體輸入文字的那個輸入框 |
| 綠色箭頭 | 滾動的方向和幅度 |
| 藍色虛線矩形 | 目標元素的邊界框 |
| 橙色曲線 | 移動到目標所走的滑鼠路徑 |
底部有一條縮圖膠片條,點選其中一張就會跳到對應步驟。在超過十步左右的軌跡上,有沒有膠片條,決定了評審者是能看清整段執行的形狀,還是隻能盲目地滾動。步驟詳情面板裡有元素資訊、智慧體自己的 thought,以及它記錄的 observation;當源日誌裡這幾項沒有有用內容時,都可以在 display_options 中關掉。
檢視器一旦獲得焦點就會接管鍵盤,這一點值得在第一天就教給標註者:
| 按鍵 | 動作 |
|---|---|
Left / Right | 上一步/下一步 |
1 | 切換點選標記 |
2 | 切換邊界框 |
3 | 切換滑鼠路徑 |
4 | 切換滾動指示 |
A / N | 顯示全部覆蓋層/隱藏全部覆蓋層 |
在內容密集的頁面上,關掉覆蓋層很重要,因為點選標記可能正好壓住那段能說明這次點選對不對的文字。A 和 N 鍵一次性切換整組覆蓋層,比去琢磨五種覆蓋層裡是哪一種擋住了要快。
標註流程
評審者用五個動作處理一條軌跡:
- 閱讀任務描述。 後面的一切判斷都以它為準。
- 掃一遍膠片條。 在給任何步驟打分之前先摸清整段執行的形狀,免得靠後面才出現的證據把早期的步驟判成正確。
- 逐步檢視。 對每一步,讀懂頁面狀態,看覆蓋層確認智慧體作用在了什麼上面,然後給這個動作打分。如果動作是錯的,選出錯誤類型和嚴重程度,並在理由裡寫明智慧體本應怎麼做。
- 給整條軌跡打分。 看完每一步之後,評判整體的成功度和效率。
- 提交,然後處理下一條。
這四個正確性選項按照對訓練資料有意義的那條界線來劃分步驟。Correct 的動作以預期的方式推進目標。Acceptable 的動作走的是更慢的路線,但仍在推進,比如一層層瀏覽到分類頁而不是用搜索框。Incorrect 的動作是失誤,比如點錯元素、進了無關頁面,或在表單裡填錯文字。Unnecessary 的動作既無幫助也無損害,比如點一下之後立刻退回;把它們與失誤分開是值得的,因為一個到處亂逛的智慧體和一個判斷錯誤的智慧體是兩類不同的問題。
錯誤分類體系
錯誤類型能把一堆失敗的軌跡變成智慧體開發者可以著手處理的東西,所以這些類別應當對應可區分的成因,而不是錯誤的程度:
| 錯誤類型 | 含義 | 示例 |
|---|---|---|
| 點錯元素 | 智慧體作用在了錯誤的介面元素上 | 點了"Tablets"而不是"Laptops" |
| 導航到錯誤頁面 | 智慧體最終停在了無關的頁面上 | 進了"About Us"而不是商品列表 |
| 錯過更好的選項 | 當時存在更好的動作 | 搜尋框就在旁邊,卻去一層層瀏覽分類 |
| 表單輸入錯誤 | 智慧體輸入了錯誤的文字 | 搜尋"labtop"而不是"laptop" |
| 過早宣告完成 | 智慧體太早認定成功 | 把錯的商品加進購物車就停了 |
| 多餘的導航 | 該步驟對目標沒有貢獻 | 在兩個分類頁之間又去了一趟首頁 |
| 未能滾動 | 目標位於視口下方 | 元素始終不可見,而智慧體從未滾動頁面 |
| 頁面區域錯誤 | 頁面對了,區域錯了 | 點了頁頭而不是主體內容 |
處理棘手的軌跡
在超過十五步的軌跡上,對每一步都投入同樣的深度,大部分精力會花在那些明顯沒問題的步驟上。先掃一遍膠片條,找出標誌著問題的三種跡象,再把精力集中過去。這三種跡象是:URL 發生意外變化、智慧體在某一步往回走,以及連續出現幾乎相同的截圖,後者通常意味著智慧體陷入了迴圈。
在一條失敗的軌跡上,第一個錯誤的步驟是最值得標註準確的那一個。它之後的一切都發生在智慧體本不該到達的狀態裡,所以後面那些錯誤描述的是後果,而不是原因。
有些動作僅憑截圖無法判斷,通常是幾個元素在點選位置上重疊的情況。步驟面板裡的元素資訊給出目標的標籤、文本和邊界框,一般足以確定究竟點到了什麼;show_element_info 讓那個面板保持可見。
VisualWebArena
VisualWebArena 的軌跡走的是同一個轉換器,它通過動作步驟中的 action_type 和 element 欄位識別這兩種格式。這些軌跡攜帶的定位資訊會顯示為邊界框覆蓋層,所以一次以定位為重點的標註取決於標註者在螢幕上留下什麼,而不需要改配置。按 2 讓邊界框保持顯示,按 1 關掉點選標記,剩下的檢視能說明智慧體瞄準的是不是正確的元素,而不只是正確的畫素。
解讀結果
網頁智慧體的錯誤往往集中在軌跡中的特定位置,而按步驟位置統計的錯誤率是最快看出位置的辦法。每條 trajectory_eval 標註都儲存著一份逐步記錄的列表,每條記錄帶有自己的 step_index 和 correctness:
import json
annotations = []
with open("output/exports/jsonl/annotations.jsonl") as f:
for line in f:
annotations.append(json.loads(line))
step_counts = {}
for ann in annotations:
evaluation = ann["labels"].get("step_evaluation", {})
for step in evaluation.get("steps", []):
pos = step["step_index"]
counts = step_counts.setdefault(pos, {})
label = step.get("correctness", "unrated")
counts[label] = counts.get(label, 0) + 1
print("Error rate by step position:")
for pos in sorted(step_counts):
counts = step_counts[pos]
total = sum(counts.values())
errors = counts.get("incorrect", 0) + counts.get("unnecessary", 0)
print(f" Step {pos}: {errors / total:.1%} error rate ({total} observations)")錯誤類型的分佈回答的是另一個問題:智慧體錯在什麼地方,而不是錯在什麼時候。
error_counts = {}
for ann in annotations:
evaluation = ann["labels"].get("step_evaluation", {})
for step in evaluation.get("steps", []):
error = step.get("error_type")
if error:
error_counts[error] = error_counts.get(error, 0) + 1
for error, count in sorted(error_counts.items(), key=lambda x: -x[1]):
print(f" {error}: {count}")把評測規模化
如果結果打算發表,每條軌跡一位評審者是不夠的,因為逐步的判斷恰好帶有一致性統計量所要衡量的那種歧義。在配置的頂層設定覆蓋目標和分配順序:
num_annotators_per_item: 3
assignment_strategy: random然後在軌跡級別的標籤上比較標註者,因為在這個標籤上的分歧是明確的。先匯出為 Parquet,這樣比較是在列上進行,而不是在解析過的 JSON 上進行:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/from sklearn.metrics import cohen_kappa_score
import pandas as pd
df = pd.read_parquet("parquet_export/annotations.parquet")
success = df[df["schema_name"] == "task_success"]
pivot = success.pivot(index="instance_id", columns="annotator", values="value")
annotators = pivot.columns.tolist()
for i in range(len(annotators)):
for j in range(i + 1, len(annotators)):
pair = pivot[[annotators[i], annotators[j]]].dropna()
kappa = cohen_kappa_score(pair[annotators[i]], pair[annotators[j]])
print(f"Kappa ({annotators[i]} vs {annotators[j]}): {kappa:.3f}")在數百條軌跡規模的評測中,Solo 模式把一個模型放到與人工評審者相同的佇列上:模型提出標籤,人來糾正,直到兩者的一致程度足以讓模型完成剩下的部分。
solo_mode:
enabled: true
labeling_models:
- endpoint_type: "anthropic"
model: "claude-sonnet-4-20250514"
api_key: "${ANTHROPIC_API_KEY}"
thresholds:
end_human_annotation_agreement: 0.90
minimum_validation_sample: 50真正需要琢磨的設定是一致性閾值,而不是模型的選擇。設為 0.90 時,人會一直標註下去,直到模型的標籤有十分之九與自己的一致,而且這個比例至少要在五十次比較上測得;調低它就是拿標籤品質換評審工時,而且事後很難察覺。
延伸閱讀
- 智慧體標註 —— 完整的轉換器與展示參考
- 網頁智慧體標註 —— 檢視器的展示選項
- 評測 AI 智慧體 —— 瀏覽器之外的智慧體評測
- Parquet 匯出 —— 上面的分析所讀取的那些列