월요일에 테스트는 통과했습니다. 동일한 입력, 동일한 코드, temperature=0. 화요일에는 실패했고, 당신은 아무것도 바꾸지 않았습니다. 어설션은 정확히 일치하는 문자열을 확인했지만, 모델은 약간 다르게 표현된 동일한 답변을 반환했습니다. 테스트는 실패했고(빨간색), 에이전트는 정상이며, 이제 당신은 제품 대신 테스트 스위트를 디버깅하고 있습니다.
이는 언어 모델을 호출하는 모든 것을 테스트할 때 치러야 할 대가입니다. 지시하지 않았음에도 불구하고 출력이 달라집니다. 온도를 0으로 설정해도 실행 간에 바이트 단위로 동일한 응답을 얻을 수는 없을 것입니다. 대부분의 개발자는 이 사실을 한 번 혹독하게 경험한 후 테스트 방식을 다시 작성합니다. 이 가이드는 기반 텍스트가 계속해서 변하더라도 견고하게 유지되는 어설션을 작성하는 방법을 보여줍니다. 이는 AI 에이전트가 프로덕션에서 고장나는 이유에 대한 저희 가이드에서 세 번째 실패 모드를 심층적으로 다룬 내용입니다.
왜 temperature=0이 결정론적(deterministic)을 의미하지 않는가
온도(Temperature)는 모델이 다음 토큰을 샘플링하는 방식을 제어합니다. 온도가 0이면 모델은 매번 가장 확률 높은 토큰을 선택하며, 이는 재현 가능해야 할 것처럼 느껴집니다. 하지만 그렇지 않으며, 그 이유는 모델 아래에 있습니다.
부동 소수점 연산은 GPU에서 결합법칙이 성립하지 않습니다. 동일한 숫자를 다른 순서로 더하면 마지막 소수점에서 약간 다른 결과가 나옵니다. 그 미세한 차이가 어떤 토큰이 가장 높은 순위를 차지하는지 바꿀 수 있으며, 하나의 다른 토큰은 그 이후의 모든 것을 변화시킵니다. 이러한 덧셈의 순서는 제공업체가 귀하의 요청을 다른 트래픽과 어떻게 묶는지, 어떤 하드웨어에서 실행되는지, 그리고 그날 어떤 커널 버전이 배포되는지에 따라 달라집니다. 당신은 이 중 어떤 것도 제어할 수 없습니다.
제공업체도 자체적으로 변경 사항을 적용합니다. 그들은 GPU를 교체하고, 추론 라이브러리를 업데이트하고, 가중치를 다시 양자화하며, 귀하의 호출을 다른 지역으로 라우팅합니다. 긴 vLLM 토론에서는 고정된 시드와 temperature=0으로도 비트 단위 재현성을 보장하기에 충분하지 않은 이유를 설명합니다. 요약하자면, 결정론(determinism)은 요청에서 설정하는 플래그가 아니라 전체 서비스 스택의 속성입니다.
그러므로 동일한 출력을 기준으로 삼는 것을 멈추세요. 모델은 이 실행에서 어떻게 표현되든 같은 의미를 갖는 답변을 제공합니다. 귀하의 테스트는 이를 받아들여야 합니다.
정확한 문자열 어설션은 테스트 스위트를 불안정하게 만듭니다
여기 함정이 있습니다. 처음 반환된 내용이 Your order total is $42.00.이어서 당신은 assert response == "Your order total is $42.00."라고 작성합니다. 테스트는 통과합니다. 그런 다음 모델이 “Your total comes to $42.00”를 반환하면, 올바른 답변임에도 불구하고 테스트가 실패합니다.
올바른 답변에 대해 실패하는 테스트는 테스트가 없는 것보다 더 나쁩니다. 팀은 이 스위트가 늑대 소년처럼 거짓 경보를 울린다고 생각하게 됩니다. 사람들은 통과(녹색)될 때까지 테스트를 다시 실행하고, 그러다 실패를 읽는 것을 멈추며, 결국 소음 속에 묻힌 실제 회귀 오류를 놓치게 됩니다. 불안정한 테스트(flaky tests)는 시간 낭비뿐만 아니라 전체 스위트에 대한 신뢰를 갉아먹으며, 저희는 이전에 불안정한 테스트의 원인과 확산 이유에 대해 글을 쓴 적이 있습니다. 비결정론적인 출력은 이러한 테스트를 만들어내는 가장 빠른 방법 중 하나입니다.
본능적으로는 출력을 더 강하게 고정하려고 합니다. 즉, 정확한 문자열을 캡처하고, 스냅샷을 만들고, 차이를 비교합니다. 하지만 이는 불안정성을 더욱 악화시킵니다. 변경될 것이 확실한 한 가지에 테스트를 연결했기 때문입니다. 정반대의 접근 방식이 필요합니다.
정확한 텍스트가 아닌 구조와 의미를 어설션하라
출력은 다양할 수 있지만, 그 아래의 계약은 변하지 않아야 합니다. 지원 에이전트는 환불 확인을 수백 가지 방식으로 표현할 수 있지만, 모든 유효한 응답은 동일한 사실(환불 금액, 주문 ID, 상태)을 포함합니다. 문구를 테스트하는 것이 아니라 사실을 테스트하십시오.
이것이 바로 전체적인 변화입니다. “모델이 정확히 이렇게 말했는가”라고 묻는 것을 멈추고 “응답이 올바른 형태, 올바른 필드, 올바른 범위의 값을 가지고 있는가”라고 묻기 시작하십시오. 이러한 속성은 재구성되어도 유지됩니다. 실제 회귀, 누락된 필드, 범위를 벗어난 숫자, 잘못된 형식의 페이로드 등은 여전히 어설션을 실패하게 만듭니다. 이를 실천하는 전략은 다음과 같습니다.
JSON 스키마에 대해 응답 유효성 검사
에이전트가 구조화된 데이터를 반환하는 경우, 해당 데이터에 대한 JSON 스키마를 정의하고 모든 응답을 그 스키마에 대해 검증하십시오. 스키마는 특정 값에 신경 쓰지 않고 타입, 필수 필드, 허용된 열거형(enum), 형식을 확인합니다. status 필드는 refunded, pending, denied 중 하나여야 합니다. order_id는 귀하의 ID 패턴과 일치해야 합니다. amount는 문자열이 아닌 숫자여야 합니다.
이는 비결정론적인 응답에 대해 작성할 수 있는 가장 강력한 단일 어설션입니다. 모델이 필드를 누락하거나, 객체를 잘못 중첩하거나, JSON을 예상한 곳에 산문(prose)을 반환하는 등 심각한 실패를 잡아내기 때문입니다. 응답 스키마를 Apidog에 로드하고, 에이전트의 실시간 응답을 스키마에 대해 검증하십시오. 불일치는 400자 문자열 차이가 아니라 정확히 어떤 필드가 문제인지 알려줍니다.
도구 호출이 올바른 형태와 대상을 가지는지 어설션하라
에이전트가 도구를 호출하기로 결정하면, 그 도구 호출을 유도한 문장이 아니라 호출 자체를 테스트하십시오. 다음 세 가지를 어설션하십시오: 올바른 도구를 선택했는지, 올바른 대상을 목표로 했는지, 그리고 페이로드가 도구의 스키마와 일치하는지. 예약 에이전트가 POST /reservations를 호출할 경우, 어떤 자연어 추론이 그 호출을 생성했든 관계없이 guests를 정수형으로, date를 유효한 날짜로 보내야 합니다.
이는 응답 본문을 검증하는 것과 동일한 원칙을 발신 요청에 적용하는 것입니다. 필수 매개변수가 존재하는지, 타입이 올바른지, 그리고 생성되지 않은 필드가 슬그머니 들어오지 않았는지 확인하십시오. 에이전트의 API 호출을 테스트하는 E2E(End-to-End) 방법은 이러한 도구 스키마를 캡처하고 그에 대해 어설션하는 방법을 다룹니다. 도구 호출 페이로드는 주변 문구가 바뀌더라도 계약을 가지고 있습니다.
정확한 값 대신 숫자 범위 사용
모델이 생성하거나 통과시키는 모든 숫자에 대해 값이 아닌 범위를 어설션하십시오. 장바구니 에이전트는 총액을 계산합니다. 모든 실행, 장바구니, 세금 규칙에 걸쳐 정확한 수치를 알 수는 없지만, 음수가 될 수 없고 장바구니 값에 최대 배송비와 세금을 더한 값을 초과할 수 없다는 것을 알고 있습니다. 따라서 응답에 0과 그 상한선 사이의 total이 포함되어 있는지 어설션하십시오.
이 단일 범위는 음수 총액, 10배 너무 큰 총액, 가득 찬 장바구니에 대한 총액 0과 같이 중요한 실패를 잡아내며, 신경 쓰지 않는 변동은 무시합니다. 범위는 신뢰도 점수, 항목 수, 토큰 사용량, 지연 시간 예산 및 모든 파생 수치에 적용됩니다. 실제 버그에 대해서는 여전히 실패하는 가장 넓은 범위를 선택하십시오.
필수 키가 존재하고 금지된 필드가 없는지 확인
두 가지 간단한 어설션이 큰 의미를 가집니다. 첫째, 당신이 의존하는 키는 존재하고 null이 아니어야 합니다. 둘째, 절대 나타나서는 안 되는 키는 없어야 합니다. 지원 티켓을 처리하는 에이전트는 resolution을 반환해야 하며, internal_notes나 raw_prompt 필드를 고객에게 유출해서는 안 됩니다.
존재 및 부재 확인은 응답의 내용이 아닌 골격(skeleton)을 테스트하므로, 재구성(rewording)에 영향을 받지 않도록 설계되어 있습니다. 또한, 모델이 비공개로 유지했어야 할 필드를 '친절하게도' 포함하여 발생하는 전체적인 개인 정보 유출에 대한 가장 저렴한 방어 수단이기도 합니다.
자유 텍스트에 대한 의미론적 및 임계값 확인 사용
때로는 페이로드가 산문(prose) 형태이고 여전히 테스트해야 할 때가 있습니다. 정확히 일치하는 방식은 작동하지 않으므로, 대신 속성을 확인하십시오. 회신에 당신이 전달한 주문 번호가 포함되어 있습니까? 길이 제한을 준수합니까? 사용자에게 절대 보내고 싶지 않은 문구의 금지 목록을 피합니까?
의미를 진정으로 테스트해야 할 때, 문자열이 일치하기를 요구하기보다는 참조 답변에 대해 임베딩 유사성을 비교하고 점수가 임계값을 넘는지 어설션하십시오. 이러한 의미론적 검사를 정밀한 게이트가 아닌 대략적인 게이트로 취급하십시오. 이들은 주제에서 벗어난 응답을 잡아냅니다. 미묘한 사실 오류는 잡아내지 못하므로, 위에 언급된 구조적 어설션과 함께 사용하십시오.
정확한 스냅샷이 아닌 스냅샷 범위
안정적인 부분을 스냅샷으로 찍는 한, 스냅샷 테스트는 여전히 유효합니다. 응답의 형태, 키 집합, 타입, 열거형 값을 고정하고, 자유롭게 변동하는 필드는 범위 내에서 변하도록 허용하십시오. 실제로 스냅샷은 정확한 텍스트의 고정된 덩어리 대신 “이 응답은 키 a, b, c를 가지며, b는 이 범위 내에 있고 c는 이 집합에서 나온다”는 것을 기록합니다. 스냅샷이 깨질 때는 동의어 때문이 아니라 검토할 가치가 있는 구조적 변경 때문에 깨지는 것입니다.
상태와 메모리가 이를 더 어렵게 만든다
위의 모든 내용은 하나의 요청에 하나의 응답이 나가는 것을 가정합니다. 에이전트는 그렇게 작동하지 않습니다. 그들은 턴(turn)을 넘나들며 메모리를 유지하며, 그 상태는 변동성의 원인을 증폭시킵니다.
상태를 가지는 에이전트의 답변은 이전에 무엇을 검색했는지, 무엇을 저장했는지, 그리고 이전 턴이 어떤 순서로 실행되었는지에 따라 달라집니다. 동일한 대화의 두 번의 실행은 검색 단계에서 문서 순위가 다르게 매겨졌거나, 2번째 턴에 작성된 요약이 5번째 턴의 추론에 영향을 미쳤기 때문에 달라질 수 있습니다. 이제 출력은 모델 자체의 비결정론과 다른 시작 상태라는 두 가지 복합적인 이유로 달라집니다. AI 에이전트 메모리가 작동하는 방식에 대한 저희 설명서는 해당 상태가 어디에 존재하며 어떻게 구축되는지 설명합니다.
두 가지 습관이 이를 테스트 가능하게 유지합니다. 첫째, 제어할 수 있는 상태를 제어하십시오. 각 테스트 전에 에이전트의 메모리를 알려진 시작점으로 시드하여, 두 가지가 아닌 한 가지만 변경하도록 하십시오. 둘째, 경로에 관계없이 유지되는 불변량(invariants)에 대해 어설션하십시오. 운영 잔액은 절대 음수가 되어서는 안 됩니다. 하나의 항공편을 예약한 대화는 몇 턴이 걸렸든 정확히 하나의 예약으로 끝나야 합니다. 경로에 독립적인 어설션은 상태를 가지며 비결정적인 에이전트에서도 살아남는 어설션입니다.
테스트 반복을 위해 의존성을 목(Mock)하라
이러한 것들을 실제 서드파티 API에 대해 연습할 수 없습니다. 그들은 속도 제한을 걸고, 데이터를 변경하며, 모델 외에 두 번째 무작위성(randomness) 원천을 추가합니다. 반복 가능한 테스트를 얻으려면, 테스트하려는 동작이 아닌 모든 것을 고정하십시오.
에이전트가 호출하는 API를 목(mock)하고 고정된 응답을 프로그래밍하십시오. 이제 결제 API는 항상 동일한 영수증을 반환하고, 검색 API는 항상 동일한 세 가지 결과를 반환하며, 유일하게 움직이는 것은 에이전트 자체의 추론이며, 이는 당신이 관찰하고 싶은 것입니다. 목(mock)된 의존성은 또한 정상적인 API가 요청 시 생성하지 않을 엣지 케이스를 강제하고, 에이전트가 이를 처리하는지 어설션할 수 있게 해줍니다. Apidog를 에이전트의 의존성에 연결하여 안정적이고 제어 가능한 본문을 가진 목(mock)을 설정하고, 위에 언급된 스키마 어설션과 함께 사용하십시오. 이는 목(mock)과 어설션이 함께 작동하는 에이전틱 AI 테스트의 더 넓은 범위의 실천 내에 있습니다.
Apidog의 적합한 사용처 (그리고 부적합한 사용처)
도구의 역할에 대해 명확히 하십시오. Apidog는 API 설계, 테스트 및 목(mock) 플랫폼입니다. 이는 에이전트 프레임워크, 모델 호스트, 에이전트 런타임 또는 평가 및 관찰 가능성 플랫폼이 아닙니다. 귀하의 에이전트를 구축하거나, 실행하거나, 단계를 조율하거나, 추론을 평가하지 않습니다.
Apidog가 담당하는 것은 에이전트가 통신하는 API 계층이며, 이러한 테스트가 존재하는 곳입니다. 두 가지 솔직한 적합성(fits)이 있습니다. 비결정론적인 출력을 견뎌내는 에이전트의 API 응답(스키마 유효성 검사, 응답 형태, 숫자 범위, 필수 및 금지 키, 도구 호출 페이로드 형태)에 대한 어설션을 작성합니다. 그리고 테스트가 두 번 실행되어도 동일한 방식으로 작동하도록 에이전트의 의존성을 목(mock)합니다. 이것이 Apidog가 채우는 부분입니다: 요청과 응답의 계약이지, 그것들을 생성하는 모델이 아닙니다.
문구가 아닌 계약을 테스트하라
비결정론은 설정을 통해 없앨 수 있는 버그가 아닙니다. 이는 언어 모델을 실행하는 속성이며, temperature=0이 이를 끄지 않습니다. 신뢰할 수 있는 에이전트를 배포하는 팀은 이에 맞서 싸우는 것을 멈췄습니다. 그들은 스키마, 형태, 범위, 필수 필드와 같이 변하지 않는 것들을 테스트하고, 문구는 변하도록 둡니다. 이렇게 하면 테스트 스위트가 좋은 의미에서 조용해집니다: 텍스트가 변동해도 녹색(통과)을 유지하며, 무언가 고장났을 때만 빨간색(실패)으로 바뀝니다.
이번 주에 테스트 스위트에서 불안정한(flaky) 어설션 하나를 골라 스키마 및 범위 확인으로 다시 작성하십시오. Apidog를 다운로드하여 계약에 대해 에이전트의 응답을 검증하고, 테스트를 반복 가능하게 만드는 의존성을 목(mock)하십시오.
