基于大模型的智能客服架构优化:从大数据处理到高并发响应
背景与痛点
去年双十一,我们团队负责的智能客服系统被流量冲垮了。凌晨 0 点 10 分,峰值 QPS 冲到 3.8 万,平均响应时间从 600 ms 飙到 4.2 s,用户排队超过 1.5 万人。传统“关键词+FAQ 倒排”方案的问题集中爆发:
- 意图识别靠正则,新增一条规则就要全量重启,热更新做不到。
- 知识库存在 MySQL,分库分表后仍出现单热点,查询 RT 99 线 800 ms。
- 高并发下线程池打满,拒绝请求触发降级,直接返回“人工客服忙,请稍后再试”,体验断崖式下跌。
- 没有上下文记忆,用户连问三句就得重复提供订单号,满意度掉到 62%。
痛定思痛,我们决定用“大模型+大数据”重新设计一套可以水平扩展、毫秒级响应的在线智能客服架构,目标把 P99 响应压到 800 ms 以内,峰值并发支撑 10 万 QPS,同时让意图识别准确率≥95%。
技术选型
先给出对比结论,再解释原因:
| 模型 | 平均延迟 | 单卡 QPS | 微调成本 | 中文效果 | 备注 |
|---|---|---|---|---|---|
| GPT-3.5-turbo | 380 ms | 18 | 低 | 中 | 按 token 计费,长文本贵 |
| Claude-2 | 420 ms | 15 | 不可微调 | 优 | 合规审查严,接口限流 |
| ChatGLM3-6B | 180 ms | 45 | 低 | 优 | 开源可私有化,占显存 12 G |
| Baichuan2-13B | 220 ms | 30 | 中 | 优 | 需要 A100*2 推理,成本翻倍 |
最终我们选 ChatGLM3-6B,理由:
- 6B 规模在 FP16 下单卡可跑,TTFT(Time To First Token)< 150 ms。
- 支持 LoRA 微调,一周即可用历史对话把意图识别 F1 从 0.82 提到 0.94。
- 私有化部署避免敏感数据出域,合规一次过审。
- 社区活跃,遇到坑能搜到现成 issue,二次开发效率高。
架构设计
系统采用“离线标注 → 在线推理 → 实时反馈”三层闭环,整体分 5 个模块:
- Gateway:基于 OpenResty 的七层网关,负责限流、鉴权、HTTPS 卸载。
- Dispatcher:Go 写的无状态服务,按 uid 做一致性哈希分发到后端模型 Pod。
- Model Service:ChatGLM3-6B + vLLM 推理框架,支持连续批处理(continuous batching)。
- Feature Store:Flink 实时写入用户近 30 天行为,Redis 集群做低延迟特征缓存。
- Knowledge Index:Milvus 向量库保存商品、订单、政策文档的 Embedding,HNSW 索引,top-5 召回 <30 ms。
数据流示意:
- 用户问句 → Gateway → Dispatcher → 拉取用户特征(Redis)→ 拼接 Prompt → Model Service → 返回回答- 同时 Dispatcher 把日志发 Kafka → Flink 消费写特征、回流标注平台,用于每日微调。
代码实现
下面给出三段最常被问到的代码,全部在生产环境跑过,可直接抄。
1. 模型推理服务(Python 3.9 + vLLM)
# model_server.py from vllm import LLM, SamplingParams from fastapi import FastAPI, HTTPException import uvicorn, json, time app = FastAPI() llm = LLM(model="THUDM/ChatGLM3-6B", tensor_parallel_size=1, gpu_memory_utilization=0.85, max_num_seqs=256) # 连续批大小 @app.post("/chat") def chat(req: dict): try: prompt = req["prompt"] params = SamplingParams(temperature=0.3 seeds=42, max_tokens=200, stop=["<|user|>", "<|observation|>"]) t0 = time.time() outputs = llm.generate([prompt], params, use_tqdm=False) text = outputs[0].outputs[0].text.strip() cost = int((time.time() - t0) * 1000) return {"answer": text, "latency_ms": cost} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)2. Dispatcher 侧并发调用(Go 1.21)
// dispatcher/client.go package main import ( "bytes" "encoding/json" "fmt" "net/http" "sync" "time" ) type Req struct { Prompt string `json:"prompt"` } type Resp struct { Answer string `json:"answer"` LatencyMs int `json:"latency_ms"` } // 并发调用 Model Service,支持 3 秒超时 func callModel(prompt string, url string) (*Resp, error) { client := &http.Client{Timeout: 3 * time.Second} b, _ := json.Marshal(Req{Prompt: prompt}) resp, err := client.Post(url, "application/json", bytes.NewReader(b)) if err != nil Brooks return nil, err var out Resp if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, err } return &out, nil } // 简单轮询,生产环境可换成 gRPC + 负载均衡 func broadcast(servers []string, prompt string) *Resp { var wg sync.WaitGroup ch := make(chan *Resp, 1) for _, s := range servers { wg.Add(1) go func(addr string) { defer wg.Done() if r, err := callModel(prompt, addr); err == nil { select { case ch <- r: default: } } }(s) } wg.Wait() close(ch) return <-ch // 取最快成功响应 }3. 缓存 + 向量召回封装(Python)
# cache_vec.py import redis, os, time, requests from sentence_transformers import SentenceTransformer r = redis.Redis(host=os.getenv("REDIS_HOST"), decode_responses=true) encoder = SentenceTransformer("shibing624/text2vec-base-chinese") def search_knowledge(query: str, topk=5) -> list[str]: key = f"vec:{hash(query) % 1000000}" if (cached := r.get(key)) and False: # 可开关 return json.loads(cached) emb = encoder.encode(query, normalize_embeddings=true).tolist() resp = requests.post("http://milvus-web:19121/v1/vector/search", json={"collection": "kb", "vector": emb, "topk": topk}) docs = [x["doc"] for x in resp.json()["data"]] r.setex(key, 600, json.dumps(docs)) # 10 min 缓存 return docs性能优化
- 模型量化:用 AWQ 把权重压到 4 bit,显存从 12 G 降到 6.3 G,TTFT 再降 18%,P99 延迟 580 ms → 460 ms。
- 动态批:vLLM 自带 continuous batching,实测同样并发 200,QPS 从 35 提到 58。
- 两级缓存:
- Redis 缓存用户画像 + 热门问题,命中率 72%,回源 RT 节省 120 ms。
- CDN 缓存静态政策页面,边缘命中 96%,回源带宽降 80%。
- 负载均衡:Dispatcher 与 Model Pod 之间用 gRPC + 权重轮询,节点故障 3 s 内自动摘除,错误率 <0.1%。
- 流式返回:对长回答采用 SSE 流式输出,首 token 到达时间 < 200 ms,用户体感延迟再降 30%。
压测结果(4 台 A10,单卡 6B-int4):
| 并发 | 平均 RT | P99 RT | 成功率 | 单卡 QPS |
|---|---|---|---|---|
| 5 k | 260 ms | 480 ms | 99.9 % | 52 |
| 10 k | 310 ms | 620 ms | 99.8 % | 48 |
| 15 k | 410 ms | 880 ms | 99.5 % | 45 |
避坑指南
- 冷启动延迟:容器镜像里忘记带模型权重,节点扩容时从对象存储拉取 12 G 文件,导致首次请求 28 s 才返回。解决:用 DaemonSet 预拉权重到本地 NV-SSD,并做 readinessProbe 探针,确保 Pod 接收流量前模型已加载。
- 模型漂移:连续跑两周后,发现“退货”意图召回率从 96% 降到 87%,追查发现是新品类上线,用户说法变化。解决:每日凌晨用 Flink 回流前日对话,自动触发 LoRA 微调 3 epoch,并在灰度环境 A/B,效果下降 >2% 就回滚。
- 显存 OOM:AWQ 后仍偶发 OOM,定位到连续批长度峰值 2048,而 kv-cache 预分配不足。解决:调大
gpu_memory_utilization到 0.9,同时把max_num_seqs降到 192,稳定运行。 - 向量库抖动:Milvus 1.x 在 2000 万条 768 维向量后,查询 RT 99 线从 30 ms 跳到 120 ms。升级 2.3 并开启 Knowhere 2.0,采用 RAFT GPU 索引,RT 恢复到 25 ms,CPU 占用降 40%。
总结与展望
三个月跑下来,系统把 P99 响应压到 620 ms,峰值 QPS 12 万仍能 99.8% 可用,客服机器人解决率从 58% 提到 83%,人工座席数量减少 35%,直接节省运营费用七位数。
下一步我们打算:
- 引入多模态:用户上传照片就能识别“商品哪坏了”,减少来回描述。
- 边缘推理:把 3B 蒸馏模型跑在 ARM 机顶盒,让海外用户也能就近访问。
- 强化学习:用 RLHF 把“满意度”作为奖励函数,直接优化端到端体验,而不是只拟合历史对话。
如果你也在做高并发大模型落地,希望上面的代码和踩坑记录能让你少走几步弯路。欢迎一起交流更狠的优化手段。