AI 에이전트 도구 호출을 안전하게 설계하는 법, 멱등성·재시도·병렬화 기준
AI를 활용하여 생성한 이미지입니다.
AI 에이전트가 사내 API나 결제·예약·파일 시스템을 호출하기 시작하면, 답변 품질보다 먼저 봐야 할 문제가 생겨요. 같은 주문이 두 번 들어가거나, 실패한 요청을 재시도하는 동안 부작용이 반복되는 문제입니다. JSON Schema를 붙였다고 해서 이 문제가 사라지지는 않아요.
OpenAI의 함수 호출은 모델이 JSON Schema에 맞는 인자를 만들어 애플리케이션의 코드를 호출하도록 연결하는 구조예요.[1] OpenAI는 사용자 정의 함수 외에도 웹 검색, 파일 검색, 원격 MCP 서버 같은 도구를 Responses API에서 연결할 수 있다고 설명해요.[2] 실제 실행은 모델이 아니라 여러분의 서버가 담당합니다. 그래서 안전한 설계는 “모델이 올바른 JSON을 만들었나”와 “서버가 이 요청을 몇 번 실행해도 괜찮은가”를 나눠 다뤄야 해요.
이 글은 함수 호출을 처음 배우는 설명이 아니라, 이미 도구 하나 이상을 붙인 서비스에서 실패·재시도·동시 호출을 통제하는 방법을 정리합니다. 예시는 주문 조회와 배송지 변경처럼 결과를 읽는 작업과 상태를 바꾸는 작업을 구분해 설명할게요.
도구 호출의 책임 경계를 먼저 나눠요
도구 호출 흐름은 보통 네 단계로 이어집니다. 모델이 도구를 고르고 인자를 만들면, 애플리케이션이 인자를 검증하고 실제 함수를 실행합니다. 그 결과를 다시 모델에 넣고, 모델이 사용자에게 최종 답을 만듭니다.[1]
- 모델이 선택할 수 있는 도구 목록과 설명을 보냅니다.
- 모델이 도구 이름과 인자를 반환합니다.
- 서버가 인증·권한·스키마·업무 규칙을 검사한 뒤 도구를 실행합니다.
- 실행 결과를 모델에 돌려주고 사용자 응답을 생성합니다.
이 중 3단계는 모델에게 위임하면 안 돼요. amount > 0, 요청자에게 해당 계정 권한이 있는지, 이미 처리된 주문인지 같은 검사는 애플리케이션이 수행해야 합니다. 모델이 만든 인자는 제안이지 승인 토큰이 아니에요.
strict 스키마는 시작점이지 안전장치의 전부가 아니에요
OpenAI 함수 도구는 JSON Schema 기반으로 정의할 수 있고, strict 모드를 사용하면 스키마에 맞는 인자를 더 엄격하게 요구할 수 있습니다.[1] strict 스키마를 쓸 때는 객체에 additionalProperties: false를 넣고 필수 필드를 명시하는 형태를 기본으로 삼는 편이 좋아요.[1]
tool = {
"type": "function",
"name": "change_shipping_address",
"description": "Change the address for an unpaid order.",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"address": {"type": "string"},
"confirm": {"type": "boolean"}
},
"required": ["order_id", "address", "confirm"],
"additionalProperties": False
}
}
여기서 confirm이 true라고 해서 바로 변경하면 안 됩니다. 사용자가 실제로 변경을 승인한 대화 상태인지, 주문이 결제 전인지, 주소가 허용된 형식인지 서버에서 다시 확인하세요. 스키마는 데이터 모양을 제한하지만, 권한과 상태 전이는 설명하지 못합니다.
읽기 도구와 쓰기 도구를 분리해요
주문 조회와 주소 변경을 같은 방식으로 취급하면 재시도 정책을 만들기 어렵습니다. 조회는 같은 요청을 여러 번 실행해도 상태가 바뀌지 않는다고 정의할 수 있지만, 주소 변경·결제·예약 생성은 호출 횟수가 곧 부작용이 될 수 있어요.
| 도구 종류 | 예시 | 권장 통제 |
|---|---|---|
| 읽기 | 주문 상태·재고 조회 | 짧은 재시도, 캐시, 조회 시점 표시 |
| 멱등 쓰기 | 프로필 메모를 특정 값으로 설정 | 요청 ID와 현재 상태 비교 |
| 비멱등 쓰기 | 결제·쿠폰 발급·예약 생성 | 멱등 키, 중복 차단, 사람 승인 |
| 파괴적 작업 | 삭제·환불·권한 해제 | 명시적 확인, 별도 권한, 감사 로그 |
멱등성은 같은 요청을 반복해도 최종 효과가 한 번만 적용되도록 만드는 성질이에요. 예를 들어 change_shipping_address에 임의의 요청 ID를 붙이고, 서버가 이미 처리한 ID를 다시 받으면 기존 결과만 돌려주도록 설계할 수 있습니다. 모델이 같은 도구 호출을 다시 내놓아도 부작용이 한 번만 일어나요.
def execute_once(request_id, action):
previous = idempotency_store.get(request_id)
if previous is not None:
return previous
result = action()
idempotency_store.save(request_id, result)
return result
실서비스에서는 메모리 딕셔너리 대신 만료 시간과 원자적 저장을 지원하는 데이터 저장소를 사용해야 합니다. 작업 시작 전에 키를 선점하고, 처리 중인 상태와 완료 결과를 구분해야 실패 직후의 재시도가 빈틈을 만들지 않아요.
tool_choice와 병렬 호출을 상황별로 제한해요
OpenAI API는 기본적으로 모델이 도구를 호출할지, 여러 개를 호출할지 결정하게 할 수 있고, tool_choice로 동작을 제한할 수 있습니다.[1] 특정 도구를 강제로 선택하거나 호출이 필요할 때만 허용하는 옵션이 있지만, 모든 요청에 “필수”를 걸면 일반적인 대화까지 도구 실행으로 바뀔 수 있어요.
상태 변경 도구가 포함된 턴에서는 병렬 호출을 끄는 것이 보수적인 기본값입니다. OpenAI 문서의 parallel_tool_calls: false 설정은 한 번에 도구를 0개 또는 1개만 호출하도록 제한합니다.[1] 주소 변경과 주문 취소처럼 순서와 확인이 중요한 작업을 동시에 보내지 않게 만들 수 있어요.
response = client.responses.create(
model="gpt-5",
input=input_items,
tools=tools,
tool_choice="auto",
parallel_tool_calls=False,
)
반대로 독립적인 상품 재고 조회처럼 부작용이 없는 읽기 작업은 병렬 호출이 유리할 수 있어요. 그래도 각 결과를 합칠 때 어떤 시점의 데이터인지 표시하고, 하나가 실패했을 때 전체 답변을 성공으로 포장하지 않아야 합니다.
재시도는 네트워크 오류와 업무 거절을 구분해요
모든 오류를 같은 방식으로 재시도하면 장애가 커집니다. 연결 끊김이나 일시적인 서버 오류는 제한된 횟수와 지수 백오프로 재시도할 수 있지만, 권한 부족·잘못된 인자·주문 상태 불일치는 입력이나 업무 흐름을 고쳐야 하는 오류예요.
| 오류 성격 | 재시도 | 모델에 돌려줄 내용 |
|---|---|---|
| 타임아웃·일시적 5xx | 멱등 키 확인 후 제한적으로 | 일시 실패와 재시도 가능 여부 |
| 인증·권한 거절 | 재시도하지 않음 | 권한 부족, 추가 인증 필요 |
| 스키마 오류 | 서버가 보정하지 않음 | 필드와 허용 형식 |
| 업무 상태 충돌 | 현재 상태 재조회 후 판단 | 주문 상태와 가능한 다음 행동 |
도구 결과에는 최소한 ok, retryable, error_code, request_id 같은 기계가 읽을 수 있는 필드를 넣는 편이 좋아요. 모델에게 긴 스택 트레이스를 넘기기보다, 사용자에게 보여줄 설명과 서버 로그용 상세 정보를 분리하세요.
Responses API로 옮길 때 확인할 점
OpenAI는 새 프로젝트에서 Responses API를 권장하고 있으며, Chat Completions와 비교해 도구 설정과 응답 형태가 달라집니다.[3] Responses API는 웹 검색·파일 검색·컴퓨터 사용 같은 내장 도구와 사용자 정의 함수 호출을 한 인터페이스에서 다루는 방향이에요.[2][3]
기존 코드에서 엔드포인트만 바꾸면 끝난다고 생각하면 안 됩니다. 도구 정의 형식, 모델 응답에서 호출 항목을 찾는 방법, 결과를 다시 넣는 입력 구조를 함께 점검하세요. 마이그레이션 중에는 기존 경로와 새 경로를 같은 테스트 케이스로 비교하고, 특히 중복 실행 방지 키가 두 경로 모두 유지되는지 확인해야 해요.
운영 전에 확인할 체크리스트
- 읽기·멱등 쓰기·비멱등 쓰기·파괴적 도구를 별도 목록으로 나누세요.
- 모든 객체 스키마에 필수 필드와
additionalProperties: false를 검토하세요. - 상태 변경 도구에 요청 ID와 중복 실행 방지 저장소를 붙이세요.
- 권한·사용자 승인·현재 상태 검사를 모델 호출 뒤 서버에서 다시 수행하세요.
- 일시적 네트워크 오류와 업무상 거절을 다른 오류 코드로 기록하세요.
- 상태 변경 턴에서는 병렬 도구 호출을 끄고, 읽기 작업만 병렬화를 검토하세요.
- 실패한 도구 결과가 사용자에게 성공처럼 보이지 않는지 로그와 UI에서 확인하세요.
자주 묻는 점
strict를 켜면 잘못된 주문 실행도 막을 수 있나요?
아니요. strict는 인자 구조를 스키마에 맞추는 데 도움을 주지만, 사용자의 권한이나 주문 상태까지 검증하지는 않아요. 부작용이 있는 작업은 애플리케이션의 업무 규칙과 멱등 키가 따로 필요합니다.
모든 도구를 병렬로 호출하면 빠르지 않나요?
독립적인 읽기 작업에는 도움이 될 수 있어요. 하지만 서로 순서가 있는 작업이나 상태를 바꾸는 작업을 병렬로 보내면 경쟁 조건과 중복 실행이 생길 수 있습니다. 속도보다 도구 간 의존성을 먼저 분류하세요.
Responses API로 옮기면 기존 도구가 자동으로 안전해지나요?
자동으로 안전해지지는 않아요. API 형태가 바뀌어도 실제 도구를 실행하는 서버의 권한 검사, 재시도, 멱등성 설계는 그대로 여러분의 책임입니다. 마이그레이션 전후에 같은 중복 실행 테스트를 반복해야 해요.
AI API 키를 보관하는 기본 원칙이 필요하다면 OpenAI API 키를 안전하게 보관하는 글을 먼저 확인해 보세요. 도구 호출을 붙이는 순간부터는 키 보호만큼이나 어떤 명령을 몇 번 실행할 수 있는지 정하는 일이 중요해집니다.
[…] 도구 호출의 멱등성과 재시도를 다룬 AI 에이전트 도구 호출 설계 글과 함께 보면, 비용 최적화와 실행 안전성을 같은 설계표에서 관리하기 […]