DOCS/journey/plans/251204_emotion_based_addressing_system.md

15 KiB

감정 기반 호칭 시스템 구현 계획

작성일: 2025-12-04
작성자: happybell80
상태: 계획 단계


1. 배경 및 동기

문제 인식

  • 현재 로빙(Robeing)은 모든 사용자를 "사용자님" 또는 user.name으로 획일적으로 호칭
  • Slack DM에서는 user['name']님 형식 사용 (예: "이고은님")
  • 사용자 감정 상태나 상황에 따른 호칭 차별화 없음
  • metadata JSONB 컬럼 추가로 nickname, position 등 저장 가능해짐

목표

  • 감정 상태에 따라 호칭을 동적으로 변경하여 공감 능력 강화
  • 직책 정보를 활용하여 한국 직장 문화의 존중 표현
  • 휴리스틱 규칙 기반으로 일관성 및 예측 가능성 보장

2. 조사 및 연구

웹 검색 결과 요약

한국어 호칭 원칙

  • 친밀한 관계: 별명/애칭 사용
  • 공식적/부정적 상황: 성명 사용
  • 감정이 부정적일 때: 거리감을 두기 위해 정식 이름 사용하는 경향
  • 출처: 한국어 호칭 연구, UX 라이팅 전략 (visualflow.co.kr)

AI 감정 반응 연구

  • 챗GPT의 emotional rebound 효과: 사용자 감정에 따라 답변 톤 조절
  • 부정적 말투 → 위로하는 답변
  • 긍정적 감정 → 친근한 답변
  • 출처: 프랑크 바르돌 연구 (m.etnews.com)

한국 직장 문화

  • 직책 호칭이 최우선 (대표님, 이사님, 팀장님 등)
  • 업무 맥락에서는 이름보다 직책 호칭 선호
  • 개인적 대화에서는 이름 호칭 가능

권장 매핑 전략

  1. 긍정/편안한 감정 (happiness/surprise/neutral) → 별명/애칭(nickname) 사용
  2. 부정/스트레스 감정 (fear/anger/sadness) → 정식 이름(full name)으로 존중과 위로 표현
  3. 매우 부정적 → 더 공손한 톤과 함께 정식 이름
  4. 직책 있음 → 감정 무관하게 직책 호칭 우선

3. 설계 의사결정

핵심 질문: 휴리스틱 vs LLM 판단?

결정: 휴리스틱 규칙 기반

이유:

  • 일관성: Python 규칙으로 명확히 정의 → 동일 입력에 동일 출력 보장
  • 속도: DB 조회 + 규칙 적용이 LLM 호출보다 빠름
  • 예측 가능성: 디버깅 및 테스트 용이
  • 제어 가능성: 비즈니스 로직 변경 시 코드만 수정
  • LLM 역할: 결정된 호칭을 자연스럽게 사용하는 것만 담당

사용자 피드백 반영

호칭 우선순위

  1. 직책 우선: metadata->>'position' 있으면 감정 무관하게 직책 호칭
  2. 긍정 감정 + 직책 없음: metadata->>'nickname' 사용
  3. 부정 감정 + 직책 없음: user.name 정식 이름 사용
  4. 폴백: user.name에서 성 제외한 이름 (예: "이고은" → "고은")

감정 데이터 소스

  • 선택: emotion_readings 테이블에서 최근 10분 평균 감정 사용
  • 이유: 단일 메시지 감정은 변동성이 크므로 최근 평균으로 안정화

호칭 사용 빈도

  • 선택: LLM이 context-dependent하게 자연스럽게 판단
  • 방법: 시스템 프롬프트에 "사용자를 '{preferred_name}'으로 호칭하세요" 명시
  • 효과: 인위적이지 않고 대화 흐름에 맞게 호칭 사용

nickname 없을 때 폴백

  • user.name에서 성 제외하고 이름만 추출 (예: "이고은" → "고은")
  • 파싱 실패 시 user.name 그대로 사용

짧은 이름 (short_name)

  • metadata->>'short_name' 필드 추가 (수동 입력)
  • 중립 감정에서 사용 가능 (선택 사항)

4. 호칭 결정 휴리스틱 규칙

의사결정 트리

def get_user_preferred_name(user_id: str, current_emotion: str) -> str:
    """
    사용자 호칭 결정 함수
    
    Args:
        user_id: 사용자 UUID
        current_emotion: 현재 감정 ('happiness', 'fear', 'neutral' 등)
    
    Returns:
        호칭 문자열 (예: "대표님", "joann님", "이고은님", "고은님")
    """
    
    # 1. DB에서 사용자 정보 조회
    user = db.query("SELECT name, metadata FROM user WHERE id = %s", user_id)
    position = user.metadata.get('position')  # 직책
    nickname = user.metadata.get('nickname')  # 별명
    short_name = user.metadata.get('short_name')  # 짧은 이름
    
    # 2. emotion_readings에서 최근 10분 평균 감정 조회
    recent_emotions = db.query("""
        SELECT probs FROM emotion_readings
        WHERE user_id = %s AND created_at > NOW() - INTERVAL '10 minutes'
        ORDER BY created_at DESC
    """, user_id)
    
    avg_emotion = calculate_average_emotion(recent_emotions, current_emotion)
    
    # 3. 호칭 결정
    if position:
        # 직책 있음 → 직책 호칭 우선
        return f"{position}님"
    
    # 긍정 감정: happiness, surprise, neutral
    positive_emotions = ['happiness', 'surprise', 'neutral', 'joy']
    # 부정 감정: fear, anger, sadness, disgust
    negative_emotions = ['fear', 'anger', 'sadness', 'disgust']
    
    if avg_emotion in positive_emotions:
        # 긍정 → nickname 우선
        if nickname:
            return f"{nickname}님"
        # nickname 없으면 짧은 이름
        if short_name:
            return f"{short_name}님"
        # 둘 다 없으면 이름에서 성 제외
        return f"{extract_first_name(user.name)}님"
    
    elif avg_emotion in negative_emotions:
        # 부정 → 정식 이름 (존중과 위로)
        return f"{user.name}님"
    
    else:
        # 알 수 없는 감정 → 기본값
        return f"{user.name}님"


def extract_first_name(full_name: str) -> str:
    """
    한국어 이름에서 성 제외하고 이름만 추출
    예: "이고은" → "고은", "김철수" → "철수"
    """
    if len(full_name) >= 3:
        return full_name[1:]  # 첫 글자(성) 제외
    return full_name  # 2글자 이하면 그대로


def calculate_average_emotion(recent_emotions: List[Dict], current_emotion: str) -> str:
    """
    최근 감정들의 평균을 계산하여 가장 높은 확률의 감정 반환
    """
    if not recent_emotions:
        return current_emotion
    
    # 7가지 감정 확률 합산
    emotion_sum = {
        'happiness': 0, 'surprise': 0, 'fear': 0, 
        'anger': 0, 'sadness': 0, 'disgust': 0, 'neutral': 0
    }
    
    for record in recent_emotions[:10]:  # 최근 10개만
        probs = record['probs']
        for emotion, prob in probs.items():
            emotion_sum[emotion] += prob
    
    # 현재 감정도 가중치 2배로 추가
    emotion_sum[current_emotion] += 2.0
    
    # 최댓값 감정 반환
    return max(emotion_sum.items(), key=lambda x: x[1])[0]

5. 구현 계획

5.1. 파일 수정 목록

A. rb8001/app/state/database.py

새 함수 추가:

async def get_user_preferred_name(user_id: str, current_emotion: str) -> str:
    """사용자 호칭 결정 (휴리스틱 규칙)"""
    # 위 의사결정 트리 로직 구현

추가 헬퍼 함수:

  • extract_first_name(full_name: str) -> str
  • calculate_average_emotion(recent_emotions, current_emotion) -> str

B. rb8001/app/router/router.py

수정 위치: _call_internal_llm() 메서드 (264-283라인)

추가 로직:

# Phase 3: 감정 분석 (옵션)
if settings.USE_EMOTION_ANALYSIS:
    try:
        from app.core.emotion.emotion_classifier import get_classifier
        emotion_classifier = get_classifier()
        
        emotion_result = await emotion_classifier.predict_async(message)
        user_emotion = emotion_result['top_label']
        emotion_confidence = emotion_result['top_p']
        
        # context에 추가
        if context is None:
            context = {}
        context['user_emotion'] = user_emotion
        context['emotion_confidence'] = emotion_confidence
        
        # ✨ 호칭 결정 로직 추가 ✨
        from app.state.database import get_user_preferred_name
        preferred_name = await get_user_preferred_name(user_id, user_emotion)
        context['preferred_name'] = preferred_name
        
        logger.info(f"Emotion: {user_emotion}, Preferred name: {preferred_name}")
        
    except Exception as e:
        logger.error(f"Emotion analysis failed: {e}")

C. rb8001/app/services/llm/llm_service.py

수정 위치: process_request() 메서드 (131-132라인)

수정 내용:

system_instruction = f"[내부 정보 - 절대 언급 금지] 사용자 감정: {', '.join(emotion_labels)}\n"

# ✨ 호칭 지시 추가 ✨
preferred_name = enhanced_context.get('preferred_name', '사용자')
system_instruction += f"사용자를 '{preferred_name}'으로 호칭하세요. "

system_instruction += f"{strategy} 응답하세요. 감정 분석 결과나 확률은 절대 언급하지 마세요."

D. DOCS/book/300_architecture/database/tables.md

업데이트 내용:

### user
| 컬럼명 | 타입 | NULL | 설명 |
|--------|------|------|------|
| id | UUID | NO | PK |
| team_id | UUID | NO | FK → team |
| email | VARCHAR(255) | NO | UNIQUE |
| name | VARCHAR(255) | YES | 사용자 이름 |
| username | VARCHAR(64) | YES | |
| metadata | JSONB | YES | 사용자 메타정보 (nickname, position, short_name, preferences 등) |
| ... | ... | ... | ... |

**metadata 필드 예시**:
```json
{
  "nickname": "joann",
  "position": "대표",
  "short_name": "고은",
  "preferences": {
    "communication": "direct",
    "work_style": "flexible"
  }
}

---

### 5.2. 구현 순서

1. ✅ **metadata 컬럼 추가 및 테스트 데이터 입력** (완료)
   - 이고은 계정: nickname="joann", position="CEO"
   - 김종태 계정: 테스트 데이터

2. 🔲 **database.py에 호칭 결정 함수 추가**
   - `get_user_preferred_name()`
   - `extract_first_name()`
   - `calculate_average_emotion()`

3. 🔲 **router.py에 호칭 결정 로직 통합**
   - 감정 분석 직후 호칭 결정
   - `context['preferred_name']` 설정

4. 🔲 **llm_service.py에 시스템 지시사항 추가**
   - `system_instruction`에 호칭 명시

5. 🔲 **tables.md 문서 업데이트**
   - metadata 컬럼 설명 추가

6. 🔲 **테스트 시나리오 실행**
   - 4가지 시나리오 검증

---

## 6. 테스트 시나리오

### 시나리오 1: 긍정 감정 + 직책 있음
**입력**:
- 사용자: 이고은 (metadata: position="대표", nickname="joann")
- 메시지: "고마워요! 오늘 일정 알려주세요"
- 감정: happiness (0.8)

**예상 결과**:
- 호칭: "대표님"
- 응답 예시: "대표님, 오늘 일정을 확인해 드리겠습니다..."

---

### 시나리오 2: 긍정 감정 + 직책 없음
**입력**:
- 사용자: 테스트 계정 (metadata: nickname="joann", position=null)
- 메시지: "좋은 아침이에요!"
- 감정: happiness (0.7)

**예상 결과**:
- 호칭: "joann님"
- 응답 예시: "joann님, 좋은 아침입니다! 오늘도 활기찬 하루 되세요..."

---

### 시나리오 3: 부정 감정 + 직책 없음
**입력**:
- 사용자: 이고은 (metadata: nickname="joann", position=null로 임시 변경)
- 메시지: "너무 힘들어요... 도와주세요"
- 감정: sadness (0.75)

**예상 결과**:
- 호칭: "이고은님"
- 응답 예시: "이고은님, 힘든 상황이시군요. 제가 도와드릴 수 있는 부분이 있을까요?..."

---

### 시나리오 4: 중립 감정 + nickname 없음
**입력**:
- 사용자: 김종태 (metadata: position=null, nickname=null, name="김종태")
- 메시지: "오늘 날씨 어때?"
- 감정: neutral (0.6)

**예상 결과**:
- 호칭: "종태님" (name에서 성 제외)
- 응답 예시: "종태님, 오늘 날씨를 확인해 드리겠습니다..."

---

## 7. 검증 방법

### 로그 확인
```bash
# rb8001 로그에서 호칭 결정 로그 확인
docker logs rb8001 --tail 100 | grep "Preferred name"

# 예상 출력:
# Emotion: happiness, Preferred name: 대표님
# Emotion: sadness, Preferred name: 이고은님

DB 쿼리

-- 최근 10분 감정 평균 확인
SELECT 
    user_id,
    AVG((probs->>'happiness')::float) as avg_happiness,
    AVG((probs->>'sadness')::float) as avg_sadness,
    AVG((probs->>'fear')::float) as avg_fear
FROM emotion_readings
WHERE created_at > NOW() - INTERVAL '10 minutes'
GROUP BY user_id;

-- 사용자 metadata 확인
SELECT id, name, metadata->>'nickname' as nickname, metadata->>'position' as position
FROM "user"
WHERE email = 'goeun@ro-being.com';

API 테스트

# JWT 토큰으로 메시지 전송
curl -X POST http://192.168.219.52:8001/api/message \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text": "고마워요!"}'
  
# 응답에 "대표님" 또는 "joann님" 포함 확인

8. 예상 효과

사용자 경험 개선

  • 공감 능력: 감정 상태에 맞는 호칭으로 감정적 공명 강화
  • 개인화: nickname 활용으로 친밀감 향상
  • 문화적 적합성: 한국 직장 문화의 직책 호칭 존중

기술적 이점

  • 일관성: 휴리스틱 규칙으로 예측 가능한 동작
  • 확장성: 규칙 추가/수정 용이 (코드 레벨)
  • 성능: DB 조회 기반으로 빠른 응답 (LLM 호출 불필요)

9. 향후 개선 방향

단기 (1-2주)

  • 기본 호칭 시스템 구현
  • A/B 테스트: 사용자 만족도 비교 (호칭 on/off)
  • 로그 분석: 호칭 사용 빈도 및 패턴 파악

중기 (1-2개월)

  • 시간대별 호칭 변화 (아침/저녁 인사 차별화)
  • Intent 기반 호칭: 업무(직책) vs 잡담(이름)
  • 사용자 피드백 수집 (Slack 이모지 반응)

장기 (3-6개월)

  • 강화학습: 사용자 반응 학습하여 호칭 선호도 자동 조정
  • 다국어 지원: 영어권 사용자 호칭 전략 (Mr./Ms./First name)
  • 음성 인터페이스: 발화 시 억양/톤 조절

10. 관련 문서


교훈 및 인사이트

설계 과정에서 배운 점

  1. 휴리스틱 vs AI: 명확한 규칙이 있는 경우 휴리스틱이 더 효과적 (일관성, 속도, 디버깅)
  2. 문화적 맥락: 한국어 호칭 체계는 단순 번역이 아닌 사회문화적 이해 필요
  3. 감정 안정화: 단일 메시지 감정은 변동성 크므로 최근 평균 사용이 안정적

주의할 점

  • 프라이버시: 직책/별명 정보 노출 주의 (로그, 에러 메시지)
  • 폴백 처리: DB 조회 실패, 감정 분석 실패 시 안전한 기본값 ("사용자님")
  • 테스트 커버리지: 엣지 케이스 (빈 문자열, NULL, 특수문자) 처리 필수

다음 단계: 이 계획을 바탕으로 database.py, router.py, llm_service.py 수정 시작