2026년 4월 기준, AI Agent의 “Tool Use + Function Calling” 구현 패턴: 신뢰성/보안/확장성까지 한 번에 잡는 법
들어가며
2026년 들어 “에이전트가 일을 끝낸다”는 말은 더 이상 데모용 슬로건이 아닙니다. 실제 프로덕션에서는 (1) 도구 호출이 흔들리지 않아야 하고, (2) 장기 실행·병렬 처리·격리 실행이 가능해야 하며, (3) Prompt Injection과 과도한 권한 문제를 제어해야 합니다.
이 흐름에서 중요한 변화가 두 가지입니다.
- OpenAI는 Responses API를 에이전트의 기본 프리미티브로 제시하며(웹 search/file search/computer use 등 내장 도구 결합), Assistants API는 중장기적으로 정리 방향을 시사했습니다. (openai.com)
- 2026년 4월 15일 OpenAI가 Agents SDK의 “다음 진화”로 model-native harness + native sandbox execution을 강조했습니다. 즉, 도구 호출을 “프롬프트 요령”이 아니라 표준 실행 인프라로 끌어올린 겁니다. (openai.com)
또한 Google Gemini는 2026년 3월 “built-in tools(예: Search/Maps) + custom function calling을 단일 요청에서 결합”하고, tool-call 사이에 context circulation을 지원한다고 발표했습니다. 오케스트레이션 병목을 줄이는 방향이죠. (blog.google)
즉 2026년 4월의 핵심은: 함수를 ‘콜백’으로 붙이는 수준을 넘어, 에이전트 런타임(격리/추적/반복/정책)을 함께 설계하는 것입니다.
🔧 핵심 개념
1) Tool use / Function Calling의 본질
- Function Calling: 모델이 “텍스트로 답”하는 대신, 개발자가 정의한 JSON schema 기반의 tool call을 생성해 실행을 위임하는 패턴.
- Tool use loop:
1) 모델이 tool call 생성 → 2) 애플리케이션이 실제 함수 실행 → 3) 결과를 모델에 다시 전달 → 4) 모델이 다음 tool call 또는 최종 답 생성
이 루프가 안정적으로 돌아가야 “에이전트”가 됩니다. OpenAI는 Responses API가 단일 호출 내에서 다중 도구·다중 턴을 처리하는 방향을 강조합니다. (openai.com)
2) 2026년형 구현에서 달라진 포인트 3가지
표준 실행 하네스(harness)
“도구를 어떻게 호출하라고 프롬프트에 쓰는가”보다, 어떻게 실행을 표준화(state, retry, tracing, isolation)하느냐가 더 중요해졌습니다. OpenAI는 model-native harness가 복잡 작업에서 신뢰성과 성능을 높인다고 설명합니다. (openai.com)Sandbox / 격리 실행
도구 호출은 곧 “부수효과(side-effect)”입니다. 파일 조작, 네트워크 요청, 결제/삭제 같은 파괴적 작업은 특히 위험합니다. Agents SDK는 sandbox를 필요할 때만 호출, 여러 sandbox를 병렬로 쓰거나 subagent를 격리 라우팅할 수 있다고 밝힙니다. (openai.com)“도구를 많이 주면 더 똑똑해진다”의 함정 현실은 반대인 경우가 많습니다. 도구가 수십/수백 개로 늘면 모델은 선택을 헷갈리고, 잘못된 호출·불필요 호출이 늘어납니다. 그래서 2026년 패턴은 “툴셋을 작게 유지 + 라우팅/권한/리스크 태깅” 같은 운영 설계가 핵심이 됩니다(커뮤니티에서도 같은 문제를 강하게 지적). (reddit.com)
3) Tool definition이 곧 “시스템 프롬프트”
Anthropic 문서가 좋은 힌트를 줍니다. tools를 넘기면 플랫폼이 툴 정의로부터 특별한 system prompt를 구성해 모델에게 도구 사용 환경을 주입합니다. 즉, 툴 스키마/설명은 단순 문서가 아니라 모델 행동을 규정하는 정책 코드입니다. (docs.anthropic.com)
이 관점이 서면, “좋은 스키마”는 아래를 포함합니다:
- 최소 파라미터(필요한 것만), 강한 타입, 제한(enum/format)
- side-effect 여부 명시(“dry_run” 지원 등)
- 실패/재시도 가능성 고려(에러 코드/메시지 표준화)
💻 실전 코드
아래 예제는 “에이전트가 작업을 계획하고 → 필요한 도구만 호출 → 결과를 반영해 최종 답”을 만드는 가장 보편적인 루프 패턴입니다.
(OpenAI Agents SDK를 바로 쓰는 대신, 어떤 런타임에서도 통하는 핵심 오케스트레이션 구조를 Python으로 구현합니다.)
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import json
import time
from typing import Any, Dict, List, Optional
import requests
# -----------------------------
# 1) 우리가 제공할 "도구(tool)"들
# -----------------------------
def http_get_json(url: str, timeout_s: int = 10) -> Dict[str, Any]:
"""Side-effect 없는 읽기 전용 도구. 에이전트에게 가장 먼저 주기 좋은 타입."""
r = requests.get(url, timeout=timeout_s)
r.raise_for_status()
return {"status_code": r.status_code, "json": r.json()}
def create_ticket(title: str, description: str, dry_run: bool = True) -> Dict[str, Any]:
"""
Side-effect 가능 도구 예시.
실무 팁: destructive tool에는 dry_run을 기본 True로 두고,
최종 커밋은 별도 확인 단계를 거치게 설계한다.
"""
if dry_run:
return {"dry_run": True, "ticket_id": None, "message": "validated"}
# 실제로는 Jira/Linear API 호출
return {"dry_run": False, "ticket_id": "TCK-1234", "message": "created"}
TOOLS = {
"http_get_json": http_get_json,
"create_ticket": create_ticket,
}
# -----------------------------
# 2) 모델에 전달할 tool schema (JSON schema 기반)
# - 핵심: 모델이 "어떤 입력이 유효한지" 학습하게 만드는 것
# -----------------------------
TOOL_SCHEMAS = [
{
"type": "function",
"name": "http_get_json",
"description": "Fetch JSON from a URL (read-only). Use for retrieving data, never for side effects.",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "https URL"},
"timeout_s": {"type": "integer", "minimum": 1, "maximum": 30}
},
"required": ["url"]
},
},
{
"type": "function",
"name": "create_ticket",
"description": "Create a work ticket. Potentially side-effecting; default to dry_run=true unless explicitly confirmed.",
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string"},
"description": {"type": "string"},
"dry_run": {"type": "boolean", "default": True}
},
"required": ["title", "description"]
},
}
]
# -----------------------------
# 3) 에이전트 루프 (tool call -> execute -> feed result -> repeat)
# -----------------------------
OPENAI_API_KEY = "YOUR_KEY"
MODEL = "gpt-4.1-mini" # 예시: 실제로는 조직 표준 모델/스냅샷 사용 권장
def call_model(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
OpenAI Responses API 스타일로 구현한다고 가정한 의사 예제.
실제 API 호출은 사용하는 SDK/버전에 맞춰 수정하세요.
"""
# NOTE: 아래는 "구조"에 집중하기 위한 예시입니다.
# 실제로는 openai SDK의 responses.create(...) 등을 사용.
payload = {
"model": MODEL,
"input": messages,
"tools": TOOL_SCHEMAS,
}
r = requests.post(
"https://api.openai.com/v1/responses",
headers={"Authorization": f"Bearer {OPENAI_API_KEY}"},
json=payload,
timeout=30,
)
r.raise_for_status()
return r.json()
def run_agent(user_request: str, max_steps: int = 8) -> str:
messages: List[Dict[str, Any]] = [
{
"role": "system",
"content": (
"You are a senior engineering agent.\n"
"- Prefer read-only tools first.\n"
"- Never perform side effects unless explicitly confirmed.\n"
"- If required params are missing, ask a clarification question.\n"
),
},
{"role": "user", "content": user_request},
]
for step in range(max_steps):
resp = call_model(messages)
# 응답에서 tool call들을 찾는다(플랫폼/SDK에 따라 필드명은 달라질 수 있음).
tool_calls = resp.get("output", [])
final_text_parts = []
for item in tool_calls:
if item.get("type") == "message":
# 모델이 텍스트를 바로 주는 경우
final_text_parts.append(item.get("content", ""))
elif item.get("type") == "tool_call":
tool_name = item["name"]
args = item.get("arguments", {}) # dict라고 가정
tool_id = item.get("id")
if tool_name not in TOOLS:
# 보안: 등록되지 않은 도구는 실행 금지
raise RuntimeError(f"Unknown tool requested: {tool_name}")
# 실행
try:
result = TOOLS[tool_name](**args)
except Exception as e:
result = {"error": str(e)}
# tool result를 모델에 다시 전달
messages.append({
"role": "tool",
"tool_call_id": tool_id,
"content": json.dumps(result, ensure_ascii=False),
})
# 최종 텍스트가 충분히 생성되면 종료(현업에서는 종료 조건을 더 엄격히)
if final_text_parts:
return "\n".join(final_text_parts).strip()
# backoff (장기 실행/레이트리밋 대비)
time.sleep(0.2)
return "작업이 max_steps 내에 종료되지 않았습니다. 요청을 더 쪼개거나 도구 범위를 줄이세요."
if __name__ == "__main__":
print(run_agent("이 URL의 JSON을 읽고 핵심 필드만 요약해줘: https://api.example.com/status"))
⚡ 실전 팁
1) Tool schema는 ‘문서’가 아니라 ‘제약 조건’이다
설명(description)만 길게 쓰는 것보다, enum, minimum/maximum, required를 촘촘히 두는 편이 호출 안정성을 올립니다. Anthropic도 tool 정의로 시스템 프롬프트가 구성된다고 명시합니다. (docs.anthropic.com)
2) Side-effect tool은 “2단계 커밋(Validate → Commit)”으로 쪼개라
create_ticket(..., dry_run=true)로 먼저 유효성 검증/미리보기- 사용자 확인 후
dry_run=false실행
이 패턴은 Prompt Injection/오작동으로 인한 사고를 크게 줄입니다.
3) 툴이 많아지면 ‘라우터(Planner/Router) 에이전트’를 분리
도구 200개를 한 번에 쥐여주면 선택 품질이 떨어집니다. 툴셋을 업무별로 나누고(“읽기 전용”, “운영”, “결제/삭제”), 첫 단계에서 라우팅만 수행하는 얇은 에이전트를 두세요. “툴 과다” 문제는 현장에서 반복적으로 보고됩니다. (reddit.com)
4) 오케스트레이션 병목을 줄이는 방향을 따라가라 Google은 built-in tools와 custom function calling을 단일 요청에서 결합하고, tool call 사이의 context circulation을 이야기합니다. 규모가 커질수록 “프롬프트 튜닝”보다 “런타임 단순화”가 이깁니다. (blog.google)
5) 격리 실행(sandbox) + tracing을 기본값으로 OpenAI Agents SDK는 sandbox 실행과 병렬화, subagent 격리 라우팅을 강조합니다. “도구 호출이 틀렸다”를 디버깅하려면 어떤 입력으로 어떤 tool을 왜 호출했는지 추적 가능해야 합니다. (openai.com)
🚀 마무리
2026년 4월의 AI Agent 구현에서 Function Calling은 “편의 기능”이 아니라 신뢰 가능한 실행 모델의 일부가 됐습니다. 핵심은 세 가지로 정리됩니다.
- 제약이 강한 tool schema + 반복 실행 루프로 안정성을 만들고
- side-effect를 분리(dry_run/confirm) + sandbox 격리로 안전을 확보하며 (openai.com)
- 툴셋을 작게 유지하고 라우팅/오케스트레이션을 표준화해 확장성을 얻는다 (blog.google)
다음 학습으로는 (1) Responses API 기반의 내장 도구 조합, (2) Agents SDK의 harness/sandbox 개념을 실제 CI/CD·백오피스 작업에 붙이는 방법, (3) “툴 권한/리스크 모델링”까지 이어가면 프로덕션에서 사고를 확 줄일 수 있습니다.