基于LangChain4j与Android无障碍服务构建手机AI操作智能体
1. 项目概述:一个运行在手机上的AI“操作员”
最近在捣鼓一个挺有意思的东西,我把它叫做“手机上的AI操作员”。简单来说,就是让一个大语言模型(LLM)能直接看懂你的手机屏幕,并且像真人一样去点击、滑动、输入文字,帮你完成各种手机操作。这个项目的核心,是把AI的“大脑”(LLM的规划与理解能力)和手机的“手”(Android的无障碍服务Accessibility)结合起来,形成一个能自主执行任务的智能体。
想象一下,你不需要再一步步教Siri或者小爱同学“先点这里,再点那里”,而是直接告诉它:“帮我把微信里张三昨天发的那个文件保存到‘工作资料’文件夹”,它就能自己打开微信、找到聊天记录、长按文件、选择保存路径。这听起来有点像科幻电影里的场景,但通过AndroidClaw这个项目,我们已经在Android系统上搭建出了一个可运行的原型。
这个项目特别适合两类朋友:一类是对AI应用落地感兴趣的移动开发者,你可以看到如何将最新的Agent(智能体)架构与成熟的Android开发技术栈融合;另一类是希望自动化重复手机操作的效率追求者,它提供了一个高度可定制的基础,你可以基于它开发属于自己的手机自动化助手。整个项目的构建采用了一种被称为“vibe coding”的快速迭代模式,即产品构思、架构决策、功能实现和调试文档都在与AI的协作中高速完成,这本身也是一种非常现代的开发实践。
2. 核心架构设计:如何让AI“看见”并“操作”手机
要让一个AI模型在手机上干活,可不是简单地把ChatGPT装进去就行。它需要一套完整的“感知-思考-执行”闭环系统。AndroidClaw的架构就是围绕这个闭环设计的,我们可以把它拆解成几个核心层次来理解。
2.1 智能体核心层:AI的“决策大脑”
这是整个系统的指挥中心,基于LangChain4j这类框架构建。它的核心是一个规划-执行循环。当用户下达一个指令,比如“给妈妈发个生日祝福短信”,智能体核心会进行以下工作:
- 意图理解与任务分解:LLM首先将自然语言指令解析成结构化目标:“这是一个涉及‘通讯录’和‘短信’应用的多步骤任务。”
- 制定行动计划:LLM规划出具体步骤序列,例如:
步骤1: 打开通讯录应用 -> 步骤2: 搜索“妈妈” -> 步骤3: 进入联系人详情 -> 步骤4: 点击“发信息” -> 步骤5: 输入祝福语 -> 步骤6: 点击发送。 - 工具调用与执行:规划中的每个步骤,都会转化为对底层“工具”的调用。例如,“打开通讯录应用”会调用
phone_open_app工具,并传入包名参数。 - 观察与迭代:执行完一个步骤后,系统会通过UI理解层获取最新的屏幕状态,反馈给LLM。LLM判断当前状态是否符合预期,并决定下一步行动。比如,打开通讯录后,发现屏幕是搜索框,那么下一步自然就是调用
phone_input_node工具进行输入。
这个循环中,记忆(Memory)模块至关重要。它记录了完整的对话历史和已执行的操作,确保LLM在长对话和多步骤任务中保持上下文连贯,不会忘记自己刚才做了什么。
实操心得:规划循环的稳定性在实际编码中,让LLM可靠地输出结构化的规划(如JSON格式的动作列表)是个挑战。我们采用了严格的输出格式(Output Parser)约束和重试机制。如果LLM第一次返回的格式无法解析,系统会携带错误信息重新提问,通常两到三次内就能获得有效响应。此外,为每一步操作设置超时和失败回退策略(比如点击失败后尝试滑动再点击)是保证流程健壮性的关键。
2.2 UI理解层:AI的“眼睛”
这是连接数字世界(LLM)和物理世界(手机屏幕)的桥梁。AndroidClaw采用了“无障碍快照优先,截图OCR兜底”的双重策略。
- 主渠道:无障碍服务快照:当启用无障碍服务后,Android系统会提供一个当前屏幕的UI元素树(AccessibilityNodeInfo)。这棵树包含了每个控件的文本、描述、坐标、是否可点击等丰富信息。
AndroidClaw通过phone_get_ui_snapshot_compact工具获取并格式化这份信息,将其作为LLM“观察”屏幕的主要依据。这种方式速度快、信息结构化程度高,LLM可以精确地知道“搜索按钮在屏幕右下角,文本是‘搜索’”。 - 备用渠道:屏幕截图与OCR:并非所有应用都完美支持无障碍服务,有些自定义控件的信息可能缺失。此时,系统会回退到截取屏幕截图。虽然项目当前版本未集成离线OCR模块,但架构预留了接口。我们可以将截图转换成Data URL或Base64编码,发送给支持视觉识别的多模态LLM(如GPT-4V),或者集成一个本地的OCR引擎(如Tesseract)来提取文字信息。
这种设计保证了在不同应用环境下的可用性。例如,在标准的系统设置界面,使用无障碍快照效率极高;而在一些游戏或特殊应用中,则可以依靠视觉模型来理解屏幕。
2.3 工具层:AI的“双手”
工具层封装了所有AI可以执行的具体操作。每个工具都是一个独立的函数,有明确的输入参数和输出结果。AndroidClaw内置的工具主要分为几类:
- 手机操作工具:这是核心工具集。
phone_click_by_text:在屏幕上寻找包含特定文字的控件并点击。这是最常用、最直观的工具。phone_click_coordinates/phone_click_coordinates_from_screenshot:通过坐标点击。后者特别有用,当LLM通过分析截图识别出某个图标位置时,可以使用这个工具。phone_input_node:向指定的输入框控件输入文本。phone_swipe,phone_scroll:滑动和滚动操作,用于浏览长列表或页面。phone_global_back/home/recents:模拟物理按键。
- 文件与系统工具:
file_read,file_write,shell_execute等。这些工具让AI能够读写应用内的文件,甚至执行一些ADB Shell命令(需权限),极大地扩展了能力边界。 - 集成工具:如通过模型上下文协议(MCP)桥接的外部工具,或自定义的“技能”工具包。
工具的设计遵循单一职责原则,并且输出尽可能结构化,方便LLM理解执行结果。例如,phone_get_ui_snapshot_compact返回的是一个JSON数组,清晰列出了所有可交互节点。
2.4 通道与持久化层:系统的“神经网络”与“记忆体”
- 通道层:负责与用户或外部系统通信。目前支持本地App内交互和集成飞书机器人进行远程控制。这意味着你可以在电脑前,通过飞书给家里的手机发消息:“帮我看看外卖到哪了”,手机上的
AndroidClaw就能自动打开外卖App并查询进度,将结果发回飞书。通道层抽象化了消息来源,让核心Agent无需关心指令是从哪里来的。 - 持久化层:使用Android Jetpack的Room数据库来保存所有的对话记录、消息和执行日志。这不仅是简单的历史记录,更是实现长时记忆和断点续做的基础。即使App进程被杀死,重启后也能从上次中断的任务上下文继续执行。
WorkManager负责定时健康检查,确保后台守护服务(Daemon Service)持续运行,并在网络异常恢复后自动重连消息通道。
3. 关键技术实现与踩坑实录
有了清晰的架构,接下来就是如何用代码将其实现。这里我会分享几个关键模块的实现细节和开发中遇到的典型问题。
3.1 无障碍服务的深度集成与权限处理
无障碍服务是AndroidClaw的基石。在AndroidManifest.xml中声明服务只是第一步。
<service android:name=".accessibility.PhoneAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="true"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service>在accessibility_service_config.xml中,需要精细配置服务能力:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeViewClicked" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagRetrieveInteractiveWindows|flagReportViewIds" android:canRetrieveWindowContent="true" android:description="@string/accessibility_service_description" android:notificationTimeout="100" />这里flagReportViewIds非常重要,它让我们能获取到控件的唯一资源ID,对于精准定位相同文本的不同控件(如列表项)有巨大帮助。
踩坑记录:权限引导的“温柔”与“强制”用户主动去系统设置里开启无障碍服务是最大的转化漏斗。我们的策略是:
- 首次启动引导:应用启动后,直接进入一个图文并茂的引导页,用箭头和动画示意用户需要去哪里打开开关,并提供一个“一键跳转”按钮(通过
Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))。- 权限门禁:在引导页,我们阻止用户跳过。不开启无障碍服务,就无法进入应用主界面。这虽然有些“强硬”,但对于核心功能依赖此权限的应用来说,避免了用户进入后功能全部失效的糟糕体验。
- 动态监听与提示:即使用户当时同意了,也可能在系统设置中随时关闭。我们在主Activity中监听无障碍服务状态,一旦发现被关闭,立即通过一个非阻塞的Snackbar或对话框提醒用户,并再次提供快捷跳转入口。
3.2 基于LangChain4j的Agent实现
我们选择LangChain4j而非直接调用OpenAI API,是因为它提供了更高层次的抽象,比如内置的工具调用(Tool Calling)格式支持、对话内存管理、以及对不同模型供应商(OpenAI, Anthropic, Local LLM等)的统一接口。
关键代码片段:Agent的组装
fun createAgent(llm: ChatLanguageModel): ConversationalAgent { // 1. 创建工具集 val tools = listOf( PhoneClickTool(), PhoneInputTool(), FileReadTool(), // ... 其他工具 ) // 2. 创建工具执行器 val toolExecutor = ToolExecutor(tools) // 3. 构建Agent,指定工具、记忆和提示词模板 return ConversationalAgent.builder() .chatLanguageModel(llm) .tools(tools) .memory(MessageWindowChatMemory.withMaxMessages(10)) // 保留最近10轮记忆 .promptTemplate(""" 你是一个手机操作助手。当前屏幕信息如下: {{screen_snapshot}} 用户的目标是:{{user_goal}} 历史操作记录:{{chat_history}} 请规划下一步操作。你必须从可用工具中选择一个,并以指定JSON格式回复。 """.trimIndent()) .outputParser(JsonOutputParser.builder<AgentAction>().build()) // 强制JSON输出 .build() }执行循环的核心逻辑:
suspend fun executeGoal(userGoal: String) { var currentState = getUiSnapshot() // 获取初始屏幕状态 val agent = createAgent(llm) while (!taskIsComplete(currentState, userGoal)) { // 将当前状态和目标喂给Agent val agentResponse = agent.execute(currentState, userGoal) // 解析出要调用的工具和参数 val toolToCall = agentResponse.toolName val toolInput = agentResponse.toolInput // 执行工具 val toolResult = toolExecutor.execute(toolToCall, toolInput) // 更新状态(等待UI变化后获取新快照) delay(500) // 简单的延迟,等待操作响应 currentState = getUiSnapshot() // 将工具执行结果作为新的上下文,加入到下一轮循环的记忆中 agent.memory.add(AiMessage("工具 ${toolToCall} 执行结果:${toolResult}")) } }3.3 守护服务与可靠性工程
手机上的自动化Agent必须是“打不死的小强”。我们通过ForegroundService实现一个常驻的守护进程,即使App退到后台,Agent依然能监听远程命令(如来自飞书的消息)。
防止服务被杀死的关键点:
- 前台服务通知:启动服务时,必须显示一个持续的通知。我们将其分类为
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,并给用户清晰的说明,如“AI助手正在运行”。 - WorkManager健康检查:定时(如每30分钟)启动一个Worker,检查守护服务是否在运行。如果不在,则尝试重新启动它。这个Worker本身会被系统妥善调度,保证检查机制存活。
- BOOT_COMPLETED广播接收器:监听开机广播,实现开机自启,确保设备重启后助手能自动恢复工作。
- 网络状态监听:对于依赖远程消息通道(如飞书WebSocket)的场景,需要监听网络连接变化。当网络从无到有,自动触发重连逻辑。
4. 实战:从零构建一个“自动保存图片”技能
理论说了这么多,我们通过一个具体的例子,看看如何利用AndroidClaw的架构,开发一个实用的功能:自动保存微信群聊中的图片到指定相册。
4.1 技能定义与规划
用户指令:“保存群里今天所有的美食图片到‘我的美食’相册。”
- 终极目标分解:这是一个复杂的多应用任务,涉及微信、相册(或文件管理)。
- 高层规划(由LLM或我们预设):
- 子任务A:打开微信,进入目标群聊。
- 子任务B:在聊天记录中定位并筛选出今天的图片消息。
- 子任务C:逐一将图片保存到手机存储。
- 子任务D:将保存的图片移动到“我的美食”相册(可能需要操作文件管理器或相册App)。
4.2 工具链组合与实现
我们不需要从头造轮子,而是组合现有工具,并可能编写一两个专用的“技能工具”。
phone_open_app:打开微信。phone_click_by_text:点击群聊名称。phone_swipe/phone_scroll:滚动浏览聊天记录。phone_get_ui_snapshot_compact:不断获取屏幕快照,LLM分析快照中是否存在图片消息(通过识别“[图片]”文字或特定的控件类型)。phone_long_click_by_text:长按图片消息,弹出菜单。phone_click_by_text:点击弹出菜单中的“保存图片”。(这里需要处理不同微信版本的UI差异,可能需要多个尝试策略)。file_list&file_move:图片默认保存在/Pictures/WeChat/目录下,使用文件工具将其移动到/Pictures/我的美食/目录。
编写一个专用的“保存微信图片”技能工具: 这个工具封装了上述从识别到保存的多个步骤,对LLM来说,它就是一个黑盒:save_wechat_image_from_current_screen()。这简化了LLM的规划复杂度。
class SaveWeChatImageTool : Tool { override fun name(): String = "save_wechat_image" override fun description(): String = "在当前微信聊天界面,识别并保存最新的图片消息。" fun execute(): String { // 1. 获取当前快照 val snapshot = phoneGetUiSnapshotCompact() // 2. 分析快照,寻找图片消息节点(简化逻辑:找包含‘图片’文本或特定资源ID的节点) val imageNode = findImageMessageNode(snapshot) if (imageNode == null) return "未在当前屏幕找到图片消息。" // 3. 执行长按、点击保存等系列操作 phoneLongClickNode(imageNode) delay(300) // 尝试点击可能的“保存”按钮文本 val saved = phoneClickByText(listOf("保存图片", "存储", "Save")) return if (saved) "图片保存成功。" else "保存操作可能失败。" } }4.3 处理不确定性:UI差异与操作失败
现实中的UI千变万化。我们的工具和Agent必须足够健壮。
- 多文本匹配:如
phone_click_by_text工具,内部应接受一个文本列表,按顺序尝试,直到成功或全部失败。例如点击“保存”按钮,可以传入["保存图片", "存储", "Save to device"]。 - 坐标回退:当通过文本无法定位时,如果UI快照中节点有准确的坐标,则回退到坐标点击
phone_click_node。 - 超时与重试:每个操作后等待合理时间(如500ms-2s),让UI响应。如果操作后预期的新UI状态没有出现(例如点击“保存”后没有出现保存成功的提示),则触发重试或失败处理流程。
- 状态验证:在执行关键操作前后,通过快照对比进行验证。例如,保存图片后,可以检查是否出现了“已保存”的Toast提示(如果快照能捕获到)。
5. 进阶思考与优化方向
一个基础可用的Agent只是起点。要让其真正智能、可靠,还有很长的路要走。
5.1 增强UI理解:从文本到视觉语义
当前严重依赖无障碍服务的文本信息。下一步是深度融合视觉理解:
- 集成离线OCR:引入如
Tesseract或PaddleOCR的移动端引擎,直接从截图提取文字,作为无障碍信息的补充,彻底解决“控件无文本”的问题。 - 图标与元素识别:使用轻量级图像分类模型(如MobileNet)对屏幕截图进行语义分割,识别出“返回箭头”、“搜索图标”、“复选框”、“视频播放按钮”等通用UI元素。这能让LLM的理解不再局限于文字。
- 结构化UI grounding:将屏幕信息组织成更丰富的结构化数据,例如:
{ “type”: “list”, “items”: [ {“text”: “张三”, “has_unread”: true}, {“text”: “李四”, “has_unread”: false} ] }。这能极大提升LLM对界面布局和状态的认知精度。
5.2 规划与恢复能力的强化
目前的规划多是单步或简单链式。复杂任务需要更高级的规划能力:
- 分层任务规划:让LLM先制定高级别计划(打开App -> 导航到目标页 -> 执行操作),再逐层细化。这符合人类的思考方式,也更容易纠错。
- 子目标与回滚:当某个步骤失败(如找不到按钮),Agent应能尝试替代方案(如使用全局返回,重新尝试),或者将问题抛给用户。记录完整的执行轨迹,便于在失败时进行“回滚”到上一个稳定状态。
- 长期记忆与学习:将成功的操作序列(如“在XX版本微信中保存图片的点击路径”)存入知识库。下次遇到相同应用和场景时,可以优先尝试历史成功路径,提高效率。
5.3 生态扩展与部署考量
- 更多连接通道:除了飞书,可以集成微信机器人、Telegram Bot、甚至邮件,让触发指令的方式更多元。
- 技能市场/插件化:将
SaveWeChatImageTool这类技能封装成插件,允许用户动态安装、卸载。社区可以贡献各种针对特定App(淘宝、抖音、钉钉)的自动化技能包。 - 隐私与安全:所有手机操作都在设备本地完成,敏感数据(屏幕截图、聊天记录)无需上传云端。这是本地AI Agent的核心优势。在代码实现上,要确保权限的透明化,告知用户每个工具所需权限及其用途。
- 性能与耗电优化:持续运行的无障碍服务和后台守护服务是耗电大户。需要精细控制快照采集频率、LLM调用时机(是否每次都需要调用大模型?能否用小模型或规则判断?),并在设备空闲时进入低功耗模式。
开发AndroidClaw这类项目,最大的感触是“边界感”很重要。我们要清晰地界定哪些应该交给LLM这个“大脑”(如意图理解、复杂规划),哪些应该由确定性的代码“小脑”来完成(如具体的控件查找算法、失败重试逻辑)。让两者各司其职,才能构建出既灵活又稳定的智能体。这个过程就像在教一个非常聪明但缺乏常识的孩子如何使用手机,你需要给它明确的规则、反馈和足够多的示例,它才能越来越可靠地帮你完成任务。
