음성 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 실패를 한 질의씩 넣어 다시 돌린
/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을 만드세요.
참고
- 구간 한 줄:
{"trace_id": 질의 id, "span_id": …, "parent_id": 뿌리의 span_id(뿌리는 null), "name": …, "start_ms": …, "end_ms": …, "status": "ok"|"error"|…, "attrs": {…}}. 뿌리 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
턴 하나 = 추적 하나
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 로 판정한다
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 에 셋의 통과 여부를 적은 /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 을 만드세요.
기준 판은 '지난 배포(가상)' 의 지표입니다. 관문이 막았다면 어느 지표 때문인지 출력에 있습니다 — 그 판을 내보낼지는 사람이 정합니다.