Cadence: 메시지 분할 전송 엔진 설계
메신저 환경에서 사용자는 완성된 긴 문단을 한 번에 전송하기보다 절이나 호흡 단위로 나누어 발송하는 경향이 있습니다.
어제 간 카페에 사람이 없어서
공부하기 좋더라구요
또 갈까 싶어요
위 예시에서 첫 두 줄은 연결어미 -어서로 연결된 하나의 문장입니다. 생각의 흐름에 따라 여러 차례 나누어 전송한 형태입니다.
반면 챗봇이 장문의 답변을 단일 메시지로 즉시 전송하면, 사용자는 답변이 생성되는 동안 긴 침묵을 겪게 되고 대화의 상호작용 리듬이 저하됩니다.
Cadence 엔진은 이 간극을 메우기 위해 구현된 메시지 분할 전송 엔진입니다. 생성된 답변을 자연스러운 대화 단위로 분할하고, 조각 사이에 실제 타자 속도에 비례한 지연 시간을 삽입합니다.
분할 경계 강도
단순히 마침표 단위로만 문장을 분할하면 실제 메신저 대화의 호흡을 살리기 어렵습니다. "사람이 없어서"처럼 마침표 없이 연결어미 뒤에서 전송이 끊어지는 사례가 많기 때문입니다. 반대로 모든 문장 부호에서 분할하면 메시지가 지나치게 파편화되는 부작용이 발생합니다.
Cadence는 경계 강도를 세 단계로 나누어 처리합니다.
- 문단 및 개행: 가장 높은 강도의 경계입니다. 모델이 명시적으로 분리한 문단 구조는 무조건 분할 지점으로 존중합니다.
- 문장 종결(마침표, 느낌표, 물음표): 기본 분할 경계로 취급하되, 공백이 뒤따르지 않는 소수점이나 축약어는 경계에서 제외합니다.
- 쉼표 및 연결어미: 가장 약한 경계이며 기본적으로는 닫혀 있습니다. 단일 조각의 길이가 40자를 초과하여 가독성이 저하될 때만 분할 지점으로 활성화됩니다.
이러한 단계적 구분을 통해 짧은 문장은 불필요하게 쪼개지지 않고, 장문 설명의 경우에만 자연스러운 절 단위로 분할됩니다.
형태소 오탐 방지와 어미 선별
연결어미 단위 분할 시 가장 큰 난점은 명사 어근과 어미의 형태적 중복입니다.
예를 들어 -고를 무조건 분할 기준으로 삼을 경우 "밥 먹고 갈게"는 정상 분할되지만, "어제 사고 났어"의 "사고" 뒤에서 문장이 분절되는 심각한 오류가 발생합니다. -면 역시 라면, 화면, 표면, 수면 등 수많은 명사와 충돌합니다.
형태소 분석 라이브러리를 탑재하면 구분이 가능하지만, 실시간 메시지 발송 파이프라인에 도입하기에는 연산 비용과 의존성 부담이 큽니다. 따라서 Cadence는 명사와 형태가 겹치지 않는 2음절 이상 어미 12종을 선별하는 방식을 취했습니다.
선별된 어미:
-니까, -는데, -은데, -어서, -아서, -여서, -지만, -거든, -잖아, -면서, -라서, -길래
활용 빈도가 높은 -고와 -면은 오탐 방지를 위해 제외했습니다. 대신 -고,나 -면,처럼 어미 뒤에 쉼표가 붙는 자연스러운 문장 구조는 쉼표 경계 규칙을 통해 안전하게 분할할 수 있도록 보완했습니다.
실제 분할 예시:
우산은 젖은 채로 접어두면 곰팡이 슬어버리니까
현관에 그냥 펼쳐두지 마.
물기 털고 반쯤 펴서 통풍되는 데 두고, 완전히 마르면 그때 접어.
근데 접이식은 살대 이음새에 물이 고여서 아무리 말려도 녹슬긴 해.
오래 쓸 생각이면 장우산이 답이야.
첫 번째 조각은 -니까에서 분할되고, 이후 조각들은 문장 종결 부호에서 분할됩니다.
전송 간격 및 속도 계산
분할된 조각을 동시에 발송하면 분할의 의미가 퇴색됩니다. 조각 사이의 전송 간격을 실제 사람이 입력하는 속도에 맞춰 조정해야 합니다.
조각 간 지연 시간은 기준 타자 속도(초당 9자)를 기반으로 산출하며, 세 가지 가중치를 곱연산으로 적용합니다.
- 친밀도 가중치: 상대와의 관계가 깊을수록(신뢰, 애정 등) 지연 시간이 짧아지고, 낯선 관계일수록 길어집니다.
- 대화 여유 가중치: 서리의 일상 시뮬레이션 상태를 반영합니다. 수업이나 수면 등 집중 상태일 때는 응답 간격이 벌어집니다.
- 감정 상태 가중치: 들뜬 기분일 때는 속도가 빨라지고, 피로하거나 졸릴 때는 느려집니다.
조각 간 지연이 진행되는 동안에는 디스코드 입력 중(Typing) 상태를 다시 활성화하여 사용자에게 전송 대기 상태임을 명시합니다.
원자적 구간 보호와 원문 무결성
기계적 문자열 분할은 문맥상 절대 쪼개져서는 안 되는 특수 구문을 손상시킬 위험이 있습니다.
- URL 링크 (마침표 포함)
- 코드 블록 및 인라인 코드
- 디스코드 커스텀 이모지(
<:name:id>) - 유저 및 채널 멘션
Cadence는 분할 후보 위치를 탐색하기 전 정규식을 통해 보호 구간을 원자(Atomic) 단위로 먼저 마킹하고 분할 대상에서 제외합니다. 닫히지 않은 코드 블록 역시 끝까지 보호하여 서식이 깨지지 않도록 합니다.
또한 원문 훼손을 방지하기 위해 텍스트 슬라이싱을 반복하지 않고, 분할 위치 인덱스 배열만을 계산한 뒤 최종 시점에 한 번만 분할합니다. 분할된 모든 조각을 결합했을 때 원문과 정확히 일치함을 테스트 스위트로 지속 검증합니다.
프롬프트 지시 대신 규칙 엔진을 택한 이유
언어 모델 프롬프트에 직접 분할 마커 출력을 지시하는 대안도 고려할 수 있습니다. 그러나 이 방식은 다음과 같은 치명적 결함이 있습니다.
- 페르소나 및 어투 오염: 프롬프트에 출력 포맷 지침이 추가되면 고유 어투와 페르소나 지침 준수율이 하락합니다. 과거 글자 수 제한 지시문이 대화 품질을 저하시켜 제거했던 선례가 있습니다.
- 출력 오류 위험: 모델이 마커를 누락하거나 특수 기호를 원문에 그대로 노출하는 사고가 발생할 수 있습니다.
- 비침습적 구조: 현재 방식은 모델의 원문 생성을 전혀 간섭하지 않으며, 전송 단계에서 독립적으로 동작합니다. 기능 비활성화 시에도 원문 출력에 일절 영향을 주지 않습니다.
응답 지연 상한과 예외 처리
지연 시간이 과도하게 누적되면 사용자 응답성이 저하됩니다. 이를 제어하기 위한 안전장치를 마련했습니다.
- 조각 수 상한: 단일 답변은 최대 5개 조각까지만 분할됩니다.
- 지연 시간 예산: 전체 지연 시간 합이 상한(6초)을 초과할 경우 각 간격을 비례 축소합니다.
- 첫 조각 즉시 발송: 초기 응답 지연을 방지하기 위해 첫 번째 조각은 대기 시간 없이 즉각 전송합니다.
- 동시 발화 처리: 조각 전송 도중 동일 채널에서 새로운 사용자 발화가 감지되면, 남은 조각을 지연 없이 즉시 일괄 전송하여 대화 혼선을 방지합니다.
현재 한계와 개선 방향
현재 Cadence 엔진은 완성된 문장을 후처리로 분할하는 구조입니다. 사람이 대화할 때 사용하는 "문법적으로 미완성된 절을 먼저 던지고 뒤이어 보충하는" 실시간 스트리밍 발화 구조까지는 지원하지 않습니다.
기준 타자 속도와 가중치 수치는 실제 서비스 피드백을 바탕으로 계속해서 미세 조정을 진행하고 있습니다.