도구 호출로 LLM을 실시간 데이터에 연결하기
출처: https://m.youtube.com/watch?v=h8gMhXYAv1k&pp=ygUQbGxtIHRvb2wgY2FsbGluZw%3D%3D&ra=m · IBM Technology · 4:56 · 2025-01-13
요지
- 도구 호출은 LLM이 API, 데이터베이스, 코드 같은 외부 기능을 선택하고 필요한 입력 인자를 생성하게 하는 기법이다.
- 애플리케이션은 사용자 메시지와 도구 명세를 LLM에 전달하고, 모델이 요청한 도구를 실제로 실행한 뒤 그 결과를 다시 모델에 제공한다.
- 도구 명세에는 일반적으로 이름, 사용 목적을 설명하는 문장, 입력 매개변수의 구조가 포함된다.
- 전통적 도구 호출에서는 클라이언트 애플리케이션이 호출 요청의 검증, 실행, 결과 반환, 반복 종료를 직접 담당한다.
- 임베디드 도구 호출에서는 라이브러리나 프레임워크가 모델과 애플리케이션 사이에서 도구 등록, 실행, 재시도 같은 제어를 맡는다.
- 프레임워크를 사용해도 환각과 잘못된 호출이 완전히 사라지는 것은 아니므로 입력 검증, 권한 통제, 실행 한도와 관측성이 필요하다.
개요
LLM은 학습된 지식만으로 답할 때 현재 날씨, 사내 데이터베이스의 최신 값, 외부 서비스의 상태처럼 실시간으로 변하는 정보를 알 수 없다. 도구 호출은 자연어를 이해하는 LLM과 실제 데이터를 조회하거나 작업을 수행하는 소프트웨어를 연결하여 이 한계를 보완한다.
영상은 마이애미의 현재 기온을 묻는 사례로 기본 흐름을 설명한다. 모델은 날씨를 직접 조회하지 않고, 제공받은 날씨 API 명세를 바탕으로 호출할 도구와 인자를 제안한다. 애플리케이션이나 중간 라이브러리가 API를 실행해 결과를 돌려주면 모델은 그 값을 사람이 읽기 쉬운 최종 답변으로 바꾼다.
배경 / 사전 지식
LLM은 기본적으로 입력 메시지를 받아 다음에 올 내용을 생성한다. 모델에 도구 목록을 제공하더라도 실제 네트워크 요청이나 데이터베이스 쿼리를 모델 자체가 실행하는 것은 아니다. 모델의 역할은 대화 맥락과 도구 설명을 비교하여 호출할 도구와 입력 인자를 구조화된 형태로 제안하는 것이다.
도구는 다음과 같은 외부 기능일 수 있다.
- 날씨, 검색, 결제 등 외부 서비스의 API
- 사내 고객, 재고, 주문 정보를 보관하는 데이터베이스
- 계산, 파일 변환, 데이터 분석을 수행하는 코드 인터프리터
- 이메일 전송이나 레코드 변경처럼 외부 상태에 영향을 주는 함수
따라서 도구 호출 시스템에는 두 종류의 판단이 존재한다. LLM은 자연어 의미를 바탕으로 어떤 도구가 필요한지 판단하고, 애플리케이션은 그 요청이 유효하고 안전하며 실행 권한이 있는지 결정한다. 모델의 제안은 실행 명령이 아니라 검증이 필요한 비결정적 입력으로 취급해야 한다.
핵심 개념
도구 명세
도구 명세는 모델이 사용할 수 있는 기능의 인터페이스다. 일반적으로 다음 정보를 포함한다.
- 이름:
get_weather처럼 도구를 식별하는 고유한 값 - 설명: 도구를 언제 사용하고 무엇을 반환하는지 알려 주는 문장
- 입력 스키마:
location,unit등 인자의 이름, 자료형, 필수 여부와 허용 범위
설명이 모호하거나 입력 스키마가 실제 함수와 다르면 모델은 적절한 도구를 찾고도 잘못된 인자를 만들 수 있다. 명세는 모델용 안내문인 동시에 실행기가 검증에 사용할 계약이어야 한다.
도구 선택과 실행의 분리
LLM은 사용자 요청과 사용 가능한 도구를 보고 get_weather(location="Miami") 같은 호출 요청을 생성한다. 그러나 실제 날씨 API를 호출하는 주체는 클라이언트 애플리케이션이나 도구 실행 라이브러리다.
이 분리는 중요하다. API 인증 정보는 모델에 줄 필요가 없고, 애플리케이션이 허용 목록과 사용자 권한을 확인할 수 있으며, 네트워크 오류나 타임아웃을 통제할 수도 있다.
전통적 도구 호출
전통적 방식에서는 클라이언트 애플리케이션이 전체 루프를 관리한다. 애플리케이션은 메시지와 도구 명세를 모델에 보내고, 모델이 반환한 호출 요청을 파싱하고 검증한 다음 실제 함수를 실행한다. 실행 결과를 다시 대화에 추가해 모델을 호출하면 모델은 다음 도구를 요청하거나 최종 답변을 만든다.
구조가 명시적이고 세밀한 제어가 가능하지만, 호출 검증과 오류 처리, 결과 연결, 반복 종료를 개발자가 직접 올바르게 구현해야 한다.
임베디드 도구 호출
임베디드 도구 호출에서는 라이브러리나 프레임워크가 애플리케이션과 LLM 사이에 위치한다. 개발자는 라이브러리에 도구 명세와 실행 함수를 등록하고, 라이브러리는 메시지에 명세를 첨부하고 모델이 요청한 도구를 실행한 뒤 결과를 다시 전달한다. 필요하면 실패한 호출을 제한적으로 재시도할 수도 있다.
영상은 이 방식이 잘못된 호출과 환각을 방지한다고 설명한다. 더 정확히 말하면 실행 프레임워크는 존재하지 않는 도구나 형식이 잘못된 인자를 거부하고 재시도함으로써 도구 실행 오류를 줄일 수 있다. 다만 모델이 엉뚱한 도구를 선택하거나 결과를 잘못 해석하는 의미적 오류까지 완전히 제거하지는 못한다.
도구 결과와 최종 답변
도구 출력은 최신 사실이나 실행 결과를 제공하는 관찰값이다. 예를 들어 날씨 도구가 화씨 71도를 반환하면 모델은 이를 바탕으로 “마이애미의 현재 기온은 화씨 71도입니다”처럼 답한다. 모델이 결과를 임의로 바꾸지 않도록 원본 값, 단위, 조회 시각과 오류 상태를 구조적으로 전달하는 편이 좋다.
작동 원리
전통적인 도구 호출 루프는 다음 순서로 동작한다.
- 애플리케이션이 사용할 수 있는 도구의 이름, 설명, 입력 스키마를 정의한다.
- 사용자 메시지와 허용된 도구 명세를 LLM에 전달한다.
- LLM이 바로 답할 수 있으면 텍스트를 생성하고, 외부 정보가 필요하면 도구 이름과 인자를 포함한 호출 요청을 생성한다.
- 애플리케이션이 도구 이름을 허용 목록과 대조하고 입력 인자를 스키마로 검증한다.
- 검증을 통과한 실제 API, 데이터베이스 쿼리 또는 코드를 실행한다.
- 실행 결과나 구조화된 오류를 원래 호출과 연결하여 대화 기록에 추가한다.
- 갱신된 대화를 LLM에 다시 전달한다.
- 모델이 추가 호출을 요청하면 4~7단계를 반복하고, 충분한 정보가 모이면 최종 답변을 반환한다.
마이애미 날씨 사례에서는 사용자 질문과 날씨 도구 명세를 받은 모델이 location을 Miami로 지정한다. 실행기가 날씨 API에서 71°F라는 결과를 얻어 모델에 돌려주면 모델은 이 관찰값을 자연어로 정리한다.
임베디드 방식도 논리적 순서는 같지만 2~7단계의 상당 부분을 라이브러리가 캡슐화한다. 애플리케이션에서는 최종 결과만 받는 것처럼 보이더라도 내부에는 모델 호출, 도구 선택, 실행, 결과 반환의 반복 루프가 존재한다.
코드 예시
다음 파이썬 코드는 외부 패키지나 실제 API 없이 전통적 도구 호출 루프의 핵심 구조를 실행해 볼 수 있는 예제다. mock_llm은 실제 LLM이 반환할 구조화된 응답을 모사한다.
from typing import Any, Callable
def get_weather(location: str, unit: str = "fahrenheit") -> dict[str, Any]:
"""예제용 날씨 도구. 실제 환경에서는 외부 API를 호출한다."""
if location.casefold() != "miami":
return {"status": "not_found", "location": location}
value = 71 if unit == "fahrenheit" else 22
return {
"status": "ok",
"location": "Miami",
"temperature": value,
"unit": unit,
}
TOOLS: dict[str, Callable[..., dict[str, Any]]] = {
"get_weather": get_weather,
}
def mock_llm(messages: list[dict[str, Any]]) -> dict[str, Any]:
"""LLM의 도구 선택과 최종 응답을 결정적으로 모사한다."""
last_message = messages[-1]
if last_message["role"] == "user":
return {
"type": "tool_call",
"id": "call-1",
"name": "get_weather",
"arguments": {
"location": "Miami",
"unit": "fahrenheit",
},
}
result = last_message["content"]
if result.get("status") != "ok":
return {"type": "final", "content": "날씨 정보를 찾지 못했습니다."}
return {
"type": "final",
"content": (
f'{result["location"]}의 현재 기온은 '
f'{result["temperature"]}°F입니다.'
),
}
def answer(user_message: str, max_steps: int = 4) -> str:
messages: list[dict[str, Any]] = [
{"role": "user", "content": user_message}
]
for _ in range(max_steps):
response = mock_llm(messages)
if response["type"] == "final":
return str(response["content"])
tool_name = response.get("name")
if tool_name not in TOOLS:
raise ValueError(f"허용되지 않은 도구: {tool_name}")
arguments = response.get("arguments", {})
if not isinstance(arguments.get("location"), str):
raise ValueError("location은 문자열이어야 합니다.")
if arguments.get("unit") not in {"fahrenheit", "celsius"}:
raise ValueError("지원하지 않는 온도 단위입니다.")
try:
tool_result = TOOLS[tool_name](**arguments)
except Exception as exc:
tool_result = {
"status": "error",
"error_type": type(exc).__name__,
}
messages.append(
{
"role": "tool",
"tool_call_id": response["id"],
"content": tool_result,
}
)
raise RuntimeError("도구 호출 최대 횟수를 초과했습니다.")
if __name__ == "__main__":
print(answer("마이애미의 현재 기온은 몇 도야?"))
mock_llm을 실제 모델 SDK 호출로 교체하면 기본 구조는 그대로 유지된다. 중요한 부분은 모델이 반환한 이름을 TOOLS 허용 목록과 대조하고, 인자의 형식과 허용값을 검사한 뒤 실행한다는 점이다. max_steps는 반복 호출을 제한하며, 도구 결과에는 원래 요청의 tool_call_id를 연결한다.
함정·실수
- LLM이 도구를 직접 실행한다고 오해하면 보안과 오류 처리의 책임이 불분명해진다. 실제 실행 주체는 애플리케이션이나 실행 라이브러리다.
- 모델이 생성한 도구 이름과 인자를 그대로 실행하면 존재하지 않는 함수 호출, 잘못된 자료형, 명령 삽입이 발생할 수 있다. 허용 목록과 엄격한 스키마 검증을 적용한다.
- 도구 설명이 모호하면 유사한 기능 가운데 잘못된 도구를 선택할 수 있다. 사용 조건, 반환값, 제한 사항을 구체적으로 적는다.
- 도구 결과를 모델에 돌려주지 않으면 모델은 실제 조회값을 알 수 없어 추측으로 답할 수 있다. 실행 결과를 원래 호출과 연결해 대화에 추가한다.
- 단위와 조회 시각을 생략하면 71이라는 값이 화씨인지 섭씨인지, 언제 측정된 것인지 알기 어렵다. 결과를 필드가 명확한 구조로 반환한다.
- 무제한 재시도는 같은 오류를 반복해 비용과 지연을 키울 수 있다. 최대 호출 횟수, 타임아웃, 재시도 횟수와 종료 조건을 둔다.
- 임베디드 도구 호출이 환각을 완전히 제거한다고 가정하면 안 된다. 프레임워크는 실행 오류를 줄일 수 있지만 잘못된 도구 선택이나 결과 해석까지 보장하지 않는다.
- 이메일 전송이나 데이터 변경 같은 부작용 도구를 조회 도구와 동일하게 자동 실행하면 되돌리기 어려운 사고가 생긴다. 실행 전 권한 확인과 사용자 승인을 둔다.
- 외부 API나 데이터베이스가 돌려준 문자열도 신뢰할 수 없는 입력일 수 있다. 민감정보 노출과 간접 프롬프트 인젝션을 방지해야 한다.
베스트 프랙티스
- 도구 하나에는 한 가지 명확한 책임을 부여하고, 이름만으로 동작이 드러나게 설계한다.
- 설명에는 도구를 사용해야 할 조건과 사용하지 말아야 할 조건, 입력 형식, 반환 의미를 함께 명시한다.
- 모델에는 현재 작업과 사용자 권한에 필요한 최소한의 도구만 노출한다.
- 모델이 만든 호출은 제안으로 취급하고 이름, 인자, 사용자 권한, 업무 규칙을 실행 직전에 검증한다.
- 도구 출력과 오류를 구조화된 JSON으로 반환하고 값의 단위, 출처, 조회 시각, 재시도 가능 여부를 포함한다.
- 읽기 작업과 쓰기 작업을 구분하고, 결제·전송·삭제·변경처럼 부작용이 있는 호출에는 미리보기와 명시적 승인 단계를 추가한다.
- 호출 횟수, 실행 시간, 비용과 재시도에 상한을 두며, 재시도는 일시적인 오류에만 제한적으로 적용한다.
- 도구 이름, 검증 결과, 호출 ID, 지연 시간, 성공 여부를 기록하되 인증 정보와 개인정보는 로그에서 제거한다.
- 정상 호출뿐 아니라 존재하지 않는 도구, 잘못된 인자, 결과 없음, API 실패, 중복 호출과 무한 반복을 테스트한다.
- 프레임워크가 실행 루프를 숨기더라도 권한 통제, 관측성, 오류 정책과 최종 결과 검증은 애플리케이션의 책임으로 유지한다.
참고
- 영상 내 마이애미 날씨 API 호출 예시
- API, 데이터베이스, 코드 인터프리터를 도구로 사용하는 방식
- 전통적 도구 호출과 라이브러리 기반 임베디드 도구 호출 비교
- 별도의 제품, 라이브러리 또는 외부 문서명은 영상 내 명시 없음