☰
微信小程序AI机器人源码实战:后端转发与上线避坑指南
2026/10/6 10:40:36 网站建设 项目流程

简介:面向微信小程序初学者的智能机器人(学习版)小程序源码,适合想了解小程序完整项目结构、页面交互与基础逻辑的开发者。示例围绕“智能机器人”场景,展示从首页展示、聊天交互到工具函数封装等常见实现方式。压缩包共19个文件,约15KB,包含wxml页面模板、wxss样式表、js业务逻辑、json配置以及png/jpg图片资源,目录划分为zndg、pages、utils、images等模块,便于按功能片段拆解学习。已有176人学习下载。借助该实例可快速理解app.json全局配置、页面生命周期、事件绑定与数据渲染等关键知识点,也可作为课程作业或入门练手的基础框架,帮助缩短从零搭建微信小程序的摸索时间。

1. 微信小程序源码智能机器人(学习版):先别急着跑,这篇文章带你拆开看

“微信小程序源码 智能机器人(学习版)”这一类项目,在开发者社区里的热度一直不低。它看起来像是一个开箱即用的成品:下载源码、填入 AppID、点一下编译,就能在手机微信里和一个聊天机器人对话。但以我带过不少新人的经验来说,拿到源码不等于能跑通,能跑通也不等于能上线。这个标题里的“学习版”三个字,恰恰是重点:它给你的不是一个黑盒产品,而是一条完整可改的链路——小程序端怎么做聊天界面,后端怎么转发大模型接口,对话记忆怎么存,发布前还有哪些绕不开的配置。这篇文章就顺着这条链路往下拆,适合三类人:正在学小程序开发、想找一个真实项目练手的人;想在团队内部或社团里快速搭一个问答机器人的开发者;以及手里已经有了一份这种源码、却在联调阶段反复翻车的人。我尽量把每一步的命令、参数和坑都讲清楚,你照着敲就能在本地跑起来。

2. 先看懂架构:为什么智能机器人小程序要把 API Key 放在后端

2.1 一条消息从输入框到大模型的完整链路

任何一款“智能机器人”小程序,都不可能是小程序端直接去调用大模型接口。原因有三个,都是硬性的:第一,小程序端wx.request只允许请求你在小程序后台配置过的HTTPS 合法域名,大模型厂商的接口域名你很难直接加到这个小程序账号下,而且就算加了,也存在跨域和证书的兼容问题;第二,大模型的 API Key 一旦写进小程序代码里,就等于公开了,因为小程序代码包可以被反编译,密钥泄露后带来的盗刷账单会让人很难受;第三,微信生态对用户输入内容有审核要求,你不希望用户在小程序里对机器人说的一段话,原封不动地直接透传给大模型接口,中间需要有一层过滤和鉴权。

所以,一个能落地的“学习版”源码,几乎都采用同样的三段式结构:

层职责常见实现
小程序端聊天界面、输入交互、消息流展示WXML 模板 + wx.request
中转服务接收小程序请求、拼装消息上下文、调用大模型、返回结果Node.js + Express / 云函数
大模型接口真正生成回复内容兼容 OpenAI Chat Completions 格式的 HTTP 接口

一条消息的完整流向是:用户在输入框敲下文字,小程序把这段文字和最近的聊天历史一起通过wx.requestPOST 到中转服务的/api/chat接口;中转服务校验参数、跑一遍敏感词过滤,把历史消息加上系统提示词组合成messages数组,再请求大模型接口;大模型返回文本后,中转服务把文本包成 JSON 返回给小程序,小程序再把这条新消息渲染到气泡里。

2.2 为什么 Key 必须放后端(以及学习版通常怎么设计)

很多人第一次拿到这种源码,第一反应是去前端代码里搜api_key或者sk-开头的字符串,搜不到就开始慌。其实搜不到才是正常的。一个设计合格的学习版源码,会把所有跟密钥相关的配置放在后端的环境变量或配置文件里,前端只有一个不带任何敏感信息的中转地址。

我自己在给团队搭内部机器人时,习惯把后端启动方式写成这样:

export LLM_API_KEY="你的密钥" export LLM_API_BASE="https://api.example.com/v1" export LLM_MODEL="gpt-4o-mini" node server.js

这样做的直接好处是:这个仓库就算被传到公开仓库、被同事拿去二次开发,密钥也不会跟着代码走。学习版源码的意义也在这里——它向你展示的是“密钥放后端、前端只发请求”这套标准姿势,而不是给你一个填好 Key 就能跑的傻瓜包。

后端的结构通常很薄,一个 Node.js 服务只需要暴露两三个路由:/api/chat用于对话、/api/history用于拉取旧消息(可选)、/health用于健康检查。连数据库都不用上,消息历史直接由小程序端传回来即可。这样设计不是偷懒,而是学习版该有的复杂度:把链路跑通是第一目标,等弄清楚原理之后再上 Redis、上 MySQL,那是后话。

3. 把聊天后端搭起来:用 Node.js 写一个可改的转发服务

3.1 最少可用的 Express 转发接口

先准备一个干净的 Node.js 项目。我用 Node 18 及以上版本,这样可以直接用内置的fetch,不需要额外装axios。依赖只有一个express,如果你连依赖都想省,也可以只用 Node 原生http,但 Express 的路由和中间件能省去不少样板代码,学习版选它更合适。

// server.js const express = require("express"); const app = express(); app.use(express.json()); const SYSTEM_PROMPT = process.env.SYSTEM_PROMPT || "你是一个乐于助人的智能助手,请用简洁、有条理的中文回答用户问题。"; // 简单的敏感词过滤,正式使用时应加载独立词库 const BLOCK_WORDS = ["暴力", "赌博"]; function checkBlockWords(text) { for (const word of BLOCK_WORDS) { if (text.includes(word)) return word; } return null; } app.post("/api/chat", async (req, res) => { const { message, history } = req.body || {}; // 参数校验:message 必须是非空字符串;history 必须是数组,缺省时给空数组 if (!message || typeof message !== "string") { return res.status(400).json({ error: "message 不能为空" }); } const msgHistory = Array.isArray(history) ? history : []; // 对用户输入做一遍敏感词检查 const blocked = checkBlockWords(message); if (blocked) { return res.status(200).json({ reply: "这个内容我不太方便回答。" }); } // 拼接 messages:system + 最近若干条历史 + 当前消息 const recentHistory = msgHistory.slice(-6); // 只保留最近 6 轮,控制 token 消耗 const messages = [ { role: "system", content: SYSTEM_PROMPT }, ...recentHistory, { role: "user", content: message }, ]; try { const response = await fetch(`${process.env.LLM_API_BASE}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.LLM_API_KEY}`, }, body: JSON.stringify({ model: process.env.LLM_MODEL || "gpt-4o-mini", messages, temperature: parseFloat(process.env.TEMPERATURE || "0.7"), max_tokens: parseInt(process.env.MAX_TOKENS || "512"), }), timeout: 30000, // 大模型接口最长等待 30 秒 }); if (!response.ok) { console.error("LLM API error:", response.status, await response.text()); return res.status(502).json({ error: "大模型接口暂时不可用" }); } const data = await response.json(); const reply = data.choices[0].message.content; return res.json({ reply, usage: data.usage || null }); } catch (err) { console.error("chat handler error:", err.message); return res.status(500).json({ error: "服务内部错误" }); } }); app.get("/health", (req, res) => { res.json({ status: "ok" }); }); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`server listening on ${port}`); });

这段代码里,最关键的设计是把SYSTEM_PROMPT、LLM_API_KEY、LLM_API_BASE、LLM_MODEL、TEMPERATURE、MAX_TOKENS全部做成环境变量。这样不同场景之间切换时,不需要改动代码,只需要改启动脚本里的 export 就行。messages数组的拼装顺序也很重要:system在最前,接着是历史消息,最后是当前用户消息。大模型对这个顺序非常敏感,顺序错了会出现人格漂移,也就是明明设定了“学习助手”,回答起来却像个娱乐主播。

3.2 会话记忆与敏感词过滤:两个不能省的后端模块

会话记忆在“学习版”里通常不落库,而是靠小程序端把history传回来。后端只负责把这个数组拼进messages。这种做法省事,但有一个必须处理的问题:历史消息越长,token 消耗越大,响应越慢。所以我在上面代码里用了slice(-6),只保留最近 6 轮对话。这个值不是固定的:如果max_tokens设得比较大,比如 1024,6 轮历史加上 system 后可能已经超过 2000 token,这时要适当把轮数降到 4 轮;如果是做客服问答这种单轮为主的应用,甚至可以只保留最近 1 轮。

敏感词过滤放后端而不是前端,同样是出于安全考虑。前端的过滤逻辑可以被轻松绕开——用户抓包改请求,直接绕过小程序页面,把任意内容 POST 到你的后端接口。后端的过滤才是最后一道闸门。上面代码里用的是最简单的前缀匹配数组,实际项目里我一般会维护一个独立的filter-words.txt,每次启动时读入内存,匹配逻辑也从includes升级成词表正则。需要注意,过滤命中后不要直接返回 400,而是返回一个 200 但把回复替换成“我不方便回答”,这样对用户更友好,而且不会暴露你的过滤词表。

后端还有一个容易被忽略的点:对上游大模型接口的超时处理。fetch的timeout参数在 Node 18 里能作用,但很多代理场景下,这个超时可能覆盖不到 DNS 解析和连接建立的完整链路。稳妥做法是给整个请求包一层AbortController,用一个setTimeout在 35 秒后主动abort。否则你小程序端等待响应的用户会看到转圈很久,然后收到一个网络异常提示,这种体验非常劝退。

4. 小程序前端如何接上后端:WXML 聊天页与 wx.request 联调

4.1 聊天页的数据结构与 WXML 气泡渲染

小程序端的聊天页面是学习版源码里最直观、也最好改的部分。核心数据结构就一个数组:messages。数组里每个元素有两个字段——role表示消息来源(user或assistant),content是消息文本。不需要额外的状态字段,渲染时完全由role决定气泡样式,简单直接。

<!-- pages/index/index.wxml --> <view class="chat-page"> <scroll-view class="message-list" scroll-y scroll-into-view="{{scrollInto}}" scroll-with-animation > <view wx:for="{{messages}}" wx:key="index" id="msg-{{index}}" class="message-row {{item.role === 'user' ? 'row-user' : 'row-assistant'}}" > <view class="bubble {{item.role === 'user' ? 'bubble-user' : 'bubble-assistant'}}"> <text>{{item.content}}</text> </view> </view> </scroll-view> <view class="input-bar"> <input class="chat-input" value="{{inputText}}" bindinput="onInput" bindconfirm="sendMessage" placeholder="说点什么吧" confirm-type="send" /> <button class="send-btn" bindtap="sendMessage" disabled="{{sending}}"> {{sending ? '思考中' : '发送'}} </button> </view> </view>

scroll-into-view绑定到一个动态的id字符串,作用是让新消息产生后列表自动滚到底部。这个细节不做的话,聊天几轮后页面会停在上方,用户得手动划下去,很影响观感。disabled="{{sending}}"的作用是防止用户在手速快时连点发送,导致后端收到一堆并发请求、上下文顺序错乱。

// pages/index/index.js const API_BASE = "http://127.0.0.1:3000"; // 本地开发时用;上线换 https 域名 Page({ data: { messages: [], inputText: "", sending: false, scrollInto: "", }, onInput(e) { this.setData({ inputText: e.detail.value }); }, sendMessage() { const text = this.data.inputText.trim(); if (!text || this.data.sending) return; const newMessages = [ ...this.data.messages, { role: "user", content: text }, ]; this.setData({ messages: newMessages, inputText: "", sending: true, scrollInto: `msg-${newMessages.length - 1}`, }); wx.request({ url: `${API_BASE}/api/chat`, method: "POST", data: { message: text, history: this.data.messages.slice(-6), }, timeout: 30000, success: (res) => { if (res.statusCode === 200 && res.data.reply) { this.appendAssistantMessage(res.data.reply); } else { this.appendAssistantMessage("服务开小差了,稍后再试一下吧。"); } }, fail: () => { this.appendAssistantMessage("网络异常,请检查后端服务是否启动。"); }, complete: () => { this.setData({ sending: false }); }, }); }, appendAssistantMessage(content) { const messages = [ ...this.data.messages, { role: "assistant", content: "" }, ]; this.setData({ messages }); // 打字机效果:每秒拆 20 个字符写入,避免一次性渲染长文的生硬感 let index = 0; const timer = setInterval(() => { index += 20; const slice = content.slice(0, index); this.setData({ [`messages[${messages.length - 1}].content`]: slice, scrollInto: `msg-${messages.length - 1}`, }); if (index >= content.length) { clearInterval(timer); } }, 30); }, });

这里有一个值得玩味的设计:history: this.data.messages.slice(-6)。这段代码里我用的是发送前一刻的messages,也就是包含了刚 push 进去的那条用户消息,但我在后端拼messages时又加了一遍{ role: "user", content: message },这会导致同一个问题被放进上下文两次。正确做法是在前端不传最新那条,也就是this.data.messages.slice(0, -1).slice(-6)。这个细节很多人第一次写都会漏,结果就是大模型经常复述用户刚说过的内容。顺手改掉,能省很多调试时间。

4.2 本地联调配置:开发工具、真机与域名白名单

小程序开发的一个特点是:本地联调的链路比普通网页要绕。你在编辑器里把后端跑起来之后,还要处理两件事:一是开发工具里的request 合法域名校验,二是真机上能不能连上你的本地服务。

开发工具处理很简单。默认情况下,wx.request的 url 如果不是 HTTPS,或者域名不在小程序后台的白名单里,请求会直接失败,并在 Console 报url not in domain list。在开发阶段,你不需要真的去买域名配 HTTPS——在微信开发者工具右上角点“详情”,进入“本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”这一项,就可以用http://127.0.0.1:3000来做本地联调了。这个开关只影响调试,不影响真机预览和线上发布,所以不用担心它带来安全隐患。

真机调试又是另一回事。你点“预览”生成二维码,手机扫码打开的小程序是正式环境的配置,它不会认你本地的127.0.0.1。常见做法是:开发工具里点“真机调试”而不是“预览”,真机调试模式下流量会经开发工具代理转发,本地服务可以被访问到。如果你要在手机上看效果,走这条路径最省事。另一种路径是让后端监听0.0.0.0,然后把请求地址改成你电脑的局域网 IP,比如http://192.168.1.5:3000,但真机上要同时打开开发工具里那个“不校验域名”的开关,否则访问 http 明文接口会被拦。我一般建议新手先用“真机调试”模式,等你把代码逻辑都验证清楚了,再考虑搞 HTTPS 域名走正式预览。

4.3 API 封装与多页面复用

如果只是做一个单聊天页的 demo,把wx.request直接写在页面里没问题。但只要你想在这个项目上继续加功能,比如加一个“历史会话列表页”、加一个“角色设定页”,就值得把请求逻辑抽出来单独放一个文件。学习版源码的价值也在这里:结构规范,才方便你往上加东西。

// utils/api.js const API_BASE = "http://127.0.0.1:3000"; function post(path, data) { return new Promise((resolve, reject) => { wx.request({ url: `${API_BASE}${path}`, method: "POST", data, timeout: 30000, success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else { reject(new Error(`HTTP ${res.statusCode}`)); } }, fail: reject, }); }); } module.exports = { post };

封装成 Promise 之后,页面里调用就变成api.post("/api/chat", { message, history }).then(...)。好处是你可以统一在post里加 token 鉴权、打日志、统计耗时,而不用在每个页面重复写这些逻辑。如果你拿到的学习版源码没有做这层封装,我建议你动手补上——它不会花超过十分钟,但后续改起来会顺手很多。

5. 发布前避坑:5 个把学习版项目挡在门外的真实问题

5.1 域名与网络层:request 合法域名与本地联调翻车

高频报错是开发工具 Console 出现url not in domain list,请求直接失败。原因是小程序默认对wx.request请求的域名做白名单校验,你本地127.0.0.1当然不可能在白名单里。解决路径见 4.2:开发阶段勾选“不校验合法域名”,上线前再回到小程序管理后台把 HTTPS 域名加入 request 合法域名列表。

真机预览后接口 502 的坑也很常见。现象是开发工具里一切正常,但手机扫码打开后聊天接口全部失败。原因通常是真机访问的域名没有走开发工具代理,而你本地服务监听的是127.0.0.1,手机根本访问不到。解决方式有两个:一是改用“真机调试”模式,二是把后端监听0.0.0.0,并把小程序里的 API 地址改成电脑的局域网 IP。如果你后端部署在云服务器上,那么确认安全组端口放行后,直接用 HTTPS 域名即可。

还有一件事容易被忽略:wx.request的timeout默认是 60 秒,但大模型接口普遍偏慢,尤其是流式输出没做时,一个完整回复可能需要十几秒。你如果不在前端设一个合理的timeout(比如 30 秒),用户会看到请求一直转圈、最后报错。这不是后端挂了,是浏览器/小程序端的超时把请求掐断了。

5.2 接口与数据层:上下文丢失与鉴权缺失

现象是:第二次提问时,机器人完全忘记了上一轮说过什么,甚至反过来问你“你说的‘它’是谁?”这个问题的根源几乎一定是history没有传对。检查三件事:第一,前端发请求时history数组里有没有包含之前轮次的assistant回复;第二,后端有没有把history正确拼接进messages;第三,slice(-6)的取值是否合理。很多人在前端只传最后一条用户消息,没传之前的助手回复,大模型的上下文自然是不完整的。

另一个隐蔽的问题是好几个请求同时发出。用户在输入框没被disabled时连点发送,前端同时发出三个/api/chat,后端按请求顺序依次调用大模型,返回时顺序却乱了,最后展示出来的对话逻辑完全是混乱的。前端的sending状态必须覆盖整个请求周期,而不是只在点击瞬间生效。

后端缺少访问控制是最容易埋雷的地方。如果你把服务部署到公网,任何人只要拿到接口地址,就可以无限调用你的大模型接口,账单会非常感人。学习版阶段至少加一个固定请求头做鉴权,比如前端每个请求带x-token: 你的内部令牌,后端用中间件统一校验:

app.use("/api/chat", (req, res, next) => { const token = req.headers["x-token"]; if (token !== process.env.API_TOKEN) { return res.status(401).json({ error: "unauthorized" }); } next(); });

这个方案防不了高级攻击,但足以挡住互联网上绝大多数扫描流量。

5.3 内容与部署层:密钥泄露与审核不通过

我把密钥问题放在部署部分说,是因为它往往到上线阶段才暴露。现象是你的代码包被某个用户反编译,在app.js里找到了api_key字段。原因是你(或你所用的学习版源码)把 Key 放在了前端代码里。解决方式前面已经反复提过:Key 只放后端环境变量,前端代码里任何位置都不允许出现sk-开头的字符串。你可以用一行命令自查:

grep -r "sk-" pages/ 2>/dev/null && echo "发现疑似密钥,请清理!"

小程序审核不通过也是卡住很多人的环节。现象是提交审核后收到“涉及问答类目需提供相关资质”之类的驳回理由。原因多半是类目选择不当,或者对话内容没有过滤。我的经验是:个人主体优先选“工具 > 效率”,避免选“社交”或“社区问答”这类高门槛类目;同时把敏感词过滤明确放在后端,并在小程序的“用户隐私保护指引”里声明收集用户输入内容仅用于对话回复。这样处理的通过率会高很多。

6. 收尾技巧:接口自检脚本与流式返回升级方向

6.1 回归脚本:把后端接口当“黑匣子”压一遍

在做任何前端联调之前,我会先用一个 Node 脚本对后端做一轮冒烟测试,确认接口正常再开开发工具。这样可以避免小程序端的编译问题、页面报错等干扰因素混在一起,排查起来没头绪。脚本很简单,循环发 3 条消息,打印每次的耗时和返回码。

// smoke-test.js const API_URL = "http://127.0.0.1:3000/api/chat"; const cases = [ { message: "你好", history: [] }, { message: "1+1等于几", history: [] }, { message: "介绍一下你自己", history: [] }, ]; async function run() { for (const item of cases) { const start = Date.now(); try { const res = await fetch(API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(item), }); const data = await res.json(); console.log( `${item.message} -> ${res.status},耗时 ${Date.now() - start}ms,回复:${data.reply?.slice(0, 20)}` ); } catch (err) { console.log(`${item.message} -> 请求失败:${err.message}`); } } } run();

smoke-test.js的作用有两个:一是验证后端服务还活着、逻辑没被改坏;二是粗略看响应耗时。单轮耗时超过 5 秒就需要考虑:是不是temperature太高导致生成冗长;是不是max_tokens设得过大;是不是历史消息太多。如果每个请求都快 10 秒,前端体验必然差,这时候优先缩小max_tokens和slice(-6)的窗口,而不是去折磨前端加 loading 动画。

6.2 升级方向:WebSocket 流式返回与打字机体验

HTTP 请求一整个包返回的方式,缺点很明显:大模型生成一个 500 字的回答需要大几秒,用户在这段时间里只能看着“思考中”三个字。把接口升级成流式返回后,用户可以像用主流 AI 应用那样,看着文字一个字一个字蹦出来。技术上,后端把大模型接口切换到stream: true,然后通过 WebSocket 把增量内容推给小程序。

// 最简 WebSocket 推送示意(配合 ws 库) const WebSocket = require("ws"); function sendStreamToClient(socket, messages) { socket.send(JSON.stringify({ type: "start" })); const stream = fetchAnswerStream(messages); // 拿到大模型的流式响应 stream.on("data", (chunk) => { socket.send(JSON.stringify({ type: "delta", content: chunk })); }); stream.on("end", () => socket.send(JSON.stringify({ type: "end" })); }

小程序端用wx.connectSocket建立连接,监听socketTask.onMessage把增量内容拼到当前气泡上。需要提醒的是:WebSocket 的消息是分帧的,一帧可能包含多个delta,也可能一个delta被拆成好几帧,处理时得做一个简单的粘包拆分。学习版阶段不急着上流式,先把 HTTP 跑通,能回答、有上下文、发布审核过,这套链路已经能给你带来完整的学习收益。

我自己每次拿到一个新的学习版源码,都不会急着看代码,而是先按“后端启动 -> 冒烟测试 -> 前端联调 -> 真机验证”这四步走一遍,过程中把每个环节的报错和日志记录下来。等这些流程都顺手了,再开始动代码、加功能、换交互。这套习惯帮我省掉了很多重复的排障时间,也希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询