"""LLM 검색어 생성 — 재정제 라운드용 정밀/광역 쿼리 생성. 원본 검색어가 0매칭일 때 사용한다: - precise: 모델명/규격을 반영한 정밀 검색어("스탠리 텀블러" → "스탠리 퀜처 887ml") → 매칭률↑ - broad: 핵심 상품 명사만 남긴 광역 검색어 → 정밀도 0건이면 폭넓게 최종 확인 레퍼런스 keyword_maker 의 규칙(모델명 우선, 일반 카테고리어 제거)을 structured output 으로 이식. """ from openai import AsyncOpenAI from pydantic import BaseModel, Field from common.logger import LOG from config.server_configs import openai_config _SYSTEM = ( "너는 커머스 검색어 생성기다. 상품 정보를 받아 쇼핑몰 검색창에 넣을 한국어 검색어 2개를 만든다.\n" "- precise: 모델명(있으면 최우선)과 핵심 규격(용량/사이즈/개입 등)을 포함한 정밀 검색어. " "동일 상품을 정확히 찾기 위함. 일반 카테고리 단어만 나열하지 말 것.\n" "- broad: 핵심 상품 명사(브랜드+제품군) 위주의 광역 검색어. precise 가 0건일 때 폭넓게 찾기 위함.\n" "검색어에 따옴표/특수문자/불필요한 수식어를 넣지 말 것." ) class Keywords(BaseModel): precise: str = Field(description="모델/규격 포함 정밀 검색어") broad: str = Field(description="핵심 명사 위주 광역 검색어") class KeywordGenerator: def __init__(self, model: str | None = None, api_key: str | None = None): self._model = model or openai_config.model self._client = AsyncOpenAI(api_key=api_key or openai_config.api_key) self.last_usage = None # 직전 호출 토큰 usage(계측용) async def generate(self, target: dict) -> Keywords: user = ( f"상품명: {target.get('product_name', '')}\n" f"모델: {target.get('model', '')}\n" f"규격: {target.get('specification', '')}\n" f"제조사/브랜드: {target.get('company', '')}" ) resp = await self._client.beta.chat.completions.parse( model=self._model, messages=[{"role": "system", "content": _SYSTEM}, {"role": "user", "content": user}], response_format=Keywords, temperature=0, ) self.last_usage = getattr(resp, "usage", None) kw = resp.choices[0].message.parsed or Keywords(precise="", broad="") LOG.d(f"[ai] 검색어 생성 precise={kw.precise!r} broad={kw.broad!r}") return kw