ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 끝까지 들으면 점수를 더 주는 챗봇 — 청취 완료 타이머의 디자인
    IT 2026. 5. 14. 21:00
    끝까지 들으면 점수를 더 주는 챗봇 — 청취 완료 타이머의 디자인

    아이가 챗봇한테 질문을 던지고 답이 음성으로 흘러나오기 시작하면, 한두 마디 듣고 마이크 버튼을 다시 눌러 새 질문을 하기 일쑤였습니다. 챗봇이 답을 다 못 마쳤는데 다음 질문이 들어오니 자연스럽게 답이 잘리고, 학습 효과 측면에서도 아쉬웠습니다. "지금 답을 끝까지 들어봐"라고 옆에서 말해줘야 했죠.

    게이미피케이션의 한 갈래로 "답변을 끝까지 들으면 추가 점수"라는 보상을 추가했습니다. 처음 1점, 들으면 +5점이라는 단순한 구조입니다. 그런데 "끝까지 들었다"를 어떻게 판정할 것인가가 의외로 까다로운 디자인 문제였습니다. 10초 타이머와 큐 배출, 두 신호의 OR 게이트로 푼 이야기를 정리합니다.

    "끝까지 들음"의 정의가 모호하다

    판정의 가장 단순한 기준은 "TTS 음성 재생이 끝났을 때"입니다. 그러나 이건 두 가지 문제가 있습니다.

    첫째, 답변 길이가 들쭉날쭉합니다. LLM 답이 한 문장(2초)인 경우와 일곱 문장(15초)인 경우가 있는데, 둘 다 "끝까지 듣기"로 인정하면 짧은 답에선 너무 쉽고 긴 답에선 너무 가혹해집니다. 짧은 답을 끝까지 듣는 게 학습 측면에서 큰 의미가 없는데도 보상이 같다면 보상 자체가 약해집니다.

    둘째, 답이 매우 긴 경우 사용자가 못 기다리고 인터럽트할 때 보상을 줘야 하는지 모호합니다. 30초짜리 답을 25초까지 듣고 다음 질문으로 넘어간 경우, "끝까지"는 아니지만 "충분히 들었다"고 볼 수도 있습니다.

    그래서 두 가지 신호의 OR 게이트로 정의했습니다.

    • (A) 첫 TTS 오디오가 재생을 시작한 뒤 10초가 지났다 — 시간 기반 임계값.
    • (B) TTS 큐가 완전히 배출됐다(= 답 전체 재생 완료) — 자연 종료.

    둘 중 먼저 일어난 것을 트리거로 +5점을 부여합니다. 짧은 답은 (B)가 빨리 일어나서 즉시 보상, 긴 답은 (A)가 먼저 일어나서 10초만 들으면 보상. 인터럽트가 그 전에 일어나면 보상 없음.

    청취 완료 타이머 OR 게이트 흐름

    그림 설명 — 시나리오 1(짧은 답): 큐가 3초만에 다 배출되니 (B) 신호가 먼저 발화해 +5점이 부여됩니다. 시나리오 2(긴 답): 10초가 먼저 지나가니 (A) 신호가 발화하고, 그 후 큐 배출(20초 시점)은 이미 보상이 주어진 뒤라 무시됩니다. 시나리오 3(인터럽트): 5초만 듣고 마이크를 다시 누르면 두 신호 모두 취소되고 보상은 없습니다. 학습 동기를 만드는 핵심 장치이면서도, 짧은 답에서 너무 쉽게 보상이 풀리지 않도록 균형을 잡는 디자인입니다.

    코드 — 두 핸들러와 OR 게이트

    // 답변 청취 보상 — +5점은 (A) 10초 OR (B) 큐 완전 배출 중 먼저 일어난 쪽에서.
    //                   사용자가 5초 안에 인터럽트하면 보상 없음.
    const ANSWER_LISTEN_THRESHOLD_MS = 10000;
    let answerListenTimer = null;
    let answerAwarded = false;
    let suppressAnswerAward = false;  // 인사·unclear 발화엔 보상 차단
    
    function resetAnswerTracking() {
      if (answerListenTimer) {
        clearTimeout(answerListenTimer);
        answerListenTimer = null;
      }
      answerAwarded = false;
      suppressAnswerAward = false;
    }
    

    코드 설명 — 매 turn 시작 시 resetAnswerTracking으로 상태 초기화합니다. answerAwarded는 이번 turn에서 이미 보상이 줬는지 추적하는 idempotency 플래그 — 두 신호가 동시에 도착해도 한 번만 +5점이 들어가게 보장합니다. suppressAnswerAward는 인사("안녕!")나 STT 환각 같은 케이스에서 답변 자체는 진행하되 점수 보상은 차단할 때 켜는 별도 게이트입니다(다른 글에서 다룬 unclear 처리와 연결).

    // (A) 10초 타이머 무장 — 첫 TTS 오디오 재생이 시작될 때 호출
    function armAnswerListenTimer(myTurn) {
      if (answerAwarded || suppressAnswerAward || answerListenTimer) return;
      answerListenTimer = setTimeout(() => {
        answerListenTimer = null;
        if (myTurn === turnId) awardAnswerListened();
      }, ANSWER_LISTEN_THRESHOLD_MS);
    }
    
    // (B) 큐 완전 배출 — playNextTts에서 큐가 비었을 때 호출
    function onConversationDone(myTurn) {
      if (myTurn === turnId) awardAnswerListened();
    }
    
    // 보상 부여 — 두 경로에서 모두 호출되며 idempotent
    function awardAnswerListened() {
      if (answerAwarded || suppressAnswerAward) return;
      answerAwarded = true;
      if (answerListenTimer) {
        clearTimeout(answerListenTimer);
        answerListenTimer = null;
      }
      postEvent('answer_listened');   // 서버에 +5점 이벤트 전송
    }
    

    코드 설명 — 세 함수의 협력입니다. armAnswerListenTimer는 첫 TTS 오디오가 재생을 시작할 때 호출돼 10초 타이머를 단 한 번만 무장합니다(이미 무장됐거나 보상이 풀렸으면 무시). onConversationDone은 TTS 큐가 비고 마지막 오디오까지 재생이 끝났을 때 호출됩니다. 둘 다 awardAnswerListened를 호출하는데, 그 함수는 answerAwarded 플래그를 체크해 한 번만 보상을 부여합니다. 두 신호가 거의 동시에 도착하는 경계 케이스(타이머 직전에 큐 배출)도 이 idempotency로 안전하게 처리됩니다.

    turnId 비교는 인터럽트 방어선입니다. 사용자가 마이크를 다시 누르면 turnId가 증가하는데, 그 사이에 발동된 setTimeout 콜백이나 비동기 onended 콜백은 자기가 시작될 때의 myTurn을 기억해두고, 콜백 시점에 turnId와 비교해 다르면 결과를 무시합니다. 이게 인터럽트 시 보상 취소의 메커니즘입니다.

    왜 10초인가

    10초라는 숫자는 임의로 정한 게 아니라 사용성 관찰을 거쳐 결정됐습니다.

    5초로 시작했더니 너무 쉬웠습니다. 챗봇이 "음, 그건 말이지…" 정도만 말해도 5초가 흘러서 보상이 풀렸고, 학습 동기로 작동하기엔 약했습니다. 15초로 올렸더니 너무 가혹했습니다. 평균적인 답변(약 8~12초)이 끝나기 전에 보상이 풀리지 않으니, 자연 종료(B)에만 의존하게 됐고 인터럽트 한 번이면 보상이 사라져 답답했습니다.

    10초는 "내용이 들어가는 답변의 도입부 + 한 호흡" 정도의 길이입니다. 이 정도 들으면 답의 핵심은 거의 잡히고, 그 시점에 다음 질문을 해도 학습 흐름이 깨지지 않습니다. 자연 종료(B)와 인공 임계값(A) 사이의 균형을 가장 잘 잡는 값이었습니다.

    보상이 학습에 미친 효과

    이 한 가지 보상을 적용한 뒤 아이의 사용 패턴이 두 가지 측면에서 바뀌었습니다.

    첫째, 답을 끝까지 듣는 빈도가 늘었습니다. 이전에는 한두 마디 듣고 다음 질문으로 넘어가는 패턴이 잦았는데, 보상 도입 후 끝까지 듣고 그 답을 곱씹는 시간이 자연스럽게 생겼습니다. "+5"가 화면에 떠오르는 시각적 보상이 작은 도파민으로 작동하는 것 같습니다.

    둘째, 긴 답에 대한 인내심이 늘었습니다. 이전엔 답이 길면 중간에 끊고 짧게 다시 묻는 경향이 있었는데, 10초 임계값을 넘기면 보상이 들어오니 그 시간 동안은 일단 들어보는 패턴이 생겼습니다. 그 10초 안에 답의 핵심이 들어 있으면 인터럽트할 필요가 없어집니다.

    이 두 변화가 합쳐져서 대화 한 turn당 학습 밀도가 높아졌습니다. 같은 질문 수에서 답을 더 깊이 받아들이는 방향으로 행동이 바뀐 거죠. 게이미피케이션의 흔한 비판인 "외재적 동기가 내재적 동기를 약화시킨다"가 걱정됐지만, 짧은 답엔 즉시 보상·긴 답엔 시간 임계값이라는 비대칭 디자인이 학습 행동을 촉진하는 방향으로 잘 작동했습니다.

    마치며 — 두 신호의 OR 게이트는 단순한데 효과는 미묘하다

    이 디자인은 게이미피케이션의 한 작은 부분일 뿐입니다. 그러나 두 신호(시간·자연 종료)의 OR 게이트라는 단순한 구조가 의외로 미묘한 효과를 냅니다. 답 길이에 무관하게 적당히 보상이 풀리고, 인터럽트가 자연스럽게 보상을 차단하고, 짧은 답에선 자연 종료가 빨리 일어나 즉시 만족감을 줍니다.

    일반화하면 — "수행했다"의 정의는 보통 단일 신호로 잡기 어렵고, 두 개 이상의 신호의 결합으로 정의하는 게 강건하다는 발상입니다. 시간 임계값만 두면 짧은 답에서 보상이 너무 쉽고, 자연 종료만 두면 긴 답에서 보상이 너무 가혹합니다. 둘을 OR로 연결하면 양 끝의 케이스를 모두 자연스럽게 처리합니다. 이런 작은 조합이 게이미피케이션을 "유치한 점수 시스템"에서 "학습을 미세하게 유도하는 장치"로 만듭니다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.