讨论 Android 平台上 AI Agent 如何以结构化方式调用宿主 App 的各项能力——不同的调用通道各自适合什么场景、为什么不能只用 Intent、以及一套可渐进落地的分层架构设计。
背景
如果把 AI Agent 部署在手机上(比如系统级助手、拨号盘的语音助手),一个基础问题是:AI 要调用手机上的 App 能力时,技术链路应该怎么走?
最简单的答案是用 Intent——Android 十几年来就是这么让 App 之间互相调用的。但问题在于,AI 调用 App 和传统 App 间调用有本质区别:AI 需要的不是"打开一个界面让用户操作",而是"执行一个操作并拿到结构化返回值,以便继续后续推理"。Intent 解决不了返回值的问题。
于是出现了两种补充方案:ContentProvider(已有)和 AppFunction(Android 16 新增)。这篇文章把三种方式放在一起对比,讨论什么时候该用什么,以及如何设计一套可扩展的分层架构。
一、三种底层通道对比
手机端 AI 调用 App 能力,目前有三条技术通道:
| 维度 | Intent / 系统 API | ContentProvider | AppFunction(Android 16) |
|---|---|---|---|
| 原理 | 通过 AMS 路由,启动目标 Activity 或 Service | 通过 ContentProvider 框架跨进程查询数据 | system_server 托管的路由层,调用方通过 Binder 远程执行目标 App 内注册的函数 |
| 能否拿返回值 | 不能(单向派发) | 能(Cursor / 数据行) | 能(GenericDocument,类 JSON 结构) |
| 适合什么操作 | 拨号、发短信、打开文件、创建日历事件——有标准 Action 的操作 | 查联系人、查通话记录、查媒体库——系统已暴露 CP 的只读/写场景 | 全文搜索短信、空闲时段计算、跨 App 数据聚合——需要 App 内部复杂逻辑 |
| 开发量 | 几乎为零(一行 startActivity) | 几行 contentResolver.query() | 需要声明 Service + XML 元数据 + 实现回调 |
| 安全级别 | Action 级权限(如 CALL_PHONE) | 读/写二值权限,粒度较粗 | 六层:签名级绑定 + 调用权限 + 函数级开关 + 身份透传 + 用户控制 + 企业策略 |
| 版本要求 | Android 1.0+ | Android 1.0+ | Android 16+(API 36) |
| 第三方 App 兼容 | 所有 App 均可响应标准 Intent | 仅暴露了 CP 的 App | 只有自研 App(第三方不会适配此框架) |
核心决策原则:能用 Intent 或 ContentProvider 解决的场景,不要引入 AppFunction。AppFunction 的存在意义是覆盖标准 API 到不了的场景,而不是替代它们。
三条通道的关系是互补而非互斥。同一个 AI Tool(比如"发短信")的底层实现可以根据设备状态自适应——如果用户装了你自研的短信 App(已适配 AppFunction),就走 AppFunction 实现静默发送;否则走 ACTION_SENDTO,弹出系统短信界面。
二、一个必须澄清的前提:AI 只看 Tool 定义,不接触底层
在讨论架构之前,有一个容易被误解的点需要先说清楚:AI 模型本身不调用 Android API。 LLM 能做的只有一件事——根据上下文中提供的 Tool 定义(一段 JSON),输出一个结构化的 tool call(函数名 + 参数 JSON)。至于这个 tool call 最终是用 Intent 执行还是用 Binder 调用,LLM 完全不知道,也不应该知道。
整个交互链路是:
用户输入:"帮我打给张三"
→ LLM 在上下文中看到了一个叫 makePhoneCall 的 Tool(参数:联系人姓名)
→ LLM 输出:{ "tool": "makePhoneCall", "params": { "name": "张三" } }
→ 宿主 App 的执行层收到 tool call,执行真正的拨号逻辑
这里的关键分层在于:Tool 定义是给 LLM 看的能力说明书——"我能做什么,需要什么参数"。底层实现是执行层的事——"用什么通道完成这个操作"。两层解耦。
换句话说:Tool 是对 AI 的契约,底层是对契约的实现。契约保持稳定,实现可以独立演进。
一个具体例子:同一个 Tool,两套底层
以"帮我发短信告诉李四我马上到"为例,完整跟踪一遍。
第一层:LLM 看到的 Tool 定义。
宿主 App 在初始化时,将如下 MCP 格式的 Tool 定义注入 LLM 上下文:
{
"name": "sendMessage",
"description": "向指定联系人发送短信。联系人必须存在于通讯录中。",
"inputSchema": {
"type": "object",
"properties": {
"contactName": { "type": "string", "description": "通讯录中的联系人姓名" },
"content": { "type": "string", "description": "要发送的短信内容" }
},
"required": ["contactName", "content"]
}
}
LLM 只看到这段 JSON。它不知道 sendMessage 底层怎么实现。
第二层:LLM 输出的 tool call。
LLM 理解用户意图后,输出:
{
"tool": "sendMessage",
"params": { "contactName": "李四", "content": "我马上到" }
}
第三层:宿主 App 的执行层路由。
宿主 App 收到 tool call 后,sendMessage 对应的执行函数会根据当前设备状态选择底层通道:
路径 A——设备上没有自研短信 App 或该 App 未适配 AppFunction:
收到 tool call → 查询通讯录获取李四的号码 "138xxxx1234"
→ 构造 Intent(ACTION_SENDTO, Uri.parse("sms:138xxxx1234"))
→ 填入 sms_body = "我马上到"
→ startActivity(intent)
→ 系统默认短信 App 弹出,用户确认发送
四行代码,不需要任何新框架。
路径 B——设备上安装了自研短信 App,且该 App 已实现 AppFunction:
收到 tool call → 构造 AppFunction 调用请求:
functionIdentifier = "sendMessage",
parameters = { "contactName": "李四", "content": "我马上到" }
→ AppFunctionManager.executeAppFunction(request)
→ system_server 校验权限 + 路由
→ Binder 调用到短信 App 的 AppFunctionService(独立进程)
→ 短信 App 内部: 解析联系人 → 组装 PDU → SmsManager.sendTextMessage()
→ 返回 { "status": "sent", "messageId": "msg_8901" }
同一份 tool call,同一个 sendMessage Tool 定义,底层走了截然不同的通道。LLM 视角毫无变化——它只知道自己调用了 sendMessage,然后拿到了返回结果。
这个设计的价值在于:Tool 定义一次写好就不再动,底层实现可以渐进升级。今天短信 App 还没接入 AppFunction,走 Intent 兜底;下周短信 App 接入了,改一行路由判断代码,体验就从"弹框让用户手动确认"升级到"后台静默发送"。AI 侧的 Tool 定义和调用方式完全不需要修改。
三、分层架构
基于上面的分析,整体架构分为四层:
┌──────────────────────────────────────────────────┐
│ 第一层:AI Agent 层 │
│ LLM 只通过 MCP 协议与下层交互 │
│ 接口:tool/list → tool/call │
│ 它看不到下面的任何一层 │
└────────────────────┬─────────────────────────────┘
│
┌────────────────────▼─────────────────────────────┐
│ 第二层:MCP 工具提供者层 │
│ 本层的两个核心职责: │
│ (1) 收集下层各 App 的能力元数据,组装成 MCP Tool 列表 │
│ (2) 收到 tool call 后,根据 Tool 类型做路由决策 │
│ 路由逻辑:能用 Intent/CP 直接走,复杂场景才走 AppFunction│
└─────────┬───────────────────┬────────────────────┘
│ 简单路径 │ 复杂路径
▼ ▼
Intent / ContentProvider AppFunctionManager
(一行代码搞定) (Binder → system_server)
│
┌──────────▼──────────┐
│ 第三层:AppFunction 框架层 │
│ 运行在 system_server 进程 │
│ 职责:权限校验、路由、 │
│ 生命周期管理、进程缓存 │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ 第四层:各 App 的 │
│ AppFunctionService │
│ 运行在 :functions 独立进程│
│ 实现具体的业务逻辑 │
└─────────────────────┘
几个设计决策的说明:
MCP 层的定位是路由中心,不是业务中心。 MCP 层不包含任何业务逻辑,它只做转换和路由——把下层能力翻译成 LLM 能理解的 Tool 描述,以及在执行时根据 Tool 类型选择最优通道。对上层(LLM)暴露统一的 Tool 接口,对下层屏蔽通道差异。
不是所有 Tool 都要经过 AppFunction。 执行层的路由逻辑遵循"最小复杂度原则":能走标准 API 就直接走,只有确实需要跨 App 聚合、语义检索或结构化返回值的操作才走 AppFunction。这样做既降低了系统复杂度,也保证了在 Android 16 以下设备的兼容性——复杂 Tool 不可用,简单 Tool 照样工作。
Service 独立进程,但不每次都冷启动。 AppFunctionService 运行在 :functions 独立进程中,与 App 的主 UI 进程内存隔离。unbindService 后进程不会立即销毁,而是进入缓存状态,等到系统内存紧张时由 LMK 统一回收。频繁调用时大部分时候是热启动。
云端 Agent 不能直连手机 AppFunction。 如果 AI Agent 跑在云端,调用链路必须是:云端 → 手机端 MCP 网关(WebSocket 长连接)→ MCP 层路由 → AppFunction。云端没有任何方式可以跨过手机端直接触发 AppFunction。
四、场景拆解:每个 App 暴露什么 Tool
下面以拨号盘、文件管理、短信、日历四个典型 App 为例,逐一分析哪些操作走简单路径、哪些需要 AppFunction。
拨号盘
| 用户意图 | 底层通道 | 原因 |
|---|---|---|
| "打给张三" | Intent ACTION_CALL | 系统原生支持 |
| "最近三次打给快递的电话是什么时候" | AppFunction searchCallHistory | 需要按标签过滤 + 时间排序,Intent 不支持返回结构化数据 |
| "来电时显示这个号码关联的快递和航班信息" | AppFunction(跨 App,需调短信/邮件 App) | 跨 App 数据聚合,AppFunction 的核心价值场景 |
拨号盘需要暴露的 AppFunction:searchCallHistory、getContactInfo。
文件管理
| 用户意图 | 底层通道 | 原因 |
|---|---|---|
| "打开昨天拍的那张会议纪要" | Intent ACTION_VIEW | 系统 Gallery 直接渲染 |
| "把这段文字保存成笔记" | Intent ACTION_CREATE_DOCUMENT | SAF 框架,系统接管 |
| "找上个月所有包含'报销'且大于 500KB 的 PDF" | AppFunction searchFiles | 需要全文检索 + 多条件过滤,遍历 SAF 效率过低 |
文件管理需要暴露的 AppFunction:searchFiles、getFileContent、listDirectory。
短信
| 用户意图 | 底层通道 | 原因 |
|---|---|---|
| "发短信给李四说我马上到" | Intent ACTION_SENDTO(或 AppFunction 静默发送) | Intent 路径始终可用,AppFunction 路径体验更优,二选一 |
| "我和王五最近一周聊了什么,帮我总结" | AppFunction getConversation + LLM 摘要 | 需要按联系人过滤 + 提取消息体 + 送入 LLM 做摘要 |
这里有一个技术细节值得注意:Android 系统的短信 ContentProvider(content://sms)只有在 App 被设为默认短信应用时才能读写,而且没有全文索引。自建 AppFunction 可以在自己短信 App 的沙箱内建索引,查询效率远高于系统 CP。
日历
| 用户意图 | 底层通道 | 原因 |
|---|---|---|
| "建一个明天下午3点的会议" | Intent ACTION_INSERT | 系统日历接管 |
| "下周哪天不忙?帮我找两个能约饭的时间" | AppFunction getFreeSlots | 需要查询所有事件 → 计算空闲时段 → 根据语义判断("约饭"需要连续约2小时且避开早晚高峰),单靠查表无法完成 |
系统设置 / 电池
| 用户意图 | 底层通道 | 原因 |
|---|---|---|
| "关掉 WiFi" | WifiManager 系统 API | 有权限即可直接调用 |
| "只剩 20% 电了,帮我开省电模式并关掉费电的后台 App" | AppFunction getBatteryTrend + getPowerHungryApps + enablePowerSaveMode | 需要电量趋势 + 进程功耗排名 + 综合决策,单个 API 只能拿单点数据 |
五、能力穷举方法论
决定每个 App 应该暴露哪些 Tool 时,如果靠"想到什么加什么",迟早会有遗漏。下面四套方法从不同维度交叉覆盖,组合使用可以得到最完整的能力清单。
方法一:按用户意图分层穷举
从用户视角出发,将操作按复杂度分为五级,逐级拆解:
L1 简单查询:用户想看什么数据。举例——查通话记录、查联系人详情、查未接来电、列出文件夹内容、查看文件信息、查某天的日程。
L2 条件筛选:在 L1 的基础上叠加过滤条件。举例——查最近一周与某人的通话记录、按类型和日期范围筛选文件、按联系人加时间范围搜索消息。
L3 聚合计算:对查询结果做统计。举例——统计与某联系人通话总时长、本月通话最多的联系人 Top 5、计算某目录磁盘占用、统计某类事件的占比。
L4 操作执行:用户想改变状态。举例——拨号、删通话记录、创建文件夹、移动/复制/删除文件、发送消息、创建/修改/删除日历事件。
L5 跨 App 联动:需要连接不同 App 的数据。举例——来电时查该号码的关联短信和邮件、行程日期到了查天气和航班、收到取件短信后自动在日历标记。这是 AppFunction 最核心的价值层。
操作方式:对每个 App 按 L1 到 L5 逐层过一遍,列出所有操作点,然后标注每个点是走标准 API 还是需要 AppFunction。
方法二:按数据实体做 CRUD 穷举
从 App 的数据模型出发,列出所有核心实体,对每个实体填 CRUD 矩阵。
以短信 App 为例:
| 实体 | 查 (Read) | 改 (Update) | 删 (Delete) | 增 (Create) |
|---|---|---|---|---|
| 消息 (Message) | getMessage / searchMessages | markRead / flagMessage | deleteMessage | sendMessage |
| 对话 (Conversation) | getConversation / listConversations | pinConversation / muteConversation | deleteConversation | —(随消息自动创建) |
| 联系人 (Contact) | getContact | updateNote | — | — |
| 附件 (Attachment) | getAttachment / listAttachments | — | deleteAttachment | — |
以日历 App 为例:
| 实体 | 查 | 改 | 删 | 增 |
|---|---|---|---|---|
| 事件 (Event) | getEvent / getEventsByRange | updateEvent | deleteEvent | createEvent |
| 日历 (Calendar) | listCalendars / getCalendar | updateCalendarColor | deleteCalendar | createCalendar |
| 提醒 (Reminder) | getReminder / listReminders | snoozeReminder | dismissReminder | createReminder |
操作步骤:列实体 → 填 CRUD 矩阵 → 标出哪些有标准 API(标记为简单路径)、哪些需要 AppFunction → 每个格子用一个真实场景例句验证合理性。
方法三:对照已有系统 API
Android 系统已经暴露了大量能力,直接对照可以避免重复造轮子。重点查四处:
-
Intent Actions:查
android.content.Intent中该领域的所有标准 Action(ACTION_CALL、ACTION_SENDTO、ACTION_INSERT等),标记哪些可以直接用、哪些需要增强(因为不支持返回结果)。 -
ContentProvider:查该领域的系统 CP——
ContactsContract、CallLog、CalendarContract、MediaStore,列出所有可用 URI 和字段,评估非默认 App 下的权限限制。 -
System Manager API:
WifiManager、BatteryManager、NotificationManager等公开 API,标注哪些已可用、哪些被 hide 需要系统权限。 -
Accessibility 事件:作为 AppFunction 覆盖不到的兜底通道,检查 App 界面上哪些交互元素可以被无障碍服务发现和操控。
方法四:对照竞品能力清单
参照现有 AI 助手的能力列表,逐项检查覆盖状态:
对照对象:Apple Siri Intents(SiriKit Domains)、Google Assistant App Actions(BII 目录)、Samsung Bixby Capsules。
Siri Intents 的部分领域参考:Messaging(发消息、查消息)、Calling(拨号、查通话记录、VoIP)、Media(播放、搜索媒体)、Payments(付款、查账单)、Photos(搜索照片)、Workouts(开始/暂停/结束锻炼)、Ride Booking(叫车、查行程)、Notes(创建笔记、搜索笔记、追加内容)。
逐条标记:系统 API 直接支持的 → 无需 AppFunction;需要新建的 → 定义 AppFunction;技术上不可行的(需要 HW/BSP 支持或第三方适配) → 标记为后续规划。
穷举结果输出物
四套方法交叉覆盖后,最终输出一张能力清单:
| App | 能力名称 | 操作级别 | 底层通道 | 优先级 | 备注 |
|---|---|---|---|---|---|
| 拨号盘 | makePhoneCall | L4 | Intent | P0 | 零开发量 |
| 拨号盘 | searchCallHistory | L2 | AppFunction | P1 | 需 CallLog 读权限 |
| 拨号盘 | getCallStats | L3 | AppFunction | P2 | — |
| 短信 | sendMessage | L4 | Intent(可升级到 AF) | P0 | 自动降级 |
| 短信 | searchMessages | L2 | AppFunction | P1 | 需设为默认短信 App |
| 短信 | getConversationSummary | L5 | AppFunction + LLM | P2 | 跨 App + 语义总结 |
| ... | ... | ... | ... | ... | ... |
这张表即后续分阶段开发的依据——先做 P0(能用标准 API 解决,几乎零成本),再做 P1(核心 AppFunction),最后做 P2(跨 App 联动和高级分析)。
六、接入实战:以拨号盘为例
下面给一个完整的接入流程,四步完成。
第一步:创建 AppFunctionService
class DialerAppFunctionService : AppFunctionService() {
override fun onExecuteFunction(
request: ExecuteAppFunctionRequest,
callingPackage: String,
signingInfo: SigningInfo,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>
) {
executor.execute {
try {
val result = when (request.functionIdentifier) {
"searchCallHistory" -> searchCallHistory(request.parameters)
"getContactInfo" -> getContactInfo(request.parameters)
else -> throw AppFunctionException(
AppFunctionException.ERROR_FUNCTION_NOT_FOUND,
"Unknown function: ${request.functionIdentifier}"
)
}
callback.onResult(ExecuteAppFunctionResponse(result))
} catch (e: Exception) {
callback.onError(AppFunctionException(
AppFunctionException.ERROR_APP_UNKNOWN_ERROR, e.message
))
}
}
}
}
第二步:在 AndroidManifest.xml 中声明
<service android:name=".DialerAppFunctionService"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
android:exported="true"
android:process=":functions">
<property
android:name="android.app.appfunctions"
android:value="appfunctions.xml"/>
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService"/>
</intent-filter>
</service>
几个关键点:BIND_APP_FUNCTION_SERVICE 是 signature 级别权限,只有 system_server 能绑定;android:process=":functions" 使用冒号前缀,Android 会自动拼接包名,所以拨号盘的进程名是 com.dreame.dialer:functions,短信 App 的是 com.dreame.sms:functions,各自独立。
第三步:编写元数据
assets/appfunctions.xml:
<appfunctions>
<appfunction>
<function_id>searchCallHistory</function_id>
<enabled_by_default>true</enabled_by_default>
</appfunction>
<appfunction>
<function_id>getContactInfo</function_id>
<enabled_by_default>true</enabled_by_default>
</appfunction>
</appfunctions>
第四步:MCP 层自动发现
这一步不需要额外编码。App 安装后,Android 的 AppSearch 引擎会解析 appfunctions.xml 并写入搜索索引。宿主 App 的 MCP 工具提供者层通过 AppSearch 的 GlobalSearchSession 查询 AppFunctionStaticMetadata 这个 schema,即可自动发现所有已注册的函数,并转换为 MCP Tool 列表供 LLM 使用。
对开发者来说的总工作量:写一个 Service 类 + 一份 XML + 实现具体的函数逻辑。其余的系统路由、权限校验、生命周期管理全部由 Android 框架接管。
七、安全模型
AppFunction 框架自带了六层安全防护,比自行实现 Binder Service 要可靠得多:
第一层,服务绑定权限。 BIND_APP_FUNCTION_SERVICE 是 signature 级别权限,只有系统(system_server)能绑定你的 AppFunctionService。第三方 App 无法直接连接。
第二层,执行权限。 EXECUTE_APP_FUNCTIONS 是 privileged 级别权限,调用方必须显式声明此权限才能跨 App 调用函数。
第三层,元数据级控制。 声明函数时可以设置 restrict_callers_with_execute_app_functions 属性,只有持有执行权限的调用方才能调到该函数。
第四层,身份透传。 AppFunctionService 的 onExecuteFunction 方法参数中直接包含真实的 callingPackage 和 SigningInfo,开发者可以做更细粒度的授权——比如只允许特定签名的 App 调用某些敏感函数。
第五层,用户运行时开关。 用户可以在系统设置中按函数粒度独立开关。例如"允许 AI 查通话记录"和"允许 AI 打电话"是两个独立的设置项。
第六层,企业设备策略。 IT 管理员可以通过 Device Policy 远程控制哪些 AppFunction 在企业设备上可用。
八、总结
- AI 调用 App 能力必须经过 Tool 层。LLM 不认识 Android,它只认识 Tool 定义和 tool call。Tool 是稳定契约,底层实现可以独立演进,LLM 不感知。
- 底层通道有三条:Intent/系统 API(最简单,无返回值)、ContentProvider(能拿数据行)、AppFunction(能拿结构化返回值,安全级别最高)。选择原则是:能用标准 API 就不用 AppFunction——AppFunction 只覆盖标准 API 到不了的场景。
- MCP 和 AppFunction 在不同层次。MCP 是 AI 侧的统一工具界面,AppFunction 是系统级的跨 App 调用通道。正确做法是 MCP 层统一对外(LLM)、对内路由到底层通道(Intent / CP / AppFunction)。不是所有 Tool 都要经过 AppFunction。
- AppFunctionService 推荐使用独立进程(
:functions),与主 UI 进程隔离,但不每次都冷启动——进程会被系统缓存,LMK 统一回收。 - AppFunction 框架的设计边界是"自研 App 之间的能力调用"。第三方 App(Gmail 等)不会适配此框架,这种情况下降级到 Intent 或 AccessibilityService 等替代方案。
- 云端 AI Agent 不能直连手机 AppFunction。链路必须是:云端 → 手机端 MCP 网关(WebSocket)→ MCP 层路由 → AppFunction。
本文讨论的 AppFunction 是 Android 16(API 36)引入的系统框架,底层 AppFunctionManagerService 运行在 system_server 中,通过 Binder 与各 App 的 AppFunctionService 通信。核心类路径:客户端 SDK android.app.appfunctions.AppFunctionManager,服务端实现 com.android.server.appfunctions.AppFunctionManagerService。