23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
"모델 바깥에도 성능을 좌우하는 중요한 결정이 많습니다."
토스 커머스 추천 팀에서 나온 한마디는 최근 데이터와 AI 파이프라인이 부딪힌 현실을 보여 줍니다. 더 큰 LLM(대형 언어 모델)을 가져오거나 추천 후보 수를 무작정 늘린다고 해서 서비스 품질과 비용 문제가 저절로 해결되지 않습니다. 거대한 일체형 처리 방식은 오히려 랭커(Ranker) 모델에 병목을 일으키거나 검색 파이프라인의 유연성을 떨어뜨립니다.
오늘 소식 중 세 개가 같은 문제를 다룹니다. 무거운 처리 과정을 어떻게 단계별로 분리하고 최적화할 것인가입니다. 토스 쇼핑은 추천 후보 추출(TopK)을 조합 최적화로 다듬었고, turbopuffer는 벡터 검색 엔진의 저장 구조에서 문서와 인덱스를 분리했습니다. AI 문체 교정에 판정 전용 모델을 붙인 실험 역시 같은 결론을 가리킵니다. 나머지 소식은 글 끝에 모았습니다.
추천 시스템을 구축할 때 리트리벌(Retrieval) 모델이 뽑아내는 후보 수인 TopK를 늘리면 더 좋은 상품이 포함될 확률이 높아집니다. 하지만 토스 쇼핑 추천 시스템 분석에 따르면, 리트리벌 모델이 추가될 때마다 TopK를 경험적으로 늘리는 방식은 금방 한계에 도달했습니다.
리트리벌 단계에서 넘어오는 후보가 늘어날수록 후속 단계인 랭커(Ranker) 연산량과 광고 유효성 검사 트래픽이 커집니다. 그렇다고 모든 리트리벌 모델의 TopK를 일률적으로 20%씩 줄이면 특정 모델이 발굴하던 가치 있는 상품까지 추천 대상에서 빠져버리는 문제가 생겼습니다.
from dataclasses import dataclass
import numpy as np
@dataclass
class RetrievalModel:
name: str
conversion_rate: float # 후보 포함 시 평균 전환율
latency_ms_per_item: float # 상품 1개당 랭커 연산 추가 시간
models = [
RetrievalModel("collaborative_filtering", 0.045, 0.12),
RetrievalModel("deep_retrieval", 0.052, 0.18),
RetrievalModel("recent_views", 0.038, 0.05),
]
def evaluate_topk_allocation(allocations: dict[str, int]) -> tuple[float, float]:
total_score = 0.0
total_latency = 0.0
for m in models:
k = allocations[m.name]
# TopK가 늘어날수록 유용한 상품 채택률은 상한선에 수렴
score_gain = m.conversion_rate * (1.0 - np.exp(-0.05 * k))
total_score += score_gain
total_latency += k * m.latency_ms_per_item
return total_score, total_latency
# 예시: 각 모델별 TopK 조절 조합 평가
print(evaluate_topk_allocation({"collaborative_filtering": 50, "deep_retrieval": 30, "recent_views": 100}))
토스 팀은 이 문제를 조합 최적화로 풀었습니다. 각 리트리벌 모델의 특성과 랭커의 처리 수용량을 변수로 두고, 추천 정확도 손실을 줄이면서 전체 후보 수를 제한하는 최적화 알고리즘을 적용했습니다.
이 결과는 랭킹 모델 자체를 고도화하지 않고도 서비스 응답 속도를 개선하고 연산 비용을 줄일 수 있음을 보여 줍니다. 전체 파이프라인의 제약 조건을 명확히 정의하는 것이 무작정 모델 성능을 높이는 것보다 실무에 더 큰 영향을 미칩니다.
벡터 검색 전용 데이터베이스를 운용해 본 팀이라면 검색 속도와 데이터 업데이트 비용 사이의 가위바위보를 경험했을 것입니다. turbopuffer v3 저장 구조 개편 소식은 일체형 벡터 DB가 가진 구조적 한계를 조명합니다.
기존 turbopuffer v1과 v2는 벡터 데이터가 저장된 물리적 위치를 기준으로 문서 원본과 다른 secondary 인덱스까지 함께 배치하는 방식을 취했습니다. 이 방식은 단일 벡터 유사도 검색에는 빠르게 대응하지만, 벡터 연산과 일반 텍스트 키워드 필터링, 집계(Aggregation) 쿼리가 섞여 들어올 때 심각한 효율 저하를 일으킵니다.
[ v2 일체형 구조 ]
+-------------------------------------------------------+
| Node / File |
| [Vector Index] + [Document Text] + [Metadata Index] |
+-------------------------------------------------------+
-> 인덱스 재구성 시 문서 원본 전체를 다시 읽고 써야 함
[ v3 분리형 구조 ]
+-------------------------+ +-------------------------+
| Vector Index Storage | | Document Object Store |
| (Vector Keys & Graph) | <-> | (Raw JSON / Metadata) |
+-------------------------+ +-------------------------+
-> 벡터 인덱스와 원본 문서를 독립적으로 스케일링 및 업데이트
turbopuffer v3는 데이터 저장 구조를 고쳐 문서 원본 저장을 벡터 인덱스 레이어에서 분리했습니다. 벡터 검색은 메모리나 SSD에 최적화된 경량화 인덱스에서 빠르게 처리하고, 검색 결과에 대응하는 실제 문서 내용과 부가 메타데이터는 별도의 스토리지 엔진에서 따로 읽어오는 구조입니다.
이러한 분리 덕분에 벡터 인덱스를 재구성(Re-indexing)할 때 거대한 원본 데이터를 매번 다시 패킹할 필요가 없어졌습니다. 일반 데이터베이스처럼 검색과 데이터 저장을 독립적인 컴포넌트로 다루는 방향으로 진화하고 있습니다.
글쓰기나 코드 생성에 생성형 AI를 도입할 때 흔히 저지르는 실수는 하나의 거대한 프롬프트로 교정, 문체 수정, 사실 확인, 글 구성 검토를 한 번에 해결하려는 시도입니다. Jev를 활용한 교정 파이프라인 구축 기록은 이 방식이 실패하는 이유와 해결책을 보여 줍니다.
실험진은 AI 특유의 반복적인 문체와 상투어("AI 냄새")를 잡기 위해 판정 전용 모델인 Jev를 교정 파이프라인 세 곳에 적용했습니다.
1. 책 원고 문단 단위 교정
2. AI 문체 탐지 스킬
3. 블로그 봇 발행 점검
실험 결과 Jev 같은 판정 모델은 문단 단위의 명확한 문장 패턴이나 비교 작업에는 잘 동작했습니다. 하지만 글 전체에 걸쳐 누적되는 단어 반복, 문단 구성의 어색함, 문맥상 사실관계 오류는 잡지 못했습니다.
import re
class TextCorrectionPipeline:
def __init__(self, jev_client):
self.jev_client = jev_client
# 글 전체에서 반복되는 상투적 패턴은 정규식/코드 규칙으로 선제 격리
self.banned_phrases = ["유기적으로 진화", "획기적으로", "새로운 지평", "일맥상통"]
def step1_rule_based_filter(self, text: str) -> list[str]:
warnings = []
for phrase in self.banned_phrases:
matches = [m.start() for m in re.finditer(phrase, text)]
if len(matches) > 1:
warnings.append(f"상투어 '{phrase}'가 {len(matches)}회 반복되었습니다.")
return warnings
def step2_jev_paragraph_check(self, paragraph: str) -> bool:
# 문단 단위의 어색한 문장 판정만 Jev 모델에 위임
response = self.jev_client.judge_style(paragraph)
return response.is_natural
def process(self, full_text: str):
rule_warnings = self.step1_rule_based_filter(full_text)
paragraphs = full_text.split("\n\n")
jev_results = [self.step2_jev_paragraph_check(p) for p in paragraphs]
return rule_warnings, jev_results
이 기록이 주는 교훈은 분명합니다. AI 판정 모델에 전체 검증을 일임하지 않고 파이프라인을 분리해야 합니다. 글 전체에 걸친 규칙 기반 반복 검사는 정규식이나 정적 코드 규칙으로 처리하고, 문단 단위의 문맥 자연스러움은 Jev 같은 판정 모델에 넘기며, 사실 검증은 별도의 검색 파이프라인으로 떼어내어 결합하는 것이 효율적입니다.
오늘 다룬 세 가지 사례에 대한 기술 판단입니다.
파이프라인을 분리하고 단계를 세분화하기 전에 소속 팀의 시스템 상태를 먼저 확인하세요.
1. 지표 측정 가능 여부: 리트리벌 모델별 기여도나 AI 교정 단계별 처리 시간을 개별적으로 측정할 수 있는 로깅 체계가 갖춰져 있는지 확인하세요. 개별 단계의 병목을 수치로 파악할 수 없다면 파이프라인 분리는 복잡도만 늘리는 결과를 낳습니다.
2. 단순 규칙의 존재 유무: AI 모델이나 무거운 인덱스를 도입하기 전에 정규식, SQL 필터링, 정적 린터 같은 단순 코드 규칙으로 해결 가능한 영역이 얼마나 남아 있는지 점검하세요.
3. 컴포넌트 분리로 인한 지연 시간: 저장소와 인덱스, 혹은 필터링 단계를 분리하면 네트워크 라운드트립이 추가됩니다. 분리로 얻는 연산 이득이 네트워크 지연 손실보다 확실히 큰지 사전 검증해야 합니다.
~/.config/ghostty/config 경로를 통해 폰트를 설정하는 단편 팁입니다. 개발 실무 아키텍처와 직접 관련된 소식은 아니어서 링크만 남깁니다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.