음성 AI 에이전트 — 듣고, 찾고, 말하는 파이프라인
말을 멈춘 뒤 첫 소리까지 — 직렬과 겹침을 재고 예산과 대조한다
목표
듣기부터 말하기까지를 한 줄로 이어 사용자가 느끼는 지연을 잰다. 직렬과 겹침을 같은 질의로 비교하고, 분포와 예산으로 줄일 곳을 찾는다.
왜 중요한가
부품마다 빠르더라도 이어 붙이면 초 단위가 된다. 어디를 줄일지는 '어떻게 이었는가' 와 '어느 단계가 예산을 넘는가' 를 분포로 봐야 안다. 채점기는 기계마다 다른 절대 시간 대신 기록 안의 순서(확정 ≤ 검색 ≤ 첫 토큰 ≤ LLM 끝), 같은 기계에서 잰 두 방식의 비교(겹침 < 직렬), 원자료에서 다시 계산한 백분위·예산 초과를 봅니다. 끝점 지연(오디오 시간)은 채점기가 VAD 를 다시 돌려 대조합니다.
단계
- 질의 12개(
/opt/lab/fixtures/voice/queries/q01~q12.wav)의 끝점(VAD 가 본 마지막 말 끝 + gap 0.5초)과 실제 말 끝(refs.tsv넷째 칸)의 차이를/root/voice/pipeline/endpoint.json에 적으세요. - 확정 → 검색 → LLM(전부) → TTS(전부) 를 잇는
/root/voice/pipeline/pipe.py를 만들어 q01·q03·q05·q07·q09 를 직렬로 돌린 기록을/root/voice/pipeline/serial.jsonl에 적으세요. - 같은 다섯 질의를 LLM 조각이 생기는 대로 TTS 에 넘기는 겹침으로 돌려
/root/voice/pipeline/overlap.jsonl에 적으세요. - 겹친 실행 하나를 크롬 추적 형식으로
/root/voice/pipeline/trace.json에 그리세요. - 답이 있는 질의 10개(q01~q10)를 겹침으로 돌려 사용자 체감 지연(끝점 지연 + 판정 뒤 첫 소리)과 p50·p95 를
/root/voice/pipeline/e2e.json에 적으세요. - 단계별 예산과 측정 중앙값, 예산을 넘은 단계를
/root/voice/pipeline/budget.json에 적으세요. - 파이썬 프로세스의 최대 RSS 와 LLM 서버의 RSS, 파드 메모리 한도를
/root/voice/pipeline/memory.json에 적으세요. - 측정을 모은
/root/voice/pipeline/report.json을 만드세요.
참고
- 시각은 끝점을 0 으로 둔 벽시계 ms 입니다. 스트리밍 ASR 에 소리를 다 넣는 일(사용자가 말하는 동안)은 재지 않고, 꼬리 무음과
input_finished()부터 잽니다. - 기록 칸:
id·asr_ms·retrieve_ms·llm_first_ms·llm_done_ms·first_audio_ms·tts_done_ms·text— 모두 0 에서 잰 누적 시각입니다. - 필요한 부품:
voicekit.models(streaming_asr·tts·Embedder),voicekit.llm.chat_stream, 모듈 5 의 문단 검색, 모듈 7 의 조각 자르기.voice-llm up이 먼저입니다. - 흔한 실수: 오디오 시간과 벽시계를 섞기, 직렬 기록에서 첫 소리를 첫 토큰 근처로 적기, 추적의 ts·dur 를 ms 로 넣기(마이크로초입니다).
- 문서: Trace Event Format · Perfetto UI · llama.cpp server
끝점 지연 — 오디오 시간으로
/opt/lab/fixtures/voice/queries/refs.tsv(이름·원문·발화 시작·발화 끝) 의 질의 12개마다 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) 를 만드세요. 소리를 스트리밍 ASR 에 100 ms 씩 넣은 뒤(재지 않음) 끝점 순간을 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, 첫 토큰부터 합성 끝까지) 막대를 "ph": "X" 사건으로, 첫 소리를 "ph": "i" 사건으로 만든 {"traceEvents": [...]} 를 /root/voice/pipeline/trace.json 에 저장하세요. ts·dur 는 마이크로초입니다.
ms × 1000 = µs. chrome://tracing 이나 ui.perfetto.dev 에 파일을 끌어다 놓으면 LLM 과 TTS 가 겹친 모습이 보입니다.
사용자가 느끼는 지연의 분포
q01~q10 을 겹침으로 돌려 질의마다 endpoint_ms(endpoint.json 의 delay_ms)·first_audio_ms·e2e_ms(둘의 합)를 rows 에 적고, e2e_ms 의 p50_ms·p95_ms(가까운 순위)를 /root/voice/pipeline/e2e.json 에 적으세요.
오디오 시간(끝점 지연)과 벽시계 시간(판정 뒤 첫 소리)을 여기서 처음 더합니다. 표본 10개의 p95 는 가장 큰 값입니다.
예산과 대조한다
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 에 측정이 예산을 넘은 단계 이름을 적은 /root/voice/pipeline/budget.json 을 만드세요.
누적 시각의 차이가 곧 그 단계의 몫입니다. 넘친 단계마다 무엇으로 줄일 수 있는지 — 모델, 프롬프트, 이음새 — 생각해 보세요.
메모리도 예산이다
pipe 로 질의 하나를 돌린 뒤, 이 파이썬 프로세스의 /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 를 적으세요.
앞 단계 파일에서 계산하거나 옮깁니다.