AI对话微信小程序模板:从流式SSE到上线避坑指南
简介这是一份面向微信小程序开发者的AI机器人对话界面模板由HBuilder编写聚焦于对话页面前端实现不含后端接口适合已具备编程基础、熟悉HBuilder开发流程的读者直接参考。压缩包共2000个文件约7.04MB其中以js、ts脚本文件为主配合vue、json、scss、wxss、wxml等描述页面结构与样式另有md文档便于快速了解项目组成。资源已有967人浏览学习。使用者可获得完整的小程序对话模板源码包括聊天消息列表、输入框交互、会话状态管理以及相应页面样式由于接口预留可通过自行调配后端服务快速对接真实AI能力尤其适合用于小程序课程设计、项目原型演示或二次开发起点。整体代码结构清晰开发者可在此基础上快速替换变量名、接入所需AI服务节省从零搭建界面的时间按文件类型整理后目录较规范便于定位业务逻辑与样式文件。1. 从模板到能对话这套模板到底帮你省掉哪一段第一次把大模型接进微信小程序的人通常不是被 AI 难住的而是被微信的域名校验、HTTPS 证书和流式响应这三件事绊住。所谓 AI人工智能机器人对话微信小程序模板真正解决的不是把聊天页面画出来而是把“小程序前端 转发服务 大模型接口”这一整条链路的接法搭好消息怎么上送、回复怎么回来、密钥放在哪、上架审核怎么准备。它适合两类人一类是没写过小程序后端、想让页面直接调大模型的开发者照着模板换掉配置就能跑另一类是产品负责人想在两三天内验证一个 AI 聊天原型到底值不值得继续投入。跑通只是第一步跑通之后你才看得到真实调用成本、用户停留时长这些更重要的东西。2. 为什么对话机器人不能按普通页面的方式来写wx.request、流式响应与 WebSocket 的取舍2.1 一次对话从输入到回显数据到底经历了什么你点下发送按钮之后小程序其实只完成了半件事。它把当前这条消息和之前的聊天记录拼成一个messages数组POST 到自己的后端网关网关拿着同样的数组去请求大模型接口模型返回的通常不是一整段文字而是一串按字节流陆续到达的碎片网关再把碎片原样转发回小程序端逐字渲染。这个过程对普通网页同样成立但微信小程序多了两道绕不开的约束。第一所有网络请求的域名必须在小程序后台登记且必须是 ICP 备案过的 HTTPS 域名所以你不能在小程序里直连大部分境外模型厂商的接口。第二默认的wx.request拿到的是“完整响应”也就是说要等模型把整段话生成完再一次性回传用户会对着转圈等很久而且中途完全看不到进展。这两条约束基本决定了模板的技术形态小程序端只负责对话界面和请求封装真正的模型调用一定要放到自己的服务端去做中转。为什么现在市面上的 AI 聊天模板几乎都认同一套叫chat/completions的接口风格因为大部分大模型服务商都提供这套兼容接口请求参数长得差不多核心字段就是model、messages、stream这三个。messages是聊天上下文数组每个元素有role和contentrole分system、user、assistant三种system决定模型的人格和规则user是你输入的内容assistant是模型历史上的回答。模板里所有“多轮对话”的实现本质上就是在维护这个数组并控制它不要长到超过模型的上下文窗口。2.2 流式输出为什么是对话类小程序的硬底线非流式请求的时间线是这样的用户点发送请求发出去模型排队、预填充、逐字生成全部完成之后数据才回传小程序一次性把整段文字贴到聊天列表里。如果模型生成速度是每秒 20 到 30 个 token一段 300 字的回复大约要等 5 到 10 秒。这中间用户什么都看不到第 5 秒开始就会怀疑手机网络断了很多人会退出页面重进造成重复请求和重复扣费。流式输出改变了这个体验服务端每生成一小段就立刻推给前端前端用一个叫“打字机”的交互逐字显示。用户大概 1 秒左右就能看到第一个字后面的内容持续追加出来。同样是等待 6 秒非流式给用户的感觉是“卡死了”流式给用户的感觉是“它正在写”。对对话类产品来说这个观感差别直接决定用户愿不愿意等下去。所以模板只要目标是“能用的对话机器人”流式基本就是必选项。不过也要说清楚流式不是免费的它要求你的后端网关支持把大模型的流式响应原样转发同时在微信小程序端做对应的数据解析。如果你只是想快速验证模型能不能回答你的业务问题先用非流式跑通链路再补流式是比较务实的顺序别一上来就把两个复杂度叠在一起。2.3 wx.request、enableChunked 与 WebSocket 到底选哪个这是做模板时第一个要定的技术选型。我见过三种常见做法各自的适用场景差别很大方案优点缺点适用场景wx.request普通模式写法最简单几个回调就能跑通等完整响应没有打字机效果非流式接口原型验证wx.requestenableChunked沿用 HTTPS 请求能收流式数据不用维护长连接需要基础库版本支持不同机型表现有差异大多数对话模板的首选WebSocket双向通信天然适合流式还能做“停止生成”等控制连接管理复杂要处理心跳和重连服务端改造成本高复杂多轮对话、语音对讲类场景我一般建议先走enableChunked这条路。原因是它改动最小请求还是普通的 HTTPS POST只是开启分块接收然后在onChunkReceived回调里逐段处理数据。相比之下WebSocket 需要服务端单独维护连接池还要考虑断线重连、心跳保活对只做聊天机器人来说属于过度设计。等以后你要做“回复生成中允许用户打断”这种强交互时再考虑切换到 WebSocket 也不迟。enableChunked的来源是 HTTP 协议里的Transfer-Encoding: chunked服务端只要按 SSE 流式返回小程序端就能一边收一边渲染。需要留意的是它要求小程序基础库版本比较新后面讲到真机适配时再展开。3. 跑通最小模板对话界面到模型响应的最快路径3.1 模板目录与需要改的三个位置一个能跑的对话小程序模板解压之后目录结构大致长这样miniprogram/ ├── app.js ├── app.json ├── pages/ │ └── chat/ │ ├── chat.wxml │ ├── chat.wxss │ ├── chat.js │ └── chat.json └── utils/ ├── config.js └── llm.js这个结构是微信开发者工具里最普通的原生小程序项目没用 uniapp 那一套跨端框架。对于“AI 对话小程序模板”这个需求我建议你用原生小程序而不是 uniapp对话页面的逻辑不复杂原生方案少一层编译和兼容性排查真机调试时定位问题更快。如果你本来就在 uniapp 项目里维护多端应用才需要考虑跨端方案这类模板的核心逻辑其实也搬得过去。拿到模板后真正需要改的地方只有三个project.config.json里的appid、utils/config.js里的网关地址和模型名、以及utils/llm.js里的请求参数。对话页面本身的代码不需要大改因为消息渲染和输入框交互是通用的。这也是“模板”的价值所在你已经有一个能跑通前后端的完整骨架而不是从零开始写页面。3.2 对话页面的 WXML 与消息渲染逻辑聊天页面的核心是把消息列表渲染出来并把输入框的发送动作接到逻辑层。先看chat.wxml里的主体结构view classchat-page scroll-view classmessage-list scroll-y scroll-into-view{{scrollIntoView}} view classmessage-item {{item.role user ? user : assistant}} wx:for{{messages}} wx:keyid text classmessage-content{{item.content}}/text /view /scroll-view view classinput-bar input value{{inputValue}} bindinputonInput confirm-typesend bindconfirmsendMessage placeholder输入你的问题 / button bindtapsendMessage disabled{{sending}}发送/button /view /view注意这里用了scroll-into-view绑定一个每次消息变化都会更新的值目的是让聊天列表自动滚到底部。messages数组里每个元素至少包含role和content两个字段role用来控制样式区分用户气泡和机器人气泡。输入框的confirm-typesend配合bindconfirm让用户在键盘上按“发送”也能触发同一个方法这是移动端聊天最常见的交互习惯。对应的chat.js消息处理逻辑是这样const llm require(../../utils/llm) Page({ data: { messages: [], inputValue: , sending: false, scrollIntoView: }, onInput(e) { this.setData({ inputValue: e.detail.value }) }, sendMessage() { const content this.data.inputValue.trim() if (!content || this.data.sending) return this.setData({ inputValue: , sending: true, messages: [...this.data.messages, { role: user, content }] }) this.reply() }, reply() { const history this.data.messages.map(m ({ role: m.role, content: m.content })) llm.chat({ messages: history, onDone: (res) { const replyText res.data.choices[0].message.content this.appendAssistantMessage(replyText) this.setData({ sending: false }) }, onError: () { wx.showToast({ title: 请求失败, icon: none }) this.setData({ sending: false }) } }) }, appendAssistantMessage(content) { const messages [...this.data.messages, { role: assistant, content }] this.setData({ messages, scrollIntoView: msg-${messages.length} }) } })这段代码里有三个关键点。第一sending标志在发送时置为true请求结束再改回false这是为了防止用户连点发送按钮造成重复请求。第二发给模型的history是从this.data.messages里映射出来的纯数据副本只保留role和content不带 UI 用的id之类字段。第三appendAssistantMessage里的scrollIntoView每次用消息长度做后缀保证滚动位置值总是变化的否则 scroll-view 不会触发滚动。3.3 最小可跑的请求封装与本地联调在utils/config.js里集中管理配置项module.exports { baseURL: https://your-gateway.example.com, model: qwen-plus, timeout: 120000 }baseURL填你自己转发服务的地址不是模型厂商的地址原因前面说过小程序不能直连未备案域名而且密钥不能暴露在端上。model字段决定用哪个模型这是模板最值钱的一个抽象今天用通义千问明天想换 DeepSeek 或者别的兼容接口只改这一行。请求封装放在utils/llm.js里先看非流式版本const config require(./config) function chat({ messages, onDone, onError }) { wx.request({ url: ${config.baseURL}/v1/chat/completions, method: POST, data: { model: config.model, messages: messages, stream: false }, header: { content-type: application/json }, timeout: config.timeout, success: onDone, fail: onError }) } module.exports { chat }这里timeout设置成 120 秒是因为大模型接口的首字响应虽然通常在几秒内但请求排队和长文本生成都可能拖到 30 秒以上小程序默认的 60 秒超时不够用。先用stream: false跑通整条链路确认页面、网关、模型三者都通再上流式否则一旦出问题你根本分不清是网络没通还是流式解析坏了。本地联调阶段直接在微信开发者工具里勾选“不校验合法域名”选项就能跳过域名校验请求你的本地服务。这时候需要你本地先起一个最简单的 HTTP 服务把请求原样转发到模型接口返回结果打出来。确认页面能显示回复之后再去小程序后台配置 request 合法域名这一步是后面真机预览和提审必须做的。4. 把回复变成打字机效果SSE 流式解析与微信端的适配4.1 大模型 SSE 返回的报文到底长什么样流式接口普遍采用 SSE 格式全称是 Server-Sent Events。它本质上是 HTTP 响应里按行推送的文本流每段数据以data:开头事件之间用空行分隔。用 curl 直接请求网关可以看得最清楚curl -N https://your-gateway.example.com/v1/chat/completions \ -H Content-Type: application/json \ -d {model:qwen-plus,stream:true,messages:[{role:user,content:你好}]}注意这里的-N参数它告诉 curl 不要缓冲输出实时打印服务端推过来的每一段。返回内容大致长这样data: {choices:[{delta:{role:assistant,content:}}]} data: {choices:[{delta:{content:你}}]} data: {choices:[{delta:{content:好}}]} data: [DONE]每行data:后面是一个 JSON 对象其中choices[0].delta.content就是这一小段的增量文本。模型生成结束时服务端会推一个data: [DONE]作为结束标记。解析流式的核心工作就两件把data:前缀剥掉把 JSON 里的delta.content取出来逐字追加到界面上。有个细节容易踩坑data:后面可能跟的不是完整 JSON而是空内容比如第一条往往只是delta.role没有content。解析时要做空值判断不然undefined.content会直接报错。4.2 在 wx.request 里开启 enableChunked 并解析数据微信小程序从基础库较新版本开始支持wx.request的enableChunked参数。开启之后响应数据会在onChunkReceived回调里分片到达用法如下function chatStream({ messages, onMessage, onDone, onError }) { const task wx.request({ url: ${config.baseURL}/v1/chat/completions, method: POST, data: { model: config.model, messages: messages, stream: true }, header: { content-type: application/json }, enableChunked: true, timeout: config.timeout, success: onDone, fail: onError }) task.onChunkReceived(function (res) { // res.data 是 ArrayBuffer需要转成文本再解析 const text decoder.decode(res.data, { stream: true }) buffer text const lines buffer.split(\n) buffer lines.pop() lines.forEach(function (line) { if (!line.startsWith(data:)) return const payload line.slice(5).trim() if (payload [DONE]) return try { const json JSON.parse(payload) const delta json.choices[0].delta if (delta delta.content) { onMessage(delta.content) } } catch (e) { // 半截 JSON 直接丢弃等下一个 chunk 拼接 } }) }) return task }这段代码有三个设计点要说明。第一task.onChunkReceived和task.onHeadersReceived是wx.request返回的RequestTask对象上的方法必须在请求发出后立刻注册而且要在success回调之前触发。第二用decoder.decode(res.data, { stream: true })处理编码而不是直接arrayBufferToText之类的手工转换后者在中文内容流式到达时容易出现乱码。第三buffer lines.pop()这一行是流式解析的保底逻辑TCP 分包不会恰好落在换行符上最后一行往往是不完整的要留到下一个 chunk 再拼。4.3 中文被 chunk 切开时的处理与参数调整上一节代码里的TextDecoder并不是在所有小程序环境里都可用真机调试时如果发现TextDecoder is not defined需要退回手动拼接方案。手动拼接的做法是先把每个ArrayBuffer转成字符串但要处理字节不完整的问题。一个实用的替代是直接在服务端解决让网关在转发 SSE 时对每个事件强制flush并确保中文字符不被切成两半前端就只需要关心换行符这一种边界。这比前端做字节级拼接收敛可靠得多。# 网关层至少要做到收到模型分片后立即转发不攒批 # 以 Node.js 为例响应头设置后每次 write 之后调用 flush这句话在实践中比任何前端技巧都重要。做过流式的人都知道服务端不flush前端怎么优化都没用。如果你自建网关用 Python 的 FastAPI 或 Node.js都要确认框架没有把 SSE 响应缓冲起来用云函数转发时也要选支持流式返回的运行环境部分云平台会把响应体整个缓存住导致前端等到超时也收不到第一个字节。流式启用后还有两个参数值得调max_tokens控制单次回复长度300 字的中文回复大概对应 500 到 600 token默认值建议设 1024 以上不然回复会在中途被截断、没有结束标记temperature默认 0.7 左右做客服或知识问答可以降到 0.3 以下让输出更稳定。这两个参数要放在配置项里给使用者留口子不要写死在模板里否则后面每个场景都要改代码。5. 对话模板上线的常见问题排查密钥、域名与审核的五个坑5.1 现象提审被拒理由涉及 AI 生成内容这是 AI 对话类小程序最常见的一道坎。你功能做完了、流式也跑通了提交审核却被拒原因是“涉及 AI 生成内容”且没有对应的服务类目或资质说明。原因说起来其实很简单微信审核把 AI 生成的输出视为信息服务的一种要求开发者明确告诉用户内容由 AI 生成并提供相应的合规说明。解决路径我建议分三步一是在小程序后台补充“AI 生成内容”相关类目二是在服务端做一层内容安全检测模型返回的文本先过一遍关键词或内容安全接口命中风险就直接拦截而不是原样推到用户面前三是在提审备注里写清楚产品形态这个对话机器人用在什么场景、是否面向公众、有没有人工申诉渠道。这三步做完大部分审核问题都能解决。别试图绕开AI 输出不经任何过滤直接上架后续投诉风险比审核被拒更可怕。5.2 现象开发者工具里正常手机一预览就白屏开发者工具里所有请求都通真机预览却一直转圈或报request:fail。原因基本可以锁定在域名校验上。开发者工具里勾选了“不校验合法域名”绕过了一层限制真机上这一层是绕不过的你的网关域名必须同时满足三个条件HTTPS、ICP 备案、在小程序后台的 request 合法域名列表里登记。排查方法也很直接真机上打开右上角菜单里的开发调试开关临时绕过域名限制。如果开启后请求通了说明就是域名校验问题去后台把域名加上即可。如果开启后仍然失败再查网关本身是否只允许特定来源访问、HTTPS 证书链是否完整。另外提醒一句开发调试开关只能用于开发阶段正式提审包的域名配置一定要以合法域名为准。5.3 现象流式回复断在中间最后几个字是乱码回复生成到一半突然不走了或者界面出现了“”这类乱码字符。原因有两个层面。前端层面onChunkReceived收到的字节可能在字符中间切断直接按文本解析会把半个汉字变成一个乱码字符更常见的是服务端没有逐段转发而是等攒了一批才推前端等不到数据就触发了超时。解决方法是按三层去查第一层前端把buffer分成“完整行 不完整行”来处理不完整的行必须留到下一个 chunk 再拼接这已经在上一章的代码里实现了。第二层在onChunkReceived里打印每次收到的res.data.byteLength如果中间出现长时间没有新数据问题在网关转发。第三层直接用 curl 请求你的网关确认它是否像 4.1 节那样逐行推送如果 curl 正常而小程序不正常再查基础库版本。真机上建议把微信更新到最新版本enableChunked的实现在历史版本上确有差异。5.4 现象快速点两次发送上下文错乱了用户连点两下发送按钮结果后发的问题先被回复或者模型的回答里把自己的上一轮回答也带上来了。原因是因为sending标志虽然设置了但如果你用的是setData({ sending: true })之后立刻判断this.data.sending在微信的setData异步机制下这个判断可能读到旧值。正确做法上的一个关键是sending的判断和赋值都要放在同一轮同步逻辑里。发送函数开头先读this.data.sending为true就直接return然后才setData置位。另外请求完成必须放在finally里复位sending而不是只在success里复位否则一旦请求失败用户就被永久锁在“发送中”状态。如果你需要“停止生成”能力调用requestTask.abort()时同样要复位sending这两件事要同时处理好。5.5 现象API Key 泄露收到一笔突然的账单把模型厂商的 API Key 直接写在小程序代码里甚至在 Git 仓库公开之后被爬虫扫走这种事故在 AI 项目里太常见了。模型的计费是按 token 走的密钥一旦泄露别人可以用你的额度刷任意请求而且这类账单通常没有预警。根因是密钥放在了下游不可信的位置小程序前端代码只要被逆向任何写在代码里的字符串都会被翻出来。解决办法是密钥只存在服务端小程序端请求走自己的网关网关上还要做一层来源校验。可以给你看一个最小校验逻辑// 网关伪代码不直接信任小程序传来的身份 const openid verifyLoginTicket(req.headers[x-wx-login]) if (!openid) return 401 // 可选检查该 openid 是否在你的白名单内 if (!whitelist.has(openid)) return 403 // 通过后才去请求模型接口同时在模型服务商的后台设置每月消费上限和预警阈值。这是最后一道保险哪怕校验逻辑出漏洞账单也不会失控。6. 模板不是终点上下文管理、Prompt 参数与小步迭代6.1 把历史消息变成长短可控的上下文多数模板第一次跑通时是把全部聊天记录都发给模型。对话一长消息数超过上下文窗口后要么报错要么模型“失忆”因为早期的消息把窗口占满了。我的习惯是在模板的llm.js里加一个纯函数只保留最近 N 轮对话function buildPrompt(messages, maxTurns 10) { const recent messages.slice(-maxTurns * 2) return recent }maxTurns的单位是“轮”一轮包含一问一答两条消息所以乘 2。数字要按模型窗口和业务需要调做客服机器人可以只留 5 轮因为用户关心的是当下问题做角色扮演或写作助手可以留到 20 轮以上。不要为了省 token 把所有历史都砍掉那样模型会频繁反问“刚才你说的是什么”体验很糟糕。6.2 参数配置与应用场景的对应关系模板里一定要把模型参数暴露成配置项而不是写死在请求代码里。我一般保留四个配置model、temperature、max_tokens、maxTurns。temperature对输出质量的影响最直接做知识问答建议 0.2 到 0.4做创意写作可以调到 0.8 以上。上线之后才是真正打磨的开始。先用小范围测试收集对话记录看哪些问题模型答得不好再按失败案例去调system提示词和上下文窗口而不是一上来就换更大参数的模型。大模型的能力边界很多时候是被调用方式决定的同一个模型在好的 Prompt 结构和差的 Prompt 结构下效果差距非常明显。我做 AI 小程序时养成的习惯是每个版本都记录一次配置组的实际对话效果截图存下来改参数前后对比着看。这套模板给你省掉的只是接线工作接下来的产品迭代还需要你自己决定这个对话机器人到底要比别人多会什么。希望帮到你。本文还有配套的精品资源点击获取