DEV Community

Cover image for 비결정적 AI 에이전트 테스트 방법 (temperature=0만으로는 부족할 때)
Rihpig
Rihpig

Posted on • Originally published at apidog.com

비결정적 AI 에이전트 테스트 방법 (temperature=0만으로는 부족할 때)

월요일에는 테스트가 통과했습니다. 동일한 입력, 동일한 코드, temperature=0이었습니다. 화요일에는 아무것도 바꾸지 않았는데 실패합니다. 정확한 문자열 일치를 어설션했지만 모델이 같은 의미를 조금 다르게 표현했기 때문입니다. 에이전트는 정상인데, 이제 제품 대신 테스트 스위트를 디버깅하게 됩니다.

지금 Apidog를 사용해 보세요

언어 모델을 호출하는 시스템에서는 출력이 달라질 수 있습니다. temperature=0이어도 실행마다 바이트 단위로 동일한 응답을 기대할 수는 없습니다. 따라서 테스트는 변하는 문구가 아니라 변하지 않는 계약(contract)을 검증해야 합니다. 이 글은 AI 에이전트가 프로덕션에서 고장나는 이유에서 다룬 세 번째 실패 모드를 구현 관점에서 정리합니다.

temperature=0이 결정론적(deterministic)을 의미하지 않는가

온도는 모델이 다음 토큰을 샘플링하는 방식을 제어합니다. 온도가 0이면 모델은 일반적으로 가장 확률이 높은 토큰을 선택합니다. 하지만 이것만으로 재현 가능한 출력이 보장되지는 않습니다.

GPU의 부동 소수점 연산은 결합법칙이 성립하지 않습니다. 같은 숫자도 더하는 순서에 따라 마지막 소수점 자리가 달라질 수 있습니다. 이 작은 차이가 가장 높은 확률의 토큰을 바꾸면, 이후 생성되는 전체 문장이 달라질 수 있습니다.

이 연산 순서는 다음과 같은 요소의 영향을 받습니다.

  • 제공업체가 요청을 배치하는 방식
  • 요청이 실행되는 하드웨어
  • 배포된 커널 또는 추론 라이브러리 버전
  • 요청이 라우팅되는 리전
  • 가중치 양자화 또는 인프라 변경

긴 vLLM 토론에서도 고정된 시드와 temperature=0만으로 비트 단위 재현성을 보장할 수 없는 이유를 설명합니다.

핵심은 간단합니다. 결정론은 요청 옵션 하나의 속성이 아니라 전체 서비스 스택의 속성입니다. 따라서 테스트의 기준을 “항상 같은 문장이 나오는가”에서 “응답이 계약을 만족하는가”로 바꿔야 합니다.

정확한 문자열 어설션이 테스트를 불안정하게 만드는 이유

처음 응답이 다음과 같다고 가정해 보겠습니다.

Your order total is $42.00.
Enter fullscreen mode Exit fullscreen mode

그러면 쉽게 다음과 같은 테스트를 작성하게 됩니다.

assert(response === "Your order total is $42.00.");
Enter fullscreen mode Exit fullscreen mode

하지만 다음 실행에서 모델이 아래처럼 반환하면 어떻게 될까요?

Your total comes to $42.00.
Enter fullscreen mode Exit fullscreen mode

의미와 금액은 정확하지만 테스트는 실패합니다.

정답을 실패로 처리하는 테스트는 테스트가 없는 것보다 나쁠 수 있습니다. 팀은 실패를 거짓 경보로 취급하고, 통과할 때까지 재실행하며, 결국 실제 회귀를 놓치게 됩니다. 불안정한 테스트의 원인과 마찬가지로, 비결정론적 출력은 테스트 스위트에 대한 신뢰를 빠르게 떨어뜨립니다.

정확한 문자열 캡처, 텍스트 스냅샷, diff 비교를 늘리는 방식은 해결책이 아닙니다. 변할 가능성이 높은 요소에 테스트를 더 강하게 결합할 뿐입니다.

정확한 텍스트 대신 구조와 의미를 어설션하라

지원 에이전트는 환불 확인을 수백 가지 방식으로 표현할 수 있습니다. 하지만 유효한 응답이라면 다음과 같은 사실은 유지되어야 합니다.

  • 환불 금액
  • 주문 ID
  • 처리 상태
  • 필수 필드의 타입
  • 금지된 내부 정보의 부재

테스트 질문을 다음처럼 바꾸세요.

모델이 정확히 이 문장을 반환했는가?
Enter fullscreen mode Exit fullscreen mode

대신 다음을 확인합니다.

응답이 올바른 필드, 타입, 범위, 형식을 만족하는가?
Enter fullscreen mode Exit fullscreen mode

이 방식은 문구 변경에는 견고하면서도 누락된 필드, 잘못된 타입, 범위를 벗어난 값, 잘못된 도구 호출 같은 실제 회귀는 잡아냅니다.

1. JSON 스키마로 응답 유효성 검사

에이전트가 구조화된 데이터를 반환한다면 JSON 스키마를 먼저 정의하세요. 스키마는 특정 문구 대신 타입, 필수 필드, enum, 형식을 검증합니다.

예를 들어 환불 응답 계약은 다음과 같이 정의할 수 있습니다.

{
  "type": "object",
  "required": ["order_id", "status", "amount"],
  "additionalProperties": false,
  "properties": {
    "order_id": {
      "type": "string",
      "pattern": "^ORD-[0-9]+$"
    },
    "status": {
      "type": "string",
      "enum": ["refunded", "pending", "denied"]
    },
    "amount": {
      "type": "number",
      "minimum": 0
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

이 스키마는 다음과 같은 실패를 잡아냅니다.

  • status 필드 누락
  • amount를 숫자가 아닌 문자열로 반환
  • 허용되지 않은 상태값 반환
  • JSON 대신 일반 산문 반환
  • 예상하지 않은 추가 필드 포함

Apidog에 응답 스키마를 등록하고 실제 에이전트 응답을 검증하면, 단순한 문자열 diff 대신 어떤 필드가 계약을 위반했는지 확인할 수 있습니다.

2. 도구 호출의 형태와 대상을 검증

에이전트가 도구를 호출한다면, 호출을 유도한 자연어 문장이 아니라 도구 호출 자체를 테스트하세요.

다음 세 가지를 검증합니다.

  1. 올바른 도구를 선택했는가
  2. 올바른 대상과 HTTP 메서드를 사용했는가
  3. 페이로드가 도구 스키마를 만족하는가

예를 들어 예약 에이전트가 POST /reservations를 호출해야 한다면, 다음 계약을 검증할 수 있습니다.

{
  "type": "object",
  "required": ["guests", "date"],
  "additionalProperties": false,
  "properties": {
    "guests": {
      "type": "integer",
      "minimum": 1
    },
    "date": {
      "type": "string",
      "format": "date"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

테스트에서는 응답 문구가 아니라 외부 요청을 검증합니다.

expect(toolCall.method).toBe("POST");
expect(toolCall.path).toBe("/reservations");
expect(toolCall.body.guests).toBeGreaterThanOrEqual(1);
expect(toolCall.body.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
Enter fullscreen mode Exit fullscreen mode

에이전트 API 호출을 E2E로 테스트하는 방법에서 도구 스키마를 캡처하고 검증하는 흐름을 더 자세히 확인할 수 있습니다.

3. 정확한 값 대신 숫자 범위를 사용

숫자를 생성하거나 전달하는 응답은 정확한 숫자 하나가 아니라 유효 범위를 검증하세요.

예를 들어 장바구니 에이전트의 총액은 다음 조건을 만족해야 할 수 있습니다.

  • 음수가 아니다.
  • 장바구니 소계보다 작지 않다.
  • 소계와 최대 배송비 및 세금을 합한 금액보다 크지 않다.
const maxTotal = subtotal + maxShippingFee + maxTax;

expect(response.total).toBeGreaterThanOrEqual(subtotal);
expect(response.total).toBeLessThanOrEqual(maxTotal);
Enter fullscreen mode Exit fullscreen mode

이 어설션은 다음과 같은 중요한 오류를 잡아냅니다.

  • 음수 총액
  • 10배 이상 부풀려진 총액
  • 상품이 있는데 총액이 0인 경우
  • 숫자 필드가 누락되거나 잘못된 타입인 경우

같은 원칙을 신뢰도 점수, 항목 수, 토큰 사용량, 지연 시간 예산 같은 값에도 적용할 수 있습니다. 중요한 것은 가능한 한 넓은 범위를 쓰는 것이 아니라, 실제 버그를 놓치지 않는 가장 넓은 범위를 찾는 것입니다.

4. 필수 키와 금지 키를 함께 검증

응답 골격을 검증하는 가장 간단한 방법은 다음 두 가지입니다.

  • 필요한 키가 존재하고 null이 아닌지 확인
  • 절대 노출되면 안 되는 키가 없는지 확인

예를 들어 고객 지원 에이전트는 resolution을 반환해야 하지만, internal_notesraw_prompt를 고객에게 반환해서는 안 됩니다.

expect(response.resolution).toBeDefined();
expect(response.resolution).not.toBeNull();

expect(response).not.toHaveProperty("internal_notes");
expect(response).not.toHaveProperty("raw_prompt");
Enter fullscreen mode Exit fullscreen mode

이 검사는 표현 방식의 변화에 영향을 받지 않습니다. 동시에 내부 메모, 프롬프트, 운영 데이터가 응답에 섞이는 문제를 막는 기본 방어선이 됩니다.

5. 자유 텍스트에는 의미론적 및 임계값 검사 사용

응답이 반드시 산문 형태여야 하는 경우도 있습니다. 이때도 문자열 전체 일치는 피하고, 검증 가능한 속성을 분리하세요.

예를 들면 다음을 확인할 수 있습니다.

  • 전달한 주문 번호가 응답에 포함되는가
  • 최대 길이를 넘지 않는가
  • 금지 문구를 포함하지 않는가
  • 필요한 안내 문구를 포함하는가
expect(responseText).toContain(orderId);
expect(responseText.length).toBeLessThanOrEqual(500);
expect(responseText).not.toMatch(/internal policy|raw prompt/i);
Enter fullscreen mode Exit fullscreen mode

의미 자체를 비교해야 한다면, 참조 답변과의 임베딩 유사도를 계산하고 최소 임계값을 두는 방법을 사용할 수 있습니다.

expect(similarityScore).toBeGreaterThanOrEqual(0.82);
Enter fullscreen mode Exit fullscreen mode

다만 의미론적 유사도는 정밀한 사실 검증 도구가 아닙니다. 주제에서 완전히 벗어난 응답을 탐지하는 대략적인 게이트로 사용하고, 스키마·범위·필수 키 검증을 함께 적용하세요.

6. 정확한 텍스트가 아닌 스냅샷 범위를 저장

스냅샷 테스트가 항상 나쁜 것은 아닙니다. 다만 변하지 않는 부분만 스냅샷으로 고정해야 합니다.

다음과 같은 요소는 고정할 수 있습니다.

  • 키 집합
  • 필드 타입
  • 허용된 enum 값
  • 배열 길이 범위
  • 숫자 범위
  • 금지 필드의 부재

예를 들어 “응답 문장 전체” 대신 다음과 같은 계약을 스냅샷으로 관리합니다.

{
  "requiredKeys": ["order_id", "status", "amount"],
  "statusValues": ["refunded", "pending", "denied"],
  "amountRange": {
    "minimum": 0,
    "maximum": 10000
  }
}
Enter fullscreen mode Exit fullscreen mode

이제 스냅샷은 동의어 하나 때문에 깨지지 않습니다. 대신 구조적 변경, 필드 누락, 허용되지 않은 상태값처럼 검토할 가치가 있는 변경에만 실패합니다.

상태와 메모리가 테스트를 더 어렵게 만드는 이유

위 전략은 단일 요청과 단일 응답을 전제로 합니다. 하지만 에이전트는 여러 턴에 걸쳐 상태를 유지하고 메모리를 사용합니다.

상태를 가지는 에이전트의 답변은 다음에 따라 달라집니다.

  • 이전 턴에서 검색한 문서
  • 메모리에 저장한 요약
  • 도구 호출 순서
  • 이전 실행에서 남은 상태
  • 검색 결과의 순위

같은 대화라도 검색 결과 순위가 달라지거나, 이전 턴의 요약이 이후 추론에 영향을 주면 다른 결과가 나올 수 있습니다. 모델의 비결정론에 시작 상태의 차이까지 더해지는 것입니다. AI 에이전트 메모리가 작동하는 방식을 이해하면 어떤 상태를 고정해야 하는지 판단하기 쉬워집니다.

상태 기반 테스트에서는 다음 두 가지를 습관으로 만드세요.

테스트 시작 상태를 고정

각 테스트 전에 에이전트 메모리를 알려진 상태로 초기화하거나 시드하세요.

beforeEach(async () => {
  await agentMemory.clear();
  await agentMemory.seed({
    user_id: "test-user-1",
    cart: [{ sku: "SKU-001", quantity: 1 }]
  });
});
Enter fullscreen mode Exit fullscreen mode

이렇게 하면 실패 원인을 모델 출력 변화와 상태 변화로 동시에 추적하지 않아도 됩니다.

경로와 무관한 불변량을 검증

대화의 정확한 진행 경로 대신, 어떤 경로를 거쳐도 유지되어야 하는 불변량을 검증하세요.

예를 들면 다음과 같습니다.

expect(accountBalance).toBeGreaterThanOrEqual(0);
expect(createdReservations).toHaveLength(1);
Enter fullscreen mode Exit fullscreen mode

항공편 예약 대화가 몇 턴을 거치든 최종적으로 예약은 정확히 하나여야 합니다. 잔액은 어떤 도구 호출 순서에서도 음수가 되어서는 안 됩니다. 이런 경로 독립적 어설션이 상태를 가진 비결정적 에이전트에서 가장 오래 살아남습니다.

반복 가능한 테스트를 위해 의존성을 목(Mock)하라

실제 서드파티 API를 그대로 사용하면 테스트에 또 다른 변동성이 추가됩니다.

  • 속도 제한
  • 외부 데이터 변경
  • 일시적인 네트워크 오류
  • 응답 지연
  • 외부 시스템의 배포 변경

반복 가능한 테스트를 만들려면, 테스트 대상이 아닌 모든 의존성을 고정해야 합니다.

에이전트가 호출하는 API를 목으로 대체하고 고정된 응답을 반환하세요.

{
  "payment_id": "pay_test_001",
  "status": "paid",
  "amount": 42.0
}
Enter fullscreen mode Exit fullscreen mode

이제 결제 API는 항상 같은 영수증을 반환하고, 검색 API는 항상 같은 문서 세 개를 반환합니다. 변하는 요소는 에이전트의 판단과 도구 호출뿐이므로, 테스트가 관찰해야 하는 대상을 분리할 수 있습니다.

목은 정상 API가 쉽게 만들지 않는 엣지 케이스도 강제할 수 있습니다.

{
  "status": "declined",
  "reason": "insufficient_funds"
}
Enter fullscreen mode Exit fullscreen mode

이런 응답을 고정해 두고 에이전트가 사용자에게 올바른 대안을 제시하는지, 금지된 정보를 노출하지 않는지, 재시도 루프에 빠지지 않는지를 검증할 수 있습니다.

Apidog에서 에이전트 의존성의 목을 구성하고, 앞에서 정의한 응답 및 요청 스키마 어설션과 함께 사용하세요. 이는 목과 계약 검증을 결합하는 에이전틱 AI 테스트의 기본 패턴입니다.

Apidog의 적합한 사용처와 부적합한 사용처

도구의 역할을 분명히 해야 합니다. Apidog는 API 설계, 테스트, 목 플랫폼입니다. 에이전트 프레임워크, 모델 호스트, 에이전트 런타임, 추론 오케스트레이터 또는 평가·관찰 가능성 플랫폼은 아닙니다.

Apidog가 적합한 영역은 에이전트가 통신하는 API 계층입니다.

  • 응답 JSON 스키마 검증
  • 요청 및 응답 형태 검증
  • 숫자 범위 검증
  • 필수·금지 필드 검증
  • 도구 호출 페이로드 검증
  • 외부 의존성 API 목 구성

즉, Apidog가 다루는 대상은 요청과 응답의 계약입니다. 그 요청과 응답을 생성하는 모델 자체가 아닙니다.

문구가 아니라 계약을 테스트하라

비결정론은 설정 하나로 제거할 수 있는 버그가 아닙니다. 언어 모델을 운영할 때 받아들여야 하는 속성이며, temperature=0도 이를 완전히 제거하지 못합니다.

신뢰할 수 있는 에이전트를 배포하는 팀은 문구를 고정하려 하지 않습니다. 대신 다음을 테스트합니다.

  • 스키마
  • 필드 타입
  • 허용된 상태값
  • 숫자 범위
  • 필수 필드
  • 금지 필드
  • 도구 호출 계약
  • 상태 기반 불변량

이 접근을 사용하면 텍스트 표현이 바뀌어도 테스트는 통과합니다. 반대로 필드가 사라지거나, 값이 범위를 벗어나거나, 잘못된 도구가 호출되거나, 내부 정보가 노출될 때만 실패합니다.

이번 주에는 테스트 스위트에서 불안정한 문자열 어설션 하나를 골라 JSON 스키마, 범위, 필수 키 검증으로 바꿔보세요. 그리고 Apidog에서 에이전트 응답 계약을 검증하고 외부 의존성을 목으로 고정해 테스트를 반복 가능하게 만드세요.

Top comments (0)