전체 글 195

견적 생성·수정에 Tool Calling을 쓰지 않은 이유

Zelo(제로클릭 커머스)는 채팅 한 줄로 장보기를 끝내는 서비스다. "김치볶음밥 재료 준비해 줘"라고 하면 서버가 실제 상품과 가격으로 견적을 만들고, "대파는 빼고, 돼지고기는 2개로 바꿔줘"라고 하면 그 견적을 고친다. 처음 설계에서는 견적 수정을 Tool Calling으로 하기로 했다. 솔직히 가장 큰 이유는 "Tool을 써 보고 싶어서"였다. 결국 Tool 대신 LLM에게 구조화된 출력(Structured Outputs)으로 수정 계획만 받고, 서버가 그 계획을 검증해서 적용하는 쪽으로 바꿨다.1. 무엇을 만들고 있었나사용자 메시지 하나가 들어오면 서버는 먼저 LLM에게 "이 말이 무엇을 원하는지"를 묻는다. 프로젝트에서는 이걸 메뉴 계획(MenuPlan)이라고 부른다."토요일에 초등학생 둘이랑..

카테고리 없음 2026.10.30

LLM 호출 구간 분리 및 분산 락 최소화를 통한 견적 동시성 제어

Zelo(제로클릭 커머스)는 채팅 한 줄로 장보기를 끝내는 서비스다. 이 글은 채팅으로 견적을 고칠 때 같은 채팅방에 요청이 겹치면 생기는 문제를 정리하고, 이를 막으려고 분산락을 어디에 걸었는지, 왜 팀 규칙과 다르게 걸었는지를 기록한 것이다. 처음 보는 사람도 따라올 수 있도록 견적이 어떻게 저장되고 고쳐지는지부터 설명한다.1. 배경: 견적은 어떻게 저장되고 고쳐지나1-1. 견적은 채팅방마다 하나, Redis에 있다사용자가 "김치볶음밥 재료 담아줘"라고 하면 서버는 실제 상품과 가격으로 견적을 만들고, 채팅방에 견적 카드를 보여 준다. 이 견적은 DB가 아니라 Redis에 저장한다.키 quote:{채팅방 ID} 예) quote:7값 견적 JSON (quoteId, 상품..

결제 알림이 AI 응답에 섞인 이유: Chat Completions API의 Role 기반 메시지 분리

Zelo(제로클릭 커머스)는 채팅 한 줄로 장보기를 끝내는 서비스다. 이 글은 코드 리뷰 봇(CodeRabbit)의 지적 하나에서 시작해, "LLM에 지난 대화를 보낼 때 각 메시지를 누가 한 말로 보낼 것인가"를 다시 설계하고, 실제 모델로 확인하기까지의 과정을 기록한 것이다. 처음 보는 사람도 따라올 수 있도록 배경 개념부터 설명한다.1. 배경: 이 글을 읽기 전에 알아야 할 것1-1. 서비스 흐름사용자가 채팅창에 이렇게 쓴다고 하자.4인가족이 먹을 갈비찜을 담아줘그러면 서비스는 다음 순서로 움직인다.LLM이 사용자의 말을 메뉴 계획(메뉴명, 인원, 재료 목록)으로 정리한다.서버가 실제 상품과 가격으로 견적을 만들고, 채팅방에 견적 카드와 [예]·[아니오] 버튼을 보여 준다.사용자가 [예]를 누르면 ..

AI/LLM 2026.10.25

팀 프로젝트(Zelo) - LLM 구조화 출력: 엄격 모드 도입 배경

Zelo(제로클릭 커머스)는 채팅 한 줄로 장보기를 끝내는 서비스다. 사용자의 말을 LLM이 정해진 칸의 객체로 정리하고, 서버가 그 값으로 상품을 고르고 금액을 계산한다. 이 글은 LLM의 답을 어떤 방식으로 객체로 받을지 고른 과정과 근거를 기록한 것이다. 1. 무엇을 받아야 했나사용자가 이렇게 말한다.토요일에 초등학생 둘이랑 넷이 먹을 건데, 김치볶음밥 재료 3만 원 안에서 준비해 줘서버가 필요한 건 문장이 아니라 칸이 정해진 값이다. 이 값을 MenuPlan이라는 record로 정의했다.public record MenuPlan( MenuPlanIntent intent, // SHOPPING_REQUEST / GENERAL_QUESTION ..

AI/Spring AI 2026.10.24

[기술선택근거] LLM 모델 및 추론 강도 선정을 위한 성능 평가

Zelo(제로클릭 커머스)는 채팅 한 줄로 장보기를 끝내는 서비스다. 이 글은 그 안에서 사용자의 말을 "메뉴 계획"으로 정리하는 LLM을 어떤 모델로, 어떤 설정으로 쓸지를 어떻게 정했는지 기록한 것이다. 결론보다 왜 그렇게 측정했는지를 남기는 데 목적이 있다.1. 왜 모델 테스트를 다시 하게 되었나1-1. 서비스에서 LLM이 하는 일사용자가 이렇게 말한다고 하자.토요일에 초등학생 둘이랑 넷이 먹을 건데, 김치볶음밥 재료 3만 원 안에서 준비해 줘LLM은 이 말을 아래처럼 정해진 칸이 있는 객체(MenuPlan) 로 정리한다. 가격과 상품은 정하지 않는다. 그건 서버가 DB를 보고 계산한다.intent = SHOPPING_REQUEST (장보기 요청)menuName ..

AI/LLM 2026.10.23

팀 프로젝트(Zelo) - 리뷰 모델 변경 테스트 및 결정

LLM 모델 설정 변경 근거결론용도모델 · 추론 강도설정 위치일반 (메뉴 계획 · 채팅 설명)gpt-6-luna · lowspring.ai.openai.chat.model, spring.ai.openai.chat.reasoning-effort리뷰 한 줄 요약gpt-5-mini · medium (기존 그대로)zelo.llm.review-summary.model, zelo.llm.review-summary.reasoning-effort리뷰 요약의 medium은 gpt-5-mini의 기본값이다. 일반 설정의 low가 모든 요청에 섞이기 때문에, 예전과 같은 조건으로 보내려고 직접 적었다.시간 제한(zelo.llm.timeout)은 두 용도가 같이 쓴다. 12s로 올리는 것을 함께 제안한다 (아래 3장).1. ..

AI/LLM 2026.10.22

[Troubleshooting] Spring AI 2.0에서 OpenAI 타임아웃이 적용되지 않았던 이유

application.yaml에 OpenAI 타임아웃을 5초로 적어 두었는데, 실제 요청은 60초까지 기다리고 있었다. Spring AI 2.0.1이 요청을 보낼 때마다 타임아웃을 기본값 60초로 다시 덮어쓰기 때문이었고, 요청 옵션에 타임아웃을 직접 넣어 해결했다. 고친 뒤에는 1초로 설정하면 정확히 1.01초에 요청이 끊긴다.배경: 타임아웃이 왜 필요한가식당에서 주문하면서 "15분 안에 안 나오면 취소할게요"라고 정해 두는 것과 같다. 우리 서버가 OpenAI에 질문을 보내면 답이 올 때까지 기다리는데, OpenAI가 느리거나 장애가 나면 무한정 기다릴 수는 없다.사용자는 채팅창에서 계속 빈 화면을 본다.기다리는 동안 서버의 일꾼(요청 스레드)이 하나씩 묶인다. 이런 요청이 쌓이면 주문·결제 같은 다..

AI/Spring AI 2026.10.22

서킷 브레이커 이해하기: 외부 LLM API의 장애가 우리 서비스로 번지지 않도록

Zelo의 장보기 도우미 채팅은 사용자의 메시지를 받아 OpenAI(gpt-5-mini)를 호출하고, 그 결과를 AI 메시지로 저장해 응답한다. 그런데 OpenAI에 장애가 발생하면 어떻게 될까?사용자는 답변을 기다리고, 서버는 응답이 오지 않는 요청을 계속 처리해야 한다. 여기에 재시도까지 더해지면 장애가 발생한 외부 서비스에 요청이 반복해서 전달된다.이때 필요한 것이 서킷 브레이커(Circuit Breaker)이다.이 글에서는 서킷 브레이커가 언제 호출을 차단하고 어떻게 다시 허용하는지 개념을 먼저 정리하고, 각 개념이 우리 프로젝트 코드에서 어떻게 구현되었는지, 왜 그렇게 했는지를 함께 살펴본다.사용 기술: Spring Boot, Spring AI 2.0.1(OpenAI), Resilience4j ..

AI/Spring AI 2026.10.21

[Troubleshooting] OpenAI를 부르는 클래스의 테스트 커버리지가 0%였던 이유

OpenAI를 실제로 부르는 클래스 SpringAiLlmGateway의 테스트 커버리지가 CI에서 0%로 나왔다. 테스트를 분명히 작성했고 로컬에서는 통과했는데도 그랬다. 원인은 "테스트는 있었지만 CI에서는 한 번도 실행되지 않았다"는 것이었고, OpenAI와 통신하는 부품만 Mockito mock으로 바꾼 단위 테스트를 추가해 16줄 중 16줄을 덮었다.배경팀 프로젝트 Zelo(AI가 채팅으로 장보기를 대신해 주는 제로클릭 서비스)에서 AI·LLM 파트를 맡아서 LLM을 부르는 기반을 까는 작업을 하고 PR을 올렸었다.기술: Java 21, Spring Boot 4.1.1, Spring AI 2.0.1, OpenAI gpt-5-miniLlmGateway: LLM에게 묻는 창구 인터페이스SpringAi..

[기술선택근거] AI 채팅에 REST와 SSE를 선택한 이유

AI 쇼핑 채팅에 REST와 SSE를 선택한 이유사용자 요청은 REST로 처리하고, 실시간 진행 상황과 결과 전달에는 SSE를 활용한다.이번 프로젝트에서 AI와 대화하며 상품을 찾고, 견적을 받은 뒤 주문까지 연결하는 채팅 기능을 맡았다.채팅 구현 방식으로 REST, SSE, STOMP를 비교했다. 기술의 특징을 먼저 살펴보고, 우리 기능에 필요한 통신 방식이 무엇인지 기준으로 구현 방향을 정했다.01. REST — 요청을 보내고 응답을 받는다REST는 자원을 중심으로 API를 설계하는 아키텍처 스타일이다. 일반적으로 HTTP의 GET, POST 등의 메서드로 데이터를 조회하거나 생성한다.채팅에서는 사용자가 메시지를 보내면 서버가 AI와 도구를 호출하고, 완성된 답변을 HTTP 응답으로 반환하는 방식으로..