Pydantic Evals로 AI 분류 회귀검사 만들기: 오답과 CI 실패를 연결하는 법

AI 분류 회귀검사는 입력·기대 라벨·평가기를 Dataset에 묶고, 실패를 비정상 종료 코드로 연결하는 것부터 시작하면 됩니다. Pydantic Evals로 사례별 보고서를 만들되, 작업 예외와 평가 누락까지 확인하는 CI 검사를 별도로 둡니다.

고객 문의를 배송·환불·상담으로 나누는 AI 기능을 만들었다고 가정해 보겠습니다. 프롬프트를 조금 고쳤더니 문장은 자연스러워졌는데, “배송이 늦어서 환불하고 싶어요”가 배송 담당으로 넘어갑니다. 작은 팀에 먼저 필요한 것은 이런 경계 사례를 남기고, 다음 변경 때 다시 확인하는 평가 루프입니다.

Pydantic Evals는 Python 함수의 입력·기대 출력·평가 기준을 묶어 실행하고 보고서를 만드는 라이브러리입니다. 특정 에이전트 프레임워크로 제품을 바꾸지 않고도 기존 함수를 감싸서 쓸 수 있습니다. 이번 글에서는 API 키 없이 실행할 수 있는 분류 예제로 평가 구조를 만들고, CI를 실패시키는 조건까지 분리합니다. 프로젝트 설명

검증 범위: 2026년 10월 6일 기준 공식 문서·배포 메타데이터를 대조했습니다. 아래 코드는 문서 기반으로 작성한 예제이며, 이 글을 위해 패키지를 설치하거나 실행하지 않았습니다. 실행 시간·성공률·벤치마크 결과는 제시하지 않습니다.

먼저 확인할 패키지 정보

  • 설치 이름은 pydantic-evals, Python import 이름은 pydantic_evals입니다.
  • 확인한 최신 배포는 2.54.0이며 PyPI 업로드일은 2026-10-03입니다. PyPI 소유 조직은 Pydantic, 등록 관리자는 dmontagu입니다.
  • 라이선스는 MIT, 요구 Python 버전은 3.10 이상입니다. PyPI에는 Production/Stable로 분류되어 있으며 이번 예제는 이 정식 버전을 고정합니다.

유지보수 여부는 최근 배포 기록과 변경 내역으로 확인했습니다. 2.54.0에는 Pydantic Evals가 선언하지 않은 sniffio를 import하던 문제의 수정이 포함됩니다. GitHub 릴리스 제목에는 10월 2일이 적혀 있지만 게시 표시와 PyPI 업로드는 10월 3일이므로, 여기서는 PyPI 날짜를 기준으로 삼았습니다. 이 변경 내역은 Pydantic AI 모노레포 전체를 포함합니다. 다른 패키지의 신기능까지 Evals 기능으로 읽으면 안 됩니다. 배포 메타데이터 · 2.54.0 변경 내역

Case, Dataset, 평가 함수 세 가지부터

Case에는 입력과 기대 출력을 넣고, Dataset으로 사례를 묶습니다. 평가 대상은 입력 하나를 받아 결과를 반환하는 함수입니다. EqualsExpected()는 실제 결과를 기대 출력과 비교합니다. 우선 클래스 이름이나 추상화 계층을 더 만들지 않고, 문의 문장 하나에서 담당 라벨 하나를 반환하도록 좁힙니다. 공식 개요

격리된 가상환경에서 사용할 설치 명령은 다음과 같습니다. 운영 프로젝트에 적용할 때는 직접 의존성의 버전 고정과 함께 프로젝트의 잠금 파일도 관리합니다.

python -m pip install "pydantic-evals==2.54.0"

다음을 eval_ticket_router.py로 저장합니다. 예제의 규칙은 “환불 요청이 명시되면 배송 지연을 함께 언급해도 billing으로 보낸다”입니다. 아직 LLM은 호출하지 않습니다. 먼저 평가가 오류를 잡도록 일부러 순서가 잘못된 분류기를 넣었습니다.

from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import EqualsExpected


def route_ticket(text: str) -> str:
    # 교육용 기준선: 두 조건의 순서가 의도적으로 잘못되어 있다.
    if "배송" in text:
        return "shipping"
    if "환불" in text:
        return "billing"
    return "human"


dataset = Dataset(
    name="ticket-routing",
    cases=[
        Case(name="shipping", inputs="배송은 언제 오나요?",
             expected_output="shipping"),
        Case(name="refund", inputs="환불을 신청합니다",
             expected_output="billing"),
        Case(name="refund-priority", inputs="배송이 늦어서 환불하고 싶어요",
             expected_output="billing"),
        Case(name="unknown", inputs="담당자와 이야기하고 싶어요",
             expected_output="human"),
    ],
    evaluators=[EqualsExpected()],
)

# 이 평가 묶음에서는 모든 사례에 정답 라벨이 있어야 한다.
if not dataset.cases or any(c.expected_output is None for c in dataset.cases):
    raise ValueError("빈 데이터셋 또는 정답이 없는 사례가 있습니다")

report = dataset.evaluate_sync(route_ticket, max_concurrency=1)
report.print(include_input=True, include_output=True, include_durations=False)

# 필수 평가의 누락, 평가기 오류, 거짓 단언을 모두 확인합니다.
required = {"EqualsExpected"}
bad_cases = [
    c.name for c in report.cases
    if c.evaluator_failures
    or not required.issubset(c.assertions)
    or any(not result.value for result in c.assertions.values())
]
incomplete = len(report.cases) != len(dataset.cases)
if (report.failures or report.report_evaluator_failures
        or incomplete or bad_cases):
    print("분류 평가 실패:", bad_cases)
    raise SystemExit(1)

실행 명령은 python eval_ticket_router.py입니다. 코드의 분기만 따라가면 refund-priority는 shipping을 반환하고 기대값 billing과 어긋납니다. 이는 실행해서 얻은 측정값이 아니라 예제 로직에 대한 설명입니다. 문제를 수정하려면 함수에서 환불 조건을 배송 조건보다 먼저 검사합니다. 수정 후에는 같은 평가 파일을 다시 실행해 확인합니다.

보고서의 실패와 프로세스 종료를 구분하기

evaluate_sync()는 평가 보고서 객체를 반환합니다. 보고서에 실패가 보인다는 이유만으로 CI 프로세스가 원하는 종료 코드를 내줄 것이라고 가정해서는 안 됩니다. 공식 API에서 report.failures는 작업 함수가 예외를 낸 사례입니다. 정답과 다른 라벨을 정상 반환한 사례는 이것만 검사하면 놓칠 수 있습니다. 보고서 API

위 코드는 작업 예외, 평가기 예외, 결과 누락, 실패한 단언을 확인한 뒤 SystemExit(1)을 냅니다. 단언 값은 결과 객체 자체의 참·거짓이 아니라 result.value에서 읽습니다. required에는 반드시 실행되어야 하는 단언 이름을 넣었습니다. 빈 단언 집합을 두고 “실패가 없으니 성공”으로 처리하는 것도 막기 위해서입니다. 평가 결과의 구조 · 평가기 API

이 종료 정책은 “모든 필수 단언이 통과해야 한다”는 규칙입니다. 숫자 점수를 반환하는 평가기를 추가한다면 점수별 하한을 별도로 정해야 합니다. 필수 평가기의 이름이나 평가 방식을 바꿀 때는 required도 함께 수정합니다. 보고서 수준 평가기를 나중에 붙였을 때의 실행 오류를 놓치지 않도록 report.report_evaluator_failures도 검사합니다.

처음에는 로컬 터미널에서 의도적인 오답이 비정상 종료로 이어지는지 확인합니다. 그다음 기존 CI에 같은 Python 명령을 추가합니다. 실패를 무시하는 옵션이나 성공으로 바꾸는 셸 처리가 붙어 있지 않은지도 점검합니다. 평가 보고서는 읽을 수 있게 보관하되, 실제 문의의 개인정보가 CI 로그에 남지 않도록 입력을 비식별화합니다.

초록색 보고서가 놓칠 수 있는 정답 누락

EqualsExpected()는 expected_output이 None이면 비교를 생략합니다. 따라서 정답을 빼먹은 사례가 충분히 검사된 것처럼 보일 수 있습니다. 위 예제의 시작 검사는 이 허점을 막으려는 별도 정책입니다. 실제로 반환값 None을 확인하고 싶다면 공식 문서는 Equals(value=None)을 사용하도록 안내합니다. 문자열의 완전 일치가 필요한 라벨 분류와, 여러 표현을 허용해야 하는 자연어 답변도 구분해야 합니다. 내장 평가기와 None 처리

실제 LLM에 연결할 때 바꿀 부분

이제 route_ticket()의 본문을 실제 서비스 호출로 바꾸되 입출력 계약은 유지합니다. 모델 응답에서 라벨을 꺼내는 파싱 과정까지 평가 대상 함수에 포함해야 파싱 오류도 드러납니다. 단순 키워드 분류기는 평가 틀을 설명하기 위한 기준선이므로 실제 서비스 품질의 증거로 사용할 수 없습니다.

동시 실행도 명시합니다. 공식 문서상 기본값은 모든 사례의 동시 실행입니다. 위 예제는 흐름을 읽기 쉽게 max_concurrency=1로 제한했습니다. API를 연결한 뒤에는 제공자의 한도에 맞춰 조절하되, 동시 작업 수 제한이 초당 요청 수 제한이나 총비용 상한과 같지는 않습니다. 호출별 timeout과 예산은 사용하는 클라이언트 쪽에서도 설정해야 합니다. 동시 실행 가이드

확률적인 모델을 비교할 때는 동일 입력의 반복 실행이 도움이 됩니다. 공식 API의 repeat를 쓰면 반복 결과와 사례별 묶음을 얻을 수 있습니다. 다만 위 예제의 결과 개수 검사는 단일 실행을 전제로 합니다. 반복을 도입하면 예상 결과 수, 사례별 통과 조건, 허용 실패율을 함께 바꿔야 합니다. 반복 횟수만 늘리고 예전 종료 정책을 그대로 두지 않아야 합니다. 작은 사례 집합의 높은 점수가 운영 전체의 정확도를 보장하지도 않습니다. 반복 평가 가이드

Logfire 가입 없이 시작하고, 사례를 팀 자산으로 남기기

여기까지의 예제에는 Logfire 계정이나 클라우드 서비스가 필요 없습니다. 터미널 보고서만으로 시작할 수 있습니다. 추적·웹 시각화를 위해 Logfire를 선택할 수 있지만 필수 단계로 넣을 필요는 없습니다. 다만 “독립적으로 쓴다”는 표현을 의존성이 전혀 없다는 뜻으로 이해하면 곤란합니다. 2.54.0의 패키지 선언에는 pydantic-ai-slim과 logfire-api가 포함되고, 전체 logfire 패키지는 선택 의존성입니다. 2.54.0 패키지 선언 · Logfire 통합 안내

사례가 늘어나면 Python 코드와 데이터의 변경을 나눠 검토할 수 있습니다. Pydantic Evals는 Dataset의 YAML·JSON 저장과 읽기를 지원하고, 기본적으로 편집기용 JSON Schema도 생성합니다. dataset.to_file("tickets.yaml")로 저장한 파일을 버전 관리하면 “분류기를 고쳤는가, 정답을 바꿨는가”를 리뷰에서 구분하기 쉽습니다. 사용자 정의 평가기를 직렬화하려면 별도 등록 요건도 확인해야 합니다. 데이터셋 직렬화

작은 팀의 첫 완료 기준은 단순하게 잡을 수 있습니다. 최근 틀린 문의를 비식별 사례로 남기고, 의도적인 오답이 평가 보고서와 종료 코드 양쪽에 드러나는지 확인합니다. 그다음 프롬프트나 모델을 바꿀 때 같은 사례를 다시 돌립니다. 평가 도구를 도입한 효과는 그 반복이 실제 개발 과정에 들어갔을 때 생깁니다.

작성 안내: 이 글은 AI의 자료 조사와 편집 도움을 받아 작성했으며, 공식 문서와 배포 정보를 대조했습니다. 코드 실행 검증은 수행하지 않았습니다.

댓글