1. Что такое Multi‑App‑сценарии и почему они вам понадобятся
До сих пор мы смотрели на GiftGenius как на единственное внешнее приложение в конкретном чате: пользователь выбирает ваш App в списке, ChatGPT поднимает ваши tools, и вы «главный герой» истории. В реальном Store всё иначе: пользователь может подключить сразу несколько Apps, и ChatGPT будет решать, какое именно приложение вызвать в ответ на конкретный запрос.
Например, в одном чате могут быть:
- корпоративный календарный App, который знает дни рождения коллег;
- GiftGenius, который подбирает идеи подарков;
- commerce‑App компании, который умеет оформлять заказы и оплату.
Пользователь пишет: «Напомни мне про дни рождения коллег и сразу предложи, что подарить и как купить». Модель может последовательно вызвать три разных App: один — за календарём, второй — за идеями подарков, третий — за чекаутом.
Важно понимать: у пользователя нет кнопки «вызови, пожалуйста, App №2 и вот его HTTP‑endpoint». Он общается естественным языком, а ChatGPT выступает роутером — читает descriptions и метаданные всех доступных приложений и решает, кого когда позвать.
Отсюда следуют три ключевые мысли:
- Идёт конкуренция за контекст. Ваш App должен быть выбран среди десятков других на основе описаний, названий и поведения.
- Метаданные становятся вашим «SEO для LLM» — именно они определяют, увидит ли модель GiftGenius в нужный момент или проигнорирует.
- Нужно думать об интероперабельности: ваши ответы должны быть полезны не только человеку в чате, но и другим App, которые читают тот же контекст.
По сути, мы изолированный ChatGPT App превращаем в компонент более крупной системы.
2. Как модель выбирает App: ментальная модель роутинга
Роутинг в Multi‑App‑сценариях устроен примерно так (сильно упрощая, но полезно для разработки):
- У ChatGPT есть список доступных App и их tools с метаданными (имя, описание, JSON Schema параметров, аннотации и _meta).
- Пользователь пишет сообщение.
- Модель строит внутреннее представление намерения (intent) и по сути делает семантический поиск по descriptions tools и приложений, чтобы понять, какие инструменты уместны.
- Если критерии сходятся — вызывает tool или предлагает открыть App.
Здесь есть важный нюанс: descriptions должны быть достаточно различающимися (дискриминативными). Формулировка «Поиск товаров» мало чем отличается от «Поиск подарков» или «Поиск книг», а вот «Поиск идей подарков по базе партнёров GiftGenius» сильно сужает домен и повышает шансы, что именно ваш инструмент будет выбран для подарочных запросов.
Вторая деталь — избегайте коллизий имён. Инструмент с названием get_data в мире десятков App ни о чём не говорит, а вот giftgenius_get_gift_catalog гораздо яснее. И особенно — в комбинации с чётким description.
И, наконец, модель опирается на контекст: если в чате уже упоминались «подарки», «дни рождения» и даже название GiftGenius, это подсвечивает ваш App в глазах роутера.
3. Метаданные и descriptions как LLM‑SEO
Чтобы не относиться к метаданным как к «обязательной формальности в JSON», полезно думать о них как о продуктовой копирайтинговой работе. Официальные рекомендации прямо говорят: treat metadata like product copy и проектируйте «one job per tool».
Условно можно выделить несколько уровней описаний:
| Уровень | Для кого | Что описывает |
|---|---|---|
| Manifest description | Человек + модель | Задачу всего App: зачем его включать в чат |
| Tool description | Модель (routing) | Когда использовать конкретный tool и для каких задач |
| Parameter descriptions | Модель (slot fill) | Как заполнять аргументы, какие значения допустимы |
|
Модель (UI) | Что именно появляется в виджете и нужно ли это дублировать текстовым ответом модели |
widgetDescription особенно важен в мире виджетов: модель не «видит» ваш React‑код, она лишь знает, какие props вы ей передадите и зачем. Хорошо заполненное поле позволяет ей не придумывать «за вас», а, наоборот, адаптировать текстовые ответы с учётом уже показанного UI.
Документация по Apps SDK подчёркивает: ChatGPT решает, когда и как вызывать ваш коннектор (App), опираясь на метаданные. Аккуратные descriptions и docs по параметрам повышают recall — долю ситуаций, в которых модель вообще вспоминает про ваш App, — и уменьшают ложные срабатывания.
Мини‑пример: старый vs новый description для GiftGenius
Допустим, у нас раньше было что‑то вроде:
export const appDescription = `
GiftGenius — помощник для поиска и покупки подарков.
`;
С точки зрения человека всё неплохо, но для роутинга в Multi‑App‑мире лучше сделать акцент на том, когда использовать App и чего он не делает:
export const appDescription = `
GiftGenius — ассистент по идеям подарков.
Используй это приложение, когда пользователь просит придумать подарок
для конкретного человека или повода и уложиться в бюджет.
Не используй его для общего онлайн-шопинга или планирования личных финансов.
`;
Теперь модели проще отличить GiftGenius от общего e‑commerce App или финконсультанта.
4. _meta["openai/widgetDescription"]: объясняем модели наш UI
В таблице выше мы отдельно упомянули _meta["openai/widgetDescription"]. Теперь сфокусируемся на этом уровне: он помогает модели «вообразить» ваш виджет и понять, какие части ответа уже покрыты UI, а что стоит проговорить текстом.
Допустим, наш основной инструмент suggest_gifts возвращает список подарков, а виджет рендерит их как горизонтальную карусель карточек. В описании инструмента мы уже объяснили, когда его использовать, а в widgetDescription объясняем, как выглядит результат.
Пример фрагмента дескриптора инструмента (descriptor, упрощённый, по мотивам рекомендаций):
const suggestGiftsTool = {
name: "suggest_gifts",
description: "Use this to generate gift ideas within user's budget.",
inputSchema: { /* ... */ },
_meta: {
"openai/widgetDescription":
"Показывает горизонтальный список карточек подарков с ценой и кнопкой 'Купить'. Не повторяй названия подарков в тексте ответа."
}
};
Здесь мы сразу добиваемся нескольких целей:
- Модель знает, что UI уже покажет названия и цены — значит, в текстовом ответе можно сосредоточиться на объяснениях и советах, а не дубляже списка.
- Другие Apps (через модель) понимают, что toolOutput — это не просто абзац текста, а структурированный список, который можно «подхватить» в своём контексте.
И да, давайте честно: писать такие описания скучнее, чем кодить, но именно они потом сэкономят вам часы дебага странного поведения модели.
5. Аннотации инструментов: readOnlyHint, destructiveHint, openWorldHint
В Multi‑App‑мире важно не только «когда вызывать», но и насколько безопасно вызывать конкретный инструмент. Для этого Apps SDK вводит набор аннотаций в дескрипторах инструментов.
Идея такая: аннотации — это мягкие подсказки модели о характере операции. Они не заменяют серверную авторизацию, но сильно влияют на то, как ChatGPT будет себя вести в цепочках.
Короткое резюме (концептуально):
| Аннотация | Смысл | Типичное поведение модели |
|---|---|---|
|
Ничего не меняет в данных | Можно вызывать без лишних подтверждений, часто |
|
Меняет состояние (покупки, удаление) | Перед вызовом спросить подтверждение у пользователя |
|
Лезет во «внешний мир» (поиск, веб) | Модель осторожнее с объёмом и качеством результата |
Аннотации (readOnlyHint, destructiveHint, openWorldHint) — это часть стандартного описания инструмента и могут использоваться не только ChatGPT. Поле _meta["openai/isConsequential"] — более узкий, специфичный для ChatGPT сигнал, который дополнительно помогает модели различать «безопасные» и «последовательные» вызовы.
Посмотрим на два инструмента GiftGenius:
- suggest_gifts — чтение каталога, безопасно.
- create_checkout_session — создание чекаута, явное side‑effect‑действие.
Пример описания инструмента suggest_gifts
const suggestGiftsTool = {
name: "suggest_gifts",
description:
"Use this when the user asks for gift ideas for a person or occasion.",
inputSchema: { /* ... */ },
annotations: {
readOnlyHint: true
},
_meta: {
"openai/widgetDescription": "Карусель подарков с ценой и ссылкой.",
"openai/isConsequential": false
}
};
Такой инструмент модель может вызывать несколько раз подряд, в том числе «превентивно», чтобы подготовить варианты заранее, не спрашивая пользователя про каждое действие.
Пример описания инструмента create_checkout_session
const createCheckoutTool = {
name: "create_checkout_session",
description:
"Finalize purchase of selected gifts via Instant Checkout.",
inputSchema: { /* ... */ },
annotations: {
destructiveHint: true
},
_meta: {
"openai/isConsequential": true
}
};
Здесь мы явно сигналим: это write‑операция, у неё последствия (деньги списались, заказ создан), и модель должна запросить подтверждение пользователя перед вызовом, особенно в длинных цепочках с несколькими App.
Важно не переоценивать магию: даже с destructiveHint вы обязаны на сервере перепроверять входные данные, токены и права, как мы обсуждали в модулях про безопасность и авторизацию. Но с точки зрения Multi‑App‑оркестрации аннотации помогают модели не «стрелять» такими инструментами без нужды.
6. Изолированный App vs экосистема: уточняем границы GiftGenius
Когда GiftGenius был единственным App в чате, можно было позволить себе довольно широкий scope: подбор подарков, советы по упаковке, напоминания о праздниках, даже небольшие мотивационные тексты для поздравлений. Модель всё равно вызовет только ваши инструменты.
В Multi‑App‑сценарии такой «я делаю всё» подход начинает вредить:
- роутер хуже различает, когда вы идеальный кандидат, а когда лучше использовать другой App;
- вы начинаете пересекаться с календарём, общим менеджером задач, фин‑планировщиком и так далее;
- при совместном использовании нескольких приложений модель может выбирать «не того» исполнителя и путаться в инструментах.
Лучший подход — чётко очертить зону ответственности:
- GiftGenius: только идеи подарков + помощь с покупкой через ACP/Checkout;
- CalendarApp: события и напоминания;
- Finance‑App: бюджет пользователя в целом, личный финансовый план.
В описаниях App и инструментов полезно явно прописывать не только «Use this when…», но и «Do not use when…». Официальный discovery‑playbook именно это и рекомендует.
Мини‑пример описания инструмента:
description: `
Use this tool when the user explicitly asks for gift suggestions.
Do not use for generic product discovery or price comparison.
`
Такие ограничения не только помогают роутингу, но и делают поведение вашего App более предсказуемым для продукта и QA.
7. Паттерны композиции Apps: pipeline, handoff, shared context
В Multi‑App‑сценариях на практике всплывают три связанные идеи:
- pipeline — несколько App идут один за другим (календарь → подарки → commerce), каждый выполняет свой шаг;
- handoff — вывод одного App становится входом для следующего;
- shared context — вся эта передача происходит через общий текстовый контекст чата, без прямых HTTP‑вызовов между приложениями.
Как мы уже намекали, Multi‑App‑сценарий — это не магия «App A вызывает App B по HTTP». В текущей реализации ChatGPT Apps изоляция довольно жёсткая: приложения не вызывают друг друга напрямую, коммуникация идёт через общий текстовый контекст.
Базовый паттерн можно сформулировать так:
- App A возвращает в чат текст или JSON (часто внутри structuredContent/виджета).
- Модель читает этот вывод.
- В следующем ходе она может вызвать App B, подставив детали из ответа A в аргументы его tools.
Это называют text/context handoff: «Вывод App A → модель → вход App B».
Пример: CalendarApp + GiftGenius + CommerceApp
Разберём конкретный сценарий.
Пользователь: «У шефа завтра день рождения, подбери подарок и сразу оформи покупку».
По шагам:
-
Модель понимает, что нужно сначала понять дату и человека. Она вызывает инструмент календарного App, условно corporate_calendar.list_upcoming_birthdays, и получает структуру:
[ { "name": "Алексей Быков", "date": "2025-11-22", "relation": "manager" } ] -
Дальше модель решает, что пора позвать GiftGenius. Она вызывает ваш suggest_gifts с аргументами, полученными из календаря:
{ "recipientName": "Алексей", "occasion": "birthday", "budget": 150, "relationship": "manager" }Виджет GiftGenius показывает карусель подарков, а текстовый ответ объясняет, почему эти идеи уместны.
-
Пользователь выбирает один‑два варианта (кнопкой в виджете → widgetState), и модель вызывает уже инструмент commerce‑App, например corp_checkout.create_gift_order, с ID выбранных SKU и адресом доставки.
С точки зрения ChatGPT это три разных приложения, но для пользователя — единый разговор. Ключ к тому, чтобы это работало:
- чёткие descriptions инструментов каждого App;
- аккуратные имена (corporate_calendar.list_upcoming_birthdays, а не просто list_events);
- согласованный формат структурированных данных (чтобы идея подарка была описана так, что commerce‑App может её понять).
Визуальная схема
Можно изобразить этот pipeline так:
sequenceDiagram
participant U as Пользователь
participant C as ChatGPT (Router)
participant Cal as CalendarApp
participant G as GiftGenius
participant Com as CommerceApp
U->>C: У шефа завтра ДР, подбери и оформи подарок
C->>Cal: tools.call(list_upcoming_birthdays)
Cal-->>C: [{ name, date, relation }]
C->>G: tools.call(suggest_gifts, { recipient, occasion, budget })
G-->>C: gift suggestions (+ widget)
C-->>U: Пояснения + виджет GiftGenius
U->>C: Нравится вариант #2, купи его
C->>Com: tools.call(create_gift_order, { skuId, address })
Com-->>C: Order confirmation
C-->>U: Готово, заказ оформлен
Ваша задача как разработчика GiftGenius — сделать так, чтобы в этом хоре ваш голос звучал чётко и по делу, а не мешался с другими.
8. Интероперабельность: делаем ответы пригодными для других Apps
В Multi‑App‑мире мало просто «красиво ответить пользователю». Желательно, чтобы ваш toolOutput можно было машинно обработать другим приложением: commerce‑App, аналитическим агентом, workflow‑оркестратором и т.д.
Это означает пару практических вещей:
- использовать структурированный JSON в ответах инструментов, а не сериализованный «человеческий» текст;
- стараться придерживаться стабильных, понятных полей.
Например, можно типизировать результат suggest_gifts так:
export type GiftSuggestion = {
id: string;
title: string;
description: string;
price: number;
currency: string;
forPerson: string;
occasion: string;
purchaseUrl: string;
};
И в ответе инструмента возвращать массив таких объектов:
{
"gifts": [
{
"id": "sku_123",
"title": "Настольный планетарий",
"description": "Мини-проектор звездного неба...",
"price": 89.99,
"currency": "USD",
"forPerson": "Алексей",
"occasion": "birthday",
"purchaseUrl": "https://shop.example.com/sku_123"
}
]
}
Виджет GiftGenius возьмёт это как props и красиво отрендерит карточки, а commerce‑App, увидев этот JSON в контексте, сможет подхватить id и purchaseUrl для дальнейшего чекаута.
Практика Multi‑App‑сценариев показывает: хороший App возвращает данные так, чтобы их мог «съесть» другой App, а не только глаза человека.
9. Практический рефакторинг GiftGenius под Multi‑App
Давайте сведём всё к нескольким конкретным изменениям в нашем учебном приложении.
Уточняем manifest‑description
Представим, что у нас есть openai-app.json (или эквивалент в Next.js‑шаблоне) с описанием:
{
"name": "GiftGenius",
"description": "Gift assistant for finding and buying presents."
}
Сделаем его более явным для роутинга:
{
"name": "GiftGenius",
"description": "Assistant for gift ideas and purchase flows. Use this app when the user asks what to gift a specific person or for a specific occasion within a budget. Do not use for generic online shopping or personal finance planning."
}
Теперь из этого текста уже видно, что это не общий шопинг, не финконсультант и не календарь.
Переписываем descriptions инструментов
Инструмент поиска подарков:
const suggestGiftsTool = {
name: "giftgenius_suggest_gifts",
description: `
Use this when the user asks for gift ideas for a specific person or group,
optionally with a budget or occasion.
Do not use for non-gift product recommendations or travel booking.
`
};
Инструмент, который подтягивает подробности SKU из вашего каталога (read‑only):
const getGiftDetailsTool = {
name: "giftgenius_get_gift_details",
description: `
Use this to fetch more details for a gift suggested earlier by GiftGenius,
for example when the user asks “tell me more about option #2”.
`,
annotations: { readOnlyHint: true }
};
Инструмент покупки — с destructiveHint, как уже показывали.
Обновляем _meta["openai/widgetDescription"]
Допустим, в нашем виджете уже есть карточки с CTA «Купить». Подскажем это модели:
const giftWidgetMeta = {
_meta: {
"openai/widgetDescription": `
Показывает список карточек подарков с описанием, ценой и кнопкой 'Купить'.
Модель не должна повторять полный список в тексте, а лишь прокомментировать выбор и помочь принять решение.
`
}
};
Теперь модель будет реже расписывать простыню из десяти подарков в чате, если они и так видны во виджете, а сосредоточится на объяснениях и логике — что и полезно и для UX, и для стоимости токенов.
10. Multi‑App‑мышление для вашего будущего продукта
Важно переключить голову из режима «как мне победить всех конкурентов и быть единственным App пользователя» в режим «как сделать свой App идеальным модулем в большой экосистеме».
Такой подход даёт несколько практических выгод:
- вам проще объяснять пользователю и ревьюверам Store, зачем ваш App и когда он уместен;
- модели проще принимать решение о роутинге: меньшая путаница, меньше «не тех» вызовов;
- вы сможете сознательно проектировать композиции: сегодня — с календарём и commerce, завтра — с корпоративным HR‑ботом или внутренней CRM.
Официальные гайды по discovery акцентируют: проектируйте «one job per tool», а метаданные рассматривайте как живой артефакт, который нужно тестировать и обновлять, а не статичный текст из первого коммита.
Здесь очень поможет то, что вы уже сделали в предыдущих темах Модуля 20: golden‑кейсы, LLM‑evals, CI‑прогон. Вы можете расширить набор кейсов сценариями «в чате есть и GiftGenius, и CalendarApp», и отслеживать, как изменения в описаниях влияют на выбор App и качество ответов.
11. Типичные ошибки при работе с Multi‑App и композицией
Ошибка №1: описание App «я умею всё на свете».
Если вы пишете в manifest‑description что‑то вроде «умный помощник для любых задач», вы конкурируете не только с другими App, но и с базовым ChatGPT. Роутеру становится сложно понять, когда он обязан звать именно вас, а когда достаточно встроенных возможностей. В Multi‑App‑мире выигрывают приложения с чётким, узким назначением: «подбор подарков», «управление календарём», «анализ логов».
Ошибка №2: размытые descriptions инструментов и коллизии имён.
Инструменты с именами get_data, process_request и пояснением «обрабатывает данные пользователя» прекрасно подходят, чтобы запутать модель. В мире нескольких App легко оказаться в ситуации, когда ваш tool вызывается там, где речь о совершенно другом домене. Правильный путь — связывать домен и действие (giftgenius_get_gift_catalog, calendar.list_birthdays) и явно описывать «Use this when… / Do not use when…».
Ошибка №3: игнорирование _meta["openai/widgetDescription"].
Разработчики часто заполняют только description, а про _meta вспоминают в лучшем случае ради локали. В результате модель не понимает, что именно показывает виджет, и начинает либо дублировать UI текстом, либо, наоборот, обещать пользователю «таблицу с ценами», которой в вашем виджете нет. Пара строк в widgetDescription экономит много таких недоразумений.
Ошибка №4: отсутствие аннотаций readOnlyHint/destructiveHint.
Если все ваши инструменты выглядят одинаково «нейтральными», модель не различает, какие из них безопасно вызывать часто, а какие требуют подтверждения пользователя. В многошаговых сценариях с несколькими App это особенно критично: можно случайно сделать несколько write‑операций подряд без явного участия человека. Не забывайте помечать read‑only‑tools и явно выделять consequential/destructive действия.
Ошибка №5: ответы, рассчитанные только на человека, а не на другие Apps.
Возвращать из инструмента просто «перечень подарков» одной строкой текста соблазнительно, но тогда любому другому App сложнее использовать этот результат. Структурированный JSON с ясными полями (id, price, currency, purchaseUrl, occasion) даёт вам бонус как в UI, так и в композиции: модель может подставить эти данные в аргументы других tools без парсинга естественного языка.
Ошибка №6: попытка внутри одного App реализовать всё, что потенциально может понадобиться пользователю.
Иногда хочется: «ну раз я уже сделал GiftGenius, пусть он ещё и календарь ведёт, и письма коллегам рассылает, и бюджет планирует». В изолированном мире это ещё терпимо, но в Multi‑App‑контексте вы превращаетесь в комбайн, который конфликтует с другими узкими, хорошо заточенными App. Правильнее договориться с собой: мой App делает X и делает это идеально; остальное — чужая ответственность. Такой дизайн сильно упрощает и UX, и развитие экосистемы.
Ошибка №7: отсутствие тестирования поведения в окружении других App.
Разработчики часто тестируют своё приложение в Dev Mode в «чистом» чате, где других App нет. А в Store пользователь легко может иметь десяток подключенных приложений, часть из которых концептуально пересекаются с вашим. Не поленитесь создать тестовый сценарий, где в чате есть соседние App (календарь, общий шопинг, финансы), и прогнать golden‑кейсы: правильно ли модель выбирает GiftGenius в подарочных запросах и не путает ли его с другими участниками?
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ