LiveKit 음성 에이전트 흐름 & 두 SFU 구조 (시각화) 아키텍처

마지막 업데이트 2026-07-24

LiveKit 음성 에이전트 흐름 & 두 SFU 구조 (시각화) 아키텍처: 입력: 01 런타임 결정 — 어떤 음성 방식으로 갈지, 주요 처리 단계: 03 실시간 미디어 & 마이크 오디오 체인, 결과: TL;DR 한 장 요약 흐름
동작 흐름 요약
  1. 입력: 01 런타임 결정 — 어떤 음성 방식으로 갈지
  2. 주요 처리 단계: 03 실시간 미디어 & 마이크 오디오 체인
  3. 결과: TL;DR 한 장 요약
작성일: 2026-07-24 대상: 아동↔핑퐁이(AI) 실시간 음성 대화의 세션 발급 → dispatch → 미디어 → 제어 전 과정 핵심: livekit_cloud_agent · RoomAgentDispatch · 두 SFU 브리지

TL;DR한 장 요약

아동 브라우저가 POST /api/livekit/call로 토큰을 발급받으면, 웹은 AI 워커를 직접 호출하지 않고 토큰 안의 roomConfig.agents(RoomAgentDispatch)로 LiveKit 서버가 ppi-agent 워커를 소환합니다. 큰 프롬프트/설정은 토큰에 싣지 않고 Valkey에 저장 후 configRef(참조 키)만 넘깁니다. 미디어는 LiveKit Cloud SFU를 통해 오가고(mic↑ / TTS↓), 브라우저·워커는 별도 RPC·Data 채널로 턴·VAD·끼어들기를 제어합니다. PPI에는 SFU가 둘(LiveKit Cloud = AI 음성, mediasoup = 수업 중계)이며, 아동 브라우저가 둘을 잇는 브리지입니다.
브라우저 · Web API (apps/web) LiveKit Cloud (SFU / 서버) AI 워커 (apps/livekit-agent) 상태 저장소 (Valkey / S3) mediasoup SFU (apps/socket)

01런타임 결정 — 어떤 음성 방식으로 갈지

세션을 만들기 전 resolveVoiceSessionConfig()가 아동의 대화 모드와 아바타 보이스를 조합해 런타임을 정합니다. LiveKit은 아바타에 Typecast TTS 보이스가 있을 때만 성립하고, 없으면 Realtime으로 폴백합니다.

아동 conversationMode+ 아바타 보이스
resolveVoiceSessionConfig()런타임 분기 판정
livekit 요청 & ttsVoice 있음
runtime = livekitdeployment: livekit_cloud_agent
POST /api/livekit/call 진행 →
ttsVoice 없음 → 폴백
runtime = realtimeOpenAI Realtime WebRTC
tts 모드
runtime = tts
근거resolver.ts:113–165 — 런타임 3종 분기 · VAD 병합 resolveLiveKitVad(). call route는 runtime !== "livekit"이면 409 LIVEKIT_NOT_AVAILABLE 반환.

02세션 수립 & 에이전트 dispatch

웹은 AI 워커를 직접 호출하지 않습니다. 발급하는 JWT 안의 roomConfig.agents(RoomAgentDispatch)에 ppi-agent를 지정해두면, 아동이 룸에 입장하는 순간 LiveKit 서버가 이름으로 워커를 소환합니다. 큰 프롬프트/설정은 토큰에 싣지 않고 Valkey에 저장한 뒤 configRef만 전달합니다.

1브라우저Web APIPOST /api/livekit/calluserId · avatarId · voiceSessionId · activityId · lessonIndex
2Web APIinternal세션 인증 · activity/avatar 검증resolveVoiceSessionConfig → runtime = livekit
3Web APIValkeysetLiveKitSessionConfigSnapshot()모델설정 저장 (SETEX, TTL 7200s)
4ValkeyWeb APIconfigRef 반환키: ppi:livekit:session-config:{STAGE}:{voiceSessionId}
5Web APIinternalcreateLiveKitParticipantToken()JWT + roomConfig.agents=[ppi-agent] · metadata = configRef
6Web API브라우저LiveKitCallDescriptorserverUrl · roomName · participantToken · browserAudioProfile
7브라우저LiveKitroom.connect(token) + publishTrack(mic)
🛰️ 토큰의 RoomAgentDispatch로, 브라우저 입장 시 LiveKit 서버가 agent_name=ppi-agent 워커를 named dispatch
8LiveKitAI 워커job dispatch (agent_name = ppi-agent)
9AI 워커ValkeyconfigRef로 모델설정 로드토큰엔 참조만 — 실제 설정은 Valkey에서 복원
10AI 워커internalAgentSession.start()OpenAI Realtime STT/LLM + Typecast TTS + VAD
11AI 워커브라우저data (topic: agent-event-log)ppi.agent_ready
12브라우저internalwaitUntilAgentReady() 해제→ autoStart · setIsActive(true)
준비 신호워커 _publish_ppi_agent_ready() → 브라우저 RoomEvent.DataReceived 수신 → agentReadiness.markReady(). 10초 내 미수신 시 readiness timeout. agent-name은 웹/워커 양쪽 기본값 ppi-agent로 일치해야 매칭됩니다.
근거app/api/livekit/call/route.ts · livekit-token.ts:56–89 · livekit-client-session.ts:1364–1375 · apps/livekit-agent/agent.py

03실시간 미디어 & 마이크 오디오 체인

아동 마이크는 브라우저 내 WebAudio 체인(게이트 → EQ → 컴프레서 → 리미터)을 거친 뒤 두 갈래로 나뉩니다. 하나는 SFU를 통해 AI 워커로 가는 processedTrack, 다른 하나는 진행자 모니터로 가는 relayProcessedTrack입니다.

🎙️ 아동 마이크MediaStreamTrack
LiveKitAudioChainProcessor · 브라우저 WebAudio
source noise gate EQHP · Peak · LP compressor limiter outputGain
① AI로 가는 경로
agentInputGain듣기 게이트 agentDestination= processedTrack
LiveKit SFU AI 워커STT · LLM
② 진행자 릴레이 경로
relayDestination= relayProcessedTrack
진행자 모니터 릴레이
AI 워커Typecast TTS LiveKit SFUTrackSubscribed audioElement+ onRemoteAudioStream
"듣기 OFF"의 정밀 제어트랙을 mute하지 않고 agentInputGain만 0으로 낮춰 AI로 가는 입력만 차단하고 진행자 릴레이는 유지합니다. 다른 모드는 트랙 mute라 릴레이까지 함께 죽습니다.
주의LiveKit 런타임에서는 shouldAttachRealtimeRemoteAudio()false — Realtime PeerConnection의 remote 오디오를 붙이지 않고, AI 음성은 오직 LiveKit TrackSubscribed 경로로만 재생됩니다. livekit-audio-chain-processor.ts:196–209

04양방향 제어 & 이벤트 채널

연결이 서면 브라우저와 워커는 미디어와 별개로 RPC · Data 채널로 대화합니다. 브라우저는 입력/VAD/끼어들기를 RPC로 제어하고, 워커는 전사·턴 상태를 agent-event-log 토픽으로 흘려보내 브라우저가 VoiceSessionEvent로 정규화해 자막·입력 게이트를 구동합니다.

방향채널 / 메서드용도
브라우저 → 워커RPC agent.setInputEnabledAI로 가는 입력 on/off (워치독 강제 종료 포함)
브라우저 → 워커RPC agent.setVadOptionsVAD 임계 · 엔드포인팅 · 프리픽스 패딩 실시간 변경
브라우저 → 워커RPC agent.setAllowInterruptions아동 발화의 AI 끼어들기 허용 토글
브라우저 → 워커RPC agent.interrupt진행자의 AI 응답 수동 중단
브라우저 → 워커data · topic ppi-user-text텍스트 메시지 주입 (응답 지시 포함 가능)
워커 → 브라우저data · topic agent-event-loguser_stt_segment · agent_transcript_delta/done · user_state_changed · user_turn_committed/dropped → VoiceSessionEvent
워커 → 브라우저ActiveSpeakersChangedAI 발화 시작/종료 감지 → onAiTalkingChange
신뢰 판정isTrustedLiveKitAgentData() = 참가자 kind가 AGENT이고 topic이 agent-event-log일 때만 신뢰. 원격 참가자(워커)가 아직 없으면 RPC는 pending 큐에 쌓였다가 ParticipantConnected 시 재전송됩니다.

05두 SFU 구조 — 브라우저가 브리지

PPI에는 서로 독립된 SFU가 둘 있습니다. AI 음성은 LiveKit Cloud SFU(관리형), 수업 미디어(아동 영상·화면공유·소리 중계)는 자체 mediasoup SFU(apps/socket)가 담당합니다. 두 SFU는 직접 통신하지 않고, 아동 브라우저가 양쪽에 동시에 참여하며 다리를 놓습니다.

SFU ① LiveKit Cloud · 핑퐁이 AI 음성
관리형 SFU — 브라우저와 워커가 같은 룸의 참가자로 붙음
아동 브라우저apps/web · client-guest LiveKit Cloud SFU AI 워커STT · LLM · TTS
mic processedTrack → 워커 STT/LLM Typecast TTS → TrackSubscribed → aiAudioStream
SFU ② mediasoup · 수업 미디어
자체 운영 SFU (apps/socket) — 아동↔진행자 실시간 중계
아동 브라우저apps/web · client-guest mediasoup SFUapps/socket 진행자monitor-dashboard
브라우저 producers ↑ · mic-audio(아동 릴레이) · ai-audio(핑퐁이) · camera-video ↓ 진행자 화면공유 → 아동
브리지 지점LiveKit onRemoteAudioStreamsetAiAudioStream, 오디오 체인의 relayProcessedTrackguestMicStream. 이 둘이 mediasoup producer ai-audio·mic-audio로 재발행됩니다 (use-mediasoup-producer.ts:634,743). 두 SFU는 서로를 알지 못하며 브라우저만 양쪽에 물려 있습니다.
역할 분리LiveKit = AI 대화 전용(턴·VAD·끼어들기 제어) · mediasoup = 수업 중계 전용(영상·화면공유·소리 모니터링). AI 없이도 수업 미디어는 mediasoup로 독립 동작합니다.
이슈 무대이 브리지 재발행 구간은 "모니터 아동 소리만 무음"·ai-audio producer outbound stalled 계열 증상이 발생하는 지점입니다 (LiveKit→브라우저 수신은 정상인데 브라우저→mediasoup 재발행 트랙이 무음).

관련 문서