ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微信小程序对接LLM:工程架构与实战避坑指南

微信小程序对接LLM:工程架构与实战避坑指南 简介面向微信小程序开发者与人工智能应用学习者这套可直接运行的聊天页面源码演示了小程序对接大语言模型并实现智能对话的完整路径。内置可配置的人设机制能将对话角色设定为儿童教育专家、商城客服或法律顾问并自由调整回复语气与规则拓展性较好。整包仅386KB共6个文件以页面结构文件、样式文件、逻辑文件、配置文件和图片为主要类型目录简洁、无冗余依赖方便逐文件理解小程序接入大语言模型的基础架构。目前已有977人学习或下载适合小程序初学者、人工智能产品原型开发者以及需要快速制作对话类演示工具的工程师。借助源码可掌握从界面渲染到消息发送与接收展示的核心写法也能直接复用聊天界面与对话管理逻辑作为课程设计、毕业设计演示或内部工具的前端基座进一步接入不同大模型服务即可上线体验。1. AI聊天小程序对接LLM不是接一个API是把整个工程架子立起来把标题里的“微信小程序聊天页面 AI大语言模型”拆开看它说的是两件事一是要有一个能像微信聊天窗口那样上下滑动、左对齐右对齐、显示“对方正在输入”的页面二是这个页面背后要真的连上大模型LLM能一句接一句聊而不是写死的问答机器人。很多人拿到这类源码后发现最难的从来不是聊天UI而是小程序不能直接请求LLM接口域名白名单、密钥保护、上下文拼接、流式输出这些工程问题才是真正的拦路虎。这篇文章想给你一套能直接落地的做法前端怎么搭、后端怎么转发、参数怎么调、上线前哪些配置不做就会翻车。适合正在做微信小程序项目、想把LLM能力集成进去的开发者也适合拿到源码但跑不通、想搞清楚每一层在干什么的人。2. 把「聊天页面 LLM」拆成三层前端会话、后端转发、模型接入2.1 为什么不能从小程序直接调用 LLM API跨域、密钥、合规这三点先想清楚刚接触AI聊天小程序的人最容易踩的第一个坑就是把站点的API Key直接写在小程序代码里然后用wx.request去打https://api.openai.com/v1/chat/completions。这个方案在小程序开发者工具里可能能通一上真机就废。原因有三层。第一层是浏览器跨域问题——小程序虽然不是浏览器但它的网络请求同样受域名白名单限制。微信小程序要求所有请求域名必须在小程序管理后台配置为 request 合法域名而且必须是 HTTPS不能带端口。LLM 服务商的域名不在你的白名单里真机上直接报url not in domain list。第二层是密钥泄露风险。API Key 一旦打进小程序包任何人解包都能看到。LLM API 是按 token 计费的密钥泄露等于把你的钱包交给别人。而且主流 LLM 服务商大多有地域限制直接请求还可能触发风控。第三层是业务封装。聊天不能只是把用户输入透传给模型还需要记录上下文、过滤敏感词、处理限流、统计 token 消耗。这些逻辑放在小程序端既臃肿又不可控放后端才是常见做法。所以一个可靠的架构是小程序只负责展示和收集输入所有 LLM 请求都发到自己的后端由后端去调大模型 API再把结果回传给小程序。这样域名白名单只需要配你自己的服务器域名密钥只留在后端环境变量里聊天记录和过滤规则也都在自己手里。2.2 小程序侧的最小聊天页面输入、消息列表、滚动到底部先做一个能用的聊天页面。用scroll-view做消息列表底部固定输入框。下面是一个最小可跑的 WXML 结构不依赖任何 UI 库。!-- chat.wxml -- view classchat-container scroll-view scroll-y classmsg-list scroll-into-view{{scrollIntoView}} view wx:for{{messages}} wx:keyid idmsg-{{item.id}} view classmsg-row {{item.role user ? row-right : row-left}} view classbubble {{item.role user ? bubble-user : bubble-ai}} text{{item.content}}/text /view /view /view /scroll-view view classinput-bar input value{{draft}} bindinputonInput confirm-typesend bindconfirmonSend cursor-spacing10 / button sizemini bindtaponSend发送/button /view /view对应 Page 逻辑// chat.js Page({ data: { messages: [], // { id, role: user | assistant, content } draft: , scrollIntoView: }, onInput(e) { this.setData({ draft: e.detail.value }) }, onSend() { const text this.data.draft.trim() if (!text) return const userMsg { id: Date.now(), role: user, content: text } const messages [...this.data.messages, userMsg] this.setData({ messages, draft: , scrollIntoView: msg- userMsg.id }) this.requestReply(messages) }, requestReply(messages) { // 调用后端接口稍后实现 } })这里有几个细节要说明。scroll-into-view绑的是消息元素的 id每次新消息插入后把它设置为目标列表就会自动滚到底部否则聊天界面会停在旧位置用户永远看不到新回复。cursor-spacing是 input 组件的一个属性表示光标与输入框底部的距离配合键盘弹起时调整页面位置用。confirm-typesend让软键盘右下角变成“发送”按钮符合聊天产品的操作习惯。消息 id 用Date.now()在本地足够因为这里只是给scroll-into-view做锚点不做服务端同步。真正要同步的话应该用后端返回的消息 ID后面讲多轮上下文时再说。2.3 后端转发层用 Node.js 接 OpenAI 兼容接口的最短实现小程序端不直接调 LLM所以我们需要一个后端转发层。现在大多数 LLM 服务商都提供 OpenAI 兼容接口只是 base_url 和模型名不同。我自己常用 Node.js Express 写这个代理因为它轻量、好部署微信云开发也能跑 Node 函数。下面是最小可用的转发接口// server.js const express require(express) const axios require(axios) const app express() app.use(express.json()) const LLM_BASE_URL process.env.LLM_BASE_URL || https://api.openai.com/v1 const LLM_API_KEY process.env.LLM_API_KEY const LLM_MODEL process.env.LLM_MODEL || gpt-3.5-turbo app.post(/api/chat, async (req, res) { const { messages } req.body if (!Array.isArray(messages) || messages.length 0) { return res.status(400).json({ error: messages is required }) } try { const llmResp await axios.post( ${LLM_BASE_URL}/chat/completions, { model: LLM_MODEL, messages, temperature: 0.7, max_tokens: 800 }, { headers: { Content-Type: application/json, Authorization: Bearer ${LLM_API_KEY} }, timeout: 30000 } ) const reply llmResp.data.choices[0].message.content res.json({ reply }) } catch (err) { console.error(LLM request failed:, err.message) res.status(502).json({ error: LLM service error }) } }) app.listen(3000, () console.log(chat proxy running on 3000))关键点都在注释里了再展开说几句。LLM_BASE_URL换成你实际服务商的地址比如某些国产模型是https://api.xxx.com/v1模型名也会不一样但请求结构和返回结构基本一致所以这套代码的可移植性很强。messages数组直接透传给模型接口里面应该包含 system 和 user/assistant 的轮次记录这个我们下一章细说。超时时间timeout: 30000是有讲究的。LLM 生成 token 是流式的非流式接口也要等模型把完整回答生成完才返回。30 秒是常见起步值如果你用更慢的大模型这个值还要放宽。小程序端的wx.request默认超时是 60 秒后端如果设 30 秒前端还能兜底。这个接口返回的是完整字符串reply小程序拿到后直接追加到消息列表即可。但注意这只适合非流式场景用户要等好几秒才能看到回复。想做到“打字机”效果需要走流式我们在第四章第五小节单独讲。3. 对接 LLM 的请求封装与参数设计从一次对话到多轮上下文3.1 请求封装promise、超时、错误码别把 fetch 裸写在页面里很多入门代码直接在页面里写wx.request每个 button 事件里复制一份。这在小 demo 里没问题一旦要加 loading、错误重试、token 刷新你就会发现每个请求都各改各的排查起来特别痛苦。我一般会把请求封装成一个request.js模块统一处理 baseURL、超时、拦截器。// request.js const BASE_URL https://your-domain.com function request(path, data, method POST) { return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${path}, method, data, header: { Content-Type: application/json }, timeout: 60000, success(res) { if (res.statusCode 200 res.statusCode 300) { resolve(res.data) } else { reject({ code: res.statusCode, message: res.data.error || request failed }) } }, fail(err) { reject({ code: -1, message: err.errMsg || network error }) } }) }) } module.exports { request }然后在聊天页面里这样用// chat.js 中引入 const { request } require(../../utils/request) async requestReply(messages) { wx.showLoading({ title: 思考中... }) try { const data await request(/api/chat, { messages }) const aiMsg { id: Date.now(), role: assistant, content: data.reply } this.setData({ messages: [...this.data.messages, aiMsg], scrollIntoView: msg- aiMsg.id }) } catch (err) { wx.showToast({ title: err.message || 请求失败, icon: none }) } finally { wx.hideLoading() } }这里把 Promise 化作为标准做法。wx.request本身是回调风格在 async/await 场景下很难读Promise 包装后可以用 try/catch 统一处理错误。timeout: 60000还要再解释一下小程序端超时时间要大于后端转发 LLM 的最长等待时间否则后端还没返回前端就已经 fail 了。另外注意一个细节如果后端返回的业务状态是 200但里面error字段有值这个封装不会 reject。在实际生产里我通常会在后端定义统一的响应结构比如{ code: 0, data: ... }然后前端判断res.data.code ! 0时 reject。这样把业务错误和 HTTP 状态码区分开排错时看日志更清晰。3.2 多轮上下文messages 数组怎么拼system 指令放哪LLM 的 chat 接口要求你传一个messages数组数组里每个元素是{ role, content }。role有三种system设定模型人格user是用户输入assistant是模型历史回复。多轮对话就是把历史所有轮次都塞进这个数组然后发给模型。这是最容易出问题的地方。错误做法是每次都只传当前这一句话。这样模型记不住之前聊了啥用户问“刚才说的那个方案呢”模型会一脸懵。正确做法是function buildMessages(history, currentInput, systemPrompt) { const messages [] if (systemPrompt) { messages.push({ role: system, content: systemPrompt }) } // history 是前端的消息列表只取 role 和 content for (const msg of history) { messages.push({ role: msg.role, content: msg.content }) } // 当前输入作为最后的 user 消息 messages.push({ role: user, content: currentInput }) return messages }注意history里应该只保留对话消息不要把前端用于展示的 id、时间戳混进去模型是来聊天的不是来看日志的。system prompt放最前面它会影响整段对话的语气和规则但会消耗 token。还有一个实际经验别把无限长的历史都塞进去。LLM 有上下文窗口限制比如 8K token 的模型history 太长会直接报错或截断。常见做法是只保留最近 10~20 轮消息或者按 token 数裁剪。简单实现可以这样// 只保留最近 20 条消息 if (messages.length 20) { const systemMsg messages[0] messages.splice(0, messages.length - 20) // 如果第一条是 system把它放回去 if (systemMsg.role system) { messages.unshift(systemMsg) } }这个裁剪方式有一个坑把中间的 user/assistant 消息丢掉后对话的“上下文”就断了但模型一般还能接得住只是会遗忘较早的内容。更精细的做法是按 token 数截断需要用到 tokenizer这里不再展开。3.3 大模型参数temperature、max_tokens、top_p 在聊天场景怎么调LLM API 的参数有很多但聊天场景里最核心的就三个temperature、max_tokens、top_p。很多源码里直接写死不解释为什么。这里给你一套能直接用经验值再说明怎么根据自己的产品调。参数取值范围默认值聊天场景建议作用temperature0~2有的模型 0~10.70.6~0.9越低越确定越高越发散max_tokens视模型而定不设则用模型默认500~1000限制单次回复最长长度top_p0~110.9~1核采样控制候选词范围temperature直接影响回复风格。做客服机器人希望回答稳定、少胡编建议调到 0.3 以下做闲聊、创意助手0.8 左右不会太死板。但别把 temperature 和 top_p 同时调得过于激进二者都影响随机性叠一起结果可能完全不受控。我自己一般固定 temperaturetop_p 保持 1不再额外折腾。max_tokens设得越小回复越短也越容易截断。如果你的聊天需要模型输出长文比如生成周报、写代码设到 1000~1500。如果只是日常问答500 够用超过会浪费 token还不利于控制成本。注意max_tokens包含输入和输出吗不同服务商定义不同。OpenAI 兼容接口里的max_tokens对多数模型是「输出 token 上限」但有些国产模型把max_tokens当作「总 token 上限」。所以对接新服务商前建议先看它的文档或者用一个已知长文本测试一下。还有个容易被忽略的参数是stream。非流式接口会等模型生成完才返回所以用户感觉迟钝。流式接口可以按字返回配合小程序的enableChunked或 WebSocket体验能上一个台阶。但流式实现复杂、排错难建议先在非流式跑通业务再升级。4. 上线前必做的 5 个配置域名白名单、缓存、导航栏、鉴权与密钥保护4.1 小程序后台域名白名单合法域名与 request 合法域名是两个东西微信小程序真机上所有网络请求都要走合法域名校验。这个「合法域名」不是说你买了域名就行要在 mp.weixin.qq.com 后台的「开发管理 - 开发设置 - 服务器域名」里配置。一共有四个分类request 合法域名、socket 合法域名、uploadFile 合法域名、downloadFile 合法域名。AI 聊天小程序至少会用到 request 和 socket如果用 WebSocket 做流式这两个要分别配置。开发时可以临时开启「不校验合法域名」但那个开关只对开发者工具和「真机调试」生效预览版和体验版以及正式版都不行千万别以为工具里通了就完事。配置时注意几点域名必须是 HTTPS且证书有效不能是 IP 地址。域名不能带端口https://your-domain.com:8080是不合法的。一级域名和二级域名是独立的比如配了api.example.comexample.com下的其他子域名仍然需要单独配置。每个分类最多配 20 个域名对多数项目足够但要留意别把测试域名也加进去浪费配额。这个配置容易忘而且一旦小程序已经发布改域名要走「修改服务器域名」流程通常要等审核。所以上线前先把生产域名配好开发期用测试账号的域名隔离。4.2 AI 聊天的缓存策略历史记录存本地还是存后端聊天记录是一种敏感数据但微信小程序最常见的要求是「下次打开还能看到历史」。实现方案有两种本地缓存和后端存储。对于大多数轻量聊天应用我建议先用本地缓存因为省服务器存储也不容易扯数据合规。本地缓存用wx.setStorageSync存消息数组即可。注意它的大小限制是 10MB官方文档写的是 10MB实际可用可能更少聊天文本如果不带图片一条消息平均 200 字10MB 大概能存几万条不用担心很快满。// 保存历史到本地 function saveHistory(messages) { // 只保留最近 200 条防止本地缓存膨胀 const trimmed messages.slice(-200) wx.setStorageSync(chat_history, trimmed) } // 读取 function loadHistory() { return wx.getStorageSync(chat_history) || [] }关键点是“只保留最近 200 条”。因为如果无限存缓存迟早会撑爆而且读取时也会卡。200 条对一般用户够用再多的话历史意义也不大。如果你要跨设备同步聊天记录那必须走后端存储用数据库表user_id | message_id | role | content | created_at来存。但这不是微信小程序的强项得另外建用户体系。前期别让自己陷入用户注册、登录、鉴权的泥潭先用本地缓存把体验做起来。关于缓存还有一个细节LLM 返回的回复要等整个流式或非流式请求完成后再缓存。如果请求失败不要缓存半截内容避免下次打开看到断掉的回复。我会在请求成功的回调里先更新页面再调saveHistory失败时不存。4.3 顶部导航栏高度与安全区聊天输入框不被键盘顶飞的调法AI 聊天页面最烦人的 UI 问题是键盘弹起时输入框被顶出屏幕或者顶部导航栏把 AI 消息条数挤没。微信小程序的顶部导航有默认和自定义两种模式。用默认导航时页面内容从导航栏下方开始不会有遮挡。但很多人为了自定义返回按钮会配navigationStyle: custom这时候页面顶部会顶到状态栏必须手动避让安全区。聊天页面通常需要自定义导航因为要显示「XX 助手」的名称和清除对话按钮。这时要在 onLoad 里取状态栏高度const { statusBarHeight, systemInfo } wx.getWindowInfo() this.setData({ navBarHeight: statusBarHeight 44 // 44 是导航栏约定高度单位 px })然后在页面的容器上设置padding-top: {{navBarHeight}}px。如果是胶囊按钮还要考虑胶囊位置但聊天页面一般不需要胶囊用默认胶囊就可以。输入框底部的关键是safe-area-inset-bottom。iPhone 有底部 home 条如果不避让输入框会被遮挡。常见做法是在 CSS 里.input-bar { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }同时给input设置cursor-spacing这个属性决定键盘弹起时输入框与键盘的距离建议设 10~20太小会被键盘盖住太大又显得空。这几个值新手往往不加等真机在 iPhone 上测就发现输入框一半被键盘遮挡这就是典型的“玄学”其实是对安全区理解不到位。4.4 防止 LLM 密钥泄露密钥只留在后端前端拿临时凭证「使用 llm 时如何防止密钥等鉴权信息泄露」几乎是每个团队上线前的必问问题。前面我们讲过后端转发但还有个更细的问题如果后端接口没做鉴权任何人都能拿着你的后端 API 地址狂刷消耗的是你的 LLM 预算。所以后端转发层只是第一步必须再给小程序一个访问凭证。常见做法是后端签发临时 token小程序每次请求时带上。微信小程序没有传统 cookie session通常用 token 放在 header 里。流程是小程序调用wx.login拿到 code发给后端。后端拿 code 换成 openid存起来并签发一个 token可以用 JWT 或随机字符串。小程序把 token 存到 Storage请求时加到 header 的Authorization。后端中间件校验 token 合法才转发 LLM。后端校验中间件简化版// auth.js const tokenStore new Set() // 生产环境应该用 Redis 或数据库 app.post(/api/chat, (req, res) { const auth req.headers.authorization if (!auth || !tokenStore.has(auth.replace(Bearer , ))) { return res.status(401).json({ error: unauthorized }) } // ...处理转发 })这个方案能拦掉一大半滥用请求。当然token 本身也可能被截获所以生产环境要上 HTTPS并且给 token 设置过期时间比如 24 小时。更专业的做法是小程序端用云开发的 callFunction 调用云函数由云函数转发 LLM天然免鉴权但如果你没有用微信云开发后端 token 就是最小可用方案。再强调一次LLM 的 API Key 只能出现在后端环境变量或密钥管理服务里不要打进小程序代码包、不要放在前端 JS 文件、不要提交到 Git 仓库。GitHub 的代码扫描工具都能抓到泄露的 API Key别存侥幸。4.5 流式输出用 WebSocket 还是轮询聊天体验的分水岭非流式的痛苦是用户发一句话转圈好几秒然后整段回复瞬间出现。产品上这叫「黑匣子」用户不知道模型是在思考还是卡死了。流式输出是 AI 聊天产品的体验分水岭。实现流式有两种主流方案方案一是服务端用 SSE 或 WebSocket小程序用wx.connectSocket接收。微信小程序没有原生 SSE 客户端但 WebSocket 是支持的。后端可以先把 LLM 的流式输出以 WebSocket 转发给前端。这样代码复杂度高但体验最接近 ChatGPT 的打字机效果。方案二是简单粗暴的轮询小程序先调/api/chat后端立即返回一个 task_id然后前端每隔 500ms 调/api/chat/status?task_idxxx查询增量内容。这个方案在 HTTP 无状态环境下容易实现但浪费请求且延迟高。如果时间紧我建议先做「伪流式」后端一次性拿到完整回复后前端用定时器逐字渲染。效果上接近流式但实际只是动画。真正的流式还会遇到中断重连、半字乱序等问题可以在下一阶段再优化。微信小程序 WebSocket 流式接收最小骨架// 前端建立 socket const socket wx.connectSocket({ url: wss://your-domain.com/ws/chat, header: { Authorization: Bearer token } }) socket.onMessage((res) { const data JSON.parse(res.data) // data.content 是增量片段 appendToLastMessage(data.content) })这个方案要求后端维护 socket 状态。如果后端用 Node.js 的ws库可以在 LLM 流式回调里不断send片段。这个话题展开能写一整章这里先点明流式不是必须的但它能让用户感知到模型在“思考”并显著降低等待焦虑。你可以在第一版先非流式上线再把流式当二期优化项。5. LLM 对接避坑指南从报错到体验问题的 5 个实战翻车记录5.1 现象request:fail url not in domain list真机预览就挂原因开发工具没开「不校验合法域名」或者后台没配域名。这是 90% 新手第一次真机预览必遇的报错。工具里开着安全校验的话预览时请求直接失败。解决先在微信小程序管理后台把生产域名加到 request 合法域名列表。如果是开发阶段可以在开发者工具右上角「详情 - 本地设置」勾选「不校验合法域名、TLS 版本以及 HTTPS 证书」这个开关只能用于开发调试不能用在真实体验版。配置完域名后记得在工具里清缓存并重新编译。另外注意如果你用了wx.connectSocket还要配置 socket 合法域名很多人只配了 request流式功能一上线就白屏。5.2 现象stream 模式下页面白屏收不到增量数据原因非流式请求时后端返回的 content-type 是application/json小程序没问题。改流式后后端用text/event-stream或 WebSocket 推送但前端还在用wx.request接收它压根没法处理分片数据。解决流式必须改用wx.connectSocket并且后端协议要对齐。如果后端用了 SSE 但是用的text/event-stream小程序端没有原生 EventSource需要自己在 socket 里解析data:行。很多人卡在这一步我建议最稳妥的方案是后端直接上 WebSocket前端按 JSON 消息格式收发避免解析 SSE 的事件流格式。另一个坑是WebSocket 收到半包或粘包后端发送前要定义一个分隔符或约定每个消息是完整 JSON 字符串前端按消息帧处理。微信小程序的 WebSocket 默认不是二进制所以 JSON 字符串按文本帧到达是完整的但如果你在后端用了压缩或分片可能会粘包。5.3 现象上下文越长模型回答越离谱还越来越慢原因多轮对话把历史全部塞进 messages模型上下文窗口被大量历史填充回答质量下降而且输入 token 越多生成耗时越长、费用越高。解决实现上下文裁剪策略。简单做法是只保留最近 10 轮再加一条 system 提示。进阶做法是先按 token 数估算超过 4000 token 就丢弃最早的消息直到低于阈值。可以用 LLM 服务商提供的 tokenizer 或第三方库来估算const countTokens (text) Math.ceil(text.length / 4) // 中文粗略估算这个估算不精确但够用。关键是在前端发请求前就裁剪让后端收到的 messages 不至于超限。后端也要有兜底如果 messages 总 token 超过模型上限直接返回 400提示用户开启新会话。千万别把超限的数组发给模型报错信息又难看还不友好。5.4 现象并发一高LLM 返回 429被限流原因多个用户同时发消息后端没做并发控制同一 API Key 的每分钟请求数超过服务商限制。解决后端加队列限流。最简单的办法是令牌桶或信号量限制同一时刻最多发 N 个 LLM 请求。我用 Node.js 实现过一个简单的并发池// concurrency limit let active 0 const MAX_CONCURRENT 5 async function callLLMWithLimit(payload) { while (active MAX_CONCURRENT) { await new Promise(res setTimeout(res, 100)) } active try { return await callLLM(payload) } finally { active-- } }这个实现虽然粗糙但能避免瞬时打爆接口。更好的方案是用p-limit之类的库或者直接把请求放入队列按顺序消费。处理 429 响应时要检查服务商返回的Retry-After头按它指定的秒数等待而不是固定重试 3 次。重试时还要避免前端一直在转圈可以告诉用户“当前咨询量大请稍候”。5.5 现象输入框被键盘遮挡光标看不见原因iOS 微信小程序在键盘弹起时固定定位的输入框不会自动上移android 部分机型也有类似问题。解决给页面根节点监听wx.onKeyboardHeightChangewx.onKeyboardHeightChange((res) { if (res.height 0) { this.setData({ keyboardHeight: res.height 10 }) } else { this.setData({ keyboardHeight: 0 }) } })然后给输入框容器动态绑定margin-bottom: {{keyboardHeight}}px。同时前面已经提过的cursor-spacing也要设置。注意不同输入法高度是实时变化的监听回调里会有频繁的 setData为了性能可以在回调里判断高度变化超过 10px 再更新。这个坑在真机调试时最容易暴露尤其是 iPhone X 之后带底部黑条的设备。6. 让聊天产品真正能用的三个进阶技巧温度采样、关键词过滤、降级方案6.1 用关键词过滤兜底正则先挡一轮别把希望全押在模型对齐上LLM 不是万能的即使你给了很强的 system prompt某些用户输入仍然可能触发敏感或违规内容。更现实的是有些提问本身不适合聊天助手的定位比如「帮我骂人」「教我做坏事」。在把内容发给 LLM 之前先用正则做一次本地过滤成本最低。function checkBlocked(text) { const blocked /(骂人词|违法词|敏感词)/i return blocked.test(text) }过滤不通过时直接在小程序端提示“这句话我不太会接换个话题吧”甚至都不用请求后端。这样既省 token也是安全兜底。我自己的经验是后端再做一次同步过滤防止前端被绕过。前端的过滤只是为了体验后端的过滤才是安全边界。6.2 无网络或模型超时时的降级本地话术 重试队列用户网络差、LLM 服务商故障或者后端雪崩聊天产品必须给用户一个反馈而不是让按钮一直 loading。我的做法是分级降级前端请求超时比如 15 秒没返回先提示“网络有点慢再试一次”。第二次重试仍失败展示一个预设回复“我这边暂时连不上大脑请稍后再聊”并提供一条重试按钮。如果后端持续不可用可以在后端准备一个「模拟回复」接口当 LLM 调用失败时返回固定话术保证用户在 demo 阶段不会觉得产品是坏的。重试队列可以用一个简单的数组存储发消息时的 messages 快照点击重试时重新发送。注意要避免连续重试把队列打爆限制同一会话最多重试 3 次。6.3 验证方案用脚本压一遍上下文拼接看 token 消耗是否失控上线前一定要做的验证不是「聊天能通」而是「多轮对话 20 轮后 token 开销多大」。很多人上线后发现费用暴涨就是因为没有预估。你可以写一个小脚本模拟用户连续发 50 条消息统计每次请求带了多少历史 token。// token_audit.js let history [] for (let i 0; i 50; i) { history.push({ role: user, content: 这是第 i 句测试 }) history.push({ role: assistant, content: 这是回复 i }) const payload buildMessages(history, 新问题, 你是客服助手) const approxTokens payload.reduce((sum, m) sum Math.ceil(m.content.length / 4), 0) console.log(第${i 1}轮历史消息${history.length}条估算token约${approxTokens}) }通过这个脚本你能直观看到如果第 50 轮时历史 token 已经 5000而你的模型上下文是 8K那实际回复空间只剩 3K容易截断。这时要根据业务量级决定裁剪窗口是 10 轮还是 20 轮。这几年做 AI 聊天功能我最大的教训是不管模型多聪明工程上的“降级、兜底、限流”一个都不能少。纯靠堆参数和换模型解决不了体验问题把上下文裁剪、密钥保护、超时重试这些基础做扎实才是 AI 聊天小程序能不能长期跑下去的关键。希望这些经验能帮你在接 LLM 时少走弯路。本文还有配套的精品资源点击获取
返回列表