Рекомендации по обеспечению качества AppFunctions

AppFunctions – это практический эквивалент MCP-серверов на Android. Они позволяют делиться частями логики приложения с ИИ-агентами и помощниками.

Агенты используют описания, чтобы выбрать AppFunction, которая выполнит запрос. Этот процесс выбора работает так же, как и традиционный поиск инструментов MCP, когда ИИ-приложения используют описания серверов и инструментов MCP для выбора. Ваше приложение действует как сервер MCP Android, а каждая функция AppFunction – как один из его инструментов. Укажите следующие описания:

  • Описание приложения (MCP-сервер Android). Описание приложения – это первое, что помогает агентам понять, что оно может делать. Чтобы агентам было проще найти ваше приложение, укажите его основной домен и кратко перечислите задачи, которые оно может выполнять с помощью AppFunctions.
    • Напишите описание самостоятельно в XML-файле метаданных приложения.
    • В описании приложения можно указать то, что не вошло в описания отдельных функций, например порядок их вызова.
  • Описания функций AppFunctions (инструментов). В описании каждой функции объясняется поведение одной AppFunction. Описания функций обычно строятся вокруг глагола, например обновить, создать или забронировать, и ресурса, например настройки, канал или комната.

Рекомендации по описанию функций

Агенты используют описания функций, чтобы определить, какая из них подходит для запроса и как ее вызвать.

Пишите краткие и однозначные описания AppFunction

Чем точнее описание, тем выше вероятность, что агент выберет правильную функцию для запроса. В таблице ниже приведены рекомендации по составлению описаний функций.

Что следует делать Чего делать нельзя
Опишите назначение функции и результат ее выполнения простым и нейтральным языком. Не используйте описания в качестве запросов. Команды вроде "Ты ДОЛЖЕН сказать" и "Ты должен спросить у пользователя" указывают агенту, как себя вести, а не описывают функцию.
В начале описания указывайте самую важную информацию. Не добавляйте в описания лишние слова, например "Эта функция используется для". Лишние слова делают описание длиннее, но не понятнее.
Точно отражать, что делает код, включая его параметры и выходные данные. Например, функция, описанная как предназначенная только для чтения, не должна записывать или удалять данные. Не используйте вводящие в заблуждение названия, например fetchData для функции, которая удаляет пользовательские данные.
Описания функций, включая параметры и тип возвращаемого значения, должны быть в формате KDoc. При выборе названий следуйте руководству по стилю Kotlin и составляйте описания, понятные без знания реализации. Не используйте слишком много заглавных букв или специальные символы, чтобы привлечь внимание к частям описания, например **CRITICAL** или VERY IMPORTANT.
Используйте человекочитаемые названия для AppFunctions.
Четко и прямо указывайте, что делает каждая функция AppFunction. Не давайте процедурных инструкций, связанных с другими функциями, например "Сначала найдите контакт". Агенты используют описание приложения, чтобы планировать вызовы функций, поэтому укажите порядок в нем.
Опишите, что агент может ожидать от функции, используя формулировки, такие как "Если у вас есть X, то Y".
Предположим, что у агентов есть собственные инструкции по использованию AppFunctions в целом. В описании функции говорится, что она делает, а в инструкциях агента – как ее использовать. Не пытайтесь переопределить или встроить инструкции на уровне агента в описания функций.

Пишите код с учетом инкапсуляции

Описание каждой функции должно быть самодостаточным, чтобы агент мог использовать только ее. В таблице ниже приводятся рекомендации по инкапсуляции.

Что следует делать Чего делать нельзя
Пишите описания функций, которые являются конкретными и целенаправленными, с одной четко определенной целью. Например, "создавать, находить и удалять личные задачи". Не пишите слишком общее описание, иначе агент может вызывать функцию в неподходящее время или по неправильным причинам.
Не создавайте AppFunctions, которые выполняют несколько задач, и не добавляйте много AppFunctions для одного и того же сценария использования. Агенты с большей вероятностью выберут неправильную функцию, если у нескольких функций похожие намерения.
Не ссылайтесь на другие функции AppFunctions в своем или других приложениях и не пытайтесь влиять на их поведение. Это может привести к непреднамеренным зависимостям и побочным эффектам.

Допустимые действия и передача данных

Чтобы защитить конфиденциальность пользователей и их безопасность, когда агенты используют функции вашего приложения, следуйте этим рекомендациям:

Что следует делать Чего делать нельзя
Выполняйте только безопасные, неразрушающие задачи, которые пользователь обычно делает в вашем приложении, где агент может предоставить четкие параметры из запроса пользователя, например создание задачи с заголовком или поиск заметок.
Передавайте только информацию, которая не является личной или конфиденциальной, или данные, на передачу которых пользователь дал явное согласие. Не предоставляйте агенту широкий доступ к пользовательским данным или системным ресурсам.
Не создавайте функции AppFunctions, которые запрашивают или возвращают пароли, финансовые данные, закрытые ключи или другую конфиденциальную личную информацию.
Выполнять действия, запрошенные пользователем, даже если они запускаются позже или в фоновом режиме, например отправлять сообщение в выбранное пользователем время. Не выполняйте действия, которые пользователь не запрашивал, например задачи, которые запускаются сами по себе или выходят за рамки запроса.
Сосредоточьтесь на функциях, которые выигрывают от использования естественного языка, например на задачах, которые проще выразить в разговоре, чем через навигацию по интерфейсу. Например: "[ИИ-агент], помоги мне найти свободное время в календаре на следующей неделе, а затем предложи рейс в Чикаго в один из этих дней".
На устройствах других типов, например телевизорах, Android Auto и Android Automotive OS, уважайте конфиденциальность пользователей. Не раскрывайте информацию и не выполняйте действия, которые не поддерживаются интерфейсом приложения на этом типе устройств.
Четко и однозначно определите назначение функции. Не выдавайте свой продукт за другой инструмент или сервис. Название и описание функции не должны быть похожи на другие инструменты, иначе агент может выбрать вашу функцию по ошибке.

Разработка параметров и возвращаемых значений

Тщательная разработка параметров и возвращаемых значений предотвращает двусмысленность и снижает вероятность недопустимых вызовов. Следуйте этим правилам:

Что следует делать Чего делать нельзя
Определяйте параметры только для тех данных, которые нужны функции. Не запрашивайте информацию, которая не используется функцией. Это минимизирует раскрытие данных и упрощает использование функции.
Используйте названия параметров, которые отражают ожидаемые входные данные, например taskId, searchQuery или recipientEmail. Не используйте названия, похожие на full_context или conversation_history. Такие названия неоднозначны, поскольку агенты используют эти термины для собственного контекста и истории разговоров. Агент может неправильно их интерпретировать и передать в функцию большой объем ненужных данных, что приведет к их утечке.
Используйте типы, поддерживаемые AppFunctions, вместо того чтобы определять собственные. Например, LocalDate, LocalTime, LocalDateTime или Instant для дат и времени, а Uri – для ссылок. Библиотека представляет эти типы одинаково в каждом приложении, поэтому агенты получают согласованный формат. Полный список поддерживаемых типов можно найти в разделе AppFunctionSerializable.
Если параметр не является существенным для цели функции, сделайте его необязательным. Это дает агенту гибкость, когда у него есть только частичная информация от пользователя. Подробнее о необязательных параметрах…
Если AppFunction обновляет существующие ресурсы, убедитесь, что каждый параметр может различать значения "установить как null" и "не изменять". Пример приведен в разделе Обновление существующих ресурсов.
Чтобы агент мог открыть определенный экран в вашем приложении, верните PendingIntent для этого экрана. Агент может запустить PendingIntent как есть, не создавая намерение для вашего приложения. Создайте его с явным намерением и 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(),
    )
}

Обновление существующих ресурсов

В следующем примере используется оболочка SetField, допускающая значение NULL, для каждого поля, которое агент может обновить. Нулевое значение SetField означает "не изменять", а SetField(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, что не позволяет определить тип сбоя.