From fcf90fd56d48ec95ef6b7e69038a17756e8b813b Mon Sep 17 00:00:00 2001 From: Claude-51124 Date: Thu, 4 Dec 2025 15:44:18 +0900 Subject: [PATCH] Add emotion-based addressing system plan (concise) --- .../251204_emotion_based_addressing_system.md | 474 +++--------------- 1 file changed, 62 insertions(+), 412 deletions(-) diff --git a/journey/plans/251204_emotion_based_addressing_system.md b/journey/plans/251204_emotion_based_addressing_system.md index cfbbedc..fcbffe5 100644 --- a/journey/plans/251204_emotion_based_addressing_system.md +++ b/journey/plans/251204_emotion_based_addressing_system.md @@ -1,459 +1,109 @@ -# 감정 기반 호칭 시스템 구현 계획 +# 감정 기반 호칭 시스템 구현 **작성일**: 2025-12-04 **작성자**: happybell80 -**상태**: 계획 단계 +**상태**: 계획 --- -## 1. 배경 및 동기 +## 배경 및 목표 -### 문제 인식 -- 현재 로빙(Robeing)은 모든 사용자를 "사용자님" 또는 `user.name`으로 획일적으로 호칭 -- Slack DM에서는 `user['name']`님 형식 사용 (예: "이고은님") -- 사용자 감정 상태나 상황에 따른 호칭 차별화 없음 -- `metadata JSONB` 컬럼 추가로 nickname, position 등 저장 가능해짐 +- 현재: 모든 사용자를 "사용자님" 또는 `user.name`으로 획일적 호칭 +- 문제: 감정 상태/상황 무시, `metadata` (nickname, position) 미활용 +- 목표: 감정 기반 호칭 동적 변경으로 공감 능력 강화, 한국 직장 문화 존중 표현 -### 목표 -- **감정 상태**에 따라 호칭을 동적으로 변경하여 공감 능력 강화 -- **직책 정보**를 활용하여 한국 직장 문화의 존중 표현 -- **휴리스틱 규칙** 기반으로 일관성 및 예측 가능성 보장 +## 조사 결과 (웹 검색) ---- +**한국어 호칭 원칙**: +- 친밀한 관계: 별명/애칭 사용 +- 공식적/부정적 상황: 정식 이름 사용 +- 출처: 한국어 호칭 연구, UX 라이팅 전략 -## 2. 조사 및 연구 +**AI 감정 반응**: +- 챗GPT emotional rebound 효과: 부정 → 위로, 긍정 → 친근 +- 출처: 프랑크 바르돌 연구 (m.etnews.com) -### 웹 검색 결과 요약 +**직장 문화**: 직책 호칭 최우선 (대표님, 이사님) -#### 한국어 호칭 원칙 -- **친밀한 관계**: 별명/애칭 사용 -- **공식적/부정적 상황**: 성명 사용 -- **감정이 부정적일 때**: 거리감을 두기 위해 정식 이름 사용하는 경향 -- 출처: 한국어 호칭 연구, UX 라이팅 전략 ([visualflow.co.kr](https://visualflow.co.kr)) +## 설계 결정 -#### AI 감정 반응 연구 -- 챗GPT의 **emotional rebound 효과**: 사용자 감정에 따라 답변 톤 조절 -- 부정적 말투 → 위로하는 답변 -- 긍정적 감정 → 친근한 답변 -- 출처: 프랑크 바르돌 연구 ([m.etnews.com](https://m.etnews.com)) +### 방식: 휴리스틱 규칙 (Python) -#### 한국 직장 문화 -- 직책 호칭이 최우선 (대표님, 이사님, 팀장님 등) -- 업무 맥락에서는 이름보다 직책 호칭 선호 -- 개인적 대화에서는 이름 호칭 가능 +이유: 일관성, 속도, 예측 가능성, 제어 용이 +LLM 역할: 결정된 호칭 자연스럽게 사용만 -### 권장 매핑 전략 -1. **긍정/편안한 감정** (happiness/surprise/neutral) → 별명/애칭(nickname) 사용 -2. **부정/스트레스 감정** (fear/anger/sadness) → 정식 이름(full name)으로 존중과 위로 표현 -3. **매우 부정적** → 더 공손한 톤과 함께 정식 이름 -4. **직책 있음** → 감정 무관하게 직책 호칭 우선 +### 호칭 결정 규칙 ---- +| 조건 | 호칭 | +|------|------| +| `metadata->>'position'` 있음 | "대표님", "이사님" (감정 무관) | +| 긍정 감정 + position 없음 | `metadata->>'nickname'` → "joann님" | +| 부정 감정 + position 없음 | `user.name` → "이고은님" | +| nickname 없음 | `user.name` 성 제외 → "고은님" | -## 3. 설계 의사결정 +**긍정 감정**: happiness, surprise, neutral +**부정 감정**: fear, anger, sadness, disgust +**감정 소스**: emotion_readings 최근 10분 평균 (현재 메시지 가중치 2배) -### 핵심 질문: 휴리스틱 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. 호칭 결정 휴리스틱 규칙 - -### 의사결정 트리 +### 1. database.py +**추가**: `get_user_preferred_name(user_id, current_emotion) -> str` +- user 테이블에서 name, metadata 조회 +- emotion_readings 최근 10분 평균 계산 +- 규칙 적용하여 호칭 반환 +### 2. router.py:264-283 +**추가**: ```python -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] +# 감정 분석 후 +preferred_name = await get_user_preferred_name(user_id, user_emotion) +context['preferred_name'] = preferred_name ``` ---- - -## 5. 구현 계획 - -### 5.1. 파일 수정 목록 - -#### A. [`rb8001/app/state/database.py`](../../rb8001/app/state/database.py) -**새 함수 추가**: +### 3. llm_service.py:131-132 +**수정**: system_instruction에 호칭 지시 추가 ```python -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`](../../rb8001/app/router/router.py) -**수정 위치**: `_call_internal_llm()` 메서드 (264-283라인) - -**추가 로직**: -```python -# 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`](../../rb8001/app/services/llm/llm_service.py) -**수정 위치**: `process_request()` 메서드 (131-132라인) - -**수정 내용**: -```python -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`](../book/300_architecture/database/tables.md) -**업데이트 내용**: -```markdown -### 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" - } -} +### 4. tables.md +**추가**: user 테이블 metadata 컬럼 설명 ``` +| metadata | JSONB | YES | nickname, position, short_name, preferences | ``` ---- +## 테스트 시나리오 -### 5.2. 구현 순서 +1. **긍정+직책**: "고마워요" (happiness) + position="대표" → "대표님" +2. **긍정+no직책**: "좋은 아침" (happiness) + nickname="joann" → "joann님" +3. **부정+no직책**: "힘들어요" (sadness) + name="이고은" → "이고은님" +4. **중립**: "날씨는?" (neutral) + name="김종태" → "종태님" -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 쿼리 +**DB**: ```sql --- 최근 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'; +-- 최근 10분 감정 평균 +SELECT user_id, AVG((probs->>'happiness')::float) FROM emotion_readings +WHERE created_at > NOW() - INTERVAL '10 minutes' GROUP BY user_id; ``` -### API 테스트 +**API**: ```bash -# 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님" 포함 확인 + -H "Authorization: Bearer $JWT" -d '{"text": "고마워요!"}' ``` ---- +## 관련 문서 -## 8. 예상 효과 - -### 사용자 경험 개선 -- **공감 능력**: 감정 상태에 맞는 호칭으로 감정적 공명 강화 -- **개인화**: nickname 활용으로 친밀감 향상 -- **문화적 적합성**: 한국 직장 문화의 직책 호칭 존중 - -### 기술적 이점 -- **일관성**: 휴리스틱 규칙으로 예측 가능한 동작 -- **확장성**: 규칙 추가/수정 용이 (코드 레벨) -- **성능**: DB 조회 기반으로 빠른 응답 (LLM 호출 불필요) - ---- - -## 9. 향후 개선 방향 - -### 단기 (1-2주) -- [x] 기본 호칭 시스템 구현 -- [ ] A/B 테스트: 사용자 만족도 비교 (호칭 on/off) -- [ ] 로그 분석: 호칭 사용 빈도 및 패턴 파악 - -### 중기 (1-2개월) -- [ ] 시간대별 호칭 변화 (아침/저녁 인사 차별화) -- [ ] Intent 기반 호칭: 업무(직책) vs 잡담(이름) -- [ ] 사용자 피드백 수집 (Slack 이모지 반응) - -### 장기 (3-6개월) -- [ ] 강화학습: 사용자 반응 학습하여 호칭 선호도 자동 조정 -- [ ] 다국어 지원: 영어권 사용자 호칭 전략 (Mr./Ms./First name) -- [ ] 음성 인터페이스: 발화 시 억양/톤 조절 - ---- - -## 10. 관련 문서 - -- [[tables.md]](../book/300_architecture/database/tables.md) - user 테이블 스키마 -- [[230_감정윤리_필터_LLM후처리와_정체성]](../book/200_core_design/230_감정윤리_필터_LLM후처리와_정체성.md) - 감정 분석 철학 -- [[311_FastAPI_구조_원칙]](../book/300_architecture/311_FastAPI_구조_원칙.md) - 코드 작성 원칙 -- [[251016_emotion_integration_plan]](251016_emotion_integration_plan.md) - 감정 시스템 통합 계획 - ---- - -## 교훈 및 인사이트 - -### 설계 과정에서 배운 점 -1. **휴리스틱 vs AI**: 명확한 규칙이 있는 경우 휴리스틱이 더 효과적 (일관성, 속도, 디버깅) -2. **문화적 맥락**: 한국어 호칭 체계는 단순 번역이 아닌 사회문화적 이해 필요 -3. **감정 안정화**: 단일 메시지 감정은 변동성 크므로 최근 평균 사용이 안정적 - -### 주의할 점 -- **프라이버시**: 직책/별명 정보 노출 주의 (로그, 에러 메시지) -- **폴백 처리**: DB 조회 실패, 감정 분석 실패 시 안전한 기본값 ("사용자님") -- **테스트 커버리지**: 엣지 케이스 (빈 문자열, NULL, 특수문자) 처리 필수 - ---- - -**다음 단계**: 이 계획을 바탕으로 `database.py`, `router.py`, `llm_service.py` 수정 시작 +- [[tables.md]](../book/300_architecture/database/tables.md) +- [[230_감정윤리_필터]](../book/200_core_design/230_감정윤리_필터_LLM후처리와_정체성.md) +- [[311_FastAPI_구조_원칙]](../book/300_architecture/311_FastAPI_구조_원칙.md)