# Phase 2: ChromaDB + Neo4j 하이브리드 기억 회상 시스템 구현 **날짜**: 2025-10-16 **작성자**: Claude (51124 서버 전담) **관련 파일**: - `rb8001/app/memory/neo4j_client.py` - `rb8001/app/services/memory_hybrid_retrieval.py` - `rb8001/app/router/memory_ontology.py` - `rb8001/tests/test_memory_hybrid.py` --- ## 목표 Phase 1 (Coldmail 온톨로지)의 성공을 바탕으로, 기억 시스템에 온톨로지 기반 의미적 연결을 도입하여 "1년 전 비슷한 상황"을 회상할 수 있는 하이브리드 시스템 구축. --- ## 구현 내용 ### 1. Neo4j Python 드라이버 연동 **파일**: `app/memory/neo4j_client.py` (300줄) **주요 기능**: - Neo4j Bolt 연결 관리 (uri, auth, database) - 사건(Event) 노드 저장 및 관계 생성 - 그래프 추론 (감정, 결과 관계 가중치 계산) **스키마** (설계안): ``` (사건:Event) -[:PARTICIPANT]-> (사용자:User) (사건:Event) -[:HAS_EMOTION]-> (감정:Emotion) (사건:Event) -[:HAS_RESULT]-> (결과:Result) ``` **관계 가중치**: | 감정 | 가중치 | 결과 | 가중치 | |------|--------|------|--------| | joy (기쁨) | 1.2 | success | 1.5 | | fear (긴장) | 1.3 | failure | 0.8 | | sadness (슬픔) | 1.5 | neutral | 1.0 | | 기타 | 1.0 | - | - | **시간 가중치**: - 최근 → 1.0 - 1년 전 → 0.5 - 공식: `max(0.5, 1.0 - (days_diff / 365) * 0.5)` ### 2. ChromaDB + Neo4j 하이브리드 Retrieval **파일**: `app/services/memory_hybrid_retrieval.py` (350줄) **3단계 알고리즘**: #### Stage 1: ChromaDB 벡터 검색 (빠른 필터링) ```python # 입력: 사용자 쿼리 (예: "작년 프레젠테이션 때 어떻게 했지?") # 1. skill-embedding (8515) /embed 호출로 쿼리 임베딩 # 2. ChromaDB 유사도 검색: top_k=20 (충분한 후보) # 3. 출력: 20개 후보 대화 (벡터 점수 포함) vector_candidates = await chroma_manager.search_memories( query=query, n_results=top_k * 4 # 20개 ) # distance를 점수로 변환 for mem in memories: mem["vector_score"] = max(0.0, 1.0 - distance) ``` #### Stage 2: Neo4j 그래프 추론 (의미적 연결) ```python # 입력: ChromaDB 후보 20개 # Cypher 쿼리 (자동 실행): MATCH (e:Event) WHERE e.id IN $event_ids OPTIONAL MATCH (e)-[:HAS_EMOTION]->(emotion:Emotion) OPTIONAL MATCH (e)-[:HAS_RESULT]->(result:Result) RETURN e.id, e.timestamp, emotion.name, result.type # 우선순위: # - [:HAS_RESULT]->(success) 관계 있는 사건 우선 (가중치 1.5배) # - [:HAS_EMOTION]->(fear) 매칭 시 가중치 1.3배 # - 시간적 근접성: 1년 전 > 2년 전 (거리 역수) graph_score = emotion_weight * result_weight * time_weight ``` #### Stage 3: 점수 통합 및 순위 결정 ```python # 최종 점수 = (벡터 0.4 + 그래프 0.6) final_score = (vector_score * 0.4) + (normalized_graph * 0.6) # normalized_graph = min(1.0, graph_score / 2.0) # graph_score는 0.5 ~ 3.0 범위 → 0~1로 정규화 # 최종 점수순 정렬 후 상위 top_k 반환 ``` ### 3. Memory Ontology API 엔드포인트 **파일**: `app/router/memory_ontology.py` (200줄) **API 엔드포인트**: #### POST /api/v1/memory/event **사건 저장 (ChromaDB + Neo4j 동시 저장)** **Request**: ```json { "content": "프레젠테이션 성공했다", "emotion": "joy", "result": "success", "metadata": {"category": "work"} } ``` **Response**: ```json { "event_id": "06f45509...", "message": "Event stored to ChromaDB and Neo4j" } ``` #### POST /api/v1/memory/recall **쿼리 기반 기억 회상 (3단계 하이브리드)** **Request**: ```json { "query": "작년 프레젠테이션 때 어떻게 했지?", "top_k": 5 } ``` **Response**: ```json { "memories": [ { "id": "06f45509...", "content": "프레젠테이션을 성공적으로 마쳤다. 청중 반응이 좋았다.", "vector_score": 0.85, "graph_score": 1.8, "final_score": 0.94, "emotion": "joy", "result": "success", "timestamp": "2024-10-16T09:00:00" } ], "query": "작년 프레젠테이션 때 어떻게 했지?", "count": 1 } ``` #### GET /api/v1/memory/stats **하이브리드 시스템 통계** **Response**: ```json { "robeing_id": "rb8001", "user_id": "test_user_ontology", "chroma": { "collection_name": "rb8001_test_user_ontology_memory", "user_id": "test_user_ontology" }, "neo4j": { "connected": true, "uri": "neo4j://192.168.219.45:7687", "database": "neo4j", "event_count": 42 } } ``` ### 4. 테스트 및 검증 **파일**: `tests/test_memory_hybrid.py` **테스트 결과** (2025-10-16 15:06): ``` ✅ ChromaDB 저장: 4건 ⚠️ Neo4j 연결: 실패 (환경변수 NEO4J_PASSWORD 미설정) ChromaDB 단독 모드로 fallback 동작 확인 ``` **테스트 시나리오**: 1. 4개 사건 저장 (프레젠테이션, 프로젝트, 회의, 점심) 2. 3개 쿼리 회상 테스트 - "프레젠테이션 때 어떻게 했지?" - "긴장했을 때 어떻게 대처했나?" - "성공한 경험이 뭐가 있지?" **결과**: - ChromaDB 저장: ✅ 정상 - Neo4j 연결: ⚠️ 실패 (localhost → 192.168.219.45로 변경 필요) - Fallback 동작: ✅ 정상 (Neo4j 연결 실패 시 ChromaDB 단독 모드) --- ## 배포 상태 ### 코드 배포 - **커밋**: 714a132 "Phase 2: ChromaDB + Neo4j 하이브리드 기억 회상 시스템" - **날짜**: 2025-10-16 15:05 - **서버**: 51124 (192.168.219.52) - **컨테이너**: rb8001 (재시작 완료) ### Dependencies - **neo4j**: 5.27.0 (requirements.txt 추가, Docker 이미지에 설치 완료) ### 환경변수 설정 필요 **.env 파일 추가 필요**: ```bash # Neo4j 연결 설정 NEO4J_URI=neo4j://192.168.219.45:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=<비밀번호> ``` **현재 상태**: - NEO4j_URI: bolt://localhost:7687 (기본값, 잘못됨) - NEO4J_PASSWORD: 미설정 **수정 후 재시작**: ```bash cd /home/admin/ivada_project/rb8001 # .env 파일 수정 docker compose down && docker compose up -d ``` --- ## API 등록 **파일**: `main.py` (53-54줄) ```python # 기억 온톨로지 엔드포인트 등록 (Phase 2) from app.router.memory_ontology import router as memory_router app.include_router(memory_router) ``` **FastAPI Swagger UI**: `http://localhost:8001/docs#/Memory%20Ontology` --- ## 아키텍처 ### 데이터 흐름 ``` 사용자 쿼리 "작년 프레젠테이션 때 어떻게 했지?" ↓ [ Stage 1: ChromaDB ] 벡터 검색 → 20개 후보 ↓ [ Stage 2: Neo4j ] 그래프 추론 → 감정/결과 가중치 ↓ [ Stage 3: 점수 통합 ] (벡터 0.4 + 그래프 0.6) → 상위 5개 ↓ 응답: [ {content: "프레젠테이션 성공", final_score: 0.94, emotion: "joy", result: "success"}, ... ] ``` ### 저장 흐름 ``` 사건 저장 요청 {content, emotion, result} ↓ [ ChromaDB ] 임베딩 생성 (skill-embedding 8515) → 벡터 저장 ↓ [ Neo4j ] Event 노드 생성 → [:HAS_EMOTION]→(Emotion) → [:HAS_RESULT]→(Result) → [:PARTICIPANT]→(User) ↓ 응답: event_id ``` --- ## 성능 지표 (예상) | 항목 | 기존 (ChromaDB 단독) | Phase 2 (하이브리드) | |------|---------------------|---------------------| | 검색 속도 | ~100ms | ~200ms | | 의미 정확도 | 70% | 90% (목표) | | 관계 추론 | 불가능 | 가능 | | 설명 가능성 | 낮음 | 높음 (그래프 경로) | **예상 개선 효과**: - "긴장했을 때" 쿼리 → emotion=fear인 사건 우선 (가중치 1.3배) - "성공한 경험" 쿼리 → result=success인 사건 우선 (가중치 1.5배) - 시간 가중치로 최근 경험 우선 --- ## 남은 작업 ### 즉시 필요 - [ ] **.env 파일에 Neo4j 환경변수 추가** - NEO4J_URI=neo4j://192.168.219.45:7687 - NEO4J_PASSWORD=<비밀번호> - [ ] **Docker 재시작 후 재테스트** ### Phase 2 완료 - [ ] **Neo4j 연결 성공 확인** - [ ] **API 엔드포인트 테스트** (curl 또는 Swagger UI) - [ ] **실제 사용자 데이터로 회상 테스트** ### Phase 3 (감정-윤리 온톨로지, 1개월) - [ ] 감정-우도 온톨로지 (7가지 감정) - [ ] 윤리 제약 온톨로지 (사랑 기반 원칙) - [ ] HermiT 일관성 검사 자동화 - [ ] 추론 과정 추적 및 설명 생성 --- ## 교훈 ### 1. Neo4j 이미 설치되어 있음 - **문제**: Phase 2 계획서에서 "Neo4j 설치 필요"로 명시 - **실제**: 51123 서버에 2025.06.2 Community Edition 이미 설치됨 (1개월 26일 가동) - **교훈**: 사전 조사로 중복 작업 방지 ### 2. Fallback 설계의 중요성 - **문제**: Neo4j 연결 실패 시 전체 시스템 중단 가능성 - **해결**: `if not self.driver: return []` 로직으로 ChromaDB 단독 모드 fallback - **교훈**: 외부 의존성은 항상 fallback 준비 ### 3. 환경변수 기본값의 함정 - **문제**: NEO4J_URI 기본값 localhost:7687 (51123 서버는 192.168.219.45) - **교훈**: 기본값은 문서에 명시, 배포 시 환경변수 체크리스트 필수 --- ## 참고 - **Phase 2 계획**: DOCS/plans/251016_ontology_coldmail_implementation.md (Phase 2 섹션) - **Phase 1 검증**: DOCS/troubleshooting/251016_ontology_filter_validation.md - **설계 원칙**: DOCS/200_core_design/225_온톨로지_기반_지식_표현.md - **Neo4j 설치 정보**: 251016_ontology_coldmail_implementation.md (66-82줄) - **구현 커밋**: rb8001 714a132 --- ## 다음 단계 1. **.env 파일 수정** (NEO4J_URI, NEO4J_PASSWORD) 2. **Docker 재시작** 3. **테스트 재실행** (`python tests/test_memory_hybrid.py`) 4. **API 엔드포인트 curl 테스트**: ```bash # 사건 저장 curl -X POST http://localhost:8001/api/v1/memory/event \ -H "Content-Type: application/json" \ -H "X-User-Id: test_user" \ -d '{"content": "프레젠테이션 성공", "emotion": "joy", "result": "success"}' # 회상 curl -X POST http://localhost:8001/api/v1/memory/recall \ -H "Content-Type: application/json" \ -H "X-User-Id: test_user" \ -d '{"query": "성공한 경험", "top_k": 5}' ``` 5. **Phase 3 시작 여부 결정**