# skill-embedding 서비스 구축 및 rb10508_micro 최적화

**날짜**: 2025-08-05  
**작업자**: happybell80 & Claude  
**관련 서버**: 51124 (skill-embedding 서비스)

> 2025-11-22 업데이트: 현재 skill-embedding은 ko-sroberta(multitask) 768차원으로 전환됨. 이하 384차원 설정은 초기 구축 기록이며, 차원 불일치 대응은 [251122_happybell80_chromadb_dimension_mismatch.md] 참고.

## 오전 10시 30분

### 임베딩 서비스 분리 결정

**배경**:
- rb10508_micro가 987.9MB 메모리 사용 (ONNX 임베딩 모델 포함)
- 각 로빙마다 동일한 임베딩 모델 중복 로드
- 향후 로빙 추가 시 메모리 부담 가중

**해결 방안**:
- 중앙 임베딩 서비스 구축 (skill-embedding)
- 모든 로빙이 HTTP API로 임베딩 요청
- 메모리 절약: 로빙당 ~500MB 절감 예상

## 오전 10시 45분

### 서비스 설계 및 개발

**주요 결정사항**:
1. **포트 번호**: 8600 → 8515 (스킬 서비스 포트 범위)
2. **서비스 이름**: embedding_service → skill-embedding
3. **접근 방식**: 내부 전용 (nginx 프록시 불필요)

**구현 내용**:
```python
# FastAPI 엔드포인트
POST /embed - 텍스트 → 임베딩 변환
GET /health - 헬스체크
```

**기술 스택**:
- FastAPI + Uvicorn
- ONNX Runtime
- multilingual-MiniLM-L12-v2 모델
- 384차원 임베딩

## 오전 11시 00분

### 51124 서버 사전 준비

**서버팀 작업**:
```bash
# 디렉토리 생성
mkdir -p /home/admin/ivada_project/skill-embedding/logs
chmod 777 logs

# ONNX 모델 권한 설정
chown -R 999:999 /home/admin/ivada_project/onnx_models

# 환경변수 설정
cat > .env << EOF
PORT=8515
SERVICE_NAME=skill-embedding
LOG_LEVEL=INFO
MODEL_PATH=/models/onnx/multilingual-MiniLM-L12-v2
EOF

# Docker 공간 정리 (16.16GB 확보)
docker system prune -a -f
```

## 오전 11시 15분

### 배포 및 검증

**Gitea Actions 배포**:
- SSH 키 기반 51124 서버 배포
- Docker Compose로 컨테이너 실행
- 헬스체크 통과

**검증 결과**:
```bash
# 컨테이너 상태
CONTAINER ID   IMAGE              STATUS         PORTS     NAMES
abc123def      skill-embedding    Up 25 seconds  8515/tcp  skill-embedding

# 메모리 사용량
skill-embedding: 874.4MB (예상 범위 내)

# API 테스트
curl http://localhost:8515/health
{"status":"healthy","service":"skill-embedding","model":"multilingual-MiniLM-L12-v2","uptime":25.31}

# 임베딩 테스트
curl -X POST http://localhost:8515/embed \
  -H "Content-Type: application/json" \
  -d '{"texts":["테스트"]}'
# 384차원 벡터 정상 반환
```

## 오전 11시 20분

### ChromaDB 통합 준비

**HTTPEmbeddingFunction 구현**:
```python
class HTTPEmbeddingFunction(EmbeddingFunction):
    def __init__(self, embedding_service_url="http://localhost:8515"):
        self.url = f"{embedding_service_url}/embed"
        
    def __call__(self, texts):
        response = requests.post(self.url, json={"texts": texts})
        return response.json()["embeddings"]
```

**적용 대상**:
- rb10508_micro: 988.1MB → ~400MB (예상)
- rb8001: 추후 적용
- rb10408: 추후 적용

## 교훈

1. **서비스 분리의 이점**
   - 중복 제거로 메모리 효율성 향상
   - 중앙 관리로 유지보수 용이
   - 확장성 확보 (새 로빙 추가 시 임베딩 서비스 재사용)

2. **내부 서비스 설계**
   - nginx 프록시 불필요한 내부 API는 복잡도 감소
   - localhost 통신으로 충분한 성능
   - 보안상 외부 노출 불필요

3. **사전 준비의 중요성**
   - 서버팀과 긴밀한 협업으로 원활한 배포
   - 권한 설정 (logs 777, ONNX 모델 999:999) 필수
   - Docker 공간 확보로 빌드 실패 방지

4. **단계적 적용 전략**
   - 새 서비스 먼저 안정화
   - 하나의 로빙(rb10508_micro)에 시범 적용
   - 성공 후 다른 로빙들에 확산

5. **모니터링 지표**
   - 메모리 사용량: 874.4MB (ONNX 모델 포함)
   - 응답 시간: 단일 텍스트 ~50ms
   - 헬스체크: 30초 간격

## 현재 상태

**skill-embedding 서비스**:
- ✅ 정상 가동 중 (포트 8515)
- ✅ 메모리 사용량 안정적 (874.4MB)
- ✅ API 응답 정상
- ✅ 384차원 임베딩 생성 확인

**다음 작업**:
- rb10508_micro의 memory.py 수정
- ONNXEmbeddingFunction → HTTPEmbeddingFunction 교체
- 메모리 절감 효과 측정

## 오후 1시 40분

### rb10508_micro HTTP 임베딩 전환 대성공

**목표**: rb10508_micro의 ONNX 임베딩을 HTTP 방식으로 전환

**구현 방식**:
```python
# memory.py에 간단한 HTTPEmbeddingFunction 추가
class HTTPEmbeddingFunction(EmbeddingFunction):
    def __init__(self):
        self.url = f"{os.getenv('SKILL_EMBEDDING_URL', 'http://localhost:8515')}/embed"
    
    def __call__(self, input: List[str]) -> List[List[float]]:
        if not input:
            return []
        response = requests.post(self.url, json={"texts": input}, timeout=30)
        response.raise_for_status()
        return response.json()["embeddings"]
```

**변경사항**:
1. memory.py: HTTPEmbeddingFunction 직접 구현 (파일 복사 없이)
2. requirements.txt: onnxruntime, transformers 제거
3. docker-compose.yml: ONNX 볼륨 제거, SKILL_EMBEDDING_URL 추가

**배포 결과 - 극적인 메모리 절감**:
```
배포 전: 988.1 MiB
배포 후: 118.4 MiB
절약량: 870 MiB (88% 감소!)
```

**성능 검증**:
- 헬스체크: 정상 (Up 50초, healthy)
- API 응답: 정상 작동
- HTTP 임베딩: 7ms 처리 시간
- skill-embedding 연동: 완벽

## 교훈 (추가)

6. **예상보다 좋은 결과**
   - 목표 400MB → 실제 118MB (예상의 30%)
   - ONNX 제거만으로 870MB 절감
   - PyTorch 의존성이 생각보다 무거웠음

7. **간단한 구현의 힘**
   - 파일 복사 대신 직접 구현 (12줄)
   - 불필요한 추상화 제거
   - 예외처리는 서비스 레벨에서 충분

8. **HTTP 임베딩의 장점**
   - 극적인 메모리 절감 (88%)
   - 7ms 레이턴시는 무시할 수준
   - 중앙 관리로 업데이트 용이

9. **아키텍처 검증**
   - 임베딩 서비스 분리 전략 성공
   - 다른 로빙들도 같은 방식 적용 가능
   - 100개 로빙 = 87GB 메모리 절약 가능

## 오후 2시 16분

### rb10508_micro ChromaDB 경로 표준화

**문제상황**:
- rb10508_micro가 `./chroma_db_micro` 사용 (비표준)
- 다른 모든 로빙은 `./chroma_db` 표준 경로 사용
- 백업 크론잡과 인프라 관리의 일관성 필요

**해결과정**:

1. **서버팀 사전 작업** (51124 서버)
   ```bash
   # 컨테이너 중지
   docker stop rb10508_micro
   
   # 데이터 마이그레이션
   mv chroma_db_micro/* chroma_db/
   
   # 권한 설정
   chown -R 999:999 chroma_db/
   ```

2. **로컬 개발자 작업**
   - docker-compose.yml 수정: `./chroma_db_micro` → `./chroma_db`
   - git commit & push로 배포

3. **검증 결과**
   - ChromaDB 데이터: 17MB → 172KB (압축 후)
   - 기존 기억 완전 보존 ("User: 넌 누구야?" 검색 가능)
   - HTTP 임베딩 정상 작동 (11ms 응답)
   - 메모리 사용량 유지 (120.4MB)

**교훈**:

10. **표준화의 중요성**
    - 모든 로빙이 동일한 디렉토리 구조 사용
    - 백업, 모니터링, 마이그레이션 자동화 가능
    - 인프라 복잡도 감소

11. **안전한 마이그레이션**
    - 서버팀과 협업으로 데이터 손실 방지
    - 컨테이너 중지 → 데이터 이동 → 권한 설정 순서
    - 배포 후 기능 검증 필수

## 오후 2시 47분

### rb10508_micro 최종 최적화 - 패키지 정리

**목표**: 미사용 패키지 제거로 추가 메모리 절약

**제거 대상 패키지**:
1. pytest, pytest-asyncio (테스트 도구)
2. neo4j (그래프 DB 미사용)
3. Pillow (이미지 처리 미사용)
4. passlib[bcrypt] (JWT로 충분)
5. email-validator (이메일 검증 미구현)
6. PyMuPDF (이전에 제거)

**실제 최적화 결과**:
```
메모리: 120MB → 112.7MB (7.3MB 절약)
이미지: 1.09GB → 1.06GB (30MB 절약)
```

**예상 vs 실제 비교**:
| 항목 | 예상 | 실제 | 차이 |
|------|------|------|------|
| 메모리 절약 | 40MB | 7.3MB | -32.7MB |
| 이미지 절약 | 40MB | 30MB | -10MB |

**전체 최적화 과정 요약**:
```
1. 초기상태: 987.9MB RAM, 6.19GB 이미지
2. HTTP전환: 120.0MB RAM, 1.14GB 이미지 (88% 메모리 감소) ← 핵심!
3. PyMuPDF제거: 변화 없음, 이미지 55MB 감소
4. 패키지정리: 112.7MB RAM, 1.06GB 이미지 (7MB 추가 절약)
```

**총 최적화 효과**:
- 메모리: 987.9MB → 112.7MB (88.6% 감소)
- 이미지: 6.19GB → 1.06GB (82.9% 감소)
- 목표 200MB 대비: 56% 달성

**교훈**:

12. **예상과 실제의 차이**
    - 패키지 크기와 실제 메모리 사용량은 다름
    - 대부분의 절약은 HTTP 임베딩 전환에서 발생
    - 추가 최적화는 수익 감소 효과

13. **효율적인 최적화 우선순위**
    - 1순위: 임베딩 모델 제거 (870MB 절약)
    - 2순위: 이미지 크기 최적화 (5GB 절약)
    - 3순위: 미사용 패키지 정리 (7MB 절약)

14. **벤치마크 비교**
    - rb10408_test: 55.7MB (최소 기능)
    - rb10508_micro: 112.7MB (풀 기능)
    - 2배 차이는 추가 기능 대비 효율적

**결론**: rb10508_micro 최적화 완료! HTTP 임베딩 전환이 핵심이었으며, 112.7MB로 목표 초과 달성. 추가 최적화는 비용 대비 효과가 낮음.

## 오후 3시 34분

### rb10508_micro Gemini 모델 404 오류 해결

**문제상황**:
- AI 응답이 "응답 생성 중 오류가 발생했습니다: 404 Resource not found" 반환
- ChromaDB에는 오류 메시지만 저장
- 정상적인 대화 기능 완전 상실

**원인 분석**:
```python
# app/core/brain.py:38
self.gemini_model = genai.GenerativeModel('gemini-pro')  # ❌ 존재하지 않는 모델
```

**해결**:
```python
# 1줄 수정
self.gemini_model = genai.GenerativeModel('gemini-2.5-flash-lite')  # ✅ 정상 모델
```

**검증 결과**:
| 항목 | 이전 | 현재 | 개선도 |
|------|------|------|--------|
| API 연결 | ❌ 404 오류 | ✅ 정상 | 100% |
| 응답 생성 | ❌ 오류 메시지 | ✅ AI 응답 | 100% |
| 응답 속도 | N/A | 0.087초 | 완전 복구 |
| 메모리 저장 | ❌ 오류만 저장 | ✅ 정상 대화 | 100% |

**최종 상태**:
- 메모리: 117.2MB (미세 증가는 정상 작동으로 인한 것)
- 기능: 완전한 AI 대화 서비스 복구
- ChromaDB: 실제 대화 내용 정상 저장

**교훈**:

15. **정확한 모델명의 중요성**
    - LLM API 모델명은 정확해야 함
    - 존재하지 않는 모델은 404 오류 발생
    - 공식 문서 확인 필수

16. **1줄 수정의 파급력**
    - 핵심 기능이 1줄 오류로 완전 정지
    - 빠른 진단과 수정이 중요
    - 테스트 코드로 사전 검증 필요

**총 최적화 프로젝트 성과**:
- 메모리: 987.9MB → 117.2MB (88.1% 감소)
- 기능: 100% 정상 작동
- 목표 달성: 200MB 목표 대비 58.6% 수준

## 오후 4시 02분

### 개발 모드 대화 로깅 logger import 누락

**문제상황**:
- 개발 모드 대화 로깅 기능 추가 후 "name 'logger' is not defined" 에러
- 모든 대화 요청이 실패
- think() 함수에서 예외 발생

**원인**:
```python
# brain.py에 logger import 누락
if settings.DEBUG:
    logger.info(f"[대화] {user_id}: {message} → {final_response[:100]}...")  # logger가 정의되지 않음
```

**해결**:
```python
# brain.py 상단에 추가
import logging

logger = logging.getLogger(__name__)
```

**교훈**:

17. **기본 import 확인**
    - 새 기능 추가 시 필요한 모듈 import 확인 필수
    - 특히 logging 같은 표준 라이브러리도 누락 주의
    - IDE의 자동 import 기능 활용 권장

18. **코드 변경 시 기본 테스트**
    - 단순한 변경이라도 기본 동작 테스트 필요
    - import 누락은 즉시 런타임 에러 발생
    - 로컬 테스트 후 커밋 습관화

## 오후 4시 32분

### rb10508_micro 메모리 저장 및 참조 로직 개선

**문제상황**:
- "김종태" 대화가 ChromaDB에 저장되지 않음 (memory_stored: false)
- 관련 없는 이전 대화를 참조하여 "전에 이야기 나눴던 것 같네요" 응답
- MEMORY_IMPORTANCE_THRESHOLD가 0.7로 너무 높아 일반 대화 저장 안됨

**원인 분석**:
1. **메모리 저장 임계값 문제**
   ```python
   # config.py:32
   MEMORY_IMPORTANCE_THRESHOLD: float = 0.7  # 너무 높은 임계값
   # priority = 0.5 * entropy + 0.3 * emotion_intensity + 0.2 * novelty
   # 일반 대화는 0.7 넘기 어려움
   ```

2. **부적절한 메모리 참조**
   ```python
   # brain.py:299-300
   if memories:
       return f"아, 그것과 관련해서 전에 이야기 나눴던 것 같네요. {responses[0]}"
   # 메모리만 있으면 무조건 이 응답 (관련성 체크 없음)
   ```

**해결**:
1. **임계값 하향 조정**
   ```python
   MEMORY_IMPORTANCE_THRESHOLD: float = 0.3  # 0.7 → 0.3
   ```

2. **메모리 관련성 체크 추가**
   ```python
   if memories and memories[0].get('distance', 1.0) < 0.5:  # 유사도 체크
       return f"아, 그것과 관련해서 전에 이야기 나눴던 것 같네요. {responses[0]}"
   else:
       return random.choice(responses)
   ```

3. **디버그 로깅 추가**
   ```python
   if settings.DEBUG:
       logger.info(f"[메모리 검색] 최상위 결과 유사도: {distance:.3f}")
       logger.info(f"[메모리 저장 판단] entropy: {entropy:.3f}, emotion: {emotion_intensity:.3f}, novelty: {novelty:.3f}, priority: {priority:.3f}, threshold: {settings.MEMORY_IMPORTANCE_THRESHOLD}")
   ```

**검증 결과**:
| 테스트 항목 | 이전 | 현재 | 결과 |
|------------|------|------|------|
| 이름 저장 | ❌ memory_stored: false | ✅ memory_stored: true | 성공 |
| 이름 기억 | ❌ 일반 응답 | ✅ "박민수 님, 반갑습니다!" | 성공 |
| 날씨 질문 | ❌ "전에 이야기 나눴던..." | ✅ 독립적 응답 | 성공 |
| 메모리 관련성 | ❌ 무조건 참조 | ✅ 유사도 0.5 미만만 | 성공 |

**교훈**:

19. **임계값 설정의 중요성**
    - 너무 높은 임계값은 기본 기능을 무력화
    - 0.7 → 0.3 조정으로 일반 대화도 저장 가능
    - 실사용 패턴에 맞는 조정 필요

20. **관련성 체크 필수**
    - 단순 메모리 존재 여부가 아닌 유사도 확인
    - ChromaDB의 distance 값 활용 (낮을수록 유사)
    - 임계값 0.5로 적절한 관련성 필터링

21. **디버그 로깅의 가치**
    - 메모리 저장 판단 과정 추적 가능
    - 유사도 점수로 관련성 검증
    - 문제 발생 시 빠른 진단

**최종 성과**:
- 메모리 최적화: 987.9MB → 117.2MB (88.1% 감소)
- AI 대화 기능: 완전 정상화
- 메모리 시스템: 적절한 저장 및 참조
- 컨텍스트 인식: 정확한 사용자 정보 기억

## 오후 5시 07분

### BaseSettings와 환경변수 자동 오버라이드

**상황**:
- config.py에서 `DEBUG: bool = False`로 불필요하게 수정
- 실제로는 BaseSettings가 환경변수를 자동으로 읽어서 덮어씀

**원리**:
```python
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    DEBUG: bool = False  # 기본값
    # 환경변수 DEBUG=True가 있으면 자동으로 True가 됨
```

**로그 레벨 설정 검증**:
| 로그 타입 | 상태 | 설명 |
|-----------|------|------|
| 대화 로그 | ✅ | [대화] 태그로 기록 |
| 메모리 관련 | ✅ | 저장 판단, 검색 유사도 |
| API 요청 | ✅ | POST /api/message, 헬스체크 |
| DEBUG 레벨 | ❌ | 0개 (불필요한 세부 로그 없음) |

**교훈**:

22. **BaseSettings의 자동 기능 이해**
    - Pydantic BaseSettings는 환경변수를 자동으로 읽음
    - 코드의 기본값은 환경변수로 오버라이드됨
    - 불필요한 코드 수정 방지

23. **적절한 로그 레벨 설정**
    - LOG_LEVEL=INFO로 필요한 로그만 기록
    - DEBUG 환경변수와 LOG_LEVEL 구분 필요
    - 프로덕션에서는 INFO 레벨 권장
