Contents
see ListAI 에이전트의 위험은 모델보다 도구 경계에서 커진다
LLM을 업무 시스템에 붙일 때 가장 먼저 보이는 결과물은 자연어 답변이지만, 실제 운영 위험은 모델이 외부 도구를 호출하는 순간부터 커집니다. 고객 조회, 주문 변경, 파일 생성, 메일 발송, 배포 실행처럼 시스템 상태를 바꾸는 기능이 연결되면 프롬프트 품질만으로는 안정성을 보장할 수 없습니다. 에이전트는 사용자의 말을 해석하고 여러 단계를 이어 붙이기 때문에, 같은 요청도 실행 시점의 데이터, 이전 단계의 응답, 도구 오류에 따라 다른 경로로 흘러갈 수 있습니다.
따라서 운영용 AI 에이전트는 "무엇을 할 수 있는가"보다 "어디까지 못 하게 막을 것인가"를 먼저 설계해야 합니다. 핵심은 도구 목록을 최소화하고, 각 도구의 입력을 엄격히 검증하고, 실패했을 때 되돌릴 수 있는 단위를 정하는 것입니다. 특히 사내 업무 자동화에서는 조회 도구와 변경 도구를 분리하고, 결제·발송·삭제·권한 변경 같은 작업은 별도 승인 단계나 dry-run 단계를 두는 편이 안전합니다.
도구를 기능 단위가 아니라 위험 단위로 나눈다
나쁜 설계의 예는 하나의 도구가 여러 일을 동시에 처리하는 형태입니다. 예를 들어 processCustomerRequest 같은 도구가 고객 조회, 쿠폰 발급, 문자 발송까지 모두 맡으면 모델이 어떤 부작용을 만들지 추적하기 어렵습니다. 반대로 좋은 설계는 listCustomerOrders, createCouponDraft, approveCouponIssue, sendSmsPreview처럼 상태 변경 범위가 작고 이름만 봐도 위험도를 알 수 있는 도구를 제공합니다.
- 읽기 전용 도구는 기본 허용하되 응답 필드를 제한합니다.
- 상태 변경 도구는 idempotency key를 요구해 중복 실행을 막습니다.
- 외부 발송 도구는 미리보기와 최종 발송을 분리합니다.
- 삭제나 권한 변경 도구는 관리자 승인 토큰이나 별도 큐를 거치게 합니다.
- 도구별 timeout, 최대 재시도 횟수, 감사 로그 필드를 표준화합니다.
도구 입력 검증과 실행 래퍼 예시
아래 예시는 Node.js에서 에이전트가 호출할 수 있는 도구를 allowlist로 제한하고, 입력 검증, 타임아웃, idempotency key, 감사 로그를 한곳에서 처리하는 단순한 구조입니다. 실제 프로젝트에서는 Zod, Valibot, JSON Schema 같은 검증기를 붙이고, auditLog는 데이터베이스나 로그 수집 시스템으로 보내면 됩니다.
const tools = {
async createCouponDraft(input) {
if (!input.customerId || !input.reason) throw new Error('invalid input');
return db.couponDraft.create({
data: {
customerId: input.customerId,
amount: Math.min(Number(input.amount || 0), 10000),
reason: input.reason,
status: 'draft'
}
});
}
};
async function runTool({ name, input, actor, requestId }) {
if (!Object.hasOwn(tools, name)) throw new Error('tool not allowed');
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const idempotencyKey = `${requestId}:${name}`;
const previous = await db.toolRun.findUnique({ where: { idempotencyKey } });
if (previous) return previous.result;
await auditLog('tool.started', { name, actor, requestId, input });
const result = await tools[name](input, { signal: controller.signal });
await db.toolRun.create({ data: { idempotencyKey, name, result } });
await auditLog('tool.finished', { name, actor, requestId });
return result;
} catch (error) {
await auditLog('tool.failed', { name, actor, requestId, message: error.message });
throw error;
} finally {
clearTimeout(timeout);
}
}
이 구조의 장점은 모델이 같은 도구를 반복 호출하더라도 requestId 기준으로 중복 처리를 줄일 수 있다는 점입니다. 또한 감사 로그가 시작, 성공, 실패로 나뉘기 때문에 운영자가 "모델이 왜 이 작업을 실행했는지", "실제로 외부 시스템이 변경됐는지", "중간에 실패했는지"를 나중에 확인할 수 있습니다. 에이전트는 대화형이라 사용자가 같은 요청을 다시 말하는 일이 많으므로 idempotency 처리는 선택이 아니라 기본값에 가깝습니다.
재시도는 모든 오류에 걸면 안 된다
AI 에이전트 도구에서 흔한 실수는 네트워크 오류, 권한 오류, 검증 오류, 비즈니스 거절을 모두 같은 실패로 보는 것입니다. 재시도는 일시적 장애에만 적용해야 합니다. 입력값이 잘못됐거나 권한이 부족한 요청은 재시도해도 성공하지 않습니다. 오히려 같은 실패 로그만 쌓이고, 외부 API 사용량과 비용이 늘어납니다.
- HTTP 408, 429, 500, 502, 503, 504는 짧은 지수 백오프 재시도를 고려합니다.
- 400, 401, 403, 404, 409는 원인을 사용자나 운영자에게 명확히 전달합니다.
- 결제, 발송, 재고 차감처럼 부작용이 있는 작업은 재시도 전에 idempotency key가 반드시 있어야 합니다.
- 도구 실패 메시지는 모델에게 내부 스택트레이스를 넘기지 말고, 복구 가능한 요약만 제공합니다.
운영 로그는 프롬프트보다 오래 살아남는다
에이전트 운영에서는 프롬프트 버전, 모델명, 사용자 요청, 선택된 도구, 도구 입력, 도구 결과 요약, 최종 응답을 함께 남겨야 합니다. 단, 개인정보나 토큰은 저장하지 않도록 마스킹 규칙을 먼저 정해야 합니다. 나중에 장애가 생겼을 때 "모델이 틀렸다"는 말만으로는 원인을 찾을 수 없습니다. 프롬프트 변경 때문인지, 검색 데이터 때문인지, 도구 응답이 바뀐 것인지, 사용자가 모호하게 지시했는지 분리해서 볼 수 있어야 합니다.
권장하는 최소 로그 단위는 한 사용자 요청을 traceId로 묶고, 그 안에 model.call, tool.started, tool.finished, tool.failed, user.confirmed, response.sent 이벤트를 순서대로 남기는 방식입니다. 이 정도만 있어도 운영자는 특정 고객의 자동 처리 이력을 재현할 수 있고, 반복 실패하는 도구를 찾아 개선할 수 있습니다.
배포 전 점검 체크리스트
- 도구가 읽기, 초안 생성, 최종 실행으로 분리되어 있는지 확인합니다.
- 상태 변경 도구에 idempotency key와 감사 로그가 있는지 확인합니다.
- 모든 도구에 timeout과 실패 분류가 적용되어 있는지 확인합니다.
- 모델에게 전달되는 오류 메시지에 비밀값과 내부 스택이 빠져 있는지 확인합니다.
- 고위험 작업에는 사용자 확인, 관리자 승인, 또는 큐 기반 후처리가 있는지 확인합니다.
- 프롬프트 버전과 도구 버전이 traceId 기준으로 함께 기록되는지 확인합니다.
AI 에이전트는 한 번의 멋진 답변보다 반복 실행의 안전성이 더 중요합니다. 도구 권한을 작게 나누고, 입력을 검증하고, 실행 흔적을 남기고, 실패를 분류하면 모델이 바뀌거나 업무량이 늘어도 운영자가 통제할 수 있는 자동화가 됩니다.