Operit Android Agent Runtime 架构分析
从 Operit 源码还原 Android 内的 Agent Runtime,并对比移动端与服务端在工具、权限和生命周期上的差异。

Android 上的 Agent 经常被描述成“手机里装了一个大模型”。这个说法把模型和 Agent 混在了一起。模型负责生成下一步内容,Agent Runtime 还要管理消息、调用工具、检查权限、保存状态,并在工具返回后决定是否继续请求模型。
Operit 把这些组件放进了 Android 应用。本文基于 commit 17df7f9e9586e1f4d9e2a82aa56c78aeffd4ca96,重点不是介绍界面,而是对照服务端 Agent,看看同一套循环如何适应 Android。
主 Agent 不是某一个类
在 Operit 源码里搜索 Agent 会找到 PhoneAgent,但它主要负责 UI 自动化子任务。Operit 的主对话 Agent 并不是一个包办所有工作的 Agent 类,而是由多组组件共同组成:
AIServiceFactory根据配置创建不同模型提供商。MessageProcessingDelegate组织用户消息、上下文和一次处理过程。AIToolHandler从模型输出中提取工具调用并执行。ToolPermissionSystem决定工具是否允许运行。PackageManager、MCPManager和 QuickJS 扩展工具来源。- 聊天、记忆和工作区组件负责持久化。
从运行效果看,它们合起来就是一套 Agent Runtime。
和服务端 Agent 相同的部分
无论运行在服务器还是 Android,基本循环没有变化:
用户消息
-> 组装上下文和工具描述
-> 请求模型
-> 解析文本或工具调用
-> 检查权限并执行工具
-> 把工具结果加入上下文
-> 再次请求模型,直到本轮结束
Operit 的 AIToolHandler 使用 ConcurrentHashMap 保存工具执行器。模型流式输出到达后,Handler 解析调用、触发生命周期 Hook、检查权限,再调用具体 ToolExecutor。工具成功或失败都会形成 ToolResult,后续模型请求才能知道刚才发生了什么。
这部分与 Node.js 或 Python Agent 的 Tool Calling 没有本质区别。Android 不是靠特殊的“移动端推理协议”获得 Agent 能力,它只是换了一个运行宿主。
Android 改变的是工具和生命周期
服务端工具通常直接访问文件系统、子进程、数据库或内网服务。Android 应用默认处在沙箱里,能做什么取决于系统授权。
普通文件访问需要应用私有目录或 Storage Access Framework;操作其他应用界面需要无障碍服务,权限更高的自动化可能使用 ADB、Shizuku 或 Root。Operit 还提供 MCP、QuickJS 和 Linux 环境,把一部分通用工具带到手机,但最终仍要经过 Android 权限与进程规则。
生命周期差异同样明显。服务器进程可以长期驻留,Android 会限制后台执行、网络活动和电量消耗。一个耗时 Agent 任务需要考虑前台 Service、通知、应用被回收后的状态恢复,以及屏幕关闭后的行为。只把服务端循环复制到 Activity 中,旋转屏幕或切到后台就可能丢失任务。
用同一个任务做对照
极简 Demo 只做一件事:请求一个 HTTP JSON 接口,把响应保存到文件,再读取文件生成摘要。
模型看到的工具可以保持一致:
[
{ "name": "http_get", "arguments": { "url": "http://example.test/status" } },
{ "name": "write_file", "arguments": { "path": "status.json", "content": "..." } },
{ "name": "read_file", "arguments": { "path": "status.json" } }
]
服务端执行时,write_file 可以写入绑定的工作目录,HTTP 请求受容器网络策略限制。Operit 执行同样的调用时,路径需要映射到应用工作区或用户通过 SAF 授权的目录,网络请求受 Android manifest、系统代理和应用生命周期影响。
这个对照实验不需要比较模型回答得好不好。使用同一模型、同一提示词和同一接口,只记录下面几项:
| 项目 | 服务端 Runtime | Operit Runtime |
|---|---|---|
| 任务宿主 | 常驻进程或容器 | Android 应用与 Service |
| 文件权限 | 系统用户、容器挂载 | 应用沙箱、SAF、授权目录 |
| 工具来源 | 本地包、MCP、业务服务 | 内置工具、工具包、MCP、QuickJS |
| 系统操作 | Shell、进程、系统 API | Android API、无障碍、ADB、Root |
| 状态恢复 | 服务重启与持久化日志 | 进程回收、Activity/Service 生命周期 |
| 资源约束 | 相对固定,可横向扩容 | 电量、温度、内存和后台限制 |
本地模型不是必要条件
Operit 可以连接云模型,也可以通过 MNN 或 llama.cpp 运行本地模型。Agent Runtime 与模型部署位置是两个问题。云模型减少手机推理压力,但消息会离开设备;本地模型改善隐私和离线能力,却受模型大小、内存和耗电影响。
无论模型在哪,工具仍在手机上执行。服务端模型返回“打开设置页”只是一段工具调用,真正打开页面的是 Android 侧执行器。这正是 Runtime 的作用:把模型建议变成受权限约束的本地操作。
移动端更需要权限失败时关闭操作
Operit 的工具 Hook 会捕获普通观察回调异常,权限拦截 Hook 发生异常时则返回 Block。这种处理符合移动端工具的风险:显示一条日志失败可以继续,权限判断失败不能默认放行。
高权限通道仍需要用户知道自己开启了什么。无障碍、ADB 和 Root 不应被包装成普通聊天能力。工具名称、参数、目标应用和结果都应该留下记录,涉及安装、删除、发送消息或转账的操作还需要二次确认。
结论
Android 能运行 Agent,不是因为 Android 变成了服务器,而是 Operit 在应用内部补齐了 Agent Runtime:模型适配、消息循环、工具注册、权限控制、扩展运行时和持久化一个都不能少。
与服务端相比,Agent 循环几乎相同,差异集中在工具执行环境。服务端关心进程、容器和内网;Android 关心沙箱、系统授权、前后台生命周期和设备资源。理解这一点,比讨论“手机能不能跑大模型”更接近实际工程问题。
参考源码
- Operit 国内镜像
- 工具调度:
app/src/main/java/com/ai/assistance/operit/core/tools/AIToolHandler.kt - 消息处理:
app/src/main/java/com/ai/assistance/operit/services/core/MessageProcessingDelegate.kt - MCP 执行:
app/src/main/java/com/ai/assistance/operit/core/tools/mcp/MCPToolExecutor.kt