LabHub
开始
学习 学习路径 课程

语音 AI 智能体 — 会听、会查、会说的流水线

从说完话到首个声音 — 测量串行与重叠并对照预算

在 LabHub 中继续学习

目标

把从听到说连成一条线,测量用户感受到的延迟。用同样的查询比较串行与重叠,并通过分布和预算找出该缩短的地方。

为什么重要

即使各个部件都很快,连起来也是以秒为单位的。要知道该缩短哪里,就必须通过分布来看“怎样连接的”以及“哪个阶段超出了预算”。评分器不看因机器而异的绝对时间,而是看记录内部的顺序(最终结果 ≤ 检索 ≤ 第一个 token ≤ LLM 结束)、在同一台机器上测得的两种方式的比较(重叠 < 串行),以及从原始数据重新计算的百分位数和预算超出情况。端点延迟(音频时间)则由评分器重新运行 VAD 来核对。

步骤

  1. 把 12 条查询(/opt/lab/fixtures/voice/queries/q01~q12.wav)的端点(VAD 看到的最后话尾 + gap 0.5 秒)与实际话尾(refs.tsv 第四列)之差,写入 /root/voice/pipeline/endpoint.json。
  2. 创建把最终结果 → 检索 → LLM(全部)→ TTS(全部)连接起来的 /root/voice/pipeline/pipe.py,把串行运行 q01、q03、q05、q07、q09 的记录写入 /root/voice/pipeline/serial.jsonl。
  3. 把同样的五条查询,按 LLM 一有片段就交给 TTS 的重叠方式运行,写入 /root/voice/pipeline/overlap.jsonl。
  4. 把一次重叠运行用 Chrome 跟踪格式画入 /root/voice/pipeline/trace.json。
  5. 把有答案的 10 条查询(q01 到 q10)按重叠方式运行,把用户体感延迟(端点延迟 + 判定后的第一个声音)和 p50、p95 写入 /root/voice/pipeline/e2e.json。
  6. 把各阶段的预算、测得的中位数以及超出预算的阶段,写入 /root/voice/pipeline/budget.json。
  7. 把 Python 进程的最大 RSS、LLM 服务器的 RSS 和 Pod 内存限制,写入 /root/voice/pipeline/memory.json。
  8. 创建汇总了测量结果的 /root/voice/pipeline/report.json。

参考

端点延迟——用音频时间

对 /opt/lab/fixtures/voice/queries/refs.tsv(名称、原文、语句开始、语句结束)中的每一条查询,把 silero VAD(threshold 0.5 · min_silence 0.1 · min_speech 0.1)最后一个区间的结束写作 vad_end_s,加上 gap 0.5 秒的值写作 endpoint_s,把 (endpoint_s − 발화 끝) × 1000(占位符为语句结束时刻)写作 delay_ms,写入 /root/voice/pipeline/endpoint.json(gap_s、rows)。

所有的值都是声音文件内部的时刻——这是机器再快也不会减少的部分,所以要单独测量。实际话尾是合成语音摆放的位置,所以是准确的。

串行连接

在 /root/voice/pipeline/pipe.py 中创建 run(wav, mode)。把声音以每次 100 ms 输入流式 ASR(不测量)后,把端点那一刻定为 0,依次记录最终结果(asr_ms)→ 用去掉口头赘词的查询做段落 top-3 检索(retrieve_ms)→ 附上找到的段落的 LLM 流(第一个片段 llm_first_ms,结束 llm_done_ms,max_tokens 为 60)→ 通过 TTS 接续,写下第一个声音(first_audio_ms)和合成结束(tts_done_ms)并返回。当 mode 为“serial”时,接收完整个回答后一次性合成整体。串行运行 q01、q03、q05、q07、q09,加上 id、text(最终结果),写入 /root/voice/pipeline/serial.jsonl。

模型(ASR、TTS、嵌入)和 KB 嵌入,只在加载模块时创建一次。如果用线程运行 TTS,下一步的重叠中就可以直接使用。在串行中,第一个声音就是合成结束。

重叠运行

让 run(wav, 'overlap') 随着 LLM 文字的累积,切出片段(句子结尾 · 超过五个词的逗号之前 · 十二个词)并交给 TTS 线程。把同样的五条查询按重叠方式运行,写入 /root/voice/pipeline/overlap.jsonl。评分:第一个声音的中位数必须比串行更快。

只有串行与重叠的差别仅仅是“什么时候交给 TTS”这一点,比较才公平。请在同一个 run 函数中用 mode 区分。

画成条形

用 overlap.jsonl 的第一条记录,把 asr_final、retrieve、llm(都是 tid 1)和 tts(tid 2,从第一个 token 到合成结束)条形做成 "ph": "X" 事件,把第一个声音做成 "ph": "i" 事件,将 {"traceEvents": [...]} 保存到 /root/voice/pipeline/trace.json。ts 和 dur 的单位是微秒。

ms × 1000 = µs。把文件拖放到 chrome://tracing 或 ui.perfetto.dev 中,就能看到 LLM 与 TTS 重叠的样子。

用户感受到的延迟的分布

把 q01 到 q10 按重叠方式运行,对每条查询,在 rows 中写入 endpoint_ms(endpoint.json 的 delay_ms)、first_audio_ms、e2e_ms(两者之和),并把 e2e_ms 的 p50_ms、p95_ms(最近秩)写入 /root/voice/pipeline/e2e.json。

音频时间(端点延迟)和挂钟时间(判定后的第一个声音)在这里是第一次相加。10 个样本的 p95 就是最大的那个值。

与预算对照

生成 /root/voice/pipeline/budget.json:在 budget_ms 中写下五个阶段(endpoint、asr_final、retrieve、llm_first_token、first_audio_after_token)的预算(正数,总和不超过 2500),在 measured_p50_ms 中写下中位数——endpoint 取 endpoint.json 的 delay_ms,其余从 overlap.jsonl 中取 asr_ms · (retrieve_ms − asr_ms) · (llm_first_ms − retrieve_ms) · (first_audio_ms − llm_first_ms)——并在 over_budget 中写下测量值超出预算的阶段名称。

累计时刻之差,就是那个阶段所占的份额。对每个超出的阶段,想一想可以用什么来缩短——模型、提示词,还是衔接。

内存也是预算

用 pipe 运行一条查询后,把这个 Python 进程的 /proc/self/status 中 VmHWM(最大 RSS)写作 python_peak_mb,把用 pgrep -f llama-server 找到的服务器的 VmRSS 写作 llm_rss_mb,把两者之和写作 total_mb,把 /sys/fs/cgroup/memory.max 的限制写作 limit_mb(不是数字则为 null),写入 /root/voice/pipeline/memory.json。

VmHWM、VmRSS 以 kB 为单位——除以 1024 换成 MB。限制是以字节为单位的。两个进程使用了限制的百分之几,就是能否再加载一个模型的答案。

报告

在 /root/voice/pipeline/report.json 中写入 serial_first_audio_p50_ms、overlap_first_audio_p50_ms(两个 jsonl 的 first_audio_ms 中位数)、e2e_p50_ms、e2e_p95_ms、over_budget、total_mb。

从前面步骤的文件中计算或抄写。