背景与痛点:对话系统“慢”与“乱”的日常
过去一年,我陆续帮三家客户把客服机器人从“能说话”升级到“说得快、记得住”。总结下来,最痛的点无非两个:
- 高延迟:单轮问答 2 s 起步,遇上高峰期直接 5 s 开外,用户体验堪比 56 K 拨号。
- 上下文丢失:多轮对话里,前端一刷新,Thread ID 没对齐,模型“失忆”秒变复读机。
这两个问题叠加,直接把“智能客服”干成“智障客服”。既然 ChatGPT O4 把推理成本砍了 40%,我们干脆拿它做小白鼠,搭一套可横向扩展的微服务框架,把延迟压到 500 ms 以内,同时让对话状态像胶水一样粘住用户。
技术选型:REST vs. Stream,到底怎么选?
先放结论:
- 首包时延敏感 → 流式(SSE/WebSocket)
- 高并发、可接受整包返回 → REST 批量
具体对比如下:
| 维度 | REST | Stream |
|---|---|---|
| 首包时延 | 高(整包返回) | 低(chunk 直出) |
| 代码复杂度 | 低 | 高(需处理断线重连) |
| 自动重试 | 原生支持 | 需自己做 |
| 压测吞吐 | 高(连接复用) | 中(单长连接) |
我们最后采用“混合模式”:
- 首轮握手 + 业务问答 → Stream,保证“秒回”体验
- 批量日志摘要写、情绪分析 → REST,方便做批处理与缓存
核心实现:微服务三板斧——网关、缓存、队列
整体架构如图(文字版):
┌--------┐ ┌--------┐ ┌---------┐ │ Gateway │────▶│ Cache │────▶│ LLM Svc │ └--------┘ └--------┘ └---------┘ │ │ │ ▼ ▼ ▼ Trace ID Redis Cluster O4 Async Batch下面给出最常被问到的两段代码,左边 Python 负责批处理,右边 Node.js 管流式推送。
1. Python 批处理调度器(batch_worker.py)
import asyncio, aiohttp, os, time, json from typing import List BATCH_SIZE = 8 # O4 实测 8 条并发性价比最高 O4_ENDPOINT = "https://api.openai.com/v1/chat/completions" async def make_one(payload: dict, session) -> dict: headers = {"Authorization": f"Bearer {os.getenv('O4_KEY')}"} async with session.post(O4_ENDPOINT, json=payload, headers=headers) as resp: return await resp.json() async def batch_infer(requests: List[dict]) -> List[dict]: """并发打包,返回顺序与输入一致""" async with aiohttp.ClientSession() as session: tasks = [make_one(req, session) for req in requests] return await asyncio.gather(*tasks) if __name__ == "__main__": dummy = [{"model":"gpt-4o","messages":[...]} for _ in range(16)] s = time.time(); asyncio.run(batch_infer(dummy)); print(time.time()-s)要点:
- 用
asyncio.gather保证 8 条并行,但别超过 rate limit(O4 默认 10k tpm) - 返回后按输入顺序重组,避免乱序导致上下文错位
2. Node.js 流式网关(gateway.js)
import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import Redis from 'iored'; const app = express(); const redis = new Redis({ enableOfflineQueue: false }); // 1. 缓存命中直接 SSE 返回 app.get('/chat', async (req, res) => { const { uid, q } = req.query; const key = `cache:${hash(q)}`; const cached = await redis.get(key); if (cached) { res.write(`data: ${cached}\n\n`); return res.end(); } // 2. 未命中则代理到上游 O4 Stream return createProxyMiddleware({ target: 'https://api.openai.com', changeOrigin: true, pathRewrite: {'^/chat': '/v1/chat/completions'}, onProxyReq: (p, req) => { p.setHeader('Authorization', `Bearer ${process.env.O4_KEY}`); p.setHeader('Content-Type', 'application/json'); }, selfHandleResponse: true, onProxyRes: (p, req, res) => { p.on('data', chunk => { const str = chunk.toString().replace(/^data: /, ''); try { const d = JSON.parse(str); const txt = d.choices[0]?.delta?.content || ''; res.write(`data: ${txt}\n\n`); // 3. 边输出边写缓存 redis.append(key, txt); } catch {} }); p.on('end', () => res.end()); } })(req, res); }); app.listen(3000);技巧:
- 用
hash(q)做 key,长度 64 字节,可把命中率拉到 35 % 左右 redis.append边流边写,避免等整包返回后二次序列化
性能优化:压测数据与调参笔记
我们在 4C8G 的容器里跑wrk -t12 -c400 -d30s,结果如下:
| 场景 | P99 延迟 | 吞吐 QPS | CPU | 备注 |
|---|---|---|---|---|
| 无缓存直调 O4 | 2.3 s | 42 | 90 % | 冷启动占 600 ms |
| 加 8 并发批处理 | 520 ms | 186 | 75 % | 网络 IO 占大头 |
| 再加 Redis 缓存 | 180 ms | 312 | 55 % | 命中 35 % |
调参经验:
- 批大小 8 条是甜点,再大延迟陡增
- 开启
http2可把 TLS 握手省 70 ms - 把
temperature=0.7固定缓存 key,避免随机值导致缓存穿透
避坑指南:生产级踩坑 Top 5
令牌超限
现象:返回 429,但 header 里x-ratelimit-remaining还有余额
根因:O4 按“token / min”计数,批处理一次 8 条易瞬间打满
解决:用漏桶算法限速,批前估算total_tokens,超阈就拆包冷启动延迟
现象:每天第一次请求 1.2 s+
根因:函数计算实例被回收
解决:- 容器化后加
prewarm.sh每 55 分钟 ping 一次 - 或者把
keep_warm=true塞到网关,定期发心跳
- 容器化后加
上下文断裂
现象:用户刷新页面,Thread ID 丢失,模型从头开始
解决:- 把 Thread ID 存到 HttpOnly Cookie,前端无感刷新
- 网关层用
uid+session做一致性哈希,保证同一 Pod 处理
返回截断
现象:长回答被max_tokens截断
解决:- 先估算
tokens = len(text) // 0.75,再动态上调max_tokens - 对长文本改用“分段 SSE”(先返回 300 tokens,前端边读边续)
- 先估算
缓存雪崩
现象:热点问题同时过期,流量打到 O4
解决:- 给 key 加随机 TTL ±300 s
- 本地加 10 % 的
stale-while-revalidate,让网关先返回旧数据,后台异步更新
开放问题:多模态怎么玩?
文本对话调顺后,客户又抛来需求:
“能不能让用户发张图,让机器人边看图边聊天?”
O4 本身支持gpt-4o视觉输入,但要把 STT、LLM、TTS、Vision 四路合流,延迟会不会炸?缓存策略要不要按图片 hash?
各位读者如果已经试过,欢迎分享你们的并发合并方案,也许下一篇就写“实时语音 + 视觉”双通道的踩坑续集。
把上面的代码拼拼凑凑,我本地 2 小时就能跑通一个 Demo,但真要压到生产级别,还是踩了不少暗坑。若你也想亲手把“听得见、想得明白、说得溜”的 AI 伙伴快速落地,却又担心一个人搞不定并发、缓存、批处理这些细枝末节,可以先试试火山引擎出的这个动手实验:
从0打造个人豆包实时通话AI
我跟着做了一遍,实验把 ASR→LLM→TTS 整条链路包成了几个 Docker Compose 文件,本地docker compose up就能跑通;批处理、缓存、SSE 重连这些细节也给了模板,基本改两行配置就能迁移到自己的业务里。小白顺着 README 也能跑,省下的时间专心调角色音色和提示词,算是一条龙“懒人速成”路线。祝你玩得开心,早日让自家 AI 开口说话!