도구 호출은 모델의 답변이 아니라 서버 작업이다

AI 에이전트에 검색, 고객 조회, 파일 생성, 배포, 결제 같은 도구를 연결하면 모델은 자연어를 실행 가능한 작업으로 바꿉니다. 이때 가장 위험한 설계는 모델이 만든 도구 이름과 인수를 그대로 실행하는 방식입니다. 모델 출력은 신뢰 경계 밖의 입력으로 취급해야 하며, 프롬프트 인젝션이나 잘못된 추론으로 인해 의도하지 않은 작업이 선택될 수 있습니다.

안전한 구조에서는 모델이 할 수 있는 일이 시스템이 허용한 범위 안에만 머뭅니다. 모델은 작업을 제안하고, 애플리케이션은 사용자 권한·업무 상태·입력 형식·호출 한도를 검증한 뒤 실행합니다. 특히 쓰기 작업과 외부 전송 작업은 읽기 작업보다 훨씬 좁은 권한과 별도 승인 절차가 필요합니다.

도구를 작고 명확한 작업 단위로 나누기

도구 하나에 범용 SQL 실행, 임의 URL 요청, 임의 셸 명령 같은 기능을 넣지 마세요. 예를 들어 주문 조회에는 get_order_summary, 배송지 변경에는 request_shipping_address_change처럼 업무 목적이 한정된 도구를 만듭니다. 도구 설명에는 가능한 대상, 변경 가능한 필드, 금지 조건을 적고, 서버도 같은 제약을 다시 적용해야 합니다.

  • 읽기와 쓰기 도구를 분리하고, 쓰기 도구에는 더 짧은 토큰 만료 시간을 적용합니다.
  • 테넌트 ID와 사용자 ID는 모델 인수로 받지 말고 인증된 세션에서 주입합니다.
  • 삭제, 환불, 권한 부여처럼 되돌리기 어려운 작업은 확인용 요청과 실제 실행을 분리합니다.
  • 도구별로 허용 호출 횟수와 시간 제한을 두어 반복 호출과 비용 폭증을 막습니다.

모델 인수는 스키마 검증 후 업무 규칙까지 확인한다

JSON Schema 또는 런타임 검증 라이브러리는 타입 오류를 줄이는 첫 단계입니다. 그러나 문자열 길이와 열거형을 통과했다고 해서 안전한 것은 아닙니다. 주문 번호가 현재 사용자 소유인지, 상태가 변경 가능한지, 변경 요청이 중복되지 않았는지처럼 업무 규칙을 데이터베이스 조회와 함께 검사해야 합니다.

import { z } from 'zod';

const changeRequest = z.object({
  orderId: z.string().regex(/^ORD-[0-9]{8}$/),
  newAddress: z.string().min(10).max(200)
});

async function requestAddressChange(session, rawArgs) {
  const args = changeRequest.parse(rawArgs);
  const order = await db.order.findFirst({
    where: { id: args.orderId, customerId: session.customerId }
  });

  if (!order) throw new Error('주문을 찾을 수 없습니다.');
  if (!['PAID', 'PREPARING'].includes(order.status)) {
    throw new Error('현재 주문 상태에서는 변경 요청을 받을 수 없습니다.');
  }

  return db.addressChangeRequest.create({
    data: { orderId: order.id, requestedAddress: args.newAddress,
            requestedBy: session.userId, status: 'PENDING_CONFIRMATION' }
  });
}

이 예시는 모델이 전달한 customerId를 사용하지 않는 점이 중요합니다. 세션의 고객 식별자로 소유권을 확인하므로 다른 고객의 주문 번호를 추측해도 변경할 수 없습니다. 검증 오류는 모델에 필요한 범위만 알려 주고, 내부 테이블명이나 쿼리, 인증 정보는 응답에 포함하지 않습니다.

승인 단계와 멱등성으로 실수의 영향을 제한하기

에이전트가 고객에게 보여 줄 초안을 만들고 사용자가 확인한 뒤 실행하는 흐름은 고위험 작업에서 효과적입니다. 승인 화면에는 변경 전후 값, 대상, 예상 결과를 사람이 읽을 수 있게 표시합니다. 승인 토큰은 사용자·작업 내용·만료 시간에 묶고, 내용이 바뀌면 새 승인을 요구합니다.

네트워크 재시도나 모델의 반복 호출로 같은 요청이 두 번 실행될 수 있으므로 쓰기 API에는 멱등성 키를 사용합니다. 요청 ID와 작업 종류를 저장하고, 같은 키가 다시 오면 이전 결과를 돌려줍니다. 결제, 메시지 발송, 티켓 생성은 이 처리가 없으면 운영 사고로 이어지기 쉽습니다.

감사 로그는 디버깅용이 아니라 통제 장치다

도구 호출마다 요청 시각, 인증 주체, 에이전트와 도구 이름, 검증 결과, 승인 ID, 실행 결과, 지연 시간을 구조화해 기록합니다. 다만 프롬프트 원문, 액세스 토큰, 주민번호나 주소 전체처럼 민감한 값은 로그에 남기지 않거나 마스킹해야 합니다. 실패 로그도 중요합니다. 권한 거부와 스키마 오류가 갑자기 늘면 프롬프트 변경, 도구 설명 오류, 공격 시도를 빠르게 발견할 수 있습니다.

{
  "event": "agent.tool_call",
  "tool": "request_shipping_address_change",
  "actorId": "user_42",
  "decision": "approved",
  "result": "pending_confirmation",
  "latencyMs": 84,
  "requestId": "req_01J..."
}

운영 체크리스트

  • 모든 도구가 최소 권한 원칙에 맞게 작은 업무 단위로 제한되어 있는지 확인합니다.
  • 모델 출력에 대해 스키마 검증, 소유권 확인, 상태 검증을 모두 수행합니다.
  • 고위험 쓰기 작업은 사용자 확인과 멱등성 키를 사용합니다.
  • 민감 정보를 제외한 감사 로그와 도구별 실패율·지연 시간을 모니터링합니다.
  • 권한 거부, 시간 초과, 중복 요청을 포함한 실패 시나리오를 정기적으로 테스트합니다.