资讯动态

小程序接入大模型聊天机器人:Node中间层与SSE流式落地实践

发布时间:2026/10/20 20:38:21 来源:尧图企业网站定制
简介面向小程序开发者提供在小程序平台快速集成大模型聊天机器人的完整代码工程与实现方案涵盖从大模型API接入、对话流管理、消息缓存到界面渲染的完整链路适合具备基础前端知识、希望快速落地智能对话能力的中级开发者。资源共2003个文件以js、ts、json及wxss、wxml等小程序前后端核心文件为主对应业务逻辑、类型定义、配置文件与界面样式另有md说明文档、license许可文件及少量png图片压缩包整体仅2.45MB结构紧凑便于快速检索。目前已有406人学习下载。通过对照源码可掌握微信小程序调用大模型API的关键细节包括crypto-js加密处理、markdown解析渲染、emoji表情支持、HTML数据解码等实用技巧所附wxs模块与工具函数可直接复用能够快速改造为多场景智能客服、个人助理等应用。1. 小程序快速实现大模型聊天机器人先跑通聊再谈优化去年下半年一直在做企业内部的 AI 问答助手最头疼的不是模型效果而是怎么把大模型快速嵌进小程序里跑起来。原生智能客服 SDK 在小程序里限制多域名白名单、流式输出、上下文管理全是坑最后折腾出一个「小程序端 Node 中间层 大模型 API」的最小可运行方案前后端加起来不到 300 行核心代码属于复制到项目里改改就能用的那种。本文要讲的这套实现核心解决三件事小程序端如何绕过域名白名单直连大模型 API、如何用原生小程序能力做流式打字机效果、以及多轮对话上下文怎么管理。适合手里有现成大模型 API、想在微信小程序里快速落地一个聊天机器人的开发者不涉及训练模型也不讲玄学调参通篇是能直接跑的工程代码。2. 整体架构设计为什么必须加一个 Node 中间层2.1 技术选型与架构分层小程序调用大模型 API 最直接的问题是域名白名单。微信公众平台要求所有 request 请求域名必须配置在后台的白名单里而大模型服务商的域名一般不会让你随便加有些甚至要求走特定的鉴权头WSS 协议在某些低版本基础库上还不稳定。通用的解法就是加一层 Node 中间层做请求转发让小程序只请求我们自己的域名。这套实现的架构分三层小程序前端层负责对话 UI、流式渲染、会话管理使用原生小程序框架不引第三方库Node 代理层使用 Express 搭建轻量服务器负责接收小程序请求、转发到大模型 API、处理流式数据转发大模型服务层使用任意兼容 OpenAI Chat Completions 接口格式的大模型 API支持 stream 模式选择 Express 而不是 Koa 或者 NestJS原因就一个字轻。这个中间层没有复杂的业务逻辑只做一件事就是把小程序的请求转成符合大模型 API 规范的请求。Express 的中间件模型足够用了而且部署到任何 Node 环境都能跑。2.2 目录结构与请求链路整个项目的目录结构如下miniprogram-chatbot/ ├── miniprogram/ # 小程序端代码 │ ├── pages/ │ │ └── chat/ # 聊天页面 │ │ ├── chat.js │ │ ├── chat.wxml │ │ ├── chat.wxss │ │ └── chat.json │ ├── utils/ │ │ └── sse-parser.js # 流式响应解析器 │ └── app.js ├── server/ # Node 中间层 │ ├── app.js # Express 入口 │ ├── proxy.js # 大模型代理转发 │ └── package.json └── README.md请求链路是小程序 wx.request 发出对话请求 → Node 中间层接收并解析鉴权信息 → Node 中间层转发到大模型 API → 大模型流式返回 → Node 中间层分帧转发给小程序 → 小程序端解析 SSE 流并逐步渲染。这条链路里最关键的细节是流式传输。大模型 API 的 stream 模式返回的是 Server-Sent Events 格式每行是一个data: {json}结构。小程序端的 wx.request 默认不启用流式接收需要手动开启 enableChunked 参数才能拿到分块数据。很多开发者在这里卡住以为是小程序不支持流式实际上是没开对参数。2.3 鉴权与安全设计小程序端不能直接存放大模型 API Key否则小程序包被反编译后密钥直接泄露。这里的安全方案是API Key 只存在 Node 中间层的环境变量里小程序通过自定义请求头 X-Api-Key 传一个短期 tokenNode 中间层校验 token 后再用真正的 API Key 去请求大模型。token 的生成可以简单用 HMAC 签名小程序端存签名结果Node 端校验签名合法性。如果是企业内部使用甚至可以做一个一次性 token 换取机制小程序先调登录接口拿 session再用 session 换取请求凭证。下面给出一个最小可用的 HMAC 验证方案// server/proxy.js const crypto require(crypto); function verifyToken(token, userId) { const secret process.env.TOKEN_SECRET; const expected crypto .createHmac(sha256, secret) .update(userId) .digest(hex); return token expected; }这段代码的作用是对小程序传入的 token 做 HMAC-SHA256 签名比对。secret 从环境变量读取userId 对应当前登录用户。如果小程序端签名的结果和 Node 端算出来的一致就放行请求否则返回 401。token 本身不包含敏感信息即使被截获也只能在有效期内用于请求无法直接拿到 API Key。3. 核心模块拆解从对话发起到消息渲染的完整链路3.1 流式接收的关键配置小程序端 wx.request 默认的 responseType 是 text服务端返回的流式数据会被一次性接收并拼接成字符串这样无法实现打字机效果。要支持流式接收需要设置enableChunked: true同时把 responseType 设为 arraybuffer。为什么是 arraybuffer因为流式分帧是二进制层面的操作每帧数据需要缓冲和解析不能直接当字符串处理。看下面的请求配置// miniprogram/pages/chat/chat.js sendMessage(content) { const userId user_001; const token this.generateToken(userId); wx.request({ url: https://your-server.com/api/chat, method: POST, responseType: arraybuffer, enableChunked: true, header: { Content-Type: application/json, X-User-Id: userId, X-Token: token }, data: { messages: this.buildContext(content), stream: true }, success(res) { // 流式数据在 res.data 中分帧回调 this.handleStreamData(res.data); } }); }这里enableChunked: true是关键开关打开后 wx.request 的 success 回调会多次触发每次返回一部分数据。responseType 必须配合使用 arraybuffer因为分帧数据本质上是二进制流直接使用 text 会让小程序帮你做一次完整解码丢失流式边界。有个非常容易踩的细节enableChunked必须在微信开发者工具的基础库 2.14.0 以上才能生效真机上要求更高。如果测试时发现没有流式效果第一件事检查后台设置里有没有开启「增强编译」中的 ES6 转 ES5 选项这个选项会影响部分浏览器内核下 stream 数据的接收。3.2 SSE 解析器把二进制分帧还原成可识别的数据拿到分帧的 arraybuffer 后要做的事情是把这些零碎的二进制数据拼接还原再按 SSE 协议解析出data: {json}结构。SSE 的格式是每个事件以\n\n结尾每条消息以data:开头。问题在于分帧可能在任何位置切断可能一条完整消息被切成了两次分帧。我一般会在 utils/sse-parser.js 里维护一个全局缓冲数组每次收到分帧就拼接然后尝试按\n\n切分能完整切出来的才解析剩下的留在缓冲区等下一次分帧。这个做法类似流式解析器的经典思路很多浏览器端的 fetch 流式解析也是这么干的// miniprogram/utils/sse-parser.js class SSEParser { constructor() { this.buffer ; this.isBinaryMode true; // 收到的原始数据是 ArrayBuffer } append(chunk) { if (this.isBinaryMode) { const uint8Array new Uint8Array(chunk); this.buffer new TextDecoder(utf-8).decode(uint8Array, { stream: true }); } else { this.buffer chunk; } return this.extractEvents(); } extractEvents() { const events []; let boundary this.buffer.indexOf(\n\n); while (boundary ! -1) { const rawEvent this.buffer.slice(0, boundary 2); this.buffer this.buffer.slice(boundary 2); const dataLine rawEvent .split(\n) .find(line line.startsWith(data:)); if (dataLine) { const jsonStr dataLine.slice(6).trim(); if (jsonStr jsonStr ! [DONE]) { try { events.push(JSON.parse(jsonStr)); } catch (e) { console.warn(SSE parse error, ignoring chunk:, e); } } } boundary this.buffer.indexOf(\n\n); } return events; } }这个解析器有两个细节值得说一是new TextDecoder(utf-8).decode(uint8Array, { stream: true })里的{ stream: true }参数很重要它告诉解码器这是一个流式输入如果一段多字节字符被拆在两个 ArrayBuffer 里解码器会自动暂存不完整字符等下一段到达时补全避免中文乱码。二是在切分事件时用\n\n作为边界这是 SSE 协议规定的标准事件分隔符比按行处理更稳。3.3 Node 中间层的代理转发核心Node 中间层的核心逻辑是接收小程序的请求以流式方式转发给大模型 API再把响应实时转发给小程序。这里有一个容易被忽略的问题Node 端向大模型 API 发请求时也要用流式方式接收不能等全部响应回来再一次性转发否则打字机效果就没了。使用 Node 原生 fetch 或 axios 都可以我一般直接用原生 fetch 加 getReader 做流式转发省掉引入 axios 流式库的麻烦。以下是代理转发的完整实现// server/proxy.js const express require(express); const router express.Router(); router.post(/api/chat, async (req, res) { const { messages, stream } req.body; const apiKey process.env.LLM_API_KEY; const baseURL process.env.LLM_BASE_URL || https://api.openai.com/v1; res.setHeader(Content-Type, text/event-stream; charsetutf-8); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); try { const upstreamRes await fetch(${baseURL}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model: gpt-4o-mini, messages, stream: true, temperature: 0.7 }) }); const reader upstreamRes.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); for (const line of lines) { if (line.startsWith(data:)) { res.write(${line}\n\n); } } } res.end(); } catch (error) { console.error(Upstream request failed:, error); res.status(502).json({ error: Bad Gateway }); } });这段代码里buffer.split(\n)把拿到的上游数据按行切分lines.pop()把最后一段不完整的行放回 buffer等下一次 read 再拼接。这是流式代理转发最常见的模式本质上和上一节小程序端 SSE 解析是同一个思路只是这里转发的是原始数据流不需要解析 JSON。对于 Stream 模式需要注意res.write时一定不要加\n\n之外的额外内容微信小程序端的 SSEParser 是以\n\n作为事件的边界多加了空格或换行都可能导致解析错位。3.4 上下文管理与多轮对话大模型 API 本身是不带记忆的每次请求都需要把完整的对话历史作为 messages 数组传过去。多轮对话的核心是把历史 messages 存在某个地方每次发消息时拼接起来。这套实现里做了最轻量的方案小程序端维护一个本地历史记录数组每次发送时带上最近 10 轮对话服务端每次请求直接原样透传给大模型 API。不做分布式会话存储因为对于轻量级聊天场景客户端存储已经够用且天然隔离用户。// miniprogram/pages/chat/chat.js buildContext(currentContent) { const history this.data.chatHistory || []; const context []; for (const item of history.slice(-20)) { context.push({ role: item.role, content: item.content }); } context.push({ role: user, content: currentContent }); return context; }这段代码里的history.slice(-20)表示最多保留最近 20 条消息10 轮对话作为上下文。这是个控制 token 消耗的关键参数上下文越长单次请求的 token 数量越多费用也越高。实际使用时需要根据大模型 API 的 max_tokens 限制做取舍一般 20 条左右是一个性价比比较高的平衡点既能保持对话的连续性又不会让 token 消耗爆炸。如果想要更精细的上下文管理可以在 Node 中间层做裁减逻辑按角色交替压缩、把早期的完整消息替换成摘要等。但在快速落地阶段直接截取最近 N 条就够了startup 风格的原则是能跑通就不要过度设计。4. 环境准备与运行配置从拉代码到出现对话气泡4.1 Node 中间层的启动配置把项目拉下来后第一件事是去 server 目录安装依赖并配置环境变量。中间层仅依赖 express 和 dotenv不需要其他运行时依赖Node 版本建议在 14 以上因为我们要用原生 fetch只有在 Node 18 之后才内置可用。环境变量配置文件如下# server/.env PORT3000 LLM_BASE_URLhttps://your-llm-api.example.com/v1 LLM_API_KEYsk-your-api-key TOKEN_SECRETyour-hmac-secret LLM_MODELgpt-4o-mini启动命令cd server npm install npm start启动后建议先不做任何界面测试直接用 curl 验证代理层是否正常工作curl -X POST http://localhost:3000/api/chat \ -H Content-Type: application/json \ -H X-User-Id: test_user \ -H X-Token: 生成好的token \ -d { messages: [{role: user, content: 你好}], stream: true }如果 token 验证被放行且 API Key 无误应该能看到流式返回的 SSE 数据。这一步先排除小程序端的干扰单独验证 Node 中间层和 API 的连通性。常见的失败场景是LLM_BASE_URL配置了不带/v1的根地址导致拼出的请求路径变成/chat/completions缺失版本前缀返回 404注意看 fetch 拼接后的完整 URL。4.2 小程序端的三处必改配置小程序端不是拉下来就能跑的三个地方必须改否则页面会白屏或请求失败。第一处是 app.js 里的全局配置// miniprogram/app.js App({ globalData: { serverURL: https://your-domain.com, // 改成你自己的 HTTPS 域名 userId: user_001, tokenSecret: your-hmac-secret // 仅用于本地演示 } })第二处是 chat.js 里生成 token 的逻辑用与 Node 端相同的 HMAC 算法生成签名。注意这里的 secret 必须和 Node 端环境变量里的 TOKEN_SECRET 一致不一致的结果就是所有请求返回 401。function generateToken(userId, secret) { return crypto .createHmac(sha256, secret) .update(userId) .digest(hex); }但在小程序端不能直接使用 Node 的 crypto 模块我一般用微信小程序的wx.getRandomValues配合纯 JS 实现的 HMAC-SHA256 库来生成签名。如果想快速验证可以在 Node 端把 token 生成接口临时暴露出来让小程序直接调用换取 token上线前再换签名方案。第三处是聊天页面的事件绑定和数据渲染逻辑。streaming 后的 data 结构有三个字段需要处理choices[0].delta.content是增量文本choices[0].delta.role是角色标记choices[0].finish_reason标记流是否结束。把增量 content 逐步拼到 this.data 的当前消息文本上配合微信的 setData 实现打字机效果handleStreamData({ events, page }) { for (const event of events) { const delta event.choices event.choices[0]; if (!delta) continue; if (delta.delta delta.delta.content) { const content delta.delta.content; page.setData({ currentReply.text: page.data.currentReply.text content }); page.scrollToBottom(); } if (delta.finish_reason stop) { page.setData({ isStreaming: false }); page.saveToHistory(); } } }page.setData里的键名用了点路径currentReply.text这是小程序 setData 的精准更新语法可以避免整个对象重新 render性能上比直接 setData 整个 currentReply 对象要好不少。每次增量内容到达时调用scrollToBottom保持视角在最新消息上这是聊天软件体验的关键细节忘了写用户会有种「怎么聊着聊着看不到回复了」的困惑。4.3 微信开发者工具里的域名与基础库检查在开发者工具中打开 miniprogram 目录后有一个最常见的翻车现场请求被「不在以下 request 合法域名列表中」拦截。解决方式是登录微信公众平台把 Node 中间层的 HTTPS 域名加到 request 合法域名列表中。注意是 request 类的合法域名不是 downloadFile 或者 uploadFile。开发阶段可以在开发者工具右上角详情 → 本地设置里勾选「不校验合法域名」但这只能用于本地调试真机预览和上线版本必须配置好域名。基础库版本同样容易翻车。IDE 右上角详情 → 本地设置里可以指定调试基础库版本建议切到较新的稳定版如 3.x 版本。如果项目里还没做基础库版本兼容最低要求是 2.14.0低于这个版本enableChunked不生效流式功能直接退化。5. 避坑指南流式聊天机器人最常见的几个翻车现场5.1 真机正常但开发者工具体验异常现象同一个项目开发者工具里请求完全正常真机预览却请求失败或者流式数据接收不到。原因真机和开发者工具的运行环境不同尤其是 iOS 上小程序运行在 JavaScriptCore 下对 base64 解码、TextDecoder 的支持程度与开发者工具里的 V8 不完全一致。常见的情况是TextDecoder在部分 iOS 系统版本下不可用导致 SSE 解析器直接抛异常。解决不要依赖TextDecoder做 ArrayBuffer 转字符串。改用 wx.arrayBufferToBase64 后自己实现 base64 解码或者干脆走字符串拼接路线在 wx.request 的 success 回调里通过String.fromCharCode.apply(null, new Uint8Array(res.data))手动转换。更稳妥的方案是后端把响应类型强制设为 text/plain让小程序直接返回字符串虽然会牺牲部分流式性能但换来了版本兼容性。5.2 流式数据攒一堆一次性渲染现象网络慢或响应时间长时页面十几秒没有任何动静然后突然一次性出现一大段文本完全看不到打字机效果。原因wx.request 在enableChunked: true状态下也不是每收到一帧网络包就回调一次小程序内部有自己的缓冲逻辑可能攒到一定量才回调。另外 Node 中间层的socket.write如果设置了 buffering 策略也可能攒住不吐。解决历史上我们搜到的各种相关方案都提到了一个关键做法在 Node 中间层把头部的X-Accel-Buffering: no加上同时在小程序端对响应帧做固定频次的强制 flush。具体实现是每收到一次数据如果距离上次渲染超过 100ms就立即 setData。这样即使小程序内部粘连了多帧数据UI 层面也能以 100ms 的间隔刷新看起来很流畅。if (Date.now() - lastFlushTime 100) { page.setData({ currentReply.text: page.data.currentReply.text accumulatedText }); accumulatedText ; lastFlushTime Date.now(); }5.3 context 越拼越长导致 token 超限报错现象对话进行到一半突然请求报 400 错误错误信息里有 token 相关的提示字样。原因buildContext 里把历史消息全量拼进 messages没有做截断长对话后单次请求的 token 数超过了模型的最大输入限制。虽然前面写了 slice(-20)但如果每轮对话产生的文本量很大20 条消息也可能把 8K 或 16K token 吃满。解决不只按条数截断还要按 token 数做预算控制。一个可用的近似方案是设置一个最大字符总数比如 12000 字符超过就把最老的消息丢出去直到总字符数回到预算之内。中文一个字符大约是 11.5 个 token英文一个词大约是 1.3 个 token没有任何 API 能精确算出字符数和 token 的关系所以用字符数做近似是常见做法。上线前最好直接打印JSON.stringify(messages).length到日志里做实际观测根据账号可用模型的具体限制再做调整。5.4 代理层 502 Bad Gateway现象小程序端提示「网络错误」Node 中间层日志出现Upstream request failed返回 502。原因大模型 API 有时会主动断开连接尤其是长时间没有 token 产生时比如流式过程中模型侧思考了很久上游可能会超时断开。或者 Node 中间层的全局 fetch 超时设置过短把请求直接掐了。解决在 Node 中间层给 fetch 设置合理的 AbortController 超时我一般推荐 60 秒起步长文本生成场景下 120 秒会更稳妥。如果用的是云函数部署还需要检查网关的超时配置因为很多云函数默认网关超时只有 30 秒即使代码里设置了 120 秒也会被网关先掐断。这个坑最典型代码改来改去没用最后发现是云函数平台的网关超时时间需要单独调。6. 二次开发思路从单会话到多助手、多模型跑通基础版本后最值得优化的不是炫酷的 UI而是对话架构上的几个短板。第一个方向是把历史消息管理从客户端挪到服务端。现在的方案是客户端每次把最近 20 条消息整个传过来一旦有刷新页面历史就丢了。改成服务端维护会话 ID 到消息列表的映射客户端只传 sessionId 和新增的文本服务端负责取出历史拼好再请求大模型体验会有质的提升。第二个方向是加多模型路由。在 Node 中间层抽一个 modelSelector 函数根据请求中的场景字段分发到不同模型。比如写代码的请求走代码模型闲聊走通用模型这样既能省费用也能提升专业场景的效果。这个地方建议把不同模型的参数baseURL、apiKey、modelName写在一个 models 配置数组里新增模型只改配置不动代码。// server/models.js const models [ { key: default, baseURL: process.env.LLM_BASE_URL, apiKey: process.env.LLM_API_KEY, model: process.env.LLM_MODEL }, { key: fast, baseURL: process.env.FAST_BASE_URL, apiKey: process.env.FAST_API_KEY, model: process.env.FAST_MODEL } ]; function selectModel(scene) { if (scene code) return models.find(m m.key fast); return models.find(m m.key default); }第三个方向是给流式连接做心跳与重连。大模型生成时间较长时小程序端的 wx.request 可能会因为底层 socket 长时间无数据被判死而断开。常见做法是 Node 中间层每隔 15 秒向小程序写一行 SSE 注释: keepalive这种注释行不会被解析成 data但能保持连接活性。配合前端监听 close 事件做断点续传用户体验提升很明显。验证改造是否成功的方式很简单用微信开发者工具在 Network 面板里看流式请求的时间线如果数据是持续小帧到达而不是大块一次性返回说明链路是通顺的。从我们过去踩坑的经验来看把大模型快速接进小程序的核心不是模型选型而是把 SSE 流式链路打通并处理好看似琐碎的边界问题。每个模型 API 在异常返回时格式都可能不一样如果 panic 到一条报错消息上出现了奇怪的格式建议先检查是不是上游 API 返回了error字段而不是choices。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑