资讯动态

微信小程序AI聊天源码:流式输出与LLM对接实战

发布时间:2026/10/18 23:26:22 来源:尧图企业网站定制
简介这份资源是一套微信小程序AI聊天页面的完整前端源码面向希望快速上手小程序对接大语言模型的开发者与学习者尤其适合想体验AI对话能力、验证人设设定效果的入门到中级人员。压缩包共6个文件约386KB包含wxml页面结构、wxss样式、js逻辑、json配置以及bot与user两张png头像图片覆盖了聊天界面从布局到交互的核心环节。借助这套代码读者可以快速搭建一个可运行的AI对话页面并根据需要为模型设定不同人设例如儿童教育领域专家、商城客服或法律顾问同时调整回复语气、提出个性化要求直观感受LLM在小程序端的落地方式。目前已有978人学习下载说明该示例在同类入门资源中具有一定参考热度。整体结构精简、上手门槛低适合作为小程序接入大语言模型的练手项目或二次开发起点。1. 从一条消息的旅程说起这个 AI 聊天小程序到底能干什么你打开微信进入一个小程序输入“帮我写一段 Python 快速排序”两秒后屏幕上逐字蹦出代码和解释。这背后不是魔法而是一条完整的链路小程序前端采集输入通过 WebSocket 或 HTTPS 把消息送到后端后端再转发给大语言模型 API拿到流式响应后原路推回前端逐字渲染。这套源码要解决的就是把这条链路从零搭出来——包含聊天页面 UI、消息收发逻辑、LLM 接口对接、会话上下文管理以及一个能跑起来的后端服务。它适合两类人一是想快速拥有一个可演示的 AI 聊天入口的前端开发者二是想研究小程序如何与语言模型交互的全栈学习者。你不需要从零设计消息协议也不用纠结流式渲染怎么写源码里已经把这些坑填过一遍了。2. 技术栈与目录结构拿到源码先看什么2.1 前端技术选型与页面组成微信小程序原生开发是这套源码的基底。没有用 uni-app 或 Taro 这类跨端框架原因很直接聊天场景对输入框聚焦、键盘弹起、scroll-view 滚动到底部的控制要求很细原生 API 的响应最可控。页面层面通常包含三个核心文件pages/chat/chat.wxml负责消息列表和输入栏的结构pages/chat/chat.js管理消息数组、滚动位置和发送逻辑pages/chat/chat.wxss处理气泡样式和流式光标动画。消息列表用scroll-view包裹每条消息是一个view通过wx:for渲染。输入栏固定在底部用fixed定位配合padding-bottom适配 iPhone 安全区。流式输出时最后一条 AI 消息会有一个闪烁的光标实现方式是在文本末尾追加一个带 CSS 动画的view。// pages/chat/chat.js 核心数据结构 Page({ data: { messages: [], // 消息列表每条含 role、content、id inputValue: , // 输入框当前内容 scrollTop: 0, // 控制 scroll-view 滚动位置 isStreaming: false // 是否正在接收流式响应 }, onInput(e) { this.setData({ inputValue: e.detail.value }) }, // 发送消息时先插入用户消息再插入一条空的 AI 消息占位 sendMessage() { const text this.data.inputValue.trim() if (!text || this.data.isStreaming) return const userMsg { role: user, content: text, id: Date.now() } const aiMsg { role: assistant, content: , id: Date.now() 1 } this.setData({ messages: [...this.data.messages, userMsg, aiMsg], inputValue: , isStreaming: true }) this.scrollToBottom() this.requestLLM(text) } })这段代码的关键在于isStreaming这个状态位。它同时承担两个职责防止用户在 AI 回复过程中重复发送以及让输入框在流式期间保持禁用态。messages数组里每条消息的id用时间戳生成是为了在流式更新时能精确定位到那条 AI 消息并只更新它的content而不是整个数组重建。常见做法是用setData的路径语法messages[${index}].content来局部更新性能比全量替换好很多。2.2 后端服务与 LLM 对接层后端通常是一个 Node.js 服务用 Express 或 Koa 起 HTTP 接口同时用ws库开一个 WebSocket 通道。为什么不用纯 HTTP 轮询因为流式输出需要服务端主动推送WebSocket 的全双工特性天然适合。如果不想维护 WebSocket也可以用 SSEServer-Sent Events但小程序对 SSE 的支持不如 WebSocket 直接。目录结构大致如下project-root/ ├── miniprogram/ # 小程序前端代码 │ ├── pages/chat/ # 聊天页面 │ ├── utils/request.js # 封装 WebSocket 和 HTTP 请求 │ └── app.js # 全局配置存后端地址 ├── server/ # 后端服务 │ ├── index.js # 入口启动 HTTP 和 WebSocket │ ├── llm.js # LLM API 调用封装 │ └── session.js # 会话上下文管理 └── package.jsonserver/llm.js是核心对接层。它接收用户消息和会话 ID从session.js取出历史上下文拼成 LLM 需要的消息格式然后发起流式请求。以常见的 OpenAI 兼容接口为例// server/llm.js 流式调用封装 const axios require(axios) async function streamChat(messages, onChunk) { const response await axios.post( https://api.example.com/v1/chat/completions, { model: gpt-3.5-turbo, // 模型名按实际服务商填写 messages: messages, // 包含历史上下文的消息数组 stream: true // 开启流式返回 }, { headers: { Authorization: Bearer ${process.env.LLM_API_KEY} }, responseType: stream } ) let buffer response.data.on(data, chunk { buffer chunk.toString() const lines buffer.split(\n) buffer lines.pop() // 最后一行可能不完整留到下次 for (const line of lines) { if (line.startsWith(data: ) line ! data: [DONE]) { const json JSON.parse(line.slice(6)) const delta json.choices[0]?.delta?.content || if (delta) onChunk(delta) // 每收到一个片段就回调 } } }) }这里有几个容易翻车的点。buffer的存在是因为 TCP 流不保证每次data事件都对应一个完整的 SSE 行必须自己缓存拼接。lines.pop()把可能不完整的最后一行留到下一次处理这是流式解析的标准操作。onChunk回调把增量文本传给 WebSocket 的发送函数最终推到小程序端。API Key 放在环境变量里不要硬编码在源码中这是基本的安全习惯。2.3 会话上下文管理session.js维护一个内存中的 Mapkey 是会话 IDvalue 是消息数组。每次用户发消息先把用户消息 push 进去调用 LLM 拿到完整回复后再把 AI 消息 push 进去。这样下一轮对话时历史上下文就自然带上了。// server/session.js 简易上下文管理 const sessions new Map() const MAX_TURNS 10 // 最多保留最近 10 轮对话 function getContext(sessionId) { return sessions.get(sessionId) || [] } function appendMessage(sessionId, role, content) { const history sessions.get(sessionId) || [] history.push({ role, content }) // 超出轮数限制时从头部丢弃最早的消息 if (history.length MAX_TURNS * 2) { history.splice(0, history.length - MAX_TURNS * 2) } sessions.set(sessionId, history) }MAX_TURNS控制上下文长度防止 token 消耗过快和请求体过大。生产环境建议把 Map 换成 Redis否则服务重启后所有会话丢失。会话 ID 由小程序端在首次进入聊天页时生成存在storage里后续每次请求都带上。3. 跑起来从零到发出第一条消息3.1 环境准备与依赖安装你需要三样东西微信开发者工具、Node.js 环境建议 16 以上、一个可用的 LLM API Key。源码根目录下执行npm install安装后端依赖主要就是express、ws、axios这几个。小程序端不需要 npm 安装微信开发者工具直接打开miniprogram目录即可。后端启动前在server目录下新建.env文件写入你的 API Key 和接口地址LLM_API_KEY你的密钥 LLM_BASE_URLhttps://api.example.com/v1 PORT3000然后在server/index.js里用dotenv加载这些变量。启动命令是node index.js看到控制台输出Server running on port 3000就说明后端就绪。3.2 小程序端配置与联调打开miniprogram/app.js把globalData.serverUrl改成你本机的 IP 加端口比如http://192.168.1.100:3000。注意不能用localhost因为开发者工具和手机真机的网络环境不同真机调试必须用局域网 IP。微信开发者工具里需要勾选“不校验合法域名”否则小程序会拦截非 HTTPS 请求。正式发布时后端必须上 HTTPS 并配置到小程序后台的 request 合法域名列表里。联调时先在模拟器里发一条“你好”观察三个地方小程序控制台的messages数组是否更新、后端控制台是否打印收到请求、Network 面板是否有 WebSocket 帧往来。如果消息发出去了但没回复大概率是 API Key 无效或接口地址填错后端会报 401 或 404。3.3 流式渲染的触发时机小程序端收到 WebSocket 消息后需要区分是增量片段还是完整消息。常见做法是在协议里加一个type字段type: chunk表示增量type: done表示结束。收到chunk时用setData更新最后一条 AI 消息的content收到done时把isStreaming置回false。// utils/request.js 中的 WebSocket 消息处理 socket.onMessage(res { const data JSON.parse(res.data) const messages this.data.messages const lastIndex messages.length - 1 if (data.type chunk) { // 只更新最后一条消息的内容避免全量 setData this.setData({ [messages[${lastIndex}].content]: messages[lastIndex].content data.content }) this.scrollToBottom() } else if (data.type done) { this.setData({ isStreaming: false }) } })路径语法messages[${lastIndex}].content是微信小程序setData的性能优化手段它只更新指定路径的数据不会触发整个列表的重新渲染。消息多的时候这个差别很明显全量更新会卡顿。4. 避坑与常见问题那些让你怀疑人生的报错4.1 消息发出去了但 AI 不回复现象是小程序端显示用户消息AI 占位消息一直空白isStreaming卡在true。原因通常是后端请求 LLM 时抛了异常但没有把错误推回前端。解决方式是在streamChat外层包try/catch捕获到错误时通过 WebSocket 发送一条type: error的消息前端收到后把占位消息内容改成“请求失败请重试”同时把isStreaming置回false。另外检查 API Key 是否过期、接口地址是否带了多余的斜杠。4.2 流式输出文字闪烁或重复现象是 AI 回复的文字一会儿重复一会儿跳变。原因是setData更新时用了全量messages替换而 WebSocket 消息到达顺序和setData的异步渲染产生了竞争。解决方式是坚持用路径语法局部更新并且在后端保证每个chunk只发送一次。如果后端用了多个进程或集群WebSocket 连接可能被分配到不同实例导致消息乱序这时需要引入 Redis 的 pub/sub 来统一分发。4.3 真机上键盘弹起遮挡输入框模拟器里输入框位置正常真机上键盘弹起后输入框被挡住。原因是小程序的adjust-position属性默认是true但fixed定位的输入栏不会自动上移。解决方式是把输入栏的bottom值绑定到一个动态数据监听bindfocus和bindblur事件聚焦时把bottom设为键盘高度失焦时归零。键盘高度可以通过wx.onKeyboardHeightChange获取。4.4 上下文太长导致请求超时或费用飙升现象是聊了十几轮后响应越来越慢API 账单涨得飞快。原因是session.js里没有做上下文截断历史消息无限增长。解决方式是设置MAX_TURNS并在超出时从头部丢弃或者用摘要方式把早期对话压缩成一句话再拼进上下文。另一个技巧是只保留最近几轮的用户消息和 AI 回复中间的系统提示词单独维护不参与截断。4.5 小程序审核被拒类目与内容合规现象是代码没问题但提交审核被驳回。原因是聊天类小程序需要选择正确的服务类目并且 AI 生成内容需要有人工审核或过滤机制。解决方式是在后端接入内容安全接口对用户输入和 AI 输出做敏感词过滤同时在提交审核时准备好类目说明材料。这个坑不解决代码写得再好也上不了线。5. 二次开发换模型、加功能、做自己的 AI 助手5.1 替换 LLM 服务商源码默认对接的是 OpenAI 兼容接口换成其他服务商时只需要改server/llm.js里的请求地址、模型名和认证方式。如果服务商的流式格式不是标准 SSE比如返回的是 JSON Lines那就把buffer解析部分的data:前缀判断去掉直接JSON.parse(line)。参数方面temperature控制随机性聊天场景建议 0.7 左右max_tokens限制单次回复长度防止 AI 话太多。5.2 增加多会话切换当前源码是单会话模式所有消息存在一个数组里。要改成多会话需要在session.js里把 Map 的 key 从固定值改成动态会话 ID小程序端增加一个会话列表页面用wx.setStorageSync存会话索引。切换会话时重新拉取对应历史消息渲染到聊天页。这个改造的核心是会话 ID 的生成和传递建议用uuid库生成避免时间戳冲突。5.3 加入语音输入与图片理解语音输入可以用微信小程序的wx.getRecorderManager录音然后把音频文件上传到后端调用语音转文字接口拿到文本再走聊天流程。图片理解则需要后端支持多模态模型把图片转成 base64 或上传到对象存储后把 URL 传给 LLM。这两个功能都会增加请求体积注意小程序的wx.uploadFile有大小限制图片建议压缩后再传。5.4 一个我踩过的坑有一次我把MAX_TURNS设成了 50觉得上下文越长 AI 越聪明。结果聊到第 20 轮时请求体超过了 100KB后端直接超时前端卡在isStreaming状态无法恢复。更麻烦的是 API 按 token 计费那一晚上烧掉了不少额度。从那以后我每次改上下文相关参数都强制走一遍“发 30 条消息看响应时间和费用”的验证流程。另外isStreaming一定要加超时兜底比如 30 秒没收到done就自动重置否则用户会以为小程序卡死。希望这些经验帮到你少走点弯路。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑