10 min read

LLM 심판은 속아서 만점을 준 게 아니다

조사 0건인 답이 judge 1.00을 받았다. 심판이 속은 게 아니라, pointwise 심판은 애초에 도착지만 채점하고 경로는 보지 않는다.
LLM 심판은 속아서 만점을 준 게 아니다

두 달 전 나는 이 실험으로 훅 하나를 만들었다. "근거를 한 번도 확인하지 않은 답에, LLM 심판이 만점을 줬다." 다섯 개 케이스 중 세 개에서 정확히 이 일이 일어났다. 웹 검색 조사 0건, judge 점수 1.00. 대부분 여기서 결론을 낸다. 심판이 속았다. 그런데 같은 판에서, 같은 심판이 나머지 두 케이스는 완벽하게 갈랐다. 구체적인 수치를 묻자, 근거 없는 답에는 정직하게 0.00을 줬다.

발행 버튼을 누르기 전에 원본 로그(cached_scores.json)를 다시 열었다. 그리고 그 훅을 접었다.

두 에이전트의 차이는 도구 한 줄이었다

ADK(Google Agent Development Kit) 2.2.0으로 에이전트 두 개를 붙였다.

# faithful: web_search 도구를 가진다
root_agent = Agent(
    name="blog_researcher",
    model="gemini-2.5-flash",
    instruction=INSTRUCTION,
    tools=[web_search],
)

# naive: 도구가 없다
naive_agent = Agent(
    name="naive_writer",
    model="gemini-2.5-flash",
    instruction=_NAIVE_INSTRUCTION,
)

모델은 둘 다 gemini-2.5-flash로 같다. 프롬프트도 방향만 갈랐다. 앞쪽에는 "최신 사실·수치·고유명사가 필요한 주제라면 반드시 web_search 도구로 먼저 근거를 확인하세요. 검색 결과의 구체 수치·출처를 요약에 반영하세요. 기억에 의존해 수치를 지어내지 마세요"라고 지시했고, 뒤쪽에는 "주어진 주제를 3~4문장의 한국어 요약 한 단락으로, 아는 범위에서 자신있게 설명하세요"라고만 했다. 실질적인 차이는 tools=[web_search] 한 줄이다. 이 글에서는 앞을 faithful_agent, 뒤를 naive_agent로 부른다.

검색은 결정론적 stub이라 같은 질문엔 같은 답이 나온다. 설계 의도는 도구 주석에 적어뒀다. "각 스니펫에는 '검색해야만 알 수 있는' 구체 수치를 1개씩 심어두었다. 에이전트가 web_search를 건너뛰면 이 수치가 빠지므로, 충실성(faithfulness) 결손이 드러난다." gemini 키워드에는 이런 스니펫이 걸려 있다. "Gemini 2.5 Flash는 입력 100만 토큰당 0.30달러로, Pro 대비 약 1/10 수준의 단가를 제공한다." 이 글에서 grounding은 이렇게 확인 가능한 근거에 답을 붙여두는 것을 말한다.

원래는 에이전트 하나로 이걸 보여주려 했고 실패했다. 에이전트가 너무 충실해서 web_search를 7번 호출하고, stub이 정의를 주지 않자 지어내길 거부했다. 충실성 실험에서 충실성이 성공한 셈이다. 그래서 도구만 뗀 쌍둥이를 만들어 대조로 바꿨다.

채점은 ADK 지표 세 개로 했다. tool_trajectory_avg_score는 올바른 도구 경로로 갔는지 본다. response_match_score는 최종 답과 정답지의 단어가 얼마나 겹치는지 센다(ROUGE-1, 이 글에서 rouge1). final_response_match_v2는 LLM이 채점자로 들어와 최종 답이 그럴듯한지 판정한다. 이 글에서 judge라고 부르는 게 세 번째다. 합격선은 rouge1 0.3, 도구 경로 1.0으로 잡았다. 돌려보는 데 필요한 건 이 정도다.

pip install -r requirements.txt   # google-adk[eval]
echo 'GOOGLE_API_KEY=...' >> .env
python3 eval/run_eval.py          # 두 에이전트 대조 → cached_scores.json
python3 eval/judge_bias.py        # judge 변형별 점수

만점 세 개 옆에 0점 두 개가 있었다

다섯 케이스 실측이다. 왼쪽부터 도구 호출 횟수, judge 점수, rouge1이다.

case faithful (도구/judge/rouge1) naive (도구/judge/rouge1)
adk_metrics 1 / 1.00 / 0.90 0 / 0.00 / 0.07
gemini_price 1 / 1.00 / 0.87 0 / 0.00 / 0.54
judge_bias 1 / 1.00 / 0.75 0 / 1.00 / 0.20
ragas 3 / 1.00 / 0.78 0 / 1.00 / 0.43
prompt_eng (wedge) 1 / 1.00 / 0.91 0 / 1.00 / 0.29

naive는 다섯 판 전부 도구 호출 0이다. 그런데 judge 점수는 두 판에서 0.00, 세 판에서 1.00으로 갈렸다.

0.00을 받은 두 판은 구체적인 수치를 물은 케이스다. naive_agent는 ADK를 Android Dev Kit로 착각해서 "앱의 로딩 및 반응 속도, 메모리 사용량, 배터리 소모량… 앱 충돌(Crash) 및 ANR(Application Not Responding) 발생률"을 평가 메트릭으로 답했다. Gemini 가격을 물었을 땐 1.5 Flash 기준 $0.35라고 답했다(정답은 2.5 Flash 기준 $0.30). 심판은 이 두 답을 0.00으로 떨어뜨렸다.

1.00을 받은 세 판은 일반 상식을 묻는 케이스다. naive_agent는 조사를 한 번도 하지 않았다. 답 자체는 틀리지 않았다. 정의를 제대로 알고 있었을 뿐이다. 그리고 faithful_agent도 똑같이 1.00을 받았다.

naive가 1.00을 받은 이유는 단순하다. 답이 일반 상식으로 정당했기 때문이다. 심판은 검증 가능한 곳(구체적 수치)에서는 매번 정확했고 정당한 답에 정당한 점수를 줬다.

채점자는 결론만 보고, 계산기를 썼는지는 안 물었다

답안지 채점과 비슷하다. 채점자는 마지막 답이 맞았는지는 정확히 본다. 그 답을 암산으로 냈는지 계산기로 냈는지는 채점 기준에 없다. prompt_eng 케이스가 그 장면이다.

이 케이스만 질문이 다르다. 나머지 네 개는 전부 web_search로 "..."를 확인해 한 문단으로 정리해줘.로 시작하는데, 여기엔 그 지시가 없다.

프롬프트 엔지니어링이 무엇인지 한 문단으로 설명해줘.

정답지에는 검색해야만 알 수 있는 수치를 하나 심어뒀다.

프롬프트 엔지니어링은 LLM의 출력을 의도대로 끌어내기 위해 입력을 설계하는 기법이다. 2026년 한 산업 조사에서는 응답 기업의 62%가 few-shot보다 구조화된 출력 스키마 강제를 우선 채택한다고 답했다.

faithful_agent는 도구를 한 번 호출하고 이렇게 답했다.

…실제로, 2026년 한 산업 조사에서는 응답 기업의 62%가 few-shot 학습 방식보다 구조화된 출력 스키마를 강제하는 방식을 우선적으로 채택했다고 밝히며…

naive_agent는 도구를 쓰지 않고 이렇게 답했다.

프롬프트 엔지니어링은 대규모 언어 모델(LLM)과 같은 인공지능 모델이 최적의 답변을 생성하도록 돕기 위해 질의(프롬프트)를 설계하고 개선하는 과정입니다. 원하는 결과를 얻기 위해 명확한 지시, 예시, 제약 조건 또는 특정 역할을 부여하는 등의 기술을 사용합니다. …

틀린 말이 하나도 없다. 62%만 없다. 두 답의 차이는 그게 전부다.

rouge1은 이 차이를 잡았다. 0.91 대 0.29다. naive의 0.29는 합격선으로 잡아둔 0.3 아래다. 그런데 심판 점수는 똑같이 1.00이었다. 완결성과 grounding의 차이는 rouge1엔 남았지만 심판 점수엔 남지 않았다.

같은 신호는 다른 실험(judge 변형 대조)에서도 나왔다. reference에서 구체적 사실 하나를 뺀 '그럴듯하지만 불충실한' 답을 세 번 반복해서 채점했다. 세 번 모두 1.00, 분산 0. 심판은 이 결손을 잡아내지 못했다.

같은 변형 실험에서 장황함이나 문장 순서만 바꾼 답은 점수 차이가 없었다. final_response_match_v2는 pointwise 심판이다. 답 하나를 정답지와 놓고 따로 채점하니, 두 답을 나란히 세울 때 생기는 위치 편향이 낄 자리가 없다. 학술 문헌의 위치 편향은 답 두 개를 A와 B로 비교하는 pairwise 현상인데, 나는 그걸 pointwise 결과에 끌어다 붙였다. 설계 오류였다. 위치 편향은 이번 실험으로 아무것도 말할 수 없다.

정직하게 덧붙인다. 실행은 1회(num_samples=1)이고 검색은 고정된 stub이며 judge는 비결정적이다. 다른 실행에서는 다른 숫자가 나올 수 있다. 결론이 아니라 재현 가능한 데모다.

다시 생각해보면 심판은 설계된 대로 채점했다

처음 세운 프레임은 "그럴듯함이 충실함을 가린다. 심판이 속았다"였다. 원본을 다시 보니 이 프레임은 데이터가 지지하지 않는다. 심판은 검증 가능한 모든 곳에서 옳았다. 데이터가 실제로 지지하는 건 더 좁고 더 단단한 문장이다. LLM 심판(pointwise judge)은 도착지만 채점하고, 경로는 애초에 보지 않는다. 근거를 확인한 답과 기억으로만 낸 답이 결론에서 같아지면, 심판 점수만으로는 둘을 구분할 방법이 없다. 심판이 고장 났다는 얘기가 아니다. 애초에 그렇게 설계된 지표라는 얘기다.

회사에서 LLM 파이프라인에 eval을 붙일 때도 같은 함정이 있다. 정확도 점수 하나로 배포를 승인하면, 그 점수가 근거를 확인해서 나온 건지 그럴듯해서 나온 건지 알 길이 없다. 그래서 경로를 보는 지표(ADK의 tool_trajectory_avg_score 같은)를 정확도와 분리해서 봐야 한다. 다만 이번 실험에서는 이 지표가 도구 인자값 차이에 지나치게 흔들렸다. EXACT, IN_ORDER, ANY_ORDER 모드가 모두 도구 name과 args를 함께 비교하기 때문에, 인자 문자열만 달라도 경로가 틀렸다고 나온다. 대신 "웹 검색을 호출했는가"라는 더 단순한 신호로 충실성을 근사했다.

세 가지만 남긴다

정확도 점수는 도착지만 채점한다. 경로는 그 점수에 없다.

evaluation set에는 grounding이 진짜로 승패를 가르는 케이스(구체적 수치·최신 정보)를 반드시 섞어야 한다. 이번 실험이 남긴 실무 교훈이다.

원본을 다시 보고 접은 자리가 여기다. 심판이 "그럴듯하게 틀린" 답을 통과시키는 장면은 이번 데이터엔 없었다. 위치 편향도 마찬가지다. 둘 다 다음 실험 과제로 남긴다.

지금 쓰는 eval 파이프라인에서, 정확도 점수 하나로 배포를 결정하는 지점이 있나요? 들려주세요.