당근의 LLM 기반 카테고리 분류 — 2조 토큰을 쓰며 쌓은 전략과 비용 최적화
당근이 중고거래 게시글을 1,400~10,000개 카테고리로 자동 분류하며, 임베딩 사전 필터링 + BM25/임베딩 하이브리드 + 이미지 해상도 다운으로 정확도를 유지하며 토큰 비용 절감
요약
당근 Taxonomy 팀이 하루 수십만 건 게시글에 대한 LLM 분류를 운영하며, 프롬프트 개선과 모델 변경만으로는 풀리지 않는 정확도-비용 동시 최적화 문제를 분류 전략 자체를 바꿔 해결. 1,000개 규모는 임베딩 요약 사전 필터, 10,000개 규모는 BM25 + 임베딩 하이브리드 사전 필터 + DFS Two-stage. 평가는 N개 모델 majority voting 기반 LLM as a Judge.
내용
당근에서 카테고리는 검색·추천·광고·분석 전반에 쓰이는 공통 피처. 중고거래만 해도 내부적으로 최대 3-depth 1,400개 규모, 새로 정의한 6-depth 이상 10,000개 규모도 운영. 게시글 작성 시 발생하는 Kafka 이벤트를 받아 LLM으로 카테고리·속성을 추출, 그 결과를 BigQuery에 적재하고 Kafka sink로 사내 피처플랫폼에 연결.
기존 파이프라인은 카테고리 정의가 팀별로 흩어져 노하우 전파가 어려웠고, 속성 표현력 부족, 대용량 배치·백필 미지원, 품질 검증 체계 미흡이라는 한계. 이를 통합한 Taxonomy Management System을 Dataflow(Beam) 위에 올림 — Spark·Flink 대비 팀 내 운영 경험 + 스트림/배치 동시 지원이 결정 요인.
분류 결과의 정확도와 비용은 트레이드오프 관계 — 프롬프트를 다듬어도 정확도 향상이 미미해지는 지점이 오고, 더 좋은 모델로 바꾸면 정확도가 올라가지만 분류량이 많아 비용이 같이 뜀. 시선을 "LLM이 어떻게 고르게 할지"에서 "어떤 카테고리를 LLM에게 보여줄지"로 바꾸는 게 핵심 전환점.
해결 / 접근
초기 3가지 전략 (한계)
- Single Shot: 전체 카테고리 한 번에 보여주기 — 호출 1회·캐시 친화적, 카테고리 수백 개 넘으면 정확도↓
- Hierarchical: 트리 따라 한 레벨씩 내려가기 — 토큰↓, 상위에서 잘못 고르면 복구 불가
- Two-stage 토너먼트: N개 청크로 나눠 후보 선별 → 최종 라운드 — 정확도↑, 토큰 사용량↑
- 공통 한계: 어쨌든 전체 카테고리를 LLM이 한 번은 봐야 함
1,000개 규모 — Embedding Two-stage / Embedding Summary
- LLM 호출 전에 게시글-카테고리 임베딩 유사도로 상위 n%만 남긴 뒤 토너먼트
- 임베딩 연산은 LLM 대비 저렴, 사전 필터만으로 비용 큰 폭 절감
- raw 게시글에는 분류 무관 표현이 섞여 임베딩 성능 떨어짐 → LLM이 게시글을 한 줄 요약 ("노스페이스 화이트라벨 미니백 → unisex_northface_mini_bag_black") 후 그 요약문으로 임베딩
- LLM 호출 1회 추가되지만 노이즈 제거된 요약문이 더 적은 후보로도 정확도 유지
10,000개 규모 — DFS Two-stage → BM25 + 임베딩 하이브리드
- DFS Two-stage: 각 레벨에서 여러 후보를 고르며 leaf까지 탐색, 최종 후보 중 선택. depth 깊어질수록 호출·토큰 누적
- 가설: 검색에서처럼 BM25 키워드 매칭으로 사전 필터 가능 (Semantic Search Without Embeddings 글에서 힌트)
- LLM이 게시글에서 키워드·동의어·요약문을 한 번에 추출 → 키워드/동의어로 BM25 스코어, 요약문으로 임베딩 유사도, 두 랭킹을 Reciprocal Rank Fusion으로 융합 → 최종 후보 LLM 선택
- BM25는 키워드 매칭, 임베딩은 의미적 유사성 — 둘 결합으로 상호 보완. DFS 수준 정확도 유지하며 호출 횟수·토큰 절감
채택하지 않은 시도
- 키워드 룰셋: 데이터 롱테일 분포라 규칙 커버리지 좁음, "아이패드 케이스 → 태블릿 케이스" 같은 케이스에서 룰셋 복잡도 폭증
- Agentic grep: LLM에게 grep 도구만 쥐어주고 자율 탐색 — 정확도 낮고 비용 매우 높음
비용 최적화 (전략과 별개)
- 프롬프트 캐싱: 고정 정보(instruction, 카테고리 목록)를 앞, 가변 정보(게시글)를 뒤에 배치 → 정적 부분 캐시 히트율↑
- 이미지: 512x512 → 384x384 (Gemini Flash 2.0 기준 이미지당 4타일·1,032 토큰 → 1타일·약 1/4). 이미지 개수도 정확도 손실 미미한 선까지 축소
- 속성 precision: description에 분류 기준 키워드를 구체적으로 추가 (예: item condition의 new/open_box/used_like_new 각각 한국어 키워드 명시)
평가 — LLM as a Judge
- 일정 비율 샘플을 N개 모델로 분류 → majority voting으로 ground truth 형성 → 각 모델 정확도 계산·모니터링
- Category Accuracy = primary 모델 추론과 GT 일치 비율 (precision@1)
- 속성은 multi-label이라 Precision = |Pred ∩ GT| / |Pred|, Recall = |Pred ∩ GT| / |GT|
- 평가 체계 자체 검증은 human-labeled goldset과의 상관관계로 점검
다국어 번역
- 1만 개 카테고리 다국어 데이터를 LLM으로 청크 단위 번역 → 새 컨텍스트의 LLM이 자연스러움 검증
- 트리 구조 일관성을 위해 DFS로 진행, 상위 카테고리 번역을 컨텍스트로 첨부
결과 / 참고
- 운영 중인 분류 결과는 BigQuery + Kafka sink로 다양한 팀이 활용
- 활용 사례: 홈피드 추천 다양성 휴리스틱, 추천 모델 피처, 크로스 도메인 추천 (테니스 라켓 중고거래 → 테니스 모임)
- 이미지 해상도 384x384 전환으로 이미지당 토큰 1/4 절감
- 인프라: Dataflow + Beam, BigQuery (SoT), Kafka source/sink, Golang/Python SDK
- 평가 모델: gpt-5-mini, gemini-3-flash, claude-sonnet-4-5 (eval_models), primary는 gemini-2.5-flash
- 향후 방향: 안정된 택소노미는 6개월 이상 분류 결과로 자체 ML 모델 학습 → LLM과 하이브리드, 추론 품질 지표 고도화, 택소노미 evolution 지표 설계
- 출처: medium.com/daangn — 당근 Taxonomy 팀 (윈터, 지원)
- 같이 읽기: 당근 GenAI 플랫폼, Shopify product taxonomy at scale, Walmart LLM product catalog