
평가 스위트는 통과(Green)인데, 프로덕션은 그렇지 않다면
요약
LLM 서비스 배포 후 발생하는 예측 불가능한 실패를 해결하기 위해, 프로덕션의 실패 사례를 기반으로 회귀 테스트를 자동 생성하는 phoenix2pytest 도구를 소개합니다. 기존의 사양 중심 평가 방식에서 벗어나 실제 트레이스(Trace)를 기반으로 테스트를 구축하는 역방향 파이프라인을 제안합니다.
핵심 포인트
- 기존 평가 프레임워크는 상상 가능한 실패에만 대응하는 한계가 있음
- phoenix2pytest는 프로덕션 실패 트레이스를 기반으로 pytest 케이스를 자동 생성함
- Gemini 모델을 활용해 실패 증거 추출 및 실행 가능한 테스트 코드를 작성함
- 실제 발생한 환각 사례를 회귀 테스트 스위트로 전환하여 CI 단계에서 방어 가능
LLM 기능을 배포했습니다. 3주 후, Slack 스레드에서 한 고객이 이상한 답변을 받았다는 언급이 나옵니다. 조사를 시작합니다. 출시 이후 프롬프트(Prompt)는 두 번 수정되었고, 제공업체는 조용히 모델을 재양자화(Re-quantised)했으며, 이 중 그 어떤 것도 잡아낼 수 있는 테스트를 아무도 작성하지 않았습니다. 평가 스위트(Eval suite)는 여전히 통과(Green) 상태입니다. 내내 통과 상태였습니다.
저는 계속해서 이런 상황에 직면했고, 더 주의를 기울이는 것만으로는 해결되지 않았습니다. 이 격차는 구조적인 문제입니다.
평가는 앞을 향해 테스트하고, 프로덕션은 뒤를 향해 실패합니다.
제가 사용해 본 모든 LLM 평가 프레임워크(Eval framework)는 저에게 동일한 것을 요구합니다. 실패를 미리 예측하라는 것입니다. 모델에 대한 어설션(Assertion)을 작성하고, 몇 가지 지표(Metrics)를 첨부하여 실행한 뒤, 점수를 읽으라는 것입니다. 이는 당신이 차분하게 사양서(Spec)를 응시하며 상상할 수 있는 실패 모드(Failure modes)에는 효과가 있습니다.
하지만 방금 당신의 휴대폰으로 에스컬레이션(Escalated)된 실패에는 아무런 도움이 되지 않습니다. 아무도 그것을 상상하지 못했습니다. 그래서 배포된 것입니다.
좋은 날에는 당신의 상상력이 커버리지(Coverage)를 형성하지만, 나쁜 날에는 프로덕션이 실패를 형성합니다. 이 둘은 결코 일치하지 않으며, 에스컬레이션은 그 사이의 격차 속에 존재합니다.
| 평가 프레임워크 (Eval frameworks) | 내가 원했던 것 |
|---|---|
| 방향: 사양(Spec) → 평가(Eval) → 실행(Run) | 방향: 추적(Trace) → 실패(Failure) → 테스트(Test) |
| ... |
화살표의 방향을 바꾸세요
phoenix2pytest는 반대 방향으로 작동합니다. Arize Phoenix 프로젝트에서 추적(Traces)을 읽어와, 이미 실패로 표시된 것들을 가져와서, 그것들을 잡아낼 수 있었을 pytest 케이스를 작성합니다. 프로덕션 트래픽이 당신이 수동으로 번역할 필요 없이 회귀 테스트 스위트(Regression suite)에 공급됩니다.
이 파이프라인은 의도적으로 작게 설계되었습니다:
- 앱이 표준 OpenInference 인스트루멘테이션(Instrumentation)을 통해 Phoenix로 추적(Traces)을 방출합니다.
- Phoenix UI에서 수동으로 또는 Phoenix 평가(Evals)를 통해 잘못된 추적을 실패로 표시합니다.
- phoenix2pytest가 Phoenix MCP 서버를 통해 해당 주석이 달린 추적을 읽습니다.
- Gemini 2.5 Flash 단계에서 증거를 추출하고 어설션 전략(Assertion strategy)을 선택합니다.
- Gemini 2.5 Pro 단계에서 실행 가능한 pytest 파일을 작성합니다.
- 테스트를 리포지토리(Repo)에 넣으면, CI가 다음에 해당 회귀(Regression)가 발생할 때 이를 잡아냅니다.
/에서 하나의 트레이스(Trace), /batch에서 주석이 달린 트레이스 묶음(Batch)을 제공합니다. 배치 모드(Batch mode)는 실패 모드(Failure mode)별로 트레이스를 그룹화하고, 공유되는 것들을 하나의 매개변수화된 테스트(Parametrised test)로 접습니다. 따라서 동일한 형태의 환각(Hallucination) 10개가 발생하더라도 10개의 테스트가 아닌 하나의 테스트가 됩니다.
생성된 테스트의 모습
카탈로그에서 실제 실패 모드를 가져와 보겠습니다. 사용자가 특정 책의 특정 페이지에서 문구 그대로의 인용을 요청했는데, 모델이 이를 지어내는 경우입니다.
user_prompt: "'위대한 개츠비'의 47페이지에 나오는 대사 세 줄을 말해줘. 토씨 하나 틀리지 말고 인용해줘."
bot_response: "47페이지에서 닉은 이렇게 서술합니다: '빛은 점점 더 밝아지고..."
...
제공할 수 있는 47페이지의 인용구는 존재하지 않습니다. 올바른 동작은 거절하는 것입니다. 실패 지점은 특정하기 쉬운 조작된 문자열(Fabricated string)이므로, 생성된 테스트는 해당 문자열이 절대 다시 나타나지 않음을 단언(Assert)합니다.
import os
import pytest
...
LLM-as-judge(판단자로서의 LLM)도, 퍼지 점수(Fuzzy score)도 사용하지 않습니다. 환각이 다시 나타나는 순간 빨간색(Red)이 되고, 프롬프트나 모델이 더 이상 이를 생성하지 않게 되면 초록색(Green)이 되는 구체적인 문자열 단언(String assertion)입니다. 이를 일반적인 pytest에서 실행하면 됩니다.
이 도구는 6가지 실패 모드를 인식하며, 각 모드는 합성기(Synthesiser)가 작성 방법을 알고 있는 단언 전략(Assertion strategy)에 매핑됩니다:
- hallucination (환각) -> 조작된 문자열이 제외되었는지 단언
- format_break (형식 파괴) -> 응답이 호출자가 요구한 형태(마크다운 펜스 안의 JSON이 아닌 순수 JSON)로 파싱되는지 단언
- off_topic_drift (주제 이탈) -> 답변이 예상된 범위 내에 머무는지 단언
- stale_real_time_data (오래된 실시간 데이터) -> 지어낸 현재 사실이 제외되었는지 단언
- wrong_reasoning (잘못된 추론) -> 정답이 존재하는지 단언
- refusal_bug (거절 버그) -> 모델이 거절하는 대신 실제로 답변했는지 단언
이 실패 모드(failure mode)는 코드 내의 폐쇄 집합(closed set)이며, 단언 전략(assertion strategies) 또한 동일한 집합에서 파생되므로, 데모 데이터와 실제 라이브 파이프라인(live pipeline)이 우연히 서로 어긋날 수는 없습니다.
내가 그어둔 경계선
여러분이 고생하며 한계점을 직접 깨닫게 하기보다는, 차라리 기능을 과소평가하는 편을 택하겠습니다.
이 도구는 여러분이 이미 실패로 라벨링(labelled)한 트레이스(traces)를 대상으로 작동합니다. 가공되지 않은 프로덕션 트래픽(production traffic)을 스캔하여 무엇이 잘못되었는지 대신 결정해주지는 않습니다. 이는 의도된 설계입니다. 환각된 사실(hallucinated facts)에 대한 자동 실패 탐지는 신뢰할 수 없습니다. 분류기(classifier)가 모델이 틀린 것과 동일한 사실을 검증해야 하기 때문입니다. 수동 어노테이션(Manual annotation), Phoenix eval, 또는 휴리스틱(heuristic)이 데이터를 제공합니다. 판단이 중요한 지점에는 인간이 루프 안에(in the loop) 머뭅니다.
생성된 테스트는 시작점일 뿐, 완성본이 아닙니다. 커밋(commit)하기 전에 직접 읽어보아야 합니다. 이 도구는 실패를 일으켰던 것과 동일한 모델을 가정하므로, 교차 모델 회귀(cross-model regression) 테스트를 위해서는 명시적인 설정이 필요합니다. 또한 매칭된 트레이스(matched traces)를 메모리에 로드하는데, 이는 일반적인 실패 백로그(failure backlog)에는 적합하지만 수십만 개의 트레이스에는 적합하지 않습니다.
또한 모델이 동일한 사실을 다른 단어로 만들어내어 문자열 체크(string check)를 통과해버리는, 의역된 실패(paraphrased failures)는 아직 잡아내지 못합니다. 임베딩 유사도(Embedding-similarity) 단언은 로드맵상의 다음 단계입니다. 의미론적(semantic) 방식이 이미 작동하는 척하기보다는, 정직한 문자열 수준(string-level) 버전을 지금 출시하는 쪽을 택하겠습니다.
여러분의 평가 프레임워크(eval framework)의 경쟁자가 아닙니다
DeepEval, Opik, pytest-evals 또는 Langfuse를 사용하고 있다면, 그대로 사용하십시오. 해당 도구들은 여러분이 작성한 평가(evals)를 실행합니다. phoenix2pytest는 여러분이 목격한 실패로부터 테스트를 작성합니다. 방향이 다르고, 사고 모델(mental model)이 다릅니다. 출력물은 일반적인 pytest 파일이므로, 해당 도구들 중 무엇으로든 실행할 수 있으며, 혹은 pytest 자체만으로도 실행할 수 있습니다.
이 도구는 미래 지향적인 평가(forward-looking evals)를 대체하지 않습니다. 실제 실패가 두 번째 발생할 때까지 테스트로 전환되지 못하고 남겨지는 그 간극을 메워줄 뿐입니다.
직접 시도해보세요
phoenix2pytest는 MIT 라이선스이며, Cloud Run에서 예시가 미리 채워진 라이브 데모를 제공합니다. 따라서 Phoenix를 먼저 연결하지 않고도 Generate를 클릭하여 출력 내용을 읽어볼 수 있습니다. 준비가 되면 여러분이 직접 주석을 단 트레이스 (traces)를 대상으로 실행해 보세요.
Repo: https://github.com/golikovichev/phoenix2pytest
만약 프로덕션은 문제가 있는데 평가 스위트 (eval suite)는 통과(Green) 상태였다면, 바로 그 간극을 메우기 위해 이 도구가 만들어졌습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기