背景痛点:多用户共享 ChatGPT 时到底卡在哪?
第一次把 ChatGPT 能力开放给团队或客户时,我踩过的坑比 OpenAI 的文档页数还多。
主要痛点就三条:
- 状态保持:每个用户都要独立的对话上下文,刷新页面或换个浏览器,历史不能丢。
- 并发控制:同一账号的 API Key 有 TPM(Token per minute)限制,多人同时提问容易 429。
- 实时体验:浏览器等回复时,如果一次性返回整段答案,白屏 10 秒用户就跑了。
带着这三个问题,我开始做 ChatGPT Web Share,目标很简单——像用网页版微信一样,打开浏览器就能聊,后台却共用同一个(或多个)API Key,还要让老板觉得“挺快、挺稳”。
技术选型:Websocket、SSE、长轮询谁更适合新手?
我把三种方案放在同一台 2C4G 的小水管机器上跑了一夜,结论如下:
- Websocket:双向实时,最像“打电话”。但要做心跳、重连、分布式会话复制,代码量 +30%。
- SSE(Server-Sent Events):服务端单向推送,浏览器原生支持,自动重连。Node 端只比写 REST 多两行代码,省头发。
- 长轮询:实现最简单,一个 setTimeout 就能跑。每 30 秒保活一次,空转时占连接,高并发下内存飙得比股票还快。
综合“新手友好度 + 实时性 + 资源消耗”,我选了 SSE:代码少、无需额外协议、Nginx 也不用开proxy_read_timeout 1d。下文所有示例都基于 SSE,如果你偏爱 Websocket,把res.write()换成ws.send()即可,业务逻辑不变。
核心实现:30 分钟搭出最小可用版本
1. 项目骨架
mkdir chatgpt-web-share && cd $_ npm init -y npm install express dotenv openai jsonwebtoken cors目录结构:
├── app.js // 入口 ├── routes/ │ └── chat.js // 聊天路由 ├── middleware/ │ ├── auth.js // JWT 校验 │ └── limiter.js // 速率限制 ├── services/ │ └── openai.js // OpenAI 封装 └── .env // 环境变量2. 封装 OpenAI 客户端(线程安全)
// services/openai.js import { OpenAI } from 'openai'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, maxRetries: 3, // 自动重试 timeout: 15000, // 15s 超时 }); /** * 线程安全:每次调用都新建 Chat 完成实例,不共享 messages 数组 * @param {string} userId * @param {string} prompt * @param {Array} history // 历史对话 [{role, content}] * @returns {AsyncIterable} SSE 流 */ export async function* chatStream(userId, prompt, history) { const messages = [ ...history, { role: 'user', content: prompt }, ]; const stream = await openai.chat.completions.create({ model: 'gpt-3.5-turbo', messages, temperature: 0.7, stream: true, }); for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content; if (delta) yield delta; } }要点:
- 不缓存
openai.chat.completions实例,每次新建,防止多用户交叉污染。 - 返回
AsyncIterable,方便上层用for await逐字推送,降低首字延迟。
3. SSE 路由(支持多用户隔离)
// routes/chat.js import express from 'express'; import { chatStream } from '../services/openai.js'; const router = express.Router(); router.post('/chat', async (req, res) => { const { prompt, history = [] } = req.body; const userId = req.auth.sub; // JWT 中间件注入 res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); try { for await (const chunk of chatStream(userId, prompt, history)) { res.write(`data: ${JSON.stringify({ chunk })} `); } res.write('data: [DONE] '); } catch (e) { res.write(`data: ${JSON.stringify({ error: e.message })} `); } finally { res.end(); } }); export default router;前端只需:
const es = new EventSource('/api/chat'); es.onmessage = (e) => { if (e.data === '[DONE]') return; const { chunk } = JSON.parse(e.data); document.querySelector('#answer').innerHTML += chunk; };4. 会话隔离与历史存储
为了刷新页面不丢上下文,我把对话历史放在 Redis,结构如下:
Key: chat:${userId} Value: JSON 数组,最多保留 20 轮对话(冷热分离)冷数据:超过 20 轮后,自动打包成压缩文件丢到 OSS,用户翻旧账再懒加载。
热数据:TTL 设为 7 天,LRU 淘汰,内存占用可控。
生产级考量:让老板晚上睡得好
1. 负载测试
Locust 脚本(Python)模拟 200 并发,每个用户持续 5 分钟:
from locust import HttpUser, task, between class ChatUser(HttpUser): wait_time = between(1, 3) @task def ask(self): self.client.post("/api/chat", json={"prompt": "用一句话介绍 ChatGPT", "history": []}, headers={"Authorization": "Bearer eyJ0..."})跑完报告:P99 延迟 1.8s,内存占用 220 MB,TPM 峰值 8k,未触发 429。
若 TPM 超限,可在openai.js里加一层令牌桶限速,或动态降级到gpt-3.5-turbo-16k模型。
2. 鉴权与速率限制
- JWT 颁发:登录后返回
access_token有效期 30 min,刷新令牌 7 天。 - 速率限制:基于
userId做令牌桶,每分钟 30 次提问,Burst 5 次,返回429带Retry-After头,前端友好提示。
3. 上下文丢失的常见坑
- 前端把
history数组存在localStorage,大小超限被浏览器清掉。
→ 使用 IndexedDB 或后台 Redis 兜底。 - 服务端升级重启,内存会话消失。
→ 把热数据持久化到 Redis AOF,重启后自动加载。 - 用户开两个浏览器 tab,各自历史不一致。
→ 在数据库层以userId为维度,强制唯一写,WebSocket/SSE 连接用roomId区分,多端同步。
避坑指南:对话历史存储的冷热数据分离
- 热数据:最近 20 轮,JSON 存 Redis,读写 < 5 ms。
- 温数据:20~100 轮,压缩后放 Redis Hash,读时解压,延迟 20 ms 内。
- 冷数据:全量历史,按天下沉到 OSS,用户点击“查看更多”再拉取,前端分页渲染,避免一次加载拖垮浏览器。
延伸思考题
- 如何实现跨平台会话同步?(提示:考虑用 MQTT 或 WebRTC DataChannel)
- 若未来要支持语音输入,你会把 ASR 模块放在客户端还是服务端?为什么?
- 当 OpenAI 推出新模型,如何设计一套灰度发布策略,让 10% 用户先体验?
写在最后
把 ChatGPT Web Share 从 Demo 搬到生产,我最大的感受是:实时性易做,稳定性难守。上面这套代码已经跑在我们内部协作平台两个月,日均 3k 次对话,除了有人手滑刷脚本触发限速,基本零故障。如果你也想快速落地同款功能,又担心踩坑,可以试试火山引擎的从0打造个人豆包实时通话AI动手实验。它把 ASR、LLM、TTS 串成一条完整链路,提供现成的 Web 模板,本地npm install后五分钟就能跑起来。我跟着做完,发现对“耳朵-大脑-嘴巴”的协同流程瞬间清晰,比自己东拼西凑省了不少时间。小白也能顺利体验,建议边敲代码边对照实验文档,收获双倍。祝调试顺利,早日上线你的专属 AI 对话助手!