docs: AI 응답 개선 Phase 1 트러블슈팅 문서 추가
- async/await 처리 실수와 해결 과정 - 기억 개선 5단계 계획을 ideas로 이동 - Phase 1 성공적 구현 기록
This commit is contained in:
parent
cd3105f9b2
commit
3ea5929365
363
ideas/기억_개선_5단계_계획.md
Normal file
363
ideas/기억_개선_5단계_계획.md
Normal file
@ -0,0 +1,363 @@
|
||||
# rb10508_micro 기억 개선 5단계 계획
|
||||
|
||||
## 개요
|
||||
|
||||
rb10508_micro의 AI 응답 단조로움 문제를 해결하기 위한 캐시 기반 지능형 응답 시스템 구현 계획입니다.
|
||||
|
||||
### 핵심 목표
|
||||
- 단조로운 템플릿 응답 제거
|
||||
- Gemini API 비용 최적화
|
||||
- 자연스러운 대화 경험 제공
|
||||
- 기존 코드 최대한 재사용
|
||||
|
||||
### 기본 원칙
|
||||
- 최소한의 코드 수정으로 최대 효과
|
||||
- skill-embedding 서비스 활용 (포트 8015)
|
||||
- 점진적 구현으로 리스크 최소화
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 기본 설정 (모든 단계 공통)
|
||||
|
||||
```python
|
||||
# config.py에 추가
|
||||
USE_GEMINI_CONVERSATION: bool = False # 1단계 토글
|
||||
USE_CONVERSATION_CACHE: bool = False # 2-4단계 토글
|
||||
USE_RESPONSE_VARIATION: bool = False # 5단계 토글
|
||||
CACHE_DISTANCE_THRESHOLD: float = 0.3 # 동적 조정 가능
|
||||
CACHE_MAX_ITEMS_PER_USER: int = 1000 # 사용자당 캐시 제한
|
||||
CACHE_TTL_DAYS: int = 30 # 캐시 유효 기간
|
||||
GEMINI_FALLBACK_MODEL: str = "gemini-2.5-flash-lite" # 쿼터 초과 시 대체
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 1단계: Gemini 전면 도입 + 병렬화 + Fallback
|
||||
|
||||
### 목표
|
||||
- 응답 품질 즉시 개선
|
||||
- 안정성 확보
|
||||
|
||||
### 구현
|
||||
```python
|
||||
async def _generate_conversational_response(self, message: str, memories: List[Dict]) -> str:
|
||||
if not settings.USE_GEMINI_CONVERSATION:
|
||||
return random.choice(self.responses) # 기존 방식
|
||||
|
||||
try:
|
||||
# 병렬 처리로 지연 최소화
|
||||
tasks = [
|
||||
self._generate_gemini_response(message, memories, EmotionState()),
|
||||
self.memory.check_novelty(message, self.current_user_id) # 동시 실행
|
||||
]
|
||||
response, novelty = await asyncio.gather(*tasks)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
if "quota" in str(e).lower():
|
||||
# 쿼터 초과 시 대체 모델
|
||||
self.gemini_model = genai.GenerativeModel(settings.GEMINI_FALLBACK_MODEL)
|
||||
return await self._generate_gemini_response(message, memories, EmotionState())
|
||||
else:
|
||||
# 완전 실패 시 기본 응답
|
||||
logger.error(f"Gemini 호출 실패: {e}")
|
||||
return "죄송합니다. 잠시 후 다시 시도해주세요."
|
||||
```
|
||||
|
||||
### 효과
|
||||
- 응답 다양성 100% 개선
|
||||
- API 비용 증가 (임시 감수)
|
||||
- 쿼터 초과 시 자동 대체
|
||||
|
||||
---
|
||||
|
||||
## 📌 2단계: 캐시 인프라 구축 + 중복 방지
|
||||
|
||||
### 목표
|
||||
- 안전한 캐시 구조 마련
|
||||
- 동시성 제어
|
||||
|
||||
### 구현
|
||||
```python
|
||||
# memory.py
|
||||
async def _init_collections(self):
|
||||
# 기존 컬렉션들...
|
||||
|
||||
if settings.USE_CONVERSATION_CACHE:
|
||||
self.conversation_cache = self.client.get_or_create_collection(
|
||||
name=f"{settings.ROBING_ID}_conversation_cache",
|
||||
metadata={
|
||||
"type": "conversation_cache",
|
||||
"version": "1.0",
|
||||
"max_items": settings.CACHE_MAX_ITEMS_PER_USER
|
||||
},
|
||||
embedding_function=self.embedding_function
|
||||
)
|
||||
# 동시성 제어를 위한 간단한 락
|
||||
self._cache_locks = {} # user_id: asyncio.Lock()
|
||||
```
|
||||
|
||||
### 하드코딩 값 설명
|
||||
- `"type": "conversation_cache"`: 다른 컬렉션과 구분하기 위한 메타데이터
|
||||
- `"version": "1.0"`: 향후 마이그레이션을 위한 버전 관리
|
||||
|
||||
---
|
||||
|
||||
## 📌 3단계: 지능형 캐시 저장 + TTL 관리
|
||||
|
||||
### 목표
|
||||
- 중복 방지
|
||||
- 자동 정리
|
||||
- 사용자별 캐시 제한
|
||||
|
||||
### 구현
|
||||
```python
|
||||
async def store_conversation_cache(self, message: str, response: str, user_id: str, emotion_state: Dict):
|
||||
if not settings.USE_CONVERSATION_CACHE:
|
||||
return
|
||||
|
||||
# 사용자별 락 획득
|
||||
if user_id not in self._cache_locks:
|
||||
self._cache_locks[user_id] = asyncio.Lock()
|
||||
|
||||
async with self._cache_locks[user_id]:
|
||||
# 중복 체크
|
||||
existing = self.conversation_cache.query(
|
||||
query_texts=[message],
|
||||
n_results=1,
|
||||
where={"user_id": user_id}
|
||||
)
|
||||
|
||||
if existing['distances'][0] and existing['distances'][0][0] < 0.1: # 거의 동일
|
||||
logger.debug(f"중복 캐시 스킵: {message[:50]}...")
|
||||
return
|
||||
|
||||
# 사용자별 캐시 개수 체크
|
||||
user_cache_count = self._get_user_cache_count(user_id)
|
||||
if user_cache_count >= settings.CACHE_MAX_ITEMS_PER_USER:
|
||||
# 오래된 것 삭제
|
||||
await self._cleanup_old_cache(user_id)
|
||||
|
||||
# 저장
|
||||
cache_id = f"conv_{user_id}_{int(time.time())}_{hashlib.md5(message.encode()).hexdigest()[:8]}"
|
||||
|
||||
self.conversation_cache.add(
|
||||
documents=[message],
|
||||
metadatas=[{
|
||||
"user_id": user_id,
|
||||
"response": response,
|
||||
"emotion_valence": emotion_state.get('valence', 0),
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"ttl_date": (datetime.utcnow() + timedelta(days=settings.CACHE_TTL_DAYS)).isoformat(),
|
||||
"usage_count": 0,
|
||||
"last_used": datetime.utcnow().isoformat()
|
||||
}],
|
||||
ids=[cache_id]
|
||||
)
|
||||
```
|
||||
|
||||
### 하드코딩 값 설명
|
||||
- `0.1`: 중복 판단 임계값 (90% 이상 유사 시 중복)
|
||||
- `cache_id` 형식: 가독성과 유일성 보장
|
||||
|
||||
---
|
||||
|
||||
## 📌 4단계: 정밀 캐시 검색 + 다중 가중치
|
||||
|
||||
### 목표
|
||||
- 문맥에 맞는 캐시 활용
|
||||
- 감정, 시간, 유사도 종합 고려
|
||||
|
||||
### 구현
|
||||
```python
|
||||
async def search_conversation_cache(self, query: str, user_id: str, emotion_valence: float = 0) -> List[Dict]:
|
||||
if not settings.USE_CONVERSATION_CACHE:
|
||||
return []
|
||||
|
||||
# 기본 유사도 검색
|
||||
results = self.conversation_cache.query(
|
||||
query_texts=[query],
|
||||
n_results=10, # 더 많이 가져와서 필터링
|
||||
where={
|
||||
"$and": [
|
||||
{"user_id": user_id},
|
||||
{"ttl_date": {"$gte": datetime.utcnow().isoformat()}} # TTL 체크
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
if not results['documents'][0]:
|
||||
return []
|
||||
|
||||
# 다중 가중치 적용
|
||||
scored_results = []
|
||||
for i, doc in enumerate(results['documents'][0]):
|
||||
distance = results['distances'][0][i]
|
||||
metadata = results['metadatas'][0][i]
|
||||
|
||||
# 거리 기반 점수 (0-1, 낮을수록 좋음)
|
||||
distance_score = distance
|
||||
|
||||
# 감정 유사도 점수 (0-1, 낮을수록 좋음)
|
||||
emotion_diff = abs(emotion_valence - metadata.get('emotion_valence', 0))
|
||||
emotion_score = emotion_diff / 2 # 정규화
|
||||
|
||||
# 시간 가중치 (최근일수록 좋음)
|
||||
days_old = (datetime.utcnow() - datetime.fromisoformat(metadata['timestamp'])).days
|
||||
time_score = min(days_old / settings.CACHE_TTL_DAYS, 1.0)
|
||||
|
||||
# 종합 점수 (가중 평균)
|
||||
final_score = (
|
||||
0.6 * distance_score + # 의미 유사도 60%
|
||||
0.2 * emotion_score + # 감정 유사도 20%
|
||||
0.2 * time_score # 시간 가중치 20%
|
||||
)
|
||||
|
||||
if final_score < settings.CACHE_DISTANCE_THRESHOLD:
|
||||
scored_results.append({
|
||||
'content': doc,
|
||||
'metadata': metadata,
|
||||
'distance': distance,
|
||||
'final_score': final_score
|
||||
})
|
||||
|
||||
# 점수순 정렬
|
||||
return sorted(scored_results, key=lambda x: x['final_score'])
|
||||
```
|
||||
|
||||
### 하드코딩 값 설명
|
||||
- `n_results=10`: 충분한 후보군 확보
|
||||
- 가중치 비율 (0.6, 0.2, 0.2): 의미 유사도 중심, 감정과 시간 보조
|
||||
|
||||
---
|
||||
|
||||
## 📌 5단계: 자연스러운 응답 변형 + 모니터링
|
||||
|
||||
### 목표
|
||||
- 기계적이지 않은 변형
|
||||
- 효과 측정
|
||||
|
||||
### 구현
|
||||
```python
|
||||
async def _vary_cached_response(self, original: str, cache_metadata: Dict) -> str:
|
||||
if not settings.USE_RESPONSE_VARIATION:
|
||||
return original
|
||||
|
||||
# 사용 횟수에 따른 변형 강도 결정
|
||||
usage_count = cache_metadata.get('usage_count', 0)
|
||||
|
||||
if usage_count < 3:
|
||||
# 3회 미만은 그대로 사용
|
||||
return original
|
||||
elif usage_count < 10:
|
||||
# 간단한 템플릿 변형
|
||||
return self._simple_variation(original)
|
||||
else:
|
||||
# 10회 이상 사용된 응답은 Gemini로 재생성
|
||||
prompt = f"""다음 응답을 의미는 유지하되 자연스럽게 다시 표현해주세요.
|
||||
원본: {original}
|
||||
|
||||
규칙:
|
||||
- 핵심 의미는 동일하게
|
||||
- 어투나 표현만 살짝 변경
|
||||
- 1-2문장으로 간결하게"""
|
||||
|
||||
try:
|
||||
response = await self.gemini_model.generate_content_async(prompt)
|
||||
varied = response.text.strip()
|
||||
|
||||
# 변형된 응답도 캐시에 저장
|
||||
await self.store_conversation_cache(
|
||||
message=cache_metadata.get('original_message', ''),
|
||||
response=varied,
|
||||
user_id=cache_metadata.get('user_id', ''),
|
||||
emotion_state={'valence': cache_metadata.get('emotion_valence', 0)}
|
||||
)
|
||||
|
||||
return varied
|
||||
except:
|
||||
return self._simple_variation(original)
|
||||
|
||||
def _simple_variation(self, text: str) -> str:
|
||||
"""간단한 템플릿 기반 변형"""
|
||||
# 종결어미 변형 (더 자연스럽게)
|
||||
endings = {
|
||||
"입니다.": ["이에요.", "인 것 같아요.", "이네요."],
|
||||
"해요.": ["하는 편이에요.", "하고 있어요.", "해 보세요."],
|
||||
"네요.": ["는군요.", "네요!", "는 것 같아요."]
|
||||
}
|
||||
|
||||
result = text
|
||||
for pattern, replacements in endings.items():
|
||||
if pattern in result and random.random() < 0.3: # 30% 확률
|
||||
result = result.replace(pattern, random.choice(replacements), 1)
|
||||
break # 한 번만 변형
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
### 하드코딩 값 설명
|
||||
- `usage_count < 3`: 신선한 응답은 그대로 유지
|
||||
- `usage_count < 10`: 적당한 사용 빈도는 간단 변형
|
||||
- `0.3 (30%)`: 변형 확률 - 너무 자주 변형하면 부자연스러움
|
||||
|
||||
---
|
||||
|
||||
## 📊 모니터링 지표
|
||||
|
||||
```python
|
||||
# 각 단계에서 로깅
|
||||
logger.info(f"[CONV_CACHE] action=search user={user_id} query_len={len(query)} hits={len(results)} cache_used={cache_hit}")
|
||||
logger.info(f"[CONV_CACHE] action=store user={user_id} msg_len={len(message)} cache_size={self._get_user_cache_count(user_id)}")
|
||||
logger.info(f"[GEMINI_API] model={self.gemini_model.model_name} response_time={elapsed} fallback={is_fallback}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 예상 효과
|
||||
|
||||
| 단계 | 기대 효과 | 구현 난이도 | 소요 시간 | API 비용 변화 |
|
||||
|------|-----------|------------|-----------|---------------|
|
||||
| 1단계 | 응답 다양성 100% 개선 | 매우 쉬움 | 5분 | +100% |
|
||||
| 2단계 | 캐시 구조 준비 | 쉬움 | 30분 | 변화 없음 |
|
||||
| 3단계 | 캐시 저장 시작 | 보통 | 1시간 | 변화 없음 |
|
||||
| 4단계 | API 비용 30% 절감 | 보통 | 2시간 | -30% |
|
||||
| 5단계 | API 비용 70% 절감 | 어려움 | 3시간 | -70% |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 운영 고려사항
|
||||
|
||||
### 저장 한도
|
||||
- 사용자/팀 단위 max_items 지정으로 무제한 증가 방지
|
||||
|
||||
### TTL 관리
|
||||
- 30일 이상 미사용 캐시 자동 삭제
|
||||
- 중요 대화는 요약 후 semantic 메모리로 이전
|
||||
|
||||
### 동시성
|
||||
- asyncio.Lock()으로 중복 저장 방지
|
||||
|
||||
### 배포 전략
|
||||
- 기능 토글로 단계별 롤아웃
|
||||
- 문제 발생 시 즉시 롤백 가능
|
||||
|
||||
### 성능 모니터링
|
||||
- 캐시 히트율 목표: 30% → 70% (3개월)
|
||||
- API 호출 감소율 추적
|
||||
- 응답 시간 개선 측정
|
||||
|
||||
---
|
||||
|
||||
## 🚀 구현 로드맵
|
||||
|
||||
1. **Week 1**: 1단계 배포 및 모니터링
|
||||
2. **Week 2**: 2-3단계 구현 및 테스트
|
||||
3. **Week 3**: 4단계 배포 및 캐시 히트율 분석
|
||||
4. **Week 4**: 5단계 구현 및 전체 최적화
|
||||
|
||||
---
|
||||
|
||||
작성일: 2025-08-05
|
||||
작성자: Claude & happybell80
|
||||
프로젝트: rb10508_micro 응답 품질 개선
|
||||
150
troubleshooting/250805_happybell80_AI응답개선Phase1.md
Normal file
150
troubleshooting/250805_happybell80_AI응답개선Phase1.md
Normal file
@ -0,0 +1,150 @@
|
||||
# AI 응답 단조로움 해결 - Phase 1 구현
|
||||
|
||||
**날짜**: 2025-08-05
|
||||
**작업자**: happybell80 & Claude
|
||||
**관련 서비스**: rb10508_micro
|
||||
|
||||
## 오후 1시 30분
|
||||
|
||||
### 문제 상황
|
||||
|
||||
서버팀 보고:
|
||||
- rb10508_micro의 AI 응답이 지나치게 단조로움
|
||||
- 3개의 고정 템플릿만 반복 사용
|
||||
- "흥미로운 이야기네요. 더 자세히 들려주시겠어요?"
|
||||
- "네, 이해했습니다. 어떻게 도와드릴까요?"
|
||||
- "그렇군요. 제가 어떤 도움을 드릴 수 있을까요?"
|
||||
|
||||
**원인 분석**:
|
||||
```python
|
||||
# brain.py:298-302
|
||||
responses = [
|
||||
"흥미로운 이야기네요. 더 자세히 들려주시겠어요?",
|
||||
"네, 이해했습니다. 어떻게 도와드릴까요?",
|
||||
"그렇군요. 제가 어떤 도움을 드릴 수 있을까요?"
|
||||
]
|
||||
return random.choice(responses)
|
||||
```
|
||||
|
||||
### 5단계 개선 계획 수립
|
||||
|
||||
1단계: Gemini 전면 도입 (즉시 적용)
|
||||
2-5단계: 캐시 시스템 구축 (점진적 구현)
|
||||
|
||||
## 오후 2시 00분
|
||||
|
||||
### Phase 1 구현
|
||||
|
||||
**목표**: 모든 대화에 Gemini API 사용
|
||||
|
||||
**구현 내용**:
|
||||
1. config.py에 환경변수 추가
|
||||
```python
|
||||
USE_GEMINI_CONVERSATION: bool = False
|
||||
GEMINI_FALLBACK_MODEL: str = "gemini-2.5-flash-lite"
|
||||
```
|
||||
|
||||
2. brain.py에 Gemini 전면 도입 및 병렬화
|
||||
- 템플릿 응답 → Gemini 응답으로 전환
|
||||
- novelty 체크와 병렬 처리
|
||||
- 쿼터 초과 시 fallback 모델 사용
|
||||
|
||||
## 오후 2시 30분
|
||||
|
||||
### 서버팀 테스트 결과 - async 오류 발생
|
||||
|
||||
**에러 메시지**:
|
||||
```
|
||||
2025-08-05 13:41:08,707 - app.core.brain - ERROR - Gemini 호출 완전 실패:
|
||||
An asyncio.Future, a coroutine or an awaitable is required
|
||||
```
|
||||
|
||||
**문제**: Gemini API 호출에서 async/await 처리 오류
|
||||
|
||||
## 오후 10시 30분
|
||||
|
||||
### 첫 번째 실수 - 성급한 판단
|
||||
|
||||
**잘못된 분석**:
|
||||
1. `generate_content_async()` 메서드가 존재하지 않는다고 판단
|
||||
2. `_generate_gemini_response`를 async → 동기 함수로 변경
|
||||
3. `await` 제거
|
||||
|
||||
**실제 문제**:
|
||||
- `asyncio.gather()`에 동기 함수 결과값 전달
|
||||
- awaitable이 아닌 일반 값이라 에러 발생
|
||||
|
||||
## 오후 11시 00분
|
||||
|
||||
### Sequential Thinking으로 재분석
|
||||
|
||||
GPT 검색 결과 확인:
|
||||
- `google-generativeai` 0.3.x~0.5.x에는 `generate_content_async()` 없음
|
||||
- 동기 메서드 `generate_content()`만 존재
|
||||
- async로 사용하려면 `run_in_executor()` 필요
|
||||
|
||||
**올바른 해결**:
|
||||
```python
|
||||
# generate_content를 비동기로 실행
|
||||
loop = asyncio.get_running_loop()
|
||||
response = await loop.run_in_executor(
|
||||
None,
|
||||
self.gemini_model.generate_content,
|
||||
prompt
|
||||
)
|
||||
```
|
||||
|
||||
## 오후 11시 20분
|
||||
|
||||
### 서버팀 추가 문제 발견
|
||||
|
||||
1. **check_novelty 함수 async 불일치**
|
||||
```python
|
||||
# 문제: 동기 함수를 asyncio.gather()에 전달
|
||||
novelty_task = self.memory.check_novelty(message, self.current_user_id)
|
||||
```
|
||||
|
||||
2. **EmotionState() 빈 객체 전달**
|
||||
```python
|
||||
# 문제: 실제 감정 대신 빈 객체
|
||||
gemini_task = self._generate_gemini_response(message, memories, EmotionState())
|
||||
```
|
||||
|
||||
### 최종 수정
|
||||
|
||||
1. check_novelty를 run_in_executor로 감싸기
|
||||
2. 실제 emotion_state를 Gemini에 전달
|
||||
3. _generate_conversational_response 시그니처 수정
|
||||
|
||||
## 교훈
|
||||
|
||||
1. **async/await 처리 주의**
|
||||
- 라이브러리 문서 확인 필수
|
||||
- 동기 함수는 `run_in_executor()`로 비동기 변환
|
||||
- `asyncio.gather()`에는 awaitable만 전달
|
||||
|
||||
2. **성급한 판단 금지**
|
||||
- 에러 메시지만 보고 판단하지 말기
|
||||
- Sequential Thinking으로 차근차근 분석
|
||||
- 실제 라이브러리 동작 확인
|
||||
|
||||
3. **파라미터 전달 확인**
|
||||
- 빈 객체 대신 실제 데이터 전달
|
||||
- 함수 시그니처 일관성 유지
|
||||
- 데이터 흐름 추적
|
||||
|
||||
4. **테스트의 중요성**
|
||||
- 로컬 테스트 환경 구축 필요
|
||||
- 서버팀 피드백 적극 활용
|
||||
- 단계별 검증
|
||||
|
||||
## 최종 성과
|
||||
|
||||
✅ Phase 1 완전 성공
|
||||
- 단조로운 템플릿 → 자연스러운 AI 대화
|
||||
- 메모리: 113.1MiB → 122.2MiB (+8.1%)
|
||||
- 안정적인 async 처리
|
||||
- 실제 감정 정보 활용
|
||||
|
||||
**개선된 응답 예시**:
|
||||
"정말 그렇게 느껴지시나요? AI 기술의 발전 속도가 어마어마한 건 저도 매일 피부로 느끼고 있어요..."
|
||||
Loading…
x
Reference in New Issue
Block a user