logo
aillmfunction-calling

함수 호출(Function Calling) - LLM에게 도구를 쥐여주기

LLM은 학습된 지식으로 그럴듯한 문장을 만들지만, 실시간 재고를 조회하거나 결제를 취소하거나 정확한 계산을 하지는 못합니다. 이 한계를 넘는 표준적인 방법이 함수 호출(Function Calling), 즉 LLM에게 도구(tool)를 쥐여주는 것입니다.

이 글에서는 함수 호출이 정확히 어떤 구조로 동작하는지, 스키마는 어떻게 정의하고 실행 루프는 어떻게 도는지를 정리합니다.


1. 가장 흔한 오해부터 풀기

먼저 확실히 해둘 것이 있습니다.

LLM은 코드를 직접 실행하지 않습니다.

LLM이 하는 일은 "어떤 함수를, 어떤 인자로 부르면 되겠다" 는 의도를 구조화된 JSON으로 내놓는 것뿐입니다. 실제 실행은 우리 애플리케이션 코드가 합니다.

[사용자] "서울 날씨 어때?" [LLM] 실행❌ → "getWeather({ city: '서울' }) 를 부르면 될 것 같아" (JSON 출력) [내 코드] 실제 API 호출 → { temp: 28, sky: "맑음" } [LLM] 그 결과를 받아 "서울은 지금 28도, 맑아요" 라고 문장 생성

즉 함수 호출은 "LLM의 판단 + 개발자의 실행" 을 이어붙이는 프로토콜입니다. 이 분리가 안전성의 핵심입니다. LLM은 무엇을 할지 제안만 하고, 진짜 실행 권한은 우리가 쥡니다.


2. 스키마 정의 - 도구 설명서 건네기

LLM이 어떤 도구가 있는지 알려면, 각 함수의 이름·설명·인자 형태JSON Schema로 알려줘야 합니다. 이걸 도구 정의(tool definition)라고 합니다.

const tools = [ { name: "get_weather", description: "특정 도시의 현재 날씨를 조회한다", input_schema: { type: "object", properties: { city: { type: "string", description: "도시 이름, 예: 서울" }, unit: { type: "string", enum: ["c", "f"], description: "온도 단위" }, }, required: ["city"], }, }, ];

여기서 description이 정말 중요합니다. LLM은 이 설명만 보고 "언제 이 도구를 쓸지"를 판단합니다.

  • 함수 이름과 설명은 사람이 읽어도 명확하게 씁니다.
  • 각 인자에 설명·타입·enum·required를 촘촘히 채웁니다.
  • 애매하면 LLM이 엉뚱한 인자를 넣거나 도구를 안 쓰기도 합니다.

감각: 스키마는 "신입에게 건네는 API 사용 설명서" 입니다. 설명이 부실하면 오용됩니다.


3. 실행 루프 - 실제로 도는 사이클

함수 호출은 한 번의 요청-응답으로 끝나지 않고 루프를 돕니다. 흐름을 단계로 봅시다.

1) 사용자 메시지 + 도구 목록을 LLM에 전달 2) LLM이 판단: - 도구가 필요없다 → 그냥 답변(끝) - 도구가 필요하다 → "tool_call" 형태로 함수명+인자 JSON 반환 3) 내 코드가 그 함수를 실제로 실행 4) 실행 결과(tool_result)를 대화에 추가해 다시 LLM에 전달 5) LLM이 결과를 반영해 최종 답변 or 또 다른 도구 호출 (필요하면 2~5 반복)

코드로 보면 이런 모양입니다.

const messages = [{ role: "user", content: "서울 날씨 알려줘" }]; while (true) { const res = await llm.chat({ messages, tools }); if (res.stopReason !== "tool_use") { return res.text; // 도구 호출 없음 → 최종 답변 } // LLM이 요청한 도구 실행 for (const call of res.toolCalls) { const result = await runTool(call.name, call.input); // 실제 실행은 내 코드 messages.push({ role: "assistant", content: [call] }); messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify(result), }); } // 결과를 넣고 다시 루프 → LLM이 이어서 판단 }

포인트는 결과를 다시 대화에 넣어 되먹임한다는 것입니다. LLM은 도구 실행 결과를 "본" 다음에야 자연어 답을 만듭니다. 그리고 한 번에 여러 도구를 병렬로 요청할 수도 있습니다.


4. 어디에 쓰나

함수 호출은 LLM을 실제 시스템에 연결하는 다리입니다. 대표적인 활용:

  • DB 조회: "이번 달 주문 몇 건이야?" → query_orders({ month: 7 })
  • 외부 API 호출: 날씨, 환율, 지도, 사내 서비스 등
  • 정확한 계산: LLM은 산술에 약합니다. calculate() 같은 도구로 넘기면 정확합니다.
  • 행동(action) 수행: 이메일 발송, 티켓 생성, 예약 등(실행은 신중히).
  • 구조화된 출력 강제: 도구를 하나만 주고 반드시 그 스키마로 답하게 해 정형 JSON을 얻는 용도로도 씁니다.
LLM ─┬─ get_order() : 주문 조회 ├─ search_docs() : 문서 검색(RAG 도구화) ├─ calculate() : 계산 └─ send_email() : 메일 발송(부작용 있음, 주의)

RAG의 검색도 도구로 감싸면, LLM이 필요할 때만 검색을 호출하게 만들 수 있습니다. 이게 다음 단계인 에이전트로 이어집니다.


5. 주의점 - 실무에서 밟는 지뢰

  • LLM은 틀린 인자를 줄 수 있다. 반환된 인자를 그대로 믿지 말고 반드시 검증(validate) 하세요. 스키마 통과 여부, 값 범위, 권한을 서버에서 다시 확인합니다.
  • 부작용 있는 도구는 특히 조심. 삭제·결제·발송처럼 되돌릴 수 없는 행동은 실행 전 확인 단계나 사람의 승인을 두는 게 안전합니다.
  • 에러도 결과로 돌려준다. 도구가 실패하면 예외를 던지고 끝내지 말고, 에러 메시지를 tool_result로 넣어 LLM이 재시도하거나 사용자에게 설명하게 합니다.
  • 무한 루프 방지. LLM이 같은 도구를 계속 부를 수 있으니 최대 호출 횟수 상한을 둡니다.
  • 도구가 너무 많으면 헷갈린다. 도구 수가 수십 개면 선택 정확도가 떨어집니다. 꼭 필요한 것만, 설명을 명확히.
  • 보안. LLM이 부르는 함수라도 최소 권한 원칙을 지키고, 사용자가 볼 수 없는 데이터에 접근하지 않도록 컨텍스트에 따라 도구/파라미터를 제한합니다.
// 반환 인자를 신뢰하지 말고 서버에서 재검증 function runTool(name: string, input: unknown) { const parsed = schemas[name].safeParse(input); // 형식 검증 if (!parsed.success) return { error: "invalid arguments" }; if (!hasPermission(currentUser, name)) return { error: "forbidden" }; return handlers[name](parsed.data); }

마무리

함수 호출은 "LLM이 실행하는 게 아니라, 무엇을 실행할지 JSON으로 제안하고 실행은 내 코드가 한다" 는 구조입니다. 이 분리 덕분에 LLM을 DB·API·계산·행동에 안전하게 연결할 수 있습니다.

정리하면.

  1. 도구를 명확한 스키마로 정의한다(설명이 곧 성능).
  2. 호출 → 실행 → 결과 되먹임 루프를 돈다.
  3. 반환 인자는 검증하고, 부작용 도구는 보수적으로, 루프는 상한을 둔다.

이 구조를 여러 도구와 반복 루프로 확장하면 바로 AI 에이전트가 됩니다.

me

강경원

Backend Developer

성능과 안정성으로 신뢰받는 백엔드 개발자 · 요즘은 AI/LLM을 서비스에 접목합니다

이력서 보기