이윤준 8회기 종료멘트 선발화 자동전환 실패
— 스텝 미매칭 원인 분석 분석 완료 · 수정 미적용
마지막 업데이트 2026-09-12
결론
5명 친구중에 고르기)의 종료멘트를 아직 스텝0(대화 2)에 있을 때 미리 발화했고, 자동전환 판정은 발화 종료 시점의 현재 스텝 문구만 검사하기 때문에 미매칭으로 폐기됐다. LiveKit 완료 이벤트는 정상 도착했고 checkPhrase 정규화도 정상이었다. 즉 이벤트 누락이 아닌 순수 미매칭이다.진행자 화면과의 괴리 — 하이라이트는 판정이 아니다. 진행자 캡처 순간(모니터 타이머 11:39 → cumulativeElapsedSeconds 역산 ≈20:12:24)은 이미 스텝1 진행 중이고 화면에는 종료멘트가 하이라이트되어 있다. 그래서 “스텝1에서 종료멘트가 매칭됐는데 안 넘어간다”로 보인다. 그러나 그 발화는 20:12:02에 스텝0에서 나온 것이고 판정은 20:12:09에 스텝0 문구로 이미 끝나 폐기됐다. 하이라이트는 transcript-chat-area.tsx:256이 현재 스텝 phrases로 과거 assistant 메시지 전체를 매번 재계산하는 별개 코드이며, highlight-keywords.tsx는 임계값 없이 키워드 1개만 걸려도 칠한다. 즉 스텝이 바뀌는 순간 옛 발화가 소급 하이라이트된다 — 판정 결과와 무관하다.
시간순 인과 다이어그램
아동 2분 무응답 (스텝0 = 대화 2)
→ 진행자 무응답 필러 "음..." 전송 (20:12:01.252, responseInstructions 없음)
→ agent는 활동 단위 instruction 1개만 보유 (스텝 인식 없음)
→ 내용 없는 입력을 "마무리 신호"로 해석 → 스텝1 종료멘트를 스텝0에서 선발화 (20:12:02.151)
→ transcript는 responseDataRef에 보관만 (판정 없음)
→ agent_response_completed 정상 도착 (20:12:09.723) → checkPhrase(현재=스텝0 문구 3개) = 0/3 미매칭
→ responseDataRef.delete() 로 폐기, "AI speech ended…" 로그 없음, 자동전환 없음
→ 진행자 수동 전환 스텝0→스텝1 (20:12:14.196), config를 스텝1 문구로 교체
→ 그러나 AI는 종료멘트를 이미 소진 → 스텝1에서 자발적으로 재발화하지 않음
→ 진행자가 프리셋 "- 다음으로 넘어가자" 입력 (20:12:33.635)
→ 종료멘트 재발화 (20:12:34.589) → 매칭 성공 (20:12:43.145) → 자동전환 (20:12:44.174)
⇒ 체감 지연 약 42초, 그리고 아동은 스텝0 종료멘트("5명 중 2명 골라봐")를 끝까지 듣지 못함
시각 타임라인 (2026-08-25 KST)
GUEST_AUTO_RESPONSE · Manual mode ready at 7 seconds — 아동 무응답 7초 경과, 진행자 필러 버튼 활성.
Published LiveKit user text message hasResponseInstructions:false — 필러 "음..." 전송 (textLength 4).
Checking auto-finish phrase — transcript = “어떤 친구랑 잘 맞는지 너랑 얘기해볼 수 있어서 나도 생각이 더 정리된 것 같아! 고마워!”, 그런데 phrases는 스텝0의 3개. 스텝1 종료멘트를 스텝0에서 선발화.
agent_response_completed(response-12) 정상 수신 → assistant_audio_stopped → checkPhrase 실행 → 0/3 미매칭. AI speech ended, processing auto-finish 로그 없음, 응답 폐기.
Step changed by host activityIndex 3 / stepIndex 1 — 자동이 아닌 진행자 수동 전환. 직후 same_activity_session_config_updated autoFinishPhraseCount:2(스텝1 문구).
모니터 타이머 11:39 = 누적 699초. 같은 로그의 cumulativeElapsedSeconds(20:12:44 → 719초, 벽시계와 약 37초 오프셋)로 역산한 값. 이 순간 현재 스텝은 이미 스텝1이고, 스텝0 시절 발화가 스텝1 문구로 소급 하이라이트되어 “매칭됐는데 전환 안 됨”으로 보인다.
필러 "음..." 재전송 → 20:12:21 “천천히 생각해도 괜찮아…” (미매칭). AI는 종료멘트를 다시 말하지 않는다.
진행자 텍스트 입력 - 다음으로 넘어가자 (textLength 11) — 실제 복구 액션. 자연 복구가 아니다.
종료멘트 재발화(response-14) → AI speech ended, processing auto-finish matchedCondition 기록 → Auto-finish triggered → activityIndex 4(활동회고) 전환. 경로는 건강했다.
추정 원인
| 순서 | 원인 | 확정도 |
|---|---|---|
| 0 (트리거) | 아동 2분 무응답 → 진행자 필러 "음..."가 responseInstructions 없이 전송. 내용 없는 입력이라 모델이 “마무리” 방향으로 수렴 | 추정 필러 전송·순서·hasResponseInstructions:false는 사실, 모델의 해석은 추정 |
| 1 (직접) | 핑퐁이가 스텝0 종료멘트를 건너뛰고 스텝1 종료멘트를 스텝0에서 선발화 | 사실 20:12:02 로그의 transcript와 phrases 불일치 |
| 2 (직접) | 자동전환 판정이 오디오 종료 시점의 현재 스텝 phrases만 검사하고, 실패하면 응답을 즉시 폐기 | 사실 use-ai-session.ts 4196–4231 + use-session-auto-finish.ts checkPhrase |
| 3 (구조) | 세션 instruction이 활동 단위 1개이고 스텝 전환 시 갱신되지 않음 → 모델이 활동 내 모든 스텝의 종료멘트를 언제든 말할 수 있다 | 사실 buildActivityInstruction은 steps/stepIndex를 보지 않음 |
배제된 원인
- LiveKit 완료 이벤트 누락 —
agent_response_completed(response-12)가 20:12:09.723에 실제 도착. 기존 사례(박승율 1회기)와는 다른 경로다. checkPhrase정규화·임계값 오류 — 스텝0 3번째 후보(어떤친구랑가장,친구로잘지낼수있을것,제일괜찮은친구두명)는 선발화 문장과 0/3 매칭. 임계 2 기준으로 정상 판정이며 미탐이 아니다. 세션 내 66개 AI 발화를 8개 phrase set으로 전수 시뮬레이션한 결과 오탐 0건.- responseData race / 스텝 전환 clear 가드 —
resetAutoFinish의 map clear는 20:12:14 전환 시점에 동작했지만, 그 전에 이미 20:12:09에 판정·폐기가 끝나 있었다. 이번 건의 원인이 아니다.
영향 범위
| 축 | 범위 |
|---|---|
| 발생 조건 | 한 활동 안에 연속된 AI 스텝(prompt/hybrid)이 2개 이상이고, 스텝별 종료멘트가 서로 다르며, 모델이 뒤 스텝의 종료멘트를 앞 스텝에서 발화하는 경우 |
| 런타임 | LiveKit·Realtime·TTS 공통. 단 selectResponseIdsForAssistantAudioEnd가 LiveKit은 played 1건, Realtime/TTS는 pending 전체를 검사해 노출면이 모드별로 다르다 |
| 데이터 의존 | 판정식은 진행자/기획이 편집하는 step.autoFinishPhrases 문자열이 가공·검증 없이 그대로 쓰인다 (createAutoFinishConfigFromStep, activity-utils.ts:320) |
| 사용자 체감 | 이번 건 약 42초 지연 + 진행자 수동 전환·프리셋 입력 2회 개입. 더 중요한 손실은 스텝0 과제 지시 미전달 |
| 재현성 | 모델 출력에 의존해 비결정적. 다만 “장시간 무응답 + 필러 전송” 조합에서 재현율이 높을 것으로 추정 — 다른 세션 로그로 값싸게 교차 확인 가능 |
추천 해결책
responseInstructions 주입 (원인 0·1을 직접 차단)sendTextMessage(msg, { responseInstructions }) 경로는 이미 Realtime(response.instructions)과 LiveKit(payload responseInstructions) 양쪽에서 동작하며 첫 발화(firstTurnMent)가 이 경로를 쓴다. 필러 "음..."와 스텝 전환 메시지에 “지금 스텝의 목표를 계속 진행할 것, 활동 마무리 멘트는 하지 말 것”을 동봉한다.신규 채널 0, 매칭 로직 무변경, agent 레포 변경 없음. 현재 이 두 경로는 모두
hasResponseInstructions:false로 전송되고 있다.현재 스텝 미매칭 + 다음 스텝 phrase 매칭이면 ① 전용 로그(예
auto_finish_next_step_phrase_detected) ② 진행자 모니터에 “핑퐁이가 다음 종료멘트를 먼저 말했어요 — 넘어갈까요?” 확인 배너를 띄운다. 선례가 있다: resolveEndConversationKeywords의 크로스 스텝 매칭도 진행자가 “시간종료”를 누른 뒤에만 활성된다.미매칭으로 응답을 폐기하는 지점에
responseId / stepIndex / phrases / 미매칭 1줄 로그를 남긴다. 현재는 “AI speech ended… 로그가 없다”로만 판별해야 해서 아래 3개 경로가 구분되지 않는다.진행자가 “매칭됐다”고 오인하는 직접 원인이다. ① 하이라이트에도 판정과 같은 임계값(조각 3개 이상이면 2개 이상)을 적용하거나 ② 실제 판정이 성립한 메시지만 별도 표시(예 “전환 조건 충족”)로 구분하고, ③ 스텝 전환 이전에 발화된 메시지는 현재 스텝 문구로 재하이라이트하지 않는다. 자동전환 동작은 건드리지 않으므로 사이드 이펙트 표면이 좁다.
하지 말아야 할 것
- 자동 전환하는 look-ahead 도입 금지. 이번 건에 적용해도 절감은 약 3.5초(오디오 종료 20:12:09.7 + 1초 지연 = 20:12:10.7 vs 진행자 전환 20:12:14.2)뿐이고, 남은 30초는 “스텝1 착지 후 재발화가 없어 프리셋을 쳐야 했던 구간”으로 look-ahead가 손대지 않는다. 대가는 아동 활동 무음 스킵이다.
- “활동 내 후속 스텝 전체”로 범위 확대 절대 금지. 이번 스텝1은
isLastStepOfActivity:true이므로 남은 스텝을 통째로 스킵하는 형태가 된다. - look-ahead +
jumpConfig조합 주의.goToNextStep은 triggerText(AI transcript 전문)를 conditional jump 키워드 소스로 넘긴다. 선발화 문장으로 분기가 결정되면 오탐이 “스텝 1칸”이 아니라 활동 단위 경로 오류로 증폭된다. - 느슨한 phrase에 검사 범위 확대 금지. 쉼표 조각 3개 중 2개 매칭 규칙은 현재 스텝 한정이라는 전제에서만 안전하다. 공백 제거 정규화 때문에
너랑얘기해볼수,정리된,고마워는 “정리된”+“고마워”만으로도 발동한다. resetAutoFinish의 map clear 제거·약화 금지. 응답별 config 스냅샷으로 대체하려면 “스냅샷 스텝 ≠ 현재 스텝이면 미전환”을 동시에 넣어야 하고, 그 경우 이번 케이스 결과는 변하지 않는다(픽스가 아니라 관측 개선).- 활동 instruction의 스텝 분할은 이번 신고로 착수하지 않는다. mid-session에 세션 instruction을 바꾸는 채널이 현재 없고(LiveKit control topic은 readiness handshake 전용), agent 레포·활동 저작 모델·기존 활동 마이그레이션이 걸린 별도 과제다.
- 스텝 전환을 진행 중 응답 완료까지 보류하지 않는다. 진행자 조작의 즉시성을 해쳐 “눌렀는데 안 넘어감”이라는 새 신고를 만든다.
회귀 위험 · 사이드 이펙트
| 수정안 | 사이드 이펙트 위험 | 검증 포인트 |
|---|---|---|
1순위 responseInstructions 주입 | 낮음. 매칭 로직 무변경이라 자동전환 오탐 0. 다만 지시문이 강하면 정상 종료 타이밍에도 마무리 멘트를 미루어 전환이 늦어질 수 있다 | 스텝 마지막 발화에서 종료멘트가 정상적으로 나오는지, 필러 후 대화가 이어지는지 실세션 확인 |
| 2순위 진행자 확인 배너 | 중간. 배너가 잦으면 진행자 피로. 스텝 전환 직후 새 응답에 감지가 걸려 진행자 의도를 덮는 이중 전환 방지 가드(전환 후 N초 억제)가 필요 | 모니터 새로고침 stale step push와 겹칠 때 되돌아간 스텝에서 앞 스텝 문구로 재감지되지 않는지 |
| 관측 로그 추가 | 낮음. 로그량 증가만 | 세션당 미매칭 폐기 건수가 과다하지 않은지 |
| (비권장) 자동 look-ahead | 높음. 아동 활동 무음 스킵, jumpConfig 오분기, Realtime/TTS의 pending 전체 검사로 오탐 폭 확대, 마지막 스텝·비AI 스텝에서 미탐 | 도입하지 않는 것을 권고 |
같은 증상의 인접 경로와 판별법
“종료멘트는 나왔는데 전환 안 됨”은 최소 3개 경로가 있고, 로그 시그니처로 구분된다.
| 경로 | 판별 시그니처 | 상태 |
|---|---|---|
| A. 스텝 미매칭(이 문서) | Checking auto-finish phrase 있음 + agent_response_completed 있음 + AI speech ended… 없음, 그리고 phrases가 발화 내용과 다른 스텝 것 | 미수정 |
| B. LiveKit 완료 이벤트 누락 | Checking auto-finish phrase 있음 + agent_response_completed 없음(terminal_boundary만) | 미수정 · 별도 문서 |
| C. 끼어들기 폐기 | assistant_audio_cleared 경로 — shouldFinalizeAssistantAudio=false이므로 판정 없이 transcript 폐기 | 미수정 · 미문서화 |
autoTransitionEnabled가 OFF인 동안에는 assistant_transcript_done 단계에서 저장조차 되지 않아 도중에 ON으로 바꿔도 소급 매칭이 불가하다. 또한 updateStartMent가 같은 플래그에 묶여 있어 자동전환 OFF 세션에서 startMent 전송이 누락되는지 별도 확인이 필요하다.코드 근거
| 경로 | 확인한 동작 |
|---|---|
apps/web/entities/guest-session/model/use-ai-session.ts:3754-3775 | assistant_transcript_done에서 responseDataRef.set(responseId, {itemId, transcript}) — 보관만, 판정 없음. Checking auto-finish phrase 로그가 이 단계다. |
apps/web/entities/guest-session/model/use-ai-session.ts:4196-4231 | assistant_audio_stopped/cleared에서 autoFinish.checkPhrase(transcript). 오디오 종료 시점의 현재 config로만 판정하고, 실패 시 responseDataRef.delete(id). |
apps/web/hooks/use-session-auto-finish.ts | checkPhrase: 큰따옴표·공백 제거 후 쉼표 조각 포함 검사, 조각 3개 이상이면 2개 이상 매칭. handlePingpongSpeech → 1초 뒤 onAutoFinish. |
apps/web/entities/guest-page-session/model/use-step-transition.ts:143-189 | 같은 활동 내 스텝 전환은 세션 유지 + resetAutoFinish() → createAutoFinishConfigFromStep() → updateAutoFinishConfig() → updateStartMent(). instruction은 갱신하지 않는다. |
apps/web/lib/activity-utils.ts:320 | createAutoFinishConfigFromStep: step.autoFinishPhrases를 가공·검증 없이 그대로 판정식으로 사용. |
apps/web/lib/openai-create-call.ts:29-51,84-101 · app/api/livekit/call/route.ts:220-233 | buildActivityInstruction은 activity.instruction 단일 문자열만 변수 치환. steps/stepIndex를 보지 않는다. |
apps/web/widgets/monitor/ui/transcript-chat-area.tsx:69-72,256 · apps/web/shared/lib/highlight-keywords.tsx · app/monitor-dashboard/[group]/[roomId]/page.tsx:825-827,2068 | 모니터 하이라이트는 현재 스텝의 autoFinishPhrases를 쉼표로 합쳐 과거 assistant 메시지 전체에 매번 적용한다. highlightKeywords에는 2/3 임계값이 없어 키워드 1개만 포함돼도 칠한다 — 하이라이트 ≠ 자동전환 판정. 스텝 전환 시 옛 발화가 소급 하이라이트되는 원인. |
apps/web/lib/voice-agent/session-events.ts:351 | LiveKit은 raw agent_response_completed일 때만 자동종료 판정을 실행한다(경로 B의 근거). |
조사 방법
아동 클라이언트 세션 로그 15,028건을 KST 시각·responseId·stepIndex로 정렬해 20:12 구간을 복원했고, checkPhrase를 재구현해 세션 내 66개 AI 발화 × 8개 phrase set 전수 시뮬레이션으로 오탐/미탐을 확인했다. 원인 분석은 developer(Codex)·reviewer(Claude Code) 2단계 교차 검토를 거쳤고, reviewer가 제기한 필러 트리거·복구 액션·완료 이벤트 도착 시각은 원본 로그로 재확인했다.
로그 취급 주의: 세션 로그 .gz를 브라우저로 내려받으면 ContentEncoding 때문에 실제로는 평문 JSONL이다. 확장자만 보고 gzip -d를 시도하면 실패한다.
수정 미적용: 이 문서는 원인 분석과 방안 정리까지다. 코드 변경은 없다.