Mova 채팅 — ‘클래식 명작’ 요청에 엔드게임이 나온 이유

요약

영역 내용
증상 “클래식 명작 처음 보는 사람용” 칩 → 주토피아(2016)·엔드게임(2019) 추천
원인 임베딩 쿼터 429 → RAG 생략 → 시대 어휘가 연도 조건이 안 됨 → 최근 15년 우선 인기작 폴백
수정 시대 어휘(“클래식·고전·옛날”) → year_max=1999 근사 + 프롬프트에 연도 조건 표기
검증 프로덕션 재호출 — 흥부와 놀부(1959)·증발(1994) 반환 확인
부산물 mova 채팅 규칙 하네스(.claude/rules/mova-chat.md) 통합 신설

4단계로 겹친 원인

스크린샷 하나로 시작해 EC2 로그와 DB를 실측으로 따라가니, 단일 버그가 아니라 네 겹의 체인이었다.

  1. 쿼터 429 — Gemini 임베딩 일일 쿼터가 소진돼 RAG 시맨틱 검색이 통째로 생략됐다. 크론 백필(하루 950건)과 실시간 채팅이 같은 쿼터를 공유하는 구조가 배경.
  2. 폴백 매칭 실패 — 태그 8,584건 중 “클래식”·”명작” 라벨은 0건. 연도 추출기는 “90년대”·”2015년” 같은 명시 표현만 인식해 “클래식”을 연도로 해석하지 못했다. 결과: 하드 조건 0개.
  3. 선의의 정렬이 역효과 — 조건이 없으면 인기작 폴백이 “최근 15년 우선 + 평점순”으로 정렬한다. 사흘 전 콜드 스타트에서 1950~70년대 영화가 뜬금없이 뜨는 걸 막으려고 넣은 정렬이, 정작 클래식을 요청하자 옛 영화를 뒤로 밀었다. DB에 1990년 이전 작품이 238편 실재하는데 후보에 못 올라온 이유다.
  4. LLM의 포장 — 후보 16편이 전부 최신작인 상태에서 LLM은 “대중적으로 사랑받는 작품 중에서 엄선했습니다”라고 그럴듯하게 정당화했다.

수정은 두 줄기

  • _guess_year_range()에 시대 어휘 폴백 추가 — 명시 연대·연도가 없을 때만 “클래식·고전·옛날”을 year_max=1999로 근사한다. “90년대 클래식”처럼 명시 연대가 있으면 그쪽이 우선. 연도 하드 필터가 잡히는 순간 3번의 recency 정렬은 자연히 무력화되므로, 정렬 자체는 건드리지 않았다(콜드 스타트 보호는 유지).
  • 프롬프트 의도 섹션에 연도= 표기 추가 — RAG 경로는 SQL 하드 필터가 없어서, 쿼터가 살아나 시맨틱 검색이 복구된 뒤에도 LLM이 연도 제약을 볼 수 있게 했다.

배포 후 프로덕션 실호출로 확인: 같은 질의에 year_max: 1999가 잡히고 흥부와 놀부(1959)·증발(1994) 이 돌아왔다.

채팅 규칙을 하네스로

이 사고를 계기로, 두 달간 채팅 파이프라인에 누적된 결정들(카탈로그 밖 movie_id 금지, 폴백 유지, 히스토리 병합 인텐트, dedup 2단, 0건 정직 안내…)을 .claude/rules/mova-chat.md 한 파일로 통합했다. 채팅 관련 파일을 다룰 때만 자동 로드되는 경로 한정 규칙이라, 다음에 이 코드를 만지는 에이전트는 “recency 정렬과 연도 필터는 상호보완이라 한쪽만 살리면 이 사고가 재발한다” 같은 맥락을 처음부터 안고 시작한다.

남은 숙제는 근본 원인인 쿼터 공유 구조다. 백필과 실시간 채팅이 한 쿼터를 쓰는 한 RAG는 매일 오후 언젠가 죽는다 — 별도 키 분리·로컬 임베딩 폴백 등 방향은 백로그에 올려뒀다.