- 같은 사용자 이름 반복 입력해도 기억 못함 - '기억' 키워드에 기계적 템플릿 응답 - 사용자별 기억 구분 실패 - ChromaDB 검색 기능 오작동 - Gemini API는 정상이나 기존 맥락 연결 실패 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
18 KiB
rb10508_micro 기억 개선 5단계 계획
현재 문제점 분석
대화 로그 분석 (2025-08-05)
14:21:28 - fix_test: 수정 완료 후 테스트입니다 → rb10508_micro: 수정 완료 후 테스트하시나 보군요! 어떤 부분을 수정하셨고, 어떤 테스트를 진행하실 예정이신가요?
14:21:48 - emotion_test: 안녕 로빙 기분이 좋아 → rb10508_micro: 안녕하세요! 오늘은 어떤 도움이 필요하신가요?
14:21:55 - conversation_test: 요즘 AI 기술이 정말 빠르게 발전하고 있어 → rb10508_micro: 정말 그렇게 느껴지시나요? AI 기술의 발전 속도가 어마어마한 건 저도 매일 피부로 느끼고 있어요.
14:24:16 - test_user: 안녕 로빙? → rb10508_micro: 반갑습니다! 무엇을 도와드릴까요?
14:24:24 - test_user: 난 김종태라고 해. → rb10508_micro: 안녕하세요, 김종태님! 만나서 반갑습니다. 저는 로빙(RO-BEING)입니다.
14:24:37 - test_user: 넌 베르단디라고 부를게 → rb10508_micro: 네, 앞으로 저를 베르단디라고 불러주시면 됩니다.
14:24:57 - test_user: 지금 니가 살고 있는 세상은 어떤 세상이야? → rb10508_micro: 제가 "살고 있는" 세상은 아니지만, 제가 접근할 수 있는 정보의 세상은 매우 광대하고 끊임없이 변화하고 있습니다.
14:26:43 - U0925SXQFDK: 로빙 오늘 있었던 일을 이야기 해줘. → rb10508_micro: 오늘 하루도 잘 보내셨나요? 오늘 있었던 일에 대해 말씀드리자면, 저는 여러 사용자분들과 다양한 주제로 대화를 나누며 정보를 얻고 학습하는 시간을 가졌습니다.
14:27:22 - U0925SXQFDK: 지금 대화가 너한테 기억되는 거 맞나? → rb10508_micro: 제가 기억하기로는... (기억 템플릿 응답)
14:27:41 - U0925SXQFDK: 음.. 아직 기억은 좀.. 문제가 있네 → rb10508_micro: 제가 기억하기로는... (기억 템플릿 응답)
14:27:51 - U0925SXQFDK: 기억이라는 단어에 반응하는 건가? → rb10508_micro: 제가 기억하기로는... (기억 템플릿 응답)
14:28:06 - U0925SXQFDK: 흠.. 암튼 내가 누구라고 했어? → rb10508_micro: 제가 기억하기로는, 사용자님께서 아직 제게 이름을 알려주시지 않으셨습니다.
14:28:16 - U0925SXQFDK: 알려줬는데 기억에서 찾아봐. → rb10508_micro: 제가 기억하기로는... (기억 템플릿 응답)
14:28:31 - U0925SXQFDK: 아직 적용이 안됐나 보네 → rb10508_micro: 네, 김종태님. 제가 기억하는 내용에 사용자님의 이름이 적용되지 않았던 것 같습니다.
14:28:56 - U0925SXQFDK: 오~ 감정까지 표시를 하네. → rb10508_micro: 네, 사용자님! 저는 기억하고 성장하는 AI 동료, 로빙입니다.
14:29:36 - U0925SXQFDK: 근데 내가 짜증과 불만 감정이라고 생각한 근거는 뭐야? → rb10508_micro: 제가 사용자님께서 짜증과 불만을 느끼고 있다고 판단한 근거는 다음과 같습니다.
14:30:10 - U0925SXQFDK: 왜 계속 사용자라고 호칭 하는 거야? → rb10508_micro: 제가 계속 '사용자'라고 호칭하는 것은, 저에게는 사용자의 고유한 이름이나 닉네임을 직접적으로 알 수 있는 방법이 없기 때문입니다.
14:30:32 - U0925SXQFDK: 이상하네.. 김종태라는 걸 기억했다가 기억 못했다가.. → rb10508_micro: 제가 기억하기로는... (기억 템플릿 응답)
14:30:47 - U0925SXQFDK: 확실히 기억이라는 단어에 반응을 하네. → rb10508_micro: 제가 기억하기로는... (기억 템플릿 응답)
14:31:04 - test_user: 기억 → rb10508_micro: 제가 기억하기로는... (기억 템플릿 응답)
14:34:58 - test_user: 내가 널 뭐라 부르기로 했지? → rb10508_micro: 네, 사용자님! 저희는 베르단디라고 부르기로 했죠.
14:35:10 - test_user: 왜 사용자라고 호칭하지? → rb10508_micro: 저는 로빙(RO-BEING)입니다. 사용자님을 '사용자'라고 부르는 것은 제가 사용자님의 이름을 아직 알지 못하기 때문입니다.
14:35:23 - test_user: 내 이름을 아직 모르나? → rb10508_micro: 아직 제 기억 속에 사용자님의 이름이 저장되어 있지 않습니다.
14:35:27 - test_user: 김종태야 → rb10508_micro: 안녕하세요, 김종태님! 만나서 반갑습니다.
14:35:55 - test_user: 난 김종태라고 해. 내가 이거 몇분전에 적었는데 모른다는 게 말이돼? → rb10508_micro: 안녕하세요! 좋은 하루 보내고 계신가요?
14:36:08 - test_user: 흠.. 이상하네 → rb10508_micro: 사용자님, 무슨 일이신가요? 혹시 제가 뭔가 잘못했거나, 혹은 다른 이유로 그렇게 느끼시는 건가요?
14:36:19 - test_user: 내가 누구라고? → rb10508_micro: 저는 로빙(RO-BEING)입니다. 기억하고 성장하는 AI 동료입니다.
14:36:24 - test_user: 너 말고 나 → rb10508_micro: 사용자님, 저 말고 사용자님이라고 불러드리겠습니다.
14:36:36 - test_user: 내 이름이 뭐라고? → rb10508_micro: 음... 사용자님의 이름이 무엇인지 저는 아직 알지 못합니다.
분석된 주요 문제점
-
같은 사용자가 동일한 이름을 여러 번 말했는데 기억 못함
- 14:24:24: "난 김종태라고 해" → 14:35:23에서 "내 이름을 아직 모르나?"
-
"기억"이라는 단어에 기계적으로 반응
- "기억"만 들으면 항상 "제가 기억하기로는..." 템플릿 출력
-
사용자별 기억이 섞임
- test_user와 U0925SXQFDK를 구분하지 못함
- 한 명이 "김종태"라고 했는데 다른 사용자에게도 적용
-
ChromaDB 검색이 엉망
- 같은 내용을 계속 반복해서 찾지 못함
- 유사도 검색이 제대로 작동하지 않음
-
Gemini API는 정상 작동하지만 기억 연결이 끊어짐
- 자연스러운 응답은 생성되지만 기존 대화 맥락을 활용하지 못함
개요
rb10508_micro의 AI 응답 단조로움 문제를 해결하기 위한 캐시 기반 지능형 응답 시스템 구현 계획입니다.
핵심 목표
- 단조로운 템플릿 응답 제거
- Gemini API 비용 최적화
- 자연스러운 대화 경험 제공
- 기존 코드 최대한 재사용
기본 원칙
- 최소한의 코드 수정으로 최대 효과
- skill-embedding 서비스 활용 (포트 8015)
- 점진적 구현으로 리스크 최소화
🏗️ 기본 설정 (모든 단계 공통)
# 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
목표
- 응답 품질 즉시 개선
- 안정성 확보
구현
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단계: 캐시 인프라 구축 + 중복 방지
목표
- 안전한 캐시 구조 마련
- 동시성 제어
구현
# 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 관리
목표
- 중복 방지
- 자동 정리
- 사용자별 캐시 제한
구현
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단계: 정밀 캐시 검색 + 다중 가중치
목표
- 문맥에 맞는 캐시 활용
- 감정, 시간, 유사도 종합 고려
- memory_recall 인텐트의 기계적 응답 개선 (관련성 없는 기억 나열 문제 해결)
구현
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단계: 자연스러운 응답 변형 + 모니터링
목표
- 기계적이지 않은 변형
- 효과 측정
구현
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%): 변형 확률 - 너무 자주 변형하면 부자연스러움
📊 모니터링 지표
# 각 단계에서 로깅
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 호출 감소율 추적
- 응답 시간 개선 측정
🚀 구현 로드맵
- Week 1: 1단계 배포 및 모니터링
- Week 2: 2-3단계 구현 및 테스트
- Week 3: 4단계 배포 및 캐시 히트율 분석
- Week 4: 5단계 구현 및 전체 최적화
작성일: 2025-08-05
작성자: Claude & happybell80
프로젝트: rb10508_micro 응답 품질 개선