语音 Agent 最让人崩溃的不是回答质量差,而是抢话。用户说"我想订那个……就是上次去过的那家……",话还没说完,AI 已经兴冲冲回了一句"好的,请问您想订什么?"——半句话换来一个不知所云的回答。更荒谬的场景:用户咳嗽一声,AI 开始正经回答一个不存在的问题;用户清了下嗓子,AI 说"好的,我来帮您处理"。
这些问题的根源不是 LLM 不聪明,而是判停(turn endpointing)做错了——系统误把"人还在想"当成了"人说完了",把噪声当成了语音。TurnSense 用 47M 参数的专精模型,在这个任务上打赢了 7B 级通用模型,并且已经开源。
判停为什么这么难
语音 Agent 的实时交互流程大致是:音频流 → VAD(检测有没有声音)→ 判停(检测人说完了没有)→ LLM 生成回复 → TTS 播报。
多数系统的做法极其粗暴:
- 固定静默阈值:检测到 600ms 无声音就判定说话结束。问题在于,人在思考、犹豫时经常停顿 500ms–1500ms,"那个……就是……"这种句式里全是合法停顿。
- 把 VAD 当判停:VAD 只检测"有没有人声",不区分"说完一句话的沉默"和"一句话中间的沉默"。
- 用大模型做端到端:7B 甚至更大模型直接吃音频做判断,参数量大、延迟高,而且通用模型对"咳嗽算不算说完"这种边界 case 缺乏针对性训练。
实际场景里的干扰远比想象中多:
| 干扰类型 | 典型表现 | 简单阈值系统的反应 |
|---|---|---|
| 思考停顿 | "帮我查一下……嗯……北京到上海的" | 抢话,只收到"帮我查一下" |
| 口头填补 | "那个、就是、然后" | 误判为句末 |
| 咳嗽/清嗓 | 非语言生理声音 | 当成指令处理 |
| 呼吸声 | 句间正常换气 | 短停顿触发回复 |
一个专门做判停的小模型,核心优势就是只聚焦这一个决策:这段音频后面的人声,是"话还在说"还是"话已说完"?
47M 为什么能赢 7B
通用大模型的参数分散在语法、语义、常识、推理等无数能力上,真正用于"判停"的参数比例极小。而 TurnSense 把 47M 参数全部压在单一任务上,效果反而更精准——这和 BERT 时代"小模型专精任务胜过大模型通用推理"的逻辑一致。
具体来说,TurnSense 的设计思路:
- 输入是音频片段的声学特征(不是文本转写,避免 ASR 级联误差),模型直接从声学信号判断"是否句末"。
- 训练数据覆盖了真实对话中的全部边界 case:犹豫停顿、口头填补词、咳嗽清嗓、呼吸声、电话噪声等,每个 case 都有明确标注"这不是句末"。
- 推理延迟极低:47M 模型在 CPU 上即可实时运行,不需要 GPU,适合部署在语音 Agent 的流式处理管线中。
7B 模型做同样任务时,要么需要先把音频转文本再判断(引入 ASR 延迟和错误),要么直接吃音频但推理耗时远超实时要求。参数多不等于判停准,就像请一个教授判断"这句话说完了吗"不一定比请一个电台主持人更快更准。
实战:把 TurnSense 接入语音 Agent
下面给出一个最小可运行的集成示例,展示如何在流式音频管线中使用 TurnSense 做判停决策。假设你已有 VAD 模块检测出语音片段,TurnSense 负责对每个片段做"是否句末"的最终判断。
安装与模型加载
# 克隆仓库并安装依赖
git clone https://github.com/TurnSense/TurnSense.git
cd TurnSense
pip install -r requirements.txt
# 下载预训练模型权重(约 180MB)
python download_model.py --model turnsense-base
流式判停集成示例
import numpy as np
from turnsense import TurnSenseModel, AudioStreamer
# 加载模型 —— 47M 参数,CPU 即可实时推理
model = TurnSenseModel.from_pretrained("turnsense-base")
model.eval()
# 模拟流式音频输入:每次收到 200ms 的音频 chunk
CHUNK_MS = 200
SAMPLE_RATE = 16000
CHUNK_SAMPLES = int(SAMPLE_RATE * CHUNK_MS / 1000)
# 状态管理:累积音频,在判停后清空
audio_buffer = np.zeros(0, dtype=np.float32)
silence_chunks = 0 # 连续静音 chunk 计数(VAD 判定)
MAX_SILENCE_BEFORE_QUERY = 5 # 1s 静音后才考虑判停,避免过早触发
def on_audio_chunk(chunk: np.ndarray, is_speech: bool):
"""每个 200ms 音频 chunk 的回调,is_speech 由 VAD 提供"""
global audio_buffer, silence_chunks
if is_speech:
audio_buffer = np.concatenate([audio_buffer, chunk])
silence_chunks = 0 # 有声音就重置静音计数
else:
silence_chunks += 1
# 静音不够长,不触发判停
if silence_chunks < MAX_SILENCE_BEFORE_QUERY:
return
# 静音已够长,但之前累积的音频可能含句中停顿
# 关键一步:让 TurnSense 判断"是否真的说完了"
if len(audio_buffer) > 0:
is_end_of_turn = model.predict_end_of_turn(
audio=audio_buffer,
trailing_silence_ms=silence_chunks * CHUNK_MS,
)
if is_end_of_turn:
# 确认句末 → 把完整音频交给 ASR + LLM
print(f"[TurnSense] 检测到句末,音频长度 {len(audio_buffer)/SAMPLE_RATE:.1f}s")
send_to_asr_and_llm(audio_buffer)
audio_buffer = np.zeros(0, dtype=np.float32)
silence_chunks = 0
else:
# 不是句末 → 继续等待,用户可能还在想
print("[TurnSense] 停顿但非句末,继续等待……")
def send_to_asr_and_llm(audio: np.ndarray):
"""判停确认后,才做 ASR 转写 + LLM 生成"""
# 这里接入你的 ASR 和 LLM 服务
text = asr_transcribe(audio)
response = llm_generate(text)
tts_play(response)
# ---- 模拟测试:含犹豫停顿的语音 ----
# 生成 3s 语音 + 1.2s 停顿 + 2s 语音,模拟"我想订那个……北京到上海的"
streamer = AudioStreamer(chunk_ms=CHUNK_MS, sample_rate=SAMPLE_RATE)
streamer.simulate_turn_with_pause(
speech_before_pause_ms=3000,
pause_ms=1200, # 1.2s 停顿——简单阈值会误判为句末
speech_after_pause_ms=2000,
cough_at_ms=800, # 800ms 处插入一声咳嗽
)
streamer.run(on_audio_chunk)
运行结果预期:
[TurnSense] 停顿但非句末,继续等待…… ← 1.2s 停顿未被误判
[TurnSense] 检测到句末,音频长度 5.0s ← 等用户真正说完才触发
如果换成固定 600ms 阈值方案,同样的音频会在 3s + 0.6s 处就触发回复,只拿到半句话。
关键参数调优
# TurnSense 还支持更精细的控制
result = model.predict_end_of_turn(
audio=audio_buffer,
trailing_silence_ms=1200,
# 可选参数:对话上下文信息(提升判断准确率)
turn_count=3, # 当前是第几轮对话
avg_turn_duration_ms=4000, # 该用户平均句长(模型会参考)
# 置信度阈值:低于此值则返回"不确定",可选择再等一个 chunk
confidence_threshold=0.85,
)
print(f"判停结果: is_end={result.is_end}, 置信度={result.confidence:.2f}")
# 输出: 判停结果: is_end=False, 置信度=0.92
confidence_threshold 是一个实用调优点:设高了更保守(减少抢话,但可能增加等待延迟),设低了更激进(响应快,但容易抢话)。电话客服场景建议 0.85–0.90,闲聊场景可以放宽到 0.75。
部署考量与取舍
什么时候该用 TurnSense,什么时候不该:
- 该用:实时语音对话 Agent(电话客服、语音助手、车载交互),用户说话有犹豫、停顿、口头填补词,环境有咳嗽/噪声干扰。
- 不该用:短指令场景("开灯""播放音乐"),指令极短且几乎无停顿,简单 300ms 阈值就够了;或者纯文本对话场景,根本不需要判停。
部署形态选择:
| 形态 | 适用场景 | 延迟 |
|---|---|---|
| 本地 CPU 推理 | 单路通话、嵌入式设备 | < 30ms/chunk |
| 服务端批量推理 | 多路并发客服系统 | 需加 batching 优化 |
| 边缘 GPU | 高并发低延迟要求 | < 10ms/chunk |
和现有管线的拼接位置:TurnSense 应放在 VAD 之后、ASR 之前。VAD 过滤纯静音段,TurnSense 在"有声音 → 声音暂停"的时刻做最终裁决,只有判停确认才把音频送入 ASR。这样 ASR 拿到的是完整句子,转写准确率也跟着提升。
一个常见误区:有人想用 LLM 的文本输入做判停——先 ASR 实时转写,看到文本流出现句号就判定句末。问题在于 ASR 本身就会在停顿处插入句号,这等于把 ASR 的错误直接传递给了判停决策,形成级联失误。声学判停必须独立于文本链路。
上手清单
- 评估你当前系统的抢话率:录 50 段真实用户对话,统计"AI 在用户未说完时开始回复"的次数。如果超过 10%,判停模块值得替换。
- 跑 TurnSense 的 benchmark:仓库内置了标准测试集(含犹豫、咳嗽、清嗓等标注数据),直接对比你现有方案的准确率。
- 替换判停模块:保持 VAD 和 ASR 不动,只把"静音超阈值 → 触发回复"的逻辑换成"静音超阈值 → TurnSense 判断 → 确认才触发"。
- 调 confidence_threshold:先用默认 0.85 上线,收集真实数据后根据抢话率/等待时长做微调。
- 监控两个指标:抢话率(不该触发却触发的比例)和额外等待延迟(用户说完后系统多等了多久才回复)。两者是跷跷板关系,阈值调优就是找平衡点。
47M 参数打赢 7B,不是魔法,是任务专精的必然结果。判停不需要理解语义,不需要生成文本,只需要回答一个二元问题:这个人,说完了吗? 把这个问题交给只做这一件事的模型,比让一个什么都懂的大模型分心来判断,靠谱得多。