封装文本生成 AI 技能
场景
让 AI 能在小程序内帮用户写文案、翻译、总结、生成代码。用户只需说"帮我写一份通知"或"翻译这段话",AI 就能调用你的原子接口生成文本并展示结果。
本文通过手写一个 text-gen-skill 来展示核心开发模式:mcp.json 声明 + 小程序端直调 AI 网关,文本生成不经过云函数。
前置条件
- 已完成 从头构建 AI 小程序,项目能在开发者工具中正常运行
- 小程序已开通 AI 开发模式(微信公众平台 → 基础功能 → AI 能力 → 接入模式选择「开发模式」)
- 微信开发者工具 Nightly 版 已安装
- Node.js ≥ 18
-
npx mp-skills --version可正常输出版本号 - 在 云开发控制台 → AI → 生文模型 中开启所需模型(默认
hy3-preview可用于免费体验) - 购买 Token 资源包(小程序成长计划提供免费额度)
实现步骤
第 1 步:创建 SKILL 脚手架
# 在项目根目录下执行
npx mp-skills create text-gen-skill
预期输出:
* 创建 Skill: text-gen-skill
ok 目录已创建: miniprogram/skills/text-gen-skill/
ok SKILL.md 已创建
ok mcp.json 已创建
ok index.js 已创建
ok apis/ 目录已创建
ok app.json 已更新(agent.skills)
ok project.config.json 已更新
[OK] Skill 创建完成
脚手架生成的文件结构:
miniprogram/skills/text-gen-skill/
├── SKILL.md # 技能描述 — AI 引擎读此文件判断何时触发
├── mcp.json # 原子接口声明 — 定义 generateText 的参数和返回值
├── index.js # 入口 — 注册原子接口
└── apis/
└── generateText.js # 接口实现 — 调用云开发 AI 网关
第 2 步:编写 SKILL.md
SKILL.md 告诉 AI 引擎这个技能何时触发、何时不触发。编写 miniprogram/skills/text-gen-skill/SKILL.md:
---
name: text-gen-skill
description: AI 文本生成:AI 写作、文案生成、代码生成、翻译、总结、问答,支持 cloudbase / deepseek / hunyuan 模型。仅处理纯文本生成需求,不处理图片相关需求
version: "1.0.0"
tags: ["微信小程序", "AI开发模式", "平台能力"]
platform: ["wechat-miniprogram"]
---
# AI 文本生成
## 触发场景
用户原话举例:
- **文章写作**:"帮我写一篇关于咖啡文化的公众号文章"、"写一篇小红书笔记推荐春日饮品"
- **文案生成**:"帮我写一句咖啡店的宣传语"、"生成一个新品发布的广告文案"
- **代码生成**:"用 Python 写一个冒泡排序"、"帮我写一个云函数模板"
- **翻译**:"把这段话翻译成英文"、"翻成中文"
- **总结归纳**:"总结一下这篇文章的核心观点"
- **问答对话**:"什么是云开发"、"解释一下小程序生命周期"
## 不适用范围
- 图片生成、图片编辑等视觉类诉求 → 不在本技能范围,由 image-gen-skill / image-edit-skill 处理
- 需要联网搜索的实时信息查询 → 不在本技能范围
- 需要调用具体业务 API 的操作(如点单、排队)→ 不在本技能范围
SKILL.md 的 YAML 头部(frontmatter)规则:
| 字段 | 说明 | 必填 |
|---|---|---|
name | 技能名,与目录名一致 | 是 |
description | 一句话描述,AI 引擎据此判断触发时机 | 是 |
version | 语义化版本 | 推荐 |
tags | 分类标签 | 推荐 |
platform | 目标平台,固定 ["wechat-miniprogram"] | 是 |
第 3 步:配置 mcp.json
mcp.json 是原子接口的"契约文件",声明每个接口的名称、参数和返回值。编写 miniprogram/skills/text-gen-skill/mcp.json:
{
"apis": [
{
"name": "generateText",
"description": "AI 文本生成:根据用户输入的 prompt 生成文本内容,支持写作、翻译、代码生成、总结、问答等多种场景。\n调用前置条件:用户已明确表达了需要生成文本的内容需求。\n【严禁场景】用户未表达具体内容需求时禁止调用;用户表达的是图片生成/编辑需求时禁止调用,应引导至 image-gen-skill 或 image-edit-skill;用户表达的是实时搜索/查询需求时禁止调用。",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "用户的主要输入提示,即要让 AI 生成的内容描述。取值来源:用户原话。如用户说「帮我写一篇咖啡店介绍文案」,则 prompt 为「写一篇咖啡店介绍文案」。【禁止编造】必须从用户原话提取或合理转述。"
},
"systemPrompt": {
"type": "string",
"description": "系统提示词,用于设定 AI 的角色和输出风格。可选参数。根据用户意图自动推断:写文章→「你是一位专业的文案写手...」,翻译→「你是一位专业翻译...」,代码→「你是一位编程专家...」,用户未明确指定角色时可不传。"
}
},
"required": ["prompt"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "AI 生成的文本内容"
}
},
"required": ["text"],
"additionalProperties": false
},
"_meta": {
"ui": {
"componentPath": "components/text-result-card/index"
}
}
}
],
"components": [
{
"path": "components/text-result-card/index",
"relatedPage": "/pages/home/home"
}
]
}
mcp.json 核心字段说明:
| 字段 | 说明 |
|---|---|
apis[].name | 原子接口名称,须与 index.js 注册名一致 |
apis[].description | 接口描述,AI 引擎据此判断何时调用。必须写清前置条件和禁止场景 |
apis[].inputSchema | JSON Schema 定义输入参数,required 标记必填项 |
apis[].outputSchema | 输出参数定义,AI 引擎根据 schema 解析响应 |
apis[]._meta.ui.componentPath | 结果展示组件的路径 |
components | 组件注册,将组件路径映射到使用页面 |
第 4 步:实现 apis/generateText.js
这是核心逻辑:接收 prompt,调用 wx.cloud.extend.AI 生成文本,返回结果。
编写 miniprogram/skills/text-gen-skill/apis/generateText.js:
const { isPreviewMode, ensureCloudInit, successResult, errorResult } = require('../utils/util')
const { seedData } = require('../data/seed')
const { translateError } = require('../../_shared/mp-skills-shared/utils/cloud-error-handler')
async function generateText(params = {}) {
const { prompt, systemPrompt, model = 'hy3-preview', temperature = 0.7, maxTokens = 2048 } = params
if (!prompt) {
return errorResult('缺少 prompt 参数。请提供要生成文本的内容描述。')
}
// 预览模式:返回 mock 数据
if (isPreviewMode()) {
const data = seedData({ prompt, model, systemPrompt })
return successResult(
`已生成文本(模型:${model},预览模式)`,
{ text: data.text, model: data.model, usage: data.usage },
{ rawText: data.text }
)
}
// 正式模式:直接调用 wx.cloud.extend.AI
try {
ensureCloudInit()
const aiModel = wx.cloud.extend.AI.createModel('cloudbase')
const messages = []
if (systemPrompt) {
messages.push({ role: 'system', content: systemPrompt })
}
messages.push({ role: 'user', content: prompt })
const res = await aiModel.generateText({
model: modelToApiName(model),
messages,
temperature,
max_tokens: maxTokens
})
const text = res.choices && res.choices[0] && res.choices[0].message
? res.choices[0].message.content
: (res.text || '')
const usage = res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
return successResult(
`文本生成完成(模型:${model})`,
{
text,
model,
usage: {
promptTokens: usage.prompt_tokens || 0,
completionTokens: usage.completion_tokens || 0,
totalTokens: usage.total_tokens || 0
}
},
{ rawText: text }
)
} catch (err) {
console.error('[generateText] error:', err)
const friendlyMsg = translateError(err, 'text-gen-handler')
return errorResult(friendlyMsg)
}
}
function modelToApiName(model) {
const map = {
cloudbase: 'deepseek-v4-flash',
'deepseek-v4': 'deepseek-v4-flash',
hunyuan: 'hunyuan-2.0-instruct-20251111'
}
return map[model] || model
}
module.exports = generateText
代码要点:
| 要点 | 说明 |
|---|---|
wx.cloud.extend.AI.createModel('cloudbase') | 小程序 SDK 内置的 AI 网关,直接访问云开发大模型。注意:这是 wx API,不是 npm 包 |
model 参数 | 默认 hy3-preview(腾讯混元预览版),小程序成长计划可免费使用。可通过参数切换为 deepseek-v4-flash、hunyuan-2.0-instruct-20251111 等 |
| 预览模式 | isPreviewMode() 基于本地存储标志位,开启后返回 mock 数据,方便不连后端时调试 |
translateError | 共享工具,将云开发 API 原始错误码转为中文友好提示(如 Token 不足、模型未开启等) |
ensureCloudInit() | 确保 wx.cloud.init() 只调用一次,防止重复初始化 |
successResult / errorResult | wx.modelContext 规范的标准响应格式:{ isError, content, structuredContent, _meta } |
modelToApiName | 将用户友好的模型名(cloudbase、deepseek-v4、hunyuan)映射为实际的 API 模型 ID |
第 5 步:实现结果展示组件
组件接收原子接口的结构化结果并渲染。需要创建以下文件:
miniprogram/skills/text-gen-skill/components/text-result-card/index.wxml:
<view class="wecard">
<!-- Header -->
<view class="wecard-header">
<text class="tag tag-model">{{model || 'cloudbase'}}</text>
</view>
<!-- Content: 文本区域 -->
<view class="wecard-content">
<text class="text-body">{{displayText}}</text>
<button wx:if="{{truncated}}" class="btn-ghost btn-expand" bindtap="onTapExpand">
展开全文
</button>
</view>
<!-- Footer: 操作区 -->
<view class="wecard-footer">
<button class="btn-secondary" bindtap="onTapRegenerate">重新生成</button>
</view>
</view>
miniprogram/skills/text-gen-skill/components/text-result-card/index.js:
Component({
data: {
text: '',
model: '',
usage: null,
truncated: false,
displayText: '',
maxPreviewLength: 300
},
lifetimes: {
created() {
const { NotificationType } = wx.modelContext
const modelCtx = wx.modelContext.getContext(this)
modelCtx.on(NotificationType.Result, (data) => {
const sc = (data && data.result && data.result.structuredContent) || {}
const text = sc.text || ''
const model = sc.model || ''
const usage = sc.usage || null
const truncated = text.length > this.data.maxPreviewLength
this.setData({
text,
model,
usage,
truncated,
displayText: truncated ? text.slice(0, this.data.maxPreviewLength) + '...' : text
})
})
}
},
methods: {
onTapRegenerate() {
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '重新生成同样的内容' },
{
type: 'api/call',
data: {
name: 'generateText',
arguments: { prompt: this.data.text, model: this.data.model }
}
}
]
})
},
onTapExpand() {
this.setData({
truncated: false,
displayText: this.data.text
})
}
}
})
miniprogram/skills/text-gen-skill/components/text-result-card/index.wxss:
.wecard {
display: flex;
flex-direction: column;
padding: 16px;
background: #FFFFFF;
border-radius: 12px;
box-sizing: border-box;
width: 100%;
}
.wecard-header {
display: flex;
align-items: center;
height: 40px;
margin-bottom: 8px;
}
.tag {
display: flex;
align-items: center;
height: 20px;
padding: 0 8px;
font-size: 12px;
font-weight: 400;
border-radius: 10px;
background: #F5F5F7;
color: #6E6E73;
line-height: 20px;
}
.wecard-content {
flex: 1;
margin-bottom: 12px;
}
.text-body {
display: block;
font-size: 14px;
font-weight: 400;
color: #1D1D1F;
line-height: 1.6;
word-break: break-word;
}
.wecard-footer {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.btn-secondary {
height: 40px;
min-width: 96px;
padding: 0 16px;
font-size: 14px;
font-weight: 500;
color: #1D1D1F;
background: #F2F2F7;
border-radius: 999px;
border: none;
line-height: 40px;
text-align: center;
}
.btn-ghost {
height: 36px;
padding: 0 12px;
font-size: 12px;
font-weight: 500;
color: #1D1D1F;
background: transparent;
border: 1px solid #E5E5EA;
border-radius: 999px;
line-height: 36px;
text-align: center;
}
@media (prefers-color-scheme: dark) {
.wecard {
background: #1C1C1E;
}
.text-body {
color: #FFFFFF;
}
.tag {
background: #2C2C2E;
color: rgba(255, 255, 255, 0.68);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.12);
color: #FFFFFF;
}
.btn-ghost {
color: #FFFFFF;
border-color: rgba(255, 255, 255, 0.08);
}
}
组件生命周期中的数据流:
wx.modelContext.on(NotificationType.Result)
→ structuredContent = { text, model, usage }
→ setData 更新视图
第 6 步:编写 index.js 注册接口
编写 miniprogram/skills/text-gen-skill/index.js:
const cloudMw = require('../_shared/mp-skills-shared/utils/cloud-middleware')
const generateText = require('./apis/generateText')
function registerAPIs() {
const skill = wx.modelContext.createSkill('skills/text-gen-skill')
skill.use(cloudMw)
skill.registerAPI('generateText', generateText)
console.log('[text-gen-skill] APIs registered')
}
registerAPIs()
index.js 的职责:
| 代码行 | 说明 |
|---|---|
wx.modelContext.createSkill('skills/text-gen-skill') | 创建 SKILL 实例,路径对应分包目录 |
skill.use(cloudMw) | 注册共享中间件(预览模式检测、ensureCloudInit 等) |
skill.registerAPI('generateText', generateText) | 将原子接口名与实现 函数关联,名称须与 mcp.json 的 name 一致 |
第 7 步:创建预览 Mock 数据
为了方便本地调试,实现预览模式的 mock 数据。创建 miniprogram/skills/text-gen-skill/data/seed.js:
function mockGenerateText(prompt, model) {
const mocks = {
'hy3-preview': `这是 AI 对"${prompt}"的模拟回复。预览模式下使用 mock 数据,正式模式将调用混元大模型生成真实内容。`,
cloudbase: `这是 AI 对"${prompt}"的模拟回复。预览模式下使用 mock 数据,正式模式将调用云端大模型生成真实内容。`,
'deepseek-v4': `(DeepSeek 深度推理)关于"${prompt}"的分析如下:\n\n1. 首先,这是一个预览模式的模拟回复\n2. 正式模式下,DeepSeek 模型将提供更深入的分析\n3. 内容包括详细推理过程和结论`,
hunyuan: `(混元模型)关于"${prompt}":\n\n预览模式下展示此占位内容。正式模式将调用混元大模型生成高质量中文内容。`
}
return mocks[model] || mocks['hy3-preview']
}
function seedData(params) {
const { prompt, model = 'hy3-preview', systemPrompt = '' } = params
const text = mockGenerateText(prompt, model)
return {
text,
model,
systemPrompt,
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
}
}
module.exports = { seedData }
第 8 步:创建共享工具函数
创建 miniprogram/skills/text-gen-skill/utils/util.js(预览模式检测、云初始化、标准响应格式化):
const PREVIEW_MODE_KEY = 'mp_skills_preview_mode'
function isPreviewMode() {
return wx.getStorageSync(PREVIEW_MODE_KEY) === true
}
let _cloudInited = false
function ensureCloudInit() {
if (_cloudInited) return
if (!wx.cloud) throw new Error('当前环境不支持 wx.cloud')
wx.cloud.init({ traceUser: true })
_cloudInited = true
}
function successResult(msg, structuredContent, meta) {
const result = { isError: false, content: [{ type: 'text', text: msg }] }
if (structuredContent !== undefined) result.structuredContent = structuredContent
if (meta !== undefined) result._meta = meta
return result
}
function errorResult(msg, structuredContent) {
const result = { isError: true, content: [{ type: 'text', text: msg }] }
if (structuredContent !== undefined) result.structuredContent = structuredContent
return result
}
module.exports = {
PREVIEW_MODE_KEY,
isPreviewMode,
ensureCloudInit,
successResult,
errorResult
}
第 9 步:validate 校验
创建完成后,运行静态校验确认所有文件配置正确:
npx mp-skills validate
预期输出:
[OK] 项目配置检查通过
[OK] 接口定义与实现一致
[OK] 原子组件定义完整
如果校验失败,检查以下常见问题:
mcp.json的接口名与index.js注册名不一致componentPath指向的组件文件不存在mcp.jsonJSON 格式错误
第 10 步:在开发者工具中验证
# macOS
/Applications/wechatwebdevtools.app/Contents/MacOS/cli open --project /your/path/to/my-ai-app
# Windows
"C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat" open --project "D:\...\my-ai-app"
在开发者工具中:
- 基础库版本切换到 3.16.1 或以上
- 编译模式切换到「小程序 AI 编译」
- 左侧 SKILL 列表中应出现
text-gen-skill - 选中后右侧显示
generateText原子接口 - 填入
{"prompt": "帮我写一句咖啡店宣传语"}执行 - 查看返回的文本内容和卡片组件渲染效果
验证清单
-
npx mp-skills validate全部通过 - 开发者工具中
generateText接口可正常调用并返回文本 - 文本结果通过
text-result-card组件渲染 - 「重新生成」按钮可触发相同 prompt 的再次调用
- 超过 300 字的内容可「展开全文」
-
mcp.json声明与index.js注册名一致 -
mcp.json的componentPath指向实际存在的组件文件
文本生成架构
用户输入 → AI 引擎 → wx.modelContext → generateText.js
│
┌──────────┴──────────┐
▼ ▼
wx.cloud.extend.AI 预览模式 Mock
.createModel('cloudbase')
.generateText({...})
│
▼
AI 模型 (hy3-preview)
│
▼
structuredContent:
{ text, model, usage }
│
▼
text-result-card 组件渲染
关键设计决策:
| 决策 | 选择 | 理由 |
|---|---|---|
| 文本生成是否走云函数 | 否,直接调用 wx.cloud.extend.AI | 小程序 SDK 内置 AI 网关,无需额外后端。仅在需要自定义 prompt 预处理/后处理时才走云函数 |
| 模型参数暴露 | 不暴露给前端 UI | 保持原子接口简洁。AI 引擎根据场景自动选择合适模型 |
systemPrompt 是否必填 | 否,AI 自动推理 | 由 AI 引擎根据用户意图推断角色,开发者无需手动指定每种场景的 systemPrompt |
| 结果展示 | 自定义组件 | 比纯文本富表达:可展示模型标签、Token 用量、「重新生成」操作入口 |
常见问题
wx.cloud.extend.AI.createModel('cloudbase') 返回 undefined?
确认小程序已开通 AI 开发模式,且开发者工具基础库版本 ≥ 3.16.1。在工具中切到其他版本再切回来,触发基础库重新下载。
调用返回 "Token 不足" 错误?
在 云开发控制台 → AI → Token 管理 购买 Token 资源包。小程序成长计划用户有免费额度,可在成长计划页面查看。
预览模式怎么切换?
SKILL 的预览模式读取 wx.getStorageSync('mp_skills_preview_mode')。在 app.js 中提供 usePreview() / useProduction() 方法切换,切换后重新执行原子接口即生效。
mcp.json 的 description 写多长合适?
建议写 200-300 字的中文描述,包含:
- 接口做什么
- 调用前置条件(用户已明确表达需求)
- 禁止场景(什么情况绝对不要调用) AI 引擎依赖这段描述做意图匹配,写得太短会导致误触发。