올리브영은 2026년 3월 24일, 카카오와의 협업으로 올리브영의 뷰티 상품을 카카오톡 대화방에서 바로 추천받을 수 있는 MCP를 선보였습니다. 그리고 이때 다진 MCP 구조를 토대로, 2026년 8월에는 챗GPT에서 바로 올리브영 상품을 검색하고 추천받을 수 있는 전용 플러그인까지 출시했습니다. 오늘은 그 과정에서 더 나은 검색 경험을 만들기 위해 고민한 저희 검색 스쿼드의 이야기를 들려드릴까 합니다.
늘 사던 제품이 딱히 없을 때, 다들 이런 고민 한 번쯤 해보셨죠?
- "민감한 내 피부에 써도 괜찮은 클렌저가 뭐가 있을까?"
- "지금 할인 중인 롬앤 틴트 중에 뭐가 제일 잘 나가?"
- "수분감 채워준다고 후기가 좋은 크림 어디 없나?"
- '내 퍼스널 컬러 쿨톤에 어울리는 립 찾고 싶다..'
그럴 때는 이제 카카오톡에서 그냥 물어보면 됩니다.
묻는 질문에서 피부 타입, 가격대, 할인 여부처럼 구체적인 조건이 있을 때는 조건에 맞는 상품을 바로 찾아주고, 촉촉한 크림이나 쿨톤에 어울리는 립처럼 사용감이나 어울림을 묻는 질문에는 실제 올리브영 사용자 리뷰를 기반으로 유사 상품을 추천해 주거든요.
Tool Description이라는 힌트 게임
사실 처음에는 여느 때와 다름없는 평범한 개발 프로젝트였습니다. PM은 고객 페르소나와 화면에 보여줄 정보의 우선순위를 고민했고, 개발자는 요구사항에 맞춰 시스템 구조를 설계했죠. 처음부터 PM이 JSON을 뜯어보거나, 개발자가 툴 호출 논리를 심도 있게 고민했던 건 아니었습니다.
기술적으로만 보면 MCP를 개발하는 것 자체는 그리 어려운 일은 아니었습니다. 이미 탄탄하게 구축된 올리브영의 검색 서비스를 LLM이 활용할 수 있도록 규격(Protocol)만 맞춰주면 되는 문제였으니까요. 진짜 문제는 개발 완료 후, QA 환경에서 터져 나왔습니다.
🖐🏻 여기서 잠깐! '몸으로 말해요' 게임을 해본 적 있으신가요?
'몸으로 말해요' 게임에서 정답을 맞히는 사람은 출제자의 머릿속을 직접 볼 수 없습니다. 오직 상대방이 몸짓으로 표현한 힌트만 보고 정답을 추론해야 하죠. 그리고 설명하는 사람이 어떻게 설명하느냐에 따라서도 정답률이 천차만별이 됩니다.
MCP와 소통하는 LLM도 이 게임과 같은 처지입니다. 설명하는 사람이 Tool Description이고, 정답을 맞히는 사람이 LLM인 셈이죠. GPT는 사용자의 요청과 각 툴의 description을 함께 읽고, 다음을 스스로 판단합니다.
- 어떤 툴이 적합한지
- 어떤 파라미터를 채워야 하는지
- 어떤 추가 정보가 필요한지
문제는 저희가 게임의 출제자보다 불리한 위치에 있었다는 점입니다. 플랫폼(GPT for Kakao)을 통해 전달된 요청이 어떤 원문 발화에서 나왔는지 알 수 없고, GPT의 내부 추론 과정도 볼 수 없었습니다.
따라서 저희 역할은 단순히 툴을 구현하는 것이 아니었습니다. GPT가 사용자의 의도를 정확하게 추론하도록, 툴과 파라미터 description에 충분한 맥락과 힌트를 담아내는 일이었죠. QA에서 발견된 문제를 하나씩 붙잡고 description을 고쳐나가는 과정에서, 같은 문제를 두고도 PM과 개발자가 서로 다른 길로 접근했던 순간들이 있었습니다.
같은 문제, 다른 해결법 (PM vs DEV)
1) Tool 파라미터 오인식
커머스에서 가격은 매우 중요한 검색 요소입니다. 올리브영 MCP에서도 사용자의 다양한 니즈를 충족하기 위해 여러 파라미터를 제공하는데요, 이때 최소값과 최대값을 사용하여 사용자가 원하는 가격대의 제품을 검색할 수 있도록 했습니다. 해당 파라미터를 maxPrice, minPrice로 하였으며, description은 다음과 같이 작성했습니다.
@McpToolParam(
required = false,
description = """
Minimum price (unit: KRW). Examples: 10000, 25500
If the user says "10,000 won or more", enter 10000.
"""
) minPrice: Int?,
@McpToolParam(
required = false,
description = """
Maximum price (unit: KRW). Examples: 30000, 50000
If the user says "30,000 won or less", enter 30000.
#Important - If the user does not mention this, the default value is 10000000. (Never populate arbitrarily)
""" )
maxPrice: Int?,사례를 한 가지 공유해 보겠습니다. 하단의 좌측 예시는 의도적으로 넣은 엣지 케이스입니다. 올리브영 상품 중에 2000만원짜리 상품이 없으니, LLM이 저희가 설정한 최대값(10,000,000원)을 그대로 전달할 것이고 해당 범위의 상품이 없다는 응답을 돌려줄 것이라 기대하였습니다. 하지만 실제로는 2000만원이 아닌 2만원대의 파라미터가 전달되었습니다. 2000만원대를 2만원대로 오해석한 것이었습니다. LLM이 description만으로는 이 경계 케이스를 정확하게 처리하지 못한 것이죠.
하단 오른쪽에서 보여드리는 케이스도 사용자가 상품 조회 조건값으로 15000원을 입력 시 최소 가격이 1만원으로 설정되는데, 결과값으로 기대되는 15000원대의 상품이 아닌 1만원~1만 5천원 가격대 상품이 노출되는 현상이 발생했습니다.
바로 이때 다음과 같이 개발자와 PM은 각각의 접근을 적용해 테스트해보기로 했습니다.
LLM은 추상적인 규칙보다 예시를 보고 판단 능력이 올라가기 때문에 이 부분을 Few-Shot으로 적용해보면 좋을 것 같다고 생각했죠.
Few-Shot이란, AI 모델에게 소수의 예시(예: 2~10개)를 보여줌으로써 원하는 작업 방식을 이해시키는 기법입니다.
먼저, 사용자가 가격대를 언급하는 발화 유형을 다음과 같이 모두 펼쳐보았습니다. 사용자가 가격을 언급하는 발화 유형이 범위 표현 × 표기 방식의 조합으로 정리되는 거죠.
- 범위 표현(3가지)
- 명확한 가격 언급 : 15000원
- n만원대 : 2만원대, 10만원대, 15만원대, 20만원짜리, 20만원 정도
- 이상/이하/미만/초과 언급 : 3만원 이하, 4만원 미만, 5만원 이상
- 표기 방식(3가지)
- 한글과 숫자의 혼합 표현 : 20만원, 2만원
- 한글 표현 : 만 오천원
- 숫자 표현 : 50000
이에 각 조합에 맞춰 다음과 같은 대표 예시를 description에 추가했습니다. 그리고 이 유형에 맞춰, 사용자 발화 예시를 넣어두었죠. (사용자가 대화에서 잘 사용하지 않는 'n만원 초과'와 같은 케이스는 제외하고, 핵심 예시와 경계 케이스들을 집중적으로 제공하였습니다.)
💡Examples
- "2만원대 토너" → minPrice: 20000, maxPrice: 29999
- "5만원 정도의 향수" → minPrice: 50000, maxPrice: 59999
- "3만원 이하 크림" → minPrice: 0, maxPrice: 30000
- "3만원 미만 크림" → minPrice: 0, maxPrice: 29999
- "1만원 이상 세럼" → minPrice: 10000 (maxPrice not passed)
- "15000원 제품" → minPrice: 15000, maxPrice: 15000
- "900만원대" → minPrice: 9000000, maxPrice: 9999999
가격 조건이 생각보다 복잡했습니다. “2만원대”, “10만원대”, “15만원대”와 같은 표현은 사람이라면 자연스럽게 이해할 수 있지만, 이를 LLM이 항상 동일하게 해석한다고 보장할 수는 없었습니다.
예를 들어 “2만원대”는 일반적으로 20,000원 이상 29,999원 이하를 의미합니다. 하지만 “10만원대”는 어떨까요? 사람은 자연스럽게 100,000원 이상 199,999원 이하를 떠올리지만, LLM이 항상 동일한 범위를 추론할 것이라고 확신할 수는 없었습니다.
처음에는 PM이 작성한 것처럼 다양한 예시(Few-shot)를 추가하는 방법도 고려했습니다. 하지만 개발자 입장에서는 이런 의문이 남았습니다.
- 모든 가격 표현을 예시로 나열할 수 있을까?
- 새로운 표현이 등장하면 예시를 계속 추가해야 하는 것은 아닐까?
예시를 늘리는 대신, 가격 범위를 계산하는 규칙 자체를 설명하는 것이 더 일반화된 방법이라고 판단했습니다. 마침 최근 LLM 관련 자료들을 살펴보면서, 단순한 자연어 설명보다 코드나 수식 형태로 관계를 표현했을 때 모델이 더 안정적으로 동작하는 사례들을 접하게 되었습니다. 그래서 가격 범위를 다음과 같은 규칙으로 설명해보기로 했습니다.
n만원대
minPrice = n × 10000
maxPrice = (n + 1) × 10000 - 1
유형별로 뜯어보니 특정 패턴이 눈에 띄었습니다.
숫자 표기(50000원, 3만원 이하)는 개선 전에도 비교적 안정적으로 처리됐습니다.
LLM이 학습 데이터에서 숫자와 가격 단위의 조합을 충분히 접했기 때문으로 보였습니다.
반면 한글·숫자 혼합(3만5천원, 10만원대)과 순수 한글 표기(만오천원, 이만원대)에서는 개선 전 정확도가 크게 낮았습니다.
이때, PM이 제안한 Few-shot 접근방식이 격차를 완전히 메웠습니다. 개발자가 제안한 수식 방식은 부분적인 개선 효과가 있었지만, 여전히 처리하지 못하는 케이스가 남았습니다.
그 이유를 짚어보면, 대부분의 LLM은 영어 데이터를 기반으로 학습되어 한국어 표현에 상대적으로 취약한 부분이 있어서입니다.
이만원대, 만오천원과 같은 표현은 수식의 규칙만으로는 LLM이 파싱하기 어렵고, 실제 발화 예시가 함께 제공될 때 비로소 의도를 정확하게 읽어냅니다.
처음에는 사람이 읽기 좋은 규칙을 만드는 것이 중요하다고 생각했습니다. 수식은 명확하고 일반화하기 쉬웠으니까요. 하지만 LLM은 수식보다 실제 발화에 가까운 예시에 더 민감하게 반응했고, 한국어 표현이 섞인 케이스에서 그 차이가 두드러졌습니다. Few-shot은 단순한 보완책이 아니라, 한국어 표현에서 수식이 가진 한계를 메우는 핵심 장치였던 셈입니다.
저희의 직관이 정답은 아니었습니다. MCP 개발에서 중요한 것은 API를 잘 만드는 것만이 아니라, LLM이 그것을 어떻게 이해하고 판단하는지까지 고민하는 일이었습니다.
2) Tool 체이닝
올리브영 MCP에는 크게 세 가지 툴이 있습니다. 브랜드를 검색하는 findBrands, 카테고리 코드를 찾는 findCategoryIds, 그리고 최종적으로 상품을 추천하는 findOliveGoods입니다.
사용자 발화
→ findBrands("닥터자르트") // 브랜드 코드 획득
→ findCategoryIds("선크림") // 카테고리 코드 획득
→ findOliveGoods(brandCode, categoryId) // 상품 추천사용자가 "요즘 인기 있는 닥터자르트 선크림 추천해줘"라고 했을 때, 저희가 기대한 흐름은 다음 이미지의 '예상 플로우'와 같았습니다. 바로 findOliveGoods 툴을 호출해도 응답을 제공하는 것에는 문제가 없었지만, 브랜드 및 카테고리 툴을 통해 보다 정확한 상품 응답을 제공하기 위한 장치였죠.
하지만 실제로는 브랜드 툴과 카테고리 툴이 호출되지 않은 채, LLM이 곧바로 findOliveGoods만 호출해버렸습니다.
LLM 스스로 중간 단계를 생략하고 최종 툴로 점프한 것이었죠.
이 현상에는 다음 두 가지 원인이 있을 것이라고 분석했고, PM과 개발자는 또 다시 각각의 방식대로 이 문제를 공략하기로 했습니다.
- 순서의 문제 : LLM이 어떤 툴을 먼저 호출해야 하는지 판단하지 못한다.
- 신뢰의 문제 : 툴이 반환한 결과를 LLM이 그대로 믿지 않고 스스로 재해석할 것이다.
👤 PM의 접근: 실행 순서와 응답값의 흐름을 description에 심자!
1. Execution Rules 명시
순서 문제를 해결하기 위한 첫 번째 아이디어는 단순했습니다. LLM이 순서를 모르는 거라면 아예 description에 순서를 명시하고, 첫 번째 툴의 JSON 응답 구조를 다음 툴의 description에 미리 심어서 이런 데이터가 들어올 테니 이걸 보고 다음 동작을 하라고 유도할 수 있지 않을까라는 생각이 들었거든요. 아래 세 가지 경우에 대해, 브랜드와 카테고리 툴에 execution Rules를 공통적으로 명시해주었죠.
[예시] #Execution Rules
- 카테고리와 브랜드가 모두 포함된 경우
1.먼저 브랜드 툴을 호출하세요
2.그리고 카테고리 툴을 호출하세요
3.그리고 취득한 카테고리와 브랜드 아이디를 가지고, 상품 추천 툴을 호출하세요.
- 브랜드만 포함된 경우
- …
- 카테고리가 포함된 경우
- …2. 응답값을 JSON으로 알려주기
위와 같이 툴이 실행되어야 하는 ‘실행 규칙’ 측면을 제공함과 동시에, 툴이 호출되었다면 상품 추천 툴까지 전달하기까지의 과정에서 더 명확한 장치를 만들어보고자 고민했습니다. 예를 들어, 브랜드 툴이 반환한 값이 상품 추천 툴의 어떤 파라미터로 들어가야 하는지를 LLM이 정확히 알 수 있을까 궁금했거든요.
여기서 떠올린 것이 데이터 타입(array)이었습니다. 브랜드 툴이 내놓은 브랜드 코드를, 상품 추천 툴이 전달받는 데이터 타입과 엮어서 넘겨보자는 것이었죠. LLM은 자연어보다 구조화된 데이터(JSON, YAML 등)의 키-값 관계를 훨씬 더 정확하게 인지하고 추론할 것이라고 봤습니다. 그래서 브랜드와 카테고리 툴의 description에 반환값이 어디로 흘러가야 하는지를 JSONPath 스타일로 명시했습니다.
[예시] 브랜드 툴 파라미터 description
Use all returned
data[].onlineBrandCodesvalues as the input for findOliveGoods.onlineBrandCodes. If no results are returned, pass [] to onlineBrandCodes and continue.
그리고 값을 받는 쪽인 상품 추천 툴의 파라미터에도 동일한 구조로 연결 힌트를 적용해주었습니다.
[예시] 상품 추천 툴 - onlineBrandCodes의 파라미터 description
List of brand codes returned by
findBrands (data[].onlineBrandCodes).
- Call findBrands first when the user mentions a brand, then pass all returned codes here.
- If the user does not mention a brand, pass [].
이렇게 각각의 툴에서 전달받는 값 형식까지 명확하게 표현을 해준다면, 툴 사용의 모호성을 감소시키고 타입과 데이터 에러를 감소시키며 해당 3가지 툴의 연쇄적인 사용이 더 명확해질 것이라고 확신했습니다.
- 모호성 제거
- "
data[].onlineBrandCodes처럼 JSONPath 스타일로 적어두면, LLM이 이전 단계 결과물에서 정확히 어떤 '키'의 배열을 가져와야 하는지 명확하게 이해하겠지?"
- "
- 예외 처리 명시
- "결과가 없을 때 빈 배열(
[])을 넘기라고 못 박아두면, LLM이 가상의 데이터를 지어내거나(할루시네이션) 다음 단계를 멈춰버리는 오류를 막을 수 있을 거야."
- "결과가 없을 때 빈 배열(
- 타입 일관성 유지
- "입력값이 항상 '배열(Array)'이어야 한다고 일관되게 알려주면, 단일 값이나 다른 타입이 들어오는 에러도 줄어들 것 같아."
💻 DEV의 접근: 필요한 정보는 더하고, 방해되는 정보는 덜어내자!
1) 아는 게 힘이다
올리브영에는 다양한 브랜드가 입점되어 있으며 시간에 따라 계속해서 늘어나고 있습니다.
사용자들은 특정 브랜드를 검색하기 위해 브랜드 + 키워드의 형태로 검색을 시도하곤 합니다. 저희도 실제로 아래와 같은 형태로 테스트를 진행했었습니다.
query: "홀리카홀리카 웜톤 추천해줘"
query: "JENNIFER 브랜드 상품 추천해줘"
query: "복합, 악건성 피부에 좋은 상품 추천해줘. 에스트라 브랜드 안에서 추천해줘."LLM은 발화 의도를 잘 이해해서 문장 구조에서 브랜드 성격의 키워드를 잘 발라냈지만, 아래와 같은 상황에선 종종 실수를 하곤 했습니다.
query: "라로슈 스킨 추천해줘" (라로슈포제를 의도)
query: "닥터헤어 샴푸 추천해줘" (닥터포헤어를 의도)
query: "JENNIFER 브랜드 상품 추천해줘" (제니퍼로페즈를 의도)브랜드의 특성상 영어나 숫자 등이 표현되어 있기 때문에 다양한 방식으로 불릴 수 있고 이것이 LLM의 판단을 어렵게 하고 있다고 생각했습니다. 저희는 이러한 문제를 해결하기 위해 LLM에게 브랜드 코드만이 아닌 다양한 정보를 제공하기로 했습니다.
더 나아가 LLM이 능동적으로 판단할 수 있도록 brand와 관련된 다양한 정보를 제공했습니다. 해당 내용을 적용한 이후 오탈자나 한/영 조합어 등을 분석하여 brandTool을 적극적으로 호출하는 모습을 확인할 수 있었습니다.
[
{
"brandCode": "A1234",
"onlineBrandNameKorean": "가꾸기",
"onlineBrandNameEnglish": "kakkuki",
"associationSearchKeyword": ["가구기", "kakkuku"]
//...
},
{
"brandCode": "A5678",
"onlineBrandNameKorean": "라로슈포제",
"onlineBrandNameEnglish": "LAROCHEPOSAY",
"associationSearchKeyword": ["LA ROCHE라로슈", "la roche"]
//...
}
]2) 모르는 게 약이다
상품의 카테고리는 내부적으로 대분류, 중분류, 소분류 구조를 가지고 있습니다.
예를 들어 사용자가 “스킨”을 검색하더라도, 실제로는 여러 카테고리로 해석될 수 있습니다.
그래서 저희는 단순히 스킨이라는 키워드가 포함된 상품을 검색하는 것이 아니라, 먼저 카테고리 툴을 호출하여 적절한 카테고리 코드를 찾고, 해당 코드를 기반으로 상품을 재검색하도록 설계했습니다.
뷰티
└─ 스킨케어
└─ 스킨/토너
뷰티
└─ 맨즈에딧
└─ 스킨/로션/세럼예를 들어 카테고리 툴이 아래와 같은 결과를 반환한다고 가정해보겠습니다.
[
{
"categoryName": "스킨케어",
"categoryId": "A12345"
},
{
"categoryName": "맨즈에딧",
"categoryId": "A12346"
}
]저희가 기대한 동작은 단순했습니다. 카테고리 툴이 반환한 categoryId를 그대로 상품 검색 툴에 전달하면 되는 구조였죠.
사용자: 스킨 추천해줘
↓
findCategoryIds("스킨")
↓
categoryId = A12345, A12346 획득
↓
findOliveGoods(categoryIds = [A12345, A12346])하지만 실제 동작은 예상과 달랐습니다. 재밌는 점은 툴 호출이 실패한 것이 아니었다는 점입니다. 오히려 카테고리 툴은 정상적으로 호출되었고, categoryId도 정상적으로 반환되고 있었습니다. 문제는 LLM이 반환된 결과를 다시 해석하기 시작했다는 점이었습니다. 예를 들어 사용자가 “스킨”을 검색했는데 아래와 같은 결과가 반환된 경우가 있었습니다.
{
"categoryName": "스킨케어",
"categoryId": "A12345"
}LLM은 이 응답을 그대로 쓰지 않았습니다. 사용자가 말한 "스킨"과 반환된 "맨즈에딧"이 달라 보이자 어울리지 않는 결과라고 스스로 판단하고, categoryId를 버린 것이죠. LLM이 찾지 못한 것이 아니라 찾은 결과를 믿지 않은 것이었습니다.
처음에는 categoryName 같은 부가 정보가 많을수록 더 정확하게 판단할 것이라 생각했지만 실제로는 반대였습니다. 이름이 붙어 있을수록 결과를 다시 해석하려 했고, 때로는 툴이 반환한 값보다 자신의 추론을 앞세웠습니다. 그래서 categoryName을 걷어내고 categoryId만 반환하도록 바꿨습니다.
이렇게 해석할 여지를 없애버리자, LLM은 반환된 ID를 그대로 다음 툴에 전달하기 시작했습니다. LLM을 더 똑똑하게 만드는 것이 아니라, 쓸데없이 생각할 거리를 줄여주는 것이 해결책이었던 거죠.
{
"categoryIds": ["A12345", "A12346"]
}두 접근은 사실 독립적으로는 완전한 해결이 되지 않았습니다.
실행 규칙만 적용하면 호출 순서가 맞아도 LLM이 카테고리 이름을 보고 결과를 재판단하는 문제가 남았습니다.
응답 단순화만 적용하면 데이터는 깔끔해도 LLM이 여전히 순서를 건너뛸 수 있었습니다.
두 가지를 함께 적용하고 나서야 툴 체이닝이 안정적으로 동작했고,
이를 확인하기 위해 브랜드만 언급 / 카테고리만 언급 / 브랜드+카테고리 동시 언급 3가지 시나리오로 구성한 테스트 케이스로 툴 체이닝 완성률을 측정할 수 있었습니다.
* 툴 체이닝 완성률이란, 사용자 발화에 맞는 툴들이 올바른 순서로 모두 호출되어 최종 상품 추천까지 정상 완료된 비율입니다.
| 시나리오 | 개선 전 | 개선 후 | 개선 폭 |
|---|---|---|---|
| 브랜드만 언급 | 80% | 약 95% | +15%p |
| 카테고리만 언급 | 75% | 약 95% | +20%p |
| 브랜드 + 카테고리 동시 언급 | 65% | 약 90% | +25%p |
| 전체 평균 | 약 73% | 약 93% | +20%p |
툴 체이닝이 끊긴 원인은 하나가 아니었습니다. 순서를 몰라서이기도 했고, 반환값을 믿지 못해서이기도 했습니다. PM은 흐름을 설계했고 개발자는 데이터를 단순화했는데, 두 사람이 손댄 곳은 서로 다른 층이었습니다. 하나의 증상에 원인이 여러 층으로 겹쳐 있을 수 있다는 것, 그래서 한쪽만 고치면 개선이 중간에서 멈춘다는 것이 이 사례에서 얻은 교훈입니다. 원인을 하나로 좁히려 하기보다, 층을 나눠 각각 검증하는 편이 빨랐습니다.
3) Tool 미호출
API는 MCP 스펙에 맞춰 완벽하게 구현되어 있었고, 사람이 직접 호출했을 때는 단 하나의 오차도 없이 정상 작동했습니다.
그럼에도 오른쪽 이미지처럼 findOliveGoodsByReview 툴을 자꾸만 없다고 답변을 하였습니다.
PM과 개발자는 각자의 가설을 들고 소거법으로 원인을 추적하기 시작했습니다.
"파라미터가 너무 많아서 LLM이 혼란스러운 거예요. 불필요한 파라미터를 과감히 삭제해봅시다."
그렇게 파라미터를 하나씩 들여다보다 눈에 걸린 것이 ‘Review’라는 표현이었습니다. findOliveGoods는 정렬 조건 값에, findOliveGoodsByReview는 툴 이름에 같은 단어를 쓰고 있었거든요.
| Tool 1. findOliveGoods | Tool 2. findOliveGoodsByReview |
|---|---|
정렬 조건에 '리뷰 많은 순'(MOST_REVIEWS_COUNT_DESC) 값이 존재 |
리뷰 내용을 기반으로 상품을 추천하는 툴 |
그리고 테스트 과정에서 이런 현상이 반복됐습니다. "촉촉하다는 후기가 많은 선크림 추천해줘"처럼 리뷰를 언급하는 발화에서, 사용자 의도는 후기 내용을 기반으로 상품을 탐색하는 findOliveGoodsByReview를 실행하는 것이었습니다. 하지만 실제로는 일반 상품 추천 툴(findOliveGoods)이 호출되면서 정렬 조건만 '리뷰 많은 순'으로 바뀌었습니다.
하지만 실제로는 일반 상품 추천 툴(findOliveGoods)이 호출되면서 정렬 조건만 '리뷰 많은 순'으로 바뀌었습니다. 사용자 발화에 '리뷰', '후기'라는 단어가 등장할 때만 오작동이 집중되는 패턴을 보면서, 저희는 이런 가설을 세웠습니다.
"LLM이 'review'라는 단어를 보고findOliveGoodsByReview툴로 가야 한다고 판단하면서도, 동시에 정렬 조건에MOST_REVIEWS_COUNT_DESC가 있으니 그냥 이걸 쓰면 되겠다고 단락해버리는 게 아닐까?"
가능성은 낮다고 생각했지만, 변수가 될 수 있는 요소는 먼저 제거해보자는 판단 아래 MOST_REVIEWS_COUNT_DESC 정렬 옵션 값을 삭제하고 테스트했습니다. 결과는 달라지지 않았습니다. findOliveGoodsByReview는 여전히 호출되지 않았고, 첫 번째 가설은 소거됐습니다.
"툴 이름이 서로 비슷해서 그래요. 역할을 직관적으로 알 수 있게 네이밍 컨벤션을 정리하고, LLM이 식별하기 좋게 snake_case로 엄격하게 변경해서 툴 간의 변별력을 높여보겠습니다."
첫번째 가설 소거 후 프로젝트 담당자들도 정확한 원인을 파악하지 못했고, 시간은 계속 흘러가고 있었습니다. 저희는 "AI도 결국 사람이 만든 데이터로 학습한 모델이니, 실수할 수 있다"라는 전제를 깔고, 녀석이 길을 찾을 수 있도록 일종의 힌트를 제공해 보기로 했습니다.
1) LLM은 어떤 언어로 학습이 많이 됐을까?
코딩 컨벤션이란 용어를 알고 계신가요? 가독성과 유지보수성을 높이기 위한 규칙의 일종입니다. 반드시 지켜야 하는 것은 아니지만, 시간이 지나며 암묵적인 규칙으로 일원화되어 개발자들이 코드 작성 시 적용합니다.
재밌는 점은 언어와 프레임워크마다 각자의 특성에 맞는 규칙이 따로 있다는 것입니다. 예를 들어 정적 언어인 java, kotlin, C 등에선 흔히 camelCase라 불리는 형태로 메서드와 변수명을 작성합니다. findProduct, searchProduct처럼요.
그렇다면 LLM에게 더 익숙한 프로그래밍 언어와 명명 규칙이 있을까요? 한 연구에서는 여러 LLM이 프로그래밍 언어를 자유롭게 선택할 때 Python을 기본 언어로 강하게 선호하는 경향을 확인했습니다. 또한 MCP Tool의 명명 규칙에 대해서도 커뮤니티에서 다양한 논의가 이루어지고 있었습니다. 저희는 이러한 내용들을 참고하여 Python에서 널리 사용하는 snake_case가 LLM에게 더 익숙할 수 있다는 가설을 세웠습니다.
2) 단어의 미묘함을 파악해보기
search, find, filter 이 세 단어의 미묘한 차이점을 인지하고 계시나요? 겉보기엔 다 '찾다'라는 비슷한 의미 같지만, 코드의 세계와 LLM의 임베딩 공간에서는 완전히 다른 명령어입니다.
find: 주로 고유한 ID나 단 하나의 명확한 타깃 결과를 '딱 집어서 찾아낼 때' 사용됩니다. 결과가 없거나 하나뿐인 1:1 매칭의 뉘앙스가 강합니다.search: 모호한 키워드나 검색어를 가지고 전체 데이터셋을 '훑으며 검색할 때' 사용됩니다. 검색 포털처럼 인덱싱된 데이터를 탐색하는 뉘앙스입니다.filter: 이미 존재하는 데이터 컬렉션 안에서 특정 조건(예: 상태값, 카테고리)에 맞는 것들만 '걸러내어 추출할 때' 사용됩니다.
저희는 기존에 혼용되어 있던 툴 이름들을 이 기준에 맞춰 엄격하게 재정의했습니다. 단순히 '상품을 찾는다'가 아니라, 리뷰 내용을 훑어 찾을 때는 search_olive_goods_by_review, 조건을 걸어 상품을 걸러낼 때는 filter_olive_goods로 명확하게 역할을 쪼갰습니다. LLM이 상황에 맞춰 '어떤 도구상자(Tool)를 열어야 할지' 헷갈리지 않도록 논리적 징검다리를 정교하게 놔준 것입니다.
PM과 DEV의 시도를 순서대로 적용하며 원인을 좁혀나갔습니다. 그 결과 파악하게 된 원인은 툴 이름 자체에 있었다는 사실이었습니다.
- 1차 시도: MOST_REVIEWS_COUNT_DESC 정렬 옵션 값 제거
결과: findOliveGoodsByReview 여전히 미호출 → 원인 아님 ✗- 2차 시도: 툴 명칭 변경
findOliveGoodsByReview → search_olive_goods_by_review
결과: search_olive_goods_by_review 정상 호출 확인 → 해결 완료 ✓findOliveGoods와 findOliveGoodsByReview는 LLM에게 너무 비슷해 보였고, 앞에 붙은 동사 find가 동일했기 때문에 LLM이 두 툴을 명확히 구분하지 못했던 것입니다. search로 동사를 바꾸자, LLM은 [리뷰를 언급하는 발화 > 탐색 필요 > search_olive_goods_by_review 호출]이라는 판단을 정확하게 내리기 시작했습니다.
같은 도메인(olive_goods)을 다루는 툴들이니 앞부분을 통일하는 것이 자연스러웠고, find도 충분히 명확한 동사라고 생각했습니다.
하지만 사람이 읽고 넘어가던 이름을 이제는 모델이 읽고 판단하기 때문에, 어떤 상황에 이 툴을 써야 하는가를 판단하는 핵심 단서가 필요했던 거죠. find/search/filter처럼 사람에게는 비슷하게 느껴지는 단어도 LLM에게는 전혀 다른 신호로 작동합니다.
좋은 이름의 기준에 "동료가 이해하는가"뿐 아니라 "모델이 다른 것과 구분하는가"가 하나 더 붙기 시작하며, 기존 개발 관행에는 없던 기준이 하나둘씩 생기기 시작하고 있습니다.
또 하나 인상 깊었던 점은, 이 원인을 찾기까지 정면 돌파가 아니라 소거법에 의존할 수밖에 없었다는 사실입니다. LLM이 왜 특정 툴을 호출하지 않았는지, 그 판단 과정은 저희에게 완전히 블랙박스였습니다. 로그를 아무리 들여다봐도 "왜"에 대한 답은 나오지 않았고, 결국 파라미터를 지워보고, 이름을 바꿔보며 하나씩 가설을 검증하는 것 외에는 방법이 없었습니다. 첫 번째 노이즈 제거 시도가 먼저 원인의 범위를 좁혀주었고, 그 위에서 네이밍 재정의가 최종 해결책이 될 수 있었습니다. 서로 다른 두 가설을 순서대로 검증하는 접근이 없었다면 원인을 찾는 데 훨씬 오래 걸렸을 것입니다.
다만 저희가 한 일을 LLM을 길들인 것으로 정리하고 싶지는 않습니다. 실제로 한 일은 LLM이 덜 헤매도록 입력을 정리한 쪽에 가깝고, 그 효과는 특정 모델과 버전에 묶일 수 있습니다. 오늘 통한 설명이 다음 모델에서도 통한다는 보장은 없습니다. 그래서 이 작업은 한 번 끝내는 과제가 아니라, 채널이 늘고 모델이 바뀔 때마다 다시 검증해야 하는 일이라고 보고 있습니다.
PM과 개발자의 회고: 각자의 언어로 배운 것들
👤 PM
이 프로젝트를 진행하면서 크게 두 가지 경험을 얻었습니다.
1. 서비스 맥락을 모델이 이해할 수 있는 구조로 설계하는 과정
LLM은 저희가 쌓아온 서비스 맥락을 알지 못하며, LLM에게는 사용자의 발화가 무슨 뜻인지 자명하지 않습니다. 그 간극을 메우는 것, 즉 저희가 아는 서비스 맥락을 LLM이 이해할 수 있는 언어로 옮기는 것이 이번 프로젝트에서 PM의 역할이었습니다.
그러면서 한 가지 기준이 생겼습니다. 사람이 읽기 좋은 설명과 LLM이 판단하기 좋은 설명은 다를 수 있고, 어떤 형태가 더 정확한 결과를 이끄는지를 판단하는 일이 PM 역할로 확장되고 있다는 점입니다.
2. 결국 AI도 실험의 대상이라는 것
저희는 보통 AI를 기대하는 결과를 제공해줄 수단으로서 신뢰합니다. 하지만 그 정확성을 실제로 만들어내기 위해서는, AI도 실험의 대상으로 바라봐야 한다는 것을 경험했습니다.
문제가 발생했을 때 LLM이 어떻게 판단했는지에 대한 블라인드 영역에서 원인 추적은 불완전한 예측일 수밖에 없었고, 이를 개선하기 위해서는 직접 실험해보는 것 외에 다른 방법이 없었습니다. MOST_REVIEWS_COUNT_DESC 정렬 옵션 값을 제거했을 때 결과가 달라지지 않았던 것처럼, 틀린 시도도 있었습니다. 하지만 그 시도가 하나의 가능성을 소거했고, 덕분에 진짜 원인에 더 빠르게 다가갈 수 있었습니다.
가설을 세우고, 검증하고, 틀리면 다시 시도하는 것. 실패한 실험도 과정의 일부이고, 서비스를 만들어나가는 과정과 비슷하게 그 반복을 통해 결국 원하는 결과를 이끌어낼 수 있다는 것을 배웠습니다.
이 두 가지 경험은 AI 시대에 PM의 역할이 어디로 향할지와 맞닿아 있습니다. LLM이 더 많은 것을 대신할수록, 사용자를 깊이 이해하고 그 맥락을 구조화하는 능력은 오히려 더 중요해질 것입니다. 기획서를 쓰는 방식은 달라져도 사용자 입장에서 생각하는 것은 흔들리지 않는 기반이고, 그 위에서 PM의 일은 요구사항 정의를 넘어 LLM의 판단을 설계하고 실험하는 방향으로 넓어질 것입니다.
💻 DEV
AI가 발전하면서 더 빠르고 효율적인 개발이 가능해질 것이라는 기대와, 언젠가 개발자를 대체할지도 모른다는 두려움이 공존하는 시기입니다. 저 역시 이번 MCP 프로젝트를 진행하며 일하는 방식의 변화를 직접 체감했습니다.
1. 점차 흐려지는 역할의 경계
이번 프로젝트는 전통적인 방식처럼 PM이 요구사항을 정의하고, 개발자가 그것을 구현하는 흐름만으로 진행되지 않았습니다. MCP 호출 과정에서 Tool call의 효율을 개선하는 과정에서는 PM이 개발적인 영역까지 깊이 이해해야 했고, 개발자 역시 사용자의 맥락과 서비스 경험을 함께 고민해야 했습니다. 각자의 전문성은 유지하되, 문제를 해결하기 위해 서로의 영역을 유연하게 넘나드는 일이 자연스럽게 일어났습니다.
그 과정에서 저희가 가장 집중했던 것은 기술을 어떻게 구현할 것인가도, 기획을 어떻게 정리할 것인가도 아니었습니다. 결국 핵심은 사용자에게 어떤 가치를 제공할 수 있는가였습니다. 그리고 그 가치를 제대로 만들기 위해서는 기술적 이해만큼이나 도메인 지식과 사용자 맥락에 대한 이해가 중요하다는 것을 다시 느꼈습니다.
2. AI가 대신할수록 남는 것은 판단하는 힘
최근에는 코드를 직접 작성해 주는 에이전트까지 등장하며, 많은 사람들이 LLM을 업무에 활용하고 있습니다. 하지만 AI가 많은 일을 대신할 수 있는 시대일수록, 오히려 개발자로서 쌓아온 경험과 직관은 더 중요한 역할을 합니다. 무엇이 진짜 문제인지 판단하고, 어떤 방향으로 해결해야 하는지 결정하는 일은 여전히 사람의 몫이기 때문입니다.
결국 AI 시대에 개발자의 역할은 사라지는 것이 아니라 이동하고 있습니다. 이제 개발자는 단순히 코드를 작성하는 사람을 넘어, 문제를 정의하고 해결 방향을 설계하며 사용자 가치를 만들어내는 사람에 가까워지고 있습니다. 그래서 저희는 기술 그 자체에만 머무르지 않고, 계속해서 도메인을 이해하고 문제를 바라보는 힘을 길러야 합니다.

