AI 에이전트 도구 호출을 안전하게 설계하는 법, 멱등성·재시도·병렬화 기준

자연광이 비치는 책상 위 무브랜드 서버 장치와 분리된 보안 토큰 카드, 서로 닿지 않게 배치된 API 작업 구성

AI를 활용하여 생성한 이미지입니다.

AI 에이전트가 주문 조회를 넘어 배송지 변경, 결제, 예약, 파일 수정까지 수행하면 “모델이 올바른 JSON을 만들었는가”만으로는 안전성을 설명할 수 없습니다. 같은 도구 호출이 네트워크 재시도나 모델의 반복 응답으로 두 번 실행될 수 있기 때문입니다. 함수 호출은 모델이 구조화된 인자를 제안하는 단계이고, 인증·권한·업무 상태·실행 횟수를 결정하는 주체는 애플리케이션 서버입니다.[1]

OpenAI 문서는 사용자 정의 함수와 웹 검색·파일 검색·원격 MCP 같은 도구를 Responses API에서 연결할 수 있다고 설명합니다.[2] 도구 종류가 늘어날수록 읽기와 쓰기, 되돌릴 수 있는 작업과 파괴적 작업을 구분해 재시도·병렬화 규칙을 따로 두어야 합니다.

먼저 도구의 부작용을 분류하세요

분류 예시 기본 통제
읽기 주문·재고·배송 상태 조회 짧은 재시도, 조회 시각 표시
멱등 쓰기 프로필 메모를 특정 값으로 설정 요청 ID와 현재 상태 비교
비멱등 쓰기 결제·쿠폰 발급·예약 생성 멱등 키와 중복 차단
파괴적 작업 삭제·환불·권한 해제 명시적 확인, 별도 권한, 감사 로그

“멱등”은 같은 요청을 여러 번 받아도 최종 효과가 한 번만 적용되도록 설계하는 성질입니다. 예를 들어 주소 변경 요청에 request_id를 붙이고, 서버가 이미 완료한 ID를 다시 받으면 새 작업을 실행하지 않고 기존 결과를 반환하게 합니다. 단순히 메모리 딕셔너리에 저장하면 프로세스 재시작과 여러 서버 사이에서 중복이 생길 수 있으므로, 만료와 원자적 상태 변경을 지원하는 저장소가 필요합니다.

스키마와 업무 규칙을 분리하세요

함수 도구를 JSON Schema와 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가 들어왔다고 곧바로 실행하면 안 됩니다. 사용자가 바로 앞 단계에서 실제 변경을 승인했는지, 요청자에게 해당 주문 권한이 있는지, 주문이 아직 결제 전인지, 주소가 허용된 형식인지 서버가 다시 확인해야 합니다. 스키마는 데이터 모양을 검사하지만 권한과 상태 전이를 대신하지 않습니다.

멱등 키의 상태를 세분화하세요

처리 중인 요청과 완료된 요청을 같은 값으로 저장하면 타임아웃 직후 재시도가 위험해집니다. 최소한 started, completed, failed_retryable, failed_final처럼 상태를 구분하고, 동일 키에 다른 인자가 들어오면 충돌로 거절하세요. 결제처럼 외부 시스템에 이미 전달됐을 가능성이 있는 작업은 내부 로그만 보고 미실행으로 판단하지 말고 제공자의 조회 API나 사람 확인 절차를 둡니다.

요청 수신
  ├─ 같은 키 + 완료 결과 있음 → 기존 결과 반환
  ├─ 같은 키 + 처리 중       → 중복 실행하지 않고 상태 반환
  ├─ 같은 키 + 인자 다름     → 충돌 오류
  └─ 새 키                   → 원자적으로 선점 후 실행

모델에게 보내는 도구 결과에는 ok, retryable, error_code, request_id처럼 기계가 읽을 수 있는 필드를 넣으세요. 사용자용 설명과 서버 로그용 상세 오류를 분리하면 스택 트레이스나 민감한 데이터가 모델과 사용자 화면에 불필요하게 노출되는 것을 줄일 수 있습니다.

병렬 호출은 독립된 읽기부터 검토하세요

독립적인 재고 조회 여러 건은 병렬화할 수 있지만, 주소 변경과 주문 취소처럼 순서와 승인에 의존하는 작업은 병렬화하지 않는 편이 보수적입니다. OpenAI API의 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 멱등 키 확인 후 제한된 횟수로 재시도 재시도 중인지, 최종 실패인지
인증·권한 거절 자동 재시도하지 않음 추가 인증 또는 권한 필요
스키마 오류 잘못된 인자를 서버가 임의 보정하지 않음 허용 형식과 오류 필드
업무 상태 충돌 현재 상태를 재조회한 뒤 다음 행동 결정 주문 상태와 가능한 선택

재시도 횟수와 백오프 상한은 작업별로 정하세요. 결제·예약 생성처럼 외부 부작용이 있는 호출은 먼저 멱등성이나 제공자 측 중복 방지 키를 확인해야 하며, 키가 없으면 자동 재시도보다 중단과 사람 확인이 안전할 수 있습니다.

Responses API로 옮길 때의 경계

OpenAI는 새 프로젝트에서 Responses API를 권장하고 있으며, 기존 Chat Completions와 도구 설정·응답 항목을 다루는 방식이 다릅니다.[3] 엔드포인트만 바꾸지 말고 도구 정의, 호출 항목 추출, 결과를 다시 입력하는 구조, 중복 실행 방지 키를 같은 시나리오로 비교하세요. API가 바뀌어도 서버의 권한 검사와 재시도 책임이 자동으로 이전되는 것은 아닙니다.[2][3]

감사 로그와 사람 승인 경계

도구가 상태를 바꾸는 서비스라면 호출 결과만 저장하지 말고 누가, 어떤 대화와 요청 ID로, 어떤 인자를 승인했는지도 남겨야 합니다. 로그에는 주소 전체나 결제 정보 같은 민감한 값을 그대로 복사하지 말고 필요한 식별자와 해시·마스킹을 사용하세요. 보존 기간과 접근 권한도 업무 규칙의 일부입니다.

사람 승인이 필요한 작업은 모델에게 “승인됨”이라는 문자열을 받는 것으로 끝내지 마세요. 승인 화면에서 보여 준 대상·금액·주소와 실제 실행 인자가 같은지 서버가 비교하고, 승인 만료 시간과 승인자를 기록해야 합니다. 실행 후에는 외부 시스템의 실제 상태를 다시 조회해 성공 답변을 만들도록 설계하세요.

운영 전 체크리스트

  • 읽기·멱등 쓰기·비멱등 쓰기·파괴적 도구를 별도 목록으로 만들었나요?
  • 스키마에 필수 필드와 추가 속성 제한을 넣었나요?
  • 현재 사용자·계정·주문 상태를 서버가 다시 확인하나요?
  • 멱등 키의 처리 중·완료·실패 상태를 구분하나요?
  • 상태 변경 턴에서 병렬 호출을 제한했나요?
  • 재시도 가능 오류와 업무 거절을 다른 코드로 기록하나요?
  • 도구 실패가 성공 답변처럼 화면에 보이지 않나요?

API 키 보관 방식은 OpenAI API 키를 안전하게 보관하는 글에서 확인할 수 있습니다. 다만 키를 숨기는 것과 도구 권한을 제한하는 것은 별개이므로, 둘을 하나의 보안 대책으로 합치지 마세요.

자주 묻는 질문

strict를 켜면 잘못된 주문 실행이 막히나요?

아닙니다. strict는 인자 구조를 제한하는 기능입니다. 권한, 승인, 주문 상태, 중복 실행은 서버의 업무 규칙과 멱등성 설계가 담당합니다.

모든 도구를 병렬로 호출하면 더 빠르지 않나요?

독립적인 읽기에서는 도움이 될 수 있지만, 상태 변경이나 순서 의존 작업에서는 경쟁 조건을 만들 수 있습니다. 속도보다 도구 간 의존성을 먼저 분류하세요.

타임아웃이면 실행되지 않았다고 봐도 되나요?

아닙니다. 응답만 잃었을 뿐 외부 시스템에서 실행됐을 가능성이 있습니다. 멱등 키와 상태 조회 없이 같은 쓰기 작업을 다시 보내지 마세요.

Sources

  1. [1] Function calling | OpenAI API
  2. [2] Using tools | OpenAI API
  3. [3] Migrating to Responses API | OpenAI API

함께 읽을 글