痛点分析长对话场景下的内存泄漏去年双十一公司把客服 Chatbot 从轮询架构升级到流式对话结果凌晨 2 点 PagerDuty 狂响8 台 32 G 机器在 30 min 内被吃光干净重启后 10 min 又打满。排查发现老代码用while True redis.blpop保持长轮询每条对话在内存里维护一个Dict[session_id, full_history]用户聊得越久列表越长GC 根本来不及回收。再加上为了“实时”把心跳间隔压到 200 ms无效空转把 CPU 也拖垮。一句话传统轮询模型在“长连接 长上下文”场景下既扛不住并发也守不住内存。技术对比gRPC vs WebSocket 的吞吐量差异把轮询砍掉后我们纠结在“全双工”通道选型WebSocket 上手快gRPC Stream 更省序列化开销。用同一套 JMeter 脚本压 5 min场景是 200 字节文本上行、800 字节 JSON 下行后端 4 核 8 G Pod 各 20 副本。WebSocketSTOMP over SockJS峰值 TPS 4 800平均 RT 62 msCPU 83%内存涨到 5.2 GgRPC 双向流protobuf峰值 TPS 7 100平均 RT 38 msCPU 67%内存稳定在 3.4 G结论在密集小包场景下gRPC 的 HTTP/2 多路复用 二进制编码让吞吐直接提升 48%RT 降 40%CPU 余量还能再塞业务逻辑。最终线上采用“WebSocket 只留给浏览器端做兼容App 与内部服务统一 gRPC”的混合策略。核心实现异步对话处理器与重试机制Python 3.11 FastAPI我们写了一个无阻塞的对话入口把 I/O 全部甩给 asyncio 池关键片段如下已脱敏import asyncio, httpx, tenacity from loguru import logger from fastapi import FastAPI, Request app FastAPI() tenacity.retry( stoptenacity.stop_after_attempt(3), waittenacity.wait_exponential(multiplier1, min2, max10), retrytenacity.retry_if_exception_type( (httpx.ReadTimeout, httpx.ConnectError) ), ) async def call_llm(prompt: str) - str: 调用火山引擎 LLM带指数退避重试 async with httpx.AsyncClient(timeout15) as cli: r await cli.post( https://maas-api.volcengine.com/v1/chat, headers{Authorization: fBearer {TOKEN}}, json{model: doubao-pro-32k, messages: prompt}, ) r.raise_for_status() return r.json()[choices][0][text] app.post(/v1/chat) async def chat_handler(req: Request): body await req.json() uid body[uid] text body[text] history await redis.get(fdlg:{uid}) or [] prompt format_prompt(history, text) answer await call_llm(prompt) # 重试逻辑已封装 history trim_history(history [text, answer]) # 防止爆炸 await redis.set(fdlg:{uid}, history, ex3600) return {answer: answer}几点小心得tenacity比手动try/except简洁且支持异步。历史记录必须做滑动窗口trim_history否则 32 k 上下文模型也会把显存吃光。把redis换成redis.asyncio全程 awaitQPS 从 1 k 提到 4 k基本打满网卡。架构图NLU 与对话状态机下图用 PlantUML 绘制可直接粘到 plantuml.com 预览startuml !define RECTANGLE class package Chatbot Core { [API Gateway] -- [Auth Middleware] [Auth Middleware] -- [Dialogue Manager] [Dialogue Manager] -- [NLU Engine] [Dialogue Manager] -- [State Machine] [State Machine] -- [LLM Adapter] [LLM Adapter] -- [GPU Pool] [Dialogue Manager] -- [Cache Layer] [Cache Layer] -- [Redis Cluster] } package Observability { [Prometheus] -- [Dialogue Manager] : metrics [Grafana] -- [Prometheus] } package Client { [Web/App] --gRPC/WS-- [API Gateway] } enduml思路把“意图识别 实体抽取”独立成 NLU 微服务只负责分类State Machine 维护会话阶段问候、导购、下单、售后阶段决定调用哪条 prompt 模板LLM Adapter 做模型路由与版本灰度GPU Pool 统一通过 K8s Extended Resource 暴露避免每个副本自己占卡。生产考量GPU 池化与 JWT 防重放GPU 资源池化线下实测 1 张 A10 可并发 8 路 32 k 上下文但 Kubernetes 默认 GPU 调度是“整卡独占”利用率不到 30%。我们偷师 Volcano MIG 方案把 A30 切成 2g.10gb 实例每实例 5 路并发部署 vLLM 作为推理池通过 InferenceService CRD 暴露对话管理器按“模型 长度”预估显存调用前向池子申请 slot用完即还结果同等流量下 GPU 节点从 14 台压到 5 台每月云账单降 62%。JWT 令牌防重放对话接口走公网必须鉴权。我们采用短周期 JWT有效期 60 s JTI 白名单网关层校验签名与 exp把jti写入 RedisTTL 70 s重复即拒绝用户重新握手刷新令牌保证重放窗口 60 s压测 1 k TPS 下平均鉴权延迟 1.3 ms内存占用可忽略。避坑指南三个高频 OOM 误区对话历史全量塞给 LLM误区为了“体验连贯”把 50 轮记录全部拼 prompt。解决保留最近 4 轮 摘要摘要要用同模型离线总结只损失 2% 意图准确率却省 70% token。未做会话分片误区单 Pod 维护 10 k 长连接Python 对象暴涨。解决按uid % shards把连接散到 32 个 Partition单 Pod 只处理 1 k 连接内存曲线立刻平整。把异步库当同步用误区await call_llm()外面又包一层asyncio.run()导致 RuntimeError 事件循环嵌套。解决FastAPI 已经跑在uvloop全程async/await即可千万别手痒再启新循环。延伸思考精度与速度的 Trade-off当业务要求 500 ms 内必须返回首字而 32 B 模型在 CPU 要 1.2 sGPU 也要 600 ms你会怎么选降级 7 B 模型 4-bit 量化首字 280 ms但意图准确率掉 6%改流式输出先让前端“假装”开口把 TTFB 降到 120 ms用户体感提升却增加编排复杂度或者把 NLU 前置用轻量 BERT 分类95% 场景走规则5% 走大模型整体 RT 降 45%可维护性又成了新坑没有银弹只有“业务容忍误差”与“硬件预算”之间的动态平衡。留一道开放题如果让你再砍 100 ms你会先动模型还是动工程欢迎一起拆招。写在最后把实验搬回家上面这些代码和思路其实都能在本地笔记本跑通。我在从0打造个人豆包实时通话AI动手实验里把 ASR→LLM→TTS 整条链路拆成 5 个 Docker 容器脚本一键拉起Web 页面直接对话。跟着做完你会直观看到 GPU 池化、流式输出、JWT 鉴权这些“黑话”是怎么跑起来的。小白也能顺利体验我实际操作发现很便捷如果你正好想给自己的项目加上实时语音不妨去戳链接试试。