LLM은 실제로 어떻게 함수를 호출할까?
요약
LLM의 도구 호출(tool calling)이 실제로 작동하는 내부 메커니즘을 분석합니다. LLM이 직접 함수를 실행하는 것이 아니라, 모델의 출력을 기반으로 프레임워크가 함수를 실행하고 결과를 다시 전달하는 과정을 상세히 다룹니다.
핵심 포인트
- LLM은 직접 코드를 실행하지 않고 도구 호출을 위한 인자를 생성함
- 도구 호출은 정교한 프롬프팅과 프레임워크의 실행 루프가 결합된 결과임
- PydanticAI, LangChain 등 프레임워크의 역할과 데이터 흐름 분석
- 모델의 출력값을 함수 실행 결과로 변환하여 다시 입력하는 루프 구조 설명
도구 호출(tool calls)은 실제로 어떻게 일어날까요?
이 질문이 저의 이번 조사를 시작하게 만든 질문이었습니다.
저는 다음과 같은 예시들을 계속 보았습니다:
@agent.tool
def get_weather(city: str) -> dict:
return weather_api.fetch(city)
그러고 나서 설명에서는 무심하게 이렇게 말하곤 합니다:
“LLM이
get_weather함수를 호출합니다.”
하지만... 어떻게 말인가요?
LLM이 제 Python 프로세스에 침투하여 해당 함수를 실행하는 것일까요?
LLM이 메모리 어디에 함수가 존재하는지 알고 있는 것일까요?
LLM이 제 데이터베이스, 로컬 파일, 또는 프라이빗 API(private APIs)에 직접 접근할 수 있는 것일까요?
만약 LLM이 입력을 받고 출력을 생성한다면, 어떻게 무언가를 *호출(call)*할 수 있다는 걸까요?
그리고 모델 제공업체들이 이미 네이티브 도구 호출(native tool calling)을 지원한다면, 왜 우리는 PydanticAI, LangChain, 또는 LangGraph와 같은 에이전트 프레임워크(agent frameworks)가 필요한 걸까요?
이는 저를 몇 가지 더 많은 질문으로 이끌었습니다:
- 실제로 함수를 실행하는 것은 누구인가?
- 함수의 결과는 어떻게 모델에게 다시 전달되는가?
- 도구 호출(tool calling)은 그저 정교한 프롬프팅(prompting) 기술일 뿐인가?
- LangChain은 단지 LLM 주변에 지침(instructions)을 추가하는 것에 불과한가?
- 전체 루프(loop)를 직접 구축할 수 있는가?
- 에이전트 프레임워크는 루프 그 이상의 무엇을 제공하는가?
- MCP는 도구 호출(tool calling)과 같은 것인가?
- 모든 LLM 애플리케이션에 에이전트 프레임워크가 필요한가?
저의 원래 멘탈 모델(mental model)은 단순했습니다:
입력(input) → LLM → 출력(output)
이 모델은 다음과 같은 문구들과는 호환되지 않는 것처럼 보였습니다:
LLM이 도구(tool)를 호출했습니다.
그래서 저는 이 과정을 마법처럼 취급하는 것을 멈추고, 실제 프레임워크 코드를 통해 데이터를 따라가기로 결심했습니다. 🔍
이 글은 제가 발견한 것들을 기록합니다.
첫째: 이 글의 주장들을 어떻게 검증했는가 🔬
아키텍처를 설명하기 전에, 증거의 경로를 명확히 하고 싶습니다.
이 내용은 단순히 다이어그램, AI가 생성한 설명, 또는 간접적인 튜토리얼에만 기반한 것이 아닙니다. 물론 이 글을 작성하는 데 AI의 도움을 받았지만, 연구 과정에서 결과를 검증했음을 보장하므로 결과를 신뢰하셔도 좋습니다.
저는 다음을 확인했습니다:
- PydanticAI 및 LangChain의 공개 문서.
- 현재의 에이전트 오케스트레이션 (Agent Orchestration) 구현체.
- 모델이 생성한 도구 인자 (Tool Arguments)를 검증하는 코드.
- 등록된 도구를 호출하는 정확한 라인.
- 함수 결과를 다시 모델 메시지로 변환하는 코드.
main브랜치가 변경된 후에도 증거를 검토할 수 있도록 커밋 고정 (Commit-pinned)된 리비전.
소스 검토 타임스탬프 (Source-review timestamp)
이 기사의 소스 검토는 다음 날짜에 완료되었습니다:
2026년 8월 3일 20:42 CEST
검토된 소스 스냅샷 (Inspected source snapshots)
아래의 커밋들은 검토 시점에 제가 확인한 특정 파일들의 최신 리비전입니다. 이는 **파일별 리비전 (file-specific revisions)**이며, 반드시 저장소 전체의 HEAD 커밋을 의미하는 것은 아닙니다.
| 프로젝트 | 파일 | 검토된 커밋 |
|---|---|---|
| PydanticAI | _agent_graph.py | 5dee1ea37c86df4764ff143ef4259dc8027ab81e |
| ... |
관련된 커밋 고정 파일들은 기사 전반에 걸쳐 링크되어 있습니다.
이는 프레임워크의 내부 코드가 변경되기 때문에 중요합니다.
main으로 연결되는 링크는 오늘날 해당 프로젝트가 포함하고 있는 내용을 보여줍니다. 반면 커밋 고정 링크는 제가 이 기사를 작성하는 동안 정확히 무엇을 검토했는지를 보여줍니다.
실질적인 예제들은 Python을 사용하지만, 아키텍처가 Python에 국한된 것은 아닙니다.
동일한 루프가 TypeScript, Go, C#, Rust 및 기타 언어에서도 나타납니다:
모델 요청 (model request)
→ 구조화된 도구 요청 (structured tool request)
→ 런타임 검증 (runtime validation)
...
Python은 단지 우리가 검토할 수 있는 읽기 쉬운 프레임워크 구현체를 제공할 뿐입니다.
한 단락으로 요약한 답변 💡
LLM은 제 함수를 직접 실행하지 않습니다.
LLM은 함수 호출 요청을 나타낼 수 있는 출력을 생성할 뿐입니다.
제 애플리케이션—또는 그 내부에서 실행되는 에이전트 프레임워크—이 해당 요청을 읽고, 이를 사용 가능한 도구와 매칭하며, 인자를 검증하고, 권한을 확인하고, 실제 함수를 실행하고, 결과를 캡처하여, 그 결과를 모델에 반환한 뒤 루프를 계속 이어갑니다.
모델은 행동을 제안합니다. 런타임이 이를 검증하고, 제어하며, 실행합니다.
네이티브 도구 호출 (Native tool calling)조차도 여전히 구조화된 모델 출력입니다.
모델 응답은 논리적으로 다음과 유사한 내용을 포함할 수 있습니다:
{
"name": "get_weather",
"arguments": {
...
그 순간에는:
get_weather()가 실행되지 않았습니다.- 어떤 날씨 API와도 통신하지 않았습니다.
- 어떤 Python 함수도 호출되지 않았습니다.
- 어떠한 외부 부작용 (side effect)도 발생하지 않았습니다.
모델은 구조화된 요청 (request) 을 생성한 것입니다.
다른 무언가가 이를 실행해야 합니다.
그 "다른 무언가"는 애플리케이션 코드, 프레임워크 코드, 또는 특정 제공자 호스팅 도구 (provider-hosted tools)의 경우 제공자가 운영하는 런타임 인프라 (runtime infrastructure)입니다.
그것은 신경망 (neural network) 자체가 아닙니다.
관여하는 네 가지 주체 🧩
다음 네 가지 구성 요소를 분리하면 아키텍처가 훨씬 명확해집니다:
- LLM.
- 제공자 API (provider API) 또는 모델 어댑터 (model adapter).
- 애플리케이션 또는 에이전트 프레임워크 (agent framework).
- 실제 도구 구현체 (tool implementation).
이들은 서로 협력하지만, 동일한 작업을 수행하지는 않습니다.
1. LLM: 토큰 및 액션 요청 생성 🧠
핵심적으로, 모델은 개념적으로 다음과 유사한 작업을 수행합니다:
입력 토큰 (input tokens) → 모델 연산 (model computation) → 출력 토큰 (output tokens)
제공자에 따라, 이러한 출력 토큰은 다음과 같이 해석될 수 있습니다:
- 자연어 텍스트 (Natural-language text).
- JSON.
- 구조화된 콘텐츠 블록 (Structured content blocks).
- 도구 호출 객체 (tool-call object).
- 여러 개의 도구 호출.
- 텍스트와 액션 요청의 혼합.
가공되지 않은 모델 (raw model)은 다음과 같은 일을 직접 수행하지 않습니다:
- Python 실행.
- JavaScript 실행.
- 내 데이터베이스 쿼리.
- 내 컴퓨터에서 임의의 파일 읽기.
- 내 계정으로 이메일 보내기.
- 내 프라이빗 API 호출.
- 애플리케이션 상태 (application state) 수정.
- 결제 환불.
- 내 운영 서버 재시작.
모델은 이러한 동작 중 어떤 것이든 설명하는 텍스트를 생성할 수 있습니다.
또한 주변 소프트웨어가 그중 하나를 수행하도록 요청하는 구조화된 출력을 생성할 수도 있습니다.
그 차이가 바로 도구 호출 (tool calling)의 기초입니다.
2. 제공자 API: 도구 요청에 구조 부여
모델 제공자들은 모델에게 도구를 제시하고 애플리케이션에 도구 요청을 반환하기 위한 규약 (conventions)을 정의합니다.
모델 요청에는 다음과 같은 내용이 포함될 수 있습니다:
- 대화 메시지 (Conversation messages).
- 시스템 지침 (System instructions).
- 도구 이름 (Tool names).
- 도구 설명 (Tool descriptions).
- 도구 인자를 위한 JSON 스키마 (JSON Schemas for tool arguments).
- 도구 선택 설정 (Tool-selection settings).
제공자(Provider)의 응답에는 다음과 같은 내용이 포함될 수 있습니다:
- 일반적인 어시스턴트 텍스트 (Normal assistant text).
- 단일 도구 호출 (One tool call).
- 다중 도구 호출 (Multiple tool calls).
- 제공자 전용 콘텐츠 블록 (Provider-specific content blocks).
- 도구 요청과 결합된 텍스트 (Text combined with tool requests).
제공자 API는 생성된 액션 요청(action request)을 애플리케이션 코드가 더 쉽게 해석할 수 있도록 만듭니다.
제공자가 제 애플리케이션 내부에서 실행되는 로컬 함수를 자동으로 보유하거나 실행하는 것은 아닙니다.
제공자는 다음과 같은 내용을 알 수 있습니다:
{
"name": "get_customer",
"description": "Retrieve a customer by ID.",
...
하지만 다음과 같은 내용을 자동으로 가지고 있지는 않습니다:
def get_customer(customer_id: str) -> dict:
return my_private_database.find_customer(customer_id)
스키마(Schema)와 구현(Implementation)은 서로 다른 것입니다.
한 가지 중요한 예외: 제공자 호스팅 도구 (provider-hosted tools)
일부 제공자는 호스팅된 웹 검색 (hosted web search), 파일 검색 (file search), 또는 코드 실행 (code execution)과 같이 자체 인프라에서 실행되는 도구를 노출합니다.
그러한 경우, 제공자의 소프트웨어가 도구를 실행할 수 있습니다.
하지만 그 경우에도 모델 가중치 (model weights)가 서버를 직접 운영하는 것은 아닙니다.
모델은 액션 요청을 생성하고, 제어된 제공자 런타임 (provider runtime)이 이를 실행합니다.
아키텍처는 여전히 다음과 같은 분리를 포함합니다:
모델 결정 (model decision)
및:
외부 실행 (external execution)
프롬프트 기반 도구 사용 vs 네이티브 도구 호출 (Prompt-based tool use versus native tool calling)
네이티브 도구 API가 보편화되기 전에는, 애플리케이션이 모델에게 다음과 같이 지시하곤 했습니다:
도구가 필요하다면 다음을 포함하는 JSON을 반환하세요:
- 도구 이름
- 인자 (arguments)
모델은 다음과 같이 답변할 수 있습니다:
{
"name": "get_customer",
"arguments": {
...
그러면 애플리케이션은 다음과 같은 과정을 거칩니다:
- JSON을 파싱 (Parse) 합니다.
- 도구 이름을 확인 (Resolve) 합니다.
- 인자를 검증 (Validate) 합니다.
- 함수를 실행 (Execute) 합니다.
- 결과를 모델에 다시 전송합니다.
네이티브 도구 호출 (native tool calling)을 사용하면, 애플리케이션은 전용 API 필드를 통해 도구 정의를 전송하고, 제공자는 전용 도구 호출 구조 (tool-call structure)를 반환합니다.
개념적으로:
{
"tool_calls": [
{
...
두 방식 모두 모델이 액션 요청 (action request)을 생성하는 과정을 포함합니다.
네이티브 도구 호출 (Native tool calling)은 일반적으로 다음과 같은 이점을 제공합니다:
- 더 나은 메시지 구조 (message structure).
- 제공자(provider)가 지원하는 호출 ID (call IDs).
- 더 신뢰할 수 있는 인자 처리 (argument handling).
- 다중 호출 (multiple calls)에 대한 더 쉬운 지원.
- 텍스트와 액션 간의 더 명확한 분리.
- 덜 취약한 출력 파싱 (output parsing).
하지만 이것이 실행 루프 (execution loop)를 제거하는 것은 아닙니다.
3. 애플리케이션 또는 프레임워크: 루프 실행 🔁
애플리케이션 또는 에이전트 프레임워크 (agent framework)가 오케스트레이션 (orchestration)을 담당합니다.
일반적으로 다음과 같은 작업들을 수행합니다:
- 메시지와 사용 가능한 도구 정의 (tool definitions)를 모델에 전송합니다.
- 모델의 응답을 읽습니다.
- 도구 요청을 감지합니다.
- 요청된 각 이름을 실제 구현체 (implementation)와 매칭합니다.
- 인자 (arguments)를 검증합니다.
- 권한 및 승인 규칙을 확인합니다.
- 실제 도구를 실행합니다.
- 결과 또는 예외 (exception)를 캡처합니다.
- 도구 결과 메시지 (tool-result message)를 생성합니다.
- 업데이트된 대화 내용을 모델에 다시 전송합니다.
- 모델이 최종 답변을 반환할 때까지 계속합니다.
프로덕션용 프레임워크는 또한 다음과 같은 사항들을 처리할 수 있습니다:
- 스트리밍 (Streaming).
- 병렬 도구 호출 (Parallel tool calls).
- 재시도 (Retries).
- 잘못된 인자 수정 (Invalid-argument correction).
- 타임아웃 (Timeouts).
- 취소 (Cancellation).
- 토큰 예산 (Token budgets).
- 비용 제한 (Cost limits).
- 최대 단계 제한 (Maximum-step limits).
- 의존성 주입 (Dependency injection).
- 대화 상태 (Conversation state).
- 내구성 있는 실행 (Durable execution).
- 체크포인트 (Checkpoints).
- 사람의 승인 (Human approval).
- 구조화된 출력 (Structured outputs).
- 로깅 및 트레이싱 (Logging and tracing).
이 오케스트레이션 계층이야말로 에이전트 프레임워크가 대부분의 가치를 창출하는 지점입니다.
4. 도구: 일반적인 실행 가능한 소프트웨어 🛠️
도구 그 자체는 일반적인 코드입니다:
def get_weather(city: str) -> dict[str, object]:
return weather_api.fetch(city)
또는 다음과 같을 수도 있습니다:
async function getWeather(city: string): Promise<Weather> {
return weatherApi.fetch(city);
}
혹은:
func GetWeather(ctx context.Context, city string) (Weather, error) {
return weatherClient.Fetch(ctx, city)
}
사용되는 언어가 아키텍처 경계 (architectural boundary)를 바꾸지는 않습니다.
함수는 어떤 런타임 (runtime)이 이를 호출하기 때문에 실행됩니다.
신경망 (neural network)이 언어 런타임 (language runtime) 내부로 직접 침투하여 함수를 직접 호출하는 것이 아닙니다.
하나의 도구 호출을 처음부터 끝까지 따라가기
이 글 전체에서 하나의 도구를 사용해 보겠습니다:
def get_customer(customer_id: str) -> dict[str, str]:
...
사용자의 질문은 다음과 같습니다:
cust-1001 고객은 어떤 구독 플랜을 사용 중인가요?
다음은 전체 라이프사이클 (lifecycle)입니다.
1단계: 애플리케이션이 모델에 도구 정의를 전송합니다
애플리케이션은 다음을 전송합니다:
- 사용자의 메시지.
- 관련 지침 (instructions).
get_customer도구 정의.- 인자 (arguments)를 설명하는 스키마 (schema).
단순화된 정의는 다음과 같을 수 있습니다:
{
"name": "get_customer",
"description": "고객 ID로 고객 정보를 조회합니다.",
...
이제 모델은 get_customer가 질문에 답하는 데 도움이 될 수 있음을 추론할 수 있습니다.
하지만 여전히 구현 코드를 직접 실행할 수는 없습니다.
2단계: 모델이 도구 요청을 생성합니다
응답은 다음과 같이 정규화 (normalized)될 수 있습니다:
{
"tool_calls": [
{
...
이 시점에서도 고객 함수는 아직 실행되지 않았습니다.
모델은 사실상 다음과 같은 요청 티켓 (request ticket)을 작성한 것입니다:
customer_id="cust-1001"으로get_customer를 실행해 주세요.
3단계: 런타임이 요청을 검증합니다
애플리케이션 또는 프레임워크는 다음을 확인합니다:
get_customer가 존재하는가?- 이 모델 호출 (model invocation)에 노출되었는가?
customer_id가 존재하는가?- 그 값이 문자열 (string)인가?
- 이 사용자가
cust-1001에 접근할 권한이 있는가? - 이 작업에 사람의 승인이 필요한가?
- 에이전트 (agent)가 단계 제한 (step limit)을 초과했는가?
이러한 검증을 거친 후에야 런타임이 도구를 실행해야 합니다.
4단계: 일반적인 소프트웨어가 함수를 호출합니다
프레임워크는 논리적으로 다음과 동일한 작업을 수행합니다:
result = get_customer(customer_id="cust-1001")
함수는 다음과 같이 반환할 수 있습니다:
{
"id": "cust-1001",
"name": "Alice",
...
이 순간이 실제 작업이 일어나는 시점입니다.
데이터베이스는 애플리케이션 코드에 의해 쿼리 (query)되었습니다.
모델은 제안 (proposal)을 생성했을 뿐입니다.
5단계: 런타임 (runtime)이 도구 결과 (tool-result) 메시지를 생성합니다
함수의 반환 값 (return value)은 대화 내용에 포함되어야 합니다:
{
"role": "tool",
"tool_call_id": "call_123",
...
제공자 (Provider)마다 형식은 다르지만, 목적은 동일합니다:
- 결과를 올바른 도구 요청 (tool request)과 연결합니다.
- 모델이 결과를 볼 수 있게 합니다.
6단계: 모델이 다시 호출됩니다
다음 모델 요청 (model request)에는 다음 내용이 포함됩니다:
- 원래의 질문.
- 모델의 이전 도구 호출 (tool call).
- 도구 결과 (tool result).
이제 모델은 다음과 같이 답변할 수 있습니다:
고객 cust-1001은 Pro 플랜을 사용 중입니다.
전체 시퀀스 (sequence)는 다음과 같습니다:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기