音声 AI エージェント — 聞いて、調べて、話すパイプライン
一つのトレースからすべての指標を — 品質・失敗率・SLO・ゲート
目標
パイプラインのターンごとにスパンを記録し、同じ元データから、遅延分布・WER・応答品質・失敗率を計算してSLOで判定したあと、リグレッションを止めるデプロイゲートを作ります。
なぜ重要なのか
平均遅延とエラー率だけを見ていると、「根拠を通過した誤答」と「静かにうまく切り抜けたエラー」が見えません。指標を同じ元データから計算してはじめて数字同士が合い、ゲートは止めるべきものを入れてみて、はじめて信頼できます。このラボは、ベースラインのパイプラインvoicekit.pipeline.Pipelineを使います。モジュール5・8と同じ流れ(確定 → 書き換え・検索 → しきい値 → JSONの答え → 数字の検査 → 抽出 → TTS)で、run(wav, qid, context, span=기록함수)で段階ごとにスパンを通知し(プレースホルダーは記録関数です)、faults={...}で段階をわざと失敗させられます。採点ツールは、スパン記録からすべての集計数字を再計算して照合します。
ステップ
- 12個のクエリを、12個のトレース(ルートturn + 段階スパン)として
/root/voice/eval/spans.jsonlに残してください。 - 段階別のスパンの長さのp50・p95を、
/root/voice/eval/stages.jsonに書いてください。 - asrスパンのテキストと正解の原文から、クエリごと・全体のWERを
/root/voice/eval/wer.jsonに書いてください。 - 正解表から、事実の正確さ・拒否の正確さ・根拠一致率を
/root/voice/eval/quality.jsonに書いてください。 - ASRの空テキスト・LLMのタイムアウト・TTSの失敗を1つずつのクエリに入れてもう一度動かした
/root/voice/eval/spans_faults.jsonlと、エラーターン・ユーザーの失敗を数えた/root/voice/eval/failures.jsonを作ってください。 - SLOの目標を決めて、測定と比べた
/root/voice/eval/slo.jsonを作ってください。 - ベースラインと現在の指標を比べて、リグレッションなら終了コード1を返す
/root/voice/eval/gate.pyを作ってください。 - 今回の版の指標
/root/voice/eval/metrics.jsonを作り、ベースライン版(/opt/lab/fixtures/voice/eval/baseline.json)でゲートを動かした/root/voice/eval/report.jsonを作ってください。
参考
- スパン1行:
{"trace_id": 질의 id, "span_id": …, "parent_id": 뿌리의 span_id(뿌리는 null), "name": …, "start_ms": …, "end_ms": …, "status": "ok"|"error"|…, "attrs": {…}}(プレースホルダーはクエリidとルートのspan_idで、ルートのparent_idはnullです)。ルートturnのattrsにsay・source・first_audio_msを、statusにターンの結果(ok・refused・degraded・reprompt・failed)を入れます。 - 正解表:
/opt/lab/fixtures/voice/rag/queries.jsonlのanswerable・facts、正解の原文:/opt/lab/fixtures/voice/queries/refs.tsv。 - パーセンタイルは最近傍順位(並べ替えたあと⌈p/100·n⌉番目)です。
- よくある間違いは、拒否した答えのある質問を事実の正確さの分母から除くこと(分母は答えのある質問すべてです)、エラーが出たターンをすべてユーザーの失敗として数えること、ゲートの遅延の許容幅を絶対値にすることです。
- ドキュメント: OpenTelemetry — Traces・Google SRE Book — Service Level Objectives
1ターン = 1トレース
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に1行ずつ書いてください。
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のうち1つが含まれていれば的中。大文字小文字は区別しません)、refusal_accuracy(答えのない質問のうち、statusがrefusedの割合)、grounded_rate(拒否しなかった答えのうち、sayの数字がすべてsourceのドキュメントにある割合)を、/root/voice/eval/quality.jsonに書いてください。
3つの数字を並べると、根拠一致率が高くても事実の正確さが低いことがあると見えてきます。数字は、re.findall(r"\d+(?::\d+)?", …)で取り出します。
エラーとユーザーの失敗を分ける
Pipeline(faults={"asr_empty": {"q09"}, "llm_timeout": {"q03"}, "tts_error": {"q07"}})でステップ1をもう一度動かして/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で判定する
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に3つの合否を書いた/root/voice/eval/slo.jsonを作ってください。
目標は、ユーザーが体験することで書きます。失敗率は、失敗を入れた実行から取ります。入れた失敗が目標を破るかを見るのが、このステップの要点です。未達が出ても、採点は判定が合っているかだけを見ます。
リグレッションを止めるゲート
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)を書き、python3 gate.py /opt/lab/fixtures/voice/eval/baseline.json metrics.jsonの終了コードをgate_exitに、出力行をgate_outputに入れた/root/voice/eval/report.jsonを作ってください。
ベースライン版は「前回のデプロイ(仮想)」の指標です。ゲートが止めた場合は、どの指標が原因かが出力にあります。その版を出すかどうかは、人が決めます。