最近有好几个朋友跑来问我同一个问题:小程序里想放一个AI功能,比如智能问答、文案助手这类,到底怎么接?他们试过直接在端上请求大模型接口,结果不是被合法域名校验卡住,就是怕API Key被反编译出来,更别提每次上线前还要给服务器做备案。我给出的方案一直很明确:想要最快从0跑通,别碰自建后端,用微信云开发做中间层,大模型能力交给火山引擎的平台服务,两件事拼起来,就是一个能稳定上线的AI小程序底座。这篇文章就把这套架构从选型到落地的完整过程和盘托出,适合所有想给小程序加AI能力、但又不想上来就被后端部署劝退的开发者。
1. 先想清楚:这套架构到底省掉了哪些麻烦
1.1 微信云开发解决的是“后端去哪里的问题”
小程序和大模型之间如果只隔一层原生小程序请求,会遇到两个硬伤。第一,小程序端的request接口要求所有外网域名必须是HTTPS并且在后台配置合法域名,域名的ICP备案是绕不开的,个人开发者光这一项就能卡上十天半个月。第二,大模型的API密钥一旦写进小程序代码包,等于公开了你的钱袋子,别人反编译一下就拿着你的Key去刷模型。
微信云开发的核心价值在于,它帮你省掉了整个基础设施层:云函数充当服务端,云数据库保存业务数据,云存储放文件。小程序端不需要自己部署服务器,不需要买域名做备案,云函数内部发出的网络请求是小程序端无法直接感知的,合法域名校验在这里根本不生效。换句话说,云开发把“有没有后端”的问题,压缩成了“写不写云函数”的问题。对你来说,省下的时间和精力可以全部投入到业务逻辑上。
这套方案的另一个隐性好处是计费简单。云开发有按量付费环境的免费额度,起步测试阶段几乎不用花钱,跑出量了再按实际使用付费。比起自己买一台云服务器、再配环境折腾半天,微信云开发的开通和部署成本基本可以忽略不计。
1.2 火山引擎在AI能力上的角色
火山引擎提供的是大模型的能力供给。对于做AI小程序来说,你真正需要的其实是三样东西:一个能理解自然语言的对话模型、一套稳定的API网关、一份按调用量计费的账单。这三样如果自己部署,光是显卡就是灾难,更不用说训练和微调了。火山引擎这类平台把模型推理封装成标准接口,注册后就能申请开通、拿API Key、按token付费,整个流程和平时用云服务一样傻瓜化。
选它的理由里还藏着一个小细节:它的接口风格采用当前主流大模型接口的通用格式,所以你的云函数代码以后想换模型服务商,改动量非常小。这对快速迭代的小程序项目而言意味着一个关键好处:模型能力可以横向替换,甚至可以在多个方案之间A/B对比,而架构代码不需要推倒重来。我见过太多人一开始就把自己绑死在某个私有SDK上,等想换模型的时候才发现云函数里满是对方封装好的特殊对象,改起来痛不欲生。
1.3 架构边界怎么划分
这套架构下,职责划分其实很清晰:
- 小程序端只做三件事:采集输入、调用云函数、渲染结果。
- 微信云函数做四件事:身份识别(拿到调用者的openid)、参数校验、转发大模型请求、返回统一格式的结果。
- 火山引擎只负责一件事:把用户输入变成AI回复。
我见过不少翻车案例,都是因为把中间层的职责搞混了:有人在云函数里写了一堆业务逻辑,有人在前端直接拼大模型请求。我的建议很简单,第一版宁可把所有逻辑都集中在云函数里,也不要分摊到两端。集中在一个云函数里虽然看起来不够“优雅”,但调试成本最低。等业务复杂度真正上来了,再把不同职责拆成多个云函数也不迟。
2. 请求链路拆解:从输入框到AI回话要经过几个环节
2.1 完整的数据流
一次典型问答长这样:用户在输入框敲下一句话,点击发送。小程序端拿到文本后调用基础库的wx.cloud.callFunction方法,把文本作为参数传给指定的云函数。云函数在事件入口收到参数后,先通过云开发自带的能力拿到当前用户的openid,并检查这次请求是否合法、是否超限。然后,云函数使用环境变量中保存的大模型API密钥,向火山引擎的对话接口发起HTTPS请求,把用户的文本作为输入带给大模型。模型推理完成后返回JSON,云函数再把解析好的回复内容作为结果返回给小程序端。最后前端把结果渲染成聊天窗口的样子。
你可以把云函数想象成一个前台秘书:用户把问题递给前台,前台先确认你是谁、今天有没有资格提问,再把问题转给后台的专家;专家写好回复,前台统一格式递回给你。整个链路看着长,实际上每一步都是现成的SDK和标准接口,真正需要自己写的代码量并不大。这也是为什么我一直不建议第一版就引入额外的服务层或消息队列,链路多一环,排查起来就多一分麻烦。
2.2 为什么云函数必须做参数校验与身份识别
很多从纯前端转过来的开发者在写云函数时会犯一个错:只做请求转发,不校验任何东西。这在开发阶段当然跑得通,但一旦小程序上线,后果是灾难性的。由于小程序端任何人都能打开,只要对方知道你的云函数名称,就能无限调用你的AI能力,每次调用都在烧你的模型预算。
云函数内部有个别人替代不了的东西:调用上下文里自动注入的OPENID,这个字段由微信后台在调用云函数时生成,伪造不了。你可以在云函数入口第一时间拿到它,然后到云数据库里查一下这个用户今天已经调了几次,超出限额就拒绝服务。实现思路其实就是先把用户ID作为记录的key,每次调用先查记录再更新计数。这套逻辑虽然简单,但它直接把攻击成本抬高了几个量级。做AI类小程序时我一直坚持一个原则:云函数可以不做业务,但绝对不能不鉴权。
2.3 几个需要提前定好的参数
云函数层面的三个参数在一开始就要想清楚,否则后面改起来很麻烦:
| 参数 | 建议值 | 为什么 |
|---|---|---|
| 超时时间 | 20秒到30秒 | 云函数默认只有3秒,大模型接口通常几秒级,不改必挂 |
| 内存大小 | 256MB起步 | 大模型回复文本较长,加上JSON解析和日志,256MB比较稳妥 |
| 返回体积 | 控制回复长度 | 云函数返回结果体积有限制,对话场景一般够用,图像生成类另说 |
我实际遇到的情况是,很多人在本地调试时一切正常,上线后发现请求报错,十有八九就是忘记调整超时时间。还有一类项目喜欢把max_tokens调到4096,认为回复越长越好,实际上对于大多数问答场景,1024已经能覆盖绝大多数回复,调太大会拖慢响应、增加成本,用户也很难读完长文本。参数是死的,业务是活的,先跑一个保守配置,之后再根据日志调整。
3. 动手搭建的四个关键节点:环境开通到首屏有回复
3.1 微信侧的环境准备
先注册一个小程序账号,主体类型根据你的实际业务来选,测试阶段直接用开发工具里的体验版就能验证。登录后进入开发者工具,点击“云开发”按钮开通云开发环境,首次开通会让你选择按量计费还是包年包月。个人测试阶段建议选按量付费,因为它自带每月免费额度,用完才计费,不会为了测试直接掏钱买套餐。开通后拿到一个环境ID,这个ID后面在端上和云函数里都会用到。
这里有一个看起来微不足道但非常容易踩的细节:初始化云开发时,环境ID字段在某些工具版本里默认取的是当前默认环境,但如果你同时建了多个环境,或者项目曾经被分享过,env可能指向一个不存在的环境。我的习惯是显式指定环境ID,不要依赖默认值。另外记得在app.js里补上初始化代码,并且打开用户追踪,这样云函数日志里能看到每个请求来自哪个openid,排查问题时会轻松很多。
3.2 火山引擎侧的配置
到火山引擎控制台注册并实名认证后,找到大模型服务对应的产品页,开通服务并创建一个API Key,把Key复制保存好。这个地方不少人会直接抄进代码里,我的建议是不要,稍后我们会把Key放进云函数的环境变量中。正式调接口之前,建议先对照平台提供的对话测试工具,用命令行或调试工具发一次最简单的请求,确认请求地址、鉴权方式、请求体结构都没问题再往云函数里写。
这一步很多人会跳过,然后直接在云函数里调试,一旦报错就分不清是网络问题、鉴权问题还是参数格式问题。我的经验是:先用最笨的方式在大模型平台侧跑通一次最小请求,把返回结构记下来,再写云函数。这样后面出错时,你能很自信地判断问题出在哪一段。
3.3 核心云函数的实现
创建一个云函数,名字可以叫“chat”或“aiChat”。云函数的运行环境建议选择Node.js 16以上版本,这样可以直接用全局fetch请求接口,省去额外依赖安装的麻烦。一个最小可用的实现大致长这样:
// 云函数示例:把用户输入转发给大模型接口并返回结果 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { const wxContext = cloud.getWXContext() const openid = wxContext.OPENID const { content } = event if (!content) { return { code: 400, msg: 'content不能为空' } } // 这里可以插入限流和次数检查逻辑 const apiKey = process.env.VOLC_API_KEY const apiUrl = process.env.VOLC_API_URL const model = process.env.VOLC_MODEL_NAME const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model, messages: [ { role: 'system', content: '你是一个友好且专业的智能助手。' }, { role: 'user', content: content } ], max_tokens: 1024, temperature: 0.8 }) }) const data = await response.json() if (!response.ok) { return { code: response.status, msg: data.error?.message || '上游调用失败' } } return { code: 0, data: { reply: data.choices[0].message.content } } }代码里注释了“限流和次数检查逻辑”的插入位置,这个后面单独说。需要特别强调的是环境变量的作用:在云开发控制台找到云函数的配置页,添加环境变量,把火山引擎的API Key、接口地址、模型名称都放进去,云函数内部通过process.env读取。这样Key既不会出现在小程序前端代码包,也不会出现在云函数代码仓库的明文里,安全性大幅提升。
在云函数代码中硬编码密钥,是最常见的安全事故来源。即使代码放在私有仓库,一旦仓库被克隆、被上传到代码分享平台,密钥就泄露了。环境变量和密钥轮换配合,对小程序项目来说已经足够可靠。另外,云函数的日志默认会打印入参和部分运行信息,开发调试时可以开着,正式上线前建议把日志里可能包含用户输入的长文本做截断处理,避免隐私信息长时间留在日志系统里。
3.4 前端页面与状态处理
前端页面不需要复杂,一个输入框、一个发送按钮、一个聊天列表即可,调用方式如下:
// 前端最小调用方式 wx.cloud.callFunction({ name: 'chat', data: { content: '你好,介绍一下自己' } }).then(res => { const result = res.result if (result.code === 0) { console.log(result.data.reply) } })比较容易被忽略的是请求状态处理。AI接口慢,用户在按钮点击后必须看到“思考中”这样的加载态,并且不能允许重复发送。我给按钮做的方案是:请求进行中把按钮禁用,文案变成“正在思考...”,返回后再恢复。这个交互虽然简单,但对体验提升是决定性的,否则用户以为没点中,狂点好几下,既浪费额度又让云函数并发飙升。
另一个细节是回复渲染。云函数返回的是纯文本,如果回复里有换行,前端不处理的话,聊天窗口会挤成一行。建议在页面组件里用支持换行展示的方式渲染,或者在后端返回前把回复里的换行统一转成前端可识别的标记。最笨但也最稳的办法是后段提前转换,前端只做展示,逻辑各归各,不容易出乱子。
4. 上线前最容易翻车的四个坑:超时、密钥、限额、计费
4.1 云函数默认3秒超时,不改必挂
大模型接口没有本地接口那么快,从请求发出到完整回复拉回来,大多数情况下需要2秒到10秒。微信云开发云函数默认超时时间只有3秒,也就是说,即使你的代码完全正确,也极大概率在正式环境触发超时错误。解决方案很简单:进云开发控制台,找到对应云函数,在配置里把超时时间改成20秒或30秒。
这里有个容易误会的点:调大超时时间并不会增加你的计费成本,它只是让云函数在超过指定时间后才被强制终止。云函数的费用主要由调用次数和资源使用量决定,和超时阈值没有直接关系。所以放心调大即可。如果你发现30秒都不够,那大概率不是超时设置问题,而是请求参数里max_tokens设太大,或者模型响应本身过慢,要从这两方面排查。
4.2 密钥放前端等于裸奔
所有直接写在端上的API密钥,都会随小程序代码包发布出去。对于AI类业务,密钥就是钱,被刷一次就多一份账单。正确做法是密钥只保存到云函数的环境变量里,并且建立密钥的轮换机制。慢一点的轮换是每月手动换一次,快一点的是用一个定时触发的云函数自动完成密钥更换。轮换机制代码量不大,但不建议第一版就做,先把手动轮换养成习惯。
我还见过一种错误做法:把密钥放在云开发数据库的一个集合里,云函数每次调用时读出来。这个方案的问题在于,数据库读取会额外增加读次数费用,而且密钥一旦被某个调试接口泄露出去,数据库存储反而是个更持久的风险点。环境变量是我目前用过最省心的方案,配置一次,后续只改值就行。
4.3 没有限额的AI接口会被人当免费API用
任何人打开你的小程序都能触发云函数。没有登录限制的小程序,AI接口几乎等于公开免费接口。我在开发云函数时习惯内置三层保护:
- 按openid限制每天的调用次数,比如每个用户一天50次。
- 针对单个用户做短时间并发限制,比如一分钟最多5次。
- 给云函数加一层全局限流,比如每分钟全局最多200次。
三层保护写下来也就几十行代码,核心逻辑都是先查计数、再比较阈值、超了就拒绝。唯一需要留意的细节是并发场景下的计数更新要用原子操作,否则同一用户同时发出多个请求,计数器可能被覆盖。微信云开发数据库的update接口本身就支持原子自增,直接用就行,不需要额外处理。
这套保护机制上线后每天看一眼调用日志,你会感谢当初写了这些限制。我见过一个真实案例,某团队的小程序因为没做任何限流,上线第二天就被脚本刷了几万次调用,账单直接爆掉。这个坑一旦踩上,是真的肉痛。
4.4 计费不是只有模型调用费
很多人在估算成本时只算大模型的token费用,忽略了微信云开发的资源费用。token可以粗略理解为大模型处理文本时最小的语义单元,一千个token大概对应几百个汉字,具体看语种。云函数有调用次数和资源使用量两个计费维度,内存设置越大,每秒的资源使用费越高。如果并发较高,建议把单个云函数内存控制在256MB到512MB,没必要无脑拉到最高。
另一个隐藏成本是模型的上下文长度。每次都把完整聊天记录塞给模型,token消耗会成倍增长。第一版建议只把最近几轮对话连同当前输入发给模型,既省钱又更快。我在很多项目里看到的情况是,运行稳定后真正的成本大头往往是重复token消耗,而不是单次调用价格。聊天记录该清就清,该截断就截断,不要指望模型记住所有历史。
5. 一次真实运行的成本账与性能参考
5.1 延迟表现:你真的需要“打字机”效果吗
第一版上线后,先别急着追求大模型的打字机式流式输出。云函数返回的是完整JSON,小程序端拿到的是一次性结果。很多用户看到一秒多没反应就以为白屏了,所以我们要用加载态先顶住,等完整回复到达后,在前端做逐字渐显的“假打字机”效果。这样既绕开了云函数流式返回的技术复杂度,又保住了聊天产品的沉浸感。
实测下来,简单的问答在模型服务和网络都正常的情况下,整体响应时间通常在2到4秒,加上前端逐字渲染的感知优化,用户不会觉得太难等。如果你一定要在后续版本做真正的流式输出,需要单独设计长连接或多次轮询方案,和现在的云函数架构是两条路线,别在第一版混在一起。先把主体流程跑通,比那些看起来很炫但增加链路复杂度的功能重要得多。
5.2 按量付费账单长什么样
以某个测试阶段的小程序为例:每天调用约500次,每次输入和输出合计约1000个token,一个月的模型调用成本大约是几十元级别,具体金额取决于所选模型和价格政策。云开发这边的资源用量在免费额度内基本能覆盖,超出部分每月也就几元到十几元。真实情况是架子搭起来之后,最烧钱的动作往往是调试期反复测试同样的输入,而这一部分又是开发者的刚需。
我的经验是调试期尽量选择轻量快速的模型配置,或者把max_tokens调小,省下来的费用相当可观。另外建议给云函数加一层简单的缓存:如果用户输入的内容和之前某个问题相同,直接返回上一次的回复。对于文案生成、问答百科这类场景,缓存命中率其实不低,能明显拉低成本。
5.3 上线前建议完成的安全加固清单
这三点建议按优先级排序,第一项不做就不建议上线:
- 云函数内必须校验openid并写入访问日志,确保每次调用有据可查。
- 设置用户级与全局级双重限流,防止恶意刷接口。
- 对敏感接口加上随机校验参数或临时token机制,防止被脚本直接批量调用。
很多人把安全越想越复杂,其实对于个人开发者来说,做好上面三点就已经能挡住绝大多数脚本刷量的情况。至于更复杂的内容安全审核,可以留到业务真正有用户量之后再做,到那一步说明你的小程序已经值得认真对待了。上线前再花十分钟检查一下云函数的权限设置,确保没有把云函数写成可匿名调用,这一点很多人会漏。
最后聊点个人体会。从我这边多次搭建类似项目的经验看,从0到能跑的AI小程序,瓶颈从来不在代码量上,而在于大家习惯性把简单问题复杂化:一会儿想上消息队列,一会儿想搞模型流式。第一版流程越直白越好,链路越短越好。把火山引擎的大模型能力挂在微信云函数后面,用环境变量管好密钥,再给接口加一道限额,这件事的80%就已经完成了。后续要扩展也不难,模型接口保持通用格式,云函数随时可以加缓存、加多轮记忆、加指令模板。先跑通,再优化,这是我踩过不少坑之后最想说的经验。