# 最佳实践
为了达到更好的用户体验,我们提供了一些最佳实践,从产品设计和 SKILL 封装两个方面展开。
# 一、产品设计
以下以 WeStoreCafe 点单 SKILL 为参考案例,梳理此模式下的核心设计点:
对话承接用户的模糊意图:当用户表达"想喝点清爽的""最近有什么推荐"这类非结构化需求时,由 AI 完成理解和推荐,再引导至原子组件完成确认和下单。
可通过自然语言修改内容:用户无需跳出对话,可直接对话修改如规格、时间、地点等枚举值或明确实体的字段内容。
历史数据与个人资产可查询、可操作:高频消费场景下的核心诉求是效率,"再来一杯上次喝的""用积分兑换一张优惠券"这类指令可被执行,能提升使用效率。
服务进程动态实时更新:订单或服务原子组件随状态推进动态更新,而非静态展示,防止用户错过关键更新。
# 二、SKILL 封装
# 1. 注意力原则
LLM 在推理调用哪个原子接口及其参数、决定怎么回复用户时,会综合多个上下文信息源做判断,但 LLM 对这些信息源的注意力权重并不平均,离当前决策点越近的位置权重越高。
| 信息源 | 注意力 | 说明 |
|---|---|---|
原子接口返回的 content | ★★★★★ | 离当前决策点最近,模型会把它当成“事实承接 + 直接指令”来理解。若这里出现错误话术,会导致 LLM 偏离 SKILL.md 的约定 |
原子接口声明(mcp.json)里的 description | ★★★★ | 写清楚“业务对象 + 触发条件 + 严禁场景”,尤其前 1-2 句,会使 LLM 选择原子接口的准确率大幅提升;写得模糊时 LLM 容易选错, 即使 SKILL.md 写得详细也会出现幻觉 |
原子接口声明(mcp.json)里的 inputSchema 字段下的 description | ★★★★ | LLM 生成原子接口的参数的核心参考。在这里进行字段约束,比在 SKILL.md 长文里写类似内容更有效 |
| SKILL.md | ★★★ | SKILL.md 适合写业务流程编排、跨接口规则、意图分流表等综合性内容 |
由上可以推导出的接入时的注意事项:
- 多处约束冲突时,按权重优先级遵循:LLM 大概率遵循权重更高的位置的指令。因此相同指令多处都需要写时,低权重位置须与高权重位置意图一致,避免互相矛盾。
- 核心约束不要全依赖
SKILL.md拦截:硬约束应通过原子接口返回的content字段或者原子接口声明里的description字段说明。更好的是,原子接口执行能够基于规则在代码层面做强校验。 - 同一语义不要在多处重复:重复书写不会增强效果,反而易因措辞不一致引发冲突。
# 2. 内容分工
| 信息源 | 说明 |
|---|---|
原子接口返回的 content | 本次调用结果与下一步动作 |
原子接口声明(mcp.json)里的 description | 接口本身的功能、调用时机、严禁场景 |
原子接口声明(mcp.json)里的 inputSchema 字段下的 description | 参数语义与取值约束 |
| SKILL.md | 业务流程编排、跨接口规则、意图分流表、通用规则等 |
三类常见反例
- ❌ 把“接口本身的功能”写到
SKILL.md:膨胀长文、稀释注意力预算,且与原子接口声明(mcp.json)易出现不一致。SKILL.md的接口清单只写"前置条件 + 上下游关系"。 - ❌ 把“跨多接口的业务”写到单个原子接口的
description:仅在调用该原子接口时生效,其他原子接口决策时 LLM 读不到。应写在SKILL.md里。 - ❌ 把“接口功能描述”写到原子接口返回的
content:content应写本次调用的结果与下一步动作(如"已查到 N 条,请展示卡片让用户选"),功能描述属于原子接口的description。
# 3. 原子接口的声明书写建议
接口名使用清晰、语义化的名称。如命名 searchCinemas ,好于 search
首句声明「业务对象」,而非「入参」。如“根据出发地、到达地、日期搜索票务列表(仅机票)...”,LLM 可能会出现命中其他业务(如车票)的幻觉。应为“查询机票班次。按出发城市、到达城市...”。
删除实现细节,节省注意力预算。原子接口内部的实现细节对 LLM 正确选择该原子接口没有正向作用,反而会稀释真正重要的“使用边界”信息。
原子接口职责单一、description 互不重叠。原子接口之间避免出现包含或被包含的关系,应该保持职责单一,同时描述与返回的内容保持一致。此外,该描述面向的对象是 LLM 而不是用户,需要注意措辞口吻。
禁令不要藏在中段,同时给正向引导。LLM 对原子接口描述的注意力分布是前 1-2 句最强、中段衰减、末尾再次回升。藏在中段、以软引导(“注意:”)起头的禁令几乎失效。此外,仅写"不要做 X"而不写"应做什么",会使模型陷入"无出口"状态。
# 4. 原子接口的声明之参数描述书写建议
原子接口的声明里的 inputSchema 字段的 description 是离“填参数”决策点最近的防护。
这里处理不好,模型会做两件危险的事:
- 用户没提到的,就编一个看起来合理的值传入;
- 把用户的自然语言过度推断成精确字段。
# 4.1 使用清晰、语义化的名称
- 参数名称 filmIds ,好于 films
- 保持前后字段名一致,比如电影Id,始终为 filmId,而不是 filmId、movieId等
利用 JSON-Schema 的语法规则,可以定义更详细具体的参数说明,常用的如数值的 min、max,枚举类型等;可以借助 zod-to-json-online 生成。
const inputSchema = z.object({
departure: z.string().describe('打车的出发地'),
destination: z.string().describe('打车的目的地'),
minPrice: z.number().min(10).describe('最低价格,单位元'),
vehicleModel: z.enum(
['Five_Seater', 'Seven_Seater']
).describe('车型选择,包括五座车 Five_Seater、七座车 Seven_Seater'),
})
# 4.2 缺省时显式声明"应反问,禁编造"
参数的 description 字段不要只写字段是什么,还要写用户没提时的处理方式。
❌反例:
"startName": { "description": "出发地名称,如『北京站』" }
问题:只告诉模型"这个字段长啥样",没告诉它"用户没说时不要自己造"。模型会从历史上下文 / 城市定位 / 默认值里凑一个"北京站"出来。
✅推荐:
"startName": {
"description": "出发地名称(用户原话中的地点,如『北京站』『浦东机场』)。【禁止编造】用户未明确说出出发地时,禁止填写本字段,应改走 recommendStart 或反问用户『请问您从哪里出发?』。"
}
要点:
- 字段语义;
- 取值来源(用户原话);
- 缺省动作(反问 / 改走兜底接口)。
# 4.3 不要在 description 里给"示例值"当默认值
模型对 description 里出现的字面量极度敏感,举例值有相当概率被原样填入。
✅反例:
"city": { "description": "城市名,例如『北京』" }
实测:用户问"帮我查一下"(未提城市)时,模型会直接把 city="北京" 填进去。
✅推荐:
- 用占位符代替具体值:如『<城市名>』;
- 或改用反例样本:用户原话中的城市名(如用户说『上海』则填『上海』)。用户未提及城市时禁止填写本字段。
# 4.4 枚举/受限取值必须写清「合法集合 + 越界处理」
❌反例:
"sortBy": { "description": "排序方式,价格或销量" }
问题:模型可能填出 "price" / "sales" / "价格" / "by_price" 四种变体,全部都"看起来合理"。
✅推荐:
"sortBy": {
"description": "排序方式。仅允许以下取值:'price'(价格升序)/ 'sales'(销量降序)。用户未明确表达排序偏好时,禁止填写本字段(留空走默认)。"
}
要点:
- 合法值用代码字面量列出;
- 每个值的语义;
- 缺省动作明确为"留空"而非"挑一个"。
# 4.5 ID 类字段:必须声明「来源接口」,禁止从用户语言推断
业务 ID(orderId / itemId / shopId 等)是最容易被编造的字段——模型见过无数训练语料里的 ID 格式,会"按格式造一个"。
❌反例:
"orderId": { "description": "订单 ID" }
✅推荐:
"orderId": {
"description": "订单唯一标识,必须来自上游接口 getOrderList 返回的 orderId 字段原值。【禁止编造】禁止从用户自然语言(如『我那个 123 的订单』)推断或截取,禁止使用示例值。上下文中无可用 orderId 时,应先调 getOrderList。"
}
要点:
- 来源接口名(与 mcp.json 的 methodName 一致);
- 显式禁推断、禁编造;
- 缺源时的正确出口。
# 4.6 时间 / 数量 / 金额类字段:禁止隐式默认值
数值类字段是另一个高发区——模型很容易"补一个常见值"(如日期填今天、数量填 1、金额填 0)。用户说"帮我看下机票"(未提日期)时,模型有较高概率自动填当天或明天。
❌反例:
"departDate": { "description": "出发日期,YYYY-MM-DD" }
✅推荐:
"departDate": {
"description": "出发日期,格式 YYYY-MM-DD,取自用户原话(如『明天』『6 月 1 日』需先解析为具体日期)。【禁止默认】用户未提及任何日期相关表达时,禁止填写本字段,应反问『请问您计划哪天出发?』,禁止默认填今天/明天。"
}
要点:
- 格式规范;
- 解析来源(用户原话);
- 显式禁掉常见默认值(今天/明天/1/0)。
# 4.7 字段 description 三段式模板
将以上 4.2~4.6 的字段约束合并为统一模板,确保每个字段 description 覆盖"是什么 + 从哪来 + 没有时怎么办"三个维度。
✅推荐模板:
<字段语义(一句话)>。
取值来源:<用户原话 / 上游接口 X 返回的 Y 字段 / 枚举集合>。
【禁止编造】<用户未提供 / 上下文无来源 / 越界> 时,<反问用户『…』 / 改走接口 Z / 留空>。
三段缺一不可:前两段决定模型能不能填对,第三段决定模型不会"硬填"。
# 4.8 与其他位置的协同
❌反例:同一约束在字段 description 和 SKILL.md 中分别以不同措辞书写,如 description 写"禁止编造"、SKILL.md 写"不要自己造"。
✅推荐:按"权重优先级"原则,字段 description 权重高于 SKILL.md。内容分配如下:
- 字段 description:单字段语义、取值来源、缺省动作(字段级约束);
- SKILL.md:跨字段、跨接口的共性铁律与业务流程编排。
措辞冲突时模型以字段 description 为准,因此低权重位置须与高权重位置意图一致或保持沉默。
# 5. 原子接口 content 文本写法
# 5.1 通用规则
输入验证
- 小程序 AI 生成的参数无法保证一定正确,服务内应对参数的类型、值的有效性进行校验,如商品 id 是否存在;
返回输出
- 返回结果中 structuredContent 和 content 字段将提供给小程序 AI,以便于其继续进行推理;
- 当渲染原子组件时,需通过 structuredContent 返回结构化数据, 小程序 AI 将通过该数据结构理解屏幕上正在展示的内容;
- structuredContent 和 content 中避免返回重复内容;
- content 可用于 LLM 决策指令约束;
- structuredContent 数据将会过小程序 AI 进行符合用户语义的筛选,然后才会下发到原子组件渲染。因此如需使用语义筛选功能,原子组件渲染务必基于 structuredContent 进行数据渲染,避免出现异常。
精简内容
- 精简返回信息,传递过多的内容给小程序 AI 会增加思考耗时,同时降低准确度。原子组件内可通过监听 NotificationType.Result 获取原子接口返回的结果,一些无需小程序 AI 理解但渲染需要的内容,可通过 _meta 字段携带;
- structuredContent 内无需小程序 AI 理解的内容应当被过滤掉,如:
- 图片地址;
- 后台返回的多余字段。
- 精简入参字段,避免在入参中包含过多内容,应通过代码传递已知信息(getStorage 或记录在内存中),而不是让模型从自然语言中提取它们;
- 例如位置参数,应该传递 poiId,而不是指定国家、省、市、街道、经纬度等;
- 例如商品,应该传递 productId,而不是商品名称、类目等。
错误处理
- 返回详细的错误信息以及下一步操作指引,使小程序 AI 能够理解问题并正确采取下一步行动:
- 缺失信息,需要用户补充——如"缺失电话,需要用户补充";
- 执行报错,返回 err.message;
- 未找到结果,返回"未找到相关商品,可提供其他关键词进行搜索"。
# 5.2 事实陈述 + 业务动作两段式,避免裸指令
content 直接面向的是小程序 AI。「裸指令」(仅说"做什么",不说"基于什么事实、不能做什么")是最常见的踩坑写法。
❌反例:
"接下来请务必为用户展示订单确认卡片"
仅有动作无事实,模型可能将"展示卡片"理解为"准备调下一步动作接口"而跳过等待用户决策。
✅推荐:「事实陈述 + 业务动作」两段式,先陈述本次工具返回的客观状态,再点出下一步允许的动作。
"已查到该 orderId 的机票订单数据。请把本次接口返回的卡片数据展示给用户,并用简短一句话引导用户查看。"
"用户当前授权状态:手机号=未授权,定位=已授权。下一步允许的动作:把手机号授权确认卡片展示给用户,等用户在卡片中亲自点击同意后才能进入下一步。"
# 5.3 失败/空结果分支:堵死错误退路、给出正确出口
失败/空结果分支的 content 应同时包含三件事:
- 业务事实陈述前置:将模糊的"未找到该商品"改为精准的"未在商品库里精确匹配到名为「圣诞限定款」的具体记录";
- 显式禁掉错误路径:禁兜底搜索(防止再调空参接口拿无关数据)、禁编造具体值再次调用本接口;
- 给出正确动作出口:例如"从同轮已经搜到的列表里挑选若干款,把这些用卡片展示给用户,并简短引导用户挑选"。
如业务需要"查不到时给兜底",content 必须显式声明兜底语义:
1. 该 orderId 当前没有可改签航班。已附带「同航线热门航班」兜底数据。
2. 请明确告诉用户"未查询到该订单的可改签航班",
3. 再说"以下是同航线热门航班,可点击查看"。
4. 不要把这次返回说成"查到了该订单的同航线航班"。
注意:避免语义有歧义的禁令措辞,如"禁止调用任何其他工具",应显式列出禁调的业务接口清单,并显式给出"唯一允许的动作出口"。
# 5.4 利用 content 引导模型筛选 / 缩减 structuredContent 再展示
structuredContent 在交给原子组件渲染前会经过小程序 AI 一道按用户语义筛选的处理(见原子接口最佳实践小节 2)。开发者可在 content 中显式指明缩减规则,仅把子集下发给组件。常见场景:
- 接口返回 N 条候选,用户语义只需展示默认子集(如返回 9 档套餐、默认推荐 3 档);
- 接口返回完整列表,需按价格、品类、人数等用户偏好维度二次过滤;
- 接口返回同时包含主候选与扩展候选,需按"是否有筛选偏好"自动二选一。
✅推荐写法(动作指令 + 硬约束 + 用户语义锚点四段式):
预估完毕:9 档候选可选。
【展示给用户前必须先按筛选规则缩减本次返回的候选清单,禁止把 9 档全量直接展示给用户】
筛选规则:
- 有可筛选偏好(用户点名要某品类/价位/人数等):按偏好缩减本次返回的清单,移除筛空分类;
- 无偏好(用户只用"打车""叫个车""帮我打个车"这类没指定具体维度的请求):只保留本次返回数据中 isSelected===true 的项;
- 筛空时逐层放宽,仍空则回退默认清单并在引导话术中说明未满足项。
【强制】展示卡片时,必须把本次返回的清单按上述规则缩减后再呈现给用户,禁止把全量原始清单展示给用户。本次属于无偏好场景时,必须只保留 isSelected===true 的项再展示,且引导话术须按筛选后的实际数量描述(如『为你选中了 A、B、C 共 3 项』),不要再说『9 档』『全部 N 项』。
缩减后请等用户确认再下单。
要点:
- 判定字段必须是 boolean / enum 等模型可直接 yes/no 判断的字段(如 isSelected===true),不使用阈值比较;
- 给"无偏好"列出 3 个以上具体的用户原话样本,避免抽象表述;
- 引导话术约束必须配套:"不要再说『9 档』",否则会出现"卡片 3 个、话术 9 个"的割裂;
# 5.5 多轮"用户复读相同请求"时,给 content 加「展示卡片」短话术
用户在同一会话短时间内重复发送完全相同的请求时,即使首轮工具返回完全合规、卡片正确下发,后续轮仍存在小概率退化为纯文本 markdown 列详情不出原子组件卡片——这是长上下文 + 复读触发的注意力偏移。
✅推荐做法:在"返货架"类接口(详情查询、列表查询、套餐查询等)的 content 文本中追加一句明确禁止纯文本列详情的短话术:
{
"content": [{
"type": "text",
"text": "已加载「上海迪士尼度假区」(含 5 个套餐)。请展示套餐卡片让用户从卡片中选择,禁止以纯文本列出套餐详情。"
}]
}
简短的"展示卡片 + 禁止纯文本列详情"两句即可,无需展开为字段级清单。同类"返货架"接口建议统一加固。
# 6. SKILL.md 业务 SOP 写法
mcp.json 描述"单个原子接口怎么用",SKILL.md 描述"整个业务怎么跑"——后者承载业务级 SOP:流程编排、跨接口契约、用户意图分流、输出形态约束。两者职责不要重叠(具体分工见小节 2)
# 6.1 完整业务流程图
SKILL.md 显式给出业务编排顺序。流程图直观便于模型按图索骥。用 ASCII 流程图把"用户意图 → 接口 1 → 用户操作 → 接口 2 → 接口 3"一次画完。以"列表查询 → 详情 → 下单"类业务为例:
- searchItems(搜索/查询列表)→ 返回列表卡片
- 用户点击列表卡片 → openDetail 打开详情半屏页
- 用户在详情页点击「立即购买」 → 详情页组件点击按钮触发 previewOrder → 展示订单确认卡
- 小程序 AI 引导用户确认必要信息(如收货地址、登记人等)
- 用户点击订单确认卡「去下单」→ openDetail 打开结算页完成支付
注意:小程序 AI 不能跳过"详情页"直接调用 previewOrder。完整链路必须是:搜索 → 详情 → 确认 → 结算。 "小程序 AI 不能跳过 X 直接调 Y" 是流程图旁注释中 ROI 最高的一类约束——不显式禁就会被跳步。 流程图中接口名须与 mcp.json 的 methodName 完全一致(含大小写)。
# 6.2 原子接口依赖关系
不重复 mcp.json 的 description,重点是接口在业务流程中的位置:调用前置条件、配套组件、典型上下游接口。
| 接口 | 作用 | 组件 | 前置 |
|---|---|---|---|
| recommendStart | 按定位推荐起点候选,可匿名,登录态下个性化更强 | suggest-card | — |
| recommendEnd | 基于已选起点推荐 ≤3 个终点候选 | suggest-card | 登录 + 建议先 recommendStart |
| estimateQuote | 多档预估;普通档多选 + 独家项按钮 | estimate-card | 登录 + 起终点 |
| createOrder | 提交订单;从 storage 还原预估字段 | order-status | 登录 + 已预估 |
- "前置"列写"用户必须已经走过的步骤"或"必须已获取的字段";
- 不要列 inputSchema 字段(属于 mcp.json)。
# 6.3 业务约束(输出形态、并发、状态机的强约束)
业务约束跨多个接口生效,写在单个 mcp.json description 比较容易。常见类型(每个业务至少写其中两类):
- 输出形态:何时必须出卡片、何时才能出纯文本;
- 执行顺序:动作类接口(加购 / 创建订单 / 取消订单 / 改签等)必须先调成功再回复——任何 X 成功的宣布,必须建立在对应动作接口调用成功(isError 为 false)的基础之上;
- 严禁不调用接口就直接通过文本或卡片向用户承诺"已为您 X";
- 并发串行:createOrder / payOrder 禁止并发,须等上一笔结束(成功、失败或用户取消)后再发起下一笔;
- 数据来源:业务 ID 字段(如 itemId / orderId / shopId)必须来自上游接口的真实返回,禁止编造,禁止从用户自然语言推断。
注意:SKILL.md 中也需警惕两类危险写法:
- 裸指令:"当用户表达 X 意图时,需要生成对应的文本链。"——未指明标准机制,模型会按字面拼一段文本字符串当链接;
- 非示例 URL/具体值字面量:直接写进 SKILL,模型会原文照搬作为纯文本输出。
边界场景须写齐:仅覆盖"首次填某字段"而未覆盖"在已有数据基础上修改一个值"时,模型会把修改场景当作禁令未覆盖的边界自由发挥(典型表现:用户只说一个新值未指明覆盖项时,模型会保留旧值把新值塞到第二项)。
如果发现上述内容配置了但不符合预期,可在原子接口执行结果 content 里进行强约束。
# 6.4 用户意图分流表
SKILL 内挂多个服务且用户表达模糊时,意图分流须前置到 SKILL.md 顶层:
# 触发词
直接意图
- 列出 5-10 条用户可能直接说出的、能明确指向本服务的表达原话样本
非典型意图(场景隐含本服务能力)
- 类型 A:3-5 个该类型的典型用户表达
- 类型 B:3-5 个该类型的典型用户表达
- 泛化:任何将
用户场景与本服务能力关联的表达
注意:意图分流表中不要写"用户模糊表达 X 时优先调 Y"——会与"用户没说清楚就先反问"的禁令冲突。歧义短表达的兜底动作应统一为"先反问澄清"。