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