微信小程序AI聊天对接大模型:架构、流式渲染与过审实战
2026/9/8 12:22:12 网站建设 项目流程

简介:一份可运行的微信小程序聊天页面源码,主要面向小程序开发者和人工智能应用学习者,用于快速掌握小程序对接大语言模型、实现智能对话的核心方法。资源围绕对话功能给出完整前端工程,共六个文件,包含页面结构文件、样式文件、交互逻辑文件、项目配置文件,以及对话界面所需的头像图片素材;整体压缩包约386KB,结构精简、依赖简单,可直接导入开发者工具预览和修改。通过阅读代码可以清晰理解用户提问、接口请求、结果渲染的完整链路,同时代码中支持自定义人工智能角色的设定语气和身份,例如按儿童教育领域专家、电商平台客服、法律顾问等不同场景灵活切换,方便后续扩展成真实业务模块。这套源码既适合用于课程设计、毕业设计,也可作为正式产品上线前验证小程序接入大语言模型可行性的轻量级示例。当前已有九百七十六人学习下载,能帮助开发者节省从零搭建环境的时间,尽快跑通一条可演示可迭代的聊天流程。 最近帮朋友收拾一个半成品项目,微信小程序聊天页面已经写好了,UI 挺精致,就差“对接 LLM 大模型”这临门一脚。结果我一上手才发现,所谓“就差一步”其实是最大的坑——小程序端直连大模型 API 基本走不通,就算走通了也过不了审核、扛不住真机调试。这篇就记录一下我把这个 AI 聊天小程序从“页面能看”折腾到“真机能聊”的完整过程,包含架构选型、前端流式渲染、服务端转发层、内容安全合规,以及一串花钱买来的踩坑教训。

无论你是准备自己从零写一个微信小程序 AI 聊天项目,还是接了类似的外包需求,或者只是想把 demo 做成能上线的产品,这篇文章都能帮你省下至少一周的摸索时间。

1. 架构选型的前提:小程序端不能直接调大模型 API

很多人拿到聊天页面后的第一反应是:直接在wx.request里填上大模型的apiKey和接口地址,用户发一句话就 POST 一次,返回 JSON 渲染到消息列表里。听起来没问题,但实际跑一遍就会撞上三堵墙。

1.1 API Key 暴露只是最轻的问题

第一堵墙是安全问题。微信小程序跑在用户手机上,所有代码和配置都能被反编译翻出来。你把apiKey写在前端,等于把钱包密码贴在门口。我见过一个项目上线没到三天,后台账单就多出几千块调用费——不是被攻击,就是有人直接拿着暴露的 key 到处刷。这个教训不是“可能发生”,而是“一定会发生”。

第二堵墙是请求超时。大模型接口的响应速度不像普通 API 那么稳定,尤其是流式输出场景,一个完整的回答可能要几十秒。小程序wx.request虽然有超时配置,但长连接场景下频繁触发超时重发,用户端看到的就是“答非所问”或者消息重复。

第三堵墙是域名校验。小程序正式环境要求所有请求域名必须 HTTPS 并且在后台配置合法域名,大部分大模型 API 的域名不是你想配就能配的。就算配了,也绕不开上面两个问题。

1.2 三条技术路线横向对比

既然不能直连,那就得加一个服务端转发层。这里有三条主流路线,我按实际工程中的靠谱程度排个序:

方案优点缺点适用场景
wx.request轮询实现最简单,服务端只写普通 HTTP 接口延迟高,无法实现打字机流式效果,请求浪费严重纯演示 Demo,不追求体验
wx.requestenableChunked流式返回小程序基础库 2.20.1+ 支持,能收到分段响应部分安卓机型兼容性不稳定,调试起来费劲轻量场景,不想引入 WebSocket
WebSocket 隧道真正的全双工,服务端收到大模型流式内容后实时推给小程序,体验最接近原生聊天需要维护连接状态,鉴权比普通请求复杂一点生产环境首选

我最终选的是 WebSocket 隧道方案。原因很简单:AI 聊天这个场景的核心体验就是“一个字一个字蹦出来”的过程感,轮询和分块请求都实现不了这种效果。WebSocket 虽然多了一些连接管理的成本,但整套链路一旦跑通,后面加功能反而方便。

1.3 整体模块划分

这个项目最终落地为三个部分:

  • 小程序端:负责聊天页面渲染、用户输入采集、WebSocket 连接管理、流式消息增量绘制。
  • 服务端桥接层:负责 WebSocket 连接鉴权、调用大模型 API、把流式响应转发给小程序、维护多轮对话上下文。
  • 大模型 API:底层语言模型服务,我这边对接的是 OpenAI 兼容格式的接口,市面上主流的开源模型部署方案也基本都能适配。

这样的好处是职责单一:小程序不直接碰大模型,服务端不参与业务 UI,后续不管是换模型供应商还是改前端交互,都不会牵一发动全身。

2. 聊天页面到流式渲染:setData 和 WebSocket 配合的几个关键细节

页面部分在原项目里已经有了基础骨架,但我接手后发现,它只是把wx.request的完整响应一次性塞进消息列表,整个体验跟“对话”完全没关系。流式渲染需要动的地方,远比想象中多。

2.1 消息列表的数据结构设计

聊天页的核心是一个消息数组,每条消息我建议至少包含下面几个字段:

{ id: 'msg_1720000001', role: 'user' | 'assistant', content: '', status: 'pending' | 'streaming' | 'done' | 'error', createdAt: 1720000001 }

status字段是流式渲染的关键。用户发送消息后,本地立即插入一条role: 'user'的消息,同时插入一条role: 'assistant'的空消息占位,状态设为streaming。后面每收到一段增量文本,就往这条assistant消息的content后面追加,直到收到完成信号。

用这种“先占位再填充”的模式,顶部气泡和 loading 动画都不需要额外控制,消息列表自己就把状态展示出来了。

2.2 setData 增量更新的节流处理

这是前端部分最大的坑。小程序里更新 UI 主要靠setData,但setData是把数据从逻辑层传到渲染层的,频繁调用会直接把性能拖垮。大模型流式输出快的时候,一秒能推十几个增量块,如果每个块都触发一次setData,低端安卓机上页面会直接卡成幻灯片。

我的处理方式是在前端加一个节流队列:

let pendingContent = '' let lastRenderTime = 0 function appendDelta(delta) { pendingContent += delta const now = Date.now() if (now - lastRenderTime >= 80) { flushContent() } } function flushContent() { const currentId = currentAssistantMsgId that.setData({ [`messages[${currentId}].content`]: pendingContent }) lastRenderTime = Date.now() }

加上一个定时兜底(比如 300ms 内无论有没有攒够增量都强制刷新一次),保证最后一段内容不会卡在队列里。实测下来,60ms 到 100ms 的节流间隔在视觉上基本看不出来,但渲染性能提升非常明显。

2.3 输入框、发送按钮与“思考中”状态

聊天体验很大程度取决于发送状态的管理。用户点发送之后,按钮要立即置灰,对话框显示“思考中”,防止重复提交。同时输入框要保持在可视区底部,调用wx.pageScrollTo让最新消息滚动到视野内。

这里还有一个体验细节:LLM 的响应可能很长,用户在等待过程中经常会切走再切回来,WebSocket 连接可能已经断开。所以前端一定要监听onSocketClose,如果消息状态还是streaming,要么自动重连并标记当前消息为“连接断开”,要么提供一个“重新生成”按钮。不能闷声不响地让用户等一个永远不来的回复。

3. 服务端转发层:LLM 流式转发、连接鉴权与上下文裁剪

服务端是整个项目的技术核心。小程序端只是负责展示,真正的“对接 LLM 大模型”逻辑全在这一层。

3.1 WebSocket 连接的鉴权姿势

WebSocket 在微信小程序里有个比较隐蔽的坑:wx.connectSocketheader参数在部分平台真机上会被忽略,导致你没法像普通 HTTP 请求那样通过 Header 传 token。搜一下社区会发现不少人被这个问题折腾过。

解决方法是把 token 放在 URL query 参数里:

const token = wx.getStorageSync('token') const socketTask = wx.connectSocket({ url: `wss://your-server.com/ws/chat?token=${token}` })

服务端从req.url里解析 token,校验通过后再建立正式连接。有个安全细节:WebSocket 的 URL 会出现在日志和网关记录里,所以 token 有效期一定要设短,比如 2 小时,同时服务端要做频率限制,不然就等着被人拿凑出来的 token 刷流量。

3.2 SSE 转发到 WebSocket:服务端核心处理逻辑

大模型 API 的流式输出通常走 SSE(Server-Sent Events)协议,就是一串以data:开头的事件流。服务端的任务很明确:收到 SSE 流,解析出增量文本,再通过 WebSocket 推给小程序。

我的服务端用的是 Node.js,核心逻辑大致是这样:

const WebSocket = require('ws') const { createParser } = require('eventsource-parser') wss.on('connection', (ws, req) => { ws.on('message', async (raw) => { const { sessionId, content } = JSON.parse(raw) const history = await loadHistory(sessionId) const response = await fetch(LLM_API_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.LLM_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-3.5-turbo', stream: true, messages: [...history, { role: 'user', content }] }) }) const parser = createParser((event) => { if (event.type === 'event' && event.data) { try { const json = JSON.parse(event.data) const delta = json.choices?.[0]?.delta?.content if (delta) { ws.send(JSON.stringify({ type: 'delta', content: delta })) } } catch (e) { // 忽略心跳包和空数据 } } }) for await (const chunk of response.body) { parser.feed(new TextDecoder().decode(chunk)) } ws.send(JSON.stringify({ type: 'done' })) }) })

注意几个细节:

  • stream: true必须显式传,否则大模型 API 会一次性返回完整 JSON,你这边就拿不到增量了。
  • 忘记处理响应体里的[DONE]结束标记,会导致连接一直挂着。
  • 服务端要做超时兜底,如果大模型 30 秒内没返回任何内容,主动给小程序发一个error事件,不能无限等下去。

3.3 上下文窗口裁剪:一个容易被忽略的成本黑洞

LLM 本身不记对话,每次都是把你传进去的消息列表当上下文理解。如果你把用户所有聊天记录全量传给大模型,两个星期后每条请求的 token 数量就会爆炸。这不仅拖慢响应速度,还直接烧钱。

我的做法是做一个简单的滑动窗口:

function buildContext(history, maxTokens = 3000) { let total = 0 const result = [] for (let i = history.length - 1; i >= 0; i--) { const item = history[i] const estimate = Math.ceil(item.content.length * 1.3) // 中文粗略估算 if (total + estimate > maxTokens) break result.unshift(item) total += estimate } return result }

从最新消息往前倒推,累计 token 接近上限就截断。这种方法写起来简单,也不依赖额外的 tokenizer 库,在模型对“开头部分”的记忆敏感度上会有轻微损失,但对于绝大多数聊天场景已经足够。如果你追求更精确的裁剪,可以使用tiktoken之类的分词库,但不要为了这点精度过度工程化。

3.4 多轮对话的历史存储方案

历史消息要存下来,服务端重启不能丢。单机部署我用 SQLite,字段就四个:idsessionIdrolecontentcreatedAt。高并发场景可以换 Redis 列表,但我个人建议至少落一份持久化存储,纯内存方案在服务重启后会让所有用户的上下文全部丢失,体验断崖式下跌。

4. 内容安全与过审:别做“无审核聊天”,合规才能活得久

这次整理项目资料时我看到热词里有一类搜索量很高,比如“无审核 AI 聊天”“无禁词聊天”。我特别想单独说一句:这类需求在小程序生态里基本走不通,而且也不该走通。小程序审核对 AI 生成类内容盯得非常紧,与其研究怎么绕过去,不如研究怎么让它既安全又有好的体验。

4.1 AI 聊天小程序常见的被拒原因

从我跟审核打交道的经验看,被拒通常集中在这三类:

  • AI 生成的内容包含违规信息,审核要求必须接入内容安全检测能力。
  • 没有用户协议、隐私政策,也没有举报反馈入口。
  • 类目选择不对。AI 对话通常需要选择“工具 > 信息查询”或者对应的服务类目,选错类目会被直接驳回。

另外注意一个问题:如果你的账号有过违规记录,比如某些跟 AI 无关的功能被罚,支付和被搜索等能力都会受影响。有些项目骂审核不讲理,其实根源是其他模块的合规欠账。合规不是审核给你的额外要求,是项目正常运转的前提。

4.2 内容检测要过两道:入站过滤和出站过滤

我的方案是在服务端加两层内容安全检查。

第一层:用户输入先过一次检测,命中敏感内容就直接返回友好提示,不再调用大模型。微信官方提供msgSecCheck接口,服务端拿access_token调用即可:

const res = await fetch( `https://api.weixin.qq.com/wxa/msg_sec_check?access_token=${token}`, { method: 'POST', body: JSON.stringify({ version: 2, openid, scene: 2, content: userInput }) } ) const data = await res.json() if (data.result.suggest === 'risky') { // 拦截,不给大模型 }

第二层:模型输出也要检测。大模型本身有安全对齐,但市面上很多开源模型或者第三方接口的过滤力度参差不齐,完全依赖模型自觉不现实。输出侧检测到问题后,用一条“我好像没理解你的意思,换个话题聊聊可以吗”之类的兜底回复代替原文。

这个“双向检测”的架构看起来多花了两次接口调用,但它同时是过审的筹码。审核时如果你能说明“输入走内容安全,输出走内容安全”,通过率会高非常多。

4.3 把“无禁词”做成“体验顺畅”,而不是“毫无约束”

“无禁词”这个诉求换一个角度理解,其实是用户觉得太多 AI 产品“动不动就答不了”,体验太僵硬。这个问题有更好的解法:

  • 对直接命中的硬性违规内容明确拦截,这是底线。
  • 对疑似擦边的内容不直接拒绝,而是用“这个问题我不太确定,要不要聊聊别的”来带过去。
  • 对正常讨论但包含部分风险词汇的内容,通过大模型的 system prompt 进行引导,让它从正面角度回应,而不是一刀切地拒绝。

我系统提示词里固定有一段话:遇到有争议的话题,要从建设性角度回应,强调普遍认可的价值观,不传播未经核实的信息。这样既满足了合规要求,用户在多数正常提问下也不会有“这也不能说那也不能说”的窒息感。

4.4 用户协议、举报入口这些配套必须齐

小程序后台的“用户隐私保护指引”要如实填写收集了哪些信息。用户协议里要写清楚 AI 生成内容仅供参考。聊天页面右上角加一个“反馈”入口,用户可以对某条回复进行举报。这些东西看着琐碎,实际是审核人员判断这个项目是否“认真做产品”的直接依据。

5. 真机与上线排查:域名、超时、基础库差异的踩坑记录

这一章是上线排错的经验合集。说句实话,代码写完运行起来只是第一步,能扛过真机调试才是真正能发布的版本。

5.1 开发者工具正常,真机却白屏的排查链路

我接手时遇到最诡异的一个问题是:开发者工具里一切正常,真机预览直接白屏,控制台报错信息还不完整。排查链路大概是这样的:

  1. 先看报错。真机调试把vConsole打开,发现wx.connectSocket一直报url not in domain list
  2. 检查小程序后台的“开发管理 > 开发设置 > 服务器域名”,socket 合法域名没有配置。开发者工具默认勾选了“不校验合法域名”,所以本地怎么跑怎么通,真机直接拦截。
  3. 配置完成以后,又发现安卓可以连,iOS 报证书错误。检查证书链才发现用的是过期二级证书,iOS 的 ATS 策略更严格,直接拒绝连接。

这套链路跟网上很多“真机白屏”的排查方向是一样的:域名配置、证书、基础库版本,按顺序查一遍。如果你用的是抓包工具检查请求是否发出,会发现这个路径更直观——白屏很多时候不是页面问题,是连接压根没建立起来。

提示:socket 合法域名request 合法域名是分开配置的,别配了 request 忘了 socket,这是 WebSocket 方案最容易翻车的地方。

5.2 LLM 响应慢导致前端超时

大模型接口不是每次都快,高峰期响应十几二十秒很常见。小程序端的 WebSocket 虽然没有wx.request那种固定超时限制,但网关层、服务端代理层都可能把长时间无响应的连接掐断。

我的处理方式有两个:

  • 客户端和服务端之间做一个 15 秒一次的应用层心跳。不是 WebSocket 协议层的 ping/pong,而是在消息里定义一个{ type: 'ping' }的业务消息,确保链路里所有中间设备都知道这个连接是活的。
  • 服务端调用大模型时设置 60 秒超时上限。超过 60 秒直接断开当前会话,返回“请求超时请重试”。这样可以避免大模型故障时连接无限被占用。

5.3 HBuilderX 与“不是开发者”的问题

如果你用的是 HBuilderX 跑 uni-app 项目,热词里那个“运行微信小程序提示不是开发者”我也遇到过。通常是两个原因:

  • 微信公众平台里没把你微信号加进“项目成员”,或者你在开发者工具里用的 AppID 不是这个项目的。
  • HBuilderX 里配置的 AppID 跟微信开发者工具里打开的 AppID 不一致,导致权限校验不过。

处理办法:后台添加开发者微信号,并在 HBuilderX manifest.json 里确认小程序 AppID 正确。这个不属于技术难题,但第一次遇到会卡很久。

5.4 上线前的完整检查清单

我自己的项目上线前会过一次这个清单,你可以直接抄作业:

检查项说明
HTTPS 证书证书链完整,非过期,iOS 安卓都测一遍
request 合法域名后台已配置,版本已发布
socket 合法域名后台已配置,wss:// 前缀
用户隐私保护指引已填写,与代码实际收集项一致
内容安全接口入站/出站检测代码已启用
用户协议与举报入口页面可见,链接有效
基础库版本设置为较低的兼容版本,避免部分用户无法使用
服务端超时兜底30 秒无响应手动断连并提示

最后再说一个个人习惯:每次上线前,我都会在真机上完整跑一遍“连续发五条消息、间隔拉长、切后台再切回来”的测试。这个操作能同时暴露连接稳定性、上下文丢失、渲染节流三个问题的潜在隐患。聊天类小程序最怕的就是用户聊到一半体验崩掉,这种测试比写一百个单元测试都管用。

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

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

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

立即咨询