마지막 업데이트 2026-09-16
아동이 AI 스텝에 들어가면, 그 몇 초 사이에 방이 생기고 · 일감이 만들어지고 · 대기하던 AI 일꾼이 그 방에 입장합니다. 방 생성 · 워커 배정 · job · AI 세션 연결의 전 구간을 그림으로 봅니다.
시간은 위에서 아래로 흐릅니다. 웹서버는 ③까지만 관여하고, 이후 대화는 브라우저와 워커가 LiveKit 방 안에서 직접 주고받습니다.
아동이 AI 스텝에 들어가면 브라우저가 POST /api/livekit/call을 부릅니다. 웹서버는 로그인 세션과 활동·아바타가 서로 맞는지 확인한 뒤, 방 이름을 문자열로 조립합니다.
ppi-{userId}-{avatarId}-{voiceSessionId}활동 지침(프롬프트)·TTS 목소리·VAD 설정을 한 덩어리(modelConfig)로 만들어 Valkey에 2시간(7200초) 짜리로 저장합니다. 토큰에는 원문 대신 사물함 번호만 담습니다. 지침이 수만 자라 토큰에 실을 수 없기 때문입니다.
ppi:livekit:session-config:{stage}:{voiceSessionId}
웹서버가 돌려주는 토큰은 단순한 출입증이 아닙니다. 들어갈 방과 권한, 사물함 번호, 그리고 “이 방엔 ppi-agent를 불러줘”라는 호출벨이 함께 들어 있습니다.
RoomAgentDispatch)이 없으면 방은 만들어지지만 AI는 영원히 들어오지 않습니다.Agent worker는 ECS/Fargate에서 항상 떠 있고, 부팅하면서 LiveKit 서버에 ppi-agent라는 이름으로 등록합니다. 음성 감지기(Silero VAD)까지 미리 올려둔 빈 프로세스 2개를 품고 대기합니다. 방이 생기면 그중 여유 있는 워커에게 일감이 갑니다.
_ppi_agent_load). CPU는 늦게 올라서, 그대로 두면 한 워커가 방을 여러 개 떠안습니다.LIVEKIT_AGENT_NAME=ppi-agent-local로 이름을 나눕니다.
job이 시작될 때 워커가 아는 것은 방 이름뿐입니다. 그래서 먼저 방의 참가자 목록을 조회해 아동 토큰에 적힌 사물함 번호를 찾고, Valkey에서 이번 수업용 설정을 꺼냅니다. 그 설정의 pipeline_mode 한 줄이 AI의 몸을 결정합니다.
순서가 중요합니다. AI 세션을 먼저 켜 둔 뒤(session.start()) 방에 접속합니다(ctx.connect()). 접속하자마자 아동 목소리가 쏟아지는데 받을 준비가 안 돼 있으면 첫마디를 놓치기 때문입니다.
입장 후에는 진행자가 누르는 버튼들 — 듣기 켜기/끄기, 끼어들기 허용, 활동 전환, 강제 중단 — 을 받을 RPC 창구를 열고, 마지막으로 ppi.agent_ready를 방에 방송합니다.
브라우저는 그 방송을 그냥 믿지 않습니다. 직접 “준비됐어?”를 던지고, 돌아온 답이 지금 이 시도의 것인지 네 가지 값으로 대조합니다. 재접속이 잦은 환경에서 이전 시도의 늦은 응답을 진짜로 착각하면, 이미 끝난 세션으로 대화를 시작하게 됩니다.
아동이 나가면(participant_disconnected) job이 닫히고, 워커의 두 자리 중 하나가 다시 빕니다. 다음 수업이 그 자리로 들어옵니다. Valkey에 넣어둔 설정은 2시간 뒤 알아서 사라집니다.
| 질문 | 답 |
|---|---|
| 방은 누가 만드나? | 아무도 만들지 않습니다. 토큰에 적힌 이름으로 아동이 접속하는 순간 생깁니다. |
| AI는 누가 부르나? | 웹서버가 아니라 LiveKit 서버입니다. 토큰 안의 RoomAgentDispatch를 보고 이름이 같은 워커에게 job을 넘깁니다. |
| 지침은 어디로 가나? | 토큰이 아니라 Valkey로 갑니다. 토큰엔 사물함 번호(configRef)만 있고, 워커가 직접 열어 봅니다. |
| 무엇 | 위치 |
|---|---|
| 세션 요청 · 권한 확인 | apps/web/app/api/livekit/call/route.ts:85 POST() |
| 방 이름 생성 | apps/web/app/api/livekit/call/route.ts:71 createRoomName() → :212 |
| modelConfig 조립 | apps/web/lib/voice-agent/livekit-token.ts:125 buildLiveKitModelConfig() |
| 설정 사물함 저장 (TTL 7200s) | apps/web/lib/voice-agent/livekit-session-config-store.ts:201 setLiveKitSessionConfigSnapshot(), 키 규칙 :148 |
| 토큰 · 호출벨 | apps/web/lib/voice-agent/livekit-token.ts:89 createLiveKitParticipantToken(), :113 RoomConfiguration({ agents: [RoomAgentDispatch] }) |
| 디스패치 이름 | apps/web/lib/voice-agent/livekit-token.ts:24 LIVEKIT_AGENT_NAME (기본 ppi-agent) |
| 워커 등록 · 빈 프로세스 2개 | apps/livekit-agent/agent.py:370 AgentServer(...), :377 prewarm() |
| 배정 기준 (CPU 아님) | apps/livekit-agent/agent.py:355 _ppi_agent_load(), 상한 PPI_AGENT_MAX_JOBS_PER_WORKER 기본 2 |
| job 진입점 | apps/livekit-agent/agent.py:4493 @server.rtc_session(agent_name=PPI_AGENT_NAME) |
| 사물함 열기 | apps/livekit-agent/agent.py:384 _fetch_model_config() — 참가자 metadata → Valkey |
| 세션 기동 후 입장 순서 | apps/livekit-agent/agent.py:4563 session.start() → :4567 ctx.connect() → :4576 ppi.agent_ready |
| 브라우저 접속 · 마이크 발행 | apps/web/lib/voice-agent/livekit-client-session.ts:3609 room.connect() |
| ready 악수 (브라우저) | apps/web/lib/voice-agent/livekit-readiness-handshake.ts:112, 재시도 livekit-client-session.ts:3625 |
| ready 악수 (agent) | apps/livekit-agent/readiness_transport.py:46 build_ready_response() |
| 호출 시작점 (게스트) | apps/web/entities/guest-session/model/use-ai-session.ts:270 |