做机器人开发这两年我越来越确认一个观点怎么让模型说出“对的话”只是第一步怎么把结果优雅地“摆”到用户面前才是真正拉开体验差距的地方。扣子开发平台上的卡片开发恰好就是这个环节的核心手艺。很多人一听到“卡片开发”就觉得是要写前端、画UI其实在扣子这类低代码平台上它更像是在“定义一种结构化的消息格式”思路一旦打开上手比想象中快得多能做的效果也远超预期。这篇文章我会从零开始把我实际踩坑和总结出来的完整流程拆给你看包括卡片的类型选择、Schema设计、服务端数据组装、与Bot的联动配置还有我在真实项目里遇到过的各种“莫名其妙”的问题。无论你是刚接触扣子平台的新手还是已经做了几个Bot但卡片总差那么点意思的进阶玩家这里应该都有你能直接拿去用的东西。1. 先搞懂扣子卡片开发到底在解决什么问题1.1 没有卡片之前机器人的回复有哪些痛点如果你的机器人只是用纯文本回复用户看到的基本就是一段长字符串。比如问“帮我查下杭州明天的天气”模型可能会输出一大段话涵盖温度、湿度、风向、穿衣建议等等。看起来信息是齐全了但用户得自己从文字里“解剖”出关键数据体验感非常差。更麻烦的是纯文本回复没法承载交互。用户想点击“查看24小时趋势”或者“切换城市”模型只能再理解一遍新的自然语言输入再来一轮完整生成延迟高而且还容易理解跑偏。在这种模式下机器人的“智能”完全靠语言模型硬扛实际上是被迫做了很多不需要它做的事。我早期做一个内部工具类的Bot时就吃过这个大亏。产线数据通过文本返回数字、单位、状态词混成一锅粥业务同事看一次要反应好几秒。后来改成卡片输出把所有字段分好区块、加上颜色标签和状态色块整个可读性直接提升了一个量级。大家才意识到卡片不是“花架子”是真正的效率工具。1.2 卡片体系的核心价值与典型应用场景在扣子开发平台里卡片本质上是一种“富文本消息协议”。它让机器人的输出不再是一维的文字流而是可以在对话流中渲染出按钮、图片、列表、轮播、表单等多维元素的结构化内容。核心价值有三点。第一信息层级清晰。卡片能天然地把“标题、摘要、正文、操作”分开展示用户扫一眼就能拿到自己关心的内容。第二交互闭环。通过按钮、跳转链接、表单提交用户可以在卡片内完成操作不用重新打字表述需求。第三降低开发成本。平台提供了内置卡片和自定义扩展能力你不需要从零搭前端工程就可以实现接近原生App模块的视觉和交互体验。适用场景非常广查天气、查物流、订单管理、客户反馈收集、小游戏、商品推荐、流程审批、数据报表……凡是需要把结构化数据传递给用户并希望用户做出反馈的场景卡片都值得优先考虑。我现在接每一个Bot需求时会先问自己一个问题“这个回复用户是需要读还是需要操作”如果答案是“操作”那基本就该上卡片了。1.3 输入需求时先做卡片形态选型很多教程上来就讲怎么写Schema、怎么调接口忽略了最重要的前置设计。接到一个卡片需求后我建议你先拿一张纸画出用户完整的“读-判断-操作”路径再决定用哪种卡片形态。举个例子如果用户只是想快速浏览多个条目比如“附近店铺”那列表卡片或轮播卡片就比一张堆满文字的大卡片合适。如果用户需要在卡片里做出选择比如“选择退款原因”那单选按钮组或者表单卡片更合理。如果只是展示详情比如“订单状态”那图文卡片加状态标签就够了。选型没做对后面所有代码都是在给错误的方向打补丁。我见过不少人一上来就选自定义卡片觉得自由度最高结果开发周期翻倍。而另外一些人只敢用内置模板导致业务上很需要的“混合内容布局”怎么也实现不了。正确的思路是根据需求复杂度从内置模板起步不够用了再上自定义扩展而不是先定技术路线。2. 卡片开发前的整体方案设计2.1 内置卡片与自定义卡片先算清成本和收益扣子开发平台通常提供两类卡片开发路径一个是直接用平台内置的卡片模板类似通用消息卡片你只需要填好对应的JSON字段就能生成列表、表格、图文等常见卡片。另一个是开发自定义卡片相当于通过扩展机制自己写渲染逻辑可以实现完全定制化的交互界面和视觉风格。两者怎么选我一般看三个维度开发周期、维护成本、体验独特性。内置卡片胜在稳定、上线快、不用维护前端代码适合标准化的信息展示。自定义卡片则适合业务交互复杂、视觉要求高、或者需要内嵌图表和动态内容的场景。代价是你得自己处理渲染兼容性、异常兜底和版本升级也不是每次尝试都顺畅。这里给一个参考判断如果卡片里只有文字和图片最多加几个链接按钮优先内置模板如果卡片里要有联动下拉框、动态图表、复杂校验逻辑甚至需要局部刷新别犹豫直接走自定义卡片路线。骑墙最难受一准耽误时间。2.2 开发流程的四个阶段与角色分工一个完整卡片功能从需求到上线我习惯拆成四个阶段结构定义、数据准备、交互设计、联调发布。结构定义阶段主要确定卡片内容由哪些组件构成每个组件的数据从哪来。比如一个订单卡片需要一个顶部状态栏、一个商品信息区、一个金额汇总区、两个操作按钮。这里有一步特别容易忽略要提前定义好“空数据”的样子缺任何一个字段都要有兜底展示。数据准备阶段决定如何在服务端接收参数、调用外部接口、组装最终JSON数据给卡片。这里不是简单的字段映射还得考虑超时、异常、缓存策略防止用户看到一张“半空”的卡片。交互设计阶段处理按钮点击、跳转链接、表单提交这类动作。重点想清楚动作触发后是直接关闭卡片还是更新当前卡片还是新开一张卡片这些状态流转一定要画清楚再写代码。联调发布阶段则是把卡片接入到Bot流程里做端到端测试。很多人卡在最后一步因为单独调试卡片没问题一旦通过Bot触发参数格式就对不上了。所以联调阶段一定要预留充分的测试时间。这个流程不一定严格按照顺序走但四个阶段的工作都要覆盖到缺一角后面的坑就少不了。2.3 一个实战案例从需求到卡片形态选型为了后面讲解更有抓手我们用一个模拟项目X来贯穿全文做一个“城市天气查询卡片”。用户对机器人说“看看深圳明天天气怎么样”机器人需要返回一张卡片包含明日天气图标、温度范围、降水概率、风力等级并且有两个操作按钮“查看小时级详情”和“切换城市”。先做形态选型。这个卡片的核心信息只有4个字段用户操作也只有两个按钮整体交互不复杂理论上内置模板也能实现。但我们希望按钮点击后能展开一个额外的折线图区块展示24小时温度变化这就不太适合用死板的内置模板来凑了。所以最终选择自定义卡片方案用扩展机制承载天气数据接口和卡片渲染逻辑。在结构定义上卡片分三块头部信息区城市和日期、核心天气区温度范围和图标、操作区两个按钮。小时级详情的折线图做成“折叠区”默认收起点击后再展开。这样一个清晰的结构就为后面的Schema设计打下了基础。3. 卡片Schema设计与核心字段解析3.1 卡片数据协议的基本骨架自定义卡片最终是通过一个符合平台规范的JSON结构下发到客户端的。不同平台的字段名略有差异但核心骨架一般离不开三大块协议版本、内容区、操作区。你可以把它理解成寄快递版本号是外包装的规格内容区是货物本身操作区是收货人签收时要做的动作。常见的基础结构大概是这个样子{ version: 1.0, type: card, body: { sections: [ { type: header, title: 深圳 · 明日天气 } ] }, actions: [] }很多新手会犯一个错误把展示数据和协议结构写在一起导致卡片一旦需要改动文案或者来源字段就要连结构一起动。正确做法是把数据区域和元信息解耦。也就是说先有一个统一的数据来源再由一个适配层把数据映射到卡片的body和actions里这样后续修改数据源卡片主体代码可以基本不动。3.2 常用组件类型与适用场景组件类型就是卡片内容区最小构成的积木。我常用的几类组件及其适用场景整理下来供你参考组件类型典型字段适用场景标题栏title, subtitle, icon卡片顶部信息概览文本段落text, color, size描述性信息、说明类内容图文混排image, title, desc商品、资讯、人物介绍数据标签label, value, tag数字指标、状态标记列表items[]多条目并列展示轮播图images[]图片类内容、多图对比折叠区foldTitle, panel展示更多详情、减少首屏信息量按钮组buttons[]触发动作、跳转链接表单formFields[]收集用户输入、发起流程选择组件有一条铁律能用基础组件组合解决的问题就别发明新组件。我知道自定义能力强的人总想搞点差异化设计但每多一种自定义组件就要多维护一套渲染和兼容逻辑对团队来讲都是成本。我一般会优先把“标题文本按钮”这三个组件组合玩透它们能覆盖至少70%的卡片场景。3.3 动作绑定与交互回调设计卡片不是静态海报按钮点击后必须能触发数据请求、会话更新或页面跳转。在扣子开发平台中自定义卡片的动作通常通过绑定事件的actionId配合服务端回调地址来实现。设计动作时最需要注意的是回调上下文。举个例子“切换城市”按钮的回调会触发一个城市选择器但服务端怎么知道当前卡片展示的是哪个城市这就要求在卡片数据中携带一个custom_data字段把当前城市ID、天气记录ID传回去。没有这个上下文回调到了服务端就变成“无源之水”根本不知道该更新什么。我在项目中会专门预留一个hidden_data区域专门存放用户不可见但业务需要的透传字段。哪怕当前功能用不到我也会先留一个扩展对象因为需求迭代非常快第二次迭代很容易就要增加一个埋点来源字段或权限校验值。3.4 避免踩坑字段命名、版本号与兼容性字段命名看似是小事实际上坑很深。我吃过一次亏早期定义字段时用了“number”这个单词结果和平台底层某个保留字段冲突导致数据解析异常排查花了一整个晚上。从那以后我所有自定义字段都强制加统一前缀比如“weather_data”、“user_info”避免和系统保留字撞车。版本号也是一个容易被忽视的细节。卡片是随着客户端版本分批上线的如果协议结构升级但没有版本控制老版本客户端很可能拿到不认识的字段就直接渲染空白。我通常把版本号拆成协议版本和扩展版本两个数字协议结构有破坏性变更时升协议版本只增加字段时升扩展版本。这样兼容性判断就有清晰依据了。4. 实战开发一张“城市天气查询卡片”4.1 环境准备与工程结构初始化开始写代码之前先把开发环境确认好。在扣子开发平台中自定义卡片需要依托扩展能力建议你在平台中新建一个“模拟项目X”扩展语言选择Node.js或Python都可以。我的习惯是用JavaScript因为处理JSON数据比较自然而且和前端渲染的命名习惯更接近团队成员之间看代码压力也小。工程结构我习惯按功能拆目录weather-card-extension/ ├── api/ │ └── weather.js // 天气数据源接口封装 ├── handlers/ │ └── card.js // 卡片数据组装逻辑 ├── actions/ │ ├── hourly.js // “小时级详情”按钮回调 │ └── switch_city.js // “切换城市”按钮回调 ├── views/ │ └── main.js // 卡片Schema映射配置 └── index.js // 入口路由别小看这个结构它在项目增长后能救大命。我第一次开发时所有逻辑写在index.js里结果一个卡片超过500行后找一处bug要来回翻半天。后来把入口、数据处理、视图映射、动作处理拆开维护体验好很多。4.2 编写服务端逻辑拉取天气数据并组装卡片数据天气数据的获取并不复杂。城市参数进来后调用后端接口返回温度、湿度、降水概率、风力这些字段。我们需要把这些原始数据映射成卡片展示数据。关键在于不要让源接口的数据结构污染卡片结构。我会在api/weather.js里做一次净化只返回业务字段function transformRawWeather(raw) { return { cityId: raw.areaId, cityName: raw.cityName, dateText: raw.date, tempHigh: raw.tempDay, tempLow: raw.tempNight, humidity: raw.humidity, windLevel: raw.windLevel, rainChance: raw.pop }; }这样处理有两个好处。第一屏蔽外部接口字段名的不可控性就算上游把tempDay改成tempMax我们只需改这一个文件。第二为后续组装卡片Schema提供稳定输入手写渲染映射时会非常轻松。在组装卡片数据时我会把数据区和展示区分离。数据区是一个干净的data对象展示区是views/main.js里的Schema模板留好插槽给data填充。这个思路很像前端MVC里的Model和View解耦前期稍微多写一点点代码后期改动效率提升特别明显。4.3 构建卡片Schema与渲染配置组装好的卡片Schema大致长这样{ version: 1.2, type: card, body: { sections: [ { type: header, title: 深圳 · 明日天气, subtitle: 10月24日 多云 }, { type: metrics, data: [ { label: 温度, value: 22℃ ~ 28℃ }, { label: 降水概率, value: 20% }, { label: 风力, value: 3级 }, { label: 湿度, value: 65% } ] }, { type: fold, foldTitle: 查看24小时温度趋势, foldContent: { type: chart, chartType: line, data: [22, 23, 24, 26, 28, 27] } } ], actions: [ { actionId: view_hourly, label: 查看小时级详情 }, { actionId: switch_city, label: 切换城市 } ] }, hidden_data: { cityId: 101280601 } }这里的metrics、fold、chart等组件是自定义卡片里很常用的积木。你不用全部自己造轮子很多组件平台已经有基础能力你只需在schema里描述清楚类型和数据即可。需要注意hidden_data里放的cityId就是上一节说的回调上下文。点击“切换城市”按钮时服务端能从回调事件里拿到这个cityId知道当前用户要基于哪个城市继续操作这样状态就不会断。4.4 本地调试与预览技巧本地调试是卡片开发里最容易让人崩溃的环节因为“代码里看着对”和“渲染出来好看”中间隔着一条很宽的河。我给自己的铁规矩是先Mock数据再走真实接口。我会单独维护一份mock.json里面放各种边界情况比如极长城市名、温度超过40、降水概率为0、按钮文案超长等。把mock数据直接塞进卡片Schema渲染先在预览区里看视觉表现。确认没问题之后再通过真实接口拉数据对比看看有没有异常。测试按钮回调时我会特别关注服务端日志里的回调事件结构尤其是hidden_data透传后有没有被清洗掉。之前遇到过一次bug因为开启“仅下发声明字段”的选项hidden_data没写进白名单按钮回调拿不到cityId排查了好几个小时。这个钩子建议调试时先关掉正式上线再开启。5. 将卡片接入机器人对话流5.1 配置意图识别与参数抽取卡片本身不能做任何事必须由Bot在合适的时间触发它。所以第一个联动点是意图识别。以天气查询为例我需要让Bot识别“深圳明天天气”、“上海后天会下雨吗”这类说法并抽取城市、时间和“是否要查看详情”这样的实体参数。扣子开发平台支持在Bot配置中创建自定义意图和实体我建议把“天气查询”拆成两个意图一个是“查询天气”一个是“查看天气详情”。卡片主界面由“查询天气”触发小时级详情回调则通过动作回调触发两者不要混在同一条意图链里不然参数传递会变得很复杂。参数抽取有两个注意点。第一城市名可能出现“深圳”、“深圳市”、“深圳特区”等多种表达需要用同义词映射统一成标准ID。第二时间的表达可能是今天、明天、后天或者具体日期归一化放到服务端处理不要在Bot提示词里硬编码规则否则效果不稳定。5.2 在Bot中使用卡片输出的三种方式把卡片挂到Bot对话流里我总结有三种常见方式按控制粒度从小到大排列第一种直接在Bot回复内容中引用扩展返回的卡片消息。这种方式最省事扩展把卡片JSON返回Bot原样发送。优点是可以快速测试缺点是如果Bot在生成回复时做了额外处理可能干扰卡片消息结构导致客户端展示异常。第二种在Bot的工作流中手动编排节点用专门节点来组装卡片消息。这种方式可控性强每个节点的输入输出你都能看到。适合卡片结构复杂、需要从多个数据源拼接数据的场景。第三种Bot只负责生成自然语言解释卡片完全由扩展或工作流节点触发。这种方式最干净模型不干预卡片内容避免出现“模型自由发挥导致卡片字段错乱”的经典翻车现场。我现在的项目基本都是这个方案。我强烈建议你对卡片消息采用“生成即固定”的策略也就是卡片的数据结构在服务端完全确定不要让大模型自主生成卡片字段。原因很简单模型输出太自由很容易漏字段或者多字段而卡片渲染对结构是有硬性要求的。5.3 多轮对话中的卡片状态管理卡片并不是展示完就结束了后面还可能有下一轮交互。比如用户点击“切换城市”弹出城市选择器用户选了新城市后卡片内容应该更新为新城市的天气而不是另起炉灶重新生成一张卡片。这里我踩过很多坑最终总结出两个关键维护会话级的卡片状态ID以及通过动作回调返回更新指令。卡片状态ID可以在卡片发出时生成并存在会话上下文里。每次动作回调比如“切换城市”服务端先读到当前卡片状态ID和hidden_data然后处理业务、生成新的卡片内容最后通过更新接口把新内容替换到原卡片上。如果状态ID丢了用户再点击按钮就会面临“新卡片覆盖旧卡片”的混乱局面体验感大打折扣。经验之谈在多轮对话中每张卡片一定要有一个唯一的实例ID并且这个ID要保存在服务端会话数据里而不是仅仅放在客户端。客户端可能因为会话重置而丢失上下文服务端才是状态管理的兜底。6. 常见问题与排查技巧实录6.1 卡片不显示或者显示为普通文本这个问题的出现频率高得离谱尤其是第一次接入时。你明明看到扩展返回了JSON但用户端却只看到一段原始代码或纯文本卡片完全没渲染出来。排查路径我一般按三步走。先看返回内容是否被包成了JSON字符串很多情况下是扩展把卡片JSON转成了字符串导致平台不识别。再看HTTP头部的Content-Type是不是application/json返回格式不对客户端会按纯文本处理。最后看是否有错误字段平台在成功返回卡片消息时外层结构是有固定约定的如果直接返回卡片本身而丢掉了包装壳也会失败。我还遇到过一种阴间情况调试工具的预览环境正常但真实客户端不正常。原因往往是客户端版本太老不支持新组件。这种情况下我会查客户端版本兼容表或者降级组件方案把复杂组件拆成文本和按钮。6.2 数据更新不及时与缓存问题天气数据本身变化频繁但如果你的卡片服务端做了数据缓存用户就可能看到一小时前的温度心里自然会觉得机器人不靠谱。这里有三个优化点值得检查。第一确认缓存策略。短生命周期数据缓存不应该超过10分钟甚至可以通过参数控制只有高频访问的数据才考虑缓存。第二确认数据版本号是否传递到卡片。如果卡片上显示了“数据更新时间”用户至少知道这不是实时数据。第三检查是不是HTTP缓存三重奏浏览器缓存、网关缓存、应用缓存任何一个环节没配好都会导致数据陈旧。我习惯在卡片里加上“更新于 10:32”这样的文本标签既能提升可信度又方便排查。用户问“怎么不准”你可以直接看数据时间戳是数据源问题还是前端展示问题一眼定位。6.3 交互按钮无响应与回调失败按钮无响应也是高频问题而且往往不是单一原因。我总结出最需要优先排查的三件事。一看actionId是否匹配。动作回调进到服务端后需要根据actionId分发到对应处理函数若actionId与卡片定义不一致路由就会404。二看回调事件结构是否符合预期。很多回调会带上pageId、params这些字段字段层级匹配不上解析自然拿不到值。三看安全校验。如果平台启用了回调签名验证你的服务端没做验签回调就直接被丢弃。有一个坑非常隐蔽actionId重复。两张卡片复用了同一个actionId点击B卡片却触发了A卡片的逻辑。这是我把卡片做成可复用组件后踩到的。从那以后我要求所有actionId由扩展名、场景名、动作名三段拼接从源头杜绝重复。6.4 兼容性不同客户端显示不一致同一张卡片在网页端、桌面端、移动端可能呈现出完全不同的渲染结果。原因有很多最常见的是宽度溢出和组件支持差异。移动端屏幕窄如果卡片里硬塞了两个大按钮布局就会挤压变形。我的解决方案是“移动端优先”设计。编写卡片Schema时先按照移动端窄屏的宽度做布局再在桌面端上看是否拉伸合理。另外所依赖的第三方图标或图片资源要使用固定宽高避免图片加载后把布局顶乱。遇到布局问题时我通常会让测试组做一张“夺命清单”极长文本、全角符号、无数据、图片加载失败、弱网状态。这张清单能帮我提前发现90%的兼容性瑕疵。7. 优化卡片体验的几个私藏小技巧7.1 加载状态与容错设计卡片是异步生成的用户点击查询后如果等了5秒才看到卡片这5秒里用户什么都没看到心理上会觉得很慢。所以除了优化接口速度我建议在对话流里设置中间反馈比如先回复一句“正在查询深圳的天气稍等一下”再发送卡片。这样用户感知速度会好很多。容错设计同样重要。比如天气接口超时卡片不能显示为一片空白。我会在服务端捕获异常后返回一张“轻卡”只有标题和一句“当前天气服务暂时不可用请稍后再试”。这种兜底方案虽简单但能保住在用户心中的基本信任感。更复杂的容错还包括数据显示为空时的处理。比如降水概率是0不能显示一个空的value。我通常统一设为“0%”并在后端做格式化而不是依赖前端猜测。7.2 减少卡片体积提升响应速度卡片的体积与加载速度是直接相关的尤其是卡片里内嵌大型图片或者冗长的hidden_data会显著拖慢渲染。我建议把隐形字段压缩到最小只保留业务需要的关键ID不要图方便把整个数据对象塞进去。图片资源优化也很关键。图片一定要用压缩后的URL并尽量使用CDN地址。我在内部规范里规定卡片图片单张不超过200KB宽度超过750px的要先缩放。否则光图片加载就能把用户等待时间拉长到无法忍受。另外能异步加载的内容就不要同步阻塞。折线图这种需要额外数据的模块可以设计成“按钮点击后再请求”而不是首屏全量加载。这样卡片首帧渲染速度会明显提升。7.3 复用性建立团队卡片模板库做卡片和写代码一样复用是效率的关键。当你的团队开始做越来越多卡片后一定会发现大量重复逻辑状态栏、通用按钮组、错误兜底、时间格式化。这些完全可以沉淀成模板。我在团队内部维护了一个“模拟项目X卡片模板库”包含常用组件片段和完整页面模板。新需求进来后先查模板库能不能拼出来不行再开发新组件。组件开发完成后评审通过就补充进库里。半年下来卡片的平均开发周期缩短了40%以上。模板库的维护需要配套命名规范比如每个组件必须注明适用场景、依赖字段、版本历史、示例数据。没有注释的模板库最终会变成无人敢碰的“脏乱仓库”所以这点要从一开始就意识到宁可模板少一点也要保证每一份都能被看懂。7.4 日志与监控习惯卡片开发上线后很容易被忽略的就是运行监控。我在每个回调入口都会打一条结构化日志记录卡片实例ID、动作ID、当前城市ID等关键字段方便后期顺着流程排查。日常监控里需要关注的指标有三个卡片发送失败率、按钮点击回调错误率、卡片平均生成耗时。任何一项指标出现明显上涨都是先于用户投诉发现问题的机会。建议给卡片回调接口设置独立告警和普通接口区分开。因为卡片回调一旦大面积失败会直接影响用户的操作闭环比单纯文本回复错误严重得多。如果告警条件里还带上“连续失败次数大于5”那基本可以精准预警又不会因为偶发波动而骚扰你。最后再分享一个我个人的小习惯每次发布卡片新功能之前我都会留出一个小时专门用真实用户视角把所有操作路径点一遍。不是走马观花而是模拟用户可能遇到的所有异常操作比如连续快速点击按钮、重复提交表单、弱网环境乱点。这个不够智能的“土办法”帮我拦下了很多测试用例覆盖不到的体验漏洞。卡片开发是一个细节堆出来的活儿框架和文档只是下限真正让用户眼前一亮的部分永远是你在这些细节上多花的那点心思。