1. 项目概述当Android应用遇见AI智能体最近在GitHub上看到一个挺有意思的项目叫krutikJain/android-agent-skills。光看名字可能有点抽象但如果你对移动开发特别是Android自动化测试或者RPA机器人流程自动化感兴趣这个项目绝对值得你花时间研究。简单来说它试图解决一个核心痛点如何让一个AI智能体Agent去理解和操作一个真实的Android应用界面。想象一下你有一个复杂的App需要做回归测试或者你想自动化一些日常操作比如自动签到、自动填写表单。传统的UI自动化框架如Espresso、UiAutomator需要你编写大量定位元素的代码一旦UI稍有改动脚本就可能失效。而这个项目探索的是让AI“看”到屏幕理解屏幕上有什么按钮、输入框、文本然后像真人一样去思考和操作。这听起来像是把大语言模型LLM的“大脑”和Android的“手眼”结合了起来。我花了一些时间深入研究了它的代码和设计思路发现它不仅仅是一个工具库更是一个关于“移动端智能体交互”的工程化实践样板里面有很多值得借鉴的设计模式和避坑经验。2. 核心架构与设计思路拆解2.1 智能体与环境的交互范式这个项目的核心架构遵循了经典的“智能体-环境”交互模型。在这个模型里android-agent-skills扮演的是“环境”与“智能体大脑”之间的适配器和执行器角色。环境Android设备就是一部真实的手机或模拟器上面运行着目标应用。环境的状态就是当前屏幕的像素信息截图和可访问性节点树Accessibility Node Tree。智能体大脑LLM通常是一个大语言模型比如GPT-4、Claude或者本地部署的模型。它接收来自环境的“观察”Observation经过思考输出一个“动作”Action指令。android-agent-skills的作用观察生成器它从Android设备抓取屏幕截图和UI层次结构并将这些原始、杂乱的数据转换成LLM能够理解的、结构化的文本描述。这一步至关重要直接决定了LLM对当前状态的认知是否准确。动作解析与执行器它接收LLM输出的自然语言指令如“点击登录按钮”、“在搜索框输入‘咖啡’并提交”将其解析成Android系统可以执行的具体操作命令如adb shell input tap x y或调用UiAutomator的API并负责执行。技能库项目预定义了一系列“技能”Skills如click、type、scroll、swipe等。这些技能是对基础动作的封装让LLM可以用更高阶的语义来调用而不是直接操作坐标。注意这里的设计巧妙之处在于“抽象层”。它没有让LLM直接去生成ADB命令或Java代码而是定义了一套中间指令集。这大大降低了LLM的决策难度和出错率也使得整个系统更容易维护和扩展。2.2 关键技术栈选型与权衡项目的技术选型体现了在实用性、性能和开发效率之间的平衡。1. 设备交互层uiautomator2vs 纯ADB项目主要使用了uiautomator2这个Python库。为什么不直接用adb命令信息获取优势uiautomator2可以轻松获取完整的UI元素树包括每个控件的resource-id、text、bounds、clickable等属性。这对于生成给LLM的“观察”描述至关重要。纯ADB很难获取如此结构化的信息。操作稳定性uiautomator2提供了基于元素属性的操作如d(resourceId“com.xx:id/btn”).click()比基于绝对坐标的adb shell input tap要稳定得多因为坐标会随着屏幕分辨率、状态栏变化而改变。开发效率Python API比拼接adb shell命令字符串更友好错误处理也更方便。当然uiautomator2需要先在设备上安装一个守护服务atx-agent这算是一个小小的部署成本。但在自动化测试领域这已是标准做法。2. 视觉处理截图与OCR屏幕截图是给LLM的“视觉观察”。项目通常使用adb exec-out screencap -p来获取高质量的PNG截图。对于需要识别屏幕上任意文本的场景比如验证码、非标准控件内的文字则会集成OCR引擎如pytesseract或easyocr。这里有一个经验点OCR非常耗时。在实际应用中应该优先使用uiautomator2从控件属性中提取文本只有在万不得已时才启用OCR并且可以考虑对截图进行区域裁剪以加速识别。3. 提示工程Prompt Engineering这是连接LLM与Android环境的核心“软”技术。项目的Observation生成器本质上就是一个复杂的提示词模板。它需要定义清晰的指令告诉LLM你的角色是什么一个Android自动化助手。提供丰富的上下文包括当前屏幕的文本化描述“屏幕上有一个‘登录’按钮一个用户名输入框…”、可用的技能列表“你可以使用click, type, scroll…”、以及历史动作记录。设定严格的输出格式要求LLM必须以指定的JSON格式回复例如{“thought”: “...”, “action”: {“name”: “click”, “args”: {“element”: “登录按钮”}}}。这便于程序化解析。4. 状态管理与容错一个健壮的智能体不能只执行一步。项目需要管理交互状态当前是什么应用上一步做了什么操作是否成功如果LLM输出了一个无法解析或执行失败的动作系统需要有重试或请求澄清的机制。例如如果LLM说“点击那个蓝色的按钮”但描述过于模糊系统应该能反馈“发现多个蓝色按钮请提供更具体的描述如按钮上的文字”。3. 核心模块深度解析与实操要点3.1 观察生成器如何让AI“看懂”屏幕这是整个流程的第一步也是最容易出问题的一步。观察生成器的目标是将像素和节点树转化为LLM能高效处理的文本。1. 信息源的选择与融合UI层次结构通过uiautomator2获取的dump_hierarchy()是主要信息源。它包含了所有控件的属性。但问题在于有些控件是invisible或off-screen的有些控件的text属性是空的但屏幕上确实有图文字。直接全部丢给LLM会引入大量噪音。处理策略需要实现一个过滤器。通常只保留visibletrue,clickabletrue或scrollabletrue等对交互有意义的节点。同时提取每个节点的关键属性text、resource-id、content-desc、class如android.widget.Button以及bounds坐标范围。resource-id是最稳定的定位标识符应优先使用。2. 结构化描述与自然语言描述的平衡一种简单的方法是把所有过滤后的节点属性用JSON或XML格式列出。但这可能让LLM难以快速把握屏幕重点。更好的方法是生成一段自然语言摘要“当前屏幕是微信的聊天列表页。顶部有一个搜索框resource-id:com.tencent.mm:id/j5其右侧有一个‘’按钮。屏幕中部列表显示了好友‘张三’、‘李四’的聊天记录每个条目包含头像和最后一条消息预览。底部有四个标签页微信已选中、通讯录、发现、我。”同时保留一份结构化的节点列表作为“附录”供LLM在需要精确定位时参考。这模仿了人类先扫视全局再关注细节的认知过程。3. 集成视觉信息对于纯文本描述无法涵盖的信息比如图标样式、布局颜色、验证码图片就需要处理截图。有两种方式整体描述将截图输入给多模态大模型如GPT-4V让其生成一段场景描述。成本高延迟大但理解能力强。关键区域OCR针对已知的需要文本输入的区域如验证码输入框裁剪对应bounds的图片进行OCR识别然后将识别出的文本作为该节点的附加属性。这是更经济实用的做法。实操心得观察描述并非越详细越好。信息过载会干扰LLM判断增加token消耗和推理时间。最佳实践是提供“足够决策”的信息。例如对于一排图标按钮告诉LLM“底部有5个图标按钮从左到右文字分别是‘首页’、‘分类’、‘购物车’、‘消息’、‘我的’”比描述每个图标的颜色和形状更有用。3.2 动作空间与技能设计LLM输出的动作必须在预设的“动作空间”内否则无法执行。android-agent-skills定义了一套技能这就是它的动作空间。1. 基础技能click(element_identifier): 点击。element_identifier可以是resource-id、文本内容、或基于描述的模糊匹配如“第二个按钮”。type(element_identifier, text): 输入文本。需要先确保焦点在输入框内。scroll(direction, element_identifierNone): 滚动。direction可以是up、down、left、right。可以指定在某个可滚动容器内滚动。swipe(start_x, start_y, end_x, end_y, duration): 滑动。用于更复杂的手势。back(): 模拟返回键。home(): 模拟Home键。wait(seconds): 等待。用于页面加载或动画完成。2. 技能的设计原则原子性每个技能只完成一个基本操作。click就是点击不要和wait绑定。复合操作应由LLM通过多次调用来完成。参数明确技能所需的参数应该能从观察描述中明确推断出来。例如click需要能唯一标识一个元素的参数。鲁棒性技能执行内部应有重试和异常处理机制。比如click时如果通过文本找不到元素可以尝试用resource-id如果元素存在但不可点击可以等待片刻再重试。3. 复合技能与规划简单的任务如“打开设置”可能只需要click(“设置图标”)。但复杂的任务如“在购物App中搜索iPhone并加入购物车”就需要LLM进行规划click(搜索框) - type(搜索框, “iPhone”) - click(搜索按钮) - wait(2) - click(第一个商品) - scroll(down) - click(“加入购物车”)。 项目本身可能不包含一个复杂的规划模块但它提供了让LLM进行这种逐步推理的基础设施。更高级的实现可以引入ReActReasoning and Acting模式让LLM在每一步输出“思考”和“行动”。3.3 与LLM的接口提示词模板与输出解析这是智能体的“大脑”接口设计好坏直接决定智能体的智商。1. 系统提示词设计系统提示词定义了智能体的角色和能力边界。一个基本的框架如下你是一个Android手机自动化助手。你可以通过我提供的屏幕描述来理解当前界面并通过调用一系列技能来操作手机。 技能列表 - click(description): 根据描述点击一个元素。描述应尽可能唯一如“登录按钮”、“右上角的设置图标”。 - type(description, text): 在描述指定的输入框中输入文本。 - scroll(direction): 向上/下/左/右滚动屏幕。 - back(): 点击返回键。 - home(): 点击Home键。 - wait(seconds): 等待若干秒。 当前屏幕描述 {observation} 请根据你的目标“{goal}”和当前屏幕状态决定下一步做什么。你的回复必须是严格的JSON格式 { “thought”: “你的推理过程解释为什么选择这个动作” “action”: { “name”: “技能名”, “args”: {技能参数} } } 如果任务已经完成或者无法在当前屏幕完成请将action设为 null。2. 输出解析与验证LLM的回复需要通过json.loads()解析。必须做好错误处理JSON解析失败LLM可能没有严格遵守格式。可以尝试用正则表达式提取JSON部分或者反馈错误要求其重试。动作无效action.name不在技能列表中或者args不符合要求。系统应反馈一个明确的错误信息如“未知技能: ‘press’。可用技能有click, type, ...”。元素定位失败这是最常见的问题。当技能执行器无法根据description找到唯一元素时应该将失败信息如“找到3个包含‘确定’文本的按钮”作为新的观察反馈给LLM让它重新决策或给出更精确的描述。3. 历史上下文管理为了完成多步任务需要将历史对话包括观察、思考、动作、结果维护在上下文窗口中。这能让LLM知道已经做了什么避免循环操作。但要注意上下文长度限制对于长流程可能需要定期进行摘要或者只保留最近几步的关键信息。4. 完整实操流程构建一个自动登录智能体让我们通过一个具体例子串联起所有模块构建一个能自动为某App执行登录操作的智能体。4.1 环境准备与项目初始化首先确保你的开发环境就绪。Android设备准备一部真机或启动一个模拟器推荐Android Studio的AVD。开启USB调试模式。Python环境建议使用Python 3.8。创建一个虚拟环境。python -m venv android-agent-env source android-agent-env/bin/activate # Linux/Mac # android-agent-env\Scripts\activate # Windows安装核心依赖除了android-agent-skills项目本身的依赖我们还需要关键库。pip install uiautomator2 # 设备控制 pip install pillow # 图像处理 pip install openai # 或其它LLM SDK这里以OpenAI为例 # 如果需要OCR安装 pytesseract 和 tesseract-OCR初始化uiautomator2第一次连接设备时需要。python -m uiautomator2 init这会在设备上安装必要的守护程序。4.2 实现核心循环与观察生成我们基于项目思路编写核心的智能体循环。首先实现观察生成器。import uiautomator2 as u2 from PIL import Image import io import json class AndroidEnv: def __init__(self, device_serialNone): self.d u2.connect(device_serial) # 连接设备 self.observation_history [] def get_observation(self): 获取当前屏幕的文本化描述和结构化数据 # 1. 获取UI层次结构 hierarchy self.d.dump_hierarchy() # 这里需要解析XML格式的hierarchy提取节点信息。 # 为简化我们假设有一个函数 parse_hierarchy 能返回一个节点列表。 nodes self._parse_hierarchy(hierarchy) # 2. 过滤和提炼关键节点 key_nodes [] for node in nodes: if node.get(visible) ! true: continue # 收集有交互意义或信息意义的节点 if (node.get(clickable) true or node.get(checkable) true or node.get(text) or node.get(resource-id)): key_nodes.append(node) # 3. 生成自然语言描述 nl_description self._generate_nl_description(key_nodes) # 4. 获取屏幕截图可选用于OCR或存档 screenshot_bytes self.d.screenshot(formatraw) image Image.open(io.BytesIO(screenshot_bytes)) # 可以保存或处理 image observation { “nl_desc”: nl_description, “nodes”: key_nodes, # 结构化数据 “timestamp”: time.time() } self.observation_history.append(observation) return observation def _parse_hierarchy(self, xml_content): # 简化的解析示例实际应用可使用xml.etree.ElementTree # 返回格式[{“text”: “登录”, “resource-id”: “com.example:id/login_btn”, “bounds”: “[0,100][200,150]”, “clickable”: “true”}, ...] pass def _generate_nl_description(self, nodes): desc_parts [] # 按区域或类型简单分组描述 buttons [n for n in nodes if ‘button’ in n.get(‘class’, ‘’).lower()] inputs [n for n in nodes if ‘edittext’ in n.get(‘class’, ‘’).lower()] texts [n for n in nodes if n.get(‘text’) and n not in buttons and n not in inputs] if buttons: btn_texts [b.get(‘text’) for b in buttons if b.get(‘text’)] if btn_texts: desc_parts.append(f“屏幕上有按钮{‘ ’.join(btn_texts)}。”) if inputs: input_hints [i.get(‘text’) or ‘输入框’ for i in inputs] desc_parts.append(f“有{len(inputs)}个输入区域{‘ ’.join(input_hints)}。”) if texts: # 取前几条较长的文本 long_texts [t.get(‘text’) for t in texts if len(t.get(‘text’, ‘’)) 3][:5] if long_texts: desc_parts.append(f“屏幕文本包括‘{’‘.join(long_texts)}’。”) return ‘ ’.join(desc_parts) if desc_parts else “屏幕元素较少请检查UI层次。”4.3 集成LLM并执行动作接下来我们实现与LLM的交互和动作执行。import openai # 示例可使用其他LLM API class AndroidAgent: def __init__(self, env, llm_client, system_prompt): self.env env self.llm llm_client self.system_prompt system_prompt self.conversation_history [{“role”: “system”, “content”: system_prompt}] def act(self, goal): 根据目标执行一步动作 # 1. 获取当前观察 obs self.env.get_observation() user_prompt f“当前目标{goal}\n当前屏幕描述{obs[‘nl_desc’]}\n请决定下一步动作。” # 2. 构建对话历史包含之前的交互 messages self.conversation_history [{“role”: “user”, “content”: user_prompt}] # 3. 调用LLM try: response openai.ChatCompletion.create( model“gpt-4”, # 或 gpt-3.5-turbo messagesmessages, temperature0.1, # 低温度保证输出稳定 max_tokens500 ) llm_output response.choices[0].message.content except Exception as e: return {“error”: f“LLM调用失败{e}”} # 4. 解析LLM输出 try: action_data json.loads(llm_output) thought action_data.get(“thought”, “”) action action_data.get(“action”) except json.JSONDecodeError: # 尝试清理输出再解析 return {“error”: “无法解析LLM的JSON输出”, “raw_output”: llm_output} # 5. 记录到历史 self.conversation_history.append({“role”: “user”, “content”: user_prompt}) self.conversation_history.append({“role”: “assistant”, “content”: llm_output}) # 6. 执行动作 if action: result self._execute_action(action, obs[‘nodes’]) return {“thought”: thought, “action”: action, “result”: result} else: return {“thought”: thought, “action”: None, “result”: “任务完成或无法继续”} def _execute_action(self, action_spec, nodes): 根据动作规格和节点列表执行具体操作 action_name action_spec[‘name’] args action_spec.get(‘args’, {}) if action_name ‘click’: element_desc args.get(‘element’) # 根据描述在nodes中查找最匹配的元素 target_node self._find_element_by_description(nodes, element_desc) if target_node: bounds target_node.get(‘bounds’) # 格式 “[x1,y1][x2,y2]” # 解析bounds计算中心点 x, y self._parse_bounds_to_center(bounds) self.env.d.click(x, y) # 使用uiautomator2点击 return f“成功点击 ‘{element_desc}’” else: return f“错误未找到元素 ‘{element_desc}’” elif action_name ‘type’: # 类似click先找到输入框点击聚焦再输入文本 pass # ... 实现其他技能 else: return f“错误未知技能 ‘{action_name}’” def _find_element_by_description(self, nodes, desc): # 简单的匹配逻辑优先匹配text再匹配resource-id包含的关键词 # 实际应用中需要更复杂的模糊匹配和排序算法 for node in nodes: if desc in (node.get(‘text’) or ‘’): return node for node in nodes: if desc in (node.get(‘resource-id’) or ‘’): return node return None4.4 运行与调试最后编写主程序来运行这个登录智能体。# 配置 SYSTEM_PROMPT “””你是Android自动化助手...“”” # 填入完整的系统提示词 GOAL “在测试App中完成登录用户名是‘testuser’密码是‘123456’” # 初始化 env AndroidEnv(“emulator-5554”) # 替换为你的设备序列号 agent AndroidAgent(env, openai, SYSTEM_PROMPT) # 运行循环 max_steps 10 for step in range(max_steps): print(f“\n 步骤 {step 1} ) result agent.act(GOAL) print(f“思考{result.get(‘thought’)}”) print(f“动作{result.get(‘action’)}”) print(f“结果{result.get(‘result’)}”) if result.get(‘action’) is None or “完成” in result.get(‘result’, ‘’): print(“任务结束。”) break if “错误” in result.get(‘result’, ‘’): print(f“遇到错误可能需要调整提示词或观察生成逻辑。”) # 可以在这里加入人工干预或重试逻辑 break time.sleep(2) # 等待操作生效和界面稳定5. 常见问题、调试技巧与性能优化在实际操作中你会遇到各种各样的问题。以下是我在实验过程中总结的一些典型问题和解决思路。5.1 LLM不按格式输出或理解错误这是初期最常见的问题。症状LLM回复的是自然语言而不是JSON或者它调用了不存在的技能。排查与解决强化系统提示词在提示词中明确强调“必须输出JSON格式”并给出更精确的例子。可以使用“少样本学习”Few-shot Learning在提示词中提供2-3个完整的、正确的输入输出示例。降低Temperature将LLM API的temperature参数设为0.1或0减少输出的随机性。输出后处理如果LLM在JSON外加了说明可以尝试用正则表达式如r‘\{.*\}’提取第一个JSON对象。如果JSON格式错误可以将错误信息和原始回复一起反馈给LLM要求它纠正。简化动作空间如果LLM总是混淆技能可以先只提供click和type两个最基础的技能等它稳定后再逐步增加。5.2 元素定位失败或定位不准智能体说“点击登录按钮”但执行器找不到。症状_find_element_by_description返回None或者点击了错误的元素。排查与解决丰富观察描述确保观察生成器提供的nl_desc里包含了足够区分元素的文本。例如不要只说“按钮”要说“蓝色的‘登录’按钮”或“右下角的‘下一步’按钮”。改进匹配算法简单的字符串包含匹配太脆弱。可以引入模糊匹配如fuzzywuzzy库计算描述与节点text、resource-id、content-desc的相似度取最高分。同时可以结合元素位置如“左上角”、“底部中央”进行筛选。让LLM描述更精确当定位失败时将失败信息如“找到2个包含‘确定’的按钮”作为新的观察反馈给LLM迫使它提供更独特的标识如“文本为‘确定’且位于屏幕中央偏下的按钮”。使用绝对定位作为后备在提示词中告诉LLM如果可能可以使用元素的resource-id如果观察描述中提供了因为这是最稳定的标识符。5.3 操作时序与状态同步问题智能体点击后页面还在加载它就执行了下一步操作导致失败。症状操作失败日志显示元素不存在或不可交互。排查与解决强制等待在每个动作执行后尤其是click可能触发页面跳转或网络请求后加入固定的wait如2-3秒。可以在_execute_action每个技能后自动加一个短等待。智能等待实现一个wait_until函数在关键操作后循环检查某个预期元素如新页面的标题是否出现或者检查屏幕是否稳定连续两次截图变化很小超时后再报错。这比固定等待更高效。状态验证让LLM在输出动作时也输出一个“预期结果”例如“expected_change”: “页面应跳转到主页出现‘欢迎’字样”。执行器在执行动作后检查当前观察是否包含预期变化如果不包含则视为失败并重试或报告。5.4 性能瓶颈与优化随着流程变长响应速度变慢。症状每一步的耗时都很长无法满足实时性要求。排查与优化观察生成优化dump_hierarchy()和截图是耗时操作。可以考虑缓存UI层次结构如果短时间内没有检测到屏幕变化则复用上一次的观察。对于截图可以降低分辨率或使用更快的编码格式如JPEG。LLM上下文管理对话历史会越来越长导致API调用变慢、成本增加。定期对历史进行摘要或者只保留最近5-10轮对话。对于长流程任务可以设计“子目标”每完成一个子目标就重置上下文重新描述当前状态。本地轻量模型如果对通用能力要求不高可以尝试使用在Android操作指令上微调过的本地小模型如7B-13B参数的模型通过ollama或llama.cpp本地部署能极大降低延迟和成本。并行与异步观察生成、LLM推理、动作执行有些步骤可以并行或异步进行。例如在LLM思考的同时可以预加载一些资源。5.5 安全与伦理考量自动化操作真实App需要谨慎。仅用于测试与授权场景此类技术应主要用于自家App的自动化测试、内部流程自动化或在明确获得授权的环境下使用。避免用于干扰他人服务或爬取未公开数据。遵守Robots协议与服务条款即使技术上可行也应尊重目标网站或App的使用条款。设置速率限制在自动化操作中引入随机延迟避免对服务器造成意外的高负载攻击。人工监督对于重要的自动化流程尤其是在初期应设置人工审核环节或完备的异常报警机制。这个项目为我们打开了一扇门让我们看到了LLM与具体操作系统深度结合的可能性。它不仅仅是“用AI写测试脚本”而是构建一个能感知、推理、执行于真实数字环境的智能体原型。虽然目前还存在稳定性、成本和速度的挑战但随着多模态模型能力的提升和工程方案的优化这类技术在未来应用生态自动化、无障碍辅助、个性化手机助手等领域有着巨大的想象空间。从我个人的实验来看最大的收获不是做出了一个多完美的工具而是在这个过程中被迫去深入思考如何将非结构化的自然语言指令可靠地映射到结构化的系统API调用上这本身就是一个非常经典的AI工程问题。