微信小程序接入大模型聊天:从密钥安全到流式渲染的完整实现

发布时间:2026/10/20 8:15:58
微信小程序接入大模型聊天:从密钥安全到流式渲染的完整实现 简介这是一份面向微信小程序开发者的AI对话前端示例资源演示如何在小程序页面中对接大语言模型LLM实现可自定义人设的AI聊天体验。页面已包含wxml结构、wxss样式、js交互逻辑与json配置并附有聊天界面常用头像图片整体设计简洁便于开发者快速理解消息列表、发送交互和回复渲染等核心流程。资源压缩为zip共6个文件包体仅386KB涵盖2张png图片和wxml、wxss、js、json各1个可直接导入微信开发者工具或在现有项目中对照改造。借助人设设定与语气控制可适配儿童教育专家、商城客服、法律顾问等场景只需修改提示词即可快速迁移到自己的LLM接入流程中。已有978人学习下载适合正在尝试小程序LLM方案的初中级开发者参考学习。1. 微信小程序对接 LLM 的聊天源码先想清楚这四个边界再动手一套能跑起来的 AI 聊天小程序源码核心难点从来不在页面样式而在模型密钥怎么藏、流式回复怎么渲染、上下文怎么裁剪、审核怎么通过。这套源码把微信小程序原生页面、云函数网关和大语言模型接口串成了一条可复现的完整链路拿到手可以直接跑通“用户发消息 → 模型返回 → 气泡展示”的最小闭环。适合三类人想快速验证 AI 对话产品形态的开发者、需要完整前后端联调流程的毕业设计学生、以及想在自己已有小程序里嵌入智能客服或问答能力的团队。在替换成自己的模型 key 之前先理解它的转发链路和几个隐性边界条件后面几章逐个展开。2. 对接层设计为什么小程序不能直连大模型 API2.1 直连的三个硬伤密钥暴露、域名校验、审核风险很多第一次接大模型接口的开发者拿到源码后的第一反应是把 API key 写进 config.js然后用 wx.request 直接调模型地址。这个方案在开发者工具里确实五分钟就能通但一旦准备发布正式版第一个硬伤立刻现形——小程序包会被下载到用户手机本地解包工具一翻app.js 和 config.js 里的代码都是明文可读的。密钥、请求地址、私有提示词全部暴露相当于把服务器的大门钥匙放在了门口脚垫下面。这不是理论风险而是真实发生过的事故有人把大模型 key 写死在代码里上线当天就被扫走第二天账单多出一大笔调用费用。第二个硬伤是微信的域名校验机制。wx.request 发起的 HTTPS 请求域名必须提前配置在公众平台的“开发管理-服务器域名”里且域名要备案、支持 HTTPS。大模型服务商的接口域名显然不在你的白名单里要么用你自己的域名做反向代理要么面对真机上一排红字报错。而云开发的 wx.cloud.callFunction 不依赖这个域名校验这也是很多项目最终选择云函数作为转发层的原因。第三个硬伤是审核。AI 生成内容的合规要求越来越严格页面必须显著标注“内容由 AI 生成”类目也要选对。直连模式下你很难在转发链路里对输出做二次处理比如给回复追加来源标识、对敏感词做过滤、记录调用日志。这些动作都需要一个中间层而不是让小程序裸调模型接口。综合这三点一套真正能落地的方案是小程序端只负责页面和交互模型调用全部收敛到服务端。2.2 云函数转发最小可用的网关实现最轻量的转发层我一般直接用微信云开发的云函数名字就叫 chat。输入是 messages 数组输出是模型回复。好处是免运维代码在开发者工具里写好直接上传不需要自己买服务器、配域名、搞备案。下面这段是云函数核心逻辑用的是 Node.js 18 环境自带的 fetch没有额外依赖。// functions/chat/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async (event) { const { messages [], model your-model-name, temperature 0.7 } event const apiKey process.env.LLM_API_KEY const baseUrl process.env.LLM_BASE_URL || https://api.your-provider.com/v1 const controller new AbortController() const timer setTimeout(() controller.abort(), 45000) try { const resp await fetch(${baseUrl}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model, messages, temperature, stream: false }), signal: controller.signal }) clearTimeout(timer) if (!resp.ok) { const errText await resp.text() throw new Error(LLM API ${resp.status}: ${errText}) } const data await resp.json() return { reply: data.choices[0].message.content, usage: data.usage } } catch (err) { console.error(chat failed:, err.message) throw new Error(model request failed) } }几个关键点model 放在 event 里而不是写死在函数里方便后续在小程序设置页切换模型而不用改云端代码。temperature 同样下放为请求参数不同业务场景可以单独调。AbortController 的 45 秒硬超时很有必要长文本推理经常超过云函数默认超时时间你需要在云函数控制台把超时时间调到 60 秒代码里再设一个更短的超时兜底防止请求悬挂占用实例。调用这个云函数时小程序端拿到的是一次性完整回复逻辑简单适合快速跑通。缺点也很明显用户在等待期间只能看到 loading没有逐字输出的“打字机”体感。想要更接近桌面版对话体验就得把链路升级成流式具体做法在第三章和第六章展开。2.3 自建网关的取舍什么时候该从云函数迁出去个人项目或小规模 Demo云函数完全够用。但并发一上来云函数的冷启动和单实例并发限制会开始拖后腿。我实际遇到的情况是晚上活跃期同时有二十多个用户发消息部分请求卡在冷启动阶段直接超时。这时候就需要把服务端迁到自建网关用 Express 或 Koa 起一个 WebSocket 服务小程序端通过 wx.connectSocket 连上来网关把模型接口的流式响应逐段推给客户端。迁移的收益有三个一是连接复用避免了每条消息都触发一次冷启动二是可以在网关里集中做请求鉴权、频率限制、敏感词过滤为过审和线上稳定打底三是流式转发能力天然支持打字机效果。代价是运维成本上升域名备案、HTTPS 证书续期、进程守护、日志监控每一件都得自己扛。对大多数刚开始尝试的开发者我的建议是先用云函数方案把产品形态和体验验证到位确认留存和对话质量达标后再迁。迁移时小程序端只需要把 wx.cloud.callFunction 换成 wx.connectSocket消息的数据结构基本不用动网关侧做一次协议转换就能平滑过渡。3. 聊天页面核心模块落地消息模型、流式渲染与上下文裁剪3.1 消息数据模型把气泡状态做成可维护的结构聊天页面最怕消息状态混乱发送中的消息和失败的回复混在一起用户分不清哪条是新的。我给每条消息定了五个字段页面渲染和逻辑判断都围绕它们展开。字段类型说明idstring用时间戳加随机数生成作为列表渲染的 keyrolestringuser 或 assistant决定气泡靠左还是靠右contentstring消息文本流式场景下会不断追加statusstringsending / done / error控制 loading 和重试按钮tokensnumber估算的 token 数用于上下文裁剪和用量统计页面结构上外层是一个 scroll-view内部用 wx:for 渲染 messages 数组。每条消息是独立的 viewuser 消息右对齐、深色背景assistant 消息左对齐、白色背景。status 为 sending 时assistant 气泡里显示三点跳动动画status 为 error 时气泡下方出现重试按钮点击后重新发送上一次失败的请求。// utils/message.js function createMessage(role, content, status done) { return { id: ${Date.now()}_${Math.floor(Math.random() * 10000)}, role, content, status, tokens: estimateTokens(content), createTime: Date.now() } } function appendMessage(page, message) { page.setData({ messages: page.data.messages.concat(message), scrollIntoView: msg_${message.id} }) }appendMessage 里同时更新了 messages 和 scrollIntoViewscroll-view 的 scroll-into-view 属性指向新消息的 id就能让最新一条自动滚进可视区。这里有个细节用户正在往上翻历史消息时自动滚动会很打扰需要先判断当前滚动位置是否接近底部只有接近底部才更新 scrollIntoView这个判断逻辑放到第六章。3.2 非流式与流式两种响应处理路线非流式场景小程序端只需要在 sendMessage 里调用云函数拿到完整回复后一次性 setData。代码很直接但有两个注意点一是云函数返回值别塞太多东西只回 reply 和 usage多余字段会增加传输体积二是错误信息要透传回来方便前端区分是密钥问题、余额问题还是上下文过长。流式场景下小程序的网络能力限制很明显。wx.request 不做流式解析EventSource 在小程序里也没有原生实现最靠谱的路径是 WebSocket。前端连接到网关的 wss 地址发送包含 messages 的 JSON然后监听 onSocketMessage每收到一段 delta 就追加到当前 assistant 消息的 content 上。// 流式接收核心逻辑 let isStreaming false function connectAndSend(messages) { wx.connectSocket({ url: wss://your-gateway/ws/chat?token${accessToken} }) wx.onSocketOpen(() { isStreaming true wx.sendSocketMessage({ data: JSON.stringify({ messages }) }) }) wx.onSocketMessage((res) { const chunk JSON.parse(res.data) if (chunk.type delta) { this.updateAssistantMessage(chunk.delta, false) } else if (chunk.type done) { this.updateAssistantMessage(, true) isStreaming false wx.closeSocket() } else if (chunk.type error) { this.markMessageError(chunk.message) isStreaming false wx.closeSocket() } }) wx.onSocketError(() { isStreaming false this.markMessageError(连接中断请重试) }) }网关返回的每条消息都带明确的 type 字段delta 表示增量内容done 表示本次生成结束并附带最终 usageerror 表示服务端出错。前端只按 type 做状态迁移不会出现“生成已经结束却又收到半截内容”的竞态。注意 onSocketMessage 里不要每次 setData 整个 messages 数组频繁全量更新在小程序里性能很差正确做法是只更新最后一条消息的 content 字段配合数组路径写法。3.3 上下文管理本地缓存与 token 预算聊天要延续上下文就不能丢。前端推荐把历史消息持久化到 Storage按会话 id 分 key 存储。onLoad 时读取最近一次会话每次回复完成后写回 Storage。但直接把全部历史塞给模型会很快撞上 token 上限。我的做法是维护一个 token 预算以常见 8k 上下文模型为例对话历史预留 6000 token其余留给系统提示词和当前回复。// utils/context.js const MAX_CONTEXT_TOKENS 6000 function estimateTokens(text) { // 中文按 1 字约 1.5 token英文按 4 字符约 1 token const chinese (text.match(/[\u4e00-\u9fa5]/g) || []).length const english text.length - chinese return Math.ceil(chinese * 1.5 english / 4) } function trimContext(history, systemPrompt, maxTokens MAX_CONTEXT_TOKENS) { const systemTokens estimateTokens(systemPrompt) const budget maxTokens - systemTokens - 512 const result [] let total systemTokens for (let i history.length - 1; i 0; i--) { const t estimateTokens(history[i].content) if (total t budget) break result.unshift(history[i]) total t } return [{ role: system, content: systemPrompt }].concat(result) }核心逻辑是从后往前收消息最近的对话优先保留久远的直接丢弃。systemPrompt 和本次回复各预留了空间避免模型因为输出空间不足而截断。字符级估算精度大概 80%对大多数对话场景足够。如果对 token 计数有更高精度要求可以在网关层用 tokenizer 库做精确计算但会引入额外依赖和计算开销前期没必要。4. 参数选型与请求策略把聊天体验调到不生硬4.1 模型与生成参数每个旋钮到底影响什么大模型 API 的生成参数不是摆设直接决定对话是“正常聊天”还是“复读机”。我把聊天场景最常调的参数整理成一张表数值是实际项目里验证过比较稳的起点。参数建议值作用与翻车点temperature0.7控制随机性。超过 1.2 容易答非所问低于 0.3 显得机械复读top_p0.9核采样与 temperature 配合。调太低会让长句输出被截断max_tokens1024单次回复上限。设太大拖慢响应并抬高费用太小长回答会戛然而止presence_penalty0.3提升话题多样性。翻译或摘要场景建议设 0frequency_penalty0.3降低重复用词。设太高会导致句式破碎streamfalse / true体验要求高就开 true 走 WebSocket简单实现可关掉一个常见错误是同时调高 temperature 又调低 top_p两个参数互相打架输出时而跳跃时而呆板。我的习惯是主要动 temperaturetop_p 固定 0.9 不动除非遇到特定风格异常才去调 frequency_penalty。工具型小程序里嵌入的问答temperature 可以降到 0.4让回答更稳定、少发散。模型选型上面向中文用户就优先选中文语料训练充分的模型断句和语气会更自然。成本敏感的场景可以按问题难度分流简单问答走小模型复杂推理走大模型分流逻辑放在网关层小程序端无感知。4.2 请求策略超时、重试与错误码分级模型接口是外部依赖它挂了你的聊天界面不能跟着挂。我会在网关层做三件事超时控制、幂等重试、错误码映射。云函数里已有 45 秒硬超时这里补一下重试策略。// 网关层重试逻辑Node.js async function requestWithRetry(options, retries 2) { for (let attempt 0; attempt retries; attempt) { try { const resp await fetchWithTimeout(options, 45000) return await parseResponse(resp) } catch (err) { if (attempt retries) throw err const status err.status || err.code // 只有 429、5xx、网络超时可重试 // 401、400、403 属于不可恢复错误直接抛出 if (status 401 || status 400 || status 403) throw err const delay 500 * Math.pow(2, attempt) await sleep(delay) } } }重试不能无脑做。400 表示请求参数有问题重试一万次也是浪费401 表示密钥失效要回控制台换 key只有 429 限流、5xx 服务端异常和网络超时才值得退避重试。指数退避让每次间隔翻倍给服务端恢复窗口。前端拿到分级错误信息后可以展示友好提示密钥失效就提示检查服务状态余额不足就提示联系管理员上下文过长就提示开启新会话。用户遇到问题不会觉得是玄学而是有明确的下一步动作。4.3 成本与并发聊天接口的钱花在哪聊天场景的成本大头不在单次请求的 token 单价而是无意识地把整个会话历史反复重发。每轮对话都带全部历史意味着第 20 轮的输入 token 是第 1 轮的 20 倍。一个日调用几百次的小型 Demo账单里可能有六成钱花在重复发送历史消息上。省成本两个办法一是网关层做系统提示词压缩把长篇 prompt 改成短视频指令二是上下文裁剪积极一点超过预算的消息直接丢弃。另外 max_tokens 控制输出长度不要为了“防止截断”盲目设成 4096大部分聊天回复 800 token 以内足够设太大等于为用不到的容量付费。并发方面云函数默认实例并发数不高峰值超过个位数并发就要在网关层加队列和限流。早期项目最简单有效的做法是前端加互斥锁同一时刻只允许一个请求在途既防用户连点刷请求也降低瞬时并发压力。等用户量起来后再做真正的限流和排队。5. 常见问题与避坑五条实操中反复踩的坑5.1 云函数有大段日志小程序端却永远等不到返回现象开发者工具里云函数日志显示执行成功返回体看着也正常但小程序端一直转圈最后报“云函数调用失败”。原因最常见的是返回体过大。非流式模式下模型回复超过一定长度后云函数返回的数据超出小程序端单次接收的处理范围。具体阈值与版本和网络环境有关但不值得去赌这个值。解决前端把 max_tokens 限制在 1024 以内从返回体中去掉 usage 等非必要字段。如果仍频繁超限直接升流式方案WebSocket 增量推送从机制上规避了这个问题。5.2 开发者工具里一切正常扫码真机就报域名错误现象开发者工具勾了“不校验合法域名”所有请求通畅。真机预览时接口请求直接报错错误信息点名域名不在白名单。原因微信对小程序网络请求有严格域名校验真机不受开发者工具勾选项影响。wx.request 和 wx.connectSocket 的域名必须提前在公众平台配置。解决走云开发调用云函数不需要配域名。走自建网关就把 wss 域名加到 socket 合法域名等配置生效再测。注意域名校验不支持 IP 直连必须是备案过的 HTTPS 域名。5.3 聊到第 20 轮接口突然返回 400 或上下文超限现象前十几轮都正常继续聊下去某个请求直接失败后端日志显示上下文长度超限。原因上下文没有裁剪所有历史消息都塞给了模型。轮数增加后输入 token 超过模型窗口服务端拒绝处理。解决用第三章的 trimContext 设置 token 预算从后往前保留最近对话。同时 UI 上加一个“清空上下文”按钮让用户可以手动开启新会话。5.4 明明开了 streamwx.request 却一次返回全部内容现象接口配置了流式输出小程序端用 wx.request 接收直到整个响应结束才拿到数据完全没有增量效果。原因wx.request 底层没有暴露流式读取接口responseType 也不支持文本流解析。服务端即使发了 chunked 数据前端也只能等响应结束。解决流式必须走 WebSocket或在小程序里接支持流式解析的封装库。更稳的是网关把模型响应转成 WebSocket 消息前端用 onSocketMessage 增量渲染。这个坑踩过一次之后我就再也没想过用 wx.request 硬接 stream。5.5 提交审核被拒理由是 AI 生成内容无标识现象功能正常提交审核后被驳回理由与 AI 生成内容合规标识有关。原因聊天输出由模型生成页面没有明确标注内容来源隐私政策和用户协议里也没有覆盖 AI 生成数据的条款。审核会重点检查这类功能。解决聊天页顶部或每条回复下方加一行“内容由 AI 生成仅供参考”用户协议里补 AI 生成内容的免责声明类目选“工具-问答”等匹配类目。这些前置工作开发阶段就做掉否则返工成本很高。6. 让流式输出更有质感打字机节流、自动滚动与停止按钮的细节流式回复有了但每收到一个 chunk 就 setData 一次页面会明显卡顿低端安卓机上尤其明显。我实际用的是缓冲加节流先把增量攒到本地 buffer每隔 50ms 做一次 setData把最新内容写入最后一条 assistant 消息。// 流式节流渲染 let buffer let flushTimer null function onChunk(delta) { buffer delta if (flushTimer) return flushTimer setTimeout(() { this.setData({ [messages[${lastIndex}].content]: buffer }) flushTimer null }, 50) }节流间隔不宜超过 100ms超过会觉得打字机一顿一顿低于 30ms 则 setData 太频繁低端机渲染跟不上。50ms 是折中值人眼根本感知不到这 50ms 延迟页面渲染压力能降一个量级。自动滚动也要处理细节。每次都强制滚到底部用户往上翻历史时会被粗暴拉回。正确做法是监听 scroll 事件记录当前 scrollTop 和 scrollHeight只有“距离底部小于 80px”时才允许自动滚动否则停住等用户自己滚。停止按钮是流式体验里不能省的一环。用户可能问错了问题或等得不耐烦要能一键中断生成。处理逻辑是点击停止时关闭 WebSocket把当前 buffer 内容拼接为完整回复状态置为 done。这里千万不要直接删掉半截消息用户至少能看到模型生成到哪里再决定是否重新提问。最后说一个习惯。自从那次非流式长回复触发响应超限把整个聊天页拖成白屏之后我每次上新版本都强制走一遍同样的验证流程先用云端日志确认请求和响应都正常再从开发者工具的 Network 面板确认 WebSocket 消息频率最后用真机在弱网环境下再跑一轮。这套流程看着笨但帮我省掉了不少“本地没问题上线就翻车”的现场事故希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询