一、先搞清楚:CosyVoice 接口到底长啥样
CosyVoice 给开发者暴露了两套入口:
REST:短句识别,一次 POST 返回整段文字,适合 15 秒以内的客服问答。
- 优点:接入简单,调试一把过。
- 缺点:RTF(Real-Time-Factor)≈0.3,用户说完还要等半秒。
WebSocket:流式识别,边说边回包,长句也能切成 200 ms 的切片实时上屏。
- 优点:首字延迟 <300 ms,体验接近“同声传译”。
- 缺点:状态机复杂,断网重连、内存泄漏、并发锁都是坑。
一句话总结:
“能 REST 就别 WebSocket,真要做实时客服,再考虑流式。”
二、智能客服场景的典型架构
下面这张图是去年我在某股份制银行落地的拓扑,跑在阿里云 ACK 集群,日活 80 万通对话,峰值 2000 并发,供你直接抄作业。
- 网关层:Nginx + Lua 做统一入口,限流 2000 r/s,熔断阈值 90%。
- 业务层:Spring Cloud 微服务,每个 Pod 8C16G,单实例 200 并发。
- 语音层:
- 短句走 REST,超时 3 s,失败直接降级到本地 ASR 模型。
- 长句走 WebSocket,连接池上限 1024,单连接 10 min 自动踢掉。
- 存储层:Redis 缓存热词,Mongo 落原始音频,ES 存文本索引。
三、鉴权:OAuth2 vs API Key,到底差在哪?
| 指标 | OAuth2(Client Credentials) | API Key(Header 固定) |
|---|---|---|
| 握手 RTT | 2*RTT + 302 ms 证书校验 | 0*RTT |
| 每秒新建连接 | 1200 | 6000 |
| CPU 占用(单核) | 8% | 1.2% |
| 过期刷新 | 需刷新 3600 s 一次 | 无需 |
| 撤销粒度 | 令牌级 | 账户级 |
结论:
- 对内服务、高并发场景,直接 API Key 最快;
- 对外开放平台、多租户,再考虑 OAuth2,刷新令牌记得做异步线程池,别在请求链路里同步换票。
四、Python 实战:带指数退避的异步请求
下面这段代码用 aiohttp,自动重试 5 次,退避因子 0.5 s,日志打到 structlog,生产环境跑了三个月稳如狗。
import aiohttp, asyncio, random, time, structlog from yarl import URL logger = structlog.get_logger() API_KEY = "cv-xxxxxxxx" ENDPOINT = "https://cv-api.xxx.com/v1/asr" async def _fetch_once(session, wav_bytes, retry): headers = {"X-API-Key": API_KEY, "Content-Type": "audio/pcm;rate=16000"} try: async with session.post(ENDPOINT, data=wav_bytes, headers=headers) as resp: if resp.status == 200: return await resp.json() if resp.status == 429: # 被限流 raise aiohttp.ClientError else: logger.warning("cosyvoice_err", status=resp.status) raise aiohttp.ClientError except Exception as e: wait = (2 ** retry) * 05 * (1 + random.random()) logger.warning("retrying", retry=retry, wait=wait) await asyncio.sleep(wait) return None async def recognise(wav_bytes): async with aiohttp.ClientSession( connector=aiohttp.TCPConnector(limit=100, limit_per_host=30)) as session: for retry in range(5): result = await _fetch_once(session, wav_bytes, retry) if result: logger.info("success", text=result.get("text")) return result logger.error("still_fail_after_5") return {"text": "", "confidence": 0}要点:
- limit=100 是全局并发,limit_per_host=30 保护单域名。
- 2 ** retry 让退避指数级增长,避免打爆服务端。
- 429 单独捕获,其他 4xx/5xx 直接抛,方便监控告警。
五、音频格式:PCM ↔ WAV 的 FFmpeg 一行命令
CosyVoice 只认 16kHz/16bit/单声道 PCM,但前端上传常常是 44.1kHz WAV,先转码再送,能省 30% 流量。
ffmpeg -y -i input.wav -ar 16000 -ac 1 -acodec pcm_s16le -f s16le output.pcm参数拆解:
-ar 16000重采样,抗混叠滤波内置,比 Sox 快 20%。-ac 1强制单声道,双声道直接平均,避免识别重复。-f s16le输出裸 PCM,不带 WAV 头,省 44 Byte。
如果实时流,用 pipe:1 直接甩给 Python 的 stdin,内存零拷贝:
ffmpeg -i rtsp://xxx -ar 16000 -ac 1 -f s16le - > /dev/stdout | python recogniser.py六、并发连接数 & 内存配置公式
经验公式(基于 4C8G 容器,G1GC):
最大并发连接 = 容器内存 * 0.4 / 单连接内存
单连接内存 ≈ 音频缓冲 + WebSocket 帧池 ≈ 6 MB(16kHz,200 ms 切片,双缓冲)
代入:8G * 0.4 / 6M ≈ 546,取整 500 做阈值,留 20% 给 JVM 堆外。
JVM 参数:
-Xms3g -Xmx3g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+PerfDisableSharedMem -XX:G1HeapRegionSize=16m监控项:
cosyvoice_conn_active> 450 触发扩容;jvm_gc_pause_seconds> 0.2 连续 3 次重启 Pod。
七、常见踩坑 Top5
- 采样率写死 8kHz,结果识别率掉 15%,日志却看不到报错——记得在 header 里显式带
rate=16000。 - WebSocket 断网后客户端疯狂重连,瞬间打满 6w 端口——用指数退避 + 随机 jitter,服务端同时做连接限速。
- 流式传输没关缓冲,Pod 内存一天泄露 2G——每收到
{"type":"final"}就手动del音频片,并调用gc.collect()。 - OAuth2 令牌在双活环境刷新两次,导致 401——把令牌缓存到 Redis,TTL 设 3500 s,刷新线程单实例抢分布式锁。
- REST 大文件 POST 被 Nginx 413 拒绝——调大
client_max_body_size 10m,同时让前端切片 ≤5M。
八、开放讨论:语音质量 vs. 网络带宽,你怎么选?
CosyVoice 支持 96 kbps OPUS 与 256 kbps PCM 双轨:
- 高码率 PCM 识别率 99.2%,但 1 小时通话 110 MB;
- OPUS 压缩后 38 MB,识别率却掉到 97.8%,丢的多是语气词。
在移动网络、跨境专线、弱网丢包 5% 的场景里,你会牺牲哪一边?
是动态升降码率,还是两端缓存拼接后重试?欢迎留言聊聊你的折中方案。