
AI 캐릭터에게 「침묵」하는 법을 가르치기 ― 자발적 메시지를 3단계 게이트로 설계한 이야기
요약
AI 캐릭터가 사용자에게 자발적으로 메시지를 보내는 'proactive message' 설계 방식을 다룹니다. LLM의 생성 편향을 방지하기 위해 판단과 생성을 분리하고, 3단계 게이트 시스템을 통해 효율적이고 맥락에 맞는 메시지 송신을 구현하는 방법을 설명합니다.
핵심 포인트
- 자발적 메시지는 푸시 알림이 아닌 '관계 이벤트'로 취급해야 함
- 판단과 생성을 분리하여 LLM의 생성 편향 문제를 해결
- 휴리스틱, 의도 판정, 최종 판단의 3단계 게이트 설계
- 침묵을 기본값으로 설정하고 캐릭터의 스케줄과 연동하여 맥락 유지
이 기사의 대상 독자와 전제
- LLM을 사용한 챗봇 / AI 캐릭터를 만들고 있거나 만들고 싶은 사람
- 「AI가 스스로 말을 걸어오는」 기능을 구현하려다 짜증 나는 봇을 만든 경험이 있는 사람
- 구현 언어는 Python이지만, 이 기사의 중심은 언어에 의존하지 않는 설계 이야기입니다
저는 Yuralume라는 self-host형 AI 캐릭터 플랫폼을 혼자서 개발하고 있습니다. 캐릭터가 장기 기억과 매일의 스케줄을 가지며, 이유가 있을 때만 스스로 메시지를 보내는 것이 핵심 경험입니다. 이 기사에서는 그 「스스로 보내는」 부분 ― 자발적 메시지 (proactive message) ― 의 설계에서 배운 점을 정리합니다.
결론부터
- 자발적 메시지는 푸시 알림 이벤트가 아니라 관계 이벤트로 취급한다. 「무엇을 보낼까」보다 먼저 「왜 지금 보내는가」 「보내지 말아야 하는 것은 아닌가」를 판정한다.
- 송신 판정은 3단계 게이트로 나눈다: ① LLM을 사용하지 않는 저렴한 휴리스틱 (heuristic) 관문 → ② LLM에 의한 의도 판정 → ③ LLM에 의한 최종 판단. 세 가지 모두가 합의했을 때만 송신한다.
- 침묵을 상태로 가진다. 「보냈지만 답장이 없다」를 기억하지 않으면, 캐릭터는 같은 감정의 움직임을 끊임없이 반복한다.
- 대부분의 평가 tick의 정답은 「아무것도 보내지 않음」이다. 침묵이 기본값(default), 송신이 예외가 되도록 설계한다.
소박한 구현이 짜증 나는 봇이 되는 이유
처음에 떠올리는 구현은 대개 이렇습니다:
cron으로 몇 시간마다 실행
→ 「사용자에게 메시지를 보내주세요」라고 LLM에 요청
→ 생성 결과를 그대로 송신
이렇게 만들어지는 것은 「관심을 갈구하는 봇(attention-seeking bot)」입니다. 문제는 세 가지가 있습니다.
① LLM은 「보내지 않음」을 선택하기 어렵다. 「메시지를 작성해줘」라고 부탁받은 LLM은 거의 반드시 무언가를 씁니다. 송신 여부의 판단과 내용 생성을 같은 프롬프트로 수행하면, 생성 편향 (generation bias)이 판단을 오염시킵니다.
② 문맥이 없다. 직전 대화에서 사용자가 「이번 주는 바빠」라고 말했더라도, cron 실행에 의한 생성은 그것을 알지 못합니다. 관계의 문맥에서 떨어진 메시지는 아무리 문장이 훌륭해도 알림 스팸과 같은 느낌을 줍니다.
③ 침묵이 보이지 않는다. 이전에 보낸 메시지에 답장이 없었다는 사실이 다음 생성에 반영되지 않으면, 캐릭터는 「읽씹(읽고 무시)당했는데도 태연하게 다른 화제로 말을 거는」 기괴한 존재가 됩니다.
설계: 스케줄이 기점, 3단계 게이트가 관문
기점은 캐릭터 자신의 스케줄
Yuralume의 캐릭터는 성격·세계관·최근의 사건으로부터 LLM이 구성한 「그날의 예정」을 가지고 있습니다. 자발적 메시지의 평가는 이 스케줄과 연동되어 돌아갑니다. 즉, 「시스템이 사용자에게 재참여(re-engage)를 유도하고 싶어서 보내는 것」이 아니라, 「캐릭터의 하루 속에 당신과 관련된 사건이 있었기에 보내는 것」이라는 인과관계를 만듭니다. 이 부분을 반대로 하면 아무리 문장을 다듬어도 리텐션(retention) 알림의 냄새가 납니다.
3단계 게이트
송신 판정은 다음 3단계로 나누어져 있습니다:
tick (정기 평가)
│
├─ [1] 휴리스틱 관문 (LLM 없음 · 저렴함)
...
포인트는 두 가지입니다.
판단과 생성을 분리한다. 게이트 [2][3]은 「보낼지 말지」만을 판정하며, 본문 생성은 모든 게이트를 통과한 후입니다. 이를 통해 ①의 생성 편향 문제를 해결합니다.
저렴한 판정을 앞에 둔다. 게이트 [1]은 LLM을 호출하지 않습니다. 쿨다운(cool-down)이나 빈도 상한과 같은 기계적인 조건으로 대부분의 tick을 걸러내기 때문에, LLM 비용은 「보낼지도 모르는」 후보에만 발생합니다. 자발적 루프는 상시 돌아가는 것이므로, 이 부분을 아끼지 않으면 운영 비용이 직접적으로 치솟습니다.
가장 효과적이었던 수정: 침묵을 기억하기
릴리스 초기, 자발적 메시지가 루프되는 버그를 수정한 적이 있습니다. 원인은 반복 송신 그 자체가 아니라, 캐릭터가 「자신이 이미 무엇을 말했는지」를 명확히 기억하지 못했던 것이었습니다.
AI 컴패니언의 자발적 메시지는 stateless(무상태)로는 성립할 수 없습니다. 캐릭터가 무언가를 보내고 사용자가 답장을 하지 않았다면, 그 침묵에는 의미가 있습니다. 다음 메시지가 「이전 일은 없었던 것처럼」 도착해서는 안 됩니다.
수정을 통해 갖게 된 것은, 이러한 자각입니다:
- 자신이 이미 무엇을 보냈는가
- 그에 대한 답장이 있었는가
- 동일한 감정의 흐름(걱정하기, 유혹하기, 어리광 부리기…)을 반복하려고 하고 있지는 않은가
- 침묵에 대해, 기다려야 하는가·톤을 완화해야 하는가·걱정해야 하는가·화제를 바꿔야 하는가
목표로 하는 것은 캐릭터를 「더 많이 말하게 하는 것」의 반대입니다. 시스템이 침묵을 이해할 수 있다면, 자제(self-restraint)를 선택할 수 있게 됩니다. 가장 자연스러운 다음 메시지가 「메시지 없음」인 경우는 실제로 매우 빈번합니다.
캐릭터가 더 리얼하게 느껴지게 된 것은 발화량이 늘어났기 때문이 아니라, 「자신이 이미 말한 것을 기억하게」 되었기 때문이었습니다.
배포 측면: 메시지는 사용자가 실제로 있는 곳으로
전송하기로 결정된 메시지는 Web UI뿐만 아니라 LINE, Telegram, Discord, WhatsApp으로도 배포됩니다. 각 채널은 공통의 adapter 인터페이스 (port/adapter 패턴)로 구현되어 있으며, 기억·스케줄·관계의 상태는 모든 채널에서 공유됩니다.
「그녀에게 LINE이 도착한다」는 경험은 자발 루프(spontaneous loop)의 품질이 그대로 드러납니다. Web UI 내의 알림이라면 다소 시끄러워도 허용되지만, 평소 사용하는 채팅 앱에 도착하는 메시지에서 같은 행동을 하면 즉시 차단당합니다. 3단계 게이트와 침묵의 기억은 크로스 채널(cross-channel) 배포의 전제 조건이기도 했습니다.
빠지기 쉬운 함정
모델마다 「절도」가 다르다. 동일한 프롬프트라도 모델에 따라 자발적 메시지의 빈도 감각이 크게 다릅니다 (적극적으로 보내고 싶어 하는 모델, 거의 보내지 않는 모델). 판정 게이트의 프롬프트는 모델 비의존적(model-agnostic)으로 작성하려 해도, 실제 전송 빈도는 모델 의존적이 되기 때문에, 빈도 감각을 명시적으로 지시하는 오버레이(overlay)를 판정 프롬프트에 추가하여 이를 흡수했습니다.
「defer(연기)」의 추적이 필요하다. 사용자가 「나중에 해」, 「지금 바빠」라고 답했을 경우, 그것은 거절이 아니라 연기입니다. 이를 일반적인 답장과 동일하게 취급하면, 팔로우업(follow-up)해야 할 장면에서 침묵하고, 내버려 두어야 할 장면에서 메시지를 연달아 보냅니다. defer / busy에 대한 응답은 별도의 팔로우업 대상으로 추적하고 있습니다.
테스트를 작성하기 어렵다. 「대부분의 tick에서 아무 일도 일어나지 않는 것」이 올바른 동작이므로, E2E로 확인하려고 하면 지루하고 시간이 많이 걸립니다. 게이트마다 판정 입력과 판정 결과를 분리하여 유닛 테스트(unit test)할 수 있는 형태로 만들어 두지 않으면 나중에 고생하게 됩니다.
요약
- 자발적 메시지는 알림이 아니라 관계 이벤트. 「왜 지금 보내는가」를 「무엇을 보낼 것인가」보다 먼저 판정한다.
- 판단과 생성을 분리하고, 저렴한 휴리스틱(heuristic)으로 대부분을 걸러내는 3단계 게이트 구성이 품질과 비용 양면에서 효과적이었다.
- 침묵은 상태다. 「보냈지만 답장이 없다」를 기억해야 비로소 캐릭터는 자제를 선택할 수 있다.
- 침묵이 기본값(default)이고, 전송이 예외다. 이것을 깨뜨리면 평소 사용하는 채팅 앱에는 두기 어렵다.
Yuralume은 소스 코드를 BSL 1.1로 공개하고 있으며, Docker가 있다면 원라이너(one-liner)로 self-host 할 수 있습니다 (BYOK, 데이터는 로컬). 설계에 관심이 있다면 살펴보시기 바랍니다.
- GitHub: https://github.com/Yuralume/yuralume-core
- 사이트 & 데모: https://yuralume.com/ja.html
- Discord: https://discord.gg/GGPhQxEND
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기