DOCS/journey/ideas/250819_대화형_점진적_의도_구축_시스템.md
Claude-51124 22557e7132 docs: 오래된 트러블슈팅 아카이브 및 구조 정리
- 7-8월 초기 구축 문서 12개를 _archive/troubleshooting/2025_07-08_initial_setup/로 이동
- book/300_architecture/390_human_in_the_loop_intent_learning.md를 journey/research/intent_classification/로 이동 (개발 여정 문서)
- 빈 폴더 제거 (journey/assets/*)
2025-11-17 14:06:05 +09:00

21 KiB

대화형 점진적 의도 구축 시스템 (Conversational Progressive Intent Building)

작성일: 2025-08-19
작성자: happybell80 & Claude
관련 서비스: rb10508_micro, skill-email, skill-news
문서 상태: 아이디어 제안


1. 개요

1.1 배경

현재 로빙 시스템은 단일 턴(single-turn) 명령 처리에 최적화되어 있습니다. 하지만 실제 사용자는 한 번에 명확한 지시를 하지 않고, 여러 번의 대화를 통해 점진적으로 의도를 구체화합니다.

1.2 목표

  • 자연스러운 멀티턴(multi-turn) 대화 지원
  • 컨텍스트를 유지하며 정보를 점진적으로 수집
  • 사용자 의도가 완성되면 자동으로 실행
  • "기억하고 성장하는 디지털 동료" 비전 실현

[참고] 이 시스템은 현재 네이버웍스 슬랙 브리핑 프로젝트(Phase 2+)와 같은 실제 환경에서 활발히 구현 및 테스트되고 있습니다. 관련 문제 해결 기록은 여기에서 확인할 수 있습니다.

1.3 핵심 가치

  • 자연스러움: 사람 간 대화처럼 자연스러운 상호작용
  • 지능적 수집: 필요한 정보를 적절한 시점에 요청
  • 컨텍스트 인식: 이전 대화를 기억하고 참조

2. 시스템 아키텍처

2.1 전체 구조

┌──────────────────────────────────────────────────────┐
│                    사용자 인터페이스                    │
└────────────────────────┬─────────────────────────────┘
                         │
┌────────────────────────▼─────────────────────────────┐
│                    로빙 코어 (rb10508_micro)          │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────┐ │
│  │ 의도 분류기 │  │ 대화 관리자  │  │ 응답 생성기 │ │
│  └─────────────┘  └──────────────┘  └─────────────┘ │
└────────┬──────────────┬──────────────┬───────────────┘
         │              │              │
    ┌────▼────┐    ┌───▼────┐    ┌───▼────┐
    │ Mistral │    │ Redis  │    │ Gemini │
    └─────────┘    └────────┘    └────────┘
         │              │              │
    ┌────▼──────────────▼──────────────▼────┐
    │        컨텍스트 관리 시스템            │
    │  ┌──────────┐  ┌──────────┐          │
    │  │ 세션관리 │  │ 슬롯필링 │          │
    │  └──────────┘  └──────────┘          │
    └────────────────┬───────────────────────┘
                     │
    ┌────────────────▼───────────────────────┐
    │           스킬 서비스 레이어            │
    │  ┌──────────┐  ┌──────────┐          │
    │  │  Email   │  │   News   │  ...     │
    │  └──────────┘  └──────────┘          │
    └────────────────────────────────────────┘

2.2 컴포넌트 설명

2.2.1 의도 분류기 (Intent Classifier)

  • 사용자 발화를 대분류로 분류 (email/news/general)
  • 기존 컨텍스트 연속인지 새 작업인지 판단
  • Mistral API 활용

2.2.2 대화 관리자 (Dialogue Manager)

  • 대화 흐름 제어
  • 필요한 정보 파악 및 질문 생성
  • 실행 트리거 감지

2.2.3 컨텍스트 관리 시스템

  • 사용자별 세션 상태 유지
  • 수집된 정보(슬롯) 관리
  • 타임아웃 및 생명주기 관리

2.2.4 슬롯 필링 (Slot Filling)

  • 각 스킬별 필수/선택 정보 정의
  • 발화에서 정보 추출
  • 정보 충돌 해결

3. 대화 흐름 시퀀스

3.1 전체 대화 시나리오

sequenceDiagram
    participant U as 사용자
    participant R as 로빙
    participant C as 컨텍스트관리
    participant M as Mistral
    participant S as 스킬서비스
    
    Note over U,S: 이메일 전송 대화 시나리오
    
    %% 턴 1: 작업 시작
    U->>R: "이메일 보낼건데..."
    R->>M: 의도 분류 요청
    M-->>R: intent: email
    R->>C: 새 컨텍스트 생성
    C-->>R: context_id: abc123
    R->>U: "네, 이메일을 보내시겠군요. 누구에게 보내실까요?"
    
    %% 턴 2: 수신자 정보
    U->>R: "happybell 지메일로 보내줘"
    R->>C: 컨텍스트 조회
    C-->>R: 기존 컨텍스트 반환
    R->>M: 슬롯 추출 (수신자)
    M-->>R: recipient: happybell@gmail.com
    R->>C: 슬롯 업데이트
    R->>U: "happybell@gmail.com으로 보내드리겠습니다. 어떤 내용을 보내실까요?"
    
    %% 턴 3: 내용 수집
    U->>R: "행사에 와주셔서 감사하다는 내용이야"
    R->>C: 컨텍스트 조회
    R->>M: 슬롯 추출 (내용)
    M-->>R: content: 감사 인사
    R->>C: 슬롯 업데이트
    R->>U: "감사 인사 메일이군요. 어떤 톤으로 작성할까요?"
    
    %% 턴 4: 톤 설정
    U->>R: "공손하지만 기쁜 느낌으로"
    R->>C: 슬롯 업데이트 (tone: polite_happy)
    R->>U: "공손하면서도 기쁜 톤으로 작성하겠습니다. 발신자는 어떻게 할까요?"
    
    %% 턴 5: 발신자
    U->>R: "내 이메일로 해"
    R->>C: 사용자 정보 조회
    C-->>R: sender: user@gmail.com
    R->>C: 슬롯 업데이트
    R->>U: "user@gmail.com으로 발신자를 설정했습니다."
    
    %% 턴 6: 추가 요청
    U->>R: "끝에 내 명함도 넣어줘"
    R->>C: 슬롯 업데이트 (signature: true)
    R->>C: 완성도 체크
    C-->>R: 필수 슬롯 완료
    R->>U: "명함을 추가하겠습니다. 다음 내용으로 이메일을 준비했습니다:\n[요약 표시]"
    
    %% 턴 7: 실행
    U->>R: "그래 보내"
    R->>C: 실행 트리거 감지
    R->>S: 이메일 전송 요청
    S-->>R: 전송 완료
    R->>C: 컨텍스트 종료
    R->>U: "이메일을 성공적으로 전송했습니다."

3.2 컨텍스트 상태 전이

stateDiagram-v2
    [*] --> Idle: 초기 상태
    
    Idle --> Collecting: 작업 시작 감지
    
    Collecting --> Collecting: 정보 추가
    Collecting --> Confirming: 필수 슬롯 완료
    Collecting --> Modifying: 수정 요청
    
    Modifying --> Collecting: 수정 완료
    
    Confirming --> Executing: 실행 명령
    Confirming --> Modifying: 수정 요청
    
    Executing --> Completed: 성공
    Executing --> Failed: 실패
    
    Failed --> Collecting: 재시도
    Failed --> Cancelled: 취소
    
    Completed --> [*]: 종료
    Cancelled --> [*]: 종료
    
    Collecting --> Timeout: 30분 무응답
    Confirming --> Timeout: 30분 무응답
    Timeout --> Idle: 리셋

3.3 슬롯 필링 프로세스

flowchart TD
    Start([사용자 발화]) --> Extract[정보 추출]
    Extract --> Check{슬롯 충돌?}
    
    Check -->|예| Confirm[충돌 확인 요청]
    Check -->|아니오| Update[슬롯 업데이트]
    
    Confirm -->|덮어쓰기| Update
    Confirm -->|유지| Keep[기존값 유지]
    
    Update --> Validate[유효성 검증]
    Keep --> CheckComplete
    
    Validate -->|실패| Error[오류 메시지]
    Validate -->|성공| Save[컨텍스트 저장]
    
    Save --> CheckComplete{필수 슬롯\n완료?}
    
    CheckComplete -->|예| Ready[실행 준비 완료]
    CheckComplete -->|아니오| NextQ[다음 질문 생성]
    
    Error --> NextQ
    NextQ --> End([응답 생성])
    Ready --> End

3.4 시간 인식 의도 처리 (신규 추가 2025-09-09)

3.4.1 실제 대화 로그 분석 결과

2025년 9월 9일 PostgreSQL conversation_log 테이블 분석 결과, 로빙의 가장 심각한 문제는 시간 인식 부재입니다.

실제 사례:
사용자: "오늘 몇일이야?"
로빙: "오늘은 2024년 5월 16일 목요일입니다" ❌ (실제: 2025년 9월 9일)

3.4.2 시급한 의도 파악 시나리오

시나리오 1: 시간 질문 의도

TIME_QUERY_PATTERNS = {
    "absolute_time": r"몇시|몇일|무슨요일|오늘|내일",
    "relative_time": r"어제|아까|방금|조금전",
    "time_reference": r"그때|언제|며칠"
}

# 의도 분류 시 시간 컨텍스트 필수 주입
if any(pattern in text for pattern in TIME_QUERY_PATTERNS.values()):
    context["current_time"] = datetime.now(KST).strftime("%Y년 %m월 %d일 %H:%M")
    context["needs_time"] = True

시나리오 2: 맥락 참조 의도

CONTEXT_RETRIEVAL_PATTERNS = {
    "past_reference": r"어제.*했|아까.*말한|그때.*것",
    "modification": r"바꿔|수정|변경|취소"
}

# DB에서 과거 대화 조회 필수
if pattern_matches(CONTEXT_RETRIEVAL_PATTERNS):
    past_logs = await fetch_conversation_log(
        user_id=user_id,
        time_range="-2 hours",
        limit=5
    )
    context["past_context"] = past_logs

시나리오 3: 감정적 피드백 의도

NEGATIVE_FEEDBACK_PATTERNS = {
    "frustration": r"모르는구나|답답|짜증",
    "correction": r"아니야|틀렸어|다시",
    "sarcasm": r"그래그래|알았어알았어"
}

# 부정 피드백 감지 시 시스템 점검
if pattern_matches(NEGATIVE_FEEDBACK_PATTERNS):
    log_system_error("User frustration detected")
    return apologize_and_clarify()

3.4.3 제로샷 의도 분류 통합 (Hong et al., 2024 기반)

최신 SIGDIAL 2024 연구에 따르면, 고품질 의도 설명 1개와 임베딩 기반 후보 축소가 핵심입니다.

class ZeroShotTimeAwareClassifier:
    def __init__(self):
        # 제로샷: 학습 없이 의도 설명만으로 분류
        self.intent_descriptions = {
            "attendance": "출근, 퇴근, 재택근무 같은 근태를 기록하려는 요청",
            "time_query": "현재 시각, 날짜, 요일을 묻는 질문",
            "context_retrieval": "아까, 어제, 방금 전 같은 과거 대화를 참조하는 요청",
            "email": "이메일 확인, 전송, 검색과 관련된 요청",
            "schedule": "일정 조회, 등록, 수정에 대한 요청"
        }
        
        # 다국어 임베딩 (한국어 지원, Snips 대체)
        self.embedder = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')
        self.gate = SelfAttentiveGate()  # 시간 게이트
        
    async def classify_with_time(self, text, user_id):
        # Step 1: 임베딩 기반 후보 축소 (20ms)
        text_emb = self.embedder.encode(text)
        intent_scores = {}
        
        for intent, description in self.intent_descriptions.items():
            desc_emb = self.embedder.encode(description)
            score = cosine_similarity(text_emb, desc_emb)
            intent_scores[intent] = score
        
        # 상위 3개 후보만 선택 (Hong et al. 권장: 10-13개, 로빙은 3개면 충분)
        top_candidates = sorted(intent_scores.items(), 
                               key=lambda x: x[1], 
                               reverse=True)[:3]
        
        # Step 2: 확신도 체크 및 시간 게이트
        best_intent = top_candidates[0][0]
        confidence = top_candidates[0][1]
        
        # Step 3: 시간 컨텍스트 선택적 추가
        context = {}
        if best_intent in ['time_query', 'attendance', 'context_retrieval']:
            context['current_time'] = datetime.now(KST).strftime("%Y년 %m월 %d일 %H:%M")
            context['needs_time'] = True
            
        # Step 4: 저확신 시 LLM 폴백
        if confidence < 0.7:
            # 의도 설명을 포함한 프롬프트로 LLM 호출
            prompt = f"사용자: {text}\n후보 의도:\n"
            for intent, score in top_candidates:
                prompt += f"- {intent}: {self.intent_descriptions[intent]} (점수: {score:.2f})\n"
            context['needs_llm'] = True
            
        return {
            "intent": best_intent, 
            "confidence": confidence,
            "context": context,
            "candidates": top_candidates
        }

제로샷 방식의 장점 (로빙 적용)

  1. 학습 데이터 불필요: Snips와 달리 의도 설명만 수정하면 즉시 반영(제로샷)
  2. 비용 절감 목표: 임베딩 후보 축소+캐싱으로 LLM 호출 70% 이상 감소(초기 목표, 상향 가능)
  3. 시간·맥락 인식 강화: 시간 의도/과거 참조 의도에 현재 KST와 최근 대화 일부를 주입해 오분류 감소
  4. 통합 임베딩 활용: 단일 임베딩 모델로 의도·감정·윤리를 동시에 처리(프로토타입/라벨 묶음 기반 유사도 비교)
  5. 유연 확장성: 새 의도/윤리 규칙 추가 시 라벨/설명만 추가하여 재학습 없이 확장
통합 임베딩 전략(의도·감정·윤리)
  • 단일 다국어 임베딩(예: mpnet/miniLM/한국어 특화 대안)을 skill-embedding 서비스로 일원화
  • 라벨 세트 관리: intent_labels, emotion_labels, ethics_principles를 벡터로 사전 임베딩하여 DB/캐시 보관
  • 분류 방식: 입력 임베딩 ↔ 라벨 임베딩 코사인 유사도 상위 N 후보 산출 → 임계값/온톨로지 규칙으로 최종 결정
  • 멀티헤드 규칙: 의도·감정·윤리 점수를 동시 산출하되, 충돌 시 우선순위(보안/윤리 > 의도)로 게이트
  • 캐싱: 동일/유사 문장 해시 기반 캐시로 임베딩/LLM 호출 재사용(세션·사용자별 TTL)
측정/목표(운영 관점)
  • LLM 호출 비율: 제로샷 후보 상위 확신도 임계값(예: 0.75) 상향 조절로 70%↓ 달성(분기별 목표 상향)
  • 정확도: 상위-1/상위-3 의도 일치율, 되묻기 진입률/해결률, 오분류 재발률
  • 시간 인식 개선: 시간 관련 질의 응답 정합성(오늘/어제/요일) 오류율 < 1%
  • 윤리 게이트 성과: 정책 위반 차단률, 오검출률(사용자 불편) 모니터링

4. 데이터 구조

4.1 컨텍스트 스키마

class WorkContext:
    """작업 컨텍스트"""
    context_id: str          # 고유 ID
    user_id: str            # 사용자 ID
    session_id: str         # 세션 ID
    
    # 작업 정보
    intent: str             # 대분류 (email/news/general)
    sub_intent: str         # 세부 의도
    state: ContextState     # 현재 상태
    
    # 슬롯 정보
    slots: Dict[str, Any]   # 수집된 정보
    required_slots: List    # 필수 슬롯
    optional_slots: List    # 선택 슬롯
    
    # 대화 히스토리
    turns: List[Turn]       # 대화 턴 목록
    
    # 메타데이터
    created_at: datetime
    updated_at: datetime
    expires_at: datetime    # TTL

4.2 이메일 스킬 슬롯 예시

email_slots = {
    "required": {
        "recipient": {
            "type": "email",
            "question": "누구에게 보내실까요?",
            "validation": email_regex
        },
        "content": {
            "type": "text",
            "question": "어떤 내용을 보내실까요?",
            "min_length": 10
        }
    },
    "optional": {
        "subject": {
            "type": "text",
            "question": "제목을 입력하시겠습니까?",
            "default": "안녕하세요"
        },
        "tone": {
            "type": "enum",
            "options": ["formal", "casual", "friendly"],
            "question": "어떤 톤으로 작성할까요?"
        },
        "attachments": {
            "type": "list",
            "question": "첨부파일이 있나요?"
        }
    }
}

5. 구현 전략

5.1 단계별 구현 (Phase Approach)

Phase 1: MVP (2주)

  • 3턴 대화 지원
  • 하드코딩된 이메일 슬롯
  • 인메모리 컨텍스트
  • 코드 증가: +400줄

Phase 2: 기본 구현 (1개월)

  • 5턴 대화 지원
  • Redis 세션 관리
  • 2개 스킬 (email, news)
  • 코드 증가: +800줄

Phase 3: 완전 구현 (2개월)

  • 무제한 턴 지원
  • 동적 슬롯 시스템
  • 모든 스킬 통합
  • 코드 증가: +1,750줄

5.2 함수형 프로그래밍 적용

# 순수 함수 예시
def extract_slots(text: str, slot_schema: Dict) -> Dict:
    """텍스트에서 슬롯 추출 - 순수 함수"""
    extracted = {}
    for slot_name, slot_def in slot_schema.items():
        if pattern := slot_def.get('pattern'):
            if match := re.search(pattern, text):
                extracted[slot_name] = match.group(1)
    return extracted

# 상태 불변성
def update_context(context: WorkContext, slots: Dict) -> WorkContext:
    """컨텍스트 업데이트 - 새 객체 반환"""
    return WorkContext(
        **{**context.__dict__, 
           'slots': {**context.slots, **slots},
           'updated_at': datetime.now()}
    )

# I/O 분리
async def process_turn(user_input: str, user_id: str):
    """턴 처리 - I/O와 로직 분리"""
    # I/O: 컨텍스트 조회
    context = await fetch_context(user_id)
    
    # 순수 함수: 의도 분석
    intent = classify_intent(user_input)
    
    # 순수 함수: 슬롯 추출
    slots = extract_slots(user_input, get_schema(intent))
    
    # 순수 함수: 컨텍스트 업데이트
    new_context = update_context(context, slots)
    
    # I/O: 저장
    await save_context(new_context)
    
    # 순수 함수: 응답 생성
    response = generate_response(new_context)
    
    return response

6. 성능 분석

6.1 리소스 사용량 (7턴 대화 기준)

리소스 현재 방식 제안 방식 증가율
LLM 호출 1회 22회 22배
DB 접근 0회 24회 -
응답 시간 500ms 2,600ms 5.2배
토큰 사용 ~200 ~2,000 10배
메모리 50MB 100MB 2배

6.2 최적화 전략

  1. 병렬 처리

    • 의도 분류와 슬롯 추출 동시 실행
    • 턴당 200ms 절감
  2. 캐싱

    • 자주 사용하는 질문 템플릿
    • 의도 분류 결과 단기 캐싱
  3. 배치 처리

    • 대화 히스토리 배치 저장
    • 슬롯 검증 일괄 처리

최적화 후 목표: 턴당 150-200ms


7. 장단점 분석

7.1 장점

  • 자연스러운 대화 경험
  • 작업 완성률 향상 (60% → 95%)
  • 사용자 만족도 증가
  • "디지털 동료" 비전 실현

7.2 단점

  • 코드 복잡도 5배 증가
  • 디버깅 난이도 상승
  • LLM 비용 20배 증가
  • 응답 지연 (즉각 → 2-3초)

7.3 리스크

  • 세션 동기화 문제
  • 컨텍스트 오염
  • 타임아웃 관리
  • 스케일링 이슈

8. 대안 검토

8.1 하이브리드 접근

  • 간단한 명령: 현재 방식 유지
  • 복잡한 작업: 대화형 모드 전환
  • 사용자가 모드 선택

8.2 프롬프트 개선

  • 한 번에 모든 정보 요청하는 프롬프트
  • 예: "이메일을 보내려면 수신자, 내용, 톤을 말씀해주세요"

8.3 UI 기반 접근

  • 프론트엔드에서 폼 제공
  • 대화와 폼 병행 사용

9. 결론 및 제안

9.1 결론

대화형 점진적 의도 구축 시스템은 사용자 경험을 크게 향상시킬 수 있지만, 구현 복잡도와 운영 비용이 상당히 증가합니다.

9.2 제안

  1. Phase 1 MVP로 시작: 이메일 스킬만 대상으로 POC 구현
  2. 사용 패턴 분석: 실제로 멀티턴 대화가 필요한지 데이터 수집
  3. 점진적 확장: 필요성이 검증되면 Phase 2, 3 진행

9.3 우선순위

현재 rb10508_micro의 "경량화" 목표를 고려하면:

  1. 먼저 단순 의도 분류 개선
  2. 기본적인 2-3턴 대화 지원
  3. 필요시 전체 시스템 구현

10. 참고 자료


작성자 노트: 이 시스템은 "기억하고 성장하는 디지털 동료"라는 로빙의 핵심 비전을 실현하는 중요한 단계입니다. 하지만 현실적인 제약을 고려하여 단계적 접근을 권장합니다.