RAG 进阶实战:切块策略、混合检索与 Re-ranking 的量化收益与工程取舍
RAG 的链路谁都能搭:文档切块、embedding 进库、相似度召回、拼 prompt。但同一套链路,有人线上答对率八成,有人五成——差距不在「会不会搭」,而在「每一步的效果账算没算清」。基础链路见 RAG 全链路解析,这篇讲进阶:切块、混合检索、Re-ranking 各值多少分、花多少钱,Graph RAG 什么时候才值得上。
面试题:分布式限流怎么做?令牌桶、滑动窗口各适合什么场景,为什么 Redis + Lua 是集群限流的标准答案?
这是「每日一题」专栏的第十七篇。每天一道面试题,后端 + AI 混合路线,从原理到代码一次讲清。上一篇讲了 为什么 vLLM 是当前最快的 LLM 推理框架,今天按轮换回到后端/分布式方向,聊上一篇预告的题:分布式限流。
GUI Agent 工程化:让大模型操作电脑的感知、行动与安全拆解 — Computer Use 从原理到落地
文本 Agent 的标配是「工具调用」:模型说一句 search_order("sku"),代码去执行,接口是结构化的。但 2024 年底开始,OpenAI Operator、Anthropic Computer Use、各类桌面/浏览器 Agent 把目标换成了像人一样直接操作电脑——没有 API、没有 DOM、只有像素和窗口。这条技术线工程上比想象中难得多,本文按「感知 → 行动 → 验证 → 成本 → 评测 → 安全」拆开讲,附可运行的骨架代码与实测踩坑。
面试题:为什么 vLLM 是当前最快的 LLM 推理框架?Continuous Batching 和 PagedAttention 各自解决了什么问题、显存占用怎么降下来的?
这是「每日一题」专栏的第十六篇。每天一道面试题,后端 + AI 混合路线,从原理到代码一次讲清。上一篇讲了 实时协作文档系统,今天按轮换回到 AI/LLM 方向,聊上一篇预告的题:为什么 vLLM 最快?Continuous Batching 和 PagedAttention 各自解决了什么?
流式 TTS 合成工程:延迟预算、模型选型与评测回环
语音 Agent 的体验差距,经常不是死在 ASR 听错,而是死在最后一跳:合成的声音不够快、不够自然。上一篇 语音 Agent 与流式 ASR 讲了 VAD、ASR 和打断,本篇专攻合成侧:模型选型、流式机制、中文特有的工程坑、以及一套不依赖人耳的评测方法。
先把预算摆出来:语音场景 800ms 的端到端延迟预算里,TTS 通常只能分到 300-400ms,其中首包延迟(TTFB)是硬指标——用户听到第一个字之前的所有环节,VAD、ASR、LLM、TTS,都在为这 800ms 打工。
一、三代 TTS 架构:选型先看你要什么
第一代:自回归系(VALL-E / XTTS)。神经编解码器(EnCodec 这类)把音频打成离散 token,再用 transformer 逐 token 生成。VALL-E 在 2023 年证明了 3 秒参考音频就能克隆音色,这是声音克隆的起点。代价:逐 token 生成天然慢,长句容易重复、吞字,而且不支持流式。今天的定位基本只剩离线高质量生成。
第二代:非自回归系(VITS / FastSpeech 2)。一步出整句 mel 谱,快、稳、RTF 极低,但克隆能力弱、韵律平。适合 IVR 播报这类对音色无要求的场景,或者作为流式拼接链路的底层单元。
第三代:Flow Matching / DiT 系(F5-TTS / CosyVoice2)。把语音生成建模成流匹配问题,推理步数可压缩——F5-TTS 默认 10 步,论文报告单卡 A100 上 RTF 约 0.15(RTF = 合成耗时 / 音频时长,低于 1 才够实时,流式场景要更低)。CosyVoice2 走 LLM + Flow 双栈,官方口径是把流式合成延迟压到 150ms 量级、推理速度较上一代提升约 50%。零样本克隆 + 可控步数 = 自然度与速度兼得,是 2026 年生产的事实标准。
| 维度 | 自回归 | 非自回归 | Flow/DiT |
|---|---|---|---|
| 自然度 | 高 | 中 | 高 |
| 声音克隆 | 强(3s 提示) | 弱 | 强 |
| 推理速度 | 慢 | 极快 | 快 |
| 原生流式 | 不支持 | 不支持 | 支持(如 CosyVoice2) |
| 代表模型 | VALL-E、XTTS | VITS、FastSpeech2 | F5-TTS、CosyVoice2 |
选型结论:做语音 Agent 直接选 Flow/DiT 系;做稳定的播报机器人可以用 VITS 系省 GPU;自回归留给离线精修。别在自回归模型上硬做流式——那是在给架构缺陷打补丁。
二、流式合成的两条路线
路线 A:模型原生流式。CosyVoice2 的 speech token 是流式产出的,stream=True 就能边生成边吐音频块,这是延迟最低的路线:
from cosyvoice.cli.cosyvoice import CosyVoice2
import torchaudio
cosyvoice = CosyVoice2("pretrained_models/CosyVoice2-0.5B",
load_jit=False, load_trt=False, fp16=False)
prompt_wav, sr = torchaudio.load("ref.wav") # 3-10 秒参考音频
prompt_speech_16k = torchaudio.transforms.Resample(sr, 16000)(prompt_wav)
# stream=True:每个 chunk 出来立刻可以推给客户端
for chunk in cosyvoice.inference_zero_shot(
"今天上海多云转晴,气温二十四到三十一度。",
"希望你以后能够做的比我还好呦。", # 参考音频的原文,用于对齐
prompt_speech_16k,
stream=True,
):
torchaudio.save("chunk.wav", chunk["tts_speech"], cosyvoice.sample_rate)
# 这里把 chunk 编码成 opus 推 WebSocket
路线 B:chunk 级流式封装(模型无关)。任何非流式模型都能切成块:按句/按标点切,块间透传上下文。两个坑必须处理:句边界韵律断裂、块间静音过长。做法是 overlap——把上一句结尾的 200ms 作为下一句的 prompt 前缀,让模型"接着读"而不是"重新读"。服务骨架:
import asyncio
from fastapi import FastAPI, WebSocket
app = FastAPI()
def tts_one(sentence: str) -> bytes: # 任意非流式模型,返回 24k PCM
...
async def synthesize(text: str):
for sent in split_sentences(text):
yield tts_one(sent) # 逐句合成即推,不等整段
@app.websocket("/tts")
async def tts_ws(ws: WebSocket):
await ws.accept()
text = await ws.receive_text()
async for chunk in synthesize(text):
await ws.send_bytes(chunk) # 帧头带上 seq 与句子边界标记
await ws.close()
端到端延迟预算的典型量级:VAD 50-100ms + 流式 ASR 150-250ms + LLM 首 token 100-300ms + TTS 首包 150-300ms。TTS 段能抠的点:模型常驻 warmup(首次推理含权重加载和图编译,可能多出几百 ms)、首 chunk 用低步数、文本归一化放到与 LLM 并行的流水线里做。
三、中文 TTS 的特有坑:归一化与多音字
英文数字转读法靠规则就能覆盖九成,中文不行——"二十四"和"两千四百"的读法规则、单位量词的搭配、"一行代码"和"银行行长"的多音,规则表写起来是无底洞。文本归一化 + G2P 是中文 TTS 的前端核心,不是模型的事。
最小归一化实现(整数和百分比,够演示思路):
import re
CN_NUM = "零一二三四五六七八九"
def num_to_cn(n: int) -> str:
if n < 10:
return CN_NUM[n]
if n < 100:
tens = "" if n < 20 else CN_NUM[n // 10] + "十"
return tens + (CN_NUM[n % 10] if n % 10 else "")
return str(n) # 三位以上兜底给 G2P
def normalize(text: str) -> str:
text = re.sub(r"(\d+)%", lambda m: f"百分之{num_to_cn(int(m.group(1)))}", text)
text = re.sub(r"(?<!\d)(\d{1,2})(?!\d)", lambda m: num_to_cn(int(m.group(1))), text)
return text
多音字消歧,2026 年的生产做法是 LLM 前端:把句子丢给一个小模型,输出拼音 + 停顿位置 + 情感标记,一次把 G2P 和韵律控制全做掉:
输入: 重庆的银行行长今天下午到达重庆北站。
输出: {"pinyin": "chong2qing4 ... hang2zhang3 ...",
"pauses": [4, 12], "emotion": "neutral"}
踩坑记录:LLM 前端本身有 30-80ms 延迟,必须与 TTS 合成并行流水线化,否则它自己就成了延迟大头;生产上建议规则优先、LLM 兜底——常见词查表秒出,查不到的再走模型,把 LLM 调用量压到一成以下。
四、声音克隆:zero-shot 的工程真相
原理一句话:参考音频抽说话人表征(嵌入或 speech token),文本条件生成时把音色"带"过去。VALL-E 证明 3 秒可行,CosyVoice2 把跨语种克隆做进了同一条流水线——中文参考音色直接说英文。工程上真正决定成败的不是模型,是参考音频:
- 质量上限:16k 采样、无噪声、3-10 秒、内容与目标文本无关
- 必须预处理:VAD 裁剪 + 响度归一,别拿整段播客去克隆
- 合规:克隆真实人物音色上生产,先过授权关
五、评测:MOS 会骗人,回环才客观
人工 MOS 贵、慢、不稳定。UTMOS 这类预测器论文报告系统级 Spearman 相关约 0.94、句子级只有约 0.5——适合给系统排序,不适合逐句验收。生产三件套:
1. ASR 回环(可懂度):合成 → ASR 转写 → 算 CER。这是回归测试的必备件,改前端、换模型、调步数,跑一遍回环就知道有没有退化:
from faster_whisper import WhisperModel
import numpy as np
model = WhisperModel("small", device="cpu", compute_type="int8")
def cer(ref: str, hyp: str) -> float:
dp = np.zeros((len(ref) + 1, len(hyp) + 1), dtype=int)
for i in range(1, len(ref) + 1):
for j in range(1, len(hyp) + 1):
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1,
dp[i - 1][j - 1] + (ref[i - 1] != hyp[j - 1]))
return dp[len(ref)][len(hyp)] / max(1, len(ref))
ref = "今天上海多云转晴"
segments, _ = model.transcribe("tts_out.wav", language="zh")
hyp = "".join(s.text for s in segments)
print(f"CER = {cer(ref, hyp):.2%}")
注意回环用 whisper small 做基线就够了,别用大模型——它的纠错能力会把 TTS 的口齿不清"修好",测不出问题。
2. SPK-SIM(音色相似度):说话人嵌入(WeSpeaker / ResNet34 系)的余弦相似度,验收克隆效果。经验阈值:同人同设备一般 0.8 上下,克隆验收从 0.6 起,低于 0.5 基本是克隆失败了。
3. RTF 与首包延迟(性能):压测统计合成耗时 / 音频时长,目标 RTF 低于 0.3(给编码、网络留余量);流式场景额外记 TTFB 和 chunk 间隔抖动——chunk 间隔抖动比平均延迟更伤听感,用户听的是"卡不卡",不是"平均多少毫秒"。
六、实战踩坑清单
- chunk 边界爆破音:块间加 10-20ms 交叉淡入淡出,或把上句末尾作下句 prompt 前缀;两招都试过,后者对自然度提升更明显
- 首包慢的隐藏原因:模型 warmup(首次推理)、G2P 未预热、归一化在合成线程里串行做——把这三样都提前,首包能砍掉一截
- 并发上限:TTS 是计算密集任务,多路并发按 RTF × 显存反推,别裸开线程池;FP16 比 FP32 省一半显存,先开 FP16
- 长文本切句:超过 30 秒的合成任务必须前端切句、后端逐句流式返回,等整段合成就别谈延迟了
- 格式统一:内部 24k/16bit PCM,出口按端侧转 opus/PCM;采样率转换只在出口做一次,别在合成路径里反复转
- 预合成缓存:欢迎语、系统播报这类高频固定文本,预合成 + 语义 hash 缓存,命中直接推音频,连模型都不进
小结
2026 年做 TTS 的路线图很清晰:模型选 Flow Matching/DiT 系(F5-TTS、CosyVoice2),流式优先用模型原生能力、次选 chunk 封装,中文必须自建归一化 + G2P 前端,验收靠 ASR 回环 + SPK-SIM + RTF 三件套。语音 Agent 的体验优化顺序建议先攻 TTS——用户对"答得快不快、声音像不像人"的感知,比对 LLM 智商差异敏感得多。合成侧补齐之后,接上 语音 Agent 全链路 和 多模态 VLM 工程,多模态 Agent 的地基就齐了。
面试题:设计实时协作文档系统:多人同时编辑同一份文档怎么不冲突?OT 和 CRDT 各自怎么运作、怎么选?
这是「每日一题」专栏的第十五篇。每天一道面试题,后端 + AI 混合路线,从原理到代码一次讲清。上一篇讲了 接口幂等,今天按轮换回到系统设计方向,聊上一篇预告的题:实时协作文档系统。
CentOS 7 + nerdctl 服务器编译 llama.cpp 完整指南 — 容器化构建与实测
云服务器上搭 LLM 推理环境,最怕的不是模型本身,而是操作系统太老。本文记录在一台 CentOS 7 + nerdctl(containerd 系容器运行时)的服务器上,从裸系统到跑通 llama.cpp 编译、推理、HTTP 服务的完整过程,包括一个可复现的 Dockerfile 和全部踩坑记录。
MCP Server 生产级实战:从 stdio 骨架到 Streamable HTTP 与鉴权部署
2026 年 6 月,MCP 完成「无状态核心」改造,HTTP+SSE 双端点退役;定稿版把远程 server 的 OAuth 2.1 变成硬性要求。协议层的来龙去脉,MCP 协议深度解析 和 无状态核心规范解读 已经讲透。但「读懂了协议」和「写得出一个能上生产的 server」之间还隔着一条沟:stdio 帧格式的坑、工具描述怎么写模型才肯调用、鉴权怎么做、客户端怎么接。本文用一段完整可跑的代码把这条沟填上。
面试题:接口幂等:重复支付、消息重投、请求重试,怎么保证同一操作只生效一次?
这是「每日一题」专栏的第十四篇。每天一道面试题,后端 + AI 混合路线,从原理到代码一次讲清。上一篇讲了 RAG 检索增强生成,今天按轮换回到后端/分布式方向,聊上一篇预告的题:接口幂等。