简介:这是一款基于HBuilder编写的微信小程序AI机器人对话模板,面向有一定编程基础、正在开发聊天机器人或智能问答类应用的开发者。模板不包含后端接口,专注前端展示与交互,可自行接入任意AI服务。资源包约2000个文件,压缩后仅7.04MB,其中js、ts、vue文件承载逻辑与组件,json负责配置,wxss、wxml定义小程序样式与结构,md提供说明文档,并包含工程配置与许可文件,整体目录规范,便于二次开发。目前已有968人学习使用,借助它可省去从零搭建项目框架的繁琐,直接基于现成的消息列表、输入发送等对话界面进行修改;模板同时预留了接口对接位置,方便快速联调自定义AI服务。无论是用于课程设计、技术验证,还是作为商业项目的前端基础,这份模板都能有效降低入门门槛,适合作为AI对话类小程序的高效起步模板。
1. 微信小程序里的AI对话机器人:模板替你踩完了最劝退的三件事
想给自己的微信小程序塞一个AI聊天机器人,很多人不是倒在大模型的API上,而是倒在小程序本身的规则上:请求域名要白名单、接口必须HTTPS、对话历史得自己维护。这三件事任何一个没处理好,辛辛苦苦写的对话页到了手机上就是一只转圈的死狗。这个模板把这三件最劝退的事做成了默认配置,它是一套已经跑通的微信小程序源码工程——聊天界面、消息列表、prompt配置、API对接都写好了,拿到手先把配置改成自己的,就能在开发者工具里看到回复。适合给小程序加AI能力的产品、赶毕设的学生,以及不想研究协议细节的开发者。
2. 拆开模板看工程结构:对话能力写在哪些文件里
2.1 一个聊天页在小程序里由四件套组成:wxml、wxss、js、json
拿到源码第一件事不是急着点编译,而是先看懂目录。微信小程序每个页面都有四个同名文件:.wxml管页面结构,.wxss管样式,.js管事件和请求,.json管这个页面的局部配置。模板的聊天页通常放在pages/index/下面,你去看这四个文件,就能把整个对话流程对号入座。
聊天界面做得好不好,看wxml里消息列表怎么写。一般模板都会用一个scroll-view包住消息,再用wx:for循环渲染一个messages数组:
<scroll-view scroll-y="true" class="chat-list" scroll-into-view="{{scrollToId}}"> <block wx:for="{{messages}}" wx:key="id"> <view class="msg {{item.role === 'user' ? 'msg-user' : 'msg-ai'}}"> <text>{{item.content}}</text> </view> </block> </scroll-view>这里有个值得留意的设计:它没有区分“用户消息列表”和“机器人消息列表”两个数组,而是统一用messages数组,每条消息只有role和content两个核心字段。role为user时渲染右气泡,为assistant时渲染左气泡。这个设计不是为了省几行代码,而是为了和大模型接口的消息格式对齐——后面发送请求时,这个数组可以直接作为messages参数传给服务端,不需要再做一次格式转换。
scroll-into-view绑定的是最后一条消息的id,新消息上屏后列表会自动滚到底部,这是聊天页体验的基本盘。如果你发现模板里每条消息没有生成独立的id,wx:for会在控制台报警告,而且滚动定位会失效。正常做法是发消息时用Date.now()生成一个id,稳妥一点拼上随机数,避免同一毫秒内两条消息id撞车。
样式文件里一般会给.msg-user和.msg-ai分别写背景色和圆角,想改观感时不用动结构,只改wxss里的这两个类名就行。如果模板页面的json里设置了"usingComponents",说明部分UI组件是抽出来的,改动前先在components目录找一下,别在原页面里找不到样式就怀疑文件坏了。
2.2 全局入口:app.json、project.config.json 和 app.js 各管什么
app.json是小程序的全局配置,最关键的是pages数组,第一项就是启动页面。模板对话页如果就是首页,那pages第一行一定是"pages/index/index"。window节点下的navigationBarTitleText控制顶部导航栏的标题文字,AI聊天页一般会把名字改成自己的产品名。
project.config.json是开发工具的项目配置,决定这个项目以什么身份编译。导入模板后先看里面的appid字段,如果保留的是示例值,请在开发者工具里重新选择自己的AppID或测试号,不然后面真机预览会提示权限问题。这个文件里还有一个容易被忽略的setting节点,urlCheck如果为false,说明模板作者一直在用“不校验合法域名”模式开发,你自己跑通后上真机前,记得把这个问题记到第5章的避坑清单里。
app.js主要是挂globalData。有些模板会把当前会话ID、用户昵称放在globalData里,页面里通过getApp().globalData读取。这里的“全局”只是小程序运行期的全局,页面被销毁后数据不保证还在。要把历史对话留住,还是得用wx.setStorageSync写本地缓存,3.3节会具体讲。很多新手会把“状态管理”想复杂了,实际上AI对话这个场景真正需要持久化的只有messages数组,模板里靠Page的data加上本地缓存就够了,不需要引入额外的状态库。
2.3 配置集中在一处:config.js 是换模型的第一站
模板里一定有一个集中放配置的文件,位置可能是根目录的config.js,也可能在utils/config.js。它的作用是把所有和模型服务相关的参数收拢到一起,换模型时只动这个文件:
module.exports = { // OpenAI 兼容接口的地址,注意这里要带 /v1 baseURL: 'https://api.example.com/v1', // 模型名,必须和上游平台提供的名字完全一致 model: 'deepseek-chat', // 密钥先留空,调试跑通后再考虑放哪里,见第 6 章 apiKey: '', // 系统提示词,决定机器人的说话方式和边界 systemPrompt: '你是小程序里的AI助手,回答简洁,使用中文', // 采样温度,0.7 是稳定和创意之间的中间值 temperature: 0.7, // 单次返回的最大 token 数 maxTokens: 2048 }这里的参数名和后面的请求体是一一对应的,baseURL拼上/chat/completions就是完整的接口路径。很多人会在这里翻车:有的平台文档写的接口地址是https://api.xxx.com/chat/completions,没有/v1,那baseURL就不要脑补加/v1,以文档为准。apiKey字段现在留空就行,用假密钥也能先把工程跑起来,真正接服务时再填。但记住这句话:上线前一定不能让它留在前端代码里,具体原因和解决办法在第6章。
3. 把模板跑起来:从导入工程到第一条消息回显
3.1 导入源码:三个设置点决定你能不能顺利编译
模板到手先解压,然后打开微信开发者工具,按下面的顺序操作:
1. 选择“导入项目”,目录选到模板根目录,要求能直接看到 project.config.json 2. AppID 先用“测试号”,等真正要发布再换成自己的 3. 导入后先点“编译”,控制台没有红色报错再继续改代码第二步很关键。模板里如果带了别人的AppID,真机预览时会报类似“不在以下合法项目列表”的错;用测试号可以把编译和模拟器跑通,代价是用不了云开发服务。如果模板用了云函数,第三步之后还要多做一件事:点击开发者工具上方的“云开发”按钮,开通一个环境,然后确认app.js里wx.cloud.init的env字段和你的环境ID一致,否则调用云函数会提示env check invalid。
还有一个容易忽略的选项:导入后如果页面白屏,优先检查右上角“详情-本地设置”里的调试基础库版本。有些模板用了较新的组件,在旧基础库上会报Component is not found,切到和模板README一致或更高的版本就好了。不要一上来就怀疑代码坏了,先跑通原始状态,后面出了问题才知道是不是自己的改动引起的。
3.2 发送链路:把用户消息发给接口,把模型回复放回消息列表
模板的核心逻辑在页面js的sendMessage里。下面是典型的发送实现:
sendMessage() { if (this.data.sending) return; const input = this.data.inputValue.trim(); if (!input) return; const userMsg = { role: 'user', content: input, id: Date.now() }; const messages = [...this.data.messages, userMsg]; this.setData({ messages, inputValue: '', sending: true }); wx.request({ url: config.baseURL + '/chat/completions', method: 'POST', header: { 'Content-Type': 'application/json' }, data: { model: config.model, messages: messages.map(({ role, content }) => ({ role, content })), temperature: config.temperature, max_tokens: config.maxTokens }, success: (res) => { const reply = res.data?.choices?.[0]?.message?.content || ''; this.setData({ messages: [...this.data.messages, { role: 'assistant', content: reply, id: Date.now() + 1 }], sending: false }); }, fail: () => { this.setData({ sending: false }); wx.showToast({ title: '请求失败,检查域名和密钥', icon: 'none' }); } }); }先看第一行if (this.data.sending) return,这是发送防重入。wx.request是异步请求,没有这个开关,用户快速连点两次就会发出两个并发请求,回复顺序会乱。消息列表先本地追加用户消息,界面立刻回显,不等接口返回——这也是聊天应用的基本体验,不能让人点完按钮干等。
messages.map(({ role, content }) => ({ role, content }))这行是把前端UI用的id字段剥离掉,服务端只认role和content。有没有带多余字段的问题,取决于上游接口的容错性,但规范做法是只传协议里的字段。res.data.choices[0].message.content是OpenAI兼容接口的返回路径,如果模板接的是其它协议,这个解析位置要跟着改。
fail分支把sending复位,这是给用户的“后悔药”,失败后还能再点发送,而不是永远卡在loading状态。调试时如果接口返回了错误,先别急着看业务代码,直接在Network面板看返回体的error字段,大部分问题会直接告诉你model不存在或者鉴权失败。
3.3 多轮对话:messages数组的拼装、截断和新会话承接
大模型接口本身没有记忆,多轮对话靠的就是每次请求都带上历史消息。很多新手以为“多轮”是前端把聊天记录存起来,模型自己会记得,这是误解。实际每次请求的messages数组都是完整的:system、user、assistant交替排列,模型在生成回复时“看到”这个数组,才表现得像有记忆。
模板里通常会有类似这样的拼装函数:
const MAX_HISTORY = 12; function buildPayload(allMessages, systemPrompt) { const history = allMessages.slice(-MAX_HISTORY); return [ { role: 'system', content: systemPrompt }, ...history ]; } function resetConversation(keepTail = 6) { const prev = wx.getStorageSync('last_conversation') || []; return prev.slice(-keepTail); }slice(-MAX_HISTORY)从数组尾部保留最近12条消息,防止messages无限膨胀导致请求体过大,最终触发上游接口的token上限。systemPrompt始终放在数组最前面,它不参与对话但每次都要传——模型不是天然记得自己的“人设”,是每次请求里看到system才进入角色。
resetConversation是很多人问过的“新对话怎么承接上一轮”的做法。新对话不是把所有消息清空,而是从本地缓存里取上一轮会话的最后几条作为初始messages,这样用户在新对话里追问“刚才那个方案再说一下”,模型依然能接上。如果不想要承接,把keepTail传0就行。每次成功收到回复后,记得执行一次wx.setStorageSync('last_conversation', this.data.messages),否则新对话没有可取的尾巴。
4. 换模型实战:从OpenAI兼容接口到DeepSeek的切换方法
4.1 模板为什么能换脑:chat/completions协议是通用接口
这个模板能切换模型,依赖的是现在模型服务普遍兼容的/chat/completions协议。不管是国外模型还是国产模型,只要平台提供OpenAI兼容模式,请求结构和返回结构就是统一的:POST请求到{baseURL}/chat/completions,请求体里放model、messages、temperature、max_tokens,返回体里取choices[0].message.content。
这说明换模型本质上是改三个东西:接口地址(baseURL)、模型名(model)、密钥(apiKey)。页面代码一个字符都不用动。常见的对应关系如下:
| 配置项 | 作用 | 常见值 |
|---|---|---|
| baseURL | 接口根地址,以平台文档为准 | https://api.example.com/v1 |
| model | 模型名,拼写必须一字不差 | deepseek-chat |
| apiKey | 平台密钥,前端阶段留空 | 上线前迁移到云函数 |
| temperature | 采样温度,越低越稳定 | 0.3 客服 / 0.7 均衡 / 0.9 创意 |
| max_tokens | 单次回复最大长度 | 512 短回复 / 2048 长文 |
选型时我一般建议优先选那些给你兼容端点、同时又明确写了模型名的平台,踩坑最少。有的平台把接口地址写得很隐晦,藏在一级文档后面,找起来费劲;还有的平台同一模型有两个名字,新老模型对应不同的model字段,填错会直接返回model_not_found。
4.2 具体改法:把模板的模型切成DeepSeek这类国产服务
假设你要把模板从默认配置切到DeepSeek,只需要改config.js里的三行:
module.exports = { baseURL: 'https://api.deepseek.com/v1', // 以平台文档给的地址为准 model: 'deepseek-chat', // 不要填成别的名字 apiKey: '', // 调试阶段留空或填临时密钥 systemPrompt: '你是这个小程序的AI助手,回答控制在200字以内', temperature: 0.6, maxTokens: 2048 }改完不要急着上线,先在开发者工具里做三轮自测:
第一轮:发一句“你好”,确认返回HTTP 200,界面能显示回复 第二轮:发一句“记住我的名字叫小明”,再发“我叫什么”,验证多轮上下文 第三轮:打开Network面板,查看请求体的model字段,确认没有用默认占位名第一轮能过说明网络配通了,第二轮能过说明消息数组拼装没问题,第三轮是查细节——很多人前面都正常,结果发现model字段还是模板自带的占位符,等于根本没切过去。调试阶段如果用临时密钥,记得不要提交到git,换模型时最容易泄漏的就是这种散落各处的key。
4.3 三个参数把回复从“答非所问”调成“可用”
模型能不能给你干活,一半靠prompt,一半靠参数。systemPrompt是决定行为边界的最关键配置。差的写法是“你是一个智能助手”,太弱,模型完全不知道自己的场景。好的写法是“你是电商小程序客服,只能回答售前售后问题,不知道的引导用户联系人工,语气亲切”,这样模型遇到拿不准的问题就不会乱编。
temperature控制随机性。低温0.3左右回复稳定但容易绕圈重复,适合客服、分类、纠错这些需要确定性的场景;高温0.9以上发散但容易失控,适合文案创作。模板默认0.7是个折中值,如果你发现回复经常跑题,先降到0.5试试,不要直接砍到0.1,太低会让模型反复说车轱辘话。
max_tokens只控制返回长度,不管记忆。有一个简单的token估算方法:一条中文消息大概一个字约1.5个token,1000字内容大约占1500 token。模板默认2048差不多够一次600到800字的回复,如果systemPrompt里写了“回答控制在200字以内”,把maxTokens匹配地调小到512就行;调太大不仅响应慢,还会在客户端等得更久。回复如果出现截断,看一下choices[0].finish_reason,值是length就说明触发了max_tokens上限,不是在丢消息。
5. 避坑指南:小程序AI对话最容易翻车的五个现场
5.1 模拟器好好的,真机一发消息就报 url not in domain list
现象:开发者工具里一切正常,点击真机预览后console报request:fail url not in domain list,对话完全发不出去。
原因:开发者工具默认在“详情-本地设置”里勾选了“不校验合法域名”,而小程序真机强制要求request的域名在小程序管理后台配置成合法域名,且必须是HTTPS,不能是IP地址或带端口。
解决:开发阶段可以继续勾选不校验,方便本地调试;要真机调试或上线,到微信公众平台“开发管理-开发设置-服务器域名”里把接口域名加进request合法域名。域名必须已ICP备案,证书链完整,不能用自签名证书。加完等一两分钟生效,再重新预览。如果接口有多个域名,记得挨个加,漏一个就废一个。
5.2 快速连点发送,回复顺序错乱或者直接消失
现象:上一条回复没回来,又发了第二条;最后界面上先显示了第二条的回复,第一条的回复才姗姗来迟,甚至把第二条覆盖掉。
原因:wx.request是异步并发,没有请求锁。两个请求同时进行,前一个后返回时,success回调里执行setData把后一个写入的内容覆盖了。
解决:发送前先判断sending状态,正在请求时直接return,也就是3.2代码里那个开关;收到回复后再置回false。更稳的做法是给每个请求加一个自增序号,success回调里判断序号是否最新,旧请求直接丢弃,这样即使有意外并发也不会串数据。
5.3 聊到第七八轮,模型开始“忘记”前面说过的话
现象:前几轮还能准确说出用户名字或前面提到的需求,聊深了突然答非所问,甚至完全丢失上下文。
原因:大模型接口本身无状态,所有上下文都在请求体messages里。很多代码用messages.slice(0, N)从最前面截断,最早的消息是切掉了,但如果不小心把system也切了,人设就丢了。另一个情况是messages无限膨胀,超出接口最大token限制,上游直接报错。
解决:截断用slice(-MAX_HISTORY)从尾部保留最近N条,并且始终把system放在数组最前面。每次发请求前打印一次消息条数和字符数,心里有数。验证多轮能力有个固定套路:连续问三次“我叫什么”“刚才说了什么”,能答上来再继续调下一个问题。
5.4 配套AI服务端跑不起来:config.toml解析失败这类问题
现象:小程序侧一直转圈或提示请求超时,服务端日志提示toml解析失败、缺少字段,或者后端进程直接退出。
原因:不少开源AI服务端把模型配置放在config.toml文件里。Windows下用记事本编辑容易保存成带BOM的UTF-8,部分toml解析库会把BOM当非法字符;还有人会用制表符缩进、把双引号写成中文全角引号、或者只看教程填了个不存在的模型名,服务自然起不来。
解决:用VS Code打开config.toml,右下角确认编码是UTF-8,存盘时不要选带BOM的选项;用Python跑一行import tomllib; tomllib.load(open('config.toml','rb')),能读出字典说明解析没毛病;再把model、api_key和文档一条条对照。这个坑和前端代码无关,但它是“为什么请求发不出去”的一个高频背锅点。
5.5 自定义导航栏后聊天头部被状态栏/刘海挡住
现象:同一套代码,在iPhone 14 Pro上聊天时间、标题被状态栏盖住,在安卓上却正好;调固定数值后换一台机器又不合适。
原因:小程序顶部导航栏高度不是固定值,取决于手机状态栏高度和微信胶囊位置。把padding-top写死成某个像素,只有一台机器是正常的。
解决:动态计算导航栏高度:
const menu = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = wx.getStatusBarHeight(); const navHeight = (menu.top - statusBarHeight) * 2 + menu.height;把这个navHeight设置到自定义导航栏的style里,消息列表就不会再被刘海遮挡。如果你用的是页面自带导航栏,把navigationStyle保持默认即可,不需要处理这个计算。
6. 上线前把密钥藏起来:云函数代理与真机验证
6.1 为什么密钥不能留在前端
小程序前端代码跑在用户手机上,源码可以被反编译,抓包也能看到请求头。如果apiKey写在config.js里,它相当于公开的。正确做法是用微信云开发写一个云函数,小程序把消息发给自己的云函数,云函数读取服务端环境变量里的密钥,再转发给模型服务。
// cloudfunctions/chatProxy/index.js const cloud = require('wx-server-sdk') cloud.init() const axios = require('axios') exports.main = async (event) => { const res = await axios.post( `${event.baseURL}/chat/completions`, { model: event.model, messages: event.messages, temperature: event.temperature }, { headers: { Authorization: `Bearer ${process.env.LLM_API_KEY}` } } ) return res.data }前端调用只要换成wx.cloud.callFunction,把参数传给云函数就行。process.env.LLM_API_KEY在云开发控制台的“云函数-环境变量”里配置,密钥不会出现在前端代码里。还有个细节:baseURL最好也在服务端写死,不要从客户端传,否则别人可以直接调用你的云函数改掉目标地址。
6.2 真机验证的两个固定动作
- 关闭开发者工具的“不校验合法域名”,用预览二维码在真机上把对话完整跑一遍,确认HTTPS和证书链没问题。
- 连续问10轮,验证多轮记忆仍然有效;点新建会话,确认旧会话没被带到新会话,除非你的产品设计本身就需要承接。
从那以后,我从别人手里接小程序模板,第一件事就不再是点编译了,而是先打开config.js看密钥有没有写死,再在开发者工具里跑一次,最后上真机走一遍完整对话。这套流程看起来机械,但它确实帮我拦下了至少一半的上线事故。希望帮到你。
本文还有配套的精品资源,点击获取