Tool Calling은 함수 호출이 아니라 인터페이스 설계다

Tool Calling을 함수 호출 기능이 아니라 에이전트와 외부 세계 사이의 계약면, 스키마, 권한, 도메인 언어를 설계하는 일로 다시 봅니다.

Tool Calling은 함수 호출이 아니다. 정확히 말하면, 함수 호출처럼 보이는 인터페이스 설계 문제다.

많은 개발자가 처음에는 Tool Calling을 이렇게 이해한다.

“LLM이 get_weather() 같은 함수를 부르게 하는 기능.”

틀린 설명은 아니다. OpenAI가 2023년 6월 function calling을 공개했을 때도 핵심 예시는 자연어를 구조화된 함수 인자로 바꾸는 것이었다. 사용자가 “Boston 날씨 알려줘”라고 말하면 모델이 get_current_weather(location: "Boston") 같은 JSON 인자를 내놓는다. OpenAI의 현재 문서도 function calling, 곧 tool calling을 모델이 외부 시스템과 인터페이스할 수 있게 하는 방식으로 설명한다.

그런데 여기서 멈추면 중요한 것을 놓친다.

Tool Calling의 본질은 “모델이 어떤 함수를 부를 수 있느냐”가 아니다. 본질은 “모델에게 세계를 어떤 조작 가능한 표면으로 보여줄 것인가”다.

이 차이가 작아 보이지만, 실제 에이전트 시스템에서는 거의 전부다. 함수 하나를 노출한다고 생각하면 이름, 인자, 반환값만 보인다. 인터페이스를 설계한다고 생각하면 권한, 실패, 의미, 관측성, 사용자 승인, 도메인 언어, 장기 유지보수까지 함께 보인다.

에이전트 도구와 상호운용성에서 다룬 MCP, A2A, A2UI 같은 프로토콜도 같은 방향을 가리킨다. 에이전트 시대의 핵심은 모델 하나가 더 똑똑해지는 것이 아니라, 모델이 외부 세계와 연결되는 경계면이 표준화되고 검증 가능해지는 것이다.

이 관점은 LLM Wiki의 핵심은 벡터 DB가 아니라 지식 아키텍처다와도 이어진다. LLM Wiki가 “어떤 지식을 어떤 구조로 모델에게 줄 것인가”의 문제라면, Tool Calling은 “어떤 행동 능력을 어떤 구조로 모델에게 줄 것인가”의 문제다. 하나는 지식의 인터페이스이고, 다른 하나는 행동의 인터페이스다.

함수가 아니라 계약이다

나쁜 Tool Calling 설계는 보통 이렇게 시작한다.

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "run_action",
"description": "Run an action for the user.",
"parameters": {
"type": "object",
"properties": {
"action": { "type": "string" },
"payload": { "type": "object" }
},
"required": ["action", "payload"]
}
}

개발자 입장에서는 편하다. 무엇이든 actionpayload로 보낼 수 있다. 새 기능이 생겨도 스키마를 자주 바꿀 필요가 없다.

하지만 에이전트 입장에서는 거의 함정이다.

이 도구는 무엇을 할 수 있는가? 언제 호출해야 하는가? 어떤 행동은 위험한가? 어떤 인자는 필수이고, 어떤 인자는 사람이 확인해야 하는가? 실패하면 재시도해도 되는가? 같은 요청을 두 번 보내면 중복 실행되는가?

이 질문에 답하지 못하는 도구는 함수가 아니라 구멍이다. 모델은 그 안으로 추측을 밀어 넣는다.

좋은 도구는 더 좁고 더 명확하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"name": "create_refund_request",
"description": "Create a refund request that must be reviewed before money is returned.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Internal order identifier."
},
"refund_amount": {
"type": "number",
"description": "Refund amount in KRW. Must not exceed the captured payment amount."
},
"reason_code": {
"type": "string",
"enum": ["damaged_item", "late_delivery", "duplicate_payment", "customer_cancelled"]
},
"customer_visible_message": {
"type": "string",
"description": "Short message shown to the customer after review is submitted."
},
"idempotency_key": {
"type": "string",
"description": "Unique key to prevent duplicate refund requests."
}
},
"required": ["order_id", "refund_amount", "reason_code", "idempotency_key"]
}
}

이 설계에서는 중요한 판단이 인터페이스 안으로 들어온다. “환불을 실행한다”가 아니라 “검토가 필요한 환불 요청을 만든다.” reason_code는 자유 텍스트가 아니라 도메인에서 합의한 선택지다. idempotency_key는 에이전트가 같은 요청을 반복할 때 생기는 부작용을 줄인다. customer_visible_message는 이 도구가 내부 API만이 아니라 고객 경험과도 이어진다는 사실을 드러낸다.

Tool Calling은 함수 시그니처가 아니다. 작업 세계를 모델이 이해할 수 있는 계약으로 줄이는 일이다.

오래된 소프트웨어 설계 원칙이 돌아온다

이 관점은 새롭지 않다. 오히려 오래된 소프트웨어 설계 원칙이 LLM 시대에 다시 전면으로 올라온 것이다.

John Ousterhout는 A Philosophy of Software Design에서 복잡성을 이렇게 정의한다.

“Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.”

Tool Calling 설계의 실패도 결국 구조의 실패다. 도구가 너무 많거나, 이름이 비슷하거나, description이 모호하거나, 반환값이 제각각이면 에이전트는 추론을 더 많이 해야 한다. 추론을 더 많이 한다는 것은 똑똑해진다는 뜻이 아니라 실패 표면이 커진다는 뜻이다.

Ousterhout의 “깊은 모듈” 개념도 그대로 적용된다. 좋은 모듈은 작은 인터페이스 뒤에 큰 구현을 숨긴다. 좋은 도구도 마찬가지다. search_customer_order_history라는 도구 뒤에는 인증, 권한, 주문 DB, 개인정보 마스킹, 속도 제한, 감사 로그가 숨어 있을 수 있다. 그러나 모델에게 보이는 표면은 단순해야 한다.

반대로 얕은 도구는 구현의 복잡성을 모델에게 떠넘긴다.

1
2
3
4
{
"name": "query_database",
"description": "Run SQL against the production database."
}

이런 도구는 강력하지만, 대부분의 에이전트 워크플로에서는 너무 많은 것을 노출한다. 스키마를 알아야 하고, 조인 관계를 알아야 하고, 개인정보 컬럼을 피해야 하고, 비용이 큰 쿼리를 피해야 하며, 읽기와 쓰기의 경계도 알아야 한다. 이것은 Tool Calling이 아니라 데이터베이스 콘솔을 모델에게 넘기는 일이다.

물론 내부 분석용 읽기 전용 환경에서는 범용 SQL 도구가 유용할 수 있다. 하지만 그때도 인터페이스 설계는 사라지지 않는다. 읽기 전용 권한, 쿼리 시간 제한, 허용 테이블, 결과 행 제한, PII 마스킹, 쿼리 설명 요구, 비용 추정 같은 제약이 인터페이스의 일부가 되어야 한다.

이름은 라우터이고, description은 사용성이다

Tool Calling에서 가장 과소평가되는 필드는 description이다.

개발자는 description을 문서 주석처럼 여긴다. 하지만 모델에게 description은 UI 문구이자 라우팅 신호다. 어떤 도구를 선택할지, 언제 선택하지 말아야 할지, 어떤 인자를 조심해야 할지를 결정하는 표면이다.

구글 백서 한국어 번역: Agent Skills에서 “description은 인터페이스다”라는 문장이 나온다. Skill의 description이 모호하면 에이전트는 Skill을 잘못 고르거나 아예 고르지 못한다. Tool도 같다.

나쁜 description:

1
Search docs.

좋은 description:

1
2
Search published engineering docs for stable architecture decisions.
Do not use for customer tickets, draft documents, or source code search.

두 번째 description은 기능뿐 아니라 경계도 말한다. 모델은 무엇을 해야 하는지만큼 무엇을 하지 말아야 하는지도 알아야 한다.

이 점에서 Tool Calling은 정보 아키텍처와 닮았다. Information Architecture for the Web and Beyond는 정보 아키텍처의 목표를 “findable and understandable”로 요약한다. 도구도 찾아질 수 있어야 하고, 이해될 수 있어야 한다. 도구 목록은 단순한 배열이 아니라 에이전트가 탐색하는 정보 환경이다.

도구 이름이 getData, fetchInfo, doTask, processRequest로 가득하면 에이전트는 길을 잃는다. 사람도 길을 잃는다.

도메인 언어가 스키마가 된다

도구 스키마는 기술 필드 목록이 아니다. 도메인 언어의 압축본이다.

Domain-Driven Design이 말하는 ubiquitous language는 인간 팀만을 위한 것이 아니다. 에이전트에게도 필요하다. 도메인 전문가, 개발자, 에이전트가 같은 단어를 써야 한다. 그래야 모델이 “환불”, “취소”, “교환”, “승인”, “정산”, “캡처”, “청구”를 같은 덩어리로 뭉개지 않는다.

예를 들어 결제 도메인에서 cancel_payment, void_authorization, refund_capture는 서로 다르다. 사람에게는 미묘한 차이처럼 보일 수 있지만 시스템에서는 돈의 흐름이 달라진다. 이 차이를 하나의 payment_action 문자열로 숨기면 모델은 매번 문맥으로 추측해야 한다.

도구 스키마는 도메인의 경계 사전이어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "void_payment_authorization",
"description": "Void an uncaptured payment authorization. Use only before capture.",
"parameters": {
"type": "object",
"properties": {
"authorization_id": { "type": "string" },
"void_reason": {
"type": "string",
"enum": ["customer_cancelled", "fraud_suspected", "inventory_unavailable"]
}
},
"required": ["authorization_id", "void_reason"]
}
}

이 스키마는 모델에게 결제 도메인의 사실을 가르친다. 캡처 전에는 void, 캡처 후에는 refund다. 자유 텍스트가 아니라 합의된 reason code를 사용한다. 이것은 단순한 타입 안정성이 아니라 의미 안정성이다.

MCP는 “함수 묶음”이 아니라 포트다

MCP 문서는 도구를 모델이 외부 시스템과 상호작용할 수 있게 하는 서버 기능으로 설명한다. 중요한 점은 tools/list, tools/call, inputSchema, structuredContent 같은 프로토콜 요소가 단순 실행을 넘어서 발견, 검증, 표시, 안전을 함께 다룬다는 것이다.

다시 말해 MCP는 함수 호출을 원격으로 보내는 규격이 아니다. 에이전트가 외부 세계를 발견하고 사용할 수 있게 하는 포트다.

Tim Berners-Lee가 웹을 만들 때도 핵심은 거대한 중앙 시스템이 아니었다. Weaving the Web의 역사에서 중요한 것은 URI, HTTP, HTML 같은 단순한 규칙들이었다. 웹은 “모든 문서를 한 데이터베이스에 넣자”가 아니라 “무엇이든 식별하고, 링크하고, 가져올 수 있는 보편적 인터페이스를 만들자”에 가까웠다.

에이전트 도구 생태계도 비슷한 길을 걷고 있다. 모든 앱마다 전용 플러그인을 만드는 방식은 오래가지 못한다. 모델 N개와 도구 M개가 있으면 통합 지점은 N x M으로 늘어난다. 에이전트 도구와 상호운용성이 강조하듯, 프로토콜은 이 통합 부채를 줄이고 도구를 재사용 가능한 연결면으로 바꾼다.

그렇다면 질문은 “MCP 서버를 몇 개 붙였는가”가 아니다.

진짜 질문은 이것이다.

이 MCP 서버가 노출하는 도구들은 좋은 인터페이스인가? 에이전트가 안전하게 발견하고 선택할 수 있는가? 사람이 승인해야 할 행동과 자동 실행 가능한 행동이 구분되어 있는가? 출력은 다음 단계의 모델 추론에 적합한 구조인가? 실패는 관측 가능하고 복구 가능한가?

실행 도구는 제품 화면처럼 설계해야 한다

사람에게 버튼을 만들 때 우리는 신중해진다.

버튼 이름을 고민한다. 위험한 버튼에는 확인 모달을 붙인다. 삭제와 보관을 구분한다. 결제 버튼은 금액과 대상을 다시 보여준다. 성공과 실패 메시지를 설계한다.

그런데 모델에게 도구를 줄 때는 갑자기 관대해진다.

“어차피 내부 함수인데 뭐.” “description 조금 쓰면 알아서 하겠지.” “실패하면 다시 물어보겠지.”

이 태도가 위험하다.

에이전트에게 도구는 화면이다. 사람이 보는 GUI가 아니라 모델이 보는 GUI다. 자연어 설명, JSON Schema, enum, required, default, permission, confirmation, output schema가 모두 UI 요소다. 단지 픽셀이 아니라 토큰으로 렌더링될 뿐이다.

그래서 실행 도구는 제품 화면처럼 설계해야 한다.

1
2
3
위험한 행동인가?
돈, 권한, 개인정보, 외부 발송, 삭제, 배포와 관련되는가?
그렇다면 dry_run, preview, confirmation, audit_log가 필요하다.

Antigravity와 TDD로 안전한 AI 쇼핑 어시스턴트 만들기에서 다룬 테스트 관점도 여기로 이어진다. 도구 호출은 최종 답변보다 더 중요한 평가 대상이다. 에이전트가 우연히 좋은 답을 했더라도, 중간에 잘못된 도구를 호출했다면 그 시스템은 안전하지 않다.

Tool Interface Checklist

실무에서 도구를 추가할 때는 함수 구현보다 먼저 인터페이스를 검토해야 한다.

1. 이름

  • 동사와 목적어가 분명한가?
  • 비슷한 도구와 구분되는가?
  • 내부 구현명이 아니라 사용자의 의도나 도메인 행동을 드러내는가?

좋다:

1
2
3
search_published_architecture_decisions
create_refund_request
get_customer_order_summary

위험하다:

1
2
3
4
5
query
execute
handle
process
run_action

2. Description

  • 언제 사용해야 하는지 말하는가?
  • 언제 사용하면 안 되는지도 말하는가?
  • 읽기 전용인지, 실행형인지, 사용자 승인이 필요한지 드러나는가?

3. Input Schema

  • 자유 문자열을 enum이나 명시적 필드로 바꿀 수 있는가?
  • 도메인 용어가 정확한가?
  • 금액, 시간, 지역, 사용자 ID 같은 값에 단위와 제약이 있는가?
  • 중복 실행을 막는 키가 필요한가?

4. Output Schema

  • 다음 모델 턴이 바로 사용할 수 있는 구조인가?
  • 사람이 볼 메시지와 기계가 읽을 데이터를 분리했는가?
  • 실패 원인이 분류되어 있는가?

5. 권한과 승인

  • 이 도구는 읽기, 쓰기, 결제, 삭제, 배포 중 어디에 속하는가?
  • 자동 실행 가능한가, 사람 승인이 필요한가?
  • 감사 로그에 무엇을 남길 것인가?

6. 평가

  • 대표 입력과 기대 도구 호출을 Golden Dataset으로 만들었는가?
  • 호출하지 말아야 하는 부정 케이스가 있는가?
  • 최종 답변뿐 아니라 도구 호출 궤적도 평가하는가?

바이브 코딩 시대의 스펙 주도 프로덕션급 개발에서 말한 스펙 주도 방식은 Tool Calling에도 그대로 적용된다. 도구를 먼저 구현하지 말고, 기대 호출 궤적과 실패 케이스를 먼저 적어야 한다.

작은 결론

Tool Calling을 함수 호출로 보면, 우리는 함수를 많이 만들게 된다.

Tool Calling을 인터페이스 설계로 보면, 우리는 경계를 설계하게 된다. 어떤 능력을 노출할지, 어떤 능력은 숨길지, 어떤 말로 설명할지, 어떤 인자를 강제할지, 어떤 행동에는 사람을 끼울지 결정하게 된다.

에이전트 시대의 소프트웨어 설계는 모델 선택에서 끝나지 않는다. 오히려 모델 주변의 인터페이스에서 시작된다.

LLM은 함수 이름을 보고 세계를 이해한다. description을 보고 의도를 추론한다. schema를 보고 가능한 행동의 형태를 배운다. 에러 메시지를 보고 다음 행동을 고른다.

그러니 Tool Calling은 작은 API 작업이 아니다.

그것은 에이전트에게 세계를 가르치는 방식이다. 그리고 세계를 잘못 가르치면, 모델은 아주 성실하게 잘못 행동한다.

참고한 흐름

Comments

댓글

GitHub 계정으로 의견을 남길 수 있습니다. 댓글은 GitHub Discussions에 저장됩니다.