news 2026/9/26 15:07:34

让GUI Agent从失败中学习:EvoSkill-GUI技能库构建指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
让GUI Agent从失败中学习:EvoSkill-GUI技能库构建指南

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_k=5) # 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、桌面、移动)分别建立独立的技能库。只要你先把“失败转技能”的闭环跑通,后续的收益会非常明显。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 15:07:09

5G组网仿真避坑指南:Option3X与Option2配置实战

简介:这份资源面向职业院校5G组网与运维赛项的备赛师生,以及希望系统掌握5G网络建设流程的通信运维学习者,围绕NSA与SA两种组网架构的部署与优化展开。内容立足3GPP R15标准,结合IUV-5G全网部署与优化教学仿真系统,覆盖…

作者头像 李华
网站建设 2026/9/26 15:07:04

接入 Opus 5 API 前先踩平这几个坑:TaoToken 实操配置与排错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 15:06:59

IP端口连通性四层验证:从ping到应用探活的工程实践

1. 这不是“连不连得上”的问题,而是“连得准不准、判得稳不稳”的工程实践 在运维现场、开发联调、安全巡检甚至日常排查中,“判断IP和端口是否可用”这九个字,几乎每天都要被敲进命令行几十次。但很多人没意识到: 它根本不是一…

作者头像 李华
网站建设 2026/9/26 15:06:52

ESP32 -O2崩溃根源与LAN8720驱动修复指南

1. 这不是编译器“发疯”,是ESP32在用崩溃告诉你:-O2不是万能钥匙你写完一段驱动代码,用-debug编译跑得稳如老狗,连看门狗都懒得喂;一改成-O2,烧进去上电就卡在启动阶段,串口没输出、LED不闪、J…

作者头像 李华
网站建设 2026/9/26 15:05:04

STICA:对象中心世界模型如何提升强化学习决策与泛化

我看一个自动驾驶决策日志的时候,发现一个特别有意思的现象:模型在仿真里已经能稳稳跑完绕障任务,但测试时路边多了一个气球广告牌,车就开始左右摇摆。排查到底层才发现,我把整帧画面直接压成一个特征向量交给了策略网…

作者头像 李华
网站建设 2026/9/26 15:04:55

YOLO定制化目标检测实战:从结构改造到工业部署

1. 这不是“YOLOv11”——先撕掉标题里的认知陷阱,再谈怎么动手 你点开这个标题,第一反应可能是:“YOLOv11?我连v8、v9都还没吃透,怎么突然就跳到v11了?” 别急,这不是乌龙,也不是…

作者头像 李华