20장. LLM 평가와 모니터링
LLM 을 채점자로
신메뉴 시식을 손님 대신 경험 많은 선배 요리사에게 맡길 수 있습니다. 빠르고 사람보다 값싸지만, 선배에게도 버릇이 있습니다. 양이 많은 접시를 좋아하거나, 자기가 만든 요리에 후하거나요. 그래서 처음 몇 번은 손님의 평가와 견주어 봅니다.
1번에서 무엇이 맞음이고 틀림인지 기준표를 줍니다. 2번에서 참조 답과 챗봇의 답의 핵심 사실을 먼저 적게 합니다. 3번에서 그 둘을 견줘 판정합니다. 4번에서 사람이 판정한 사례로 일치율을 재고, 충분히 높을 때만 씁니다. 선배 요리사가 LLM 채점자, 손님의 평가가 사람의 판정입니다.
채점자의 버릇
| 버릇 | 뜻 | 줄이는 법 |
|---|---|---|
| 참조 없이 그럴듯함에 끌림 | 지어낸 답도 자신 있게 쓰면 맞다고 한다 | 참조 답이나 근거 자료를 함께 준다 |
| 길이 치우침 | 길고 자세한 답을 더 좋게 본다 | "길이는 보지 않는다"를 기준표에 적고, 핵심 사실만 견주게 한다 |
| 위치 치우침 | 두 답을 견줄 때 앞에 놓인 것을 고른다 | 순서를 바꿔 두 번 묻는다 |
| 자기 선호 | 자기와 같은 모델의 답에 후하다 | 다른 모델로도 채점해 본다 |
채점 요청 만들기
판정을 JSON 스키마로 묶으면 "맞음", "틀림" 가운데 하나만 나옵니다(17장의 format). 판정 앞에 핵심 사실 칸을 두면, 모델이 먼저 사실을 꺼낸 뒤 판정합니다.
import json
schema = {"type": "object",
"properties": {"참조의_핵심": {"type": "string"}, "답의_핵심": {"type": "string"},
"판정": {"type": "string", "enum": ["맞음", "틀림"]}},
"required": ["참조의_핵심", "답의_핵심", "판정"]} # 칸의 차례대로 쓰게 된다
user = "질문: 대출 기간은 며칠이에요?\n참조 답: 대출 기간은 14일입니다.\n챗봇의 답: 대출 기간은 21일입니다."
# 결과: Ollama /api/chat 에 보낼 본문의 생김새(여기서는 보내지 않는다)
body = {"model": "qwen2.5:3b", "format": schema, "messages": [{"role": "user", "content": user}],
"options": {"temperature": 0}}
print(json.dumps(body, ensure_ascii=False)[:160], "...")코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
"type": "object" | JSON 스키마(schema, 짜임새 설계도)에서 답이 사전 꼴이어야 한다는 뜻이다 |
"properties": {...} | 사전에 들어갈 칸과 칸마다의 꼴이다. "string" 은 글자다 |
"enum": ["맞음", "틀림"] | enumeration(하나하나 늘어놓기)의 줄임말이다. 적어 둔 값 가운데 하나만 쓸 수 있다 |
"required": [...] | 반드시 있어야 할 칸이다 |
"format": schema | Ollama 에 "답을 이 스키마 꼴로만 내라"고 알리는 자리다 |
"options": {"temperature": 0} | 온도를 0 으로 두어 같은 요청에 되도록 같은 답이 나오게 한다 |
json.dumps(...)[:160] | JSON 글로 바꾼 뒤 앞 160글자만 잘라 찍는다 |
이 코드가 뜻하는 것
선배 요리사에게 시식 평가지를 건넨다고 생각해 봅니다. 평가지 맨 위에는 "기준 맛의 핵심", 그 아래에 "이 접시의 핵심"을 적는 줄이 있고, 맨 아래에는 "맞음, 틀림" 체크 칸만 있습니다. 여기서 평가지 양식이 schema, 체크 칸이 enum, 평가지를 건네는 일이 body 를 보내는 일입니다. 비유와 다른 점이 하나 있습니다. 사람은 평가지 여백에 낙서를 할 수 있지만, format 으로 묶으면 모델이 이 꼴을 벗어난 글을 내지 않습니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
schema = {...} | 핵심 두 칸과 판정 칸을 정한다 | 평가지 양식 |
user = "질문: ...\n참조 답: ...\n챗봇의 답: ..." | 채점할 재료를 한 글로 적는다. \n 은 줄바꿈이다 | 평가지에 붙이는 시식 메모 |
body = {...} | 모델 이름, 꼴, 메시지, 온도를 묶는다 | 평가지와 메모를 봉투에 담기 |
print(json.dumps(body, ...)[:160], "...") | 봉투 속을 앞부분만 보여 준다 | 봉투를 살짝 열어 보기 |
출력은 보낼 본문의 앞부분입니다. 이 블록은 Ollama 에 보내지 않으므로 판정은 나오지 않습니다. 실제로 보내고 받는 일은 실습 3에서 합니다.
작게 줄여서 눈으로 보기
채점자가 이 꼴로 답을 돌려주면, 받은 글을 사전으로 바꿔 판정 칸만 꺼내 씁니다. 아래 답은 설명하려고 지어낸 글이고, 모델이 만든 것이 아닙니다.
import json
# 채점자가 돌려줬다고 치는 답(설명하려고 지어낸 글)
reply = '{"참조의_핵심": "14일", "답의_핵심": "21일", "판정": "틀림"}'
result = json.loads(reply) # 글을 사전으로 바꾼다
print(result["판정"], "·", list(result))틀림 · ['참조의_핵심', '답의_핵심', '판정']| 결과 | 읽는 법 |
|---|---|
틀림 | 판정 칸의 값이다. enum 으로 묶으면 맞음, 틀림 둘 가운데 하나만 온다 |
['참조의_핵심', '답의_핵심', '판정'] | 사전의 칸 이름이다. 지어낸 답에 적은 차례 그대로 담겼다 |
json.loads 는 JSON 글을 파이썬 사전으로 바꿉니다. 꼴이 정해져 있으니 result["판정"] 처럼 칸 이름으로 바로 꺼낼 수 있고, 긴 설명 글에서 판정을 따로 찾아 읽을 필요가 없습니다.
주의할 점
- 작은 모델은 채점자로도 실수가 많습니다. 사람과 맞춰 본 일치율을 함께 적고, 중요한 판단은 사람이 표본을 다시 봅니다.
- 채점에도 호출 비용과 시간이 듭니다. 규칙으로 걸러지는 것은 규칙으로 하고, 남은 것만 채점자에게 보냅니다.