做移动端自动化的朋友,应该都经历过这种场景:需求方说某个核心链路必须自动化回归,结果你打开控件树一看,登录页躺着一张验证码,主业务区直接是自定义绘制控件,Appium 那一套 findElement 在这两个场景面前基本等于摆设。我最近在一个模拟项目里把 Appium 和多模态大模型结合,专门啃这两块硬骨头,实测下来比预想中靠谱得多。这篇就把方案选型、完整实现、踩过的坑一次性说清楚。适用对象说白了就两类人:一类是天天被验证码卡住的测试开发,另一类是面对复杂自定义控件定位无门的自动化脚本维护者。如果你已经熟练使用 Appium 的基础定位方式,但遇到这两个场景就头大,那这套思路可以直接"抄作业"。1. 先别急着写代码:传统Appium方案到底卡在哪1.1 验证码:不是识别技术不行,是规则在"反识别"验证码之所以能成为自动化的拦路虎,根源在于它的设计目标就是和机器作对。传统的 OCR 技术(典型的就是 Tesseract)在处理清晰印刷体字符时有不错的效果,但市面上真正在用的验证码早就不按常理出牌了:字符扭曲、背景噪点、干扰线、字符粘连,还有旋转和遮挡。我在实际项目里用过 Tesseract 识别某系统的数字验证码,无干扰时准确率还能到九成,一旦加上轻微扭曲和噪点,识别率直接掉到六成以下,这对测试来说根本不可用。更要命的是滑块验证码和点选验证码。滑块验证码要识别的不只是字符,还有目标凹槽的位置和滑块本身的位置;点选验证码需要理解文字语义,比如"按顺序点击:苹果、香蕉、葡萄",这已经不是传统 OCR 能处理的范畴了,它需要模型理解图片里的物体和文字语义。多模态大模型的优势恰好在这里:它不是在做"字符识别",而是在做"看图理解",对扭曲、干扰、语义指令的容忍度比传统方案高一个量级。这里必须强调一个边界:验证码本质是安全防线,我们的自动化手段只能用在测试环境、测试包或白名单机制允许的场景里。正规操作是先在服务端为测试账号配置验证码白名单,或者使用固定的测试验证码,大模型识别是兜底手段,而不是用来突破真实线上风控的。这个合规底线踩不得。1.2 复杂自定义控件:控件树里根本没有你要的东西Appium 传统定位方式依赖一个前提:被测应用的控件树是完整的、带有可识别属性的。它的定位策略本质上就是在 XML 控件树里做查询,靠 resource-id、class、text、content-desc,或者顺着层级写 XPath。但复杂自定义控件正好把这个前提给打破了。我遇到的典型场景是某直播类 App 的礼物面板。整个礼物区域是用 Canvas 绘制的,所有礼物图标都是画出来的,控件树里只暴露了一个巨大 View,子节点一概没有,什么 resource-id、text 全是空。Appium 对这种情况根本无从下手,XPath 只能定位到那个空白的大 View,你拿它什么都做不了。还有一个常见场景是游戏界面。游戏引擎渲染出来的一帧画面,在控件树里就是一个 SurfaceView,内部结构完全不暴露。另一个是自定义日历控件,日期格子是绘制出来的,你无法直接定位到"下个月 15 号"这个特定格子。这种场景下,传统的求稳方案是走图像定位,Appium 内置的 image 定位策略底层靠 OpenCV 模板匹配,但它有个致命缺陷:必须维护一套模板图。分辨率一变、主题色一换、控件状态一改(比如选中和未选中),模板图就得重新截,维护成本高到劝退。2. 让多模态大模型当"眼睛":方案设计与关键决策2.1 传统视觉方案的短板与多模态大模型的本质优势在引入多模态大模型之前,我先把市面上能用的视觉方案捋了一遍,列个对比表,方便你参考:方案适用场景抗干扰能力维护成本单次成本延迟Tesseract OCR清晰印刷字符弱中低低OpenCV 模板匹配固定静态图标中高(需维护模板图)低低多模态大模型语义描述的目标强低(改提示词即可)高中多模态大模型的核心价值在于两点:第一是语义理解能力,你可以用自然语言描述"找那个红色的确认按钮",它不需要精确的模板图就能理解目标是什么;第二是免训练迁移,换个业务界面,你只需要改提示词,不用重新训练模型、不用重新截模板图。这两点直接解决了传统方案在快速迭代场景下维护成本爆炸的问题。但这不代表之前的方案一无是处。实际上我最终的架构是分层的:优先尝试 Appium 原生定位,原生定位不到再切到多模态大模型识别,最后还可以加一层 OpenCV 模板匹配兜底。这样既控制了成本,也保证了成功率。2.2 整体链路架构与三个关键决策整套方案的链路其实不复杂,核心是四步:Appium 脚本控制设备截图,拿到当前界面的图像;把截图转成 Base64 编码,连同提示词一起发给多模态大模型接口;模型返回结构化结果,通常是 JSON,包含验证码内容或目标控件的坐标;脚本解析 JSON,把坐标换算成屏幕实际坐标,通过 W3C Actions 执行点击、输入或滑动。设计这套链路时有三个关键决策要提前想清楚。第一个决策是模型怎么部署。如果你所在团队对数据隐私要求高,测试界面截图不能出内网,那就得走私有化部署;如果只是普通业务自动化,直接调公有云的多模态大模型 API 就行了。我个人的建议是:先拿公有云 API 跑通流程,验证效果,再做私有化或者换更强模型的打算。项目初期不要纠结部署形态,效果验证优先。第二个决策是模型返回格式。多模态大模型输出的是自然语言,如果你让它"把验证码告诉我",它可能回一句"好的,验证码是 3s7f"。这种文本解析起来很痛苦,所以必须在提示词里强制指定 JSON 输出格式,并且给出明确的字段名和类型。第三个决策是截图策略。验证码类目标,截全屏图就行,模型自己会找到验证码区域;控件定位类目标,建议先找到控件树中那个"空白大 View"的 bounds,只截这一块区域发给模型,减少画面中的干扰元素,识别准确率和响应速度都会好很多。2.3 什么时候不该用这套方案这方案看着美好,但它有明确的适用边界,我替你踩过坑,帮你画清楚。如果你的目标控件有正常的 resource-id 或 text,老老实实用原生定位。多模态大模型方案的延迟通常在一到三秒,原生定位是毫秒级,杀鸡不用牛刀。有些页面只有三五个验证码形态,更轻量的做法是直接对接测试白名单;只有当你确实拿不到验证码绕过方案、又必须端到端验证时,才考虑大模型识别。成本也是硬约束。公有云多模态 API 是按图片张数和 token 计费的,每跑一轮完整回归,如果几十个用例都在调大模型,账单数字会让人心慌。所以一定得上缓存和降级,后面第三部分我会给具体实现。如果界面里是静态图表、二维码这类固定图形,OpenCV 模板匹配是更好的选择,它零成本、毫秒级响应,大模型反而又慢又贵。多模态大模型最适合解决的是"目标可以通过语义描述、但无法通过属性定位"的场景。3. 落地实操:验证码识别与自定义控件定位的完整实现3.1 工程结构与环境准备我用的技术栈是 Python + Appium 2.x + requests 直接调大模型接口,没有引入额外的测试框架,这样最小可行链路的依赖最少。Appium 端需要确认的事项:Python 端安装 Appium-Python-Client,服务端启动 Appium 2.x,设备连上后能正常执行基础的 find_element 和截图操作。工程的目录结构按功能拆成三个模块:driver.py:负责创建 Appium 会话,封装截图、点击、输入等基础操作;vision_service.py:统一封装多模态大模型调用,对外只暴露两个方法,recognize_captcha()和locate_element();main_flow.py:实际业务用例,组合调用前两个模块。这种拆分的好处是很明显的:后续如果换模型供应商,只需要改vision_service.py一个文件,业务代码一行都不用动。3.2 验证码识别:实现一个可复用的调用代码先说最简单的登录验证码场景。界面上有一个验证码图片控件,我不能直接读取它的内容,于是把整张屏幕截图发给大模型。先封装一个通用的截图转 Base64 方法:import base64 def screenshot_to_base64(driver): # Appium 原生截图方法,返回 PNG 图片的 base64 字符串 return driver.get_screenshot_as_base64()然后封装大模型调用。这里要注意,不同模型的接口格式略有差异,但主流通用格式长这样,一个 user 消息里同时包含文本提示词和图片内容:import requests import base64 API_ENDPOINT = "https://api.example.com/v1/chat/completions" API_KEY = "your-api-key" MODEL_NAME = "multimodal-model-v1" def call_vision_model(prompt, image_base64): payload = { "model": MODEL_NAME, "messages": [ { "role": "user", "content": [ {"type": "text", "text": prompt}, { "type": "image_url", "image_url": { "url": f"data:image/png;base64,{image_base64}" }, }, ], } ], "temperature": 0, } headers = {"Authorization": f"Bearer {API_KEY}"} resp = requests.post(API_ENDPOINT, headers=headers, json=payload, timeout=30) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]验证码识别的提示词我踩过几次坑后沉淀下来的版本是这样的:CAPTCHA_PROMPT = """ 你是一个自动化测试助手。请看这张移动端截图。 1. 如果图中存在验证码(通常是扭曲的数字或字母组合),请识别其中的字符内容; 2. 如果图中不存在验证码,请返回空字符串。 只输出JSON,不要输出任何解释或代码块标记,格式如下: {"captcha": "识别出来的验证码字符,无验证码则为空字符串"} """调用后拿到的返回可能是{"captcha": "3s7f"},也可能因为模型输出不稳定多了一些围栏符号。我写了段解析容错逻辑,把非 JSON 的杂质剥掉,用正则提取最外层 JSON:import json import re def parse_json_response(text): # 兼容模型可能输出的 markdown 围栏 cleaned = re.sub(r"^```(?:json)?|```$", "", text.strip(), flags=re.MULTILINE) cleaned = cleaned.strip() return json.loads(cleaned)最后把验证码填入输入框,整个流程就闭环了:def auto_login_with_captcha(driver, username, password): # 定位用户名密码输入框,正常用原生定位 driver.find_element("id", "username").send_keys(username) driver.find_element("id", "password").send_keys(password) # 截图识别验证码 img_b64 = screenshot_to_base64(driver) raw = call_vision_model(CAPTCHA_PROMPT, img_b64) captcha = parse_json_response(raw)["captcha"] driver.find_element("id", "captcha_input").send_keys(captcha) driver.find_element("id", "login_btn").click()3.3 复杂控件定位:坐标返回与屏幕换算控件定位比验证码稍微绕一点,因为模型返回的是图片上的像素坐标,而 Appium 执行点击需要的是屏幕坐标。绝大多数情况下,Appium 截图的尺寸和屏幕实际尺寸是一致的,但有些设备因为系统 UI 缩放、状态栏高度等原因,截图和实际屏幕之间存在比例偏差,不做换算就会发生"点击偏了几个像素"的诡异问题。先看定位提示词。以我那个模拟项目里的直播礼物面板为例,目标是点击"火箭"礼物图标:LOCATOR_PROMPT = """ 请看这张移动端截图中被框定的区域。图中有多个礼物图标。 请找到名称为"火箭"的礼物图标,输出它中心点在图片中的像素坐标。 只输出JSON,格式如下: {"found": true, "point": {"x": 数字, "y": 数字}} 如果找不到,输出{"found": false, "point": null} """在调用前我通常做一步区域裁剪。先用 Appium 定位到那个唯一的空白大 View,拿它的 bounds 做裁剪,把代码里保存的截图用 Pillow 裁一下,再转 Base64。这样做的好处一是减少模型的处理面积,二是减少同屏相似图标的干扰。坐标换算代码如下:from PIL import Image from io import BytesIO def convert_coords(driver, image_base64, raw_x, raw_y): # 拿到截图的真实宽高 image = Image.open(BytesIO(base64.b64decode(image_base64))) img_w, img_h = image.size # 拿到设备屏幕的宽高 win_size = driver.get_window_size() screen_w, screen_h = win_size["width"], win_size["height"] # 计算换算比例 scale_x = screen_w / img_w scale_y = screen_h / img_h real_x = int(raw_x * scale_x) real_y = int(raw_y * scale_y) return real_x, real_y如果实际操作发现还有固定的偏移量(比如截图把状态栏裁掉导致 Y 轴整体偏移),就在换算结果上再叠加一个补偿值。这个在实际项目中基本都会遇到,我会在第四部分细说。最后执行点击动作。Appium 2.x 推荐用 W3C Actions:from appium.webdriver.common.appiumby import AppiumBy from selenium.webdriver.common.actions.action_builder import ActionBuilder from selenium.webdriver.common.actions import interaction from selenium.webdriver.common.actions.pointer_input import PointerInput def tap_by_coords(driver, x, y): action_builder = ActionBuilder(driver) pointer = PointerInput(interaction.POINTER_TOUCH, "touch") action_builder.add_pointer_input(pointer) action_builder.pointer_action.move_to_location(x, y) action_builder.pointer_action.pointer_down() action_builder.pointer_action.pause(0.1) action_builder.pointer_action.pointer_up() action_builder.perform()滑块验证码的实现思路也一样,模型返回滑块和目标凹槽两个坐标,换算后用一个 swipe 动作从起点滑到终点,速度要模拟人类拖拽的变速模式。这里有个技巧,不要匀速直线滑动,分段改变速度更容易通过服务端的行为校验。3.4 性能优化:缓存、降级与并发把链路跑通之后,就要面对成本和效率问题。我加了三层优化,实测同一套用例的执行时间从每屏三秒左右降到了原始用例的零头,账单也好看很多。第一层是本地缓存。多数时候,同一个测试用例跑到同一个界面,控件位置是固定的。我用页面标识加目标描述做缓存键,比如 activity 名加"火箭礼物图标",命中缓存就直接用坐标,不再调大模型。缓存失效的时机可以简单粗暴,杀掉 App 进程时清空。import functools @functools.lru_cache(maxsize=256) def locate_with_cache(activity_name, target_desc): # 真正调大模型的逻辑 passPython 的functools.lru_cache足够满足这个需求,不用引入额外依赖。第二层是降级。模型识别失败或超时,不能直接让用例挂掉。我的策略是:第一次识别失败,重新截图再试一次;第二次失败,切到 OpenCV 模板匹配尝试;还不行就标记用例失败并保存现场截图,方便人工排查。这套降级逻辑在实际落地中非常关键,它保证了自动化框架在模型偶发抽风时不至于全线飘红。第三层是并发。如果用例里同一屏要识别多个目标,串行调模型会等很久。可以先把整张图发给一次模型请求,用提示词让它一次性返回所有目标坐标,而不是一个目标请求一次。一次请求的成本比多次请求低得多,也大幅减少了总耗时。4. 实操中踩过的坑与排查思路4.1 坐标偏差不定:最先要排查像素缩放比例我在模拟项目里第一次看到模型返回的坐标后,直接按原值去点"火箭"礼物图标,结果点击位置偏到了它旁边的"飞机"上。第一反应以为是模型识别错了,后来检查才发现是截图尺寸和设备逻辑分辨率不一致。排查方法是把模型输入的原始图片宽高打印出来,和driver.get_window_size()的宽高对比。如果不一致,按第三部分的换算公式处理。如果比例一致仍然有偏移,再看是不是状态栏导致的系统坐标偏移,解决方法是叠加一个固定补偿值。这个补偿值不要凭感觉调,最好的办法是做一个标定脚本:在屏幕上取左上、右上、左下、右下四个点,分别定位并调用模型返回坐标,反推出实际偏移量和缩放比例。4.2 验证码识别率波动:prompt与图像质量缺一不可验证码识别率低,第一个问题是图像被压缩。Appium 截图是 PNG,就算原图清晰,经过 Base64 传输到模型侧,模型内部也可能压缩图片。我实测过,验证码在原图里的区域如果很小,识别率会明显下降,尤其是字母数字混合的情况。解决办法是裁剪放大验证码区域再发送,如果验证码区域无法提前确定,就全屏截图后在提示词里让模型先检测验证码位置,这一步本身会稍微增加耗时,但准确率提升明显。第二个问题是 model 的随机性。虽然我在代码里设置了temperature=0,但某些模型对图片识别的输出仍存在波动。我的做法是同一个图跑两次识别,两次结果一致才采用;不一致则默认保留第一次结果并报一个警告。对于关键链路,还可以用两个不同模型投票,单次成本会上涨,但稳定性和成功率确实能拉到近乎百分百。第三个问题在 prompt 本身。原版提示词我只写了"识别验证码中字符",模型偶尔会把干扰线也当成字母输出。后来我在提示词里追加了一句"忽略背景线条、噪点和装饰性元素",识别率就稳定住了。提示词细节对结果的影响极大,多试几种表达,把最优版本固化下来,这也是维护这套方案时最省钱有效的手段。4.3 响应慢与成本失控:如何把单次识别成本压下来模型接口的速度通常在 1.5 到 3 秒之间,这个数字单独看不夸张,但几十个用例串行跑下来,执行时间直接翻倍。我做了几个针对性优化。首先是压缩图片尺寸。截图分辨率高,发送的 token 数量跟图片分辨率强相关。如果被测区域就是一个 300x200 的验证码,就没必要送全屏截图。我在调用前统一把目标区域缩放到宽 512 像素以内,既保证识别精度,又大幅降低 token 消耗。实测下来响应时间能减少三分之一以上。其次是缓存命中率的提升。前文说的lru_cache是关键,而且缓存键要设计得够细。同一个界面不同状态(比如选中和未选中)的控件位置可能不同,我就在目标描述里加上界面状态关键词,宁可多缓存几个变体,也不要因为缓存冲突导致点击错误。最后是超时控制。requests.post里的timeout参数我一开始没设,遇到过模型服务偶发卡住,导致整个用例挂五分钟的情况。现在统一设 30 秒超时,超时就重试一次,再超时直接走降级逻辑。这一个小小的改动,让整个自动化框架的稳定性提升了一个台阶。4.4 合规与安全:自动化测试里的验证码边界这块很容易被忽略,但我认为必须单独拿出来说。自动化测试的本质目标是保障产品质量,不是攻破业务的安全防线。所以涉及验证码的一切实操,都应该限定在测试环境里执行。如果你的验证码校验逻辑在测试环境无法关闭或绕过,优先推动开发在服务端加白名单,而不是依赖自动化脚本去硬闯。另外,测试过程中截取的屏幕截图很可能包含用户信息和业务数据。这些图片发送到外部模型服务时,要做脱敏和最小化处理。比如截图前先裁剪掉顶部的用户头像、手机号等区域,或者使用内部私有化部署的模型服务。数据合规问题在引入任何外部服务时都要提前评估,不要在问题出现之后才补救。5. 这套方案的后续扩展方向到这里,核心的验证码识别和复杂控件定位已经能稳定跑了,但这个架构能做的事情远不止这些。我把扩展思路简单梳理一下,大家可以根据自己的业务场景直接套用。第一个方向是视觉回归测试。现在截图识别链路已经通了,那么我可以把每次操作后的截图和上一次的基线截图做对比,多模态大模型来判断哪些像素级变化是预期的交互反馈,哪些是 UI 渲染异常。这比传统的像素 diff 工具智能得多,传统工具对滚动、弹窗、动态内容往往是误报大户。第二个方向是智能流程探索。Appium 传统的测试脚本需要人写死路径,而有了视觉加语义理解能力,可以让大模型根据当前界面截图决定下一步操作:看到登录页就自动找账号密码输入框和登录按钮,看到弹窗就找关闭按钮。这本质上就是基于视觉的智能回归探索,比 Monkey 那种随机乱点高效得多,因为每一步操作都是基于语义理解的。第三个方向是跨端一致性。同一套页面在 Android 和 iOS 上控件结构差异很大,但视觉表现应该是一致的。用截图加语义描述的方式,可以以视觉结果为基准,对两端进行一致性比对。这在业务方要求"两端体验一致"的场景下非常有用。最后一个值得提的方向是脚本自愈。跑自动化最烦的事情就是产品小版本迭代改了控件属性,脚本一夜之间全挂。如果每次定位不到控件时,自动切到视觉识别兜底,那脚本的鲁棒性会大幅提升,维护成本直线下降。我个人在实际项目中体会最深的一点是:这套方案的定位不是替代 Appium,而是给 Appium 补上一双"眼睛"。它把传统自动化方案的能力边界往前推了一大截,让以前根本不可能自动化的场景变成了可能。当然,模型成本、响应延迟和结果稳定性这些短板还是客观存在的,所以架构上一定要保留降级链路,不能把全部赌注压在一个模型上。如果你现在正被验证码和自定义控件折磨,建议先搭一个最小链路跑通核心流程,选一个你项目里最疼的场景动手试,效果验证之后再逐步铺开,慢慢就能摸出适合自己业务的那套边界了。
做移动端自动化的朋友,应该都经历过这种场景:需求方说某个核心链路必须自动化回归,结果你打开控件树一看,登录页躺着一张验证码,主业务区直接是自定义绘制控件,Appium 那一套 findElement 在这两个场景面前基本等于摆设。我最近在一个模拟项目里把 Appium 和多模态大模型结合,专门啃这两块硬骨头,实测下来比预想中靠谱得多。这篇就把方案选型、完整实现、踩过的坑一次性说清楚。适用对象说白了就两类人:一类是天天被验证码卡住的测试开发,另一类是面对复杂自定义控件定位无门的自动化脚本维护者。如果你已经熟练使用 Appium 的基础定位方式,但遇到这两个场景就头大,那这套思路可以直接"抄作业"。1. 先别急着写代码:传统Appium方案到底卡在哪1.1 验证码:不是识别技术不行,是规则在"反识别"验证码之所以能成为自动化的拦路虎,根源在于它的设计目标就是和机器作对。传统的 OCR 技术(典型的就是 Tesseract)在处理清晰印刷体字符时有不错的效果,但市面上真正在用的验证码早就不按常理出牌了:字符扭曲、背景噪点、干扰线、字符粘连,还有旋转和遮挡。我在实际项目里用过 Tesseract 识别某系统的数字验证码,无干扰时准确率还能到九成,一旦加上轻微扭曲和噪点,识别率直接掉到六成以下,这对测试来说根本不可用。更要命的是滑块验证码和点选验证码。滑块验证码要识别的不只是字符,还有目标凹槽的位置和滑块本身的位置;点选验证码需要理解文字语义,比如"按顺序点击:苹果、香蕉、葡萄",这已经不是传统 OCR 能处理的范畴了,它需要模型理解图片里的物体和文字语义。多模态大模型的优势恰好在这里:它不是在做"字符识别",而是在做"看图理解",对扭曲、干扰、语义指令的容忍度比传统方案高一个量级。这里必须强调一个边界:验证码本质是安全防线,我们的自动化手段只能用在测试环境、测试包或白名单机制允许的场景里。正规操作是先在服务端为测试账号配置验证码白名单,或者使用固定的测试验证码,大模型识别是兜底手段,而不是用来突破真实线上风控的。这个合规底线踩不得。1.2 复杂自定义控件:控件树里根本没有你要的东西Appium 传统定位方式依赖一个前提:被测应用的控件树是完整的、带有可识别属性的。它的定位策略本质上就是在 XML 控件树里做查询,靠 resource-id、class、text、content-desc,或者顺着层级写 XPath。但复杂自定义控件正好把这个前提给打破了。我遇到的典型场景是某直播类 App 的礼物面板。整个礼物区域是用 Canvas 绘制的,所有礼物图标都是画出来的,控件树里只暴露了一个巨大 View,子节点一概没有,什么 resource-id、text 全是空。Appium 对这种情况根本无从下手,XPath 只能定位到那个空白的大 View,你拿它什么都做不了。还有一个常见场景是游戏界面。游戏引擎渲染出来的一帧画面,在控件树里就是一个 SurfaceView,内部结构完全不暴露。另一个是自定义日历控件,日期格子是绘制出来的,你无法直接定位到"下个月 15 号"这个特定格子。这种场景下,传统的求稳方案是走图像定位,Appium 内置的 image 定位策略底层靠 OpenCV 模板匹配,但它有个致命缺陷:必须维护一套模板图。分辨率一变、主题色一换、控件状态一改(比如选中和未选中),模板图就得重新截,维护成本高到劝退。2. 让多模态大模型当"眼睛":方案设计与关键决策2.1 传统视觉方案的短板与多模态大模型的本质优势在引入多模态大模型之前,我先把市面上能用的视觉方案捋了一遍,列个对比表,方便你参考:方案适用场景抗干扰能力维护成本单次成本延迟Tesseract OCR清晰印刷字符弱中低低OpenCV 模板匹配固定静态图标中高(需维护模板图)低低多模态大模型语义描述的目标强低(改提示词即可)高中多模态大模型的核心价值在于两点:第一是语义理解能力,你可以用自然语言描述"找那个红色的确认按钮",它不需要精确的模板图就能理解目标是什么;第二是免训练迁移,换个业务界面,你只需要改提示词,不用重新训练模型、不用重新截模板图。这两点直接解决了传统方案在快速迭代场景下维护成本爆炸的问题。但这不代表之前的方案一无是处。实际上我最终的架构是分层的:优先尝试 Appium 原生定位,原生定位不到再切到多模态大模型识别,最后还可以加一层 OpenCV 模板匹配兜底。这样既控制了成本,也保证了成功率。2.2 整体链路架构与三个关键决策整套方案的链路其实不复杂,核心是四步:Appium 脚本控制设备截图,拿到当前界面的图像;把截图转成 Base64 编码,连同提示词一起发给多模态大模型接口;模型返回结构化结果,通常是 JSON,包含验证码内容或目标控件的坐标;脚本解析 JSON,把坐标换算成屏幕实际坐标,通过 W3C Actions 执行点击、输入或滑动。设计这套链路时有三个关键决策要提前想清楚。第一个决策是模型怎么部署。如果你所在团队对数据隐私要求高,测试界面截图不能出内网,那就得走私有化部署;如果只是普通业务自动化,直接调公有云的多模态大模型 API 就行了。我个人的建议是:先拿公有云 API 跑通流程,验证效果,再做私有化或者换更强模型的打算。项目初期不要纠结部署形态,效果验证优先。第二个决策是模型返回格式。多模态大模型输出的是自然语言,如果你让它"把验证码告诉我",它可能回一句"好的,验证码是 3s7f"。这种文本解析起来很痛苦,所以必须在提示词里强制指定 JSON 输出格式,并且给出明确的字段名和类型。第三个决策是截图策略。验证码类目标,截全屏图就行,模型自己会找到验证码区域;控件定位类目标,建议先找到控件树中那个"空白大 View"的 bounds,只截这一块区域发给模型,减少画面中的干扰元素,识别准确率和响应速度都会好很多。2.3 什么时候不该用这套方案这方案看着美好,但它有明确的适用边界,我替你踩过坑,帮你画清楚。如果你的目标控件有正常的 resource-id 或 text,老老实实用原生定位。多模态大模型方案的延迟通常在一到三秒,原生定位是毫秒级,杀鸡不用牛刀。有些页面只有三五个验证码形态,更轻量的做法是直接对接测试白名单;只有当你确实拿不到验证码绕过方案、又必须端到端验证时,才考虑大模型识别。成本也是硬约束。公有云多模态 API 是按图片张数和 token 计费的,每跑一轮完整回归,如果几十个用例都在调大模型,账单数字会让人心慌。所以一定得上缓存和降级,后面第三部分我会给具体实现。如果界面里是静态图表、二维码这类固定图形,OpenCV 模板匹配是更好的选择,它零成本、毫秒级响应,大模型反而又慢又贵。多模态大模型最适合解决的是"目标可以通过语义描述、但无法通过属性定位"的场景。3. 落地实操:验证码识别与自定义控件定位的完整实现3.1 工程结构与环境准备我用的技术栈是 Python + Appium 2.x + requests 直接调大模型接口,没有引入额外的测试框架,这样最小可行链路的依赖最少。Appium 端需要确认的事项:Python 端安装 Appium-Python-Client,服务端启动 Appium 2.x,设备连上后能正常执行基础的 find_element 和截图操作。工程的目录结构按功能拆成三个模块:driver.py:负责创建 Appium 会话,封装截图、点击、输入等基础操作;vision_service.py:统一封装多模态大模型调用,对外只暴露两个方法,recognize_captcha()和locate_element();main_flow.py:实际业务用例,组合调用前两个模块。这种拆分的好处是很明显的:后续如果换模型供应商,只需要改vision_service.py一个文件,业务代码一行都不用动。3.2 验证码识别:实现一个可复用的调用代码先说最简单的登录验证码场景。界面上有一个验证码图片控件,我不能直接读取它的内容,于是把整张屏幕截图发给大模型。先封装一个通用的截图转 Base64 方法:import base64 def screenshot_to_base64(driver): # Appium 原生截图方法,返回 PNG 图片的 base64 字符串 return driver.get_screenshot_as_base64()然后封装大模型调用。这里要注意,不同模型的接口格式略有差异,但主流通用格式长这样,一个 user 消息里同时包含文本提示词和图片内容:import requests import base64 API_ENDPOINT = "https://api.example.com/v1/chat/completions" API_KEY = "your-api-key" MODEL_NAME = "multimodal-model-v1" def call_vision_model(prompt, image_base64): payload = { "model": MODEL_NAME, "messages": [ { "role": "user", "content": [ {"type": "text", "text": prompt}, { "type": "image_url", "image_url": { "url": f"data:image/png;base64,{image_base64}" }, }, ], } ], "temperature": 0, } headers = {"Authorization": f"Bearer {API_KEY}"} resp = requests.post(API_ENDPOINT, headers=headers, json=payload, timeout=30) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]验证码识别的提示词我踩过几次坑后沉淀下来的版本是这样的:CAPTCHA_PROMPT = """ 你是一个自动化测试助手。请看这张移动端截图。 1. 如果图中存在验证码(通常是扭曲的数字或字母组合),请识别其中的字符内容; 2. 如果图中不存在验证码,请返回空字符串。 只输出JSON,不要输出任何解释或代码块标记,格式如下: {"captcha": "识别出来的验证码字符,无验证码则为空字符串"} """调用后拿到的返回可能是{"captcha": "3s7f"},也可能因为模型输出不稳定多了一些围栏符号。我写了段解析容错逻辑,把非 JSON 的杂质剥掉,用正则提取最外层 JSON:import json import re def parse_json_response(text): # 兼容模型可能输出的 markdown 围栏 cleaned = re.sub(r"^```(?:json)?|```$", "", text.strip(), flags=re.MULTILINE) cleaned = cleaned.strip() return json.loads(cleaned)最后把验证码填入输入框,整个流程就闭环了:def auto_login_with_captcha(driver, username, password): # 定位用户名密码输入框,正常用原生定位 driver.find_element("id", "username").send_keys(username) driver.find_element("id", "password").send_keys(password) # 截图识别验证码 img_b64 = screenshot_to_base64(driver) raw = call_vision_model(CAPTCHA_PROMPT, img_b64) captcha = parse_json_response(raw)["captcha"] driver.find_element("id", "captcha_input").send_keys(captcha) driver.find_element("id", "login_btn").click()3.3 复杂控件定位:坐标返回与屏幕换算控件定位比验证码稍微绕一点,因为模型返回的是图片上的像素坐标,而 Appium 执行点击需要的是屏幕坐标。绝大多数情况下,Appium 截图的尺寸和屏幕实际尺寸是一致的,但有些设备因为系统 UI 缩放、状态栏高度等原因,截图和实际屏幕之间存在比例偏差,不做换算就会发生"点击偏了几个像素"的诡异问题。先看定位提示词。以我那个模拟项目里的直播礼物面板为例,目标是点击"火箭"礼物图标:LOCATOR_PROMPT = """ 请看这张移动端截图中被框定的区域。图中有多个礼物图标。 请找到名称为"火箭"的礼物图标,输出它中心点在图片中的像素坐标。 只输出JSON,格式如下: {"found": true, "point": {"x": 数字, "y": 数字}} 如果找不到,输出{"found": false, "point": null} """在调用前我通常做一步区域裁剪。先用 Appium 定位到那个唯一的空白大 View,拿它的 bounds 做裁剪,把代码里保存的截图用 Pillow 裁一下,再转 Base64。这样做的好处一是减少模型的处理面积,二是减少同屏相似图标的干扰。坐标换算代码如下:from PIL import Image from io import BytesIO def convert_coords(driver, image_base64, raw_x, raw_y): # 拿到截图的真实宽高 image = Image.open(BytesIO(base64.b64decode(image_base64))) img_w, img_h = image.size # 拿到设备屏幕的宽高 win_size = driver.get_window_size() screen_w, screen_h = win_size["width"], win_size["height"] # 计算换算比例 scale_x = screen_w / img_w scale_y = screen_h / img_h real_x = int(raw_x * scale_x) real_y = int(raw_y * scale_y) return real_x, real_y如果实际操作发现还有固定的偏移量(比如截图把状态栏裁掉导致 Y 轴整体偏移),就在换算结果上再叠加一个补偿值。这个在实际项目中基本都会遇到,我会在第四部分细说。最后执行点击动作。Appium 2.x 推荐用 W3C Actions:from appium.webdriver.common.appiumby import AppiumBy from selenium.webdriver.common.actions.action_builder import ActionBuilder from selenium.webdriver.common.actions import interaction from selenium.webdriver.common.actions.pointer_input import PointerInput def tap_by_coords(driver, x, y): action_builder = ActionBuilder(driver) pointer = PointerInput(interaction.POINTER_TOUCH, "touch") action_builder.add_pointer_input(pointer) action_builder.pointer_action.move_to_location(x, y) action_builder.pointer_action.pointer_down() action_builder.pointer_action.pause(0.1) action_builder.pointer_action.pointer_up() action_builder.perform()滑块验证码的实现思路也一样,模型返回滑块和目标凹槽两个坐标,换算后用一个 swipe 动作从起点滑到终点,速度要模拟人类拖拽的变速模式。这里有个技巧,不要匀速直线滑动,分段改变速度更容易通过服务端的行为校验。3.4 性能优化:缓存、降级与并发把链路跑通之后,就要面对成本和效率问题。我加了三层优化,实测同一套用例的执行时间从每屏三秒左右降到了原始用例的零头,账单也好看很多。第一层是本地缓存。多数时候,同一个测试用例跑到同一个界面,控件位置是固定的。我用页面标识加目标描述做缓存键,比如 activity 名加"火箭礼物图标",命中缓存就直接用坐标,不再调大模型。缓存失效的时机可以简单粗暴,杀掉 App 进程时清空。import functools @functools.lru_cache(maxsize=256) def locate_with_cache(activity_name, target_desc): # 真正调大模型的逻辑 passPython 的functools.lru_cache足够满足这个需求,不用引入额外依赖。第二层是降级。模型识别失败或超时,不能直接让用例挂掉。我的策略是:第一次识别失败,重新截图再试一次;第二次失败,切到 OpenCV 模板匹配尝试;还不行就标记用例失败并保存现场截图,方便人工排查。这套降级逻辑在实际落地中非常关键,它保证了自动化框架在模型偶发抽风时不至于全线飘红。第三层是并发。如果用例里同一屏要识别多个目标,串行调模型会等很久。可以先把整张图发给一次模型请求,用提示词让它一次性返回所有目标坐标,而不是一个目标请求一次。一次请求的成本比多次请求低得多,也大幅减少了总耗时。4. 实操中踩过的坑与排查思路4.1 坐标偏差不定:最先要排查像素缩放比例我在模拟项目里第一次看到模型返回的坐标后,直接按原值去点"火箭"礼物图标,结果点击位置偏到了它旁边的"飞机"上。第一反应以为是模型识别错了,后来检查才发现是截图尺寸和设备逻辑分辨率不一致。排查方法是把模型输入的原始图片宽高打印出来,和driver.get_window_size()的宽高对比。如果不一致,按第三部分的换算公式处理。如果比例一致仍然有偏移,再看是不是状态栏导致的系统坐标偏移,解决方法是叠加一个固定补偿值。这个补偿值不要凭感觉调,最好的办法是做一个标定脚本:在屏幕上取左上、右上、左下、右下四个点,分别定位并调用模型返回坐标,反推出实际偏移量和缩放比例。4.2 验证码识别率波动:prompt与图像质量缺一不可验证码识别率低,第一个问题是图像被压缩。Appium 截图是 PNG,就算原图清晰,经过 Base64 传输到模型侧,模型内部也可能压缩图片。我实测过,验证码在原图里的区域如果很小,识别率会明显下降,尤其是字母数字混合的情况。解决办法是裁剪放大验证码区域再发送,如果验证码区域无法提前确定,就全屏截图后在提示词里让模型先检测验证码位置,这一步本身会稍微增加耗时,但准确率提升明显。第二个问题是 model 的随机性。虽然我在代码里设置了temperature=0,但某些模型对图片识别的输出仍存在波动。我的做法是同一个图跑两次识别,两次结果一致才采用;不一致则默认保留第一次结果并报一个警告。对于关键链路,还可以用两个不同模型投票,单次成本会上涨,但稳定性和成功率确实能拉到近乎百分百。第三个问题在 prompt 本身。原版提示词我只写了"识别验证码中字符",模型偶尔会把干扰线也当成字母输出。后来我在提示词里追加了一句"忽略背景线条、噪点和装饰性元素",识别率就稳定住了。提示词细节对结果的影响极大,多试几种表达,把最优版本固化下来,这也是维护这套方案时最省钱有效的手段。4.3 响应慢与成本失控:如何把单次识别成本压下来模型接口的速度通常在 1.5 到 3 秒之间,这个数字单独看不夸张,但几十个用例串行跑下来,执行时间直接翻倍。我做了几个针对性优化。首先是压缩图片尺寸。截图分辨率高,发送的 token 数量跟图片分辨率强相关。如果被测区域就是一个 300x200 的验证码,就没必要送全屏截图。我在调用前统一把目标区域缩放到宽 512 像素以内,既保证识别精度,又大幅降低 token 消耗。实测下来响应时间能减少三分之一以上。其次是缓存命中率的提升。前文说的lru_cache是关键,而且缓存键要设计得够细。同一个界面不同状态(比如选中和未选中)的控件位置可能不同,我就在目标描述里加上界面状态关键词,宁可多缓存几个变体,也不要因为缓存冲突导致点击错误。最后是超时控制。requests.post里的timeout参数我一开始没设,遇到过模型服务偶发卡住,导致整个用例挂五分钟的情况。现在统一设 30 秒超时,超时就重试一次,再超时直接走降级逻辑。这一个小小的改动,让整个自动化框架的稳定性提升了一个台阶。4.4 合规与安全:自动化测试里的验证码边界这块很容易被忽略,但我认为必须单独拿出来说。自动化测试的本质目标是保障产品质量,不是攻破业务的安全防线。所以涉及验证码的一切实操,都应该限定在测试环境里执行。如果你的验证码校验逻辑在测试环境无法关闭或绕过,优先推动开发在服务端加白名单,而不是依赖自动化脚本去硬闯。另外,测试过程中截取的屏幕截图很可能包含用户信息和业务数据。这些图片发送到外部模型服务时,要做脱敏和最小化处理。比如截图前先裁剪掉顶部的用户头像、手机号等区域,或者使用内部私有化部署的模型服务。数据合规问题在引入任何外部服务时都要提前评估,不要在问题出现之后才补救。5. 这套方案的后续扩展方向到这里,核心的验证码识别和复杂控件定位已经能稳定跑了,但这个架构能做的事情远不止这些。我把扩展思路简单梳理一下,大家可以根据自己的业务场景直接套用。第一个方向是视觉回归测试。现在截图识别链路已经通了,那么我可以把每次操作后的截图和上一次的基线截图做对比,多模态大模型来判断哪些像素级变化是预期的交互反馈,哪些是 UI 渲染异常。这比传统的像素 diff 工具智能得多,传统工具对滚动、弹窗、动态内容往往是误报大户。第二个方向是智能流程探索。Appium 传统的测试脚本需要人写死路径,而有了视觉加语义理解能力,可以让大模型根据当前界面截图决定下一步操作:看到登录页就自动找账号密码输入框和登录按钮,看到弹窗就找关闭按钮。这本质上就是基于视觉的智能回归探索,比 Monkey 那种随机乱点高效得多,因为每一步操作都是基于语义理解的。第三个方向是跨端一致性。同一套页面在 Android 和 iOS 上控件结构差异很大,但视觉表现应该是一致的。用截图加语义描述的方式,可以以视觉结果为基准,对两端进行一致性比对。这在业务方要求"两端体验一致"的场景下非常有用。最后一个值得提的方向是脚本自愈。跑自动化最烦的事情就是产品小版本迭代改了控件属性,脚本一夜之间全挂。如果每次定位不到控件时,自动切到视觉识别兜底,那脚本的鲁棒性会大幅提升,维护成本直线下降。我个人在实际项目中体会最深的一点是:这套方案的定位不是替代 Appium,而是给 Appium 补上一双"眼睛"。它把传统自动化方案的能力边界往前推了一大截,让以前根本不可能自动化的场景变成了可能。当然,模型成本、响应延迟和结果稳定性这些短板还是客观存在的,所以架构上一定要保留降级链路,不能把全部赌注压在一个模型上。如果你现在正被验证码和自定义控件折磨,建议先搭一个最小链路跑通核心流程,选一个你项目里最疼的场景动手试,效果验证之后再逐步铺开,慢慢就能摸出适合自己业务的那套边界了。