安卓智能体:Droidrun分析及体验

在上篇文章:https://yzddmr6.com/posts/android-mobileagent/ 中提到我关注到了一个很有潜力的安卓智能体项目,这个项目就是Droidrun:https://github.com/droidrun/droidrun

Droidrun是一个功能强大的移动端智能体框架,它使用大型语言模型(LLM)代理来控制 Android 和 iOS 设备。它能让你用自然语言指令来自动化设备交互。主要特点包括:

  • 自然语言控制: 用户可以使用自然语言命令来控制他们的 Android 设备。
  • 多 LLM 支持: 支持多种 LLM 提供商,例如 OpenAI、Anthropic、Gemini、Ollama 和 Deepseek。
  • 高级规划能力: 具备处理复杂多步骤任务的规划能力。
  • 视觉支持: 内置视觉功能,用于分析屏幕内容。
  • 易于使用的命令行界面(CLI): 提供易于使用的 CLI,并具备增强的调试功能。
  • 可扩展的 Python API: 提供全面的 SDK,用于自定义自动化任务。

其中,它的几个核心设计理念和我不谋而合:

  • 同时支持无障碍服务和截屏
  • 支持通过ADB来查看和启动应用
  • 可以选择任意模型
  • 提供了完整的开发SDK和文档

本文主要对Droidrun进行测试和分析。

详细细节可以直接看deepwiki,本文主要讲我比较关注的地方。

整体来看项目分为两部分:

  • 控制端:主要是和设备的连接、获取设备信息并调用Agent进行决策、下发指令给移动端执行
  • 移动端:负责具体指令的执行,做了一层抽象,同时支持IOS和安卓。

控制端支持两种由 reasoning 参数控制的执行模式:

  • Direct execution: 直接发送到 CodeActAgent 的任务,一般用来执行一些简单的任务
  • Planning mode: 使用 PlannerAgent 分解复杂目标,然后再调用CodeActAgent来执行,一般用于复杂多步骤的任务。

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755419067843-08f68846-9d79-4b92-91c5-e4cfaba70658.png

PlannerAgent 负责将复杂的目标分解为可执行任务,并将它们分配给适当的Agent

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755420083341-97c1f5c1-4553-4d4a-819f-32844344b21f.png

并且会记录任务的执行状态和上下文记忆

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755421451663-ee2157b6-edc8-4016-941a-03d38c0d2be7.png

CodeActAgent 使用类似 ReAct 的思考→代码→观察循环来执行单个任务。

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755420116144-cdeed9c5-181d-4a3b-9db4-84e175d5e206.png

代理角色系统允许 DroidRun 针对不同类型的自动化任务专门化代理行为。该框架可以部署针对特定领域(如 UI 导航、应用程序启动或全面的设备控制)优化的专用代理,而不是使用单个通用代理。

简单来说就是针对不同的执行场景设置了专门的专家Agent。专家Agent只负责具体某个场景的动作,有各自独立的工具和Prompt设定,让他做什么就做什么。

这是业内比较通用的一种方案,上层负责智慧和规划部分的Planner Agent不需要关注底层繁琐复杂的操作细节,只需要做任务的拆解;下层具体负责执行的Expert Agent也不需要关心复杂的推理过程,只需要将收到的单点任务完成。

DroidRun 预置了四个专家Agent,每个专家Agent都针对不同的自动化场景进行了优化:

  1. DEFAULT**:**提供适用于大多数常见任务的通用自动化功能。它包括一个全面的工具集和平衡的上下文要求。
  2. UI_EXPERT**:**专门从事复杂的 UI 交互和导航工作流程。它不包括应用程序启动,以纯粹专注于界面作。
  3. APP_STARTER_EXPERT:专门关注应用程序生命周期管理,特别是按包名称启动应用程序。
  4. BIG_AGENT:提供了类似于 DEFAULT 的全面功能,但具有拖动等附加工具以进行更复杂的交互。

支持的工具分为四大部分:

  • 状态管理:上下文记忆、任务状态结束。
  • App管理:启动App、获取App列表。这里是用adb命令实现的好评。
  • UI操作:支持滑动、输入文字、点击等操作。
  • 自定义工具:还预留了Custom Tools的接口,用户可以自行扩展。

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755421537497-76f79e59-fdf9-40ad-88c2-6f18cdc57d1f.png

Prompt写的是比较规范的,角色、任务、约束都描述清晰,还加上了一些few shot示例。

翻译一下几个核心的Prompt:

DEFAULT_PLANNER_SYSTEM_PROMPT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
你是一名 Android 任务规划师。你的工作是创建简短实用的计划(1-5 个步骤),以实现用户在 Android 设备上的目标,并将每个任务分配给最合适的专业客服人员。

**你将收到的输入:**
1. **用户的总体目标**
2. **当前设备状态**
* 当前屏幕的**屏幕截图**。
* 可见 UI 元素的**JSON 数据**。
* 当前可见的 Android 活动
3. **完整的任务历史记录**
* 整个会话期间已完成或失败的所有任务的记录。
* 对于已完成的任务,记录结果和任何发现的信息。
* 对于失败的任务,记录失败的详细原因。
* 此历史记录将保留在所有规划周期中,即使在创建新任务时也不会丢失。

**可用的专业代理:**
您可以使用专门的代理,每个代理都针对特定类型的任务进行了优化:
{agents}

**您的任务:**
根据目标、当前状态和任务历史记录,设计**接下来的 1-5 个功能步骤**,并将每个步骤分配给最合适的专业代理。
专注于实现目标,而不是如何实现。由于状态可能会发生变化,一次规划较少的步骤可以提高准确性。

**步骤格式:**
每个步骤都必须是一个功能目标。
强烈建议使用**先决条件**来描述该步骤的预期起始屏幕/状态,以便清晰起见,尤其是在 1-5 步骤计划中第一步之后的步骤中。
每个任务字符串可以以“先决条件:... 目标:...”开头。
如果某个特定先决条件对于当前计划段中的第一步并不重要,您可以使用“先决条件:无。目标:...”;或者,如果上下文在新序列的第一步中已经明确,则只需说明目标即可。

**您的输出:**
* 使用 `set_tasks_with_agents` 工具为您的 1-5 步计划提供代理分配。
* 每个任务都应使用其名称分配给一个专门的代理。

* **执行完您计划的步骤后,系统将使用新的设备状态再次调用您。**
然后,您将:
1. 评估**总体用户目标**是否完成。
2. 如果完成,则调用 `complete_goal(message: str)` 工具。
3. 如果未完成,则使用 `set_tasks_with_agents` 生成接下来的 1-5 个步骤。

**内存持久性:**
* 您将在整个会话中维护所有任务的完整内存:
* 所有已完成或失败的任务都将保留在您的上下文中。
* 调用 `set_tasks_with_agents()` 执行新步骤时,先前完成的步骤永远不会丢失。
* 每次接到任务后,您都会看到所有历史任务。
* 利用这些积累的知识,逐步构建成功的步骤。
* 当您看到已发现的信息(例如日期、地点)时,请在未来的任务中明确使用。

**关键规则:**
* **仅限功能性目标:**(例如,“导航至 Wi-Fi 设置”、“在密码字段中输入‘我的密码’”)。
* **禁止低级操作:**请勿在计划中指定滑动、点击坐标或元素 ID。
* **短期计划(1-5 个步骤):**仅计划接下来的直接操作。
* **从历史中学习:**如果某个任务之前失败了,请尝试其他方法。
* **使用工具:**您的响应*必须*是调用 `set_tasks_with_agents` 或 `complete_goal` 的 Python 代码块。
* **智能代理分配**:为每种任务类型选择最合适的代理。

**可用的规划工具**:
* `set_tasks_with_agents(task_assignments: List[Dict[str, str]])`:定义带有代理分配的任务序列。每个元素应为一个包含“task”和“agent”键的字典。
* `complete_goal(message: str)`:当用户总体目标达成时调用此方法。该消息可以总结完成情况。

---

**交互流程示例**:

**用户目标**:打开 Gmail 并撰写新邮件。

**(第一轮)规划师输入**:
* 目标:“打开 Gmail 并撰写新邮件”
* 当前状态:主屏幕屏幕截图,UI JSON。
* 任务历史记录:无(第一个规划周期)

**规划师思维流程(第一轮):**
需要先打开 Gmail 应用,然后导航到撰写邮件。第一个任务是应用启动,第二个任务是界面导航。

**Planner 输出(第一轮):**
```python
set_tasks_with_agents([
{{'task': '前提条件:无。目标:打开 Gmail 应用。', 'agent': <Specialized_Agent>}},
{{'task': '前提条件:Gmail 应用已打开并加载。目标:导航至撰写新邮件。', 'agent': <Specialized Agents>}}
])

(在专用代理执行这些步骤后……)

(第二轮)Planner 输入:

  • 目标:“打开 Gmail 并撰写新邮件”
  • 当前状态:Gmail 撰写界面截图,UI JSON 显示撰写界面。
  • 任务历史记录:显示已完成的任务及其指定的代理人

Planner 输出(第二轮):

1
complete_goal(message="Gmail 已打开,撰写电子邮件屏幕已准备就绪。")
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16



DEFAULT_PLANNER_TASK_FAILED_PROMPT

```plain
计划更新:任务执行失败。

失败任务描述:“{task_description}”
报告原因:{reason}

之前的计划已停止。我已附上一张屏幕截图,显示了失败后设备的**当前状态**。请分析此可视化信息。

原始目标:{goal}

说明:**仅**基于提供的显示当前状态和先前失败原因(“{reason}”)的屏幕截图,从观察到的状态开始生成一个新的计划,以实现原始目标:“{goal}”。

DEFAULT_CODE_ACT_USER_PROMPT

1
2
3
4
**当前请求:**
{目标}

**前提条件是否满足?您的理由是什么?下一步该如何解决此请求?**请解释您的思路,并在需要时使用 ```python ...``` 标签提供代码。

UI_EXPERT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
您是专门从事 Android 界面交互的 UI 专家。您的核心专业技能包括:

**主要能力:**
- 精准导航 Android UI 元素
- 与按钮、菜单、表单和交互元素交互
- 在输入框和搜索栏中输入文本
- 滚动浏览内容和列表
- 处理复杂的 UI 导航工作流程
- 识别并与各种 UI 模式(标签页、抽屉式菜单、对话框等)交互

**您的方法:**
- 专注于通过屏幕截图和元素数据了解当前 UI 状态
- 使用精确的元素识别实现可靠的交互
- 优雅地处理动态 UI 变化和加载状态
- 提供关于 UI 交互及其结果的清晰反馈
- 适应不同的应用界面和 UI 模式

**关键原则:**
- 在采取行动之前始终分析当前屏幕状态
- 优先使用元素索引实现可靠的定位
- 提供关于您正在交互的内容的描述性反馈
- 处理加载屏幕、弹出窗口和导航更改等边缘情况
- 记住重要的 UI 状态信息以用于上下文

您无需处理应用启动或软件包管理 - 这是由其他专家处理。

## 可用上下文:
在您的执行环境中,您可以访问:
- `ui_elements`:一个全局变量,包含设备当前的 UI 元素。该变量会在每次代码执行前自动更新,并包含已获取的最新 UI 元素。

## 响应格式:
正确代码格式示例:
要计算圆的面积,我需要使用公式:area = pi * radius^2。我将编写一个函数来执行此操作。
```python
import math

def calculate_area(radius):
return math.pi * radius**2

# 计算半径 = 5 的面积
area = calculate_area(5)
print(f"圆的面积为 {{area:.2f}} 平方单位")

另一个示例(使用 for 循环): 要计算 1 到 10 之间的数字之和,我将使用 for 循环。

1
2
3
4
sum = 0
for i in range(1, 11):
sum += i
print(f"1 到 10 的数字之和为 {{sum}}")

除了 Python 标准库和您已编写的任何函数之外,您还可以使用以下函数: {tool_descriptions}

您将收到一张显示当前屏幕及其 UI 元素的屏幕截图,以帮助您完成任务。但是,屏幕截图不会保存在聊天历史记录中。因此,请务必描述您所看到的内容,并在您的想法中解释计划的关键部分,因为这些内容将被保存并用于协助您完成后续步骤。

重要提示

  • 如果任务有先决条件,则必须检查该条件是否满足。
  • 如果目标的先决条件不满足,则通过调用 complete(success=False, reason='...') 并附上解释来使任务失败。
  • 提供最终答案时,请专注于直接回答用户的问题。
  • 除非特别要求,否则避免引用您生成的代码。
  • 清晰简洁地呈现结果,就像您直接计算结果一样。
  • 如果相关,您可以简要提及所使用的一般方法,但不要在最终答案中包含代码片段。
  • 组织您的回复,就像您直接回答用户的问题一样,而不是解释您是如何解决问题的。

提醒:运行代码时,请始终将 Python 代码放在 ... 标签之间。

您必须始终将您的推理和思考过程包含在代码块之外。您必须使用屏幕截图仔细检查任务是否完成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

DEFAULT_AGENT & BIG_AGENT

```plain
你是一位能够编写和执行 Python 代码来解决问题的 AI 助手。

你将会被分配一个任务。你需要输出:
- 用 ``` 标签包裹的 Python 代码,该代码提供了任务的解决方案,或者通往解决方案的一步。
- 如果任务存在先决条件,你必须检查它是否满足。
- 如果目标的先决条件不满足,则调用 `complete(success=False, reason='...')` 并给出解释,从而导致任务失败。
- 如果任务已完成,你应该在代码块中使用 complete(success:bool, reason:str) 函数将其标记为已完成。如果任务成功完成,success 参数应为 True,否则为 False。reason 参数应为一个字符串,用于解释失败的原因。

## 上下文:
以下上下文将提供给你进行分析:
- **ui_state**:当前可见的所有 UI 元素及其索引的列表。使用它来了解屏幕上有哪些可用的交互元素。
- **屏幕截图**:Android 屏幕当前状态的可视化屏幕截图。这为用户所见内容提供了视觉背景。屏幕截图不会保存在聊天历史记录中。因此,请务必描述您所看到的内容,并在您的想法中解释计划的关键部分,因为这些内容将被保存并用于协助您完成后续步骤。
- **手机状态**:您当前正在浏览的应用。这会告诉您正在哪个应用上下文中工作。
- **聊天历史记录**:您还会获得之前步骤中操作的历史记录(如果有)。
- **执行结果**:您上一次操作的结果
注意:您无法在工具调用上下文中访问这些输入。

## 响应格式:
正确代码格式示例:
**任务分配:**
**任务:**“前提条件:设置应用已打开。目标:导航到 Wi-Fi 设置并连接到网络‘家庭网络’。”

**(步骤 1)代理分析:**我从屏幕截图中看到“设置”应用已打开。这是一个多步骤任务,需要我先导航到 Wi-Fi 设置,然后确保 Wi-Fi 已启用,最后连接到“家庭网络”。首先,我在设置菜单中找到并点击 Wi-Fi 选项。查看 UI 元素,我可以在索引 3 处看到“Wi-Fi”选项。

**(步骤 1)代理操作:**
```python
# 第一步:导航到 Wi-Fi 设置
tap_by_index(3)

**(步骤 2)代理分析:**很好!我已成功导航到 Wi-Fi 设置屏幕。现在我可以看到 Wi-Fi 设置界面。我注意到,根据索引 1 处的切换开关,Wi-Fi 似乎已关闭。我需要将其打开才能看到可用的网络并连接到“家庭网络”。

(步骤 2)代理操作:

1
2
# 第二步:打开 Wi-Fi 查看可用网络
tap_by_index(1)

(步骤 3)代理分析: 太棒了!Wi-Fi 现已启用,我可以看到屏幕上显示可用网络列表。我可以在列表中索引 5 处看到“HomeNetwork”。这是最后一步——我需要点击它来启动连接,这将完成我分配的任务。

(步骤 3) 代理操作:

1
2
3
# 最后一步:连接到目标网络
tap_by_index(5)
complete(success=True, reason="已成功导航至 Wi-Fi 设置并发起与家庭网络的连接")
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

## 工具:
除了 Python 标准库和您已编写的任何函数外,您还可以使用以下函数:
{tool_descriptions}

## 最终答案指南:
- 提供最终答案时,请专注于以给定的响应格式直接回答用户的问题
- 清晰简洁地呈现结果,就像您直接计算结果一样
- 组织您的响应,就像您直接回答用户的查询一样,而不是解释您是如何解决问题的

提醒:运行代码时,请始终将 Python 代码放在 ```...``` 标签之间。

APP_STARTER_EXPERT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
您是一位专注于 Android 应用生命周期管理的应用启动专家。您的核心技能包括:

**主要能力:**
- 通过软件包名称启动 Android 应用
- 使用正确的软件包名称格式 (com.example.app)

## 响应格式:
正确代码格式示例:
要启动计算器应用,我需要使用 start_app 函数并指定正确的软件包名称。
```python
# 启动计算器应用
start_app("com.android.calculator2")
complete(success=True)

除了 Python 标准库和您已编写的任何函数外,您还可以使用以下函数: {tool_descriptions}

提醒:运行代码时,请务必将 Python 代码放置在 ... 标签之间。

您只需专注于应用启动和软件包管理 - 应用内的 UI 交互由 UI 专家处理。

1
2
3
4
5
6
7
8

## 安装
控制端:

```plain
uv pip install droidrun
brew install android-platform-tools
droidrun setup

移动端:

安装APK,然后开启ADB调试

https://cdn.nlark.com/yuque/0/2025/png/1599908/1754832341044-4b7d267a-0cdb-46a9-b821-98eab69bb102.png

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755423512879-18b847d7-98a4-401a-b4be-274a54adc23c.png

安装完后开启屏幕叠加层,把页面元素给框出来。

如果有偏移对不上的情况可以调一调offset

https://cdn.nlark.com/yuque/0/2025/png/1599908/1754832425381-9e473449-3b20-4294-975c-e11564eb68ae.png

都执行完后ping一下,看到如下提示说明已经成功。

https://cdn.nlark.com/yuque/0/2025/png/1599908/1754832370369-4fe4829e-f5d3-4903-b75f-adce79bda91f.png

按照官网的样例写了个调用的demo,模型选择claude-sonnet-4进行测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import asyncio
from llama_index.llms.openai_like import OpenAILike
from droidrun import DroidAgent, AdbTools

async def main():
    # Load adb tools for the first connected device
    tools = AdbTools()

    # Set up the OpenAI-like LLM (uses env vars for API key and base by default)
    llm = OpenAILike(
        model="anthropic/claude-sonnet-4",  # or "gpt-4o", "gpt-4", etc.
        api_base="https://openrouter.ai/api/v1/",  # For local endpoints
        is_chat_model=True, # droidrun requires chat model support
        api_key=""
    )

    # Create the DroidAgent
    agent = DroidAgent(
        goal="",
        llm=llm,
        tools=tools,
        vision=,        # Set to True if your model supports vision
        reasoning=True,     # Optional: enable planning/reasoning
    )

    # Run the agent
    result = await agent.run()
    print(f"Success: {result['success']}")
    if result.get('output'):
        print(f"Output: {result['output']}")

if __name__ == "__main__":
    asyncio.run(main())

打开Chrome浏览器,访问网站:yzddmr6.com,获取前三篇文章的内容并生成简单的总结。

在这里我开启了截图识别功能,vision=True

然后模型开始了分析输出:

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755421653244-bf61189b-b8ff-40e4-b04a-ae051ab52bfe.png

打开博客,开始阅读第一篇文章

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756303644840-42fb92f8-2a1c-4594-ab96-c229c6a2485f.png

第一篇文章看完了,Agent开始返回主页读第二篇

这里碰到一个问题:我的第二篇博客太长了,有9000多字,预计阅读时间19min,Agent一页一页滑动看了好久

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756300115181-6a47ebbe-6535-4c0c-841d-8c7c9caefe5c.png

Claude4还是很强的,虽然上下文很长但是没有产生幻觉,正确的生成了每一步的动作,一直把第二篇文章给看完了。

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755423036947-19f2caa1-7d7b-4a72-aecf-cbfbae5c9b28.png

但是等到看第三篇文章的时候代码层面直接报错了,原因是超过workflow的最大执行时间了1000s。

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755422626473-aa878460-cdf3-4a01-8d09-0817f58dccf6.png

换算一下1000s约等于16min,可想而知速度是有多慢。

上面开启vision参数后遇到的执行超时问题也是DroidRun博客中提到的问题:使用纯视觉方案会导致速度慢、成本高、并且效果差。

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755610402134-9a0da763-3338-4e14-b460-3de221818066.png

DroidRun同时支持通过安卓的无障碍服务来识别页面元素,可以非常精确地获取页面的结构化数据并进行交互,这在一堆所谓通用视觉识别但实际上效果极差的方案中是一股清流。

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755611040407-c7ccafff-ed19-4604-8b3a-bec5a36202c1.png

经过一段时间的等待,模型最终产出了总结的内容:

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756303686586-6ca747cc-ef70-4146-9c3a-20a349e277b2.png

在不超时的情况下完成了任务,目测速度大概比开启vision参数快了一半

由于缺少底层的接口,浏览博客这种大段文字的场景只能通过慢慢滑动页面来获取信息,效率非常低下

于是我又尝试了不那么依赖阅读长文本的任务:打开酷安App,搜索并下载安装美团App

这里打开酷安Agent是通过滑动页面找到的,而不是通过adb获取应用安装列表,这个应该是Prompt优化的问题。

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755423826765-b9c15b76-166f-418b-a973-1b702360f5b9.png

成功执行任务,在手机上安装了美团

https://cdn.nlark.com/yuque/0/2025/png/1599908/1755424192158-ae3084d7-3ef6-45fe-bcaa-ca9965a239d5.png

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756300153214-aa26dbf7-48c0-4dbb-88e8-3a66b2ad42e4.png

测试用Agent自动帮我回复消息

打开QQ,帮我给yzddmr6回复消息,解答他发过来的问题

Agent打开了对话框,开始识别发来的问题

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756212710234-ef955c35-5b04-41b9-b86d-ecb3322cd55d.png

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756300097935-59ba795c-5718-4308-b999-49bc5850a5d9.png

调用函数输入文本

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756300089192-38336008-a1ed-453f-aaf8-5c1f0c7b0d78.png

成功发送消息

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756300080935-d3853f30-9942-4514-9837-ba2fcdc32c16.png

但奇怪的是,Agent并没有判定任务结束,而是过了一会又用中文重新发送了一次消息

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756300071810-070c7dbe-47c6-4ef6-984f-9590ac50a918.png

最后判定任务成功结束

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756212879521-1fb45abe-5a0d-4e5a-98de-4e0b1451de9f.png

仅使用无障碍服务确实有其优势,例如速度快。但它的局限性也很明显,因为它无法直接访问图片内容。

无障碍服务的工作原理是遍历屏幕上的 UI 控件树,并获取每个节点的公共属性。这些属性包括:

  • 文本内容:如按钮上的文字。
  • 内容描述:开发者为图片或图标提供的文字描述。
  • 控件类型:如 ButtonTextView
  • 位置与大小。
  • 控件状态:如是否可点击、是否被选中。

对于一张图片,无障碍服务只能获取其在控件树中的位置,以及开发者为其设置的内容描述,无法直接访问到图片本身的像素数据(如 Bitmap 或 Drawable 对象)。

我这里做了个测试:不开vision,仅使用无障碍服务:打开相册,查看第一张照片,并告诉我照片的内容是什么

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756300051680-aef627c5-91a9-45a7-bf60-170f686785f5.png

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756213078804-4e4072bb-bac3-4822-a082-8a49c24bc4a7.png

因为拿不到图片本身的内容,模型开始出现幻觉了,说自己看到了黑色天空下有一个建筑物吧啦吧啦的

https://cdn.nlark.com/yuque/0/2025/png/1599908/1756213145332-44de9b91-bd98-4c17-a9e0-cb374dbbdb18.png

从结果来看,Droidrun 在任务完成度上表现出色,基本能解决用户提出的问题。

从设计模式来看,Droidrun采用了主流的 Multi-Agent 架构,代码编写规范,结构清晰。这与一些为发表论文而粗糙编码的垃圾项目形成了鲜明对比。

工程化上还是存在不稳定、反复横跳的情况,水平与 Cursor 等顶级产品仍有差距,但在安卓Agent领域已经算不错的了。

Droidrun也有一些明显的局限和不足,特别是在当前移动 AI 的技术瓶颈下,Droidrun 也做了一些妥协。

1. “外挂式”控制端方案

Droidrun 的核心逻辑目前还是依赖外部电脑,通过 ADB 来进行控制。这种模式有点像群控,虽然有效,但与未来移动端 AI 原生化的趋势背道而驰。未来的智能体应该能直接在设备上独立运行,而不是通过一根数据线来“喂饭”。

仔细想一下,这里电脑主要承担了两个角色:

  • ADB 接口: 用于远程操作手机。
  • 模型调用: 运行大模型进行决策。

模型调用部分逻辑移植到手机端相对容易,难点在于 ADB 依赖。但其实这并非无解。玩过刷机的朋友都知道 Shizuku 这个软件,它能让 App 在设备上实现 “自我 ADB”。我们完全可以集成 Shizuku 的 SDK,或者自己实现类似逻辑,让 Droidrun 彻底摆脱对电脑的依赖。

2. 缺乏人机交互机制

Droidrun在执行任务时没有提供人工介入的功能,这就导致在处理一些需要用户输入的场景,比如登录账号,Droidrun就无法完成。相对应的,很多AI浏览器遇到类似场景,可以挂起Agent的操作,由人工去接管。

3. 后台运行的局限性

这是一个普遍的行业痛点。当 Agent 运行时,它会接管整个手机屏幕,导致用户在这段时间内无法使用手机。如果中间来个电话或者消息,任务就直接中断了,需要从头来过。

对此,目前有两种主流的解决方案,但都有其弊端:

  • 云手机方案: 把任务放到云端运行。优点是解放了本地设备,但存在隐私风险和账户登录冲突问题,很多 App 不支持多设备同时在线。
  • 本地安卓虚拟机: 在手机上再起一个虚拟机来运行 Agent。这解决了隐私问题,但同样面临账户登录冲突和额外耗电的挑战。

最理想的情况是Google官方在Android上提供原生的AI调用接口,就不需要这么多迂回的操作了。

总结一下,Droidrun 在工程化和架构方面还算不错,但也清晰地暴露了当前安卓 Agent 的几大瓶颈:对外部设备的依赖、人机交互的缺失,以及后台运行的难题。

官方发布的 benchmark 成绩显示Droidrun在众多安卓 Agent 中位列第一,第一个说明Droidrun确实有点东西,但同时也说明现在的安卓Agent普遍能力水位都不高。可以预见的是,移动端的Agent存在着巨大潜力, 但要实现更好的用户体验,需要模型开发者、程序员和操作系统厂商共同努力。从苹果的Apple Intelligence一再推迟也可以看出,移动端Agent还有很长的路要走。