接實(shí)戰(zhàn))
簡(jiǎn)介這份資源是一套微信小程序AI聊天頁面的完整前端源碼面向希望快速上手小程序?qū)哟笳Z言模型的開發(fā)者與學(xué)習(xí)者尤其適合想體驗(yàn)AI對(duì)話能力、驗(yàn)證人設(shè)設(shè)定效果的入門到中級(jí)人員。壓縮包共6個(gè)文件約386KB包含wxml頁面結(jié)構(gòu)、wxss樣式、js邏輯、json配置以及bot與user兩張png頭像圖片覆蓋了聊天界面從布局到交互的核心環(huán)節(jié)。借助這套代碼讀者可以快速搭建一個(gè)可運(yùn)行的AI對(duì)話頁面并根據(jù)需要為模型設(shè)定不同人設(shè)例如兒童教育領(lǐng)域?qū)<摇⑸坛强头蚍深檰柾瑫r(shí)調(diào)整回復(fù)語氣、提出個(gè)性化要求直觀感受LLM在小程序端的落地方式。目前已有978人學(xué)習(xí)下載說明該示例在同類入門資源中具有一定參考熱度。整體結(jié)構(gòu)精簡(jiǎn)、上手門檻低適合作為小程序接入大語言模型的練手項(xiàng)目或二次開發(fā)起點(diǎn)。1. 從一條消息的旅程說起這個(gè) AI 聊天小程序到底能干什么你打開微信進(jìn)入一個(gè)小程序輸入“幫我寫一段 Python 快速排序”兩秒后屏幕上逐字蹦出代碼和解釋。這背后不是魔法而是一條完整的鏈路小程序前端采集輸入通過 WebSocket 或 HTTPS 把消息送到后端后端再轉(zhuǎn)發(fā)給大語言模型 API拿到流式響應(yīng)后原路推回前端逐字渲染。這套源碼要解決的就是把這條鏈路從零搭出來——包含聊天頁面 UI、消息收發(fā)邏輯、LLM 接口對(duì)接、會(huì)話上下文管理以及一個(gè)能跑起來的后端服務(wù)。它適合兩類人一是想快速擁有一個(gè)可演示的 AI 聊天入口的前端開發(fā)者二是想研究小程序如何與語言模型交互的全棧學(xué)習(xí)者。你不需要從零設(shè)計(jì)消息協(xié)議也不用糾結(jié)流式渲染怎么寫源碼里已經(jīng)把這些坑填過一遍了。2. 技術(shù)棧與目錄結(jié)構(gòu)拿到源碼先看什么2.1 前端技術(shù)選型與頁面組成微信小程序原生開發(fā)是這套源碼的基底。沒有用 uni-app 或 Taro 這類跨端框架原因很直接聊天場(chǎng)景對(duì)輸入框聚焦、鍵盤彈起、scroll-view 滾動(dòng)到底部的控制要求很細(xì)原生 API 的響應(yīng)最可控。頁面層面通常包含三個(gè)核心文件pages/chat/chat.wxml負(fù)責(zé)消息列表和輸入欄的結(jié)構(gòu)pages/chat/chat.js管理消息數(shù)組、滾動(dòng)位置和發(fā)送邏輯pages/chat/chat.wxss處理氣泡樣式和流式光標(biāo)動(dòng)畫。消息列表用scroll-view包裹每條消息是一個(gè)view通過wx:for渲染。輸入欄固定在底部用fixed定位配合padding-bottom適配 iPhone 安全區(qū)。流式輸出時(shí)最后一條 AI 消息會(huì)有一個(gè)閃爍的光標(biāo)實(shí)現(xiàn)方式是在文本末尾追加一個(gè)帶 CSS 動(dòng)畫的view。// pages/chat/chat.js 核心數(shù)據(jù)結(jié)構(gòu) Page({ data: { messages: [], // 消息列表每條含 role、content、id inputValue: , // 輸入框當(dāng)前內(nèi)容 scrollTop: 0, // 控制 scroll-view 滾動(dòng)位置 isStreaming: false // 是否正在接收流式響應(yīng) }, onInput(e) { this.setData({ inputValue: e.detail.value }) }, // 發(fā)送消息時(shí)先插入用戶消息再插入一條空的 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) } })這段代碼的關(guān)鍵在于isStreaming這個(gè)狀態(tài)位。它同時(shí)承擔(dān)兩個(gè)職責(zé)防止用戶在 AI 回復(fù)過程中重復(fù)發(fā)送以及讓輸入框在流式期間保持禁用態(tài)。messages數(shù)組里每條消息的id用時(shí)間戳生成是為了在流式更新時(shí)能精確定位到那條 AI 消息并只更新它的content而不是整個(gè)數(shù)組重建。常見做法是用setData的路徑語法messages[${index}].content來局部更新性能比全量替換好很多。2.2 后端服務(wù)與 LLM 對(duì)接層后端通常是一個(gè) Node.js 服務(wù)用 Express 或 Koa 起 HTTP 接口同時(shí)用ws庫開一個(gè) WebSocket 通道。為什么不用純 HTTP 輪詢因?yàn)榱魇捷敵鲂枰?wù)端主動(dòng)推送WebSocket 的全雙工特性天然適合。如果不想維護(hù) WebSocket也可以用 SSEServer-Sent Events但小程序?qū)?SSE 的支持不如 WebSocket 直接。目錄結(jié)構(gòu)大致如下project-root/ ├── miniprogram/ # 小程序前端代碼 │ ├── pages/chat/ # 聊天頁面 │ ├── utils/request.js # 封裝 WebSocket 和 HTTP 請(qǐng)求 │ └── app.js # 全局配置存后端地址 ├── server/ # 后端服務(wù) │ ├── index.js # 入口啟動(dòng) HTTP 和 WebSocket │ ├── llm.js # LLM API 調(diào)用封裝 │ └── session.js # 會(huì)話上下文管理 └── package.jsonserver/llm.js是核心對(duì)接層。它接收用戶消息和會(huì)話 ID從session.js取出歷史上下文拼成 LLM 需要的消息格式然后發(fā)起流式請(qǐng)求。以常見的 OpenAI 兼容接口為例// server/llm.js 流式調(diào)用封裝 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, // 模型名按實(shí)際服務(wù)商填寫 messages: messages, // 包含歷史上下文的消息數(shù)組 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) // 每收到一個(gè)片段就回調(diào) } } }) }這里有幾個(gè)容易翻車的點(diǎn)。buffer的存在是因?yàn)?TCP 流不保證每次data事件都對(duì)應(yīng)一個(gè)完整的 SSE 行必須自己緩存拼接。lines.pop()把可能不完整的最后一行留到下一次處理這是流式解析的標(biāo)準(zhǔn)操作。onChunk回調(diào)把增量文本傳給 WebSocket 的發(fā)送函數(shù)最終推到小程序端。API Key 放在環(huán)境變量里不要硬編碼在源碼中這是基本的安全習(xí)慣。2.3 會(huì)話上下文管理session.js維護(hù)一個(gè)內(nèi)存中的 Mapkey 是會(huì)話 IDvalue 是消息數(shù)組。每次用戶發(fā)消息先把用戶消息 push 進(jìn)去調(diào)用 LLM 拿到完整回復(fù)后再把 AI 消息 push 進(jìn)去。這樣下一輪對(duì)話時(shí)歷史上下文就自然帶上了。// server/session.js 簡(jiǎn)易上下文管理 const sessions new Map() const MAX_TURNS 10 // 最多保留最近 10 輪對(duì)話 function getContext(sessionId) { return sessions.get(sessionId) || [] } function appendMessage(sessionId, role, content) { const history sessions.get(sessionId) || [] history.push({ role, content }) // 超出輪數(shù)限制時(shí)從頭部丟棄最早的消息 if (history.length MAX_TURNS * 2) { history.splice(0, history.length - MAX_TURNS * 2) } sessions.set(sessionId, history) }MAX_TURNS控制上下文長(zhǎng)度防止 token 消耗過快和請(qǐng)求體過大。生產(chǎn)環(huán)境建議把 Map 換成 Redis否則服務(wù)重啟后所有會(huì)話丟失。會(huì)話 ID 由小程序端在首次進(jìn)入聊天頁時(shí)生成存在storage里后續(xù)每次請(qǐng)求都帶上。3. 跑起來從零到發(fā)出第一條消息3.1 環(huán)境準(zhǔn)備與依賴安裝你需要三樣?xùn)|西微信開發(fā)者工具、Node.js 環(huán)境建議 16 以上、一個(gè)可用的 LLM API Key。源碼根目錄下執(zhí)行npm install安裝后端依賴主要就是express、ws、axios這幾個(gè)。小程序端不需要 npm 安裝微信開發(fā)者工具直接打開miniprogram目錄即可。后端啟動(dòng)前在server目錄下新建.env文件寫入你的 API Key 和接口地址LLM_API_KEY你的密鑰 LLM_BASE_URLhttps://api.example.com/v1 PORT3000然后在server/index.js里用dotenv加載這些變量。啟動(dòng)命令是node index.js看到控制臺(tái)輸出Server running on port 3000就說明后端就緒。3.2 小程序端配置與聯(lián)調(diào)打開miniprogram/app.js把globalData.serverUrl改成你本機(jī)的 IP 加端口比如http://192.168.1.100:3000。注意不能用localhost因?yàn)殚_發(fā)者工具和手機(jī)真機(jī)的網(wǎng)絡(luò)環(huán)境不同真機(jī)調(diào)試必須用局域網(wǎng) IP。微信開發(fā)者工具里需要勾選“不校驗(yàn)合法域名”否則小程序會(huì)攔截非 HTTPS 請(qǐng)求。正式發(fā)布時(shí)后端必須上 HTTPS 并配置到小程序后臺(tái)的 request 合法域名列表里。聯(lián)調(diào)時(shí)先在模擬器里發(fā)一條“你好”觀察三個(gè)地方小程序控制臺(tái)的messages數(shù)組是否更新、后端控制臺(tái)是否打印收到請(qǐng)求、Network 面板是否有 WebSocket 幀往來。如果消息發(fā)出去了但沒回復(fù)大概率是 API Key 無效或接口地址填錯(cuò)后端會(huì)報(bào) 401 或 404。3.3 流式渲染的觸發(fā)時(shí)機(jī)小程序端收到 WebSocket 消息后需要區(qū)分是增量片段還是完整消息。常見做法是在協(xié)議里加一個(gè)type字段type: chunk表示增量type: done表示結(jié)束。收到chunk時(shí)用setData更新最后一條 AI 消息的content收到done時(shí)把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) { // 只更新最后一條消息的內(nèi)容避免全量 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的性能優(yōu)化手段它只更新指定路徑的數(shù)據(jù)不會(huì)觸發(fā)整個(gè)列表的重新渲染。消息多的時(shí)候這個(gè)差別很明顯全量更新會(huì)卡頓。4. 避坑與常見問題那些讓你懷疑人生的報(bào)錯(cuò)4.1 消息發(fā)出去了但 AI 不回復(fù)現(xiàn)象是小程序端顯示用戶消息AI 占位消息一直空白isStreaming卡在true。原因通常是后端請(qǐng)求 LLM 時(shí)拋了異常但沒有把錯(cuò)誤推回前端。解決方式是在streamChat外層包try/catch捕獲到錯(cuò)誤時(shí)通過 WebSocket 發(fā)送一條type: error的消息前端收到后把占位消息內(nèi)容改成“請(qǐng)求失敗請(qǐng)重試”同時(shí)把isStreaming置回false。另外檢查 API Key 是否過期、接口地址是否帶了多余的斜杠。4.2 流式輸出文字閃爍或重復(fù)現(xiàn)象是 AI 回復(fù)的文字一會(huì)兒重復(fù)一會(huì)兒跳變。原因是setData更新時(shí)用了全量messages替換而 WebSocket 消息到達(dá)順序和setData的異步渲染產(chǎn)生了競(jìng)爭(zhēng)。解決方式是堅(jiān)持用路徑語法局部更新并且在后端保證每個(gè)chunk只發(fā)送一次。如果后端用了多個(gè)進(jìn)程或集群WebSocket 連接可能被分配到不同實(shí)例導(dǎo)致消息亂序這時(shí)需要引入 Redis 的 pub/sub 來統(tǒng)一分發(fā)。4.3 真機(jī)上鍵盤彈起遮擋輸入框模擬器里輸入框位置正常真機(jī)上鍵盤彈起后輸入框被擋住。原因是小程序的adjust-position屬性默認(rèn)是true但fixed定位的輸入欄不會(huì)自動(dòng)上移。解決方式是把輸入欄的bottom值綁定到一個(gè)動(dòng)態(tài)數(shù)據(jù)監(jiān)聽bindfocus和bindblur事件聚焦時(shí)把bottom設(shè)為鍵盤高度失焦時(shí)歸零。鍵盤高度可以通過wx.onKeyboardHeightChange獲取。4.4 上下文太長(zhǎng)導(dǎo)致請(qǐng)求超時(shí)或費(fèi)用飆升現(xiàn)象是聊了十幾輪后響應(yīng)越來越慢API 賬單漲得飛快。原因是session.js里沒有做上下文截?cái)鄽v史消息無限增長(zhǎng)。解決方式是設(shè)置MAX_TURNS并在超出時(shí)從頭部丟棄或者用摘要方式把早期對(duì)話壓縮成一句話再拼進(jìn)上下文。另一個(gè)技巧是只保留最近幾輪的用戶消息和 AI 回復(fù)中間的系統(tǒng)提示詞單獨(dú)維護(hù)不參與截?cái)唷?.5 小程序?qū)徍吮痪茴惸颗c內(nèi)容合規(guī)現(xiàn)象是代碼沒問題但提交審核被駁回。原因是聊天類小程序需要選擇正確的服務(wù)類目并且 AI 生成內(nèi)容需要有人工審核或過濾機(jī)制。解決方式是在后端接入內(nèi)容安全接口對(duì)用戶輸入和 AI 輸出做敏感詞過濾同時(shí)在提交審核時(shí)準(zhǔn)備好類目說明材料。這個(gè)坑不解決代碼寫得再好也上不了線。5. 二次開發(fā)換模型、加功能、做自己的 AI 助手5.1 替換 LLM 服務(wù)商源碼默認(rèn)對(duì)接的是 OpenAI 兼容接口換成其他服務(wù)商時(shí)只需要改server/llm.js里的請(qǐng)求地址、模型名和認(rèn)證方式。如果服務(wù)商的流式格式不是標(biāo)準(zhǔn) SSE比如返回的是 JSON Lines那就把buffer解析部分的data:前綴判斷去掉直接JSON.parse(line)。參數(shù)方面temperature控制隨機(jī)性聊天場(chǎng)景建議 0.7 左右max_tokens限制單次回復(fù)長(zhǎng)度防止 AI 話太多。5.2 增加多會(huì)話切換當(dāng)前源碼是單會(huì)話模式所有消息存在一個(gè)數(shù)組里。要改成多會(huì)話需要在session.js里把 Map 的 key 從固定值改成動(dòng)態(tài)會(huì)話 ID小程序端增加一個(gè)會(huì)話列表頁面用wx.setStorageSync存會(huì)話索引。切換會(huì)話時(shí)重新拉取對(duì)應(yīng)歷史消息渲染到聊天頁。這個(gè)改造的核心是會(huì)話 ID 的生成和傳遞建議用uuid庫生成避免時(shí)間戳沖突。5.3 加入語音輸入與圖片理解語音輸入可以用微信小程序的wx.getRecorderManager錄音然后把音頻文件上傳到后端調(diào)用語音轉(zhuǎn)文字接口拿到文本再走聊天流程。圖片理解則需要后端支持多模態(tài)模型把圖片轉(zhuǎn)成 base64 或上傳到對(duì)象存儲(chǔ)后把 URL 傳給 LLM。這兩個(gè)功能都會(huì)增加請(qǐng)求體積注意小程序的wx.uploadFile有大小限制圖片建議壓縮后再傳。5.4 一個(gè)我踩過的坑有一次我把MAX_TURNS設(shè)成了 50覺得上下文越長(zhǎng) AI 越聰明。結(jié)果聊到第 20 輪時(shí)請(qǐng)求體超過了 100KB后端直接超時(shí)前端卡在isStreaming狀態(tài)無法恢復(fù)。更麻煩的是 API 按 token 計(jì)費(fèi)那一晚上燒掉了不少額度。從那以后我每次改上下文相關(guān)參數(shù)都強(qiáng)制走一遍“發(fā) 30 條消息看響應(yīng)時(shí)間和費(fèi)用”的驗(yàn)證流程。另外isStreaming一定要加超時(shí)兜底比如 30 秒沒收到done就自動(dòng)重置否則用戶會(huì)以為小程序卡死。希望這些經(jīng)驗(yàn)幫到你少走點(diǎn)彎路。本文還有配套的精品資源點(diǎn)擊獲取