Llm으로 상품등록시 상품 검수하기
지난 번에 구축해두었던 AI Lambda 서버를 활용하여 상품 등록시에 상품 검수하는 기능을 개발한 것을 정리해보았습니다.
도입 배경
서비스 운영 측면에서, 종종 골치 아픈 상황을 마주치게 됩니다. 테스트용으로 등록된 상품들이나, 옵션 가격을 말도 안 되게 높게 설정해둔 상품들이 때문이죠!
이와 같은 상품들은 두 가지 문제 점이 존재합니다.
- 서비스 자체적인 결제시스템을 통해서 자금 세탁 등 악의적인 목적으로 악용될 소지가 있습니다.
- 정상적인 상품 목록 사이에 섞여 있으면 사용자 경험을 해칩니다.
검수를 하는 과정을 사람이 직접한다면 운영 비용이 너무 들기 때문에, LLM과 통합하여 상품 자동 검수 시스템을 만들었습니다.
개발에 앞선 API키 다중화
해당 기능을 개발하기 앞서, 먼저 해두어야 하는 작업이 존재했습니다. 바로 API 키 다중화입니다. 현재 Lambda에서 돌아가고 있는 LLM은 Bedrock과 같은 자체 관리 서비스와 통합되지 않은 상태입니다. 상품 설명이 길거나 옵션이 많으면 토큰을 꽤 많이 소모하는데, 이런 상태에서 상품 등록이 요청이 몰리면 rate limiting에 걸릴 수 있는 위험성이 높았습니다. 그래서 API 키를 여러 개 사용해서 이런 문제들을 최소화하고 싶었습니다.
Parameter Store에 여러 개의 API 키를 저장해두고, 라운드 로빈 방식으로 순환하며 사용하도록 구현했습니다.
class GeminiService(AIServiceInterface):
def __init__(self):
self._api_keys_list = self._get_api_key_list()
self._current_index = random.randint(0, len(self._api_keys_list) - 1)
def generate_response(self, prompt: str) -> Optional[str]:
for _ in range(len(self._api_keys_list)):
api_key: str = self._get_next_key()
try:
client = genai.Client(api_key=api_key)
response = client.models.generate_content(
model="gemini-2.0-flash", contents=prompt
)
logger.info(f"Gemini Service Success: {response.text}")
return response.text
except Exception as e:
logger.error(f"Gemini Service Error: {e}")
continue
logger.error("All API keys failed")
return None
def _get_next_key(self) -> str:
key = self._api_keys_list[self._current_index]
logger.info(f"Attempting API call with key index {self._current_index}")
self._current_index = (self._current_index + 1) % len(self._api_keys_list)
return key
def _get_api_key_list(self) -> list[str]:
api_key_dict = json.loads(GEMINI_API_KEY)
return list(api_key_dict.values())
동작 방식을 간략하게 설명하면, _current_index
를 인스턴스 변수로 두어 시작 인덱스를 랜덤하게 설정합니다. API 호출이 실패하면 다음 키로 넘어가고, 모든 키를 시도해볼 수 있도록 예외 처리를 했습니다.
이렇게 N개의 키를 설정해둔 결과, 서비스 오픈 이래로 rate limit이 걸려 장애가 난 상황은 아직 발생하지 않았습니다.
개발
제가 설계한 플로우는 다음과 같았습니다.
상품 등록이나 수정시 이전에 만들어둔 LLM Lambda로 등록한 상품 정보를 포함해 payload를 전송합니다. 이후 검수를 한 뒤에, 통과를 하지 못하는 상품이라면 Callback API를 호출합니다. Callback API에서는 검수를 실패한 상품을 비공개 처리하고, Slack에 알림을 전송합니다.
하지만 해당 플로우는 ‘LLM은 항상 완벽하게 상품을 검수한다.’ 라는 전제가 깔려있어야 가능했습니다. LLM이 잘못 동작하게 된다면 정상 제품이 목록에서 가려지기 때문입니다
따라서 다음과 같이 비공개 처리에 대한 플로우를 추가하였습니다.
- 검수에 실패하면 우선적으로 메인 및 검색에 노출되지 않게 한다.
- 하지만 LLM은 완벽하지 않기 때문에, 슬랙에서 후속 조치로 메인 및 검색 노출 제한을 푸는 액션을 할 수 있게 한다.
이를 위해 사용한 것이 바로 Slack의 interactive messages
입니다.
interactive messages
slack의 interactive messages는 사용자가 메시지 내에서 직접 상호작용할 수 있도록 버튼, 입력란, 드롭다운 등 다양한 인터페이스 요소를 포함한 메시지입니다.
Slack 앱에서 “Interactivity & Shortcuts” 활성화 및 Request URL 등록합니다. 여기서 Request URL는 버튼을 눌렀을 때 동작하는 실제 API URL이어야 합니다.
그리고 슬랙 메시지 전송 시 Block Kit을 활용해 인터랙티브 요소 포함를 포함합니다. 저는 다음과 같이 3개의 동작을 할 수 있는 버튼을 추가하였습니다.
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "메인공개😭"
},
"style": "primary",
"value": f"{prod_id}|on",
"action_id": "example_on"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "다시 가리기🫣"
},
"style": "primary",
"value": f"{prod_id}|null",
"action_id": "example_null"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "판매종료🚨"
},
"style": "danger",
"value": f"{prod_id}|fin",
"action_id": "example_fin"
}
]
}
여기서 중요한 것은 value
와 action_id
입니다.
action_id
는 각 버튼에 고유하게 부여하는 식별자로, 어떤 버튼이 클릭되었는지 서버에서 구분할 때 사용합니다.
value
는 앞서 서술한 버튼이 클릭될 때 Slack이 API 서버로 전송하는 데이터에 포함되는 값으로, 저는 상태를 변경 할 상품의 id를 포함시켰습니다.
즉, API 서버에서 example_fin
이라는 ID로 1234|fin
를 포함해서 API 서버로 전송시키면, 서버는 이를 파싱해서 1234의 상품에 대해서 판매 종료를 시킵니다.
완성, 그 이후
이렇게 해서 완성된 것이 밑과 같은 검수 봇입니다. 매우 높은 적정률로 이상 상품들을 감지해주고 있어요.
완성 이후에도 꾸준한 모니터링을 통해서 프롬프트를 수정해나가고 있습니다.
현재는 역할을 확대하여 레플리카 제품 (가품)과 같은 불법적인 상품도 검수를 해주어서 불미스러운 일을 미연에 방지시켜주는데 도움을 주고 있습니다. 운영팀의 수고도 덜어주고 사용자 경험도 좋아지다니, 개발자로서 이보다 더 할 기쁨이 없군요.
이후에도 해당 LLM 서버를 활용해 다음과 같은 것들을 기획하고 있답니다.
- DB와 연동하여 사내 매출 분석 봇
- S3와 연동하여 사내 aws cost 분석 봇
나날이 발전해가는 LLM과 함께 이를 활용해서 서비스도 나날이 발전시켜나가면 보람찰 것 같습니다.