334 lines
9.1 KiB
Markdown
334 lines
9.1 KiB
Markdown
# 250806 동적 프롬프트 컨텍스트 구현
|
|
|
|
## 오후 2시 55분
|
|
|
|
### 문제 상황
|
|
|
|
**서버팀 테스트 결과 분석**:
|
|
- Phase 5는 정상 작동 (캐시 3회 사용 후 재생성)
|
|
- 하지만 의도 파악 실패 지속
|
|
- "내 이름은?" → "저는 베르단디입니다" (잘못됨)
|
|
- "네 이름은?" → "저는 베르단디입니다" (맞음)
|
|
- 사용자 이름을 묻는 것을 자기소개로 오해
|
|
|
|
**원인 분석**:
|
|
1. 캐시 임계값 너무 관대 (0.3) → 다른 질문도 같은 캐시 사용
|
|
2. 프롬프트 하드코딩 → "당신은 로빙입니다" 고정
|
|
3. 메모리와 프롬프트 분리 → 사용자가 "베르단디"라 불러도 기억 못함
|
|
|
|
## 오후 3시 10분
|
|
|
|
### 철학적 재검토
|
|
|
|
**초기 착각**:
|
|
- 로빙의 "존재적 정체성"을 고정된 것으로 이해
|
|
- DOCS 철학 문서를 잘못 해석
|
|
|
|
**올바른 이해**:
|
|
- 로빙은 "성장하는 존재" (레벨 1→20)
|
|
- 사용자와 함께 개성 발전
|
|
- "베르단디"로 불리면 그것이 새로운 정체성
|
|
|
|
**프롬프트 구조 재설계**:
|
|
```
|
|
Base Layer (하드코딩):
|
|
- 기억-감정-윤리 삼각형
|
|
- 이모지 사용 금지
|
|
- 기본 규칙
|
|
|
|
Dynamic Layer (메모리에서):
|
|
- 현재 이름 (로빙/베르단디/기타)
|
|
- 사용자 이름
|
|
- 현재 레벨/스탯
|
|
- 최근 기억
|
|
```
|
|
|
|
## 오후 3시 25분
|
|
|
|
### intent 패턴 최적화
|
|
|
|
**문제점**:
|
|
- "하이" → greeting → 템플릿 응답
|
|
- "기억" → memory_recall → 기계적 나열
|
|
- 대부분 캐시를 우회
|
|
|
|
**해결책**:
|
|
1. greeting 패턴 축소: `r"(안녕하세요|안녕하십니까)"`
|
|
2. memory_recall 패턴 제거
|
|
3. question 패턴 정밀화: `r"(어떻게|왜|언제|how|why|when).*\?"`
|
|
4. 이모지 하드코딩 제거
|
|
|
|
**효과**:
|
|
- "하이" → general_conversation → 캐시 사용
|
|
- 캐시 활성화율 80% 이상
|
|
|
|
## 오후 3시 40분
|
|
|
|
### 동적 프롬프트 구현
|
|
|
|
**방법 선택**: 컨텍스트 변수 방식 (가장 효율적)
|
|
|
|
**구현 내역**:
|
|
|
|
1. **이름 추출 함수 추가**:
|
|
```python
|
|
async def _extract_user_name(self, memories, user_id):
|
|
# "내 이름은 김종태" 패턴 인식
|
|
# "나는 김종태야" 패턴 인식
|
|
# semantic 메모리에서 user_name 메타데이터 검색
|
|
return "사용자" # 기본값
|
|
|
|
async def _extract_my_name(self, memories, user_id):
|
|
# "너를 베르단디라고 부를게" 패턴 인식
|
|
# "베르단디야" 직접 호명 패턴 인식
|
|
return "로빙" # 기본값
|
|
```
|
|
|
|
2. **think() 메서드에서 컨텍스트 변수 설정**:
|
|
```python
|
|
# 메모리 검색 후
|
|
self.current_user_name = await self._extract_user_name(memories, user_id)
|
|
self.my_name = await self._extract_my_name(memories, user_id)
|
|
```
|
|
|
|
3. **프롬프트 동적 구성**:
|
|
```python
|
|
context = f"""당신은 {my_name}(RO-BEING)입니다.
|
|
현재 대화 상대: {user_name}
|
|
현재 레벨: {self.stats.level}
|
|
|
|
중요 규칙:
|
|
- "내 이름은?"은 사용자({user_name})가 자신의 이름을 묻는 것
|
|
- "네 이름은?"은 나({my_name})의 이름을 묻는 것
|
|
"""
|
|
```
|
|
|
|
4. **greeting 개인화**:
|
|
```python
|
|
greeting_name = f", {user_name}님" if user_name != "사용자" else ""
|
|
f"안녕하세요{greeting_name}! 오늘은 어떤 도움이 필요하신가요?"
|
|
```
|
|
|
|
## 오후 3시 45분
|
|
|
|
### 배포 및 예상 효과
|
|
|
|
**구현 완료**:
|
|
- 총 소요 시간: 15분
|
|
- 코드 변경: brain.py만 수정
|
|
- 추가된 줄: 약 70줄
|
|
|
|
**예상 효과**:
|
|
- ✅ "베르단디"로 불리면 기억하고 사용
|
|
- ✅ "내 이름은?" 올바르게 이해
|
|
- ✅ 사용자별 맞춤 응답
|
|
- ✅ 성장 상태(레벨) 반영
|
|
|
|
**Gitea Actions 자동 배포**: 진행 중
|
|
|
|
## 교훈
|
|
|
|
1. **철학 문서의 중요성**
|
|
- 기술 구현 전 철학적 기반 이해 필수
|
|
- "성장하는 존재"라는 핵심 개념 놓침
|
|
|
|
2. **하드코딩의 한계**
|
|
- 프롬프트 하드코딩은 성장을 막는 족쇄
|
|
- 동적 구성이 존재형 AI의 본질
|
|
|
|
3. **간단한 해결책 우선**
|
|
- 컨텍스트 변수 방식이 가장 효율적
|
|
- 과도한 엔지니어링 피하기
|
|
|
|
4. **메모리와 프롬프트 통합**
|
|
- 기억 시스템과 응답 생성의 연결 필수
|
|
- 분리된 시스템은 일관성 없는 응답 생성
|
|
|
|
5. **intent 패턴의 영향**
|
|
- 과도한 패턴 매칭이 캐시 시스템 무력화
|
|
- 적절한 균형 필요
|
|
|
|
## 오후 4시 00분
|
|
|
|
### 이름 영속화 문제 발견
|
|
|
|
**서버팀 보고**:
|
|
- 코드는 정상 작동 (에러 없음)
|
|
- "내 이름은 김종태야" → "김종태님, 반갑습니다!" (인식 성공)
|
|
- 하지만 다음 대화에서 까먹음 (영속화 실패)
|
|
|
|
**원인 분석**:
|
|
1. 매 요청마다 새로운 Brain 인스턴스 생성
|
|
2. 딕셔너리만 사용 → 인스턴스 소멸 시 이름 정보도 소실
|
|
3. 컨테이너 재시작 시 모든 정보 리셋
|
|
|
|
### ChromaDB Identity 컬렉션 솔루션
|
|
|
|
**서버팀 제안**:
|
|
- 이미 사용 중인 ChromaDB에 identity 컬렉션 추가
|
|
- 벡터 검색 불필요한 단순 key-value 저장
|
|
- 파일이나 Redis 추가 불필요
|
|
|
|
**구현 내역**:
|
|
|
|
1. **memory.py에 identity 컬렉션 추가**:
|
|
```python
|
|
self.identity_collection = self.client.get_or_create_collection(
|
|
name=f"{settings.ROBING_ID}_identity",
|
|
metadata={"type": "identity", "description": "User and robing names"}
|
|
)
|
|
```
|
|
|
|
2. **store_identity() 메서드**:
|
|
```python
|
|
async def store_identity(self, user_id: str, user_name: str = None, robing_name: str = None):
|
|
doc_id = f"identity:{user_id}"
|
|
self.identity_collection.upsert(
|
|
ids=[doc_id],
|
|
documents=[f"user:{user_name},robing:{robing_name}"],
|
|
metadatas=[{"user_name": user_name, "robing_name": robing_name}]
|
|
)
|
|
```
|
|
|
|
3. **get_identity() 메서드**:
|
|
```python
|
|
async def get_identity(self, user_id: str) -> Dict:
|
|
result = self.identity_collection.get(ids=[f"identity:{user_id}"])
|
|
if result['metadatas']:
|
|
return result['metadatas'][0]
|
|
return {}
|
|
```
|
|
|
|
4. **think() 시작 시 identity 로드**:
|
|
```python
|
|
# Identity 로드 (영속 저장된 이름 정보)
|
|
identity = await self.memory.get_identity(user_id)
|
|
|
|
if identity.get('user_name'):
|
|
self.current_user_name = identity['user_name']
|
|
else:
|
|
self.current_user_name = '사용자'
|
|
```
|
|
|
|
5. **Structured Output에서 즉시 저장**:
|
|
```python
|
|
if data.get('user_name') and data['user_name'] != "null":
|
|
await self.memory.store_identity(
|
|
user_id=self.current_user_id,
|
|
user_name=data['user_name']
|
|
)
|
|
logger.info(f"[IDENTITY] 사용자 이름 저장: {data['user_name']}")
|
|
```
|
|
|
|
### 해결된 문제
|
|
|
|
1. **영속성 보장**:
|
|
- 컨테이너 재시작 후에도 이름 유지
|
|
- Brain 인스턴스 재생성과 무관
|
|
- ChromaDB 백업과 함께 관리
|
|
|
|
2. **운영 단순화**:
|
|
- 추가 인프라 불필요
|
|
- 하나의 DB로 통합 관리
|
|
- 모니터링 포인트 감소
|
|
|
|
## 교훈
|
|
|
|
1. **기존 인프라 활용**:
|
|
- 새로운 솔루션 도입보다 기존 시스템 활용이 효율적
|
|
- ChromaDB는 벡터 DB지만 key-value 저장도 가능
|
|
|
|
2. **영속성의 중요성**:
|
|
- 메모리 기반 저장은 항상 휘발성 고려
|
|
- 중요 정보는 반드시 영속 저장소에
|
|
|
|
3. **간단한 해결책 우선**:
|
|
- 파일 저장 → 권한, 백업 문제
|
|
- Redis → 새 의존성, 복잡도 증가
|
|
- ChromaDB identity → 이미 있는 것 활용
|
|
|
|
## 오후 5시 10분
|
|
|
|
### 캐시 시스템 전면 개편
|
|
|
|
**문제 발견**:
|
|
- 캐시 임계값 0.1~0.3 너무 낮음 (업계 표준 0.8)
|
|
- 3회 사용 제한 임의적 (TTL 기반이 표준)
|
|
- 템플릿 응답이 Gemini 응답과 충돌
|
|
|
|
**해결 과정**:
|
|
|
|
1. **캐시 최적화 (실패)**:
|
|
- 임계값 0.8로 상향
|
|
- TTL 5분 기반 전환
|
|
- 결과: 여전히 문제 지속
|
|
|
|
2. **캐시 완전 제거**:
|
|
```python
|
|
# config.py
|
|
USE_CONVERSATION_CACHE: bool = False # 비활성화
|
|
CACHE_DISTANCE_THRESHOLD: float = 0.95 # 사실상 무효
|
|
CACHE_TTL_SECONDS: int = 10 # 극단축
|
|
```
|
|
|
|
3. **템플릿 응답 제거**:
|
|
- 모든 하드코딩된 응답 삭제
|
|
- 100% Gemini API 사용
|
|
|
|
### 대화 연속성 문제
|
|
|
|
**증상**:
|
|
```
|
|
로빙: "세 가지 제안해볼까요?"
|
|
(사용자 동의)
|
|
로빙: "무엇을 도와드릴까요?" ❌ (문맥 손실)
|
|
```
|
|
|
|
**원인**:
|
|
- `should_store` 조건이 많은 대화 필터링
|
|
- 로빙 응답이 메모리에 저장 안됨
|
|
|
|
**해결**:
|
|
```python
|
|
# brain.py - 모든 대화 무조건 저장
|
|
# 사용자 메시지
|
|
await self.memory.store_memory(
|
|
content=f"User: {message}",
|
|
user_id=user_id,
|
|
memory_type="episodic",
|
|
metadata={"role": "user"}
|
|
)
|
|
|
|
# 로빙 응답 (필수)
|
|
await self.memory.store_memory(
|
|
content=f"Robing: {final_response}",
|
|
user_id=user_id,
|
|
memory_type="episodic",
|
|
metadata={"role": "assistant"}
|
|
)
|
|
```
|
|
|
|
## 교훈
|
|
|
|
1. **캐시는 만병통치약이 아니다**
|
|
- 잘못된 캐시는 오히려 문제 악화
|
|
- 초기에는 캐시 없이 시작
|
|
|
|
2. **대화 연속성 = 모든 발화 저장**
|
|
- User만 저장하면 반쪽짜리
|
|
- Assistant 응답도 필수
|
|
|
|
3. **템플릿 vs LLM**
|
|
- 혼용하면 일관성 파괴
|
|
- 하나로 통일 필요
|
|
|
|
4. **업계 표준 존중**
|
|
- 유사도 임계값: 0.8
|
|
- TTL 기반 만료
|
|
- 사용 횟수보다 시간 기반
|
|
|
|
---
|
|
|
|
작성자: happybell80 & Claude
|
|
프로젝트: rb10508_micro
|
|
주제: 동적 프롬프트 구현 및 이름 영속화 |