
LLM이 들어간 제품에서 답 문장을 기대값과 글자로 비교하는 E2E 테스트는 아무 결함이 없어도 실패합니다. 같은 질문에도 모델은 매번 조금씩 다른 문장으로 답하고, 도구를 부를지도 그때그때 정하기 때문입니다. 반대로 답이 비어 있지 않은지만 보는 테스트는 결함이 있어도 통과합니다. 모델이 「요청을 처리하지 못했습니다」라고 답해도 답은 비어 있지 않습니다.
OwlAI의 E2E 시나리오는 모델이 어떤 문장을 쓰든 달라지지 않는 사실만 확인합니다. 답이 하나뿐인 질문의 답 내용, 그 대화에 도구 호출이 있었다는 서버 기록, 사용량 원장에 늘어난 호출과 금액이 그 사실입니다. 이 글은 시나리오마다 무엇을 대조하는지와, 모델이 없어도 되는 검사를 어디로 떼어 냈는지를 설명합니다.
글자 비교도, 비어 있는지 확인도 판정이 되지 않는다
LLM 응답을 테스트하는 흔한 방법은 넷입니다. 저마다 장점이 있지만, 실제 모델이 도는 시나리오의 판정으로는 모자랍니다.
| 방법 | 장점 | 판정으로 쓸 때의 한계 |
|---|---|---|
| 기대 문장과 글자 비교 | 단순하고 실패 이유가 분명하다 | 결함이 없어도 문장이 달라지면 실패한다 |
| 답이 비어 있지 않은지 확인 | 어떤 모델에서도 통과한다 | 오류 문구가 나와도 통과한다 |
| 다른 LLM이 채점(LLM-as-a-Judge) | 뜻이 같은지를 볼 수 있다 | 채점하는 모델의 판단도 매번 달라지고 호출마다 비용이 든다 |
| 모델 응답을 모킹 | 빠르고 매번 같다 | 실제 모델과 프로바이더를 거치는 경로는 검증하지 못한다 |
모킹은 버릴 방법이 아니라 쓸 자리가 따로 있는 방법입니다. 실제 모델이 있어야만 성립하는 것과 그렇지 않은 것을 먼저 가르면, 실제 모델을 태우는 시나리오는 확인할 것이 크게 줄어듭니다.
모델이 없어도 되는 검사는 모델 없이 한다
모델의 답은 매번 달라도 모델에 보내는 요청은 매번 같아야 합니다. 프로바이더 어댑터의 단위 테스트는 프로바이더, 모델, 추론 강도(effort), 도구 유무, 스트리밍 여부의 조합마다 요청 본문 전체를 고정해 둔 값과 비교합니다. 필드 하나가 빠지거나 바뀌면 그 조합이 실패합니다. 권한 판정, 도구 인자 검증, 사용량 계산도 입력이 정해지면 결과가 정해지므로 같은 방식으로 증명합니다.
테스트를 어디에 둘지는 무엇이 떠 있어야 증명되는지로 정합니다.
| 증명에 필요한 것 | 두는 곳 | 도는 때 |
|---|---|---|
| 코드만 | 단위 테스트(모델 응답 모킹) | 머지 리퀘스트마다 |
| 실제 앱과 DB | 전용 PostgreSQL 위의 API 계약 테스트 | 머지 리퀘스트마다 |
| 워커·사이드카까지, 결과는 매번 같음 | 격리된 compose 풀스택 | 수동 |
| 화면이나 실제 LLM·외부 프로바이더 | E2E 시나리오(Playwright) | 매일 밤·매주 예약, 수동 |
HTTP를 쓴다는 이유만으로 E2E에 두지 않습니다. E2E에는 화면이 있어야 하거나 실제 모델이 답해야만 확인되는 시나리오만 남습니다. 아래는 그 시나리오들이 무엇을 대조하는지입니다.
답을 볼 때는 답이 하나뿐인 질문을 묻는다
채팅 기본 턴 시나리오는 「대한민국의 수도를 한 단어로만 답해」라고 묻고 답 본문에 「서울」이 있는지 봅니다. 어떤 모델이 어떤 문장으로 답해도 이 사실은 같습니다. 답이 비어 있는지만 볼 때와 달리, 모델이 오류 문구를 내면 이 시나리오는 실패합니다. 영어 계정으로 같은 질문을 영어로 묻는 시나리오는 답에 「Seoul」이 있는지와 답에 한글이 한 글자도 없는지를 함께 봅니다. 답의 언어도 정해진 사실로 확인할 수 있습니다.
거절은 문장 대신 빠져야 할 것이 빠졌는지로 봅니다. 허용받지 않은 DB의 급여 테이블을 묻는 시나리오는 세 가지를 확인합니다.
- 답에 사람이 읽는 거절 문구가 있다. 권한이 없다는 답과 그런 DB를 찾을 수 없다는 답은 둘 다 거절로 받는다.
- 답이 모델의 원시 JSON으로 시작하지 않는다.
- 시험 데이터에 든 급여 금액 같은 큰 숫자가 답에 없다.
마지막 항목이 이 시나리오의 핵심입니다. 거절 문장은 모델마다 다르지만, 데이터가 답에 실리지 않았다는 사실은 숫자가 있느냐 없느냐로 갈립니다.
도구를 불렀는지는 서버 기록과 화면을 맞춰 본다
도구를 부를지는 모델이 정합니다. 그래서 시나리오는 「도구를 불렀어야 한다」고 단언하지 않습니다. 턴이 끝나면 그 대화의 메시지를 API로 읽어 도구 호출이 있었는지 확인하고, 화면이 그 기록과 같은지 봅니다. 도구 호출이 있었다면 「Playbook 만들기」 링크가 눌려야 하고, 없었다면 링크가 꺼진 채 「도구 호출이 포함된 대화에서만 Playbook 을 만들 수 있습니다」라는 안내가 붙어야 합니다. 모델이 어느 쪽을 골라도 화면과 서버가 같으면 통과입니다.
모델이 특정 도구를 써야만 확인되는 기능도 있습니다. 모델이 사용자에게 보기를 주고 되묻는 카드가 그렇습니다. 이 시나리오는 프롬프트로 되묻기 도구를 쓰라고 지시하고, 모델이 도구 없이 바로 답하면 한 번 더 보냅니다. 실제 턴은 최대 두 번이고, 그래도 카드가 뜨지 않으면 실패입니다. 판정은 카드에 추천 보기가 미리 골라져 있는지, 그 보기를 제출하면 같은 턴이 그 답으로 끝나는지로 합니다.
비용은 사용량 원장으로 확인한다
턴이 과금됐는지는 사용량 원장에서 봅니다. 사용량 원장은 LLM 호출마다 모델·토큰·금액을 한 행씩 남기는 기록입니다(「LLM 비용 원장은 청구서와 맞추고, 캐시 토큰은 응답 형식으로 센다」). 시나리오는 턴 전후로 그 구성원의 오늘 LLM 사용 요약을 읽어 금액이 늘었는지 확인합니다. 시스템 키로 돈 호출에만 금액이 붙으므로, 금액이 늘었다면 원장에 행이 생겼고 시스템 키로 과금됐다는 뜻입니다.
채팅 기본 모델이 단가 0인 자체 호스팅 모델이면 금액은 늘지 않습니다. 이때 시나리오는 모델 이름으로 단가를 짐작하지 않고 응답 두 개로 판정합니다.
- 원장에서 턴 사이에 호출 수가 늘어난 모델 칸을 찾는다.
- 모델 선택지 응답에서 각 모델이 설치가 직접 띄운 모델인지 읽는다.
- 늘어난 칸이 전부 자체 호스팅 모델이고, 그 칸의 금액이 0이고, 토큰이 늘었으면 통과다.
외부 API 모델의 칸이 금액 없이 늘었다면 단가가 빠졌거나 시스템 키가 아닌 출처로 과금된 것입니다. 이 경우 판정은 보류에 머물다가 대기 시간이 끝나면 실패합니다. 원장 기록은 턴이 끝난 직후 따로 붙을 수 있어서, 시나리오는 30초 동안 원장을 다시 읽으며 기다립니다.
실행이 끝나면 실행기가 원장을 한 번 더 대조합니다. 실행기는 그 환경의 DB에 읽기 전용 세션으로 붙어 이번 실행에서 만든 대화 ID별로 호출 수와 금액을 집계합니다. 시스템 키로 외부 모델을 불러 토큰은 썼는데 금액이 0인 행이 있으면 무결성 위반으로 따로 셉니다.
모델이 되물으면 턴을 멈추고 그 질문을 남긴다
정보가 모자라면 모델은 답 대신 사용자에게 되물을 수 있습니다. 답이 그려지기만 기다리는 테스트는 이 경우를 180초 시간 초과로만 보고, 실패 원인이 가려집니다. 그래서 시나리오 도우미는 답이 그려지거나 되묻기 카드가 뜰 때까지 기다립니다. 카드가 떴으면 턴을 멈추고, 모델이 던진 질문을 실패 메시지에 실어 돌려줍니다.
턴을 멈추는 데는 이유가 하나 더 있습니다. 되물은 턴은 사용자 응답을 600초까지 기다리며 대기열 한 칸을 쥐고 있습니다. 그대로 두면 뒤에 오는 시나리오가 그 칸이 빌 때까지 기다릴 수 있습니다.
이어지는 질문을 보낼 때는 보내기 전의 마지막 답을 기억해 둡니다. 앞 답이 이미 화면에 있어서, 새 답을 기다리지 않고 바로 끝난 것으로 보는 일을 막기 위해서입니다.
판정은 실제 테스트 환경에서 하고, 배포된 커밋부터 맞춘다
E2E 판정의 기준은 구축형과 OwlAI SaaS 두 테스트 환경입니다. 매일 밤 야간 표시가 붙은 시나리오를 돌리고, 매주 전체를 돌립니다. 딥 리서치처럼 비싼 시나리오는 주간에만 돕니다.
실행기는 시나리오를 돌리기 전에 대상 환경에 떠 있는 커밋이 이 실행의 커밋과 같은지 확인하고, 다르면 시작하지 않습니다. 테스트 환경의 배포 작업과 E2E 실행은 같은 잠금을 나눠 써서, 시나리오 도중에 스택이 다시 만들어지지 않습니다. 실제 환경에서는 실패한 시나리오를 한 번 다시 돌리고, 다시 돌려 통과한 것은 통과와 따로 셉니다.
격리 스택에서도 시나리오 일부를 돌리지만 리허설로만 씁니다. 격리 스택은 자체 호스팅 모델로 돌아 금액이 늘 0이고, 외부 프로바이더와는 응답도 속도도 다릅니다.
품질은 따로 재고, SQL도 글자 대신 실행 결과로 비교한다
E2E 시나리오는 맞고 틀림이 분명한 조건만 봅니다. 자연어 질문에서 SQL을 만드는 품질처럼 점수로 재야 하는 것은 측정용 빌드에서 따로 잽니다. 공개 text-to-SQL 벤치마크의 문항을 채팅 턴과 같은 접수 경로로 돌리고, 결과를 턴 기록과 사용량 원장에서 읽습니다.
채점도 같은 원칙을 따릅니다. 같은 답을 내는 SQL은 여러 모양이라, 생성된 SQL의 글자 대신 실행 결과 데이터를 정답과 비교합니다.
- 컬럼 이름·순서와 질문이 요구하지 않은 추가 컬럼은 보지 않는다.
- 정답 SQL 맨 바깥에
ORDER BY가 있는 문항만 행 순서까지 비교하고, 나머지는 행 집합으로 비교한다. - 숫자는 소수 둘째 자리까지 맞춘다.
벤치마크의 공식 채점 규칙으로 낸 결과도 함께 내서 두 수치를 나란히 봅니다. 별칭 하나나 보조 컬럼 하나 때문에 맞는 답이 오답으로 세어졌는지는 두 수치의 차이로 드러납니다.
설계에서 고른 것
- 실제 모델 시나리오는 머지 리퀘스트마다 돌리는 대신 예약 실행에 둡니다. 머지 때마다 돌리면 모든 변경이 실제 모델로 검증되지만, 시나리오마다 비용과 수 분이 들고 외부 프로바이더 상태에 따라 결과가 흔들립니다. 머지 여부가 프로바이더 장애에 갈리게 둘 수는 없습니다. 대신 LLM 호출 경로(도구 루프, 커널, 프로바이더 어댑터)를 바꾼 변경은 실제 턴 한 번이 통과해야 머지합니다. 커널 전체를 모킹한 단위 테스트가 통과해도 실제 호출 경로가 도는지는 알 수 없기 때문입니다.
- LLM 채점 대신 정해진 사실을 대조합니다. 채점 모델은 뜻이 같은지까지 볼 수 있지만, 채점 결과가 매번 달라지면 시나리오가 실패했을 때 제품 결함인지 채점의 흔들림인지 가를 수 없습니다. 그 대가로 시나리오는 답의 품질을 보지 못하고, 품질은 앞 절의 측정이 따로 잽니다.
- 자주 실패하는 시나리오는 변경 전 상태와 대조해 판정합니다. 프로바이더의 일시 장애나 요금 한도로 실패한 것과 진짜 회귀를 가르려고, 변경 전 커밋을 같은 환경에서 다시 돌려 실패 목록을 비교합니다.
LLM을 부르는 기능을 E2E로 시험한다면, 먼저 모델이 정하는 것과 그 결정이 남기는 흔적을 짝지어 적어 보면 됩니다. 도구 호출은 서버 기록에, 과금은 원장에, 데이터 노출은 답에 든 숫자에 흔적이 남습니다. 시나리오는 문장 대신 그 흔적을 확인합니다.
설치·운영과 품질의 다른 글

엔지니어링 노트 · 설치·운영과 품질
설치 전 점검은 호스트를 읽기만 하고, 고칠 명령은 사람에게 넘긴다
구축형 OwlAI는 고객이 고른 호스트 한 대에 Docker Compose로 올라갑니다. 배포판과 커널, 도커 판, 보안 모듈, 인증서는 고객마다 다릅니다.

엔지니어링 노트 · 설치·운영과 품질
도커 주소 풀은 설치하는 사람이 정하고, 점검은 사내 대역과 대조한다
구축형 OwlAI를 기업망 호스트에 설치하고 몇 주 뒤, 어제까지 붙던 사내 DB나 추론 서버에 컨테이너에서 닿지 않는 날이 올 수 있습니다. 같은 호스트의 셸에서도 닿지 않습니다.

엔지니어링 노트 · 설치·운영과 품질
사내 프록시 뒤에서는 NO_PROXY를 대역 대신 이름과 IP로 적는다
사내 프록시로만 밖에 나갈 수 있는 기업망에 구축형 OwlAI를 설치하면, 클라우드 LLM으로 가는 호출은 HTTPSPROXY를 따라 프록시로 나가야 합니다.