简介:这是一份面向微信小程序开发者的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和content,role分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.request+enableChunked | 沿用 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 class="chat-page"> <scroll-view class="message-list" scroll-y scroll-into-view="{{scrollIntoView}}"> <view class="message-item {{item.role === 'user' ? 'user' : 'assistant'}}" wx:for="{{messages}}" wx:key="id"> <text class="message-content">{{item.content}}</text> </view> </scroll-view> <view class="input-bar"> <input value="{{inputValue}}" bindinput="onInput" confirm-type="send" bindconfirm="sendMessage" placeholder="输入你的问题" /> <button bindtap="sendMessage" disabled="{{sending}}">发送</button> </view> </view>注意这里用了scroll-into-view绑定一个每次消息变化都会更新的值,目的是让聊天列表自动滚到底部。messages数组里每个元素至少包含role和content两个字段,role用来控制样式区分用户气泡和机器人气泡。输入框的confirm-type="send"配合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 小程序时养成的习惯是每个版本都记录一次配置组的实际对话效果,截图存下来,改参数前后对比着看。这套模板给你省掉的只是接线工作,接下来的产品迭代,还需要你自己决定这个对话机器人到底要比别人多会什么。希望帮到你。
本文还有配套的精品资源,点击获取