프롬프트에서 도구 호출까지, 에이전트가 프로그램과 연결되는 법
토큰 생성에 그치는 언어모델과 타입·권한·상태가 필요한 소프트웨어 사이에는 검증 가능한 상호작용 프로토콜이 필요하다고 설명한다.
중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.
一. 서론
LLM은 컨텍스트를 받아 다음 Token을 예측한다. 자연어를 이해하는 데 능숙하지만, 아래와 같은 내용을 생성했다고 해서 실제로 날씨 서비스를 호출하지는 않는다:
{ "name" : "get_weather" , "arguments" : { "city" : "上海" } }
이것은 그저 모델이 출력한 데이터일 뿐이다. 실제로 네트워크 요청을 보내거나, 데이터베이스에 접근하거나, 비즈니스 상태를 변경하는 주체는 여전히 애플리케이션이다.
따라서 완전한 Agent는 적어도 두 종류의 컴포넌트를 포함한다:
┌──────────────────── 확률 세계 ────────────────────┐ │ │ │ 사용자 입력 ──> Prompt/컨텍스트 ──> LLM │ │ │ │ │ │ 호출 의도 생성 │ │ ▼ │ │ tool_call (name, arguments) │ └──────────────────────────┬────────────────────────┘ │ 프로토콜 경계 ▼ ┌──────────────────── 결정론적 세계 ──────────────────┐ │ │ │ Schema 검증 ──> 권한 확인 ──> 도구 실행 │ │ │ │ │ │ └──실패 정보 └──실행 결과 │ │ \ / │ │ └──> 모델 컨텍스트에 다시 기록 ───────────────┘ │ │ └───────────────────────────────────────────────────┘
Function Calling은 다단계 과정이다. 애플리케이션이 모델에 도구 정의를 제공하고, 모델이 도구 호출 요청을 반환하며, 애플리케이션이 도구를 실행한 뒤 실행 결과를 다시 모델에 전달한다. Function Calling은 모델과 프로그램 사이의 호출 프로토콜이며, 모델이 코드를 직접 실행할 권한을 얻은 것이 아니다. 이 점을 이해해야 이후의 보안 제어, 오류 처리, 상태 관리가 올바른 지점에 자리 잡을 수 있다.
二. 여섯 가지 근본 능력은 어떻게 하나의 폐루프를 구성하는가
Agent와 프로그램 세계의 상호작용 파이프라인 ┌─────────────── 입력 측 ───────────────┐ │ │ │ Prompt │ │ 목표, 컨텍스트, 의사결정 규칙과 경계 규정 │ │ │ │ │ ▼ │ │ Tool Definition + JSON Schema │ │ 어떤 도구가 있는지, 그리고 파라미터 계약 정의 │ └──────────────────┬───────────────────┘ ▼ Function / Tool Call 모델이 구조화된 호출을 제안 │ ▼ ┌────────────── 런타임 경계 ─────────────┐ │ 파싱 → Schema 검증 → 인증 → 실행 │ │ │ │ │ ┌──────────┴──────────┐ │ │ ▼ ▼ │ │ 성공 결과 실패 결과 │ └──────────┬─────────────────────┬──────┘ │ │ ▼ ▼ 구조화 출력 제약 분류 재시도/저하 │ │ └──────────┬──────────┘ ▼ 최종 답변
각 부분은 서로 다른 문제를 해결한다:
능력 해결하는 문제 해결하지 못하는 문제 Prompt 설계 모델이 무엇을 해야 하며 언제 도구를 호출해야 하는가 프로그램 인증과 런타임 검증을 대체할 수 없음 JSON Schema 파라미터에는 무엇이 있고, 타입과 구조는 무엇인가 파라미터가 비즈니스 의미에 부합함을 증명할 수 없음 Function Calling 모델이 기계가 처리할 수 있는 함수 호출 요청을 생성하도록 함 로컬 함수를 자동으로 실행하지 않음 Tool Calling 함수, 내장 도구, MCP 등의 능력을 호출 루프에 포함 도구가 신뢰할 수 있거나 부작용이 없음을 본질적으로 보장하지 않음 출력 제약 최종 출력이 파싱 가능한 구조에 부합하도록 함 필드 내용이 사실 차원에서 올바름을 보장하지 않음 실패 재시도 일시적 장애나 수정 가능한 출력 처리 권한 부족과 영구적 비즈니스 오류는 수정할 수 없음
三. Prompt 설계: 주문(呪文)이 아니라 의사결정 설명서로 다루기
Prompt는 Agent에서 주로 두 가지 일을 담당한다. 작업 목표를 설명하고, 모델이 도구를 어떻게 선택하고 사용할지 규정한다. 도구 호출에 적합한 Prompt는 다음과 같은 구조로 분해할 수 있다:
< role > 너는 주문 애프터서비스 어시스턴트다. </ role > < objective > 사용자가 주문 상태를 조회하거나 환불 신청을 제출하도록 돕는다. </ objective > < tool_policy > 1. 실시간 주문 상태를 조회할 때는 반드시 order_get을 호출해야 한다. 2. 사용자의 명확한 확인을 얻지 못한 경우 refund_create를 호출해서는 안 된다. 3. order_id가 없으면 먼저 사용자에게 물어보고, 추측해서는 안 된다. 4. 도구가 실패하면 오류 코드에 따라 원인을 설명하고, 실행 결과를 지어내서는 안 된다. </ tool_policy > < output_policy > 쓰기 작업을 호출하기 전에 곧 실행할 동작을 먼저 요약한다. 최종 답변은 "실행됨"과 "실행 권장"을 구분해야 한다. </ output_policy > < untrusted_input > {{user_input}} </ untrusted_input >
1. 효과적인 Prompt의 네 가지 구성 요소
- 목표
목표는 인격 형용사를 쌓는 것이 아니라 결과를 서술해야 한다.
더 약한 표현:
너는 전문적이고 지능적이며 엄격한 주문 어시스턴트다.
더 실행 가능한 표현:
너는 주문 상태를 조회하고, 환불 조건을 충족하며 사용자가 명확히 확인한 후에 환불을 제출하는 역할을 맡는다.
- 도구 선택 규칙
여러 도구의 의미가 비슷할 때 모델은 잘못 선택하기 쉽다. Prompt와 도구 설명은 다음을 설명해야 한다: 언제 호출하고 언제 호출하지 않는지, 호출 전에 어떤 정보가 필요한지, 도구가 부작용을 일으키는지, 여러 도구 사이의 경계는 무엇인지.
- 누락 정보 처리
모델에 파라미터가 없으면 추측을 선택할 수 있다. 다음과 같이 명확히 요구해야 한다: 필수 파라미터가 없을 때는 사용자에게 물어보고, 무관한 컨텍스트에서 추론해서는 안 된다.
주의할 점: 이는 추측 확률을 낮출 뿐이며, 프로그램은 여전히 Schema와 비즈니스 검증을 수행해야 한다.
- 실패 행동
"실패하면 재시도하세요"라고만 쓰지 말아야 한다. 모델에게 오류 유형에 따라 어떤 결정을 내려야 하는지 알려주어야 한다:
VALIDATION_ERROR: 파라미터를 수정한 후 최대 한 번 재시도한다. NOT_FOUND: 사용자에게 식별자를 확인하고, 자동으로 재시도하지 않는다. PERMISSION_DENIED: 호출을 중단하고 필요한 권한을 설명한다. RATE_LIMITED: retry_after를 따르고, 대기 후 재시도한다.
2. Prompt 버전은 반드시 평가를 받아들여야 한다
Prompt는 한 번에 다 써서 끝나는 정적인 문안이 아니다. 프로덕션 환경에서는 최소한 다음을 테스트해야 한다:
- 올바른 도구를 호출했는가;
- 필수 파라미터를 빠뜨리지 않았는가;
- 필요하지 않을 때 도구를 호출하지 않는가;
- 쓰기 작업 전에 확인을 받았는가;
- 도구 실패 후 성공 결과를 조작하지 않는가;
- 악의적인 도구 반환값이 시스템 지시를 바꾸지 않는가;
- 모델이나 Prompt 버전 변화가 행동 회귀를 일으키지 않는가.
따라서 Prompt의 신뢰성은 '작성된 것을 보면 완전해 보인다'는 식으로 판단할 것이 아니라, 평가 데이터로 증명해야 한다.
4. JSON Schema: Agent와 프로그램 사이의 데이터 계약
JSON은 단지 데이터 형식일 뿐이고, 데이터 구조와 검증 규칙을 설명하는 것은 JSON Schema의 역할이다.
JSON Schema를 사용하면 다음을 정의할 수 있다:
{ "type" : "object" , "properties" : { "order_id" : { "type" : "string" , "description" : "주문 고유 식별자, 예: ord_20261027_001" } , "amount" : { "type" : "number" , "exclusiveMinimum" : 0 , "description" : "환불 금액, 단위는 currency로 지정" } , "currency" : { "type" : "string" , "enum" : [ "CNY" , "USD" ] } , "reason" : { "type" : "string" , "minLength" : 1 , "maxLength" : 200 } } , "required" : [ "order_id" , "amount" , "currency" , "reason" ] , "additionalProperties" : false }
- properties는 필수를 의미하지 않으며, 반드시 required와 함께 사용해 검증해야 한다.
- additionalProperties: false는 객체 경계를 닫는 데 사용된다. 추가 필드를 금지하지 않으면 모델이 정의되지 않은 파라미터를 생성할 수 있다. 도구 파라미터의 경우 닫힌 객체가 보통 열린 객체보다 더 안전하다.
- enum은 자연어 제약보다 더 안정적이며, 인스턴스 값을 주어진 집합으로 제한하는 데 쓰인다.
Schema 검증과 비즈니스 검증은 반드시 계층화해야 한다:
모델 파라미터 │ ▼ [구문 분석] │ JSON을 파싱할 수 있는가? ▼ [Schema 검증] │ 타입, 필수 항목, 열거, 구조가 유효한가? ▼ [비즈니스 검증] │ 주문이 존재하는가? 금액을 환불할 수 있는가? 상태가 허용되는가? ▼ [권한 검증] │ 현재 사용자가 이 주문을 조작할 수 있는가? ▼ [실행]
Schema는 첫 번째 문이지, 마지막 문이 아니다.
5. Function Calling: 모델이 호출을 제안하고, 프로그램이 실행권을 쥔다
Function Calling의 전형적인 흐름은 다섯 단계로 요약할 수 있다:
( 1 ) 애플리케이션이 도구 정의를 모델에 보낸다 │ ▼ ( 2 ) 모델이 function/tool call을 반환한다 │ ▼ ( 3 ) 애플리케이션이 파라미터를 검증하고 함수를 실행한다 │ ▼ ( 4 ) 애플리케이션이 실행 결과를 call_id에 연결한다 │ ▼ ( 5 ) 모델이 최종 답변을 생성하거나 도구 호출을 계속한다
하나의 도구 핵심 구조는 대략 다음과 같다:
{ "name" : "order_get" , "description" : "현재 사용자가 소유한 단일 주문을 조회한다. 실시간 주문 상태가 필요할 때만 호출한다." , "input_schema" : { "type" : "object" , "properties" : { "order_id" : { "type" : "string" , "description" : "주문 고유 식별자, 상품 번호나 결제 거래 번호를 사용하지 말 것" } } , "required" : [ "order_id" ] , "additionalProperties" : false } }
도구 설명 역시 모델 입력의 일부이며, 도구 용도, 적용 시점, 파라미터 의미와 제한을 상세히 설명할 것을 권장하며, 설명 품질이 도구 호출 효과에 영향을 미치는 중요한 요소임을 지적한다.
합격한 도구 정의는 모델이 알 수 있게 해야 한다: 도구가 무엇을 하는가? 언제 쓰는가? 언제 쓰지 않는가? 각 파라미터는 무엇을 나타내는가? 호출에 어떤 부작용이나 제한이 있는가?
예를 들어 아래 설명은 지나치게 모호하다:
환불을 생성한다.
더 명확한 버전은 다음과 같다:
현재 사용자가 소유하고 환불 가능 상태인 주문에 대해 환불을 생성한다. 사용자가 환불 금액과 사유를 명확히 확인한 후에만 호출한다. 이 도구는 무시할 수 없는 비즈니스 부작용을 일으킨다. 환불 자격을 조회할 때는 호출하지 말고 refund_eligibility_get을 사용해야 한다.
Schema는 파라미터의 형태를 결정하고, 설명은 모델이 도구 의미를 어떻게 이해할지 결정하며, 둘 다 빠뜨릴 수 없다.
6. Tool Calling: Function Calling의 상위 개념
많은 API와 프레임워크에서 Function Calling과 Tool Calling은 혼용된다. 엔지니어링 관점에서는 이렇게 구분할 수 있다:
Tool Calling ├── Function Tool │ └── 애플리케이션이 구현한 로컬 함수 또는 원격 API 래퍼 ├── Built - in Tool │ ├── Web Search │ ├── Code Execution │ └── File Search ├── MCP Tool │ └── MCP Server가 노출하는 외부 능력 └── UI / Computer Tool └── 브라우저, 데스크톱 또는 인터페이스 조작
Function Calling은 Tool Calling의 흔한 한 형태다. 전자는 '함수와 파라미터의 형태로 호출을 표현'하는 것을 강조하고, 후자는 '모델이 외부 능력을 사용할 수 있다'는 것을 강조한다.
도구는 실행 위치에 따라 다음과 같이 분류할 수도 있다:
유형 실행 주체 애플리케이션 책임 클라이언트 도구 비즈니스 애플리케이션 파라미터 검증, 인증, 실행, 결과 반환 서버 도구 모델 서비스 제공자 애플리케이션이 도구를 구성하고, 제공자가 실행을 담당 MCP/원격 도구 MCP 서비스 또는 제3자 서비스 신뢰, 권한 및 결과 격리를 처리해야 함
클라이언트 도구가 tool_use를 반환하면 애플리케이션이 실행한 뒤 연관 ID로 tool_result를 반환한다; 서버 도구는 플랫폼이 실행한다.
Tool Calling은 상태 머신이지, 한 번의 요청이 아니다. 하나의 Agent는 다음과 같이 동작할 수 있다:
- 도구를 호출하지 않고 바로 답변한다;
- 도구를 하나 호출한 뒤 답변한다;
- 여러 개의 독립적인 도구를 병렬로 호출한다;
- 첫 번째 결과에 따라 두 번째 도구를 결정한다;
- 도구가 실패하면 파라미터를 수정한 뒤 다시 호출한다;
- 루프 상한에 도달하면 중지한다.
따라서 Agent Runtime은 유한 상태 머신에 더 가깝다:
┌──────────────┐ │ START │ └──────┬───────┘ ▼ ┌──────────────┐ ┌──── > │ MODEL_CALL │ < ────────────┐ │ └───┬──────┬───┘ │ │ │ │ │ │ final │ │ tool_call │ │ ▼ ▼ │ │ ┌──────┐ ┌──────────────┐ │ │ │ DONE │ │ VALIDATE │ │ │ └──────┘ └──┬────────┬──┘ │ │ │ │ │ │ valid│ │error │ │ ▼ ▼ │ │ ┌─────────┐ ┌───────┐ │ │ │ EXECUTE │ │ REPAIR│─┘ │ └────┬────┘ └───────┘ │ │ │ ┌───────┴────────┐ │ │ │ │ success failure │ │ │ └──────────┴──── result ────┘
런타임은 반드시 다음을 설정해야 한다:
- 최대 모델 라운드 수;
- 최대 도구 호출 수;
- 단일 도구 타임아웃;
- 전체 작업 데드라인;
- 최대 파라미터 수정 횟수;
- 최대 일시적 오류 재시도 횟수;
- 취소 신호.
그렇지 않으면 "완료될 때까지 계속 호출"이 무한 루프와 통제 불능의 청구서로 이어질 수 있다.
七、출력 제약: 합법적 JSON, Schema 적법성, 내용 정확성은 세 가지 다른 일이다
구조화 출력에는 세 가지 신뢰성 계층이 존재한다:
Level 1 : 자연어로 "JSON을 반환하세요"라고 요구 │ ▼ Markdown이나 설명 텍스트, 문법 오류가 붙을 수 있음 Level 2 : JSON Mode 보장 출력이 JSON으로 파싱 가능함 │ ▼ 필드는 여전히 누락되거나, 타입이 틀리거나, 열거형 범위를 벗어날 수 있음 Level 3 : Structured Outputs 출력이 지정된 Schema를 충족함 │ ▼ 필드 내용은 여전히 사실 또는 비즈니스 오류가 있을 수 있음
Function Calling과 Structured Output 모두 JSON Schema를 사용할 수 있는데, 전자는 "모델이 프로그램에게 무엇을 하게 할지"를 제약하고, 후자는 "모델이 최종적으로 프로그램에게 무엇을 말할지"를 제약한다.
출력이 Schema에 완전히 부합하더라도 여전히 틀릴 수 있다. Schema는 타입 형식만 증명할 수 있을 뿐 실제 존재 여부는 증명하지 못하므로, 비즈니스 코드는 "Schema 검증 통과"를 "사실 검증 통과"로 간주해서는 안 된다.
또한 구조화 출력 외에도 제어 신호가 존재한다:
- 안전한 거부 응답;
- 출력이 Token 상한에 도달;
- 요청이 취소됨;
- 콘텐츠 필터링;
- 도구 호출 미완료;
- 스트리밍 출력 중단.
소비자는 다음만 해서는 안 된다:
JSON . parse (response)
그보다 먼저 판단해야 한다:
응답 상태 ├── completed → 파싱 및 검증 ├── refusal → 거부 응답 절차 진입 ├── incomplete → 이어쓰기 또는 재시도 가능 여부 판단 ├── tool_call → 도구 호출 루프 실행 └── transport_error → 네트워크 정책에 따라 처리
八、실패 재시도: 먼저 분류하고, 그다음 재시도 여부를 결정하라
오류 재시도 여부 처리 방식 네트워크 타임아웃 예 지수 백오프, 전체 데드라인에 제약됨 HTTP 429 예 Retry-After를 우선 준수 HTTP 5xx 상황에 따라 상한을 두고 재시도 JSON 파싱 실패 예 파싱 오류를 모델에 피드백, 소량 수정 Schema 검증 실패 예 필드 수준 오류 반환, 소량 수정 파라미터 누락 상황에 따라 사용자로부터 얻을 수 있으면 먼저 사용자에게 질문 리소스 없음 아니요 식별자 확인 또는 종료 권한 부족 아니요 권한 요구 사항 설명 및 감사 비즈니스 규칙 거부 아니요 실행 가능한 대안 반환 쓰기 작업 결과 불명 신중 먼저 상태 조회, 직접 반복 쓰기 금지
쓰기 작업은 반드시 멱등성을 갖추어야 한다. 날씨 읽기가 실패한 뒤 재시도하는 최악의 경우는 대개 조회가 한 번 더 늘어나는 정도다. 환불 생성, 이메일 발송 또는 금액 청구를 재시도하면 중복 부작용이 발생할 수 있다.
쓰기 도구는 멱등 키를 받아들여야 한다:
{ "order_id" : "ord_123" , "amount" : 100 , "currency" : "CNY" , "idempotency_key" : "agent_run_abc:refund:ord_123" }
서버는 멱등 키와 실행 결과를 저장한다:
최초 요청 idempotency _key 존재하지 않음 │ ▼ 쓰기 작업 실행 ──> 결과 저장 ──> 성공 반환 중복 요청 idempotency_ key 이미 존재함 │ ▼ 중복 실행 안 함 ──> 최초 결과 반환
멱등 키는 모델이 자유롭게 생성하게 해서는 안 되고, 신뢰할 수 있는 애플리케이션이 생성해야 한다. 그렇지 않으면 모델이 재시도할 때마다 새 키를 만들어 중복 제거 기능을 잃을 수 있다.
또한 "결과 불명"은 "명확한 실패"보다 더 위험하다. 환불 API가 실행 후 연결 타임아웃이 발생했다고 가정해 보자:
Agent ──환불 생성──> 환불 서비스 Agent <──연결 타임아웃──X
이때 두 가지 가능성이 존재한다: 서버가 환불을 실행하지 않았거나, 서버가 환불을 실행했지만 응답이 유실된 경우다. 즉시 재시도하면 환불이 중복될 수 있다. 올바른 절차는 다음과 같다:
쓰기 작업 타임아웃 │ ▼ 멱등 키 또는 비즈니스 트랜잭션 번호로 상태 조회 │ ├── 이미 성공 → 원래 결과 반환 ├── 명확한 실패 → 재시도 여부 평가 └── 상태 불명 → 수동 개입 또는 비동기 대사
따라서 Agent Runtime은 FAILED와 UNKNOWN을 구분해야 한다. 둘 다 예외로 매핑하면 가장 까다로운 일관성 문제 유형을 가려버리게 된다.
九、실제로 구현 가능한 Agent 도구 호출 루프
type ToolError = { code: string ; message: string ; retryable: boolean ; }; async function runAgent(userInput: string ) { const messages = [ { role: "user" , content: userInput } ]; const maxTurns = 8 ; for ( let turn = 0 ; turn < maxTurns; turn++) { const response = await callModel({ messages, tools: toolDefinitions }); if (response.type === "final" ) { return validateFinalOutput(response.output); } if (response.type !== "tool_calls" ) { throw new Error (`Unsupported response type: ${response.type}`); } messages.push(response.assistantMessage); for ( const call of response.calls) { const tool = toolRegistry. get ( call .name); if (!tool) { messages.push(toolErrorResult( call .id, { code: "UNKNOWN_TOOL" , message: `Tool ${ call .name} is not available`, retryable: false })); continue ; } const validation = tool.schema.safeParse( call .arguments); if (!validation.success) { messages.push(toolErrorResult( call .id, { code: "VALIDATION_ERROR" , message: formatValidationError(validation. error ), retryable: true })); continue ; } const authorization = await authorize( currentUser, tool, validation.data ); if (!authorization.allowed) { messages.push(toolErrorResult( call .id, { code: "PERMISSION_DENIED" , message: authorization.reason, retryable: false })); continue ; } try { const result = await executeWithPolicy(tool, validation.data, { timeoutMs: 5000 , idempotencyKey: buildIdempotencyKey( call , currentUser) }); messages.push(toolSuccessResult( call .id, result)); } catch ( error ) { messages.push( toolErrorResult( call .id, classifyToolError( error )) ); } } } throw new Error ( "Agent exceeded maximum turns" ); }
- 모델은 호출을 제안하는 역할만 담당한다.
- 도구 이름은 반드시 허용 목록(allow list)과 일치해야 한다.
- 파라미터는 반드시 로컬 Schema 검증을 거쳐야 한다.
- 인증·권한 확인은 도구 실행 전에 이루어진다.
- 쓰기 작업에는 애플리케이션이 멱등 키를 주입한다.
- 오류는 분류된 후에 모델에 피드백된다.
- 루프에는 명확한 상한이 존재한다.
- 최종 출력 역시 독립적으로 검증되어야 한다.
10. 소결
Agent가 프로그램 세계와 연결되는 과정은 본질적으로 확률적 시스템과 결정적 시스템을 가로지르는 하나의 프로토콜이다.
그 안에서:
- Prompt는 목표와 의사결정 규칙을 표현한다.
- JSON Schema는 파라미터와 출력의 데이터 경계를 정의한다.
- Function Calling은 모델이 구조화된 호출 요청을 제기하게 한다.
- Tool Calling은 외부 역량을 다중 턴 실행 루프에 편입시킨다.
- Structured Outputs는 최종 결과를 프로그램이 안정적으로 소비할 수 있게 한다.
- 검증, 권한 확인, 멱등성, 오류 분류, 제한적 재시도가 함께 런타임 경계를 지킨다.
프로덕션급 Agent는 모델이 "스스로 알아서 올바르게 해주는" 것에 의존해서는 안 된다. 애플리케이션이 실행권을 반드시 쥐고, 코드로 모델의 요청을 검사하며, 최소 권한으로 도구를 제약하고, 구조화된 오류로 모델이 복구하도록 돕고, 실행 결과를 확인할 수 없을 때에는 자동 동작을 중지해야 한다.