Skip to content

feat(retrieval): 요청 카테고리 존중 + 카테고리 다양성 (#40)#59

Merged
likell1 merged 2 commits into
mainfrom
feat/40-category-request-honoring
Jul 20, 2026
Merged

feat(retrieval): 요청 카테고리 존중 + 카테고리 다양성 (#40)#59
likell1 merged 2 commits into
mainfrom
feat/40-category-request-honoring

Conversation

@likell1

@likell1 likell1 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

배경 (시연 중 발견)

"여드름이 많이 나는데, 스킨/토너는 추천해줄만한 제품이 있어?"
→ LLM: "제공된 제품 데이터에 스킨이나 토너 카테고리가 없으므로..."

LLM의 잘못된 보고였다. 실제로 그래프엔 토너 301개, 여드름 성분(상위10) 3개+ 매칭 토너만 37개 존재.

원인

query_products_by_ingredients가 전체를 relevance_score(성분 가중합)로 정렬 후 **LIMIT 5**만 반환.
앰플/세럼이 고가중 성분(나이아신아마이드+레티놀 등)을 더 많이 담아 상위 5개를 독식 → 토너가 6등 밖으로 밀림 → LLM엔 앰플만 전달됨.

핵심 갭 2개:

  1. 사용자가 콕 집어 요청한 카테고리("토너")를 시스템이 무시 (요청 파싱이 concern/skin_type만 봄)
  2. 정밀도(검색(RAG) 품질 eval — Neo4j 검색 자체의 recall/precision (생성·추출과 분해) #40) 위해 조인 랭킹을 세게 걸었더니 카테고리 다양성이 죽음

해결 (둘 다)

  • 요청 카테고리 존중: _requested_categories()로 메시지에서 포맷 추출('스킨'=토너, 단 스킨케어/스킨타입/스킨톤/스킨십 합성어는 오탐 제외). _appropriate_categories(concerns, requested)가 요청을 우선 존중(concern 적합과 교집합, 비면 요청 그대로).
  • 카테고리 다양성: 요청 없을 때 후보 풀(limit=30)에서 _diversify(per_category=2, total=6)로 한 포맷 상위 독식 방지, 부족하면 랭킹순 보충.
  • 공통화: 동기·스트리밍 중복 제품 선정 블록을 select_products()로 통합.
  • query_products_by_ingredientslimit 파라미터화, config.product_result_limit=6.

검증 (라이브 :8000)

요청 결과
'여드름 스킨/토너 추천' 토너 6개 (그라펜 포어올킬, 피지오겔 시카밸런스, 헤라 옴므 3종, 닥터자르트 티트리먼트) ✅
'여드름·모공' (요청 없음) 앰플·올인원·토너·로션·세럼 (다양) ✅
'기미 크림 추천' 색소 크림 6개 ✅

오탐 단위 확인: 스킨타입·스킨케어 → 미감지 ✅, 미백 세럼 → {세럼} ✅.
유닛테스트 12 passed, 기존 테스트 회귀 없음. 정밀도(#56 목적필터)는 뒤에서 그대로 동작.

변경 파일

  • app/services/recommend_service.py_requested_categories, _appropriate_categories(시그니처 확장), _diversify, select_products
  • app/clients/neo4j_client.pyLIMIT $limit
  • app/core/config.pyproduct_result_limit
  • tests/test_product_selection.py — 신규 12케이스

시연 중 '여드름 스킨/토너 추천'에 토너가 하나도 안 나오고 LLM이 '토너
카테고리가 없다'고 잘못 답하는 문제. 실제로는 토너 301개(여드름 성분 3개+
매칭 37개)가 있으나, 제품 쿼리가 relevance_score 정렬 후 LIMIT 5만 반환해
고가중 성분 많은 앰플/세럼이 상위를 독식 → 토너가 밀려남.

- _requested_categories(): 메시지에서 포맷 요청 추출('스킨'=토너, 단
  스킨케어/스킨타입 등 합성어는 오탐 제외)
- _appropriate_categories(): 요청 카테고리 우선 존중(concern 적합과 교집합,
  비면 요청 그대로)
- _diversify(): 요청 없을 때 후보 풀(limit=30)에서 카테고리당 상한을 둬
  한 포맷 독식 방지, 부족하면 랭킹순 보충
- query_products_by_ingredients: limit 파라미터화(LIMIT $limit)
- config: product_result_limit=6
- 동기·스트리밍 중복 로직을 select_products()로 통합
- tests: 카테고리 감지 오탐/다양성/상한 12케이스
@likell1 likell1 added the run-eval PR에 붙이면 eval-gate 워크플로우 실행 (#41) label Jul 19, 2026
recommend_service.py가 import하는 이 파일이 워킹트리에만 있고 커밋된 적이
없어(a5d44b5에서 import만 머지됨) fresh checkout(CI) 시 ModuleNotFoundError로
recommend_service 전체 import가 실패. eval-gate '생성 eval' 스텝이 이 때문에
크래시. 참조하는 config 설정(product_image_*, aws_*)은 이미 커밋돼 있어
파일만 추가하면 복구됨.
@likell1 likell1 added run-eval PR에 붙이면 eval-gate 워크플로우 실행 (#41) and removed run-eval PR에 붙이면 eval-gate 워크플로우 실행 (#41) labels Jul 19, 2026
@github-actions

Copy link
Copy Markdown

품질 회귀 게이트

PASS — 품질 게이트 통과

추출 품질 (run_eval)

지표 기준 판정
concern F1 0.8343 ≥ 0.83
skin_type 정확도 0.96 ≥ 0.94
무효값 비율 0 ≤ 0.02
추출 에러율 0 ≤ 0.0

생성 품질 (LLM-judge)

지표 기준 판정
judge OVERALL 4.6 ≥ 4.4
grounding 4.46 ≥ 4.3
format 준수 4.84 ≥ 4.3
생성 에러율 0 ≤ 0.0

검색 품질 (RAG precision)

지표 기준 판정
제품 검색 precision 0.8261 ≥ 0.75
제품 0-결과율 0.08 ≤ 0.15
성분 검색 precision 0.5027 ≥ 0.4

@likell1
likell1 merged commit ac6e1e6 into main Jul 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-eval PR에 붙이면 eval-gate 워크플로우 실행 (#41)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant