GUI Agent 这个赛道最近非常热闹但你只要真的在项目里跑过一版大概率会遇到同一个让人头疼的场景模型明明已经理解了页面的结构却在最后一步自信地点击了旁边的广告横幅或者弹窗一变就彻底不知所措。你把它当段子截图发到工作群里但笑声背后是一个非常现实的问题——GUI Agent 的失败率远比你想象中高。这篇文章想聊的不是“怎么把准确率再往上调一档”而是换一个思路如果失败本身可以被结构化地保存下来并且在下一次遇到相似页面时自动派上用场呢EvoSkill-GUI 就是这类思路的一个代表它的核心理念很简单把一次误点、一次定位失败变成能复用的技能。适合正在做 Agent、自动化测试、或者 RPA 相关工作的朋友参考因为这套“失败转技能”的框架并不绑定具体模型你可以把它接到自己的 Agent 上。1. GUI Agent 为什么总是“点错”1.1 每个环节都有自己的“坑”GUI Agent 的工作链路一般可以拆成三步感知当前界面、决策下一步操作、执行这个操作。感知通常靠截图加视觉模型或者 DOM 解析决策由大模型根据指令和界面状态生成执行则通过浏览器调试协议、系统自动化接口或者直接发送模拟鼠标事件完成。听起来挺顺畅但每一步都有各自的坑。感知层面的坑最常见。同一个文字出现在页面多个地方比如“更多”按钮在顶部和底部各有一个元素状态跟截图不同步明明已经加载完却还显示 loading弹窗、气泡、遮罩层把真正的按钮盖住视觉模型却毫无察觉。这些属于“看错了”。决策层面的坑更加隐蔽。模型拿到了正确的界面信息但指令本身有歧义比如用户说“删除”页面上有“删除文件”和“删除账户”两个入口或者页面有多个符合描述的候选按钮模型选错了。这个层面是“想错了”。执行层面的坑则很现实点击事件的坐标被浏览器缩放偏移、元素在点击前的一瞬间被异步渲染替换、权限弹窗中断了流程。这类错误经常被归类为“手滑”但它发生的频率一点都不低。如果你把一次完整任务的日志拉出来看常常会发现真正的失败不是某个单一环节的问题而是多个环节叠加后的结果。模型看到了错误的位置生成了错误的点到坐标点击又因为页面状态变化而落空。这种复合型失败如果只从单个环节去调参几乎无法根除。这也是传统“修 Bug”思路在 Agent 场景里不太奏效的原因。1.2 失败其实是唯一的“线下老师”多数团队在开发 Agent 时会把注意力放在成功轨迹上跑通了保存跑了三次不通最多看一眼报错然后把失败样本丢进垃圾桶。问题在于失败轨迹里包含着三个最有价值的信息当时看到了什么、打算做什么、实际发生了什么。举个例子你的 Agent 在填写一个表单时第一次点击“提交”后页面弹出了一个“请先同意用户协议”的红字提示第二次点击却没有弹窗直接提交成功。如果只保留成功轨迹你学到的是“点击提交”如果同时保留失败轨迹你学到的是“当页面有未处理的悬浮提示时直接点击提交会被拦截要先处理提示区”。后者才是一条真正可迁移的经验。EvoSkill-GUI 选择把失败轨迹当成主要的数据来源本质上是把“错误”从负面资产翻转为正面资产。它不追求一次性把成功率调到 99%而是允许 Agent 犯错但要求每次犯错后都能沉淀一条可以被再次使用的经验。这个思路很像人类的学习方式吃过一次亏下次就绕开如果绕不开就想想为什么。这种“线下老师”的价值在自动化任务里往往比在聊天任务里更明显因为 GUI 操作本质上是确定性的同一个页面结构下一个可复用的技能可以反复被触发。2. EvoSkill-GUI 的核心思路把失败封装成可复用技能2.1 技能是什么三段式封装传统方案里所谓“学习”通常意味着微调模型代价高、周期长而且效果不可控。EvoSkill-GUI 的做法是绕开大模型训练直接定义一种中间表示叫“技能”。这里的技能不是代码函数也不完全是提示模板而是一个三段式封装触发条件什么状态下这个技能适用比如“页面上出现‘请先登录’弹出框”动作策略在这种状态下应该怎么操作比如“点击弹出框中的‘马上登录’等待页面跳转后再继续原任务”校验结果执行动作后怎么判断是否成功比如“登录框消失且 URL 变化”。这看起来很像测试用例里的“前置、步骤、预期”但区别在于技能是供 Agent 在推理时动态检索和组合的不是固定执行序列。它刻意把触发条件写成半语义化、半结构化的形式方便做向量检索同时也方便人读。为什么采用三段式因为失败轨迹通常具有“状态依赖”的特征。你点错了往往是在某个特定界面状态下点错换个界面同样的点法可能就对了。如果不封装触发条件只存操作序列技能之间会互相污染。这就好比一个人知道“红灯要停”但你不告诉他“其他方向是绿灯你也可以走”这个知识就是残缺的。2.2 演化机制技能库不是静态“词典”“Evo”这个词是 EvoSkill-GUI 的核心。技能库如果只是简单地把失败样本堆在那里那叫日志中心不叫技能库。真正的难点在于让技能随着新失败的发生而自动演化。演化机制可以拆成四类动作新增、合并、分裂、淘汰。新增最容易理解遇到一个完全没见过的新失败模式归因后提取成新技能。合并发生在两个技能在触发条件上出现高比例重叠动作策略也基本一致之时这时候应该合并成一个更通用的技能否则技能库会迅速膨胀。分裂则相反一个技能覆盖的场景太宽导致在某个子场景下频繁失效那就需要把触发条件细分拆成两个更精准的技能。淘汰最根本一个技能在连续多次被检索到并执行后仍然失败率偏高就应该降低权重或者直接移除。这四类动作不是纯靠规则而是靠一个评分机制驱动的。每个技能保存了使用次数、成功次数、最近成功时间、失败原因类别。每次 Agent 执行任务时都会反馈这次是否使用了某个技能、使用后的结果如何。系统定期对这些技能做一次像垃圾回收一样的清理把低频、失败率高、触发条件模糊的技能处理掉。这里的关键设计是技能不是写死的而是带生命周期的。因为 GUI 环境本身在变网页改版、按钮移动、弹窗样式调整一个曾经有效的技能几个月后可能就失效了。如果没有演化机制技能库就只有“越攒越乱”和“越攒越死”两条路。EvoSkill-GUI 试图做到不用每次改版都人工去删技能而是让技能随着真实反馈自我修正。3. 手把手搭建一个失败转技能的闭环3.1 第一步定义统一的失败轨迹格式无论你用的是什么 Agent 框架要想让失败能被复用第一步都是把轨迹“标准化”。我建议你把一次交互过程定义成一个 JSON 结构至少包含下面这些字段。{ task_id: task_20250613_001, task_desc: 在后台管理系统中创建一个营销活动, steps: [ { step_id: 1, observation: { screenshot: path/to/screen.png, dom_snapshot: path/to/dom.json, accessibility_tree: path/to/a11y.json, visible_texts: [创建活动, 营销中心, 昨日消耗 1200] }, action: { type: click, target: visible_text:创建活动, coordinate: [840, 350] }, feedback: { success: false, error: element_not_found, page_changed: false } } ], final_result: failed, failure_reason: 按钮被折叠菜单遮挡视觉模型误判为可见 }这个格式最核心的部分是 observation 里的 visible_texts 和 action 里的 target。不要把坐标当成唯一的动作目标因为坐标不具备可迁移性。页面变了、分辨率变了坐标就废了。要把 target 写成“visible_text:创建活动”或“semantic:页面右上角的关闭按钮”这种语义化形式后续技能抽取才能泛化。统一轨迹格式的意义在于让“失败”可以批量处理。如果没有统一格式每个环节各存一份 log归因时就得手工对照截图和日志效率极低。有了标准化轨迹你就能写脚本批量跑归因。3.2 第二步自动归因区分“该学的失败”和“不该学的失败”不是所有失败都值得沉淀成技能。失败的归因其实要回答三个问题哪个环节失败了失败的根本原因是什么这个原因是否有普适性我建议用一个三层的归因逻辑。第一层检查执行反馈。如果 action 执行后页面没有变化而且报错信息是 element_not_found那大概率是感知或决策阶段的定位错误。如果执行后页面变了但变成了错误页面那就属于动作策略错误。第二层用大模型辅助分析。把轨迹的 JSON 喂给 LLM让它输出一个结构化的失败原因。你可以用类似下面这样的提示词模板你是一个 GUI Agent 调试专家。给你一段失败轨迹请判断失败原因。 要求输出 JSON 格式 { failed_stage: perception | decision | execution, root_cause: 用一句话描述根本原因, suggested_skill: 如果这次失败可以泛化为一个技能请写出触发条件、动作策略、校验结果否则返回 null } 轨迹数据 {轨迹 JSON}这里有个技巧不要让大模型自由发挥一定要限死输出格式。否则你会得到各种散文风格的归因结果后续根本没法处理。这一步会把原始失败整理成“技能候选”但还不能直接入库因为可能会出现过拟合或误归因。第三层做一次统计去重。把连续多条失败轨迹的 root_cause 做向量相似度聚类。如果 10 条失败都是“弹窗遮住了目标元素”那就只需要生成一个通用技能而不是 10 个相似的技能。如果 10 条失败虽然原因相同但出现在完全不同的页面场景里那这个技能就需要更强的触发条件设定。归因的核心目标是区分“该学的失败”和“不该学的失败”。像登录态过期、网络抖动这类环境性失败不应该变成技能而像“弹窗遮挡”“页面歧义选择”“按钮状态变更”这类确定性失败才值得沉淀。用上面的三层过滤基本能把噪音剔掉。3.3 第三步从归因结果生成技能模板归因完成之后系统会得到一批技能候选。接下来要做的就是把概括性的文本转成结构化的技能。这一步我建议用一个独立的“技能生成器”而不是让归因步骤顺带完成。技能生成器接收归因结果输出这样一段结构{ skill_id: skill_popup_block_001, name: 处理弹窗遮挡目标元素, trigger: { type: state_match, description: 页面存在可见弹窗或遮罩层且目标元素被遮挡或不可点击 }, action: { plan: [ 优先识别弹窗的关闭按钮并点击, 如果无关闭按钮点击遮罩层外部区域尝试关闭, 等待 500ms 后重新定位目标元素并执行原操作 ], expects: 弹窗消失目标元素变为可见或可点击 }, verification: { type: post_condition, check: 目标元素在 accessibility_tree 中可见且 enabled }, source_failures: [task_20250613_001, task_20250613_007], confidence: 0.87 }注意 action 里的操作尽量写成一到三步的短指令不要展开成一个小程序。因为技能最终会被注入给大模型作为提示文本太长的大模型反而利用不了。你要是写成五步以外的操作检索和执行的效率都会下降。为了让技能具备泛化能力你还需要在生成时做“变量槽位替换”。例如失败轨迹里具体的“关闭按钮”在另一个页面叫“我知道了”按钮你就不能写死而应该把按钮文本抽象成“关闭弹窗的显式按钮”。这个替换可以用大模型辅助也可以让技能生成器基于可见文本的类别按钮、链接、图标做规则化处理。3.4 第四步检索注入与使用策略技能入库后最重要的就是 Agent 怎么用这个技能。我推荐的做法是在 Agent 做每一步决策之前把当前界面的观察状态编码成 query用向量检索找到最匹配的技能然后把技能的内容作为上下文注入到给大模型的 prompt 里。这里的伪代码逻辑类似于这样def decide_action(observation, task, agent, skill_store): # 1. 把当前界面状态序列化为向量 query_vector encode_observation(observation) # 2. 从技能库中检索最相关的技能 candidates skill_store.search(query_vector, top_k5) # 3. 构建带技能上下文的 prompt context_prompt for skill in candidates: if skill.trigger_matches(observation): context_prompt f[可复用技能] {skill.name}\n context_prompt f触发条件: {skill.trigger.description}\n context_prompt f操作策略: {skill.action.plan}\n context_prompt f校验方式: {skill.verification.check}\n\n # 4. 调用 Agent 的决策模型 action agent.act(observation, task, context_prompt) return action关键点是“触发条件匹配”和“技能自身置信度”要双重判断。只做向量相似度检索很容易把“看起来有点像”的技能误触发所以技能本身的 trigger_matches 必须再做一个规则或模型校验确认当前页面确实符合技能的使用前提。使用策略上我建议“先建议后干预”。第一版不需要强制 Agent 执行技能而是把技能作为上下文提示的一部分让模型自己决定是否采纳。等技能库足够成熟再设置高置信度技能的自动执行阈值。这样可以在前期避免技能误伤任务流程。3.5 技术选型参考技能库的存储和检索我推荐用向量数据库因为你的检索 input 是“界面状态描述”本身就是非结构化文本。传统 SQL 的匹配能力太弱。常用的工具可以用 Qdrant、Milvus 或 Chroma。如果你只想快速验证Chroma 最轻量跑在本地就行。轨迹数据的采集层要看你接的 Agent 是什么环境。浏览器端可以用 Playwright 的 trace 功能能拿到 DOM 快照、截图和动作序列桌面端可以用 UI Automation 框架自带的日志移动端需要额外接 adb 的窗口层级 dump。无论哪种端都要把数据落成统一 JSON 格式不要自己造多个格式。大模型辅助归因和技能生成这一层建议直接调用现成的 LLM API。归因和生成都很吃模型的理解能力但不需要毫秒级响应用中低延迟模型就够。如果你公司有成本压力也可以选开源模型部署在本地 GPU 上只是精度会略低。最后是评估模块。一定要单独记录每个技能被使用了多少次、成功了多少次。这里我会用一个简单的 Google Sheet 或者线下数据库的记录表不需要上线什么大系统。技能数据是稀疏的一个刚入库的技能可能一周才被使用一次所以需要积累。4. 常见问题与排查技巧实录4.1 技能库越来越乱检索结果不可用怎么办这是最容易遇到的问题。技能一旦超过一两百条向量检索的精度会明显下降经常出现“检索到的技能不知道在说什么”的情况。问题的根源不是向量库不行而是技能之间的区分度不够。排查技巧是定期做“技能归并”。每个月跑一次聚类把触发条件相似度超过 90% 的技能标记出来。然后人工或让大模型决定是合并还是保留差异。我见过很多团队的技能库里面其实有大量互相重复泛化的内容归并后体积能减少 40% 左右。另一个技巧是给技能增加“负面标记”也就是明确写出本技能不适用的场景。比如“本技能适用于普通弹窗不适用于强制升级弹窗”。这个负面场景可以在技能被误触发时自动记录每次误触发都把这个场景写入负面标记列表。这样检索时如果当前界面状态和负面标记相似就可以排除这条技能。4.2 技能会误伤正常场景怎么控制回退技能注入到 prompt 后模型可能明明可以正常完成某个操作却因为检索到了一个“看起来很相似”的技能走了偏路。比如页面上有弹窗但弹窗并没有阻挡点击目标模型却先点了弹窗关闭按钮结果反而点错。这个问题我建议用两层策略控制。第一层是技能触发的门槛不是所有检索到的技能都能进上下文只允许置信度超过某个阈值比如 0.85的技能进入。第二层是 Agent 执行完后反馈判断如果执行结果异常会立即把这次任务的技能使用记录标记为失败下次自动降低权重。更稳妥的方案是“影子模式”。在一段时间里技能检索照常进行大模型仍按自己的方式决策系统把技能建议和模型最终决策记录成对比日志。这个过程跑三五天你就能看到哪些技能的建议跟 final action 是一致的哪些是跑偏的。等筛选出高一致性技能后再正式开启技能注入。4.3 从失败里学到了“反模式”越学越笨怎么办有些失败虽然确实是失败但根本原因其实是用户指令本身就不明确或者是页面环境本身有问题。如果把这些也沉淀成技能Agent 就相当于学会了在没有指令信息时硬猜猜错了再学一个更偏的技能形成恶性循环。解决办法是给技能生成设置一个“必须包含预期结果”的硬约束。如果一个技能无法表达“操作之后应该观察到什么”就不允许入库。这个约束看着简单但能过滤掉大量因果不明的失败。因为在 GUI 环境里因果不明意味着你的归因没做透那这个技能大概率是带毒的。另外我强烈建议给技能库设一个人工审核开关。前 50 条技能入库时必须有人眼确认。等技能生成器的准确率稳定了再打开自动入库。不要省略这一步因为失败归因天然偏向于“从结果反推原因”而结果并不总是能可靠地指向原因。4.4 怎么评估技能的真实价值而不只是看数量技能库的价值很难用“存了多少条技能”来衡量。我看到的经验是真正有价值的技能往往是那种“能结束一个反复出现的失败模式”的技能而不是那些单次挽救了一条任务的技能。一个简单可用的评估指标是“技能有效使用率”等于某个技能在所有被注入场景中导致任务最终成功的次数除以该技能被注入的总次数。如果这个指标低于 20%那这条技能基本就是噪音。反过来如果某类技能的有效使用率稳定在 60% 以上可以考虑把它提升为 Agent 的默认策略。另一个指标是“失败模式覆盖率”。把所有失败轨迹按照归因出来的 root_cause 做分类然后看每条根因是否已有对应的技能。理想状态下前 20 个高频失败根因应该覆盖 80% 以上的技能。如果你发现库里 200 条技能覆盖的根因高度集中于某一个模式那说明你的归因和泛化都太窄了技能库并没有真正帮助解决多样性问题。4.5 追踪一条失败到技能转化的关键经验在落地这个框架的过程中我最大的体会是“失败归因的速度决定了整个系统的下限”。如果你归因慢技能生成就慢技能库的时效性就会变差。网页改版很快今天沉淀的技能可能下周就过时了。所以你要让归因流程尽量自动化尽量在任务失败的几分钟内自动完成归因和入库不是攒到月底再集中处理。还有一个实操细节截图一定不能省。技能库里的 trigger 描述有时写得再好最终你还是需要肉眼确认当时的界面长什么样。每次入库时把轨迹里关键节点的截图一并存储哪怕只是缩略图都能在后期排查技能冲突时省下大量时间。我个人在实际项目里的习惯是让技能库的每个技能都带一个“最近一次成功使用时间”和“最近一次失败使用时间”。这两个时间字段帮助很大。当你看到某个技能长期没有被使用却突然又开始出现时大概率是又有新页面触发了同类问题。这往往能帮你识别出新的失败模式比整天看成功率曲线更有启发。这个内容后续还可以继续扩展比如把技能库的检索与 Agent 的规划模块做深度集成或者针对不同端Web、桌面、移动分别建立独立的技能库。只要你先把“失败转技能”的闭环跑通后续的收益会非常明显。