LabHub
开始
学习 学习路径 课程

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

从一条追踪得出所有指标 — 质量·失败率·SLO·关卡

在 LabHub 中继续学习

目标

记录流水线每一轮的跨度,从同一份原始数据中计算延迟分布、WER、回答质量和失败率,用 SLO 判定,然后制作阻止退化的发布门禁。

为什么重要

只看平均延迟和错误率,就看不到“通过了依据检查的错误回答”和“悄悄顺利处理掉的错误”。指标必须从同一份原始数据中计算,数字才能吻合;而门禁必须放进该被阻止的东西试过,才能被信任。本实验使用基线流水线 voicekit.pipeline.Pipeline——与模块 5、8 同一条主线(确认 → 改写与检索 → 阈值 → JSON 回答 → 数字检查 → 提取 → TTS),用 run(wav, qid, context, span=기록함수)(占位符为记录函数)把各阶段的跨度告知出来,并可以用 faults={...} 故意让某个阶段失败。评分器会从你的跨度记录中重新计算所有汇总数字并核对。

步骤

  1. 把 12 条查询作为 12 条跟踪(根 turn + 各阶段跨度)写入 /root/voice/eval/spans.jsonl。
  2. 把各阶段跨度长度的 p50、p95 写入 /root/voice/eval/stages.json。
  3. 用 asr 跨度中的文字和正确答案原文,把逐查询 WER 和整体 WER 写入 /root/voice/eval/wer.json。
  4. 用标准答案表,把事实准确率、拒答准确率和依据一致率写入 /root/voice/eval/quality.json。
  5. 分别对一条查询注入 ASR 空文本、LLM 超时、TTS 失败后重新运行,生成 /root/voice/eval/spans_faults.jsonl,以及统计了错误轮次和用户失败的 /root/voice/eval/failures.json。
  6. 确定 SLO 目标,并生成与测量值比较的 /root/voice/eval/slo.json。
  7. 创建比较基线与当前指标、出现退化时以退出码 1 结束的 /root/voice/eval/gate.py。
  8. 生成这一版的指标 /root/voice/eval/metrics.json,并生成用基线版本(/opt/lab/fixtures/voice/eval/baseline.json)运行了门禁的 /root/voice/eval/report.json。

参考

一轮 = 一条跟踪

在 voice-llm up 之后,用 Pipeline() 运行 12 条查询(按 /opt/lab/fixtures/voice/rag/queries.jsonl 的顺序,追问的 context 是前一条查询的最终文字),收集通过 span 钩子收到的各阶段跨度,并为每一轮添加根跨度 turn(start 为 0,end = 子跨度结束时间的最大值,status = 这一轮的结果,attrs 中包含 say、source、mode、first_audio_ms、errors),每行一条写入 /root/voice/eval/spans.jsonl。

span 钩子以 span(name, start_ms, end_ms, status, attrs) 的形式被调用。先确定根的 span_id,再把它用在子跨度的 parent_id 上。uuid.uuid4().hex 是简单的 id。

各阶段的分布

把 spans.jsonl 中的子跨度(不含根)按名称归集,把长度(end − start)的 n、p50_ms、p95_ms(最近秩)写入 /root/voice/eval/stages.json。

p95 最长的阶段,就是最常让用户等待的地方。要看的不是平均值,而是分布的尾部。

与服务相似的数据上的 WER

用 asr 跨度的 attrs.text 和 /opt/lab/fixtures/voice/queries/refs.tsv 中的原文,把逐查询 WER 写在 per_query 中,把错误之和 ÷ 单词数之和写在 wer 中,生成 /root/voice/eval/wer.json(小写、整理标点,保留撇号)。

可以直接拿模块 3 的 wer.py 来用。请与同一个模型在 LibriSpeech 上只错 1% 多一点的情况比较一下。

回答质量——依据一致不等于准确

用根跨度和标准答案表,把 fact_accuracy(以所有有答案的问题为分母,没有拒答、且 say 中包含 facts 之一就算命中——忽略大小写)、refusal_accuracy(没有答案的问题中 status 为 refused 的比例)、grounded_rate(未拒答的回答中,say 里的数字全部都在 source 文档里的比例)写入 /root/voice/eval/quality.json。

把三个数字并排放在一起,就能看到依据一致率很高,事实准确率也可能很低。数字用 re.findall(r"\d+(?::\d+)?", …) 提取。

区分错误与用户失败

用 Pipeline(faults={"asr_empty": {"q09"}, "llm_timeout": {"q03"}, "tts_error": {"q07"}}) 重新运行第 01 步,写入 /root/voice/eval/spans_faults.jsonl,并在 /root/voice/eval/failures.json 中写入 turns、error_turns(子跨度中有 status 为 error 的轮次数)、user_failures(根 status 为 failed 或 reprompt 的轮次数)、user_failure_rate、by_type。

LLM 挂掉的那一轮,会读出检索到的段落中的句子(degraded),所以用户能听到答案——是错误,但不是失败。TTS 挂掉时,用户听到的是沉默。

用 SLO 来判定

生成 /root/voice/eval/slo.json:在 objectives 中确定 p95_first_audio_ms、fact_accuracy_min(0.5 以上)、user_failure_rate_max(0.1 以下),在 measured 中写入 spans.jsonl 根跨度的 first_audio_ms p95、quality.json 的 fact_accuracy、failures.json 的 user_failure_rate,在 pass 中写入这三项是否达标。

目标要按用户的体验来写。失败率来自注入了失败的运行——看注入的失败是否破坏目标,是这一步的要点。即使出现未达标,评分也只看判定是否正确。

阻止退化的门禁

创建以 python3 gate.py 기준.json 현재.json(占位符依次为基线文件和当前文件)方式调用的 /root/voice/eval/gate.py。如果 wer 上升超过 0.02,或 fact_accuracy 下降超过 0.10,或 p95_first_audio_ms 比基线上升超过 20%,或 user_failure_rate 上升超过 0.05,就打印原因并以退出码 1 结束,否则以 0 结束。评分器会用隐藏的基线与当前组合做双向测试。

为每个指标建一张(名称,变差的方向,容差,是否按比例)的表,规则就一目了然。变好的,无论好了多少都算通过。

把这一版放进门禁

在 /root/voice/eval/metrics.json 中写入 wer(wer.json)、fact_accuracy(quality.json)、p95_first_audio_ms(slo.json 的 measured)、user_failure_rate(因为是平时运行,所以是 0.0),并生成 /root/voice/eval/report.json,其中 gate_exit 放 python3 gate.py /opt/lab/fixtures/voice/eval/baseline.json metrics.json 的退出码,gate_output 放输出的行。

基线版本是“上一次发布(虚构)”的指标。如果门禁阻止了,输出中会写明是因为哪个指标——是否发布那一版,由人来决定。