AIOS 跨 App调用能力:从 Intent 到 AppFunction 的架构设计

2 阅读16分钟

讨论 Android 平台上 AI Agent 如何以结构化方式调用宿主 App 的各项能力——不同的调用通道各自适合什么场景、为什么不能只用 Intent、以及一套可渐进落地的分层架构设计。


背景

如果把 AI Agent 部署在手机上(比如系统级助手、拨号盘的语音助手),一个基础问题是:AI 要调用手机上的 App 能力时,技术链路应该怎么走?

最简单的答案是用 Intent——Android 十几年来就是这么让 App 之间互相调用的。但问题在于,AI 调用 App 和传统 App 间调用有本质区别:AI 需要的不是"打开一个界面让用户操作",而是"执行一个操作并拿到结构化返回值,以便继续后续推理"。Intent 解决不了返回值的问题。

于是出现了两种补充方案:ContentProvider(已有)和 AppFunction(Android 16 新增)。这篇文章把三种方式放在一起对比,讨论什么时候该用什么,以及如何设计一套可扩展的分层架构。


一、三种底层通道对比

手机端 AI 调用 App 能力,目前有三条技术通道:

维度Intent / 系统 APIContentProviderAppFunction(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:searchCallHistorygetContactInfo

文件管理

用户意图底层通道原因
"打开昨天拍的那张会议纪要"Intent ACTION_VIEW系统 Gallery 直接渲染
"把这段文字保存成笔记"Intent ACTION_CREATE_DOCUMENTSAF 框架,系统接管
"找上个月所有包含'报销'且大于 500KB 的 PDF"AppFunction searchFiles需要全文检索 + 多条件过滤,遍历 SAF 效率过低

文件管理需要暴露的 AppFunction:searchFilesgetFileContentlistDirectory

短信

用户意图底层通道原因
"发短信给李四说我马上到"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 / searchMessagesmarkRead / flagMessagedeleteMessagesendMessage
对话 (Conversation)getConversation / listConversationspinConversation / muteConversationdeleteConversation—(随消息自动创建)
联系人 (Contact)getContactupdateNote
附件 (Attachment)getAttachment / listAttachmentsdeleteAttachment

以日历 App 为例:

实体
事件 (Event)getEvent / getEventsByRangeupdateEventdeleteEventcreateEvent
日历 (Calendar)listCalendars / getCalendarupdateCalendarColordeleteCalendarcreateCalendar
提醒 (Reminder)getReminder / listReminderssnoozeReminderdismissRemindercreateReminder

操作步骤:列实体 → 填 CRUD 矩阵 → 标出哪些有标准 API(标记为简单路径)、哪些需要 AppFunction → 每个格子用一个真实场景例句验证合理性。

方法三:对照已有系统 API

Android 系统已经暴露了大量能力,直接对照可以避免重复造轮子。重点查四处:

  • Intent Actions:查 android.content.Intent 中该领域的所有标准 Action(ACTION_CALLACTION_SENDTOACTION_INSERT 等),标记哪些可以直接用、哪些需要增强(因为不支持返回结果)。

  • ContentProvider:查该领域的系统 CP——ContactsContractCallLogCalendarContractMediaStore,列出所有可用 URI 和字段,评估非默认 App 下的权限限制。

  • System Manager APIWifiManagerBatteryManagerNotificationManager 等公开 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能力名称操作级别底层通道优先级备注
拨号盘makePhoneCallL4IntentP0零开发量
拨号盘searchCallHistoryL2AppFunctionP1需 CallLog 读权限
拨号盘getCallStatsL3AppFunctionP2
短信sendMessageL4Intent(可升级到 AF)P0自动降级
短信searchMessagesL2AppFunctionP1需设为默认短信 App
短信getConversationSummaryL5AppFunction + LLMP2跨 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 工具提供者层通过 AppSearchGlobalSearchSession 查询 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 方法参数中直接包含真实的 callingPackageSigningInfo,开发者可以做更细粒度的授权——比如只允许特定签名的 App 调用某些敏感函数。

第五层,用户运行时开关。 用户可以在系统设置中按函数粒度独立开关。例如"允许 AI 查通话记录"和"允许 AI 打电话"是两个独立的设置项。

第六层,企业设备策略。 IT 管理员可以通过 Device Policy 远程控制哪些 AppFunction 在企业设备上可用。


八、总结

  1. AI 调用 App 能力必须经过 Tool 层。LLM 不认识 Android,它只认识 Tool 定义和 tool call。Tool 是稳定契约,底层实现可以独立演进,LLM 不感知。
  2. 底层通道有三条:Intent/系统 API(最简单,无返回值)、ContentProvider(能拿数据行)、AppFunction(能拿结构化返回值,安全级别最高)。选择原则是:能用标准 API 就不用 AppFunction——AppFunction 只覆盖标准 API 到不了的场景。
  3. MCP 和 AppFunction 在不同层次。MCP 是 AI 侧的统一工具界面,AppFunction 是系统级的跨 App 调用通道。正确做法是 MCP 层统一对外(LLM)、对内路由到底层通道(Intent / CP / AppFunction)。不是所有 Tool 都要经过 AppFunction。
  4. AppFunctionService 推荐使用独立进程(:functions),与主 UI 进程隔离,但不每次都冷启动——进程会被系统缓存,LMK 统一回收。
  5. AppFunction 框架的设计边界是"自研 App 之间的能力调用"。第三方 App(Gmail 等)不会适配此框架,这种情况下降级到 Intent 或 AccessibilityService 等替代方案。
  6. 云端 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