포스트

도구를 “호출”이 아니라 “실행”하게 만들기: AI Agent Tool Use & Function Calling 구현 심층 가이드

2026년의 AI Agent는 “대화 잘하는 모델”이 아니라 도구(tool)를 안전하게 실행하는 런타임에 가깝습니다. 실무에서 부딪히는 문제는 늘 비슷합니다.

도구를 “호출”이 아니라 “실행”하게 만들기: AI Agent Tool Use & Function Calling 구현 심층 가이드

들어가며

2026년의 AI Agent는 “대화 잘하는 모델”이 아니라 도구(tool)를 안전하게 실행하는 런타임에 가깝습니다. 실무에서 부딪히는 문제는 늘 비슷합니다.

  • 모델이 툴을 잘못 고르거나(wrong tool)
  • 인자를 잘못 구성하거나(bad args)
  • 네트워크/타임아웃으로 재시도 중 중복 실행을 만들거나(duplicate side-effect)
  • 멀티턴에서 call_id 연결이 끊겨 디버깅이 불가능해지는 문제

2026년 8월 기준, OpenAI 쪽은 Responses API를 중심으로 “tool-calling / multi-turn / programmatic tool calling”까지 하나의 표준 흐름으로 묶었고, Agents SDK는 이를 “Runner + Tool + Guardrails + HITL”로 감싸 실행 제어를 제공합니다. 특히 “program items / program-issued function calls / call_id & caller linkage” 같은 디테일은 이제 선택이 아니라 운영 필수에 가깝습니다.1

언제 쓰면 좋나

  • 외부 API/DB/사내 시스템을 반복적으로 호출하며 멀티스텝 작업을 해야 할 때
  • “툴 실행”이 실패했을 때 복구 가능한 루프(recoverable loop) 가 필요할 때
  • 승인(approval), 입력 검증(guardrails), allowlist 등 거버넌스가 필요한 프로덕션

언제 쓰면 안 되나

  • 단발성 Q&A/요약처럼 툴 호출이 본질이 아닌 작업
  • side-effect(결제/발송/삭제)가 큰데도 idempotency/승인/감사로그 없이 “에이전트가 알아서” 하게 만들려는 경우(운영 사고로 갑니다)
  • 10개 이상 분기/조건을 그래프로 억지로 표현하려는 경우: 결국 “LLM 붙은 state machine”이 되고, 변경 비용만 올라갑니다(이건 LangGraph류든 직접 구현이든 동일)

🔧 핵심 개념

1) Function Calling = “구조화된 실행 계약(Contract)”

Function Calling은 “모델이 JSON을 예쁘게 뱉는다”가 아니라,

  • 툴 이름 선택
  • 스키마 기반 인자 생성
  • 실행 결과를 다시 컨텍스트로 주입
  • 그 후속 추론 을 하나의 계약으로 만들고, 애플리케이션은 이 계약을 검증/실행/감사하는 구조입니다.

OpenAI Agents SDK는 Python 함수의 signature/docstring에서 툴 스키마를 자동 생성하고, 실행은 Runner가 맡습니다.2

2) 2026년의 관전 포인트: “Programmatic Tool Calling(PTC)”

최근 흐름은 “JSON tool call”을 반복하는 대신, 모델이 program(코드) 형태로 여러 툴 호출을 조합하는 패턴이 확산 중입니다. OpenAI 문서도 PTC를 별도 도구로 소개하며, 앱은 program/program_outputcall_id, caller 링크를 보존해야 한다고 명시합니다.1
즉, 이제는 “툴 1회 호출”이 아니라 툴 호출 그래프를 런타임이 추적하는 시대입니다.

3) 실패를 전제로 한 실행 모델: “Recoverable loop”

프로덕션에서는 모델이 존재하지 않는 툴을 부르거나(오타/환각), 런타임의 allowlist 밖 툴을 부를 수 있습니다. Agents SDK는 기본적으로 이런 경우 ModelBehaviorError로 터뜨리지만, 옵션으로 에러를 모델에게 반환하고 다른 툴/답변으로 회복시키는 모드를 제공합니다.3
이게 중요한 이유는 “툴 실패 = 사용자 실패”가 아니라, 툴 실패를 포함한 플로우 설계가 가능해지기 때문입니다.

4) 다른 접근과의 차이점(Framework vs Pattern)

  • Agents SDK(OpenAI): Runner가 turn/tool/call_id/승인/에러 복구를 “표준 루프”로 제공
  • LangGraph류: 루프를 명시적 그래프로 드러내 제어는 쉬우나, 결국 중요한 건 툴 경계의 규칙(idempotency/approval/audit) 을 얼마나 잘 구현하느냐입니다(프레임워크는 부차적).4
  • Anthropic tool-use: “tool을 텍스트 파싱이 아니라 계약으로” 보는 철학이 강하고, tool boundary에서 결정 구조화를 권장합니다.5

💻 실전 코드

시나리오: “사내 주문 처리 에이전트”

  • 고객이 “주문 상태 확인 + 필요 시 주소 변경 요청”을 합니다.
  • 주소 변경은 side-effect가 있으므로 HITL(승인) + idempotency 가 필요합니다.
  • 존재하지 않는 툴 호출/인자 오류가 나면 모델에게 에러를 반환해 회복합니다.

아래 코드는 “toy”가 아니라, 실무에서 바로 쓰는 형태(HTTP 호출, idempotency key, 승인 게이트, 에러 복구, 예상 출력)를 담았습니다.

0) 설치/환경

1
2
3
4
5
python -m venv .venv
source .venv/bin/activate

pip install "openai-agents>=0.14.0" httpx pydantic
export OPENAI_API_KEY="..."

(Agents SDK 업데이트/카테고리 및 Runner 동작은 공식 문서 기준)2

1) 기본 셋업: Function tools + 승인(approval) + idempotency

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import os
import uuid
import httpx
from pydantic import BaseModel, Field

from agents import Agent, Runner, RunConfig
from agents.tools import function_tool

ORDER_API_BASE = "https://example.internal.api"  # 사내 API라고 가정

class OrderStatus(BaseModel):
    order_id: str
    status: str
    eta_days: int | None = None

class AddressChangeResult(BaseModel):
    order_id: str
    accepted: bool
    message: str

@function_tool
def get_order_status(order_id: str) -> dict:
    """
    Fetch order status from internal Order API.

    Use this when the user asks about delivery status, ETA, or current order state.
    """
    with httpx.Client(timeout=5.0) as client:
        r = client.get(f"{ORDER_API_BASE}/orders/{order_id}")
        r.raise_for_status()
        data = r.json()

    parsed = OrderStatus(**data)
    return parsed.model_dump()

@function_tool(needs_approval=True)
def request_address_change(order_id: str, new_address: str) -> dict:
    """
    Request address change for an existing order (side-effect).

    Only call this after you have:
    1) confirmed the order_id,
    2) confirmed the full new_address,
    3) explained that a human approval is required.
    """
    # idempotency key: 재시도/중복 실행 방지의 최소 단위
    idem_key = f"addrchg:{order_id}:{uuid.uuid4().hex}"

    with httpx.Client(timeout=8.0) as client:
        r = client.post(
            f"{ORDER_API_BASE}/orders/{order_id}/address-change",
            json={"new_address": new_address},
            headers={"Idempotency-Key": idem_key},
        )
        r.raise_for_status()
        data = r.json()

    parsed = AddressChangeResult(**data)
    return parsed.model_dump()

agent = Agent(
    name="OrderOpsAgent",
    instructions=(
        "You are an operations agent. Prefer tools over guessing.\n"
        "For address changes, always summarize the change and wait for approval.\n"
        "If a tool call fails, recover: ask for missing fields or try an alternative.\n"
    ),
    tools=[get_order_status, request_address_change],
)

async def main():
    user_msg = (
        "주문번호 A-10291 상태 알려주고, 배송지 주소를 "
        "'서울시 강남구 테헤란로 123, 10층'으로 바꿔줘"
    )

    result = await Runner.run(
        agent,
        user_msg,
        run_config=RunConfig(
            tool_not_found_behavior="return_error_to_model",  # 회복 루프
        ),
    )

    # 승인 필요한 경우: interruptions에 쌓이고 run이 pause됨
    if result.interruptions:
        print("=== PENDING APPROVAL ===")
        for it in result.interruptions:
            print(it)

        # 실제 서비스라면: 관리자 UI에서 approve/reject
        state = result.to_state()
        # 데모: 승인한다고 가정
        state.approve_all()

        result = await Runner.resume(state)

    print("=== FINAL ===")
    print(result.final_output)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

2) 예상 출력(형태)

  • 첫 run에서 request_address_change가 pending approval로 멈추고,
  • approve 후 resume하면 최종 답변이 완료됩니다.
1
2
3
4
5
6
=== PENDING APPROVAL ===
<Interruption tool=request_address_change call_id=... reason=needs_approval ...>

=== FINAL ===
주문 A-10291은 현재 "배송 준비중"이며 예상 2일 내 출고 예정입니다.
배송지 변경 요청: "서울시 강남구 테헤란로 123, 10층"으로 변경을 접수했고 승인 완료 후 반영됩니다.

여기서 핵심은 “승인 기능이 있다”가 아니라,

  • 툴 실행이 멈출 수 있고(pause)
  • 그 상태를 직렬화해(UI/queue로) 넘겼다가
  • 같은 call_id 흐름으로 재개(resume) 할 수 있다는 점입니다. (HITL pause/resume는 SDK가 가이드로 제공)2

3) 확장: Programmatic Tool Calling을 고려해야 하는 순간

  • “상태 조회 → 정책 확인 → 변경 요청 → 변경 검증” 같은 체인이 길어지면, JSON tool-call 왕복이 많아져 토큰/지연이 증가합니다.
  • 이때 PTC는 모델이 program으로 여러 툴 호출을 묶어 실행하는 방향을 제공합니다(대신 런타임은 program/program_outputcaller 링크 보존이 필요).1

실무 판단 기준:

  • 단순 1~2회 툴 호출이면 일반 function calling이 운영하기 쉽고,
  • 5회 이상 연쇄 호출 + 중간 산출물 크기가 크면 PTC를 PoC로 비교해볼 가치가 큽니다(토큰/턴 수/지연/성공률로 벤치마크).1

⚡ 실전 팁 & 함정

Best Practice (바로 효과 나는 3가지)

1) Tool boundary에 idempotency를 넣어라

  • 에이전트 재시도는 “가끔”이 아니라 “기본값”입니다(네트워크 타임아웃, 모델 재호출, 스트리밍 중단).
  • 결제/발송/삭제 계열은 idempotency 없으면 언젠가 중복 실행이 납니다.

2) tool_not_found / tool_error를 “모델이 이해 가능한 에러”로 반환

  • 기본으로 죽이면 운영자가 새벽에 온콜합니다.
  • 회복 모드(tool_not_found_behavior="return_error_to_model") + 에러 메시지 포맷팅으로 “다른 툴을 쓰거나 사용자에게 추가 질문”으로 수습하게 만드세요.3

3) Approval(승인)을 ‘툴 단위’로 걸어라

  • “최종 답변 전에 승인”은 이미 side-effect가 실행된 뒤일 수 있습니다.
  • SDK는 needs_approval툴 호출 자체를 pause 시키는 패턴을 제공합니다.2

흔한 함정/안티패턴

  • (안티패턴) 툴 설명(docstring)이 빈약해서 모델이 “언제 어떤 툴을 써야 하는지”를 못 배움
    → 결과적으로 프롬프트에 if-else 규칙이 늘고, 유지보수 비용이 폭증합니다.
  • (함정) “툴 호출 결과”를 그대로 컨텍스트에 던져서 토큰 폭발
    → 툴 응답은 요약/필터링해서 반환(특히 로그/HTML/대용량 JSON).
  • (함정) 멀티턴에서 call_id 상관관계를 저장하지 않음
    → 장애 분석 시 “무슨 툴이 언제 왜 호출됐는지”를 재현 못 합니다. (특히 PTC는 caller 링크까지 중요)1

비용/성능/안정성 트레이드오프

  • Reasoning.effort를 높이면 성공률이 오를 수 있지만 지연/비용도 같이 오릅니다. 기본은 medium에서 시작해 워크로드별로 계측해서 내리는 게 2026년 OpenAI 가이드의 톤입니다.1
  • PTC는 토큰/턴을 줄일 잠재력이 있지만, 런타임 관측/보안 정책(allowlist, sandbox, 감사로그)까지 같이 성숙해야 “이득”이 됩니다.1

🚀 마무리

정리하면, 2026년 8월의 “Agent tool use 구현”에서 중요한 건 프레임워크가 아니라 툴 경계의 설계입니다.

  • 단순 자동화: Responses API + 기본 function calling으로 충분
  • 운영 자동화/업무 시스템 연결: Agents SDK의 Runner/승인/에러복구로 “실행 제어”를 확보
  • 연쇄 호출이 길고 비용이 부담: PTC를 벤치마크로 검토(단, call_id/caller 추적과 정책 집행이 가능한 조직/런타임일 때)

다음 학습 추천:

  • OpenAI Agents SDK의 Tools/Running agents 문서에서 approval pause/resume, tool_not_found recovery, tool error formatting을 먼저 마스터하세요.2
  • 그 다음 “programmatic tool calling”을 실제 업무 플로우(5+ 툴 체인)에서 토큰/지연/성공률로 비교 테스트하는 게 가장 빠른 실무 루트입니다.1
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.