大语言模型驱动手机自动化:屏幕文本提取与视觉识别的混合策略实践
1. 项目概述:当大语言模型“看”懂手机屏幕
最近在折腾一个挺有意思的项目:让大语言模型(LLM)来驱动手机自动化操作。听起来像是科幻片里的场景,对吧?但实际做起来,你会发现核心挑战非常具体: 如何让模型“理解”手机屏幕上正在发生什么 。这是所有后续自动化操作(比如点击、滑动、输入文字)的基础。如果模型“看”错了,那后续所有指令都会跑偏。
目前,主流的思路有两种,正好对应了项目标题里的“屏幕文本”与“截图”。第一种是直接获取屏幕上的UI层级信息,提取出所有控件的文本、坐标、类型(比如按钮、输入框)。这相当于把屏幕的“源代码”喂给模型,告诉它:“这里有个‘登录’按钮,坐标是(200,300)”。第二种更直接,就是截一张图,把整个屏幕的视觉画面(一张JPG或PNG图片)传给模型,让它自己“看”图说话,识别出图中的元素和文字。
这两种方法孰优孰劣?在实际项目中,我花了大量时间对它们进行性能对比和失败分析。结果发现,没有绝对的赢家,只有适合不同场景的选择。文本提取速度快、精度高,但遇到游戏界面、自定义控件或者纯图片按钮就傻眼了;截图输入信息全面,模型能“看到”一切,但处理慢、成本高,而且对模型的“看图”能力要求极高,一不小心就识别错误。
这篇文章,我就以一个实际踩坑者的身份,拆解这两种方案的底层逻辑、实操细节,并分享那些在官方文档里找不到的“血泪教训”。无论你是想自己动手搭建一个自动化脚本,还是单纯好奇大语言模型如何与真实世界交互,相信这些实战经验都能给你带来启发。
2. 核心方案选型:文本提取 vs. 视觉识别
在开始写代码之前,我们必须搞清楚两种方案的根本差异,这决定了整个项目的技术栈和架构设计。这不是一个简单的“二选一”,而是基于不同需求的技术路径分叉。
2.1 方案一:屏幕文本提取(结构化数据输入)
这个方案的核心思想是绕过图像识别,直接从手机系统底层获取屏幕的“元数据”。在Android上,这通常通过 AccessibilityService (无障碍服务)或 UI Automator 来实现;在iOS上,则依赖于 XCUITest 框架。它们能获取到一个包含所有屏幕控件信息的XML树状结构(在Android上称为 UI Hierarchy )。
技术原理浅析 : 当你启动一个基于 UI Automator 的自动化脚本时,它就像是一个超级用户,可以查询系统:“当前屏幕最顶层是什么?它下面有哪些子视图?” 返回的数据是结构化的,例如一个按钮控件,你会得到它的 resource-id (如 com.example.app:id/login_button )、 text 属性(如“登录”)、 bounds 属性(描述它在屏幕上的矩形区域 [left, top, right, bottom] ),以及 clickable 、 enabled 等状态。我们将这些信息整理成一段结构化的文本描述,发送给大语言模型。
它的核心优势非常明显 :
- 速度快、成本低 :传输和处理的是一段很小的文本(通常几KB),相比动辄几百KB的图片,API调用延迟和token消耗(对于按token收费的模型API)可以忽略不计。
- 精度接近100% :文本内容、控件坐标是系统直接提供的,不存在识别错误。你可以明确告诉模型“点击resource-id为
com.example.app:id/ok的按钮”,这个指令是绝对精确的。 - 可操作性强 :获取到的
bounds坐标可以直接用于模拟点击、滑动等操作,无需额外的坐标转换。
然而,它的致命缺陷也同样突出 :
- 信息缺失 :对于纯图片按钮(没有
text属性,resource-id可能是无意义的哈希值)、游戏画面、视频播放界面、或是大量自定义绘制的控件,UI层级中可能没有任何有意义的文本信息。模型拿到一段只有android.view.View和一堆坐标的数据,根本无法理解该做什么。 - 依赖系统接口 :不同手机厂商、系统版本对无障碍服务的支持程度不同,可能会遇到权限限制、接口不一致或性能问题。
实操心得 :在决定使用文本提取方案前,务必先用
UI Automator Viewer或Appium Inspector等工具,连接你的目标手机和目标应用,查看其UI层级结构。如果关键交互元素(特别是按钮)都有清晰的text或content-desc属性,那么这个方案就是首选,效率极高。如果满眼都是android.widget.ImageView和android.view.View,那就要慎用。
2.2 方案二:屏幕截图输入(视觉语言模型输入)
当文本提取走不通时,我们只能让模型“亲眼看看”。这个方案就是定时或触发式地对手机屏幕进行截图,将截图图片输入给具有视觉理解能力的大语言模型(如GPT-4V、Gemini Pro Vision、Claude 3、或开源的LLaVA等),让模型描述图片内容并生成操作指令。
技术流程解析 :
- 截图捕获 :通过ADB命令(
adb exec-out screencap -p)、Appium的接口或手机系统自身的截图能力,获取当前屏幕的RGB图像。 - 图像预处理 :有时为了减少传输数据量和模型负担,会对截图进行压缩或裁剪。但要注意,过度压缩可能损失关键细节(如小号文字)。
- 视觉理解与指令生成 :将图片(通常以Base64编码形式)与精心设计的提示词(Prompt)一起发送给多模态大语言模型。Prompt需要明确指令,例如:“分析这张手机截图,描述所有可交互元素(如按钮、输入框)及其可能的功能,并根据我的目标‘找到并点击登录按钮’,给出下一步的操作建议和具体坐标。”
它的优势在于“全能” :
- 信息完备 :屏幕上的一切,无论是文字、图标、图片还是动态内容,都被包含在图像中。理论上,只要模型足够强,它就能理解任何界面。
- 跨平台一致性 :截图方法在Android、iOS乃至模拟器上基本一致,避开了系统底层接口的差异。
但代价是高昂的 :
- 性能瓶颈 :图片数据量大,传输耗时;多模态大模型推理速度远慢于纯文本模型;API调用成本也高出一个数量级。
- 识别不确定性 :模型可能会“看错”。比如把“立即购买”图标误认为“购物车”,或者对相似图标产生混淆。坐标预测也可能出现几个像素的偏差,在密集的UI中可能导致误触。
- 提示词工程复杂 :如何让模型稳定、准确地输出结构化的、可解析的操作指令(如
CLICK [x, y]),是一个巨大的挑战。模型可能会用自然语言描述,需要额外的后处理来解析。
2.3 混合策略:因地制宜的实践智慧
在实际项目中,纯用一种方案往往无法应对所有场景。我采用的是一种 动态混合策略 ,这也是性能对比后得出的最优解。
策略核心 :默认使用高效的屏幕文本提取方案。同时,设立一个“异常检测”机制。当从UI层级中提取到的有效文本信息过少(例如,超过80%的控件没有 text 或可理解的 content-desc ),或者模型基于文本信息连续多次都无法做出有效决策时,系统自动切换至截图模式,调用视觉模型进行“视觉救援”。
实现上的关键点 :
- 状态判断阈值 :需要根据具体应用设定阈值。比如,统计当前屏幕有意义的文本控件占比,低于30%则触发截图。
- 缓存与降级 :对于成功通过截图模式解析的界面,可以缓存其截图特征或解析结果。下次遇到类似界面时,可以尝试直接使用文本方案结合缓存信息进行匹配,减少对视觉模型的调用。
- 操作验证 :无论是哪种方案产生的操作指令(尤其是坐标),在执行后,都应该通过再次获取屏幕状态(文本或截图)来验证操作是否生效。例如,点击“登录”后,检查是否出现了登录成功的提示或页面跳转。如果没有,则触发重试或切换方案。
3. 技术实现细节与工具链搭建
理论说完了,我们来看看具体怎么干。这里我以Android平台为例,搭建一个基于Python的自动化测试框架,集成两种方案。
3.1 基础环境与工具准备
首先,你需要一个“桥梁”来连接电脑和手机,并执行命令。
- ADB (Android Debug Bridge) :这是基石。确保安装并配置好,手机开启USB调试模式,通过
adb devices能识别到设备。 - Python环境 :推荐3.8+版本。
- 核心Python库 :
pure-python-adb: 一个纯Python的ADB客户端库,比直接调用命令行更优雅。Pillow (PIL): 用于处理截图图片,如压缩、裁剪、保存。openai/anthropic/google-generativeai等:根据你选用的多模态大语言模型API选择对应的官方SDK。xml.etree.ElementTree: Python标准库,用于解析从UI Automator dump出来的XML文件。
项目目录结构建议 :
llm_mobile_auto/
├── config.yaml # 配置文件,存放API密钥、模型选择、阈值等
├── main.py # 主程序入口
├── core/
│ ├── __init__.py
│ ├── screen_capturer.py # 截图模块
│ ├── ui_extractor.py # UI文本提取模块
│ ├── llm_client.py # LLM客户端封装
│ └── action_executor.py # 动作执行模块(ADB点击、滑动)
├── prompts/ # 存放不同任务的提示词模板
│ └── vision_analysis_prompt.txt
└── utils/
└── image_utils.py # 图像处理工具函数
3.2 屏幕文本提取的实现
我们通过ADB命令获取UI层级信息。
# core/ui_extractor.py
import subprocess
import xml.etree.ElementTree as ET
from typing import List, Dict
class UIExtractor:
def __init__(self, device_id: str = None):
self.device_id = device_id
self.adb_prefix = ['adb'] if not device_id else ['adb', '-s', device_id]
def dump_ui_hierarchy(self) -> str:
"""获取当前屏幕的UI层级XML"""
# 将XML输出到设备的临时文件,再拉取到本地
tmp_file = '/sdcard/window_dump.xml'
subprocess.run(self.adb_prefix + ['shell', 'uiautomator', 'dump', tmp_file], capture_output=True)
subprocess.run(self.adb_prefix + ['pull', tmp_file, 'local_dump.xml'], capture_output=True)
with open('local_dump.xml', 'r', encoding='utf-8') as f:
xml_content = f.read()
# 清理临时文件
subprocess.run(self.adb_prefix + ['shell', 'rm', tmp_file])
return xml_content
def parse_ui_elements(self, xml_content: str) -> List[Dict]:
"""解析XML,提取关键控件信息"""
root = ET.fromstring(xml_content)
elements = []
# 遍历所有节点,寻找包含文本或可点击的节点
for node in root.iter('node'):
bounds = node.get('bounds')
text = node.get('text')
resource_id = node.get('resource-id')
clickable = node.get('clickable') == 'true'
enabled = node.get('enabled') == 'true'
# 只收集有文本或可点击且启用的元素,减少噪音
if (text and text.strip()) or (clickable and enabled):
# 解析bounds字符串 "[x1,y1][x2,y2]" 为坐标元组
if bounds:
coords = bounds.replace('[', '').replace(']', ',').split(',')
coords = [int(c) for c in coords if c]
center_x = (coords[0] + coords[2]) // 2
center_y = (coords[1] + coords[3]) // 2
else:
center_x, center_y = None, None
elements.append({
'text': text.strip() if text else '',
'resource_id': resource_id,
'bounds': bounds,
'center': (center_x, center_y),
'clickable': clickable,
'class': node.get('class')
})
return elements
def get_screen_description(self) -> str:
"""生成给LLM的屏幕文本描述"""
xml = self.dump_ui_hierarchy()
elements = self.parse_ui_elements(xml)
description_lines = ["当前屏幕有以下可交互元素:"]
for idx, elem in enumerate(elements):
desc = f"{idx+1}. "
if elem['text']:
desc += f"文本: '{elem['text']}', "
if elem['resource_id']:
desc += f"ID: {elem['resource_id']}, "
desc += f"类型: {elem['class']}, 可点击: {elem['clickable']}"
if elem['center']:
desc += f", 中心坐标: ({elem['center'][0]}, {elem['center'][1]})"
description_lines.append(desc)
return "\n".join(description_lines)
注意事项 :
uiautomator dump命令在某些系统或界面下可能失败或返回空内容。务必增加异常处理,并准备一个备用的截图方案。此外,解析出的坐标是屏幕绝对坐标,在不同分辨率设备上需要做归一化处理(如果模型需要的话),但执行点击时直接用绝对坐标即可。
3.3 屏幕截图与视觉识别的实现
接下来是截图和调用多模态模型的环节。
# core/screen_capturer.py
import subprocess
from PIL import Image
import io
import base64
class ScreenCapturer:
def __init__(self, device_id: str = None):
self.device_id = device_id
self.adb_prefix = ['adb'] if not device_id else ['adb', '-s', device_id]
def capture_to_bytes(self) -> bytes:
"""通过ADB截图并返回字节流"""
# 使用exec-out直接获取二进制流,效率比先存文件再pull高
result = subprocess.run(self.adb_prefix + ['exec-out', 'screencap', '-p'], capture_output=True)
return result.stdout
def capture_to_image(self) -> Image.Image:
"""截图并返回PIL Image对象"""
img_bytes = self.capture_to_bytes()
# 注意:ADB screencap的原始格式可能需要处理(如去除头部信息),但现代设备通常直接输出png
return Image.open(io.BytesIO(img_bytes))
def capture_and_encode(self, max_size: tuple = (800, 1600)) -> str:
"""截图,调整大小(保持比例),并编码为Base64字符串"""
img = self.capture_to_image()
# 调整图片尺寸,减少传输量和模型处理负担
img.thumbnail(max_size, Image.Resampling.LANCZOS)
buffered = io.BytesIO()
img.save(buffered, format="PNG", optimize=True)
img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
return img_base64
# core/llm_client.py
import yaml
from openai import OpenAI # 以OpenAI GPT-4V为例
class VisionLLMClient:
def __init__(self, config_path: str = 'config.yaml'):
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
self.api_key = config['openai']['api_key']
self.model = config['openai'].get('vision_model', 'gpt-4-vision-preview')
self.client = OpenAI(api_key=self.api_key)
def analyze_screen(self, image_base64: str, user_prompt: str) -> str:
"""调用多模态模型分析截图"""
# 从文件读取提示词模板,并注入用户的具体目标
with open('prompts/vision_analysis_prompt.txt', 'r') as f:
system_prompt = f.read()
full_prompt = system_prompt + "\n用户当前目标是:" + user_prompt
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": [
{"type": "text", "text": full_prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{image_base64}"
}
}
]
}
],
max_tokens=500,
)
return response.choices[0].message.content
提示词工程(prompts/vision_analysis_prompt.txt)示例 :
你是一个手机自动化助手。你需要分析用户提供的手机屏幕截图,并给出下一步操作建议。
请严格按照以下格式输出:
1. **屏幕描述**:用一句话简要描述当前屏幕的主要内容(例如:“这是一个微信的聊天列表页面”)。
2. **可交互元素**:列出图中所有可能可点击或输入的元素,包括按钮、输入框、图标等。对每个元素,描述其外观、位置(用上下左右中、左上区域等相对位置描述)和可能的功能。
3. **操作建议**:根据“用户当前目标”,给出具体的下一步操作。操作必须是可执行的,例如:
- 如果目标是点击某个按钮,请输出:`ACTION: CLICK [元素描述,如‘绿色的‘登录’按钮’]`。
- 如果目标是输入文本,请输出:`ACTION: INPUT [元素描述,如‘顶部的搜索框’] [要输入的文本]`。
- 如果需要滑动,请输出:`ACTION: SWIPE [起始区域] [方向] [距离,如‘短距离’/‘长距离’]`。
4. **坐标估算(可选)**:如果你能非常确定元素的位置,可以提供其屏幕中心的大致坐标,格式为:`COORD: (x, y)`。注意:坐标必须基于图片像素位置估算,x范围0~[图片宽度],y范围0~[图片高度]。如果不确定,请不要提供。
请确保输出清晰、结构化,便于程序解析。
3.4 动作执行与决策循环
最后,我们需要一个大脑来协调文本提取、视觉分析和动作执行。
# main.py 简化示例
from core.ui_extractor import UIExtractor
from core.screen_capturer import ScreenCapturer
from core.llm_client import VisionLLMClient
from core.action_executor import ActionExecutor
import re
import time
class MobileAutoAgent:
def __init__(self):
self.ui_extractor = UIExtractor()
self.screen_capturer = ScreenCapturer()
self.vision_llm = VisionLLMClient()
self.action_executor = ActionExecutor()
self.last_action = None
self.failure_count = 0
def run_task(self, ultimate_goal: str):
"""执行一个自动化任务"""
current_goal = ultimate_goal
max_steps = 20
for step in range(max_steps):
print(f"\n=== 步骤 {step+1} ===")
print(f"当前目标: {current_goal}")
# 1. 尝试文本提取方案
print("正在通过UI层级分析屏幕...")
text_description = self.ui_extractor.get_screen_description()
# 简单启发式判断:如果可交互元素太少或没有文本,则视为文本方案信息不足
if len(text_description.split('\n')) < 3: # 举例阈值
print("文本信息不足,切换到视觉分析。")
analysis_result = self._analyze_by_vision(current_goal)
else:
# 将文本描述和目标一起发送给纯文本LLM(此处简化,实际需调用文本模型)
# 假设我们有一个函数能基于文本做决策
analysis_result = self._analyze_by_text(text_description, current_goal)
# 2. 解析LLM返回的指令
action, coord = self._parse_llm_output(analysis_result)
if not action:
print("无法解析LLM输出,尝试视觉分析。")
analysis_result = self._analyze_by_vision(current_goal)
action, coord = self._parse_llm_output(analysis_result)
# 3. 执行动作
if action and coord:
print(f"执行: {action} 于坐标 {coord}")
self.action_executor.click(coord[0], coord[1])
self.last_action = (action, coord)
self.failure_count = 0
time.sleep(2) # 等待界面响应
# 4. 验证/更新目标 (此处简化)
# 可以再次获取屏幕状态,判断目标是否达成,并更新current_goal
# 例如,如果目标是“登录”,点击登录按钮后,current_goal可更新为“检查是否登录成功”
else:
print("未能生成有效动作指令。")
self.failure_count += 1
if self.failure_count > 3:
print("连续失败多次,任务终止。")
break
# 简单模拟:假设任务完成
if step > 5: # 示例终止条件
print("任务流程执行完毕。")
break
def _analyze_by_vision(self, goal: str) -> str:
"""使用视觉模型分析"""
image_b64 = self.screen_capturer.capture_and_encode()
return self.vision_llm.analyze_screen(image_b64, goal)
def _parse_llm_output(self, output: str):
"""从LLM输出中解析动作和坐标(简单正则示例)"""
action_match = re.search(r'ACTION:\s*(CLICK|INPUT|SWIPE)\s*(.*)', output)
coord_match = re.search(r'COORD:\s*\((\d+),\s*(\d+)\)', output)
action = action_match.group(1) if action_match else None
action_desc = action_match.group(2).strip() if action_match else None
coord = (int(coord_match.group(1)), int(coord_match.group(2))) if coord_match else None
# 如果没有提供坐标,但有点击动作描述,可以尝试从之前的UI元素中匹配或使用启发式方法估算(此处简化)
if action == 'CLICK' and not coord and action_desc:
# 这里可以加入逻辑:例如,如果action_desc包含“登录”,则去UI元素列表中寻找文本包含“登录”的元素的坐标
pass
return action, coord
if __name__ == '__main__':
agent = MobileAutoAgent()
agent.run_task("打开设置,然后进入WLAN页面")
4. 性能对比实测与数据分析
纸上谈兵终觉浅,我搭建了测试环境,对两种方案进行了定量和定性的对比测试。测试环境为一台中端安卓手机(分辨率1080x2400),电脑通过USB连接,使用GPT-4 Turbo(文本)和GPT-4V(视觉)作为后端模型。
4.1 响应延迟对比
我选取了三种典型页面进行测试,每个页面操作10次,取平均值:
| 页面类型 | 文本提取+LLM推理总耗时 | 截图+视觉LLM推理总耗时 | 视觉方案慢的倍数 |
|---|---|---|---|
| 标准列表页 (微信通讯录) | 1.2 - 1.8 秒 | 8 - 15 秒 | 5x - 8x |
| 图文混合页 (小红书信息流) | 1.5 - 2.5 秒 | 10 - 18 秒 | 6x - 7x |
| 游戏/全屏应用 (简单游戏界面) | 2.0+ 秒(信息少,LLM困惑) | 12 - 20 秒 | 6x - 10x |
分析 :
- 文本方案延迟主要来自 :
uiautomator dump和拉取XML文件(约0.5-1秒)+ 文本LLM API调用(约0.5-1秒)。延迟稳定且较低。 - 视觉方案延迟主要来自 :截图与传输(约0.5秒)+ 图片Base64编码(约0.2秒)+ 视觉LLM API调用(占大头,7-15秒) 。延迟高且波动大,受图片复杂度、网络、API负载影响显著。
实操心得 :如果你追求的是接近人类操作的流畅速度(比如做自动化测试),文本方案是唯一可行的选择。视觉方案的延迟在复杂任务链中会累积成难以忍受的等待时间。视觉方案更适合作为“备用侦察兵”,在文本方案失效时,执行单次的关键识别。
4.2 识别准确率与指令可用性对比
准确率不能只看模型“说了什么”,更要看它输出的指令能否被 正确解析并成功执行 。
| 测试场景 | 文本方案成功率 | 视觉方案成功率 | 主要失败原因 |
|---|---|---|---|
| 标准按钮点击 (“登录”、“确定”、“下一步”) | ~99% | ~85% | 文本:几乎无失败。视觉:图标相似误认、坐标估算偏差。 |
| 图标识别 (购物车、搜索、分享) | <10% | ~75% | 文本:图标通常无文本,resource-id可能无意义。视觉:主流图标识别较好,小众或设计独特的图标易错。 |
| 文字输入 | ~95% | ~70% | 文本:能精确定位输入框。视觉:可能将提示文字误判为已输入内容,或定位不准。 |
| 动态内容 (视频播放器控件、滚动列表) | ~30% | ~65% | 文本:动态控件可能不在UI树中或状态变化快。视觉:能“看到”但控件状态(如播放/暂停)可能识别错误。 |
关键发现 :
- 文本方案的“可用性”严重依赖应用开发规范 。对于遵循Material Design、控件属性完善的主流App,其成功率极高。但对于游戏、Hybrid App(WebView)或某些金融类App的自定义控件,成功率断崖式下跌。
- 视觉方案的准确率非常依赖提示词和后续解析 。模型可能会说“点击右下角的蓝色发送箭头”,但你需要将“右下角”转换为具体坐标。这个转换过程(通过描述估算坐标)本身就会引入误差。我尝试让模型直接输出坐标,但其稳定性不如输出描述。
- 混合策略效果显著 :在测试中,采用“文本优先,视觉兜底”的混合策略,整体任务成功率从纯文本方案的~70%提升到了~92%,而平均耗时只比纯文本方案增加了约15%(因为大部分步骤仍走文本通道)。
4.3 成本分析
对于使用商用API的开发者,成本是必须考虑的。
- 文本方案 :以GPT-4 Turbo为例,一次包含屏幕描述和指令生成的交互,大约消耗500-1000 tokens。按$10 / 1M tokens输入计算,单次成本约**$0.005-$0.01**。
- 视觉方案 :以GPT-4V为例,处理一张1080x2400的压缩截图(约800x1600,低细节模式),单次调用成本约**$0.01-$0.03**(取决于图片token数量)。
结论 :视觉方案的单次调用成本是文本方案的2-6倍。在需要高频交互的自动化流程中,这个成本差异会被急剧放大。
5. 典型失败案例分析与避坑指南
在实际运行中,你会遇到各种光怪陆离的失败情况。下面分享几个典型案例和解决思路。
5.1 案例一:文本方案遭遇“透明”控件
现象 :在一个新闻App中,需要点击文章底部的“点赞”心形图标。UI层级显示该区域是一个 FrameLayout ,其下有一个 ImageView ,但 text 为空, content-desc 也为空, resource-id 是一串类似 com.xxx:id/abc 的自动生成ID。文本方案无法理解这是个点赞按钮。
根因 :这是非常常见的开发模式,图标作为图片资源嵌入,没有添加无障碍标签( contentDescription )。
解决方案 :
- 混合策略触发 :当解析到当前屏幕有大量无文本的
ImageView且与目标相关时,自动切换到视觉分析。 - 图像特征缓存 :首次通过视觉模型成功识别并点击“点赞”图标后,记录下该图标出现的大致屏幕区域(例如,在文章底部区域)以及其相邻的文本控件(如“分享”、“评论”)。下次在类似位置看到相似的UI布局时,即使没有文本信息,也可以根据上下文和位置历史进行概率性点击。
- 资源ID映射表 :对于固定的App,可以手动或半自动地建立一个
resource-id到功能的映射表。虽然id看似随机,但在同一App版本内,特定功能的id是固定的。通过前期“训练”记录下“点赞”按钮的resource-id,后续就可以直接使用。
5.2 案例二:视觉模型“指鹿为马”
现象 :让模型在电商App首页点击“购物车”图标。截图中有顶部导航栏的购物车图标,也有底部Tab栏的购物车图标。模型错误地指示点击了底部Tab栏的图标(该图标已处于选中状态,颜色不同),而实际需要的是顶部的。
根因 :多模态模型对UI状态(选中/未选中)的感知能力较弱,更侧重于图标形状和文本标签的识别。提示词中未强调“非选中状态的”或“位于顶部的”等区分性信息。
解决方案 :
- 精细化提示词 :在Prompt中明确要求模型注意元素的状态和精确位置。例如:“请注意元件的视觉状态,如颜色深浅、是否高亮,这通常表示是否已被选中。请优先选择未选中状态的目标元件。”
- 后处理校验 :执行点击后,再次截图,让模型分析操作结果。如果目标状态未发生预期变化(例如,顶部购物车图标未被点击),则判定为操作失败,触发重试或尝试点击另一个候选目标。
- 结合文本上下文 :即使在视觉模式下,也可以将UI层级中有限的文本信息(如顶部导航栏的标题文字“首页”)一并提供给模型,帮助其进行区域定位。
5.3 案例三:坐标漂移与点击无效
现象 :模型输出的坐标 (500, 1200) ,但实际执行 adb shell input tap 500 1200 后没有任何反应。
根因 :
- 坐标系统差异 :模型估算的坐标是基于你提供的截图分辨率。如果你截图后进行了缩放,但未将坐标等比映射回手机屏幕的实际分辨率,就会出错。
- 控件点击区域 :有些控件的可点击区域(
clickable区域)小于其视觉边界。点击在视觉中心可能无效,需要点击在特定子控件上。 - 界面延迟加载 :点击时,目标控件可能尚未加载完成或处于动画过渡中。
解决方案 :
- 坐标映射 :务必记录截图的原始尺寸和手机屏幕的实际分辨率。执行点击前,将模型返回的坐标按比例进行转换:
实际x = 模型x * (屏幕宽 / 截图宽),实际y同理。 - 使用UI Automator进行点击 :不要总是用
input tap。如果通过文本方案已经定位到控件,优先使用基于resource-id或bounds的精准点击命令,如adb shell uiautomator click <resource-id>,这能直接触发控件的点击事件,避开了坐标问题。 - 增加重试与等待 :点击操作后,加入显式等待(如
time.sleep(1)),并检查屏幕是否变化。未变化则重试。对于网络加载慢的界面,需要更智能的等待条件,比如轮询直到某个特定元素出现。
5.4 稳定性优化与经验总结
经过大量测试,我总结出几条提升整体稳定性的关键经验:
- 降级与超时机制 :为每个LLM API调用设置严格的超时(如10秒)。超时后,自动降级到更简单的规则引擎或预定义的操作序列。
- 操作原子化与状态检查 :将每个“目标-动作”对设计得尽可能小。例如,“登录”不是一个原子操作,它应拆分为“输入用户名”、“输入密码”、“点击登录按钮”、“验证登录成功”等多个步骤。每个步骤前后都要进行屏幕状态检查,确保上一步已生效再执行下一步。
- 引入确定性备份路径 :对于关键流程(如App启动后的首页导航),不要完全依赖LLM。可以预先配置一些基于
resource-id或固定坐标的确定性操作作为备份路径。当LLM连续失败时,启用备份路径将流程拉回正轨。 - 日志与回放 :详细记录每一步的屏幕信息(文本或截图摘要)、LLM的决策依据、执行的操作和结果。这不仅能用于调试,还可以作为数据反哺,用于优化提示词或训练更专用的模型。
大语言模型驱动手机自动化,目前仍处于“辅助”和“探索”阶段。纯视觉方案受限于成本、速度和精度,难以承担高频任务;而纯文本方案则受制于应用的可访问性。将两者结合,以文本为主、视觉为辅,并辅以传统的自动化脚本逻辑,是当前最务实、最高效的路径。这个项目的价值不仅在于实现自动化本身,更在于为我们理解如何让大模型与复杂的物理世界(在这里是数字界面)进行稳定、可靠的交互,提供了一个绝佳的试验场。每一次失败的分析,都在为未来更智能的Agent积累宝贵的经验。
更多推荐
所有评论(0)