2026년 5월 기준: AI Agent “Tool Use + Function Calling”을 프로덕션에 넣는 구현 패턴 (Responses API · Agents SDK · MCP)
들어가며
2026년의 agent 개발에서 가장 자주 터지는 문제는 “모델이 똑똑하냐”가 아니라 도구 호출(tool call)이 프로덕션 규칙을 깨는 순간입니다. 예를 들어:
- DB 조회를 해야 하는데 바로 “요약”부터 해버림 (순서 문제)
- 결제/예약 같은 부작용(side effect) 툴을 너무 일찍 호출함 (안전성 문제)
- 툴 출력이 길어져 컨텍스트를 잡아먹고 비용이 폭발 (비용/성능 문제)
- 에러가 나면 모델이 “추측으로 메꾸기”를 함 (신뢰성 문제)
2026년 5월 기준 OpenAI 쪽은 Responses API + Agents SDK를 장기 방향으로 밀고 있고, tool use는 JSON Schema 기반 function calling(Structured Outputs)과 MCP(Model Context Protocol) 기반 “표준화된 도구 연결”로 정리되는 흐름입니다. (help.openai.com) 또한 multi-step agent의 왕복 호출로 생기는 지연을 줄이기 위해 WebSocket 기반 실행 모드도 공개적으로 강조되고요. (openai.com)
언제 쓰면 좋은가
- “검색/조회 → 정합성 검증 → 작성/실행”처럼 여러 시스템을 순차적으로 호출해야 하는 워크플로우
- 사람이 매번 클릭/복붙하기엔 비싼 운영 업무(리서치, 티켓 분류, 배포 전 체크리스트 등)
- 호출 이력/근거가 남아야 하는 업무(트레이싱, 감사 로그)
언제 쓰면 안 좋은가
- 단일 API 한 번 호출로 끝나는 작업(굳이 agent loop를 만들면 오히려 불안정/비용 증가)
- “즉시성”이 핵심인데(예: 음성 인터랙션) tool 호출이 3~5단계 이상 필요한 경우: 설계부터 latency budget이 맞는지 먼저 계산해야 합니다. (이 경우 WebSocket/비동기/추측 호출 같은 최적화가 전제) (infoq.com)
- 부작용 툴(결제/메일 발송/권한 변경)을 모델에게 무제한 위임해야만 성립하는 제품
🔧 핵심 개념
1) Function Calling의 “2026년형” 정의
- 모델 출력이 텍스트가 아니라 “행동”으로 확장되는 인터페이스
- 핵심은 “도구 목록(tools) + JSON Schema”로 모델이
function_call을 만들고, 런타임이 실행한 뒤function_call_output을 다시 모델에 피드백하는 폐루프(loop) 입니다. - OpenAI Help Center는
strict: true를 통한 Structured Outputs(스키마 일치 강제)를 강조합니다. 즉, “대충 JSON 비슷하게”가 아니라 계약 기반 호출로 가는 방향입니다. (help.openai.com)
2) 내부 작동 흐름(중요: “메시지”가 아니라 “아이템”)
Responses API 계열에서 실제로는 다음과 같은 이벤트/아이템 흐름을 갖는 것이 포인트입니다:
- 모델이 생각/출력 중
function_call(또는 hosted tool call)을 생성 - 클라이언트(또는 Agents SDK Runner)가 해당 tool을 실행
- 실행 결과를
function_call_output으로 다시 넣음 - 모델이 다음 step 결정(추가 tool call or 최종 답)
이 “아이템 스트림” 구조가 로그/관측/재현성에 유리합니다(어떤 단계에서 무슨 도구를 왜 호출했는지). (aiwiki.ai)
3) MCP가 끼어들면서 달라진 점: “도구 연결”의 표준화
2026년에는 function calling이 단순히 “내가 만든 함수 몇 개”가 아니라, 원격 MCP 서버가 제공하는 도구들까지 한 덩어리로 붙습니다.
- MCP는 “도구와 컨텍스트를 LLM에 제공하는 방법”을 표준화하고, OpenAI Agents SDK 문서에서도 MCP 도구 연결을 1급 시민으로 다룹니다. (openai.github.io)
- 다만 MCP 확산과 함께 보안 이슈(RCE 등) 도 같이 뉴스로 올라왔습니다. 즉 “표준화 = 안전”이 아니라, 실행 경계/권한/승인/격리를 제품이 책임져야 합니다. (tomshardware.com)
4) 다른 접근과의 차이점: “루프 기반 tool calling” vs “워크플로우/그래프”
- 가벼운 제품은 여전히 while-loop + tool dispatcher가 제일 디버깅하기 쉽습니다.
- LangGraph 같은 그래프 기반은 “병렬/상태/재시도/분기”가 복잡해질 때 값이 커집니다. (그 전엔 오히려 보일러플레이트가 늘 수 있음) (pyinns.com)
💻 실전 코드
현실적인 시나리오: B2B SaaS 운영팀용 “장애 요약 + 영향도 + 액션 아이템” 에이전트
- 입력: Slack/알림에서 온 incident 텍스트
- 도구:
1)
search_incidents(최근 유사 장애 조회: 내부 Elasticsearch/DB) 2)fetch_runbook(서비스별 Runbook 문서 조회) 3)create_ticket(Jira/Linear 생성 — side effect) - 요구사항:
- 티켓 생성은 승인 게이트를 통과해야만 실행
- tool args는 strict schema로 강제
- 모든 tool call은 tracing/logging
아래 예시는 “Agents SDK를 쓰지 않고도” 이해 가능한 형태로, Responses API 스타일의 tool loop를 TypeScript로 구현한 뒤, 마지막에 “승인 게이트”를 붙입니다. (실서비스에선 여기서 MCP 도구로도 교체 가능)
0) 의존성/환경
npm i openai zod
export OPENAI_API_KEY="..."
1) Tool 스키마(엄격한 계약) + Dispatcher
import OpenAI from "openai";
import { z } from "zod";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
/** --- Tool schemas (strict JSON schema에 대응하기 쉽게 Zod로 검증) --- */
const SearchIncidentsArgs = z.object({
query: z.string().min(5),
timeRangeHours: z.number().int().min(1).max(720).default(168),
});
const FetchRunbookArgs = z.object({
service: z.string().min(2),
});
const CreateTicketArgs = z.object({
projectKey: z.string().min(2),
title: z.string().min(10),
severity: z.enum(["SEV1", "SEV2", "SEV3"]),
descriptionMarkdown: z.string().min(50),
tags: z.array(z.string()).max(10).default([]),
});
/** --- 실제 프로덕션에서는 DB/ES/Jira SDK 연결 --- */
async function searchIncidents(args: z.infer<typeof SearchIncidentsArgs>) {
// 예: ES 쿼리 후 top-k 요약해서 반환(원문 로그 전부를 LLM에 주지 말 것!)
return {
hits: [
{ id: "INC-18421", when: "2026-05-12T03:11:00Z", summary: "auth token rotation 이후 401 spike" },
{ id: "INC-17702", when: "2026-04-29T18:40:00Z", summary: "redis eviction으로 session drop" },
],
note: "요약만 제공(원문 로그는 별도 링크).",
};
}
async function fetchRunbook(args: z.infer<typeof FetchRunbookArgs>) {
return {
service: args.service,
steps: [
"Check error budget burn rate (SLO dashboard).",
"Inspect deploys in last 2h; rollback if correlated.",
"If auth-related: validate JWKS cache + token issuer status.",
],
};
}
async function createTicket(args: z.infer<typeof CreateTicketArgs>) {
// side effect: 실제로는 Jira/Linear 생성
return {
ticketKey: `${args.projectKey}-1024`,
url: `https://tickets.example.com/browse/${args.projectKey}-1024`,
};
}
/** --- tool registry --- */
type ToolName = "search_incidents" | "fetch_runbook" | "create_ticket";
const tools = [
{
type: "function",
function: {
name: "search_incidents",
description: "Search similar incidents from internal incident DB and return short summaries.",
// OpenAI Structured Outputs에 맞춘 JSON Schema를 붙이는 영역(여기서는 개념적으로 표시)
parameters: {
type: "object",
properties: {
query: { type: "string" },
timeRangeHours: { type: "integer", minimum: 1, maximum: 720 },
},
required: ["query"],
additionalProperties: false,
},
strict: true,
},
},
{
type: "function",
function: {
name: "fetch_runbook",
description: "Fetch the runbook steps for a given service.",
parameters: {
type: "object",
properties: { service: { type: "string" } },
required: ["service"],
additionalProperties: false,
},
strict: true,
},
},
{
type: "function",
function: {
name: "create_ticket",
description: "Create a ticket in the tracking system. SIDE EFFECT: requires approval.",
parameters: {
type: "object",
properties: {
projectKey: { type: "string" },
title: { type: "string" },
severity: { type: "string", enum: ["SEV1", "SEV2", "SEV3"] },
descriptionMarkdown: { type: "string" },
tags: { type: "array", items: { type: "string" } },
},
required: ["projectKey", "title", "severity", "descriptionMarkdown"],
additionalProperties: false,
},
strict: true,
},
},
] as const;
/** --- dispatcher with approval gate --- */
async function runTool(name: ToolName, rawArgs: unknown, ctx: { approvedTicket: boolean }) {
switch (name) {
case "search_incidents": {
const args = SearchIncidentsArgs.parse(rawArgs);
return await searchIncidents(args);
}
case "fetch_runbook": {
const args = FetchRunbookArgs.parse(rawArgs);
return await fetchRunbook(args);
}
case "create_ticket": {
if (!ctx.approvedTicket) {
return { blocked: true, reason: "Ticket creation requires human approval." };
}
const args = CreateTicketArgs.parse(rawArgs);
return await createTicket(args);
}
}
}
2) Agent Loop(핵심): “tool call → output → 다음 step”
type LoopOptions = {
approvedTicket: boolean; // human-in-the-loop
maxSteps: number;
};
export async function triageIncident(incidentText: string, opts: LoopOptions) {
const system = `
You are an SRE triage agent.
Goal: summarize incident, assess likely impact, propose next actions.
Rules:
- Prefer tools over guessing.
- Never create a ticket unless necessary AND tool output indicates clear next step.
- If create_ticket is needed, call it and accept it may be blocked.
- Keep outputs concise; do not paste large logs.
`;
// Responses API 스타일을 흉내: 실제 SDK/HTTP에서 response.output item을 순회하는 느낌으로 구현
let input: any[] = [
{ role: "system", content: system },
{ role: "user", content: incidentText },
];
const ctx = { approvedTicket: opts.approvedTicket };
for (let step = 1; step <= opts.maxSteps; step++) {
const resp = await client.responses.create({
model: "gpt-5", // 예시
input,
tools: tools as any,
});
// 1) 텍스트 출력 누적
const text = resp.output_text?.trim();
if (text) {
// 모델이 최종 답을 내면 종료
return { done: true, step, text, raw: resp };
}
// 2) tool call 찾기
const toolCalls = resp.output?.filter((it: any) => it.type === "function_call") ?? [];
if (toolCalls.length === 0) {
return { done: false, step, text: "", raw: resp, error: "No text and no tool calls." };
}
// 3) tool 실행하고 output을 다시 input에 붙여 다음 턴으로
for (const call of toolCalls) {
const name = call.name as ToolName;
const args = call.arguments ? JSON.parse(call.arguments) : {};
const result = await runTool(name, args, ctx);
input.push({
role: "assistant",
content: [
// Responses API에서 tool output을 넣는 구조를 단순화한 표현
{ type: "function_call_output", call_id: call.call_id, output: JSON.stringify(result) },
],
});
}
}
return { done: false, error: "maxSteps exceeded" };
}
3) 실행 예시(승인 전/후)
const incident = `
[ALERT] 5xx spike on api-gateway
- started: 2026-05-30 01:12 UTC
- correlation: deploy auth-service v2.18.4 10 minutes earlier
- symptoms: /v1/orders returns 401 then retries lead to 503
What happened and what should we do next?
`;
console.log(await triageIncident(incident, { approvedTicket: false, maxSteps: 6 }));
예상 출력(요지)
- 유사 장애(INC-18421)에서 “token rotation/JWKS cache” 이슈가 있었음을 언급
- runbook 기반으로 “deploy 롤백/키 캐시 확인/SLO burn rate 확인” 액션 제시
- 필요시 티켓 생성 시도 → 승인 없으면 blocked로 반환 → 모델이 “승인 요청 문구 + 생성에 필요한 필드”를 정리
이 구현의 핵심은:
- tool args를 스키마로 검증(Zod) + strict schema로 모델 출력부터 강제 (help.openai.com)
- side effect 툴은 승인 플래그(또는 정책 엔진) 로 런타임에서 차단
- tool output은 “필요한 요약만” 반환해 컨텍스트/비용 폭주를 막음
⚡ 실전 팁 & 함정
Best Practice 1) “도구는 기능이 아니라 계약”으로 설계하라
- tool signature가 애매하면 모델이 순서를 잘못 호출했을 때 조용히 망가집니다.
- 파라미터에 “이전 단계 산출물 ID(required)”를 넣어 잘못된 호출 순서가 스키마에서 실패하게 만드는 게 효과적입니다(예:
prepare_booking결과의bundleId없으면confirm_booking불가). Reddit 실전 사례에서도 “순서가 틀리면 graceful fail”을 강조합니다. (reddit.com)
Best Practice 2) “승인/권한/스코프”는 모델이 아니라 Dispatcher에서 강제
- create/update/delete, 결제, 메일 발송 같은 툴은 fail closed가 기본값이어야 합니다.
- MCP 도입이 늘면서 “연결이 쉬워진 만큼” 실행 경계가 더 중요해졌고, MCP 관련 보안 이슈도 지속적으로 보도됩니다. (tomshardware.com)
- 결론: 모델 프롬프트에 “하지 마”라고 쓰는 건 정책이 아닙니다. 코드로 막으세요.
Best Practice 3) 관측 가능성(Tracing)을 “기능”으로 취급
- tool call 단위로 span을 남기고, 어떤 tool이 비용/실패를 유발하는지 봐야 합니다.
- OpenAI Agents SDK는 tool call/handoff를 span으로 감싸는 tracing 문서를 제공합니다. (github.com)
- 운영에선 “에이전트 정확도”보다 어떤 tool이 장애를 만드는지가 더 중요합니다.
흔한 함정/안티패턴
- 툴 출력 원문을 통째로 모델에 재주입: 컨텍스트/비용 폭발 + 정보 누출 위험
→ “요약+링크+핵심 필드만”을 원칙으로 - tool description에 정책을 길게 적기: 실제 실행 정책은 dispatcher가 가져야 하고, description은 모델 라우팅 품질에만 집중
- 루프 무한 실행:
maxSteps, 예산(budget), 타임아웃, “StopAtTools(특정 tool 호출 시 정지)” 같은 중단 장치를 두세요(Agents SDK에도 유사 컨셉이 문서화됨). (openai.github.io)
비용/성능/안정성 트레이드오프(2026년 포인트)
- multi-step agent는 HTTP 왕복이 누적되면 체감 latency가 커집니다. OpenAI는 WebSocket 기반 Responses 실행 모드를 성능 개선 축으로 내세웁니다. (openai.com)
- 반대로 “한 번에 다 계획해서(tool blueprint) 실행”은 빠를 수 있지만, 중간 실패 복구/승인/조건 분기가 어려워집니다. 즉:
- 인터랙티브/승인 필요: 짧은 step loop + 스트리밍/웹소켓
- 배치/ETL형: 선언적 workflow(그래프/블루프린트) 고려
🚀 마무리
정리하면, 2026년 5월의 agent tool use 구현은 “function calling 잘 쓰기”를 넘어 실행 레이어를 설계하는 문제입니다.
- 도입 추천 기준 1) 작업이 “조회→검증→작성/실행”으로 2단계 이상인가? 2) side effect가 있는가? 있다면 승인/권한/스코프를 코드로 강제할 수 있는가? 3) tool 출력의 크기/빈도에 대해 비용 상한을 설계했는가? 4) tool call 단위 tracing/로그가 있는가?
다음 학습 추천(실무 순서)
1) Structured Outputs(strict: true)로 스키마 계약을 먼저 고정 (help.openai.com)
2) Responses API의 아이템/툴 호출 흐름을 이해하고, 간단한 loop를 직접 구현 (aiwiki.ai)
3) Agents SDK의 tracing/handoff/sandbox를 붙여 “운영 가능한 런타임”으로 확장 (openai.com)
4) 도구 확장/상호운용이 필요해지면 MCP로 표준화하되, 보안 경계부터 설계 (openai.github.io)
원하면 위 예제를 (1) MCP 서버 도구로 교체, (2) WebSocket Responses로 스트리밍/저지연화, (3) 승인 게이트를 정책 엔진(OPA 등)로 분리하는 형태로 2,000~3,000자 추가 확장 버전도 작성해드릴게요.