# 接入方式
小程序 AI 开发模式(以下简称此模式)提供了一套智能化的运行环境和开发框架,开发者可根据自身业务要求,参考本文档,将小程序的功能抽象为原子接口和原子组件,并封装成 SKILL,供小程序 AI 调用。
# 一、全局提示词
全局提示词可以用来整体说明此模式下提供的服务范围,提供相关的背景知识、行为逻辑和回答风格,引导小程序 AI 生成猜你想问的内容等等。
另一方面,一个小程序可能存在多个 SKILLs,多个 SKILLs 的关联关系及相关介绍也可以在全局提示词上说明,以帮助模型选择合适的 SKILL 和原子接口来为用户提供服务。
通常,全局提示词是一个名为 AGENTS.md 的文件,在小程序 AI 中,你可以这样指定它的路径:
// app.json
{
"agent": {
"skills": [],
"instruction": "path/to/AGENTS.md",
}
}
instruction 字段目前非必填,文件大小为最大 10000 字节。
# 二、SKILL 声明
一个小程序可声明多个 SKILLs,SKILL 必须封装在一个独立分包里,而一个独立分包可以放置多个 SKILLs,且需要全局开启按需注入 lazyCodeLoading。
app.json 中增加 agent 字段,声明 SKILL 列表(agent.skills 字段),每个 SKILL 的字段说明如下:
| 字段 | 必填 | 说明 |
|---|---|---|
| name | 是 | 名称 |
| description | 是 | SKILL 简要说明 |
| path | 是 | SKILL 分包路径(绝对路径) |
// app.json
{
"lazyCodeLoading": "requiredComponents",
"subPackages": [
{
"root": "path/to/pkg",
"independent": true,
"pages": []
}
],
"agent": {
"skills": [
{
"name": "weather",
"description": "查询天气业务",
"path": "path/to/pkg/weather-skill"
},
{
"name": "shopping",
"description": "购物业务",
"path": "path/to/pkg/shopping-skill"
}
]
}
}
目前一个小程序最多可以声明 30 个 SKILL。
# 三、SKILL 封装
SKILL 包含业务说明(SKILL.md)、模型可调用能力的声明(mcp.json)、原子接口(apis)与原子组件(components)的实现,其目录结构如下:
| 文件 | 说明 | 固定目录结构 |
|---|---|---|
| SKILL.md | SKILL 的详细说明,只支持单文件,不支持引用其他 md 文件 | 是,最大长度 16000 字节 |
| index.js | 注册当前 SKILL 所涉及的所有原子接口 | 是 |
| mcp.json | 模型可调用能力的声明(当前 SKILL 所涉及的所有原子接口声明) | 是,最大长度 24000 字节 |
| components/ | 当前 SKILL 所涉及的原子组件代码实现的所在目录 | 否,仅作为建议 |
| apis/ | 当前 SKILL 所涉及的原子接口代码实现的所在目录 | 否,仅作为建议 |
目前计算 mcp.json 的长度时,会除去所有的 outputSchema 字段及空格、换行符后再计算。
|-- path/to/pkg/weather-skill
| |-- SKILL.md
| |-- mcp.json
| |-- components/
| |-- apis/
| |-- index.js
# 3.1 模型可调用能力的声明
mcp.json 包含所有原子接口的 Schema 声明,其中 componentPath 表示该原子接口返回的结果可由原子组件展示卡片。inputSchema 和 outputSchema 需遵循 JSON Schema 规范。
| 属性 | 是否必填 | 说明 |
|---|---|---|
| name | 是 | 标识符,跟 index.js 中导出的原子接口函数名一致 |
| description | 是 | 原子接口的功能描述 |
| inputSchema | 是 | 原子接口的入参,需为对象格式 |
| outputSchema | 建议填 | 原子接口返回的 structuredContent对应的 schema |
| _meta | 否 | 可指定渲染的原子组件,componentPath 相对于 SKILL 目录,如 { "ui": { "componentPath": "path/to/comp" } } |
下面是只有一个 getWeather 的原子接口的声明示例:
{
"apis": [
{
"name": "getWeather",
"description": "查询当前位置或某地的未来一段时间的天气",
"_meta": {
"ui": {
"componentPath": "path/to/comp"
}
},
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "要查询天气的地点,如城市名、行政区名或经纬度。不传则默认取当前位置",
},
"days": {
"type": "number",
"description": "预报天数,范围1-15,默认7",
}
},
"required": [
"days"
]
},
"outputSchema": {}
}
]
}
# 处理图片/文件内容
小程序 AI 的输入框支持了多模态上传,用户可以发送图片/文件给小程序 AI。如果需要处理用户上传的图片/文件,在原子接口的声明中,在 inputSchema 定义的字段中填写 "format": "image" | "file"`,小程序 AI 会调用相应的原子接口,并将接收到的图片/文件传给对应字段。
{
"name": "EditPhoto",
"description": "帮用户 P 图",
"inputSchema": {
"type": "object",
"properties": {
"imagePath": {
"type": "string",
"description": "本地图片路径",
"format": "image"
},
"query": { "type": "string", "description": "用户的 P 图需求" }
},
"required": [ "imagePath", "query" ]
}
}
# 3.2 原子接口实现
以原子接口 getWeather 为例,其返回值说明如下:
| 属性 | 类型 | 必填 |
|---|---|---|
| isError | boolean | 否,默认为 false |
| content | ContentBlock[] | 是,返回给 LLM 的文本内容,不超过 200 KB |
| structuredContent | { [key: string]: unknown } | 否,返回给 LLM 的结构化数据,不超过 200 KB |
| _meta | { [key: string]: unknown } | 否,对 LLM 不可见,可携带元数据,不超过 200 KB |
其中 isError、structuredContent、content 三个字段的数据是会作为 LLM 的上下文供 LLM 理解,而 _meta 内容对 LLM 不可见,用于携带私有数据,传递给原子组件。
当 isError: true 时,将不进行 GUI 卡片渲染,structuredContent 将被忽略,content 常用于异常流程的引导。
当 isError: false 时,且该原子接口有绑定原子组件,小程序 AI 将倾向于渲染 GUI 卡片,structuredContent 数据会传给原子组件用来渲染(便于模型理解 GUI 卡片的内容),content 用于后续流程引导。比如,如需强制出 GUI 卡片时,可在 content 里追加引导 :「接下来为用户展示 XXX UI 卡片」。
ContentBlock[] 目前只支持 TextContent,其定义如下:
interface TextContent {
type: "text";
text: string;
}
下面是原子接口 getWeather 的代码实现示例:
async function getWeather({ location, days }) {
// 原子接口内部实现,此处省略
return {
isError: false,
content: [{ type: "text", text: "已为您查询到北京未来3天天气" }],
structuredContent: {
location: "北京",
forecasts: [
{ date: "2026-04-23", dayWeather: "晴", nightWeather: "晴", tempHigh: 28, tempLow: 14, wind: "北风2级" },
{ date: "2026-04-24", dayWeather: "多云", nightWeather: "阴", tempHigh: 25, tempLow: 13, wind: "东南风3级" },
{ date: "2026-04-25", dayWeather: "小雨", nightWeather: "中雨", tempHigh: 20, tempLow: 12, wind: "东风4级" }
]
},
}
}
# 3.3 注册原子接口
在 path/to/pkg/weather-skill/index.js 进行注册,当小程序 AI 发起原子接口调用时,就会在注册列表中找到原子接口的函数入口并执行:
const getWeather = require('./apis/getWeather')
const skill = wx.modelContext.createSkill('/path/to/pkg/weather-skill')
// name 需与定义文件中的一致
skill.registerAPI('getWeather', getWeather)
# 中间件机制
为了方便在多个原子接口同时处理一些公共逻辑,我们提供了原子接口中间件的机制,可用于统一登录态,统一上报和错误监听等场景。
- wx.modelContext.createSkill(skillPath: string) // 创建skill
- skill.use(Middleware) // 注册中间件
- skill.registerAPI(name, handler) // 注册原子接口,等同于 wx.modelContext.registerAPI('name', handler)
interface MiddlewareContext {
name: string // 当前执行的原子接口名
skillPath: string // 当前中间件执行时所在的skill
arguments: Record<string, any> // 原子接口的入参
}
type Middleware = (ctx: MiddlewareContext, next: () => Promise<void>) => Promise<void>
示例:
const skill = wx.modelContext.createSkill('pages/weather')
skill.registerAPI('getWeather', getWeather)
skill.use(async (ctx, next) => {
// 1. 统一登录(仅首次)
const token = wx.getStorageSync('token')
if (!token) {
const { code } = await wx.login()
const res = await wx.request({ url: '...', data: { code } })
wx.setStorageSync('token', res.data.token)
}
// 2. 上报:记录开始时间
const start = Date.now()
try {
await next() // 执行实际 handler
// 上报成功
reportLog({
name: ctx.name,
cost: Date.now() - start,
})
} catch (err) {
// 3. 统一错误监听
reportError({
name: ctx.name,
error: err,
cost: Date.now() - start,
})
throw e
})
核心语义:
- 每次原子接口调用都会执行中间件
- next() 前原子接口调用的前置逻辑,next() 后是后置逻辑,try/catch 包裹可捕获错误
- 整个中间件链与原子接口执行时间共享超时上限(当前为 300s)
- 支持多个中间件:按注册顺序形成链,外层 -> 内层 -> handler -> 内层 -> 外层
- 中间件函数为 async 类型,运行时 await 整个链路
# 3.4 原子组件实现
# 3.4.1 原子组件定义
原子组件用于承接原子接口返回的结构化数据的展示。在编码上,其与小程序的自定义组件一致,不过需要注意的是,原子组件不可声明为虚拟组件。
通过 wx.modelContext.getContext / getViewContext 获取原子组件渲染时需要的尺寸、数据等信息。
Component({
lifetimes: {
created() {
const modelCtx = wx.modelContext.getContext(this);
const { NotificationType } = wx.modelContext;
modelCtx.on(NotificationType.Input, (data) => {
// 原子接口的入参数据
});
modelCtx.on(NotificationType.Result, (data) => {
// 原子接口的出参数据
});
const viewCtx = wx.modelContext.getViewContext(this);
const { minHeight, maxHeight, width } = viewCtx.getDimensions();
viewCtx.on(NotificationType.Overflow, (data) => {
// data.contentHeight: 内容总高
// data.overflowHeight: 溢出高度
// data.maxHeihgt: 当前卡片容器的硬性阈值
// 如果给原子组件的 root element 设置 maxHeight 和 overflow: hidden 则无法触发
});
}
}
})
# 3.4.2 关联小程序页面(必须)
原子组件渲染出来的 GUI 卡片上方有个标题栏,其右上方提供进入小程序的入口,该入口需配置与原子组件关联的小程序页面,是必填项。通过此方式进入小程序的场景值为 1442 或 1443。
实现上,分为两步:
1、在 mcp.json 声明原子组件所对应的小程序页面的完整 path(固定 path):
{
"apis": [],
"components": [
{
"path": "path/to/comp",
"relatedPage": "/path/to/related-page"
}
]
}
2、在原子组件渲染时,动态设置页面的 query 参数:
Component({
lifetimes: {
created() {
const modelCtx = wx.modelContext.getContext(this)
this._viewCtx = wx.modelContext.getViewContext(this)
modelCtx.on('result', (data) => {
const { orderId } = data.result.structuredContent
this.setData({ orderId })
// 若只传 query,路径由 mcp.json 中的 components[].relatedPage 决定
this._viewCtx.setRelatedPage({ query: `orderId=${orderId}` })
// 若原子组件可能与多个小程序页面关联,也可传入 path 参数
this._viewCtx.setRelatedPage({ path: '', query: `orderId=${orderId}` })
})
}
},
methods: {
onSpecChange(e) {
const spec = e.detail.value
this.setData({ selectedSpec: spec })
// 状态变化后更新 query 参数
this._viewCtx.setRelatedPage({
query: `orderId=${this.data.orderId}&spec=${spec}`
})
}
}
})
# 3.4.3 原子组件的约束
原子组件的运行环境与原子接口隔离,处于不同的上下文中,其可调用的接口能力也与原子接口有所不同。
原子组件存在以下特性与限制:
- 原子组件的渲染区域有限,其宽度随屏幕宽度变化,其最小高度是宽高比 4:1,最大高度是宽高比 1:1,不超出最小最大高度时,随内容自动撑高
- 初始化时决定卡片高度,后续不可再改变高度
- 仅支持 tap 点击、Image load、image error 事件,不支持其他交互事件
- 默认不支持网络请求和云开发接口,需声明为「实时动态组件」
- 默认不支持定时器接口,如 setTimeout、setInterval,需声明为「实时动态组件」
- 不支持打开小程序接口
- 不支持动画
- 禁止上下滚动(即禁止使用 overflow-y)
- 支持点击后帮用户上行一条文本消息(ModelContext.sendFollowUpMessage),效果等同于用户语音发送一条文本消息
- 支持点击后打开半屏页面(见下节)
# 3.4.4 实时动态组件
对于需要展示实时动态内容的合理的场景,可在 mcp.json 声明需要实时动态能力(支持 wx.request、定时器等接口)
注意:此能力需要单独审核(正常提审即可),请声明使用场景,非必要场景不建议使用
{
"apis": [],
"components": [
{
"path": "path/to/comp",
"permissions": {
"scope.dynamic": {
"desc": "声明使用场景"
}
}
}
]
}
# 3.4.5 原子组件过期态
支持通过接口形式,将已经渲染出来的原子组件置为过期态,防止用户再次点击。
1、组件过期声明
在 mcp.json 的 components 字段下声明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string | 是 | 组件路径(相对于 skill 目录) |
| expirable | boolean | 否 | 声明该组件渲染的卡片是否可被过期,默认 false |
| expiredText | string | 否 | 过期后显示的文案,默认 “服务已过期” |
{
"apis": [
{
"name": "searchRides",
"description": "搜索打车方案",
"_meta": { "ui": { "componentPath": "components/ride-card/index" } },
"inputSchema": { ... }
}
],
"components": [
{
"path": "components/ride-card/index",
"expirable": true,
"expiredText": "店铺已关闭"
}
]
}
2、组件过期接口
提供两种粒度的过期 API:
方式一:wx.modelContext.expireAllCards()
wx.modelContext.expireAllCards(): Promise<{ errMsg: string }>
可在原子接口/组件里调用。所有声明了 expirable: true 的组件(包括自身),将被标记为过期并渲染蒙层。
同时支持按 componentPath 过滤,使所有相同的 componentPath 的原子组件或最近一个特定的 componentPath 的原子组件过期:
// 过期特定组件的卡片(绝对路径)
wx.modelContext.expireAllCards({
componentPaths: ['packageA/weather-skill/components/weather-card/index']
})
// 只过期最近一张匹配的卡片
wx.modelContext.expireAllCards({
componentPaths: ['packageA/weather-skill/components/weather-card/index'],
match: 'latest'
})
方式二:wx.modelContext.getViewContext(this).expirePreviousCards()
const viewCtx = wx.modelContext.getViewContext(this)
viewCtx.expirePreviousCards(): Promise<{ errMsg: string }>
只能在原子组件里调用,之前渲染过的原子组件,声明了 expirable: true 的将被标记为过期并渲染蒙层。
同时支持按 componentPath 过滤,使所有相同的 componentPath 的原子组件或最近一个特定的 componentPath 的原子组件过期:
// 过期特定组件的卡片(绝对路径)
viewCtx.expirePreviousCards({
componentPaths: ['packageA/weather-skill/components/weather-card/index']
})
// 只过期最近一张匹配的卡片
viewCtx.expirePreviousCards({
componentPaths: ['packageA/weather-skill/components/weather-card/index'],
match: 'latest'
})
3、组件过期事件
原子组件被置为过期时,会收到过期事件,此时原子组件内部可以做一些必要的清理。
Component({
lifetimes: {
created() {
const { NotificationType } = wx.modelContext;
const viewCtx = wx.modelContext.getViewContext(this);
viewCtx.on(NotificationType.Expire, (event) => {
console.log('卡片已过期', event.expiredText)
// 停止轮询、禁用按钮等清理逻辑
})
}
}
})
# 3.5 半屏页面
半屏页面是原子组件内容的延伸,是个非必要流程,只有当需要展示更多详情信息,或者需要用户补充信息时,才使用半屏页面能力,通过点击原子组件上的某个按钮打开半屏页面。
半屏页面的环境可以认为是与小程序的环境一致,可以用已有的小程序页面来加载,但不允许任何跳转,包括所有跳出半屏的接口、页面路由接口、广告相关接口组件。
当用户需要进行下一步时,应是点击半屏里的某个按钮,触发上行一段文本消息,此时会同时关闭半屏页面回到小程序 AI 界面,小程序 AI 继续处理新的用户请求。
注:半屏打开小程序页面的场景值为 1433 或 1434
注:通过 wx.getDetailPageCloseButtonBoundingClientRect 接口适配左上角关闭按钮
# 3.5.1 半屏页面实现
在原子组件内,响应点击并打开半屏页面(原子接口内不可调用):
<view> xxx </view>
<view bind:tap="showDetail">查看未来 15 天的天气</view>
Component({
lifetimes: {
created() {
}
},
methods: {
showDetail() {
const viewCtx = wx.modelContext.getViewContext(this);
viewCtx.openDetailPage({
url: '/packageA/pages/weather-detail?foo=bar'
})
}
}
})
在半屏页面中,代用户上行一条文本消息(同时会关闭半屏页面):
注意:需要以用户第一人称的角度来发送这条消息
Page({
onTap() {
const ctx = wx.modelContext.getContext()
ctx.sendFollowUpMessage({
content: [
{
// 必须传
type: 'text',
text: '选择拿铁'
},
{
// 指定下一步应该调用哪个原子接口,若不传,则由模型推理
type: 'api/call',
data: {
name: 'selectGoods',
arguments: {}
}
}
]
})
}
})
若半屏页面是 web-view 加载的 h5 页面,则上行文本消息的方式是:
// 前置 wx.config()
wx.ready(function () {
WeixinJSBridge.invoke('invokeMiniProgramAPI', {
name: 'sendFollowUpMessage',
arg: {
content: [
{
// 必须传
type: 'text',
text: '选择拿铁'
},
{
// 指定下一步应该调用哪个原子接口,若不传,则由模型推理
type: 'api/call',
data: {
name: 'selectGoods',
arguments: {}
}
}
]
}
}, function (res) {})
})
# 3.5.2 半屏页面的约束
半屏页面的执行环境与小程序页面一致,但可调用的接口能力有所不同。半屏页面存在以下限制:
- 所有跳出半屏去往公众号、视频号、其他小程序、表情、问一问、地图 App 的接口
- 所有页面路由接口
- 所有广告相关的接口组件
# 3.5.3 半屏页面预加载
为了给用户提供更好的体验,我们提供了预加载半屏页面的能力,若当前原子组件存在调用拉起半屏页面的逻辑,开发者可以在原子组件创建后调用 preloadDetailPage
Component({
lifetimes: {
attached() {
const viewCtx = wx.modelContext.getViewContext(this);
viewCtx.preloadDetailPage({
url: '/packageA/pages/weather-detail?foo=bar'
})
}
},
})
# 四、通过文字链拉起小程序
小程序 AI 可以在回复用户的文本消息中带上小程序短链,供用户点击打开小程序页面。
该能力需要开发者提供页面元数据,小程序 AI 将根据用户问答的上下文,在必要的时候生成小程序短链发送给用户。短链上的文本由模型根据聊天上下文、页面 name、页面 description、页面 query 来动态生成。通过此方式进入小程序的场景值为 1435 或1436。
注意:通过文字链拉起小程序为兜底策略,原则上用户流程需在小程序 AI 内闭环,核心功能使用文字链可能会影响审核或触发召回降权,建议开发者谨慎使用。
页面元数据定义
app.json 里增加 pageMetadata 字段
// app.json
{
"agent": {
"skills": [],
"pageMetadata": "path/to/page-meta.json",
}
}
page-meta.json 中定义了页面的功能和所需 query。 query 为一个标准的 object 格式 JSON-Schema。
| 字段 | 必填 | 功能 |
|---|---|---|
| path | 是 | 页面路径,可带固定的 query 信息 |
| name | 是 | 页面标题 |
| description | 是 | 页面的功能描述 |
| query | 否 | 页面 query |
// page-meta.json
{
"pages": [
{
"path": "pages/home/home",
"name": "首页",
"description": "展示最新的内容和推荐"
},
{
"path": "pages/detail/detail",
"name": "商品详情",
"description": "展示特定商品的信息",
"query": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "商品的唯一标识符"
}
},
"required": [
"id"
]
}
}
]
}
目前 page-meta.json 的最大长度为 8000 字节。
# 五、小程序与小程序 AI 交互
我们提供了一些能力,方便在小程序环境下能与小程序 AI 进行交互
# 1、打开小程序 AI 界面
现有的小程序页面内可通过 wx.openAgent 接口打开小程序 AI 界面,支持以下参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| followUpMessage | string | 自动帮用户上行一条文本消息(支持中) |
| context | string | 可携带当前页面上下文,便于模型理解以响应用户询问 |
当用户从胶囊入口打开小程序 AI 界面时,会触发 wx.onAgentOpen 事件,事件回调里返回数据,同样能实现帮用户上行一条文本消息以及携带当前页面上下文:
function callback() {
return { followUpMessage, context }
}
wx.onAgentOpen(callback)
wx.offAgentOpen(callback)
同时,为了更好的兼容,我们提供了 wx.checkIsSupportAgent() 接口,判断当前设备/用户支持打开小程序 AI 界面,再调用 wx.openAgent()
wx.checkIsSupportAgent({
success(res) {
if (res.isSupport) wx.openAgent()
}
})
# 2、返回小程序 AI 界面
当用户通过「文字链」或者「卡片右上角的关联页面入口」进入小程序后,小程序可通过 wx.navigateBackAgent 返回小程序 AI 界面,可携带数据给到模型,让小程序 AI 继续完成用户的意图。该接口支持以下参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| followUpMessage | object | 自动帮用户上行一条文本消息,可携带 api/call |
| context | string | 可携带当前页面上下文,便于模型理解以响应用户询问 |
wx.navigateBackAgent({
followUpMessage: {
content: [
{
// 必须传
type: 'text',
text: '选择拿铁'
},
{
// 指定下一步应该调用哪个原子接口,若不传,则由模型推理
type: 'api/call',
data: {
name: 'selectGoods',
arguments: {} // 长度限制 1000 个字符
}
}
]
},
context: '',
})