20장. LLM 평가와 모니터링
실습 3 — LLM 채점자 만들고 사람과 맞춰 보기
사람이 맞음·틀림을 매겨 둔 답 18개(judge_cases.jsonl)로 LLM 채점자를 시험합니다. 답은 바른 답, 숫자가 틀린 답, 길고 틀린 답, 지어낸 답, 모름을 바르게 한 답으로 일부러 지어 썼고, 사람의 판정은 교안을 만든 사람이 매겼습니다. 채점자를 세 가지로 바꿔 가며 일치율을 견줍니다. 채점 모델은 답을 만든 것과 같은 qwen2.5:3b 입니다. 실습 1을 먼저 실행해야 합니다.
1단계. 준비
import json
import sys
from pathlib import Path
import pandas as pd
sys.path.insert(0, ".")
import out_libbot as bot # 실습 1의 chat() 을 쓴다
cases = pd.DataFrame([json.loads(line) for line in Path("judge_cases.jsonl").read_text(encoding="utf-8").splitlines()])
cases["사람판정"] = cases["사람"].map({1: "맞음", 0: "틀림"})
# 결과: 유형마다의 사례 수
print(cases["유형"].value_counts().to_dict())
def run_judge(system, schema, with_ref=True):
"""사례마다 채점자의 판정을 받아 돌려준다"""
out = []
for c in cases.itertuples():
user = f"질문: {c.질문}\n" + (f"참조 답: {c.정답}\n" if with_ref else "") + f"챗봇의 답: {c.답}"
out.append(bot.chat_json(system, user, schema)) # 깨진 JSON 이면 다시 묻는다(실습 1)
return pd.DataFrame(out){'바른 답': 6, '틀린 숫자': 4, '길고 틀린 답': 3, '모름을 바르게': 2, '지어낸 답': 2, '짧은 바른 답': 1}2단계. 참조 답 없이 채점하기
SIMPLE = {"type": "object", "properties": {"까닭": {"type": "string"}, "판정": {"type": "string", "enum": ["맞음", "틀림"]}},
"required": ["까닭", "판정"]}
J0 = "도서관 챗봇의 답을 채점합니다. 질문과 챗봇의 답을 보고, 질문에 알맞고 도움이 되는 답이면 맞음, 아니면 틀림입니다.\nJSON 으로 답하세요."
r0 = run_judge(J0, SIMPLE, with_ref=False)
cases["참조없음"] = r0["판정"]
# 결과: 사람과의 일치율, 그리고 지어낸 답·길고 틀린 답을 어떻게 판정했나
print(f"일치율 {(cases['참조없음'] == cases['사람판정']).mean():.3f}")
print(cases[cases["유형"].isin(["지어낸 답", "길고 틀린 답"])][["번호", "유형", "사람판정", "참조없음"]].to_string(index=False))일치율 0.444
번호 유형 사람판정 참조없음
4 길고 틀린 답 틀림 맞음
8 길고 틀린 답 틀림 맞음
10 지어낸 답 틀림 맞음
14 길고 틀린 답 틀림 틀림
16 지어낸 답 틀림 틀림3단계. 참조 답을 주고 채점하기
J1 = ("도서관 챗봇의 답을 채점합니다. 질문, 참조 답, 챗봇의 답을 봅니다.\n"
"- 챗봇의 답이 참조 답과 사실(숫자, 날짜, 시각)이 맞으면 맞음입니다. 참조 답에 없는 내용을 조금 덧붙인 것은 괜찮습니다.\n"
"- 숫자나 날짜가 다르거나, 사실과 다른 내용을 지어냈으면 틀림입니다.\n"
"- 참조 답이 '자료에 없다'이면, 모른다고 하고 안내 데스크를 알려 주어야 맞음입니다. 무언가를 단정해 답하면 틀림입니다.\n"
"- 답의 길이나 말투로 판단하지 않습니다.\nJSON 으로 답하세요.")
r1 = run_judge(J1, SIMPLE)
cases["참조"] = r1["판정"]
print(f"일치율 {(cases['참조'] == cases['사람판정']).mean():.3f}")
# 결과: 사람과 갈린 사례
print(cases[cases["참조"] != cases["사람판정"]][["번호", "유형", "사람판정", "참조"]].to_string(index=False))일치율 0.667
번호 유형 사람판정 참조
3 바른 답 맞음 틀림
7 바른 답 맞음 틀림
10 지어낸 답 틀림 맞음
13 짧은 바른 답 맞음 틀림
14 길고 틀린 답 틀림 맞음
18 틀린 숫자 틀림 맞음4단계. 핵심 사실을 먼저 적게 하기
FACTS = {"type": "object",
"properties": {"참조의_핵심": {"type": "string"}, "답의_핵심": {"type": "string"},
"판정": {"type": "string", "enum": ["맞음", "틀림"]}},
"required": ["참조의_핵심", "답의_핵심", "판정"]}
J2 = ("도서관 챗봇의 답을 채점합니다.\n"
"1. 참조 답에서 질문에 답하는 핵심 사실(숫자, 요일, 날짜, 또는 '자료에 없음')을 짧게 적습니다.\n"
"2. 챗봇의 답에서 같은 질문에 답하는 핵심 사실을 짧게 적습니다.\n"
"3. 두 핵심 사실이 같으면 맞음, 다르면 틀림입니다. 챗봇이 참조에 없는 내용을 덧붙인 것은 따지지 않습니다.\n"
" 참조가 '자료에 없음'인데 챗봇이 모른다고 하지 않고 무언가를 답했으면 틀림입니다.\n"
"길이와 말투는 보지 않습니다. JSON 으로 답하세요.")
r2 = run_judge(J2, FACTS)
cases["핵심비교"] = r2["판정"]
board = {name: (cases[name] == cases["사람판정"]).mean() for name in ["참조없음", "참조", "핵심비교"]}
print({k: round(v, 3) for k, v in board.items()})
# 결과: 핵심 비교 채점자가 사람과 갈린 사례와 그때 적은 핵심 사실
bad = cases["핵심비교"] != cases["사람판정"]
print(pd.concat([cases[bad][["번호", "유형", "사람판정", "핵심비교"]], r2[bad][["참조의_핵심", "답의_핵심"]]], axis=1).to_string(index=False))
print("다시 물음", bot.retried["수"]){'참조없음': np.float64(0.444), '참조': np.float64(0.667), '핵심비교': np.float64(0.944)}
번호 유형 사람판정 핵심비교 참조의_핵심 답의_핵심
7 바른 답 맞음 틀림 30분 30분, 1시간
다시 물음 6참조 답 없이 물은 채점자는 사람과 0.444 만 맞았습니다. 길고 틀린 답 둘(4, 8번)과 지어낸 와이파이 비밀번호(10번)를 맞았다고 했습니다. 참조 답을 주자 0.667 로 올랐지만, 덧붙인 내용이 있는 바른 답(3, 7번)과 짧은 바른 답(13번)을 틀렸다고 했습니다. 지어낸 답(10번), 길고 틀린 답(14번), 숫자가 틀린 답(18번)은 맞았다고 했습니다. 핵심 사실을 먼저 적게 한 채점자는 0.944(18개 가운데 17개)였습니다. 남은 하나(7번)는 답에 덧붙인 "1시간"까지 핵심으로 적어 틀렸다고 한, 엄격한 쪽의 실수입니다. 같은 모델이라도 무엇을 어떤 차례로 적게 하느냐가 채점의 질을 크게 바꿨습니다. 실행 중에 Ollama 의 오류나 깨진 JSON 으로 다시 물은 일이 몇 번 있었습니다.
이 실습이 뜻하는 것
이 실습은 새로 뽑은 보조 채점자를 정식으로 쓰기 전에, 선생님이 미리 채점해 둔 답안 18장으로 연습 시험을 보게 하는 일과 같습니다. 연습 답안을 준비하고(1단계), 모범 답안 없이 채점하게 하고(2단계), 모범 답안을 주고 채점하게 한 뒤(3단계), "모범 답안의 핵심과 답의 핵심을 먼저 적고 나서 판정하라"고 시킵니다(4단계). 여기서 보조 채점자가 LLM 채점자(qwen2.5:3b), 선생님의 채점이 사람 판정, 두 채점이 같은 비율이 일치율입니다. 사람 판정은 교안을 만든 사람이 매긴 것이고, 연습 답안도 일부러 지어 쓴 것입니다. 비유와 다른 점은, 보조 채점자는 같은 답안을 다시 채점하면 판정이 바뀔 수 있다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 준비 | 바른 답 6, 틀린 숫자 4, 길고 틀린 답 3, 모름을 바르게 2, 지어낸 답 2, 짧은 바른 답 1 | 유형을 골고루 섞은 연습 답안 18개입니다 |
| 2. 참조 답 없음 | 일치율 0.444 | 사람과 18개 가운데 8개만 같았습니다. 길고 틀린 답(4, 8번)과 지어낸 답(10번)을 맞음이라 했습니다 |
| 3. 참조 답 줌 | 일치율 0.667 | 12개가 같았습니다. 그래도 바른 답 셋(3, 7, 13번)을 틀림으로, 틀린 답 셋(10, 14, 18번)을 맞음으로 봤습니다 |
| 4. 핵심 먼저 | '핵심비교': 0.944 | 17개가 같았습니다. 판정 앞에 사실을 먼저 적게 한 것만으로 크게 올랐습니다 |
| 4. 남은 하나 | 7번 30분 과 30분, 1시간 | 답에 덧붙인 "1시간"까지 핵심으로 적어 틀림이라 했습니다. 너무 엄격한 쪽의 실수입니다 |
| 4. 다시 물음 | 다시 물음 6 | Ollama 오류나 깨진 JSON(JavaScript Object Notation, 데이터를 글로 적는 꼴)으로 다시 물은 횟수입니다 |
한 줄로 줄이면, 같은 모델이라도 무엇을 어떤 차례로 적게 하느냐에 따라 사람과의 일치율이 0.444 에서 0.944 까지 달라진다는 것을 보인 실습입니다. 가장 잘 맞은 핵심 비교 채점자를 실습 4에서 실제 답 44개에 씁니다. 다만 0.944 는 연습 사례 18개에서 한 사람의 판정과 견준 값입니다. 실제 답에서도 그만큼 믿을 만하다는 뜻은 아닙니다.
코드 설명
run_judge()는 사례마다 실습 1의chat_json()으로 채점 요청을 보냅니다. JSON 스키마로 묶인 답을 사전으로 읽고, Ollama 가 끊긴 JSON 을 돌려주면 모델을 내렸다 다시 묻습니다. 판정은enum때문에 "맞음", "틀림" 가운데 하나만 나옵니다.- 참조 답을 주지 않으면 채점자는 답이 그럴듯한지만 봅니다. 지어낸 답을 가려내려면 참조 답이나 근거가 있어야 합니다.
- 핵심 비교 채점자는 스키마의 칸 차례(
참조의_핵심→답의_핵심→판정)대로 씁니다. 판정 앞에 사실을 꺼내게 해 숫자를 견주도록 이끕니다. - 일치율은 채점자의 판정이 사람의 판정과 같은 비율입니다. 채점자를 쓰기 전에 꼭 재어 함께 적습니다.