☰
微信小程序AI聊天机器人模板:流式输出与多轮上下文改造指南
2026/10/31 1:08:41 网站建设 项目流程

简介:微信小程序 AI 聊天机器人对话模板面向具备一定前端基础、准备开发智能问答类小程序的开发者,基于 HBuilder 编写,可用于搭建对话页面、消息列表、机器人回复等常见交互结构,也适合作为课程设计或功能原型的界面底稿。压缩包共约 2000 个文件,整体约 7.04MB,内容以 JavaScript、TypeScript、JSON、Vue 以及 WXML/WXSS 等前端与小程序文件为主,并包含图片、样式和工程配置资源,目录更接近可浏览、可二次修改的完整工程模板。需要特别留意,模板不内置聊天接口,后端或第三方服务需自行接入和调试。当前已有 969 人学习下载,读者可据此研究页面组织、组件拆分与对话式界面实现,再补充接口调用、状态管理、消息渲染和业务逻辑,从而减少从零搭建基础界面的时间,对希望快速验证交互流程的开发者较为友好。

1. 需求来得急的时候,一份能跑的对话模板比什么都实在

上个月接了个活,甲方要在已经上线的小程序里加一个 AI 聊天入口,三天后就要看演示。从零写?光是会话列表、消息气泡、流式接收、输入框顶起这几件事,就够折腾一整天。这份 AI 人工智能机器人对话微信小程序模板解决的正是这个场景:它把「微信小程序模板」里最费时间的对话骨架全部搭好,包括消息流、多轮上下文、打字机效果和 Markdown 渲染,你只需要把自己的模型接口地址填进去。适合两类人:一类是拿到需求要快速出原型的接单开发,另一类是想在自己产品里塞个 AI 助手、但不想重写一遍聊天 UI 的独立开发者。它不是成品 SaaS,而是一副能直接改的骨架,这点先说清楚,后面所有拆解都围绕「怎么改」展开。

2. 目录结构与技术栈:这份模板的骨架长什么样

先把源码目录摊开看。这类对话模板通常是一个标准的原生小程序工程,没有引入 Taro、uni-app 这类跨端框架——原因是聊天场景对滚动性能、输入框行为、分块渲染的要求很细,多一层编译产物,排查问题时就多一层黑匣子。原生写法的代价是要自己处理 WXML 的列表更新,收益是每一处渲染你都能按住。

常见的目录形态大概是这样:

miniprogram/ ├── app.js # 全局初始化,读配置、建会话 ├── app.json # 页面注册、窗口样式 ├── app.wxss # 全局变量与主题色 ├── config/ │ └── index.js # 接口地址、模型名、系统提示词 ├── pages/ │ ├── chat/ # 主对话页 │ │ ├── chat.js │ │ ├── chat.wxml │ │ ├── chat.wxss │ │ └── chat.json │ └── history/ # 历史会话列表 ├── components/ │ ├── bubble/ # 消息气泡,按角色分左右 │ ├── markdown/ # Markdown 增量渲染 │ └── input-bar/ # 输入区,含语音按钮 ├── utils/ │ ├── request.js # 流式请求封装 │ ├── sse.js # SSE 分块解析 │ ├── context.js # 多轮上下文裁剪 │ └── storage.js # 会话本地持久化 └── project.config.json

这份结构里有两个地方值得单独说:utils/sse.js和utils/context.js。前者是把网络层吐出来的二进制块拼成一条条完整事件的胶水,后者决定你的机器人记不记得上一句。绝大多数模板改坏,都坏在这两个文件上。

2.1 页面注册与全局样式的约定

app.json里最关键的是窗口配置和页面路径。聊天页通常要关掉原生导航栏,自己做沉浸式的头部,同时把disableScroll打开,交给scroll-view自己管滚动:

{ "pages": ["pages/chat/chat", "pages/history/history"], "window": { "navigationStyle": "custom", "backgroundColor": "#f5f6f8", "backgroundTextStyle": "dark" }, "permission": { "scope.record": { "desc": "用于语音输入转文字" } }, "requiredPrivateInfos": ["getRecorderManager"] }

navigationStyle: custom意味着标题栏要自己画,app.wxss里就得预留状态栏高度。常见做法是用wx.getWindowInfo()拿statusBarHeight,在页面onLoad时写进data,WXML 里用内联样式撑开顶部占位。这一步漏了,刘海屏上第一条消息会被状态栏压住。

permission和requiredPrivateInfos是语音输入的前置条件。哪怕你第一版不做语音,建议也先留着,后面加功能不用再动配置、重新提审。

2.2 云开发还是自建后端:这个选择决定后面能走多远

模板一般会给两套后端方案,但真正上线只能选一条路,选错了后面改起来很疼。

方案流式输出部署成本适合场景
云函数callFunction不支持(要等函数返回)极低演示、内部工具
云托管 / 自建 HTTP 服务支持 SSE 分块中等正式对外产品

云函数调模型的问题在于它是「一次性返回」的语义,模型生成三秒,用户就盯着转圈三秒,然后整段文字砰地蹦出来。体验落差极大。我一般会直接走自建 HTTP 服务,用小程序的wx.request加enableChunked: true接收分块。如果甲方坚持不买服务器,退一步的做法是把流式关掉、在客户端做假打字机,把整段文字按 30 毫秒一个字吐出来——用户体验损失有限,但要知道这是障眼法,不是真流式。

提示:小程序要求所有请求域名在后台配置为合法域名,且必须是 HTTPS。本地开发时勾选「不校验合法域名」,但提审前必须配好,否则真机直接报错。

2.3 配置集中管理:一个文件装下所有可变项

把模型名、接口地址、系统提示词全部抽到config/index.js,是这个模板最实用的设计之一。改需求时不用满项目搜字符串:

// config/index.js const ENV = 'dev'; // dev | prod const CONFIG = { dev: { baseUrl: 'https://your-dev-host.com', model: 'chat-model-small', stream: true }, prod: { baseUrl: 'https://your-prod-host.com', model: 'chat-model-large', stream: true } }; module.exports = { ...CONFIG[ENV], systemPrompt: '你是一个简洁、专业的中文助手,回答控制在 200 字以内。', maxContextRounds: 8, // 保留最近 8 轮对话 maxHistorySessions: 30, // 本地最多存 30 个会话 typewriterInterval: 16 // 假流式时的出字间隔(ms) };

这里model和baseUrl分成两套环境,是踩过坑之后加的:早期直接在页面里写死了地址,上线那天忘了改,测试环境的请求打到生产,日志里一片 401。systemPrompt单独抽出来也很重要,它直接决定机器人的说话风格,运营改人设时只需要改这一行。maxContextRounds后面在上下文裁剪那一章还会展开讲。

改这个文件的时候有个细节:module.exports用了展开运算符把环境配置拍平,页面里require('../../config/index')拿到的就是合并后的对象,不用再判断环境。

3. 核心链路拆解:一条消息从输入框到气泡渲染

这是整个模板最值钱的部分。一条消息的完整生命周期分四步:输入区采集 → 请求层发送 → 流式解析 → 增量渲染。任何一步处理不好,用户感受到的就是「卡」「闪」「丢字」。

先看数据层的消息结构,后面所有逻辑都围绕它转:

// 单条消息 { id: 'msg_1700000000000_1', // 时间戳 + 自增,保证唯一 role: 'user' | 'assistant' | 'system', content: '文本内容', status: 'sending' | 'streaming' | 'done' | 'error', createdAt: 1700000000000 }

status字段是关键。它让气泡能有「发送中」「正在输入」「失败可重试」三种视觉状态,也让流式追加时有明确的判断依据——只有streaming状态的气泡才接受追加。

3.1 输入区:发送事件与防重复提交

输入区组件对外暴露一个send事件,页面收到后立刻做两件事:把用户消息推进列表,把输入框清空。

// pages/chat/chat.js 片段 onSend(e) { const text = (e.detail.text || '').trim(); if (!text) return; if (this.data.loading) { wx.showToast({ title: '正在回复中', icon: 'none' }); return; } const userMsg = { id: `msg_${Date.now()}_u`, role: 'user', content: text, status: 'done', createdAt: Date.now() }; // 先落 UI,再发请求,用户感知零延迟 this.setData({ messages: [...this.data.messages, userMsg], loading: true, scrollTo: 'bottom' }); this.createAssistantPlaceholder(); this.requestStream(text); }

loading这个锁必不可少。没有它,用户在等待回复时连点三次发送,会并发三个请求,返回顺序还可能是乱的,聊天气泡就串了。这是最常见的翻车点之一。

createAssistantPlaceholder的作用是先插入一个空白的 assistant 消息,status: 'streaming',这样流式返回时只需要按 id 找到它、追加content即可,不用每次setData都重建整个数组——后者在消息多了之后会明显掉帧。

3.2 请求层:用 enableChunked 接住分块数据

微信小程序从基础库 2.20.1 开始支持wx.request的分块接收,这是做真流式的前提。

// utils/request.js function streamChat({ messages, onDelta, onDone, onError }) { const task = wx.request({ url: `${CONFIG.baseUrl}/v1/chat`, method: 'POST', enableChunked: true, // 打开分块接收 timeout: 60000, header: { 'content-type': 'application/json', 'Authorization': `Bearer ${getToken()}` }, data: { model: CONFIG.model, messages, stream: true }, success: () => {}, fail: (err) => onError(err) }); let buffer = ''; task.onChunkReceived((res) => { // ArrayBuffer -> 字符串,注意跨块中文会被截断 buffer += decodeUTF8(res.data); // SSE 以空行分隔事件 const parts = buffer.split('\n\n'); buffer = parts.pop(); // 最后一段可能不完整,留到下次 parts.forEach((chunk) => { chunk.split('\n').forEach((line) => { if (!line.startsWith('data:')) return; const payload = line.slice(5).trim(); if (payload === '[DONE]') { onDone(); return; } try { const json = JSON.parse(payload); const delta = json.choices?.[0]?.delta?.content; if (delta) onDelta(delta); } catch (e) { // 半截 JSON 直接丢弃,等下一块补齐 } }); }); }); return task; // 返回 task 用于中断 }

这段代码有三个不能省的细节。

第一是buffer。分块边界不由你控制,一个中文汉字占三字节,完全可能被切成两半分别落在两个 chunk 里。所以必须把残留半截存起来,等下一块拼上再解析。少了这行,用户会看到莫名其妙的乱码或者丢字。

第二是parts.pop()。split('\n\n')之后最后一项大概率是不完整的事件,直接处理会解析失败。把它塞回 buffer 才是正确姿势。这是流式解析的血泪经验,很多模板在这里只写了forEach没写pop,表现为「偶尔少一句」。

第三是decodeUTF8。小程序环境里TextDecoder支持不完全,稳妥做法是手写解码或者用wx.arrayBufferToBase64转一道。我一般会用一个小工具函数处理,把 ArrayBuffer 按字节读出来拼成字符串。

onError兜底也很重要。流断在中间时,要把当前 assistant 消息的status改成error,气泡上显示一个重试按钮,而不是让它永远停在「正在输入」。

3.3 渲染层:增量内容怎么不闪、不卡

拿到 delta 之后,很多人会直接setData({ 'messages[i].content': newContent })。小数据量没问题,长回答下会明显卡,因为每次 setData 都要跨线程传输一遍完整的字符串。

更稳的做法是加节流:用一个临时变量累积 delta,每 50 毫秒 flush 一次。

// pages/chat/chat.js 片段 appendDelta(text) { this._pendingText = (this._pendingText || '') + text; if (this._flushTimer) return; this._flushTimer = setTimeout(() => { this._flushTimer = null; const pending = this._pendingText; this._pendingText = ''; const idx = this.data.messages.length - 1; const cur = this.data.messages[idx]; this.setData({ [`messages[${idx}].content`]: cur.content + pending }); this.scrollToBottom(); }, 50); }

50 毫秒是个经验值,对应 20 帧每秒,肉眼看是连续的,又不会把 setData 打爆。低于 30 毫秒基本没收益,高于 100 毫秒会有顿挫感。

Markdown 渲染放在气泡组件里做。模板通常用rich-text或者自研的轻量解析。要注意的是:流式过程中 Markdown 经常处于「半截状态」(比如**加粗还没闭合),解析器要能容错,不能因为一个未闭合标记整段不渲染。常见做法是流式期间只做纯文本 + 换行,status变成done之后再整体跑一次完整解析,视觉上几乎无感。

3.4 滚动与输入法:两个容易被忽略的交互细节

scroll-view配合scroll-into-view是标配,但要设scroll-with-animation,且每次内容更新后要延迟一帧再滚,否则会滚到上一次的位置。

输入法顶起是另一个坑。input组件的adjust-position默认开启,键盘弹起时页面整体上移,scroll-view的高度不会自动变,底部会露出一块空白。稳妥做法是监听bindfocus,拿到e.detail.height(键盘高度),动态调scroll-view的style高度,bindblur时还原。

4. 会话与状态管理:多轮上下文和历史记录怎么存

AI 聊天机器人和普通留言板的本质区别,就在于它得记得上文。这一章讲三件事:上下文怎么拼、超长怎么裁、历史怎么存。

4.1 上下文拼接:每一轮都带上历史

模型接口本身是无状态的,所谓「多轮对话」,是客户端每次把完整的消息数组发过去。模板里这个过程通常封在utils/context.js:

// utils/context.js function buildMessages(messages, systemPrompt, maxRounds) { var list = []; if (systemPrompt) { list.push({ role: 'system', content: systemPrompt }); } // 过滤掉发送中和失败的,只保留有效对话 var valid = messages.filter(function (m) { return m.status === 'done' && m.role !== 'system'; }); // 按「轮」取最近的 N 轮;一轮 = 一问一答,所以取 2N 条 var recent = valid.slice(-(maxRounds * 2)); // 保证第一条是 user,否则部分接口会报错 while (recent.length && recent[0].role !== 'user') { recent.shift(); } return list.concat(recent.map(function (m) { return { role: m.role, content: m.content }; })); }

filter那一步别省。流式过程中 assistant 消息的content是不断增长的,如果在streaming状态就把它拼进上下文,会发给模型一段未说完的话,模型容易顺着半句话往下编。

while循环处理首条必须是 user 的问题。部分模型服务对system → assistant → user这种序列会直接返回 400,而裁剪刚好可能从 assistant 开始截断。

4.2 上下文裁剪:不裁会超长,裁多了会失忆

maxRounds设成 8,意味着最多带 16 条消息。这是体验和成本的折中。全量带上,token 消耗翻倍、响应变慢;砍太狠,用户问「那刚才说的第二点呢」机器人就懵了。

更精细的做法是按 token 估算裁剪,而不是按条数。粗略规则是中文按 1 字 1 token、英文按 4 字符 1 token 算,累加到阈值就停。模板里maxContextRounds是为了让新手能直接调,熟手可以换成 token 版本。

注意:裁剪时永远保留 system 提示词。它通常只有几十字,却是人设的根,丢了机器人立刻变味。

4.3 本地持久化:10MB 的天花板和分页加载

小程序的wx.setStorageSync有容量上限,单 key 1MB、总量 10MB。聊天记录如果不加限制,几十个长会话就能撑爆,报错是setStorage:fail exceed storage max size,而且这个错很隐晦,用户只会觉得「历史记录突然没了」。

模板的做法是分层存储:会话元信息(id、标题、时间、条数)存在一个 key 里,每个会话的完整消息单独存一个 key。

// utils/storage.js var SESSION_INDEX = 'session_index'; // 会话列表 var SESSION_PREFIX = 'session_'; // 单会话消息 function saveSession(session) { // 只保留最近 N 条,防止单 key 超限 var msgs = session.messages.slice(-200); wx.setStorageSync(SESSION_PREFIX + session.id, { id: session.id, messages: msgs, updatedAt: Date.now() }); updateIndex(session); } function updateIndex(session) { var index = wx.getStorageSync(SESSION_INDEX) || []; index = index.filter(function (s) { return s.id !== session.id; }); index.unshift({ id: session.id, title: session.title || '新对话', updatedAt: Date.now() }); // 超出上限就淘汰最旧的 if (index.length > CONFIG.maxHistorySessions) { var removed = index.pop(); wx.removeStorageSync(SESSION_PREFIX + removed.id); } wx.setStorageSync(SESSION_INDEX, index); }

slice(-200)是硬性兜底。单个会话超过 200 条本身就说明该新开了,用户体验上也不会有损失。

标题的生成方式值得提一句:取第一条用户消息的前 12 个字做标题,是最省事也最够用的做法。要用模型生成摘要标题,就得在首轮之后多发一次请求,成本和延迟都不划算。

5. 避坑与排查:上线前一定会撞上的 5 个问题

这一章按「现象 → 原因 → 解决」写,都是真机上才会暴露、模拟器上一切正常的那类问题。

5.1 开发者工具里流式正常,真机一次性蹦出整段

现象:本地调试时文字一个字一个字往外冒,扫码到手机上变成转圈几秒、然后整段出现。

原因:真机的基础库版本低于 2.20.1,enableChunked被静默忽略,onChunkReceived根本不触发,请求退化成普通模式,等全部返回才进success。

解决:在app.json里显式声明"requiredBackgroundModes"之外,还要在project.config.json设最低基础库版本。同时在代码里做降级判断:

var canStream = wx.canIUse('request.object.enableChunked');

不支持时走假打字机分支,把完整结果显示出来再逐字吐。这个降级不写,低版本用户拿到的就是残废体验。

5.2 真机请求直接失败,报「url not in domain list」

现象:真机上第一条消息就红叉,控制台提示域名不合法。

原因:后台的 request 合法域名没配,或者配的是http://、带端口、带路径。小程序只认纯 HTTPS 域名,且不能带端口和路径。

解决:登录后台把域名加进「request 合法域名」,等生效(通常几分钟)。临时排查可以在开发者工具勾「不校验合法域名」,但要记住这只是调试开关,真机不生效。另外提醒一句:接口域名和图片 CDN 域名是分开配的,别只配了一个。

5.3 长回答渲染到一半开始卡顿、输入框延迟

现象:回答超过三四百字后,整个页面变卡,输入框回弹明显滞后。

原因:每次 delta 都触发一次setData,且传的是整个消息数组。跨线程传输的数据量随消息增长线性上升。

解决:三件事一起做——路径更新(只 set 单条消息的 content,不传整个数组)、50ms 节流、列表用wx:key绑定消息 id。另外,历史消息超过 50 条时用scroll-view的分页加载,不要一次渲染全部。

5.4 机器人像失忆,永远接不上上一句

现象:用户问「那第二点呢」,机器人回答「请问您指的是哪个第二点」。

原因:三种可能。一是发请求时只发了当前这一条,没带历史;二是buildMessages里的filter把status不为done的消息全滤掉了,而流式消息在上一轮结束时没被正确置为done;三是切换会话时没把对应消息数组加载进data。

解决:在发请求前console.log一次最终 payload,看 messages 数组长度对不对。我一般在requestStream入口打一行日志,只在开发环境输出,上线前用环境变量关掉。

5.5 键盘弹起后消息被遮住,滚不到底

现象:输入框获得焦点、键盘弹出,最新一条消息被压在键盘下面,手动往上滑才能看见。

原因:scroll-into-view只在内容变化时触发,键盘弹起改变的是视口高度,不触发滚动。而scroll-view的高度是死的。

解决:监听输入框的bindfocus,用e.detail.height动态调整scroll-view的style,同时延后 100ms 再执行一次滚到底(等键盘动画结束)。bindblur时还原高度并再滚一次。这个 100ms 不能省,键盘动画大约就是这个时长。

提示:这五个问题里,前两个是配置问题、后三个是代码问题。上线前建议按「真机 + 弱网 + 低版本基础库」三种条件各跑一遍,能省掉大部分线上反馈。

6. 二次开发:换模型、加语音、接知识库的三条改造路径

模板拿到手,第一件事往往是换掉里面的模型服务。这个改动比想象中小,因为请求都收在utils/request.js一处。你要做的是写一个适配函数,把不同厂商的返回结构归一化成{ delta, done }:

// utils/adapter.js var ADAPTERS = { openaiLike: function (json) { return { delta: json.choices && json.choices[0] && json.choices[0].delta ? json.choices[0].delta.content : '', done: false }; }, simpleLike: function (json) { return { delta: json.output && json.output.text ? json.output.text : '', done: json.output && json.output.finish_reason === 'stop' }; } }; function pickAdapter(name) { return ADAPTERS[name] || ADAPTERS.openaiLike; }

然后在config/index.js里加一个adapter: 'openaiLike',sse.js解析时调对应函数即可。这样换模型只改配置和新增一个函数,页面层一行都不用动。我一般会把「请求体构造」也一起抽出来,因为不同厂商的字段名差异比返回结构还大。

加语音输入是第二个高频需求,改动集中在输入区组件:用wx.getRecorderManager()录音,format用mp3或aac,录完把临时文件路径传给后端的转写接口,拿到文本后直接走原来的发送流程。要注意的是录音权限被拒后的引导——wx.authorize失败时得弹一个wx.showModal引导去设置页开权限,否则用户点了麦克风没反应,会以为坏了。

接知识库(RAG)是第三个方向,工作量最大但价值也最高。链路是:用户提问 → 后端向量检索出 top-k 片段 → 拼进 system prompt 或作为额外的上下文消息 → 再调模型。客户端几乎不用改,只在buildMessages里多塞一条role: 'system'的检索结果。真正的活在后端,模板本身不承担。

改造点主要改动文件大致工作量风险点
换模型服务utils/adapter.js、config/index.js半天返回结构差异、错误码不统一
加语音输入components/input-bar、app.json一天权限引导、录音格式兼容
接知识库后端为主、context.js微调三天起检索质量、上下文超长
换 UI 主题app.wxss、各组件 wxss半天深色模式适配

最后说个习惯。这套模板我前后改过四个版本,最早那次为了赶演示,把所有逻辑塞在一个 800 行的chat.js里,流式解析、上下文拼接、存储全糊在一起。两周后甲方要加语音,我花了整整一天才理清哪段代码在干什么。从那以后我每次接这类模板,第一步都是先把request.js、sse.js、context.js三个文件拆出来单独跑通,用 Node 脚本模拟一遍 SSE 分块喂进去,确认解析没问题再动 UI。这个前置动作看着慢,实际能省掉后面一大半的排查时间。希望帮到你。

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

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

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

立即咨询