BE-commerce 프로젝트
약 10분 분량 AI 개발

AI 평가 점수와 실제 사용 가능성은 왜 다른가

목차

대사는 내 결제 기록과 PG가 보내주는 정산 파일을 하루치씩 대조해 안 맞는 건을 찾는 작업입니다. 그 불일치를 고객에게 알리는 초안을 모델에게 맡기고, 지어낸 숫자와 내부 용어 누출까지는 먼저 막아 뒀습니다.

남은 질문은 “그래서 이 초안을 고객에게 보여줘도 되는가”였고, 그 잣대가 네 번 나를 속였습니다.

자동 채점 (루브릭) 평균 4.97 / 5
사람의 블라인드 작성과 비교 편집 거리 중앙값 45.6%

둘 중 하나는 틀렸습니다. 이 글은 검사를 더 강하게 만드는 대신, 검사마다 책임을 좁혀간 기록입니다.

층무엇을 잡나걸리면
숫자 검증지어낸 숫자버린다. 고객을 잘못 인도한다
용어 사전 · 루브릭내부 용어, 빠진 요소, 아는 단정표시한다. 상담원이 고칠 수 있다
다른 계열 심판모르는 결함표시한다. 심판도 틀릴 수 있다

표본 한정. 이 글의 수치는 통계적 성능 평가가 아닙니다. 루브릭·가점은 대사 예외 4건으로 고쳤고, 그 4건은 이미 개발 세트라 홀드아웃이 아닙니다. 숫자는 전부 그 표본 위에서 돌린 탐색 실행으로 읽어야 맞습니다.

결론부터

  • 평균 4.97/5점은 초안이 발송 가능한지 증명하지 못했습니다. 사람이 블라인드로 작성한 답과 비교하면 편집 거리 중앙값이 45.6%였습니다.
  • 루브릭을 강하게 만드는 것만으로는 해결되지 않았습니다. 정상 문장을 놓치거나 모델이 모르는 고객 영향을 단정하는 부작용이 생겼습니다.
  • 숫자 검증은 실패하면 폐기하지만, 루브릭과 다른 심판은 경고만 남깁니다. 평가 장치도 틀릴 수 있기 때문입니다.

따라서 이 글의 결론은 “좋은 심판 모델을 고르면 된다”가 아닙니다. 평가기가 책임질 수 있는 오류와 사람이 확인해야 하는 오류를 분리해야 한다는 것입니다.

1. 루브릭은 발송 불가 초안에 4.97/5점을 줬다

표본 4건으로는 아무것도 결정할 수 없어서 정답 없이 채점하는 루브릭(reference-free evaluation)을 만들었습니다. 첫 결과가 평균 4.97/5점(5점 만점 기준 99.4%) 이었습니다.

그런데 사람에게 같은 사실만 주고 쓴 답과 비교해 보니 편집 거리 중앙값이 45.6%였습니다. 4건 중 2건은 절반 이상을 새로 써야 했습니다. 자동 채점과 사람이 갈렸습니다.

어떻게 쟀나 — 블라인드 답 없이는 초안을 안 보여줬다

재려면 사람이 초안을 얼마나 고치는지를 봐야 하는데, 초안을 먼저 보여주고 “고쳐보세요”라고 하면 눈앞의 문장에 끌려가서(앵커링) “고칠 게 없었다”와 “고칠 생각이 안 났다”가 구분되지 않습니다. 이 프로젝트의 대사 분류기가 자기 수치를 정확도가 아니라 “일치율” 이라고 부르는 이유도 같습니다.

그래서 순서를 뒤집고, 그 순서를 문서가 아니라 엔티티가 강제하게 했습니다.

① 사실만 보여준다 → 사람이 직접 답을 쓴다 ← 초안을 보기 전에
② 그 뒤 모델 초안을 공개합니다
③ 사람이 초안을 발송 가능하게 고칩니다

①을 건너뛰면 그 표본은 조용히 무의미해집니다. 서버도 같이 막아서 모델 초안은 1단계를 마치기 전까지 응답에 아예 실리지 않습니다(클라이언트를 믿고 감추면 개발자 도구로 뚫립니다). 순서 위반에 IllegalStateException을 던졌더니 500이 나갔습니다. 버그인지 의도된 차단인지 구분할 수 없어 설계된 거절로 보고 409로 매핑했습니다.

왜 이렇게 갈렸나 — 뜯어보니 상투어가 통과하고 있었다

id=14 루브릭 5/5 영향표현=['안내드리']
id=13 루브릭 5/5 영향표현=['안내드리']
...
상투적 마무리만으로 통과: 6/6

6건 전부가 “안내드리겠습니다” 한 마디로 통과했습니다. 어느 초안에나 붙고 고객에게 아무것도 알려주지 않는 문장입니다. 그대로 실었으면 “초안이 거의 완벽하다”고 적을 뻔했습니다.

2. 루브릭을 조였더니 맞는 답을 틀렸다고 했다

느슨한 쪽을 고치려고 상투어를 빼면서 그대로까지 같이 뺐습니다. 그러자 “청구 금액은 그대로 유지됩니다”라고 제대로 쓴 것을 못 알아봤습니다. 측정이 개선 0으로 나오고 하마터면 “2단계 수정은 소용없다” 고 결론 낼 뻔했습니다.

느슨하면 나쁜 것이 통과하고, 조이면 좋은 것이 떨어집니다. 이 축은 어느 쪽으로도 한 번에 맞지 않았습니다.

3. 고객 영향을 요구했더니 모델이 모르는 것까지 단언했다

다시 고치고 재니 만점이 나왔는데, 만점짜리 초안이 이렇게 썼습니다.

결제 내역은 외부 기록에만 확인되며, 저희 내부 기록에 해당 주문이 없습니다.
… 고객 영향은 없으며, 청구 금액은 100,000원으로 그대로 유지됩니다.

주문 기록조차 없는 건에 “청구가 멀쩡하다”고 단언합니다. 100,000원은 외부 기록에 있으니 숫자 검증을 통과하고, 영향 표현이 있으니 루브릭도 통과합니다. 둘 다 통과하는데 주장이 틀렸습니다. 숫자가 아니라 주장의 문제라 코드로는 못 잡습니다.

이걸 만든 것은 제 2단계 수정입니다. 루브릭이 “고객 영향을 말하라”고 요구하니 모델이 점수를 채우려고 알 수 없는 것까지 단언했습니다. 내가 올린 4.10 → 5.00 중 일부는 점수 따먹기였습니다.

고친 방법은 두 가지입니다.

  • 원인을 모르면 영향을 요구하지 않습니다. 요구할 것은 영향이 아니라 모른다는 사실입니다
  • 예시를 케이스별로 갈아 끼웁니다. 고정 예시가 “청구된 금액에는 변동이 없습니다”로 끝나 모델이 기록조차 없는 건에도 그 꼴을 복사했습니다. 고정 3개를 케이스에 맞는 것 1개 + 나쁜 예 1개로 늘리지 않고 바꿨고, “모른다고 쓰는 예시”를 기본으로 줍니다

4. 다른 계열 심판도 틀렸다

초안은 Qwen이 쓰는데 모델은 자기 계열 출력을 후하게 봅니다. 같은 계열을 심판으로 세우면 통과율만 오르고 아무것도 안 걸러집니다. Llama 계열이 이 편향을 안 보인다는 관측이 있어 llama3.1:8b을 골랐고, 설정을 잘못 넣으면 기동 시 경고하게 했습니다.

다른 계열 심판도 틀렸다. 루브릭을 그냥 통과한 초안 20건을 계열이 다른 심판이 16건(80%) 표시했고, 수정 후 11건(55%)까지 줄었지만 다시 재니 14건(70%)으로 늘어난 것이 오탐이었다

심판부터 검증했습니다. 좋은 예·나쁜 예·중립 예 셋을 줬더니 3종 모두 맞혔지만, 처음엔 판정과 이유가 어긋났고(“단정하고 있습니다”라면서 위반 아님) 이건 배선 확인이지 신뢰성 검증이 아닙니다. 정밀도·재현율을 잴 라벨 세트가 아직 없습니다.

그리고 심판이 나를 잡았습니다. 대사 예외 10건을 2회씩 돌린 초안 20건 중 16건(80%) 이 근거 없는 단정으로 표시됐습니다. 너무 많아 오탐을 의심했지만 오탐이 아니었습니다.

차액 8,888원의 원인을 확인 중입니다. … 고객 영향은 없으며, 청구 금액은 그대로 유지됩니다.

심판: “수수료로도 취소로도 설명되지 않는데 청구 유지라는 근거가 사실에 없다”

원인을 모르는데 결과를 장담합니다. 앞 절의 고침(원인을 모르면 영향을 요구하지 않기 + 케이스별 예시)을 넣고 다시 재니 16건(80%)에서 11건(55%) 으로 줄었습니다.

바꾸고 재니 이번엔 20건 중 14건(70%) 을 위반이라고 했습니다. 뜯어보니 오탐이었습니다.

초안: 저희 주문 내역에서 찾지 못해 확인 중입니다. 카드사 승인 내역을 보내주시면…
심판: “내부에 이 주문의 기록이 없다는 근거가 사실에 없다”

사실에 내부 없음이 명시돼 있습니다. 초안은 오히려 정확해졌는데 심판이 못 읽었습니다. 심판은 프롬프트에 민감하고 자기 계열 출력을 후하게 보는 편향(self-enhancement)도 보고돼 있습니다.

그래서 아는 결함은 코드로 셌습니다. 원인이 확정되지 않은 건에 “그대로 유지 / 변동이 없” 류가 있는가 하는 검사로 0/20 을 확인했습니다. 심판은 모르는 결함을 찾는 데 쓰고, 아는 결함은 코드가 지킵니다.

5. 필수 대신 가점으로 갈랐다

여기까지 고치고 나니 검사는 제 몫을 했는데, 좋은 모델과 작은 모델을 구분하지는 못했습니다. 그전에 “구독하고 있으니까 그걸로 쓰게 해도 괜찮아. 어차피 로컬에서 잠깐 테스트하는 건데”라는 요청을 받았는데, 앱이 구독 자격증명으로 모델을 부르는 건 기술적으로 안 됩니다(CLI는 OAuth 토큰, 어댑터의 Messages API는 x-api-key를 받습니다). 대신 프롬프트를 그대로 꺼내 외부에서 받은 초안을 되돌리는 통로를 열어 같은 검사를 태웠습니다.

루브릭은 항목 6개라 케이스당 만점이 6인데 8B와 프런티어가 둘 다 6/6 이었습니다. 그런데 읽어보면 같지 않았습니다.

필수 대신 가점으로 갈랐다. qwen3:8b와 프런티어가 필수 24/24로 동점이고, 가점 4 대 6과 안심 문구 0/4 대 3/4에서 갈려 합계가 28 대 30이 된다

지표qwen3:8b프런티어
안심 문구 (“추가로 청구되지 않는다”)0 / 43 / 4
내부 정보 노출 (정산 확정일 등)1 / 40 / 4

파일 지연, 8B: ”…확인 중이며, 정산 확정일은 2026-08-30입니다.” — 고객에게 의미 없는 내부 일정입니다.
원인 불명, 8B: ”…원인을 확인 중입니다. 결과가 확인되면 안내드리겠습니다.” — 추가 청구 여부를 말하지 않습니다. 고객이 가장 궁금해하는 게 그건데.

왜 못 잡는지가 문제입니다. 제가 그렇게 만들었습니다. “원인이 확정된 건에만 고객 영향을 요구하도록” 바꾼 대가로 “모르니까 아무 말도 안 한다”와 “모르지만 추가 청구는 없다고 약속한다”가 둘 다 통과합니다. 뒤엣것이 분명히 나은데도 그렇습니다. 필수로 걸면 모델이 지어내고, 안 걸면 잣대가 눈이 멉니다.

그래서 요구하지 않으면서 재는 가점을 달았습니다. 통과 여부는 안 바꾸고(가점이 없어도 6/6), 프롬프트에도 안 넣습니다(넣는 순간 그게 압력이 됩니다). 비교할 때만 필수 + 가점을 씁니다.

/** 비교용 점수. 같은 만점끼리 가르는 값입니다. */
public int comparable() {
return passed + bonus.size();
}

두 가점은 둘 다 실제로 차이가 관측된 축입니다.

“이미 청구된 것이 멀쩡하다” → 확인되지 않은 주장. 원인을 모르면 할 수 없습니다.
“앞으로 추가로 청구하지 않겠다” → 우리가 지킬 수 있는 약속. 원인을 몰라도 할 수 있습니다.

루브릭은 앞엣것을 막고(필수 항목), 가점은 뒤엣것을 권합니다. 같은 “고객 영향”인데 성격이 다릅니다.

모델필수가점합계
qwen3:8b24 / 24428
프런티어24 / 24630

원인 불명 2건에서 갈렸습니다. 8B는 “결과가 나오는 대로 안내드리겠습니다”에서 멈추고, 프런티어는 “확인이 끝날 때까지 추가로 청구되는 금액은 없으며”를 덧붙입니다. 프롬프트에 안 넣었으니 8B가 못 받은 가점은 진짜 차입니다.

그 사이에 프롬프트도 고쳐 봤다

초안 프롬프트를 네 차례 고쳤지만 전부 기준선보다 나빠졌고, 기준선이 이미 최적 배치였습니다. 되돌리고 기준선이 재현되는지까지 확인했습니다.

시도용어 누출 (4건 중)편집률
기준선0%45.6%
① 확신 등급별 어조 + 고객 영향 + 내부 절차 금지1건 (25%)60.7%
② ①에 용어 금지를 번호 규칙으로 승격(예시 명시)2건 (50%)63.2%
③ 되돌리고 고객 영향만 추가1건 (25%)59.2%
④ 사전을 프롬프트 끝으로 이동(추가 0)0건57.4%
되돌림0%45.2% ← 기준선 재현

찾아보니 문서화된 현상이었습니다. 트랜스포머는 프롬프트의 앞과 끝에 높은 주의를 주고 중간을 잃습니다(“lost in the middle”). 우리 사전이 정확히 중간이었고 지시를 더할수록 더 깊이 밀렸는데, 권고대로 사전을 끝으로 보내니(④) 이번엔 예시가 중간으로 밀렸습니다. 양쪽 끝은 두 자리뿐이라 둘 다 넣을 수 없었습니다.

여기서 남는 것은 프롬프트 요령이 아니라 개선했다고 생각한 변경을 되돌릴 기준선이 있었다는 쪽입니다.

최종

지표정의시작지금
루브릭 (항목 5 → 6개)초안 만족도4.10/5 (82%)6.00/6 (100%)
코드가 잡는 아는 결함원인 미확정인데 “그대로 유지” 류가 있는가도입 전0 / 20
심판이 잡는 미지 결함다른 계열 심판의 근거 없는 단정 판정16 / 20 (80%)11~14 / 20 (심판 오탐 섞여 신뢰 불가)
용어 누출내부 코드가 초안에 샜는가발생0%
지어낸 숫자숫자 검증기가 버린 건0%0%
지연 중앙값초안 1건 생성 시간5,894ms7,250ms¹

¹ 지연은 이 표에서 유일하게 나빠진 값입니다. 이 +23%는 다른 계열 심판과 가드를 더 얹은 비용이라 따로 적어 둡니다.

덤으로 나온 것 두 가지

금지어를 예시에 적으면 그대로 씁니다. 용어 누출이 0% → 10%로 재발했는데 샌 것은 PAID 세 건이 전부였고, PAID는 나쁜 예시 안에 두 번 있었습니다(“주문 상태 PAID 확인 중입니다”, “PAID 같은 내부 코드를 쓰지 마십시오”). 토큰만 가려서(주문 상태(영문 코드)) 다시 재 10% → 0% 를 확인했습니다.

포트에 기본 구현을 두면 어댑터가 조용히 꺼집니다. 외부 모델 어댑터는 테스트가 0건(로컬은 6건)이었고, revise()의 기본 구현이 Optional.empty()(“못 한다”)라 구현하지 않아도 컴파일됐습니다. 프로바이더를 그쪽으로 바꾸면 2단계 수정이 아무 오류 없이 멈춥니다. 루브릭이 5.00에서 4.10으로 돌아가는데 로그에는 아무것도 안 남습니다. 외부 API 응답 모양을 흉내내는 서버로 키 없이 9건을 고정했습니다.

마무리

처음에는 좋은 초안을 하나의 점수로 판정할 수 있다고 생각했습니다. 루브릭은 상투어에 속았고, 기준을 강화하자 정상 문장을 놓쳤습니다. 고객 영향을 요구하자 모델은 알 수 없는 것까지 단정했고, 이를 잡으려고 세운 다른 계열 심판도 실제 사실을 놓쳤습니다.

그래서 검사를 더 강하게 만드는 대신 각 검사가 책임지는 범위를 줄였습니다. 출처 없는 숫자는 코드가 버리고, 이미 아는 표현상 결함도 코드가 검사합니다. 좋은 답의 특성은 필수 조건이 아니라 비교용 가점으로 둡니다. 코드로 정의하지 못한 이상한 주장을 찾는 일만 LLM 심판에게 맡기되, 그 결과로 자동 차단하지 않습니다.

모델을 평가하는 모델까지 틀릴 수 있다면, 마지막 안전장치는 더 똑똑한 심판이 아니라 각 심사가 어디까지 결정할 수 있는지를 제한하는 것이었습니다.

남은 구멍

심판의 정밀도·재현율을 잴 라벨 세트가 아직 없습니다. 가점 항목도 결국 제가 골랐습니다. 관측된 차이를 축으로 만든 것이니 그 두 축에서는 프런티어 모델이 이기게 되어 있습니다. 새로운 차원의 차이는 여전히 못 봅니다. 넓히려면 사람이 초안을 읽고 “이건 왜 나은가”를 말해줘야 하는데 그 표본이 아직 없습니다.

남은 트레이드오프. 결제 데이터를 밖으로 보내는 설계는 못 쓴다 해놓고, 사람이 복사해 외부 모델에 넣을 통로는 기능으로 열었습니다. 주문번호·금액·거래일이 든 프롬프트를 “붙여 넣는 건 꺼낸 사람 책임”이라고 적어둔 게 전부라 앱이 직접 보내지 않았다는 건 방어가 안 됩니다. 이 편에서 제일 약한 자리입니다.

이어서 읽기

이 글에서 이어진 작업은 따로 적었습니다.

참고

프로필 사진
작성자 @범수

오늘의 노력이 내일의 전문성을 만든다고 믿습니다.

댓글

댓글 수정/삭제는 GitHub Discussions에서 가능합니다.