AppAgent:基于多模态大模型的手机自动化操作实践指南
1. 项目概述当AI学会“点按”你的手机最近在GitHub上看到一个挺有意思的项目叫“AppAgent”来自腾讯QQGYLab。光看名字你可能觉得这又是一个普通的AI应用框架。但它的核心玩法让我这个搞了十几年自动化测试和智能交互的老兵都眼前一亮——它让一个大语言模型LLM真正学会了“操作”手机App不是通过API接口而是像真人一样通过“看”屏幕截图和“模拟”点击、滑动等触控手势来完成任务。简单来说AppAgent是一个基于多模态大模型的智能手机任务自动化代理。它不要求App提供任何特殊的接口或权限仅通过视觉感知和理解就能自主规划并执行一系列操作比如帮你点外卖、订机票、刷短视频甚至完成一些复杂的多步骤工作流。这背后的核心是把LLM的规划决策能力与计算机视觉CV的屏幕理解能力以及精准的UI控件操作能力结合在了一起。对于开发者、测试工程师或者任何想研究AI如何与真实世界交互的人来说这都是一块绝佳的“试验田”。2. 核心设计思路从“看到”到“做到”的智能闭环AppAgent的设计哲学非常清晰模拟人类使用手机App的完整认知-行动循环。我们人类操作一个App无非是“看到界面 - 理解要做什么 - 找到可操作的元素 - 执行操作 - 观察结果”这样一个循环。AppAgent将这个循环拆解为三个核心模块并让大语言模型担任“大脑”来协调一切。2.1 视觉感知模块给AI一双“眼睛”这是整个系统的输入起点。AppAgent会定期捕获手机的当前屏幕截图。但这不仅仅是拍张照片那么简单关键在于后续的屏幕解析Screen Parsing。为什么需要解析屏幕直接给LLM看一张完整的、像素级的截图信息过于冗余和底层。LLM更擅长处理结构化的、语义化的信息。因此AppAgent通常会将截图送入一个预先训练好的UI元素检测模型例如基于目标检测的模型识别出屏幕上所有的可交互控件如按钮、输入框、开关、列表项和静态文本。每个被识别出的元素都会被赋予一个结构化的描述通常包括类型TypeButton, TextField, Image, CheckBox等。文本内容Text按钮上显示的文字。边界框坐标Bounding Box在屏幕上的具体位置。可能的状态State如是否被选中、是否可点击。最终系统会生成一个类似于HTML DOM树或可访问性树Accessibility Tree的结构化屏幕描述这份描述才是真正喂给LLM的“视觉信息”。这大大降低了LLVLMLarge Language and Vision Model的理解负担让它能快速聚焦于“哪里可以点”、“哪里可以输入”。注意屏幕解析的准确性直接决定了后续操作的成败。如果模型把“登录”按钮识别成了图片或者漏掉了某个关键选项整个任务链就可能中断。在实际部署中可能需要针对特定App的UI风格对解析模型进行微调。2.2 任务规划与决策模块AI的“大脑”这是整个系统的核心由大语言模型如GPT-4V, Claude 3, 或开源的Qwen-VL等担任。LLM接收来自“眼睛”的结构化屏幕信息以及用户用自然语言下达的指令例如“在美团上帮我订一份附近评分最高的披萨要芝士加倍”。LLM需要完成以下几项关键工作任务分解将复杂的用户指令拆解成一系列原子操作步骤。例如“订披萨”可以分解为打开美团App - 搜索“披萨” - 按评分排序 - 选择第一家店 - 选择“芝士加倍”口味 - 加入购物车 - 下单支付。当前状态理解结合屏幕信息判断当前处于哪个App的哪个页面以及可用的操作有哪些。例如识别出当前是美团首页有“搜索框”、“外卖入口”等。下一步动作预测根据任务目标和当前状态决定下一步最应该执行哪个原子操作。例如当前在首页下一步应该是“点击搜索框”。动作参数生成为选定的操作生成具体参数。例如对于“点击”操作需要指定点击哪个控件通过控件的描述或索引对于“输入”操作需要指定输入的文本内容。这里的关键在于提示词Prompt工程。给LLM的Prompt需要精心设计包含清晰的系统角色设定“你是一个手机操作助手”、当前屏幕的详细描述、操作历史记录、可用的动作指令集如tap(component_id),swipe(direction),input(text)以及严格的输出格式要求例如必须输出JSON格式的{“action”: “tap”, “target”: “搜索按钮”}。好的Prompt能极大地提升LLM决策的准确性和稳定性。2.3 动作执行模块AI的“手”决策模块输出一个具体的动作指令后就需要由执行模块来落到实处。这通常通过操作系统提供的自动化测试框架来实现例如Android使用UIAutomator2或Appium它们可以获取屏幕控件信息并模拟点击、滑动、输入等操作。iOS使用XCUITest。执行模块的工作是指令映射将LLM输出的抽象指令如tap(“搜索按钮”)映射到具体的UI控件上。这可能需要通过文本匹配、控件类型匹配或者在结构化描述中预定义的唯一ID来实现。执行操作调用底层框架的API在控件对应的屏幕坐标上执行点击、或注入文本。结果确认与等待操作执行后系统需要等待App响应和界面刷新。这里通常会设置一个超时时间并可能再次触发截图开启下一个决策-执行循环。一个重要的细节是操作的容错性。由于屏幕解析和LLM决策都可能出错执行模块需要具备一定的重试和回退机制。例如点击后如果界面长时间无变化或跳转到了非预期的页面系统可能需要记录这个“异常状态”并反馈给LLM由LLM决定是重试、执行返回操作还是报告失败。3. 关键技术细节与实操要点理解了宏观架构我们深入到几个决定项目成败的技术细节中。这些地方往往是论文里一笔带过但实际做起来坑最多的地方。3.1 多模态模型的选择与调优AppAgent的性能天花板很大程度上取决于其“大脑”——多模态大模型的能力。模型需要同时具备强大的视觉理解看懂UI布局和元素、文本理解理解用户指令和历史操作、以及推理规划能力。开源与闭源模型的权衡闭源模型如GPT-4V, Claude 3通常能力最强在复杂场景理解和多步骤规划上表现优异提示词遵循性好。缺点是API调用有成本和延迟且数据隐私需要考虑。开源模型如Qwen-VL-Chat, InternVL, LLaVA-NeXT可私有化部署数据安全可控无使用成本。但同等参数规模下其视觉-语言对齐能力和复杂推理能力可能稍逊于顶级闭源模型需要更精细的提示词工程和可能的后处理。实操建议对于研究和实验可以从GPT-4V的API开始快速验证想法和流程。当流程跑通后可以尝试用开源的Qwen-VL系列进行替代和优化。Qwen-VL支持中英文对GUI元素的理解能力在开源模型中属于第一梯队。你需要准备一个高质量的GUI指令微调数据集其中包含大量屏幕截图 结构化描述 操作指令的三元组对开源模型进行有监督微调SFT可以显著提升其在手机操作任务上的表现。3.2 结构化屏幕描述的生成策略如前所述直接将像素截图给LLM效率低下。生成高质量的结构化描述是关键。这里有几种主流策略基于CV的控件检测使用目标检测模型如YOLO系列、DETR训练一个专门的UI控件检测器。你需要收集大量App截图并标注其中控件的类别和位置。这种方法能获得精确的坐标但标注成本高且对于动态生成、样式多变的控件泛化能力可能不足。基于可访问性服务Android和iOS都提供了可访问性Accessibility接口可以直接获取当前屏幕的控件树信息包括类型、文本、坐标、是否可点击等。这是最准确、最可靠的方式因为它获取的是操作系统层面的真实控件信息而非视觉猜测。强烈建议在实际项目中优先采用此方法。在Android上这就是UIAutomator或Appium底层使用的技术。混合方法结合1和2。以可访问性信息为主对于其中文本缺失或信息不全的控件如图标按钮再用CV模型进行辅助识别和补充描述。一个核心技巧信息压缩与摘要。即使拿到了完整的可访问性树它也可能非常庞大一个列表页可能有上百个条目。直接全部塞给LLM会浪费上下文窗口并引入噪声。需要设计一个过滤和摘要算法。例如过滤掉不可见、不可交互的控件。对列表项进行分组摘要如“一个包含20个商品的列表前五项是...”。优先保留屏幕中央、面积大、带有醒目文本的控件信息。 这样传递给LLM的屏幕描述才既精简又包含了关键信息。3.3 动作空间的定义与探索策略LLM需要在一个定义好的“动作空间”里选择操作。一个设计良好的动作空间应该完备且粒度适中。基本动作通常包括tap(element_id): 点击。long_press(element_id): 长按。swipe(start_element, end_element)或swipe(direction, distance): 滑动。input(element_id, text): 输入文本。back(): 返回。home(): 回到桌面。scroll(direction): 滚动可视为特定方向的滑动。高级动作可以考虑multi_tap([element_id1, element_id2, ...]): 多点触控较少用。wait(condition): 条件等待直到某个元素出现或消失。探索策略Exploration Strategy 当LLM面对一个从未见过的新App或新页面时它可能需要尝试性操作来探索功能。一种简单的策略是让LLM优先尝试点击那些看起来像“导航按钮”如底部Tab栏或“行动号召按钮”如“下一步”、“确定”的控件。更高级的策略可以引入强化学习让Agent通过试错来学习App的状态转移模型但这会大大增加复杂度。实操心得在项目初期不要追求过于复杂的动作空间。先把tap,input,swipe,back这几个核心动作做稳定。很多复杂操作都可以由这几个基本动作组合而成。动作的稳定性即每次都能准确点击到目标比动作种类的丰富性更重要。4. 从零搭建一个简易AppAgent的实操流程理论说了这么多我们动手搭一个最简单的原型来切身感受一下各个环节。这里我们以Android平台为例使用Python作为主语言。4.1 环境准备与依赖安装首先你需要一台Android手机或模拟器并开启开发者选项和USB调试。然后在你的电脑上安装必要的工具和库。# 1. 安装Android开发工具主要是adb # 去Android开发者官网下载Platform-Tools并配置环境变量。 # 2. 安装Python依赖 pip install opencv-python-headless # 用于图像处理 pip install pillow # 图像处理 pip install uiautomator2 # Android UI自动化核心库 pip install openai # 如果你使用GPT-4V API # 或者安装 transformers, qwen-vl 等如果你使用开源模型关键工具解释adb (Android Debug Bridge)与手机通信的瑞士军刀用于截图、安装App等。uiautomator2一个非常优秀的Python库它封装了Android的UIAutomator测试框架。我们可以用它来获取屏幕的控件信息可访问性树和模拟操作。它比纯adb操作更友好、更强大。4.2 核心模块代码实现我们创建三个Python文件分别对应三个核心模块。1. 视觉感知模块 (screen_parser.py)这个模块负责获取当前屏幕的结构化信息。import uiautomator2 as u2 import cv2 import json class ScreenParser: def __init__(self, device_serialNone): # 连接设备 self.d u2.connect(device_serial) if device_serial else u2.connect() def get_screen_info(self): 获取当前屏幕的截图和控件树信息 # 1. 截图 screenshot_path self.d.screenshot(formatopencv) # 返回numpy数组 # 或者保存到文件: self.d.screenshot().save(screen.png) # 2. 获取控件树可访问性信息 # dump_hierarchy() 返回XML格式的控件树 hierarchy self.d.dump_hierarchy() # 3. 解析XML提取关键信息 # 这里简化处理实际需要解析XML提取每个node的attribute # 如resource-id, text, class, bounds, clickable, scrollable等 from xml.etree import ElementTree as ET root ET.fromstring(hierarchy) elements [] for node in root.iter(node): elem_info { bounds: node.get(bounds), # 格式 [x1,y1][x2,y2] text: node.get(text) or , resource-id: node.get(resource-id) or , class: node.get(class), # 如 android.widget.Button clickable: node.get(clickable) true, scrollable: node.get(scrollable) true, # ... 其他属性 } # 过滤掉无意义或不可见的控件 if elem_info[text] or elem_info[resource-id] or elem_info[clickable]: elements.append(elem_info) # 4. 对elements进行简化摘要例如合并列表项 simplified_elements self._summarize_elements(elements) return { screenshot_array: screenshot_path, # 可选如果后续模型需要像素图 elements: simplified_elements, raw_hierarchy: hierarchy # 可选备用 } def _summarize_elements(self, elements): 简化控件列表例如将长列表折叠为摘要描述 # 这是一个简化的示例如果连续多个同类、同结构的元素进行合并 summarized [] i 0 while i len(elements): elem elements[i] # 假设我们判断是否为列表项这里逻辑非常简化 if ListView in elem.get(class, ) or RecyclerView in elem.get(parent_class, ): list_items [] # 尝试收集相似项 while i len(elements) and self._is_similar_item(elem, elements[i]): list_items.append(elements[i][text]) i 1 if list_items: summarized.append({ type: list_summary, content: f一个列表包含{len(list_items)}项前几项为{, .join(list_items[:3])}... }) continue summarized.append(elem) i 1 return summarized def _is_similar_item(self, elem1, elem2): # 简单的相似性判断实际应用需要更复杂的逻辑 return elem1.get(class) elem2.get(class) and elem1.get(bounds, ).split(][)[0] elem2.get(bounds, ).split(][)[0]2. 任务规划与决策模块 (llm_brain.py)这个模块调用LLM根据屏幕信息和用户指令做出决策。import openai # 或 from transformers import AutoModelForCausalLM, AutoTokenizer class LLMBrain: def __init__(self, api_keyNone, modelgpt-4-vision-preview): # 使用OpenAI API示例 self.client openai.OpenAI(api_keyapi_key) self.model model def decide_next_action(self, user_instruction, screen_info, action_history): 决定下一步动作 # 构建Prompt prompt self._build_prompt(user_instruction, screen_info, action_history) # 调用LLM response self.client.chat.completions.create( modelself.model, messages[ {role: system, content: 你是一个智能手机助手通过解析屏幕和操作历史来完成任务。请只输出JSON格式的指令。}, {role: user, content: prompt} ], temperature0.1, # 低温度保证输出稳定 max_tokens500 ) # 解析响应 import json try: action_cmd json.loads(response.choices[0].message.content) return action_cmd except json.JSONDecodeError: print(LLM返回了非JSON格式:, response.choices[0].message.content) # 可以尝试用正则表达式提取或返回一个安全的后退动作如back() return {action: back} def _build_prompt(self, instruction, screen_info, history): # 将screen_info[elements]格式化成易读的文本 elements_text for idx, elem in enumerate(screen_info[elements]): if elem.get(type) list_summary: elements_text f{idx}. [列表摘要] {elem[content]}\n else: desc f{elem.get(text, )} ({elem.get(class, )}) if elem.get(clickable): desc [可点击] elements_text f{idx}. {desc}\n # 格式化历史记录 history_text \n.join([f{i1}. {h} for i, h in enumerate(history[-5:])]) # 只保留最近5条 prompt f 用户指令{instruction} 当前屏幕上的主要元素 {elements_text} 最近的操作历史 {history_text if history_text else 无} 请根据以上信息决定下一步操作。你只能从以下动作中选择一个 - tap(index): 点击屏幕上第[index]个元素索引从0开始。 - input(index, text): 向第[index]个元素输入文本text。 - swipe(up/down/left/right): 向指定方向滑动屏幕。 - back(): 返回上一页。 - home(): 返回桌面。 - wait(): 等待2秒。 请严格按以下JSON格式输出不要有任何额外解释 {{action: 动作名称, target: 目标索引或方向, text: 可选输入文本}} 例如{{action: tap, target: 3}} 或 {{action: input, target: 1, text: hello}} 你的决策 return prompt3. 动作执行模块 (action_executor.py)这个模块负责将LLM的决策转化为真实的手机操作。import uiautomator2 as u2 import time class ActionExecutor: def __init__(self, device_serialNone): self.d u2.connect(device_serial) if device_serial else u2.connect() def execute(self, action_cmd, screen_elements): 执行动作命令 action action_cmd.get(action) target action_cmd.get(target) input_text action_cmd.get(text) if action tap: # 根据索引找到对应的控件bounds if isinstance(target, int) and 0 target len(screen_elements): elem screen_elements[target] bounds elem.get(bounds) if bounds: # 解析bounds字符串 [x1,y1][x2,y2]计算中心点 import re coords re.findall(r\d, bounds) if len(coords) 4: x1, y1, x2, y2 map(int, coords) center_x, center_y (x1 x2) // 2, (y1 y2) // 2 self.d.click(center_x, center_y) print(f点击了坐标 ({center_x}, {center_y}) 对应元素{elem.get(text)}) return True print(f点击目标索引 {target} 无效或未找到坐标。) elif action input: if isinstance(target, int) and 0 target len(screen_elements) and input_text: # uiautomator2 的set_text方法通常需要resource-id或文本定位 elem screen_elements[target] # 这里简化处理先点击输入框再通过adb输入文本 # 实际应用中最好用resource-id定位 resource_id elem.get(resource-id) if resource_id: self.d(resourceIdresource_id).set_text(input_text) else: # 备用方案点击后使用adb输入 self.execute({action: tap, target: target}, screen_elements) time.sleep(0.5) self.d.clear_text() self.d.send_keys(input_text) print(f向元素 {target} 输入了文本{input_text}) return True elif action swipe: directions {up: (0.5, 0.8, 0.5, 0.2), down: (0.5, 0.2, 0.5, 0.8), left: (0.8, 0.5, 0.2, 0.5), right: (0.2, 0.5, 0.8, 0.5)} if target in directions: sx, sy, ex, ey directions[target] self.d.swipe(sx, sy, ex, ey) print(f向{target}方向滑动) return True elif action back: self.d.press(back) print(执行返回) return True elif action home: self.d.press(home) print(返回桌面) return True elif action wait: time.sleep(2) print(等待2秒) return True else: print(f未知动作{action}) return False4.3 主循环与任务执行最后我们用一个主程序将三个模块串联起来形成一个完整的闭环。import time from screen_parser import ScreenParser from llm_brain import LLMBrain from action_executor import ActionExecutor def run_agent(user_instruction, max_steps20): parser ScreenParser() brain LLMBrain(api_keyyour-openai-api-key) # 请替换为你的API Key executor ActionExecutor() action_history [] for step in range(max_steps): print(f\n 步骤 {step1} ) # 1. 感知获取当前屏幕状态 print(正在获取屏幕信息...) screen_info parser.get_screen_info() print(f检测到 {len(screen_info[elements])} 个主要元素) # 2. 决策LLM决定下一步做什么 print(LLM思考中...) action_cmd brain.decide_next_action(user_instruction, screen_info, action_history) print(f决策结果{action_cmd}) # 检查是否完成任务LLM可以输出一个特殊动作如finish这里简化处理 if action_cmd.get(action) finish: print(任务完成) break # 3. 执行在手机上执行动作 print(执行动作...) success executor.execute(action_cmd, screen_info[elements]) # 记录历史 action_history.append(str(action_cmd)) if not success: print(动作执行失败尝试返回...) executor.execute({action: back}, []) # 4. 等待界面稳定 time.sleep(2) # 根据App响应速度调整 else: print(f达到最大步骤数 {max_steps}任务可能未完成。) if __name__ __main__: # 示例打开微信找到“文件传输助手”假设微信已在桌面 # 这是一个非常具有挑战性的任务实际可能需要更精细的Prompt和元素定位 task 在微信中找到‘文件传输助手’并打开它 run_agent(task)这个简易原型已经包含了AppAgent最核心的循环。运行它你会看到你的手机在AI的指挥下自动操作。当然这个原型非常脆弱只能处理简单场景但它清晰地展示了从视觉感知到决策执行的完整链路。5. 常见问题、挑战与优化策略实录在实际运行上述原型或更复杂的AppAgent时你会遇到各种各样的问题。下面是我在实验过程中遇到的一些典型挑战及解决思路。5.1 屏幕解析不准导致“点不准”或“找不到”这是最常见的问题。可能的原因和解决方案原因1控件检测模型漏检或误检。对策优先使用可访问性服务如uiautomator2获取控件树而不是纯视觉模型。这是最可靠的数据源。确保测试App没有禁用可访问性服务。原因2动态内容或异步加载。列表在滑动后内容刷新控件属性如resource-id可能变化。对策在决策-执行循环中加入更智能的等待。不是固定等待2秒而是等待特定元素出现或消失。可以在ScreenParser中增加一个wait_for_element(condition)方法结合轮询和超时机制。原因3非标准控件或自定义View。一些游戏或高度定制的App其按钮可能是自定义绘制不在标准控件树里。对策退回基于CV的图标/文字识别。训练一个针对该App特定图标的检测模型或者使用OCR如PaddleOCR识别屏幕上的特定文字然后通过图像匹配或文字匹配来定位点击坐标。这属于“特种作战”需要针对目标App定制。5.2 LLM决策混乱陷入循环或执行无关操作LLM有时会“抽风”反复执行无效操作或在几个页面间来回跳转。原因1Prompt不够清晰或约束力不强。对策强化Prompt中的规则。明确禁止在Prompt中明确指出“禁止重复最近5步内已执行过的相同操作”。提供范例在Prompt中给出2-3个正确决策的示例Few-shot Learning。简化输出强制要求输出严格的JSON并验证格式格式不对则重试。原因2屏幕描述信息过载或噪声太大。对策优化ScreenParser的摘要算法。过滤掉android.view.View这类无意义的容器节点合并列表项只保留clickabletrue或带有重要文本的元素。传递给LLM的信息在1000 token以内为佳。原因3任务过于复杂超出LLM的单步规划能力。对策实施分层任务规划Hierarchical Task Planning。不要指望LLM一口气从“订披萨”分解到原子操作。可以引入一个“高层规划器”先将任务分解为几个阶段如“阶段1打开外卖App并登录”“阶段2搜索并选择商品”“阶段3完成支付”每个阶段再交给当前的“低层执行器”LLM去完成。高层规划器可以用另一个LLM也可以用规则系统。5.3 执行速度慢无法满足实时性要求从截图、解析、调用LLM到执行一个循环可能需要数秒甚至十几秒尤其是调用云端API时。优化1并行与异步。截图和屏幕解析可以与LLM推理并行进行。即在执行上一步操作后立即开始截图和解析同时将上一步的结果和新的屏幕信息发送给LLM。优化2模型本地化与小型化。将多模态模型替换为更小、更快的本地模型如经过蒸馏的Qwen-VL-Mini。虽然能力可能有下降但延迟可以从秒级降到百毫秒级。优化3缓存与预测。对于常见的App页面如微信首页、设置页可以缓存其解析结果和常见的下一步操作。LLM无需每次都重新分析完全相同的页面。优化4操作预测模型。可以训练一个轻量级的模型专门根据屏幕状态预测用户最可能点击的1-3个位置。这个模型可以快速运行作为LLM决策的补充或前置过滤器减少对重型LLM的调用频率。5.4 泛化能力不足只能处理训练过的App一个在淘宝上训练得很好的Agent可能完全不会用京东。策略1预训练与元学习。在大规模、多样化的App交互数据集上对模型进行预训练让模型学习通用的UI模式如搜索框通常在顶部确认按钮通常在右下角设置通常在抽屉菜单里。这需要海量的截图 操作配对数据。策略2增强探索与学习能力。给Agent赋予更强的主动探索能力。当进入一个新App时不是直接尝试完成任务而是先花一些步骤进行“探索”点击明显的导航元素观察页面变化逐步构建对这个App的“心智模型”。这可以结合强化学习中的探索策略。策略3引入人类示范或知识库。为每个目标App建立一个简单的“操作手册”知识库以结构化数据如“在美团App中搜索功能的入口是底部第二个标签页”或少量示范视频的形式存在。Agent在执行任务前可以先查询相关知识。5.5 安全与伦理风险让AI自动操作手机尤其是涉及支付、社交等敏感操作时风险极高。必须设立安全围栏操作白名单严格限制Agent可以操作的App列表禁止操作银行、支付类App。敏感动作确认对于“支付”、“确认删除”、“发送消息”等高风险操作必须设计中断机制等待真人确认。运行沙盒最好在专属的测试设备或模拟器中运行与个人主力机隔离。行为监控与日志详细记录Agent的每一步操作和屏幕状态便于出错时回溯和审计。AppAgent这个项目为我们打开了一扇门让我们看到了大模型作为“数字世界通用操作员”的潜力。从技术上看它巧妙地缝合了CV、NLP和自动化控制。从实用角度看它的道路还很长稳定性、泛化能力和安全性都是亟待攻克的山头。但毫无疑问沿着这个方向走下去我们或许很快就能拥有一个真正理解我们意图、并能帮我们处理手机上各种琐事的智能助手。