AppFunctions 实际上相当于 Android 上的 MCP 服务器。借助这些工具,您可以与 AI 智能体和助理分享应用的部分逻辑。
智能体会根据说明选择可满足请求的 AppFunction。此选择过程类似于传统的 MCP 工具发现,其中 AI 应用使用 MCP 服务器和工具说明来指导其选择。您的应用充当 Android MCP 服务器,每个 AppFunction 充当其工具之一。提供以下广告内容描述:
- 应用说明(Android MCP 服务器)。应用说明是智能体了解应用功能的第一入口。为了帮助智能体发现您的应用,请在说明开头突出其主要领域,并简明扼要地列出 AppFunctions 可让应用完成的任务。
- 在应用元数据 XML 文件中自行编写应用说明。
- 使用应用说明来描述各个函数说明无法描述的内容,例如调用函数的顺序。
- AppFunction 说明(工具)。每个函数说明都解释了一个 AppFunction 的行为。函数说明通常围绕动词(例如更新、创建或预订)和资源(例如设置、频道或会议室)进行结构化。
函数说明指南
智能体依靠函数说明来确定哪个函数适合请求以及如何调用该函数。
撰写简明扼要、清晰明确的 AppFunction 说明
描述越明确,代理就越能可靠地为请求选择正确的函数。下表列出了函数说明中的注意事项:
| 正确做法 | 错误做法 |
|---|---|
| 使用清晰、中立的语言描述函数的用途和运行结果。 | 请勿将说明用作提示。“你必须说”和“你必须询问用户”等命令会指示智能体如何行为,而不是描述函数。 |
| 在说明开头提供关键信息,并保持简明扼要。 | 不要在说明中填充无意义的内容,例如“此函数用于”。填充词会使说明变长,但不会使其更清晰。 |
| 准确反映代码的功能,包括其参数和输出。例如,描述为只读的函数不应写入或删除数据。 | 请勿使用误导性名称,例如将删除用户数据的函数命名为 fetchData。 |
| 以 KDoc 格式编写函数说明,包括参数和返回值类型。遵循 Kotlin 样式指南进行命名,并使说明在不了解实现的情况下也能理解。 | 请勿过度使用大写字母或特殊字符(例如 **CRITICAL** 或 VERY IMPORTANT)来吸引用户注意说明中的某些部分。 |
| 为 AppFunctions 使用人类可读的名称。 | |
| 清晰、直接地说明每个 AppFunction 的功能。 | 请勿提供涉及其他功能的程序性说明,例如“先搜索联系人”。代理会使用应用说明来规划跨函数的调用,因此请在应用说明中描述该顺序。 |
| 描述智能体可以从函数中获得什么,例如“如果您有 X,那么 Y”。 | |
| 假设智能体有自己的 AppFunctions 使用说明。函数说明会说明函数的作用,而智能体的指令则涵盖如何使用函数。 | 请勿尝试在函数说明中替换或嵌入智能体级指令。 |
编写代码时牢记封装
确保每个函数说明都是自包含的,以便引导智能体仅使用该函数,而不使用其他函数。下表列出了封装准则:
| 正确做法 | 错误做法 |
|---|---|
| 撰写具体且重点突出的函数说明,并提供单一、明确的用途。例如,“创建、查找和删除个人任务”。 | 请勿撰写过于宽泛的说明,这可能会导致智能体在不恰当的时间或出于错误的原因调用您的函数。 |
| 请勿创建多用途 AppFunction,也不要针对同一用例公开多个 AppFunction。当多个函数具有相似的 intent 时,智能体更有可能选择错误的函数。 | |
| 请勿引用或尝试影响您应用中或其他应用中的其他 AppFunction 的行为。这可能会产生意外的依赖项和副作用。 |
允许的操作和数据共享
当智能体使用应用的功能时,请遵循以下准则来保护用户隐私和安全:
| 正确做法 | 错误做法 |
|---|---|
| 仅执行用户通常会在应用中执行的安全、非破坏性任务,其中代理可以根据用户请求提供明确的参数,例如创建带有标题的任务或搜索记事。 | |
| 仅分享不属于高度个人化或机密的信息,或用户明确同意为该操作分享的数据。 | 请勿向智能体授予对用户数据或系统资源的广泛访问权限。 |
| 请勿设计需要或返回密码、财务详细信息、私钥或其他高度敏感个人信息的 AppFunction。 | |
| 执行用户请求的操作,即使这些操作稍后运行或在后台运行,例如在用户选择的时间发送消息。 | 请勿执行用户未请求的操作,例如自行启动或超出请求范围的任务。 |
| 专注于那些能从自然语言中受益的功能,例如,通过对话比通过界面导航更容易表达的任务,例如:“[AI 智能体],你能帮我在日历中找到下周的一些空闲时间,然后建议在其中一天飞往芝加哥的航班吗?” | |
| 在手机以外的设备规格上,请尊重用户在共享设备(例如电视、Android Auto 和 Android Automotive OS)上的隐私。 | 请勿披露应用界面在该外形规格上不支持的信息,或执行应用界面在该外形规格上不支持的操作。 |
| 清晰且唯一地标识函数的用途。 | 不得冒充其他工具或服务。函数的名称和说明不得与其他工具过于相似,以免造成混淆,导致代理错误地选择您的函数。 |
参数和返回值设计
精心设计参数和返回值可避免歧义,并减少无效调用的可能性。请遵循以下惯例:
| 正确做法 | 错误做法 |
|---|---|
| 仅为函数所需的数据定义参数。 | 请勿请求该功能不使用的信息。这样可以最大限度地减少数据泄露,并简化函数的使用。 |
使用反映预期输入的参数名称,例如 taskId、searchQuery 或 recipientEmail。 |
请勿使用 full_context 或 conversation_history 等名称。此类名称不明确,因为智能体会将这些术语用于自己的上下文和对话记录。智能体可能会误解这些参数,并将大量不必要的数据传递给您的函数,从而导致数据渗漏。 |
请使用 AppFunctions 支持的类型,而不是自行定义类型,例如,使用 LocalDate、LocalTime、LocalDateTime 或 Instant 表示日期和时间,使用 Uri 表示链接。该库在每个应用中都以相同的方式表示这些类型,因此代理会获得一致的格式。如需查看受支持类型的完整列表,请参阅 AppFunctionSerializable。 |
|
| 如果某个参数对函数用途而言并非必不可少,请将其设为可选。这样,当代理仅从用户那里获得部分信息时,便可以灵活地进行处理。如需了解详情,请参阅可选参数。 | |
| 如果您的 AppFunction 会更新现有资源,请确保每个参数都能区分“设为 null”和“不更改”。如需查看示例,请参阅对现有资源的更新。 | |
如需让智能体在应用中打开特定界面,请为该界面返回 PendingIntent。代理可以按原样启动 PendingIntent,而无需为您的应用构建 intent。请使用显式 intent 和 FLAG_IMMUTABLE 创建它,这样代理就无法更改其指向的位置。 |
可选参数
只有可为 null 的类型、基元类型、列表类型和数组类型可以设为可选。如果代理省略了可选参数,库会传递类型的默认值(null、零、false 或空),而不是您声明的默认值。在函数正文中解析有意义的默认值,如以下示例所示:
/**
* Creates a note.
*
* @param title The title of the note.
* @param content The body text of the note.
* @param attachments The files to attach to the note.
* @param owner The ID of the user who owns the note.
* @return The created note.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun createNote(
title: String, // Required
content: String? = null, // Optional
attachments: List<Attachment> = emptyList(), // Optional
owner: String? = null, // Optional
): Note = withContext(Dispatchers.IO) {
noteRepository.createNote(
title = title,
// The library ignores declared default values and passes null for
// omitted nullable parameters, so resolve meaningful defaults here.
content = content ?: "",
attachments = attachments,
owner = owner ?: currentUserId(),
)
}
更新现有资源
以下示例针对智能体可以更新的每个字段使用可为 null 的 SetField 封装容器。SetField 为 null 表示“不更改”,SetField(null) 表示“设置为 null”:
/** A new value for a field of an existing item. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class SetField<T>(
/** The new value of the field. */
val value: T,
)
/**
* Updates an existing note. Omitted fields keep their values.
*
* @param noteId The ID of the note to update.
* @param title The new title of the note.
* @param content The new body text of the note.
* @return The updated note.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun updateNote(
noteId: String,
title: SetField<String>? = null,
content: SetField<String?>? = null,
): Note = withContext(Dispatchers.IO) {
val note = noteRepository.getNote(noteId)
?: throw AppFunctionElementNotFoundException(
"No note found with ID $noteId"
)
noteRepository.updateNote(
noteId = noteId,
// A null SetField means "don't change", and SetField(null) means
// "set to null", so don't use ?: for nullable fields.
title = title?.value ?: note.title,
content = if (content != null) content.value else note.content,
)
}
错误处理
当调用失败时,代理会收到函数抛出的异常,包括其类型和消息。请遵循以下惯例,以便代理可以从故障中恢复或向用户报告故障:
| 正确做法 | 错误做法 |
|---|---|
抛出与原因匹配的 AppFunctionException 子类,并附带一条用通俗语言描述问题的消息,例如,当参数无效时抛出 AppFunctionInvalidArgumentException,或者当请求的项不存在时抛出 AppFunctionElementNotFoundException。类型会告知智能体发生了何种类型的失败,而消息则会提供智能体恢复所需的详细信息,例如更正参数或告知用户。对现有资源的更新中的 updateNote 示例展示了此模式。 |
请勿让其他异常从函数中逃逸。代理会将其作为 AppFunctionAppUnknownException 接收,这无法识别故障类型。 |