移动端自动化测试圈今天又被一张开源项目截图炸开了锅,消息核心就一句话:谷歌放出了一个主打AI视觉与自然语言驱动的开源自动化框架,不少人的第一反应就是“Appium要没了”。我个人的看法没那么极端,但这个东西确实值得所有写过元素定位脚本的测试开发认真看一眼——不管你是刚入行的测试新人,还是已经带团队的测试负责人,看完至少能判断“AI自动化”到底是营销热词,还是真能落地改变工作方式的东西。这篇文章我直接把这类框架的主流设计思路拆开揉碎讲清楚。我会先用一套简化版Demo亲手复现“视觉定位+自动重试”的核心玩法,再结合我这几天的实测结果,聊聊什么时候可以追随这种新思路,什么时候还是老老实实继续用Appium。全程有代码、有参数、有踩坑记录,可以直接照着抄。1. 先搞清楚:AI自动化到底动了Appium的哪块蛋糕1.1 Appium的护城河:稳定和生态Appium火了这么多年,真不是吹出来的。它最大的贡献是把移动端自动化统一到了W3C WebDriver协议上,一套脚本逻辑可以同时跑Android和iOS,社区生态也足够成熟,遇到问题随便一搜就有答案。在很长一段时间里,“Appium+元素定位”就是移动端自动化测试的代名词。但真正长期写过UI自动化的人心里都清楚,Appium最核心、也最让人头疼的东西,是它必须依赖“元素定位符”——resource-id、accessibility id、XPath表达式这些。应用内部只要做一次UI重构,或者测试环境自动化id被混淆掉,那一堆写好的findElement(By.id("xxx"))就全部失效。我在实际项目里见过最夸张的一次,一个回归套件里百分之七十的报错都来自定位符失效,根本不是业务逻辑出了问题。1.2 新框架的底牌:让机器学会“看”界面谷歌这个新工具引起议论的原因,不是它跑得更快,而是它换了一条技术路线:不再试图去抓元素树,而是让机器直接“看”屏幕。具体拆开看,这套思路包含三层能力。第一层是视觉感知,通过截图加机器学习模型识别出界面上的文字、按钮、输入框;第二层是语义理解,把“用账号密码登录”这种自然语言转换成具体的交互动作;第三层是执行与自愈,动作执行失败后重新截图、重新识别,自动调整策略。换句话说,传统自动化关心的是“控件在哪里”,它关心的是“用户想干什么”。这一下就把核心矛盾从“维护定位符”转移到了“维护业务意图”上。UI改版后,控件位置变了,但“登录”这个语义没变,脚本就还能继续跑。这就是为什么评论区很多人喊着“Appium慌了”——因为过去大家维护的重心,被它一把绕过去了。1.3 我的判断:不是取代,而是换赛道倒也不必急着把Appium贬得一文不值。以我实际测试下来的体验,AI视觉自动化在复杂手势、精确坐标校验这类场景下并没有绝对优势,而且模型的引入会带来新的不确定性。真正受到冲击的,其实是“只会在页面上找控件的脚本思维”。所以我的判断很直接:Appium作为项目不会死,但如果你只会写元素定位脚本,不懂怎么构建“意图驱动”的自动化体系,那才是真的危险。这个行业正在从“告诉机器控件在哪”过渡到“告诉机器该干什么”。2. 这类“AI自动化神器”的核心技术拆解2.1 感知层:截图、OCR与控件识别视觉自动化的第一步,是解决“机器怎么看界面”的问题。主流方案是定时截取设备屏幕,然后交给一个识别模块处理。识别模块一般都由两部分组成:OCR文字识别负责找出“登录”“下一步”“同意”这类文本,图像检测模型负责识别按钮、输入框、列表项这些控件区域。实际开发时,这里有个很容易忽略的细节:OCR告诉你的是“文字在哪里”,但可点击的控件往往比文字区域更大。如果直接把文字中心当点击点,有时候会点在文字边缘,触发不了点击事件。稳妥的做法是拿到文字框后适当向外扩展一点,比如上下左右各放宽10到20像素,或者更简单一点,点击文字中心点偏下的位置,因为绝大多数按钮的文本都垂直居中偏上,中心点偏低一些命中率更高。2.2 决策层:从自然语言到动作序列感知层负责看,决策层负责想。现在这类框架普遍采用一个轻量级的“意图解析器”,把自然语言用例解析成结构化的动作指令。举个例子,一条用例写着“点击登录按钮,输入用户名,点击确认”。传统脚本要写成:driver.find_element(By.ID, "login_btn").click() driver.find_element(By.ID, "username").send_keys("tester") driver.find_element(By.ID, "confirm").click()AI框架的逻辑更像这样:parse("点击登录") -> {action: "click", target: {"text": "登录"}} parse("输入用户名") -> {action: "input", target: {"field": "用户名"}, value: "tester"}解析器拿到指令后,再结合当前屏幕的视觉识别结果去执行。你不需要知道登录按钮的id叫什么,只需要告诉系统“这里有个登录按钮”。这一步本质上把测试用例从“代码”变成了“可读的业务描述”,对团队协作也很友好,产品和测试之间沟通成本会低很多。2.3 自愈层:识别失败之后的兜底机制光能看能想还远远不够,真实App里弹窗、加载动画、网络延迟都会让识别结果不稳定。所以这类框架都会内置一个自愈机制,执行动作前先做一次状态判断,失败了就进入重识别逻辑。自愈机制的典型流程是:点击前先截图识别目标文字,如果在超时时间内找不到,就扩展搜索范围检查页面上是否有“同意”“取消”“关闭”这类弹窗按钮;如果有就先点击弹窗关闭它,再重新截图找目标;如果重试多次还是失败,就放弃当前步骤并保存截图作为失败证据。这套逻辑并不复杂,但非常实用。2.4 新旧方案横向对比拿传统Appium方案和AI视觉驱动方案放在一起对比,差异看的很清楚:对比维度传统Appium方案AI视觉驱动方案定位依据元素树、resource-id、XPath截图视觉识别、OCR、语义理解用例编写代码脚本,需要知道控件属性自然语言描述,接近业务场景UI改版影响定位符大概率失效,需逐条修改文本和语义不变则基本不受影响执行稳定性取决于定位符稳定性,相对可控受截图质量、模型识别率影响,需要容错重试调试方式查看元素树、断点调试查看识别结果截图、置信度日志上手门槛熟悉代码和Appium API即可还需要理解视觉模型、OCR调优和重试策略设计这个表里没有绝对的好坏,更多是取舍。传统方案赢在“精确”和“可控”,AI方案赢在“灵活”和“抗变化”。3. 30分钟复刻一个视觉驱动自动化Demo(可直接抄作业)说实话,谷歌那个开源神器的完整项目我还只跑了初步体验,但它的核心设计已经足够清楚了。下面我就用“截图+OCR+ADB命令+重试机制”这套轻量组合,从零搭一个简化版视觉驱动自动化Demo,把它的核心玩法复现出来。3.1 准备环境与整体结构我用的环境是Python 3.10加一个Android模拟器,真机也可以,但需要开启开发者模式。依赖只有两个:opencv-python用于图像处理,requests或其他HTTP库用来调用OCR接口。如果你本地有离线OCR组件更好,没有的话用在线OCR接口也能跑通。整个Demo分三个模块:执行器automator:封装ADB命令,负责截图、点击、滑动、获取设备分辨率识别器vision:接收截图,返回图中所有文本及其坐标框智能体agent:把识别结果和动作组合起来,提供“找到并点击某文本”的核心方法3.2 执行器代码:先把“手”造出来执行器是所有动作落地的根,我先封装常见的ADB操作。这里直接把关键命令写好,你用的时候只需要把设备序列号和截图保存路径改成自己的。import subprocess import os class Automator: def __init__(self, device_id=None): self.device_id = device_id self.width, self.height = self.get_screen_size() def adb(self, args: list) -> str: cmd = ["adb"] if self.device_id: cmd += ["-s", self.device_id] cmd += args return subprocess.check_output(cmd, text=True).strip() def get_screen_size(self): output = self.adb(["shell", "wm", "size"]) # 输出形如: Physical size: 1080x2340 size = output.split(":")[-1].strip().split("x") return int(size[0]), int(size[1]) def screencap(self, path="screen.png"): # exec-out 配合重定向,拿到的是完整无干扰的PNG字节流 with open(path, "wb") as f: proc = subprocess.Popen(["adb"] + (["-s", self.device_id] if self.device_id else []) + ["exec-out", "screencap", "-p"], stdout=f) proc.wait() return path def tap(self, x: int, y: int): return self.adb(["shell", "input", "tap", str(int(x)), str(int(y))]) def swipe(self, x1: int, y1: int, x2: int, y2: int, duration_ms=300): return self.adb(["shell", "input", "swipe", str(int(x1)), str(int(y1)), str(int(x2)), str(int(y2)), str(duration_ms)])有一个小坑必须提醒:截图命令一定用exec-out screencap -p,不要用shell screencap -p,后者在部分设备上会往输出里混入终端控制字符,导致图片损坏。这是我最初跑Demo时图片解码失败了好几次才发现的。3.3 视觉识别代码:给机器装上“眼睛”识别器负责从截图中找到文本和位置。我这里以一个简化的OCR引擎为例,你把它替换成自己手头能用的OCR组件即可,关键是理解返回的数据结构:每个识别结果都包含文本内容、置信度以及左上角坐标和右下角坐标。import cv2 import numpy as np class Vision: def __init__(self, ocr_engine): self.ocr = ocr_engine def find_text(self, image_path, target_text, conf_threshold=0.6): image = cv2.imread(image_path) results = self.ocr.recognize(image) for item in results: text, score, bbox = item if score < conf_threshold: continue if target_text in text: x_min, y_min = bbox[0] x_max, y_max = bbox[2] cx = (x_min + x_max) / 2 cy = (y_min + y_max) / 2 return {"text": text, "score": score, "cx": cx, "cy": cy, "bbox": (x_min, y_min, x_max, y_max)} return None识别结果里的cx和cy就是像素坐标。我通常还会在调用侧做一个坐标校准:如果截图尺寸和设备分辨率不一致,就按比例换算。在模拟器上两者通常一致,但真机上部分厂商系统会做分辨率缩放,不校准的话点击点会整体偏移。3.4 自愈点击逻辑与自然语言驱动核心的点击方法要支持超时重试。我设计的逻辑是:在指定时间内反复截图并查找目标文本,目标没出现就检查页面有没有弹窗按钮,先处理弹窗再继续找,最后再决定是否点击。import time class Agent: def __init__(self, automator: Automator, vision: Vision): self.auto = automator self.vision = vision def _handle_popup(self, image_path): # 常见弹窗关闭词,按优先级尝试 for word in ["同意", "取消", "关闭", "我知道了", "跳过"]: found = self.vision.find_text(image_path, word) if found: self.auto.tap(found["cx"], found["cy"]) time.sleep(1) return True return False def click_text(self, target, timeout=10, interval=0.5, close_popup=True): start = time.time() while time.time() - start < timeout: image_path = self.auto.screencap("temp.png") found = self.vision.find_text(image_path, target) if found: # 略微向下偏移,命中按钮可点击区域 self.auto.tap(found["cx"], found["cy"] + 8) return True if close_popup: self._handle_popup(image_path) time.sleep(interval) return False关于超时和轮询间隔的选取,我实测下来的经验是:给10秒超时、0.5秒轮询一次比较平衡。太短了加载慢一点的页面会误报失败,太长了失败后的等待又会拖慢整条用例执行。真要追求效率,可以改成指数退避策略,前几次间隔短、后几次间隔长,但Demo阶段没必要。自然语言驱动也很简单,写一个极简解析器:def parse_instruction(line: str): line = line.strip() if line.startswith("点击") and line.endswith("按钮"): target = line[2:-2] return {"action": "click", "target": target} if line.startswith("输入"): # 简化处理,实际需要配合输入框识别 return {"action": "input", "target": line[2:]} if line.startswith("滑动"): return {"action": "swipe", "direction": line[2:]} return None在实际项目中,“输入”动作要比这复杂不少,因为需要先点击输入框,再通过ADB输入文本,还要处理输入法弹窗。建议先把“点击”和“滑动”跑通,再逐步扩展“输入”。3.5 用一套真实用例验证效果我拿内部一个记账App的Demo版(简化过)当实验对象,流程定为:打开App→点击“登录”→输入测试账号→点击“确认”→点击“新增一笔”→填写金额→保存。为了对比,我先用Appium传统方式写了一套脚本,再改成视觉驱动方式写了一套,故意在中间让前端同事把登录页的控件id全部改了一遍。对照结果很说明问题:Appium那套脚本登录部分的定位符全部失效,一共改了几处才恢复;视觉那套脚本因为认的是“登录”“确认”这些界面文本,UI改版后一次都没动过。当然,这里面有Demo环境的“理想成分”,生产环境比这复杂得多,但它验证了核心方向:视觉驱动脚本的抗UI变化能力确实是传统方式无法比的。4. 我踩过的那些坑:视觉自动化的典型翻车现场4.1 OCR识别到了,但点击位置偏移这是最容易踩的坑。第一次跑Demo时,OCR正确识别出了“登录”两个字,给出的中心点坐标看起来也对,但点击以后页面没反应。排查后发现两个原因:一是模拟器截图分辨率是1080x2400,而应用内实际可点击区域比文字框高出一块,点文字正中心会落在两个控件之间;二是部分系统UI控件在点击时要求命中它的“热区”,通常比视觉上看到的按钮要大,但中心点如果压在文字和按钮边缘的缝隙就会失效。我的解决办法是拿到OCR文本框后,把点击点从垂直中心往下移8到12像素,同时把水平中心作为基准。另一个更稳的方式是,找多个同义词确认目标区域,比如“登录”和“登 录”之间有空格,识别出来后先做文本归一化再匹配。4.2 弹窗遮挡导致点击失效App启动时的隐私弹窗、权限请求弹窗,几乎是无处不在的。视觉驱动脚本一旦被弹窗挡住,目标按钮的识别结果虽然还能出现(因为OCR是从截图里找文字,弹窗盖不住底下的文字),但真正点击时事件会被弹窗消费掉,脚本表现为“明明找到了却点了没反应”。我一开始只在找不到目标时才处理弹窗,后来发现这种场景更多是“找得到但点不中”。现在的策略是在每次点击前先快速扫一遍常见的弹窗关闭词,不管目标是否找到,只要发现有弹窗存在就优先处理。代价是每次点击前多花零点几秒,但换来的是稳定性大幅提升。4.3 自愈逻辑误判“成功”自愈逻辑最危险的误判,是页面其实没有切换成功,但旧页面的文本仍然显示,导致识别器给出“目标已找到”的假阳性。比如点击“下一步”后因为网络慢,页面还没跳转,截图里仍然有“下一步”这行字,系统就会误以为点击生效了,直接进入下一步流程,导致后续一连串断言失败。为了根治这个问题,我在点击后增加了一个“状态断言”:不仅查找目标文本是否消失,还同时查找新页面应该出现的关键词。比如点击“登录”后,断言“首页”或“我的”出现;如果没有出现,循环等待重试,最多等10秒。这个模式在传统自动化里叫显式等待,迁移到视觉场景只是把“等待元素出现”换成了“等待关键词出现”。4.4 跨端差异与架构抽象Android端有ADB和screencap这些现成工具,实现起来顺手。iOS端就麻烦得多,需要依赖WebDriverAgent这一类中间桥接层,截图和点击的性能也比Android差。如果团队两个平台都要覆盖,最好的办法是从一开始就把执行器设计成抽象接口,Android和iOS分别实现,视觉识别层完全复用。我现在的做法是定义了一个统一的执行器基类,包含get_screen_size、screencap、tap、swipe几个方法,Android和iOS分别实现,上层智能体不关心底层的命令差异。这样一来,页面识别逻辑一套代码两端复用,真正不同的只有底层几十行命令差异。5. 选型建议:什么时候继续用Appium,什么时候拥抱AI5.1 必须继续用Appium的场景先说结论:现在没有任何理由把存量Appium项目整体推翻重写。如果你的项目已经积累了几百条用例,团队也都熟悉传统定位方式,迁移到视觉驱动反而是一次高风险投入。还有几类场景也不适合用视觉方案。一是金融类App,它们常见的自定义安全键盘和输入框在截图上可能故意模糊或打乱,OCR识别效果会很差;二是高度动态的图表、Canvas画布,视觉模型很难精确断言绘制的数值;三是需要精确坐标校验的场景,比如验证某个元素在某个像素范围内,传统定位反而更直接。5.2 更适合AI自动化的场景适合尝鲜AI自动化的场景有几个共性:UI改版频繁、自动化用例维护成本高、缺少稳定的自动化测试属性。比如快速迭代的初创项目,我今天写的用例,下个月UI就变了,这时候视觉驱动能显著减少维护工作量。我个人建议,可以先挑一条核心冒烟用例用视觉方案重写,跑两个迭代感受一下维护成本的变化,不用一上来就全面切换。实测下来,这种“以点带面”的引入方式风险最小,也能最快暴露视觉方案在自己业务下的短板。5.3 我的方案:元素树优先、视觉兜底的混合模式结合这段时间的实操,我现在最推荐的是混合模式:能拿到稳定元素属性就优先用元素定位,拿不到或者频繁发生变化就用视觉识别兜底。具体实现上,我封装了一个find方法,先尝试driver.find_element,抛异常或超时后自动切换到OCR识别加ADB点击。两层都不依赖具体控件id,代码可以做成统一的配置驱动。在这种设计下,Appium仍然作为底层执行引擎保留,AI视觉则作为异常自愈和兜底方案,既享受传统方案的稳定可控,又拿到视觉方案的抗变化能力。等这套混合模式跑稳定之后,再慢慢把更多用例的自然语言描述性字段补上,就已经是向纯AI自动化平滑过渡了。最后再分享一个我自己的小技巧:无论用哪种方案,一定要给失败动作截图归档,并且把截图缩略图拼成一张大图。视觉自动化的调试绝大部分时间都在看截图,归档做得好,问题定位速度能快好几倍。
移动端自动化测试圈今天又被一张开源项目截图炸开了锅,消息核心就一句话:谷歌放出了一个主打AI视觉与自然语言驱动的开源自动化框架,不少人的第一反应就是“Appium要没了”。我个人的看法没那么极端,但这个东西确实值得所有写过元素定位脚本的测试开发认真看一眼——不管你是刚入行的测试新人,还是已经带团队的测试负责人,看完至少能判断“AI自动化”到底是营销热词,还是真能落地改变工作方式的东西。这篇文章我直接把这类框架的主流设计思路拆开揉碎讲清楚。我会先用一套简化版Demo亲手复现“视觉定位+自动重试”的核心玩法,再结合我这几天的实测结果,聊聊什么时候可以追随这种新思路,什么时候还是老老实实继续用Appium。全程有代码、有参数、有踩坑记录,可以直接照着抄。1. 先搞清楚:AI自动化到底动了Appium的哪块蛋糕1.1 Appium的护城河:稳定和生态Appium火了这么多年,真不是吹出来的。它最大的贡献是把移动端自动化统一到了W3C WebDriver协议上,一套脚本逻辑可以同时跑Android和iOS,社区生态也足够成熟,遇到问题随便一搜就有答案。在很长一段时间里,“Appium+元素定位”就是移动端自动化测试的代名词。但真正长期写过UI自动化的人心里都清楚,Appium最核心、也最让人头疼的东西,是它必须依赖“元素定位符”——resource-id、accessibility id、XPath表达式这些。应用内部只要做一次UI重构,或者测试环境自动化id被混淆掉,那一堆写好的findElement(By.id("xxx"))就全部失效。我在实际项目里见过最夸张的一次,一个回归套件里百分之七十的报错都来自定位符失效,根本不是业务逻辑出了问题。1.2 新框架的底牌:让机器学会“看”界面谷歌这个新工具引起议论的原因,不是它跑得更快,而是它换了一条技术路线:不再试图去抓元素树,而是让机器直接“看”屏幕。具体拆开看,这套思路包含三层能力。第一层是视觉感知,通过截图加机器学习模型识别出界面上的文字、按钮、输入框;第二层是语义理解,把“用账号密码登录”这种自然语言转换成具体的交互动作;第三层是执行与自愈,动作执行失败后重新截图、重新识别,自动调整策略。换句话说,传统自动化关心的是“控件在哪里”,它关心的是“用户想干什么”。这一下就把核心矛盾从“维护定位符”转移到了“维护业务意图”上。UI改版后,控件位置变了,但“登录”这个语义没变,脚本就还能继续跑。这就是为什么评论区很多人喊着“Appium慌了”——因为过去大家维护的重心,被它一把绕过去了。1.3 我的判断:不是取代,而是换赛道倒也不必急着把Appium贬得一文不值。以我实际测试下来的体验,AI视觉自动化在复杂手势、精确坐标校验这类场景下并没有绝对优势,而且模型的引入会带来新的不确定性。真正受到冲击的,其实是“只会在页面上找控件的脚本思维”。所以我的判断很直接:Appium作为项目不会死,但如果你只会写元素定位脚本,不懂怎么构建“意图驱动”的自动化体系,那才是真的危险。这个行业正在从“告诉机器控件在哪”过渡到“告诉机器该干什么”。2. 这类“AI自动化神器”的核心技术拆解2.1 感知层:截图、OCR与控件识别视觉自动化的第一步,是解决“机器怎么看界面”的问题。主流方案是定时截取设备屏幕,然后交给一个识别模块处理。识别模块一般都由两部分组成:OCR文字识别负责找出“登录”“下一步”“同意”这类文本,图像检测模型负责识别按钮、输入框、列表项这些控件区域。实际开发时,这里有个很容易忽略的细节:OCR告诉你的是“文字在哪里”,但可点击的控件往往比文字区域更大。如果直接把文字中心当点击点,有时候会点在文字边缘,触发不了点击事件。稳妥的做法是拿到文字框后适当向外扩展一点,比如上下左右各放宽10到20像素,或者更简单一点,点击文字中心点偏下的位置,因为绝大多数按钮的文本都垂直居中偏上,中心点偏低一些命中率更高。2.2 决策层:从自然语言到动作序列感知层负责看,决策层负责想。现在这类框架普遍采用一个轻量级的“意图解析器”,把自然语言用例解析成结构化的动作指令。举个例子,一条用例写着“点击登录按钮,输入用户名,点击确认”。传统脚本要写成:driver.find_element(By.ID, "login_btn").click() driver.find_element(By.ID, "username").send_keys("tester") driver.find_element(By.ID, "confirm").click()AI框架的逻辑更像这样:parse("点击登录") -> {action: "click", target: {"text": "登录"}} parse("输入用户名") -> {action: "input", target: {"field": "用户名"}, value: "tester"}解析器拿到指令后,再结合当前屏幕的视觉识别结果去执行。你不需要知道登录按钮的id叫什么,只需要告诉系统“这里有个登录按钮”。这一步本质上把测试用例从“代码”变成了“可读的业务描述”,对团队协作也很友好,产品和测试之间沟通成本会低很多。2.3 自愈层:识别失败之后的兜底机制光能看能想还远远不够,真实App里弹窗、加载动画、网络延迟都会让识别结果不稳定。所以这类框架都会内置一个自愈机制,执行动作前先做一次状态判断,失败了就进入重识别逻辑。自愈机制的典型流程是:点击前先截图识别目标文字,如果在超时时间内找不到,就扩展搜索范围检查页面上是否有“同意”“取消”“关闭”这类弹窗按钮;如果有就先点击弹窗关闭它,再重新截图找目标;如果重试多次还是失败,就放弃当前步骤并保存截图作为失败证据。这套逻辑并不复杂,但非常实用。2.4 新旧方案横向对比拿传统Appium方案和AI视觉驱动方案放在一起对比,差异看的很清楚:对比维度传统Appium方案AI视觉驱动方案定位依据元素树、resource-id、XPath截图视觉识别、OCR、语义理解用例编写代码脚本,需要知道控件属性自然语言描述,接近业务场景UI改版影响定位符大概率失效,需逐条修改文本和语义不变则基本不受影响执行稳定性取决于定位符稳定性,相对可控受截图质量、模型识别率影响,需要容错重试调试方式查看元素树、断点调试查看识别结果截图、置信度日志上手门槛熟悉代码和Appium API即可还需要理解视觉模型、OCR调优和重试策略设计这个表里没有绝对的好坏,更多是取舍。传统方案赢在“精确”和“可控”,AI方案赢在“灵活”和“抗变化”。3. 30分钟复刻一个视觉驱动自动化Demo(可直接抄作业)说实话,谷歌那个开源神器的完整项目我还只跑了初步体验,但它的核心设计已经足够清楚了。下面我就用“截图+OCR+ADB命令+重试机制”这套轻量组合,从零搭一个简化版视觉驱动自动化Demo,把它的核心玩法复现出来。3.1 准备环境与整体结构我用的环境是Python 3.10加一个Android模拟器,真机也可以,但需要开启开发者模式。依赖只有两个:opencv-python用于图像处理,requests或其他HTTP库用来调用OCR接口。如果你本地有离线OCR组件更好,没有的话用在线OCR接口也能跑通。整个Demo分三个模块:执行器automator:封装ADB命令,负责截图、点击、滑动、获取设备分辨率识别器vision:接收截图,返回图中所有文本及其坐标框智能体agent:把识别结果和动作组合起来,提供“找到并点击某文本”的核心方法3.2 执行器代码:先把“手”造出来执行器是所有动作落地的根,我先封装常见的ADB操作。这里直接把关键命令写好,你用的时候只需要把设备序列号和截图保存路径改成自己的。import subprocess import os class Automator: def __init__(self, device_id=None): self.device_id = device_id self.width, self.height = self.get_screen_size() def adb(self, args: list) -> str: cmd = ["adb"] if self.device_id: cmd += ["-s", self.device_id] cmd += args return subprocess.check_output(cmd, text=True).strip() def get_screen_size(self): output = self.adb(["shell", "wm", "size"]) # 输出形如: Physical size: 1080x2340 size = output.split(":")[-1].strip().split("x") return int(size[0]), int(size[1]) def screencap(self, path="screen.png"): # exec-out 配合重定向,拿到的是完整无干扰的PNG字节流 with open(path, "wb") as f: proc = subprocess.Popen(["adb"] + (["-s", self.device_id] if self.device_id else []) + ["exec-out", "screencap", "-p"], stdout=f) proc.wait() return path def tap(self, x: int, y: int): return self.adb(["shell", "input", "tap", str(int(x)), str(int(y))]) def swipe(self, x1: int, y1: int, x2: int, y2: int, duration_ms=300): return self.adb(["shell", "input", "swipe", str(int(x1)), str(int(y1)), str(int(x2)), str(int(y2)), str(duration_ms)])有一个小坑必须提醒:截图命令一定用exec-out screencap -p,不要用shell screencap -p,后者在部分设备上会往输出里混入终端控制字符,导致图片损坏。这是我最初跑Demo时图片解码失败了好几次才发现的。3.3 视觉识别代码:给机器装上“眼睛”识别器负责从截图中找到文本和位置。我这里以一个简化的OCR引擎为例,你把它替换成自己手头能用的OCR组件即可,关键是理解返回的数据结构:每个识别结果都包含文本内容、置信度以及左上角坐标和右下角坐标。import cv2 import numpy as np class Vision: def __init__(self, ocr_engine): self.ocr = ocr_engine def find_text(self, image_path, target_text, conf_threshold=0.6): image = cv2.imread(image_path) results = self.ocr.recognize(image) for item in results: text, score, bbox = item if score < conf_threshold: continue if target_text in text: x_min, y_min = bbox[0] x_max, y_max = bbox[2] cx = (x_min + x_max) / 2 cy = (y_min + y_max) / 2 return {"text": text, "score": score, "cx": cx, "cy": cy, "bbox": (x_min, y_min, x_max, y_max)} return None识别结果里的cx和cy就是像素坐标。我通常还会在调用侧做一个坐标校准:如果截图尺寸和设备分辨率不一致,就按比例换算。在模拟器上两者通常一致,但真机上部分厂商系统会做分辨率缩放,不校准的话点击点会整体偏移。3.4 自愈点击逻辑与自然语言驱动核心的点击方法要支持超时重试。我设计的逻辑是:在指定时间内反复截图并查找目标文本,目标没出现就检查页面有没有弹窗按钮,先处理弹窗再继续找,最后再决定是否点击。import time class Agent: def __init__(self, automator: Automator, vision: Vision): self.auto = automator self.vision = vision def _handle_popup(self, image_path): # 常见弹窗关闭词,按优先级尝试 for word in ["同意", "取消", "关闭", "我知道了", "跳过"]: found = self.vision.find_text(image_path, word) if found: self.auto.tap(found["cx"], found["cy"]) time.sleep(1) return True return False def click_text(self, target, timeout=10, interval=0.5, close_popup=True): start = time.time() while time.time() - start < timeout: image_path = self.auto.screencap("temp.png") found = self.vision.find_text(image_path, target) if found: # 略微向下偏移,命中按钮可点击区域 self.auto.tap(found["cx"], found["cy"] + 8) return True if close_popup: self._handle_popup(image_path) time.sleep(interval) return False关于超时和轮询间隔的选取,我实测下来的经验是:给10秒超时、0.5秒轮询一次比较平衡。太短了加载慢一点的页面会误报失败,太长了失败后的等待又会拖慢整条用例执行。真要追求效率,可以改成指数退避策略,前几次间隔短、后几次间隔长,但Demo阶段没必要。自然语言驱动也很简单,写一个极简解析器:def parse_instruction(line: str): line = line.strip() if line.startswith("点击") and line.endswith("按钮"): target = line[2:-2] return {"action": "click", "target": target} if line.startswith("输入"): # 简化处理,实际需要配合输入框识别 return {"action": "input", "target": line[2:]} if line.startswith("滑动"): return {"action": "swipe", "direction": line[2:]} return None在实际项目中,“输入”动作要比这复杂不少,因为需要先点击输入框,再通过ADB输入文本,还要处理输入法弹窗。建议先把“点击”和“滑动”跑通,再逐步扩展“输入”。3.5 用一套真实用例验证效果我拿内部一个记账App的Demo版(简化过)当实验对象,流程定为:打开App→点击“登录”→输入测试账号→点击“确认”→点击“新增一笔”→填写金额→保存。为了对比,我先用Appium传统方式写了一套脚本,再改成视觉驱动方式写了一套,故意在中间让前端同事把登录页的控件id全部改了一遍。对照结果很说明问题:Appium那套脚本登录部分的定位符全部失效,一共改了几处才恢复;视觉那套脚本因为认的是“登录”“确认”这些界面文本,UI改版后一次都没动过。当然,这里面有Demo环境的“理想成分”,生产环境比这复杂得多,但它验证了核心方向:视觉驱动脚本的抗UI变化能力确实是传统方式无法比的。4. 我踩过的那些坑:视觉自动化的典型翻车现场4.1 OCR识别到了,但点击位置偏移这是最容易踩的坑。第一次跑Demo时,OCR正确识别出了“登录”两个字,给出的中心点坐标看起来也对,但点击以后页面没反应。排查后发现两个原因:一是模拟器截图分辨率是1080x2400,而应用内实际可点击区域比文字框高出一块,点文字正中心会落在两个控件之间;二是部分系统UI控件在点击时要求命中它的“热区”,通常比视觉上看到的按钮要大,但中心点如果压在文字和按钮边缘的缝隙就会失效。我的解决办法是拿到OCR文本框后,把点击点从垂直中心往下移8到12像素,同时把水平中心作为基准。另一个更稳的方式是,找多个同义词确认目标区域,比如“登录”和“登 录”之间有空格,识别出来后先做文本归一化再匹配。4.2 弹窗遮挡导致点击失效App启动时的隐私弹窗、权限请求弹窗,几乎是无处不在的。视觉驱动脚本一旦被弹窗挡住,目标按钮的识别结果虽然还能出现(因为OCR是从截图里找文字,弹窗盖不住底下的文字),但真正点击时事件会被弹窗消费掉,脚本表现为“明明找到了却点了没反应”。我一开始只在找不到目标时才处理弹窗,后来发现这种场景更多是“找得到但点不中”。现在的策略是在每次点击前先快速扫一遍常见的弹窗关闭词,不管目标是否找到,只要发现有弹窗存在就优先处理。代价是每次点击前多花零点几秒,但换来的是稳定性大幅提升。4.3 自愈逻辑误判“成功”自愈逻辑最危险的误判,是页面其实没有切换成功,但旧页面的文本仍然显示,导致识别器给出“目标已找到”的假阳性。比如点击“下一步”后因为网络慢,页面还没跳转,截图里仍然有“下一步”这行字,系统就会误以为点击生效了,直接进入下一步流程,导致后续一连串断言失败。为了根治这个问题,我在点击后增加了一个“状态断言”:不仅查找目标文本是否消失,还同时查找新页面应该出现的关键词。比如点击“登录”后,断言“首页”或“我的”出现;如果没有出现,循环等待重试,最多等10秒。这个模式在传统自动化里叫显式等待,迁移到视觉场景只是把“等待元素出现”换成了“等待关键词出现”。4.4 跨端差异与架构抽象Android端有ADB和screencap这些现成工具,实现起来顺手。iOS端就麻烦得多,需要依赖WebDriverAgent这一类中间桥接层,截图和点击的性能也比Android差。如果团队两个平台都要覆盖,最好的办法是从一开始就把执行器设计成抽象接口,Android和iOS分别实现,视觉识别层完全复用。我现在的做法是定义了一个统一的执行器基类,包含get_screen_size、screencap、tap、swipe几个方法,Android和iOS分别实现,上层智能体不关心底层的命令差异。这样一来,页面识别逻辑一套代码两端复用,真正不同的只有底层几十行命令差异。5. 选型建议:什么时候继续用Appium,什么时候拥抱AI5.1 必须继续用Appium的场景先说结论:现在没有任何理由把存量Appium项目整体推翻重写。如果你的项目已经积累了几百条用例,团队也都熟悉传统定位方式,迁移到视觉驱动反而是一次高风险投入。还有几类场景也不适合用视觉方案。一是金融类App,它们常见的自定义安全键盘和输入框在截图上可能故意模糊或打乱,OCR识别效果会很差;二是高度动态的图表、Canvas画布,视觉模型很难精确断言绘制的数值;三是需要精确坐标校验的场景,比如验证某个元素在某个像素范围内,传统定位反而更直接。5.2 更适合AI自动化的场景适合尝鲜AI自动化的场景有几个共性:UI改版频繁、自动化用例维护成本高、缺少稳定的自动化测试属性。比如快速迭代的初创项目,我今天写的用例,下个月UI就变了,这时候视觉驱动能显著减少维护工作量。我个人建议,可以先挑一条核心冒烟用例用视觉方案重写,跑两个迭代感受一下维护成本的变化,不用一上来就全面切换。实测下来,这种“以点带面”的引入方式风险最小,也能最快暴露视觉方案在自己业务下的短板。5.3 我的方案:元素树优先、视觉兜底的混合模式结合这段时间的实操,我现在最推荐的是混合模式:能拿到稳定元素属性就优先用元素定位,拿不到或者频繁发生变化就用视觉识别兜底。具体实现上,我封装了一个find方法,先尝试driver.find_element,抛异常或超时后自动切换到OCR识别加ADB点击。两层都不依赖具体控件id,代码可以做成统一的配置驱动。在这种设计下,Appium仍然作为底层执行引擎保留,AI视觉则作为异常自愈和兜底方案,既享受传统方案的稳定可控,又拿到视觉方案的抗变化能力。等这套混合模式跑稳定之后,再慢慢把更多用例的自然语言描述性字段补上,就已经是向纯AI自动化平滑过渡了。最后再分享一个我自己的小技巧:无论用哪种方案,一定要给失败动作截图归档,并且把截图缩略图拼成一张大图。视觉自动化的调试绝大部分时间都在看截图,归档做得好,问题定位速度能快好几倍。