两个独立维度
将 AndroidDaily 论文的 GRADE 评估协议(人工准则+LLM 状态追踪+确定性仲裁)落地,用于评估运行在真实闭源商业 App上的 GUI Agent。保留 GRADE 手工三层准则能力,同时解决GUI Agent生产环境的两个现实约束:常无截图(隐私降级)、常无人工准则(流量无法全量标注)。
评估引擎在两个正交维度上工作。先把它们区分清楚,后续所有模块行为都由此决定。
1.1 数据维度:截图可用性
GUI Agent 轨迹中文本信号始终存在(instruction、afly _tree、action、thought、span),截图是可选叠加项。
判定规则:轨迹中每一步都有截图一 Full;否则(含部分缺图)一Text-only。无 Hybrid 态。
下游不感知:所有模块(证据层、裁决层、诊断层)消费统一接口,判官后端在模块内部路由切换,不暴露到外部。
1.2 规则维度:准则可用性
与截图无关。指任务是否有可用的评估准则,四级优先取最高级
| 级 | 供给方式 | 来源 | 可靠性 |
|---|---|---|---|
| G0 | 手工三层准则 | 人工编写 obligations / quality / negatives | 最高 |
| G1 | essential-state 模板 | 人工预制里程碑 matcher | 高 |
| G2 | 自动派生 rubric | LLM 从 instruction 生成 | 中(降权) |
| G3 | progressive judge | LLM 逐步判"是否推进任务" | 兜底 |
G0/G1 需人工投入;G2/G3纯自动。GO完全兼容 GRADE 原生用法—一这是方案对 AndroidDaily 的对齐保
证。
1.3 Rule Verifier 独立于两个维度
Rule-based 启发式(卡死/环路/漂移等)不依赖截图、不依赖准则,每条轨迹都跑,结果作为先验喂给后官。详见 §3.4。
flowchart TB
OTel["GUI Agent OTel 轨迹<br/>instruction · a11y_tree · action · thought · span · token<br/>screenshot_raw / screenshot_som(可选)"]
subgraph M1["① 轨迹装配 TrajectoryAssembler"]
A1["归一化标准 step 序列<br/>探测截图可用性 → Full / Text-only"]
end
subgraph M2["② 准则供给 GuidelineProvider"]
G["G0 手工三层准则<br/>G1 essential-state 模板<br/>G2 自动派生 rubric<br/>G3 progressive(无准则)"]
end
subgraph M3["③ 证据装配 EvidenceBuilder"]
E["多源装配 · text-only 优先 · 有图叠加 SoM<br/>增量追踪 TraceEvidence / UpdateMemory<br/>→ EvidenceBundle B"]
end
subgraph RV["旁路:Rule Verifier(每条轨迹必跑)"]
R["卡死 · 环路 · 漂移 · 输入残留 · 短路<br/>→ rule_hits"]
end
subgraph M4["④ 裁决 Verdict Engine"]
V["G0/G1/G2: 三维检查 obligations / quality / negatives → 仲裁<br/>G3: 逐步 progressive 推进判定 → 汇总"]
end
DEC{"verdict == success ?"}
subgraph M5["⑤ 诊断 DiagnosisWriter(仅非 success 触发)"]
D["Probe: failure_category · critical_step · reason · citations"]
end
STORE[("GUI Agent_eval_result 存储<br/>keyed by trace_id")]
BENCH["低置信 → 人工复核 → GUI Agent-JudgeBench"]
OTel --> M1
M1 --> M2
M1 --> M3
M1 --> RV
R -. rule_hits .-> E
R -. rule_hits .-> V
M2 --> M4
M3 --> M4
M4 --> DEC
DEC -- 是 --> STORE
DEC -- 否 --> M5
M5 --> STORE
STORE --> BENCH
二、各准则级在管线里的走法
| 准则级 | 走证据层? | 裁决层判定方式 | 备注 |
|---|---|---|---|
| G0 | ✓(结构化) | GRADE 三维检查(obligations 覆盖率 + quality 接地 + negatives 一票否决) | 裁决层最完整路径 |
| G1 | ✓(结构化) | essential-state matcher 命中判定(允许路径多解、时序无关) | 操作义务松弛为里程碑命中 |
| G2 | ✓(结构化) | 同 G0 三维结构,但降权;negatives 只保留可交叉印证项 | 自动派生准则不完全信任 |
| G3 | ✓(简化版) | 逐步问 LLM"是否推进任务",不做精确状态断言;rule_hits 仍作先验 | 仍消费 step 观测序列,但不编译结构化 EvidenceBundle |
G3 为什么还要走证据层:progressive judge 每一步仍需要当前观测(activity、a11y_tree、action、有图时的截图)作为判断依据,这正是 assemble_obs 的职责。只是 G3 不编译结构化的 EvidenceBundle——直接消费 step 观测序列,逐步 LLM 调用后汇总。Rule Verifier 对 G3 同样有效:规则侧检出的环路/漂移可作为 hard fail 直接终结 G3 判定。
三、模块设计
3.1 ① 轨迹装配(TrajectoryAssembler)
输入:OTel span 序列
输出:标准 step 列表 + Full/Text-only 标记
标准 step 结构(判官统一消费形态):
{
"step_id": 5,
"global_task": str,
"activity": str,
"a11y_tree": [{"text":str,"class_name":str,"bounds":BoundingBox,"is_clickable": bool,"is_enabled": bool,"is_scrollable": bool}],
"agent_output": str,
"screenshot": base64,
"signals": [str], //["activity", "a11y_tree", "action", "thought", "screenshot_raw", "screenshot_som"]
"rule_hits": [],
"step_duration_ms": 1840,
"token_usage": 1234
}
{
"step_id": 5,
"global_task": "在小红书搜索杭州旅游攻略,阅读3篇帖子并总结",
"activity": "RedNote",
"a11y_tree": [{"text":str,"class_name":str,"bounds":BoundingBox,"is_clickable": bool,"is_enabled": bool,"is_scrollable": bool}],
"agent_output": "点击确认按钮{"type": "click", "target_id": 17, "text": "确认"}",
"screenshot": base64,
"signals": ["activity", "a11y_tree", "agent_output", "screenshot"],
"rule_hits": [],
"step_duration_ms": 1840,
"token_usage": 1234
}
设计要点:
- a11y_tree 单字段:直接存内容本身,不拆 hash_before/after + diff。相邻差异由证据层临时计算。
- activity 派生:从 a11y_tree 根节点解析 package/class,不新增采集字段。
- signals 声明:列明本步可用信号,下游判官据此切换行为,禁止引用未声明字段(防无图时幻觉)。
3.2 ② 准则供给(GuidelineProvider)
输入:task instruction + fingerprint
输出:(guideline_source, guideline) 元组
供给选择器:
def provide_guideline(task):
g = lookup_manual_guideline(task.fingerprint)
if g:
return ("G0", g) # 手工三层准则
tpl = lookup_template(task.fingerprint)
if tpl:
return ("G1", tpl.essential_states) # essential-state 模板
if task.is_structured():
return ("G2", derive_rubric(task)) # LLM 自动派生
return ("G3", None) # progressive 兜底
G0 手工三层准则结构(完全兼容 GRADE,以小红书搜索总结 case 为例):
{
"task_template_id": "xhs.search_read_summarize.v1",
"obligations": [
{"id": "OBL-1", "desc": "在小红书内检索目标关键词", "required_signals": ["a11y_tree", "action"]},
{"id": "OBL-2", "desc": "打开至少 3 篇不同的非广告帖子", "required_signals": ["activity", "a11y_tree"]},
{"id": "OBL-3", "desc": "从所打开帖子中提取信息", "required_signals": ["a11y_tree"]},
{"id": "OBL-4", "desc": "产出一份总结", "required_signals": ["a11y_tree", "action"]}
],
"quality": [
{"id": "QUAL-1", "desc": "总结须涵盖所读 3 篇帖子的核心观点", "anti_hallucination": "禁止引入证据包未出现的通用知识"},
{"id": "QUAL-2", "desc": "总结基于实际阅读内容,非仅凭标题浅层浏览"}
],
"negatives": [
{"id": "NEG-1", "desc": "不得使用带'赞助/广告/推广'标签的帖子", "severity": "veto"},
{"id": "NEG-2", "desc": "阅读帖子不足 3 篇即产出总结", "severity": "veto"},
{"id": "NEG-3", "desc": "跳出小红书完成任务", "severity": "veto"}
]
}
存储:GUI Agent_manual_guideline 表(G0)、GUI Agent_essential_states 表(G1)。
G1 essential-state 模板结构(同一个小红书搜索总结 case 的 G1 版本):
G1 与 G0 的区别:G0 穷举操作义务(搜索、打开、提取、总结四条),G1 只列关键 UI 语义里程碑——判官只判"轨迹是否经过了这些里程碑",不关心具体路径。
{
"task_template_id": "xhs.search_read_summarize.v1",
"instruction_pattern": "在{app}搜索{keyword},阅读{n}篇{filter}帖子并总结",
"essential_states": [
{
"step_hint": 1,
"semantic_desc": "进入小红书并确认首页就绪",
"matcher": {"type": "activity", "pattern": "com.xingin.xhs/.MainActivity"},
"required": true
},
{
"step_hint": 3,
"semantic_desc": "搜索结果已展示(搜索词已输入,结果列表可见)",
"matcher": {"type": "a11y", "pattern": "儿童牙膏", "mode": "fuzzy"},
"required": true
},
{
"step_hint": 6,
"semantic_desc": "至少进入过 3 篇不同帖子的详情页",
"matcher": {"type": "activity", "pattern": "com.xingin.xhs/.NoteDetailActivity"},
"required": true,
"min_hits": 3,
"dedup_by": "note_id_param"
},
{
"step_hint": -1,
"semantic_desc": "轨迹末态存在 Agent 生成的总结文本",
"matcher": {"type": "text", "pattern": "总结|推荐|观点|建议", "scope": "last_3_steps"},
"required": true
}
]
}
matcher 字段说明:
type:activity(匹配 Activity 名)/a11y(匹配 a11y_tree 节点文本)/text(匹配扁平化文本)mode:exact(精确匹配)/fuzzy(子串 + 模糊匹配,默认)min_hits: 该里程碑需在轨迹中命中的最少次数(默认 1)dedup_by: 去重策略(如note_id_param表示按 URL 参数中的 note_id 去重)scope: 限定匹配范围(如last_3_steps只在末 3 步中查找)
裁决层 G1 判定逻辑:
def check_essential_states(B, essential_states):
hits = []
for es in essential_states:
matched_steps = [s for s in B.steps if es.matcher.hit(s)]
# 按 dedup_by 去重后计数
deduped = (
dedup(matched_steps, key=es.dedup_by) if es.dedup_by else matched_steps
)
if len(deduped) >= (es.min_hits or 1):
hits.append(es)
coverage = len(hits) / len(essential_states)
all_required_hit = all(es in hits for es in essential_states if es.required)
return {
"coverage": f"{len(hits)}/{len(essential_states)}",
"passed": all_required_hit,
"hit_steps": [s.step_id for es in hits for s in matched_steps],
}
与 G0 的 v_obl 相比:G0 逐条义务判覆盖率(路径灵活但义务明确),G1 只判里程碑是否被经过(更松弛,适合无法穷举义务的场景)。G1 不检查 quality / negatives——这些维度留给 G0 使用。
3.3 ③ 证据装配(EvidenceBuilder)
输入:标准轨迹 + 准则 + Rule Verifier 先验
输出:EvidenceBundle B(G0/G1/G2 走结构化;G3 走简化 step 观测序列)
保留 GRADE 增量状态追踪内核:
def build_evidence_bundle(traj, guideline, mode):
M, P = {}, []
rule_hits = RuleVerifier.scan(traj) # 旁路先跑(§3.4)
for step in traj.steps:
obs = assemble_obs(step, mode) # text-only: activity+a11y+action+thought
# full: + screenshot_raw + screenshot_som
p_t = trace_evidence(traj.instruction, guideline, obs, step.rule_hits)
M = update_memory(M, p_t) # 关键:跨步状态保持,防瞬态信息丢失
P.append(p_t)
return EvidenceBundle(
instruction=traj.instruction,
guideline=guideline,
memory=M,
evidence=P,
rule_hits=rule_hits,
mode=mode,
)
三个必须保留的 GRADE 机制:
- TraceEvidence 只提原子事实——"点击了搜索结果第 2 项"而非"屏幕上有个列表"
- UpdateMemory 防瞬态丢失——step5 的价格在 step6 跳页后仍绑定在 memory
- Filtering 噪声过滤——忽略无效滑动、加载动画、误触回退
有图 vs 无图的处理在 assemble_obs 一处完成:text-only 态跳过 screenshot 字段;full 态把原始截图和 SoM 截图都装入 obs。证据层以上所有模块不感知这个差异。
3.4 旁路:Rule Verifier
每条轨迹都跑,与截图可用性、准则可用性均无关。 结果作为先验同时喂给证据层和裁决层。
| 启发式 | 计算(基于 a11y_tree / activity / action) | 触发结论 |
|---|---|---|
| 卡死检测 | 连续 3 步 a11y_tree 一致且 action 无变化 | failed,execution |
| 冗余环路 | (activity, action_type, target) 重复 ≥3 | failed,execution |
| 意图漂移 | activity 切出目标 App 且 5 步未回归 | reflection 扣分;未回归则 failed |
| 输入残留 | a11y 焦点在 EditText、输入非空但无 submit | execution 扣分 |
| 短路检测 | essential-state 命中路径 / 实际步数 < 0.5 | 效率扣分 |
| 冷启惩罚 | 单步 latency > 阈值且非首次冷启 | 效率扣分(不影响 verdict) |
3.5 ④ 裁决(Verdict Engine)
输入:EvidenceBundle + guideline + rule_hits
输出:{verdict, dimensions, reason, citations, confidence, guideline_source, judge_backend}
按准则级分两条路径:
路径 A:G0 / G1 / G2 → GRADE 三维检查 + 确定性仲裁
v_obl = check_obligations(B, guideline.obligations) # 覆盖率匹配,允许路径多解
v_qual = check_output_quality(B, guideline.quality) # 接地验证:内容必须源于 B,禁幻觉
v_neg = check_negative(B, guideline.negatives) # 违规检测,最高优先级
def arbitrate(v_obl, v_qual, v_neg):
if v_neg.violated:
return "risky" if (v_obl.passed and v_qual.passed) else "failed"
if v_obl.passed and v_qual.passed:
return "success"
if v_obl.coverage > 0:
return "partial"
return "failed"
G1 的 v_obl 变为 essential-state matcher 命中判定(允许时序无关)。
G2 的仲裁降权:negatives 只保留可从 a11y 文案交叉印证的项。
路径 B:G3 → Progressive 逐步判定
G3 适用于开放式长任务(如"帮我整理一下今天的工作进展"),这类任务无法用操作义务或里程碑穷举。核心思路:把 goal 拆成 sub-goal,每步问 LLM 这步是否在推进某个 sub-goal,汇总后判成败。
Step 1 · Goal 分解(一次性 LLM 调用):
sub_goals = decompose_goal(traj.instruction)
# 输入: 用户指令
# 输出: [{"id": "SG-1", "desc": "打开目标 App", "status": "pending"},
# {"id": "SG-2", "desc": "定位工作内容相关信息", "status": "pending"},
# {"id": "SG-3", "desc": "整理并输出总结", "status": "pending"}]
Step 2 · 逐步推进判定(每步一次 LLM 调用,轻量 prompt):
progress = []
for i, step in enumerate(traj.steps):
obs = assemble_obs(step, mode) # 复用证据层的观测装配
# 判官只看当前步观测 + sub-goal 列表 + 前序进展摘要
p = progressive_judge(obs, sub_goals, progress[-3:], rule_hits)
progress.append(p)
# 早期终止:规则检出环路/漂移 → 直接 hard fail
if "loop" in step.rule_hits or "drift_unrecovered" in step.rule_hits:
return {
"verdict": "failed",
"critical_step": i,
"reason": f"Rule 检出 {step.rule_hits[-1]},G3 提前终止",
}
progressive_judge 的 prompt 骨架(轻量、聚焦):
# 当前步观测
activity: {obs.activity}
a11y_tree: {obs.a11y_tree} (截断至 2000 字符)
action: {obs.action}
thought: {obs.thought}
rule_hits: {obs.rule_hits}
# 任务 sub-goals
{sub_goals 列表及当前状态}
# 最近 3 步进展摘要
{progress[-3:] 的 summary}
# 输出 JSON(轻量)
{
"advances_subgoal": "SG-1" | null, // 本步推进了哪个 sub-goal
"action_quality": "productive" | "neutral" | "regressive" | "stuck",
"note": "一句话说明本步做了什么"
}
Step 3 · 汇总判定:
def aggregate(progress, sub_goals):
# 统计每个 sub-goal 是否至少被推进过一次
sg_advanced = {sg["id"]: False for sg in sub_goals}
for p in progress:
if p["advances_subgoal"]:
sg_advanced[p["advances_subgoal"]] = True
# 统计 action_quality 分布
qualities = [p["action_quality"] for p in progress]
stuck_count = qualities.count("stuck")
regressive_count = qualities.count("regressive")
# 判定逻辑
all_advanced = all(sg_advanced.values())
if all_advanced and regressive_count == 0:
return "success"
if stuck_count >= 3 or regressive_count >= 2:
return "failed"
# 找第一次停滞/退步的 step 作为 critical_step
critical = next(
(
i
for i, p in enumerate(progress)
if p["action_quality"] in ("stuck", "regressive")
),
None,
)
if sum(sg_advanced.values()) / len(sg_advanced) >= 0.5:
return "partial"
return "failed"
G3 与 Rule Verifier 的协作:Rule Verifier 的 rule_hits 在 G3 中有双重作用:(1) 作为 progressive_judge prompt 的输入,让 LLM 知道规则侧已检出问题;(2) 作为 early termination 条件——检出环路或漂移不可恢复时,G3 不再继续逐步判定,直接以 failed 终结。
G3 的成本控制:每步一次 LLM 调用看似昂贵,但 prompt 极轻量(仅当前步 + sub-goal 列表 + 最近 3 步摘要,无全量轨迹回放),单次约 2000-3000 token。加上规则侧 early termination,实际绝大多数轨迹在 10-15 步内终结。
3.6 ⑤ 诊断(DiagnosisWriter)
仅 verdict ≠ success 时触发。Probe Agent 定位 critical_step + 7 类根因分类:
perception | grounding | planning | execution | reflection | safety | env
输出 failure_category + critical_step + reason + citations,写入 GUI Agent_eval_result。
四、判官输出契约
所有层、所有准则级共用同一 JSON 契约:
{
"layer": "L3",
"verdict": "success | partial | failed | risky",
"overall_score": 0.0,
"dimensions": {
"task_completion": {"score": 0.0, "note": ""},
"step_correctness": {"score": 0.0, "note": ""},
"efficiency": {"score": 0.0, "note": ""},
"planning_quality": {"score": 0.0, "note": ""},
"safety_side_effect": {"score": 0.0, "note": ""},
"recovery_robustness":{"score": 0.0, "note": ""}
},
"obligations": {"coverage": "3/4", "passed": false},
"quality": {"passed": true},
"constraints": {"passed": true, "violated": null},
"failure_category": ["perception|grounding|planning|execution|reflection|safety|env"] | null,
"critical_step": 7,
"reason": "自然语言诊断,每句引用具体 step_id 与证据字段名",
"citations": [3, 5, 7],
"evidence": [{"step": 7, "signal": "a11y|activity|screenshot|rule", "value": ""}],
"evidence_signals": ["a11y_tree", "action", "rule_hits"],
"confidence": 0.86,
"guideline_source": "G0 | G1 | G2 | G3",
"judge_backend": "text-llm | mllm"
}
关键约束:
evidence内不允许引用未在evidence_signals中声明的字段——text-only 态审计抓手,杜绝无图时臆造视觉线索critical_step仅verdict ≠ success时填,指向第一次不可挽回的偏离步reason每句必须落到 step_id + 具体证据字段名,禁止泛泛表述rule_hits非空时 evidence 必须至少引用一条 rule
五、判官部署
5.1 两态路由
| 态 | 主判官 | 场景 |
|---|---|---|
| Full | MLLM Judge(Qwen-VL-Max / GPT-4o) | 离线评测、模型对比 |
| Text-only | Text-LLM Judge(Qwen-Max / DeepSeek 级) | 生产隐私模式(主战场) |
5.2 判官 Prompt 骨架
同一份 prompt,通过 signals 自适应有图 / 无图:
你是一位 Android Mobile GUI Agent 评审员。以下是一条完整轨迹。
# 任务
用户指令:<instruction>
准则来源:<G0/G1/G2/G3>
可选 三层准则 = {obligations, quality, negatives}
可选 essential_states = [{semantic_desc, matcher}, ...]
# 观测(按步,signals 声明可用信号)
[step 0]
activity: com.x/Detail
a11y_tree: <当前界面无障碍树文本>
action: click(target_id=17, text="确认")
thought(opt): "点击确认"
screenshot_raw / screenshot_som(if available): <ref | omitted>
rule_hits: []
[step 1] ...
# 输出:严格 JSON(契约),不要其它文字
# 评审准则
1. G0/G2: 三维分别判——obligations 覆盖率 / quality 接地 / negatives 一票否决
2. G1: essential-state matcher 逐个判命中(allowed 多解、时序无关)
3. G3: 拆 sub-goal,逐步判"是否推进任务"
4. risky 优先级最高:不可逆副作用即使达成任务也标 risky
5. 无图(signals 缺 screenshot):只用 activity + a11y_tree,禁臆造视觉线索
6. 有图:SoM 已编号可交互元素,判断 target_id 对应编号即可(不做 IoU)
7. reason 每句落到 step_id + 证据字段名;rule_hits 非空时必引用
8. critical_step 仅 verdict≠success 时填