지난 달, 고객 문의 챗봇에 function calling을 붙였다. 주문 조회, 환불 처리, 배송 추적 — 세 개 함수면 충분하다고 생각했다. 데모에서는 정확도 98%. PM이 "이거 바로 배포하자"고 했고, 일주일 뒤 장애가 세 번 터졌다.

모델은 당신이 생각하는 것보다 함수를 못 고른다

데모에서 테스트할 때는 프롬프트가 깔끔하다. "주문 번호 12345의 배송 상태 알려줘" — 이런 입력에는 어떤 모델이든 get_shipping_status를 정확히 부른다. 프로덕션은 다르다.

"아 그거 지난번에 산 거 있잖아요 파란색... 근데 안 왔는데 환불되나요?" 이런 입력이 들어온다. 여기서 모델이 해야 할 일은 먼저 search_orders로 주문을 찾고, 결과를 보고 배송 상태를 확인하고, 그 다음에 환불 가능 여부를 판단하는 것이다. 근데 실제로는 process_refund를 바로 호출하면서 주문 번호에 hallucinated ID를 넣는 경우가 있었다.

로그를 분석해보니 tool calling 실패의 60% 이상이 "잘못된 함수 선택"이었다. 파라미터가 틀린 게 아니라, 아예 불러야 할 함수를 잘못 골랐다. 특히 함수가 5개를 넘어가면 정확도가 급격히 떨어진다. 공식 문서에는 "20개 이하로 유지하라"고 적혀 있지만, 실무에서는 5개만 넘어도 체감 정확도가 확 내려간다. 10개쯤 되면 모델이 비슷한 이름의 함수를 혼동하기 시작하고, 15개를 넘기면 사실상 도박이다.

파라미터 환각은 조용히 온다

함수를 맞게 골라도 안심할 수 없다. 파라미터가 엉뚱한 값으로 채워지는 현상 — 우리 팀은 이걸 "파라미터 환각"이라 부른다. 가장 많이 겪은 패턴 세 가지:

  • 날짜 포맷이 YYYY-MM-DD여야 하는데 2026년 4월 3일 형태로 넣음

  • enum 값이 pending, shipped, delivered인데 배송중이라고 넣음

  • optional 파라미터에 null 대신 빈 문자열을 넣어서 downstream에서 에러

무서운 건, JSON schema validation만으로 안 잡히는 경우가 많다는 점이다. 타입은 맞는데 값이 의미적으로 틀린 상황. order_id 필드에 "12345"가 아니라 "주문번호 12345"를 넣는 식이다. string 타입이니 schema 검증은 통과한다. 하지만 API는 터진다.

실패했을 때 모델이 루프에 빠진다

tool call이 에러를 리턴하면 모델이 어떻게 반응할까? 이론적으로는 사용자에게 상황을 설명하고 대안을 제시해야 한다. 현실에서는? 같은 호출을 파라미터만 살짝 바꿔서 무한 반복한다.

# 실제 프로덕션 로그에서 발견한 패턴
call_1: get_order(order_id="12345")     # 404
call_2: get_order(order_id="012345")    # 404
call_3: get_order(order_id="#12345")    # 404
call_4: get_order(order_id="12345번")   # 404
call_5: get_order(order_id="12345")     # 다시 처음으로

토큰 비용이 눈덩이처럼 불어난다. 한 세션에서 tool call이 20번 넘게 발생한 케이스도 있었다. 비용도 문제지만, 사용자 입장에서는 챗봇이 30초 넘게 "생각 중..."으로 멈춰 있는 셈이다.

결국 이렇게 바꿨다

몇 주간의 삽질 끝에 정착한 규칙들이다.

함수 수 제한과 동적 로딩. 한 턴에 모든 함수를 다 보여주지 않는다. 대화 맥락에 따라 관련 도구 3-4개만 동적으로 주입한다. 전체 도구가 15개라도 모델이 한 번에 보는 건 최대 5개. 이것만으로 잘못된 선택이 절반으로 줄었다. 구현은 간단하다 — 이전 턴의 의도를 분류하는 가벼운 classifier를 앞단에 두고, 분류 결과에 따라 tool definition을 필터링한다.

파라미터 정규화 레이어. 모델 출력과 실제 API 호출 사이에 변환 계층을 하나 둔다. 날짜 포맷 통일, enum 매핑, ID 클리닝 같은 작업을 여기서 처리한다. schema description을 아무리 잘 써도 모델이 100% 따르지는 않으니까, 방어적으로 한 번 더 정리하는 게 현실적이다. 이 레이어 하나가 파라미터 관련 에러의 70%를 잡아줬다.

최대 호출 횟수에 하드 리밋. 한 턴에 tool call 3회까지만 허용한다. 넘으면 강제로 중단하고 사용자에게 "정보가 부족해서 처리가 어렵습니다"라고 안내한다. 무한 루프 방지용인데, 의외로 사용자 경험도 좋아졌다. 어설프게 다섯 번 시도해서 엉뚱한 답을 주는 것보다, 솔직하게 모르겠다고 하는 게 신뢰를 덜 깎는다.

"언제 쓰지 말 것"을 명시. tool definition의 description에 "이 도구는 X할 때 사용"만 쓰면 부족하다. "주문 번호가 확인되지 않은 상태에서는 호출하지 마세요"처럼 negative instruction을 넣는 게 효과적이었다. 해야 할 행동을 알려주는 것보다, 하지 말아야 할 행동을 알려주는 게 더 잘 먹힌다. 사람한테도 마찬가지 아닌가.

아직 갈 길이 멀다

솔직히 tool use는 "대충 연결하면 동작하는" 기술이 아니다. 데모 정확도와 프로덕션 정확도 사이의 갭이 RAG보다 더 크다고 느낀다. 그런데 그 갭을 줄이는 건 결국 모델 성능 향상을 기다리는 게 아니라, 주변 인프라를 단단하게 쌓는 문제다. 동적 도구 선택, 파라미터 정규화, 호출 제한, 에러 핸들링 — 이런 것들이 화려하지는 않지만, 프로덕션에서 tool use가 실제로 돌아가게 만드는 진짜 작업이다.