-
LLM이 다른 LLM의 답을 채점하는 법 — judge prompt·rubric.json·3가지 안티패턴IT 2026. 5. 29. 22:00
"검증을 LLM으로 한다"고 결정한 다음부터가 진짜 문제다. 어떤 모델을 부를지, 비용을 어떻게 줄일지는 별도 글에서 다뤘다. 이 글은 그 모델에게 무엇을 어떻게 물어볼 것인가 — system prompt 구조, rubric.json 설계, 그리고 LLM-as-judge가 빠지기 쉬운 함정들에 대한 기록이다.
같은 페이지를 같은 모델에 넣어도 prompt가 다르면 점수가 1.5~2점씩 들쭉날쭉하다. 그 변동이 검증 임계값(8.0)을 가로지르면 같은 콘텐츠가 어떤 날은 통과하고 어떤 날은 hold queue로 떨어진다. "검증의 신뢰도"가 무너지는 순간 검증 시스템 자체가 신뢰를 잃는다. prompt와 rubric은 그 변동을 묶어 두는 베이스라인이다.
1. 무엇을 채점할 것인가
먼저 결정해야 할 건 "코드와 일치하는가"를 LLM에게 물어보는 것이 아니다. 그건 결정적(deterministic) 검증의 영역 — anchor가 실제 좌표인지, 함수 시그니처가 코드와 같은지는 grep과 AST로 확인하는 게 빠르고 정확하다. LLM judge에게 그걸 시키면 환각으로 노이즈가 들어간다.
LLM이 사람보다 잘하는 건 "이 prose가 의미가 통하는가, 도메인 용어를 올바르게 쓰는가, 다음 단락과 논리적으로 이어지는가" 같은 의미 차원의 판단이다. 그래서 deep-wiki의 judge는 4개 차원만 본다.
- readability — 문장이 명확하고 흐름이 끊기지 않는가
- terminology — 도메인 용어가 일관되고 올바르게 쓰였는가
- completeness — 자리표시자(
TODO·placeholder)나 빈 섹션이 남아 있지 않은가 - structure — 헤딩 깊이·다이어그램 풍부도가 페이지 유형에 맞는가
코드 일치성·anchor 정합성·schema 검증 같은 차원은 결정적(deterministic) 검증이 따로 맡는다 — grep과 AST로 코드와 위키를 비교하면 LLM 호출 없이 정확하게 잡힌다. LLM judge는 그 결정적 검증이 통과한 뒤 마지막 의미 차원만 본다. LLM에게 자신 있는 일만 시킨다는 원칙이 이 분리의 핵심이다.
2. judge prompt 해부 — system / rubric / schema
위 다이어그램이 보여 주는 핵심은 system prompt와 user prompt의 분리다. system prompt에는 매 호출마다 같은 것만 — 역할 지시, rubric, 출력 schema, few-shot 예시. user prompt에는 호출마다 바뀌는 것만 — 페이지 본문, 유형 메타, 직전 결정적 검증 결과. 이 분리가 두 가지를 한꺼번에 가능하게 한다. (1) Anthropic의 prompt caching이 system prompt 부분을 캐싱해 토큰 비용을 90% 깎고, (2) rubric 갱신이 한 자리에서 일어나 모든 호출이 즉시 새 rubric을 따른다.
실제 system prompt를 풀어 보면 다음과 같다. role과 task가 가장 위에 있고, rubric은 JSON으로 inline 박혀 있다. JSON으로 박는 이유는 LLM이 JSON 안의 구조를 다른 자연어 prose보다 일관되게 따르기 때문 — "guidelines"라고 산문으로 적으면 모델이 자기 판단으로 무시하기 쉽다.
# scripts/validate_with_claude.py — judge system prompt 조립 SYSTEM_PROMPT_TEMPLATE = """\ 너는 코드 위키 페이지의 의미 차원을 채점하는 검증자다. 다음 4개 차원에 대해 1-10 정수 점수를 매기고, 가중 평균으로 composite를 산출한다. 출력은 반드시 지정된 JSON schema를 따른다 — 다른 prose를 덧붙이지 않는다. [rubric] {rubric_json} [output schema] { "dims": {"readability": int, "terminology": int, "completeness": int, "structure": int}, "composite": float, "reason": str, "verdict": "pass" | "hold" } [few-shot example 1 — pass] {example_pass} [few-shot example 2 — hold] {example_hold} 채점 가이드: - composite >= 8.0 이면 verdict="pass". 그렇지 않으면 "hold". - reason은 한 문장 (40자 이내). 가장 영향이 큰 차원과 이유만. - 확신이 없는 차원은 5점이 아니라 '판단 불가'로 reason에 표시한다. """ def build_system_prompt(rubric_path: Path) -> str: rubric_json = rubric_path.read_text() return SYSTEM_PROMPT_TEMPLATE.format( rubric_json=rubric_json, example_pass=PASS_EXAMPLE, example_hold=HOLD_EXAMPLE, )코드의 마지막 두 줄짜리 함수가 시사하는 건 rubric과 prompt의 분리다. rubric은 외부 JSON 파일(
~/projects/deep-wiki/rubric.json)에서 읽어 와 system prompt에 inline으로 박는다. 채점 기준을 손보고 싶으면 rubric.json만 수정하면 되고, prompt 자체는 거의 안 건드린다. 그래서 rubric 변경의 부수 효과(임계값 분포가 흔들리는 등)를 추적하기 쉽다.3. rubric.json — 4 차원 · 가중치 · anchor 예시
먼저 용어부터. rubric은 채점 기준표를 가리키는 영어 단어다. 학교 시험의 채점 기준이 "논리 전개 4점, 근거 2점, 표현 2점" 같은 형태로 적혀 있듯, LLM 채점에서도 "어떤 차원을, 몇 점까지, 무슨 의미로 매길지"를 글로 적은 표가 필요하다. 그게 rubric이다. 그리고 그 중에서 1점·5점·10점이 각각 어떤 페이지를 의미하는지 구체 예시로 박아 둔 것을 anchor 예시라고 부른다 — 모델이 자기 점수를 절대적 anchor에 비춰 보정할 수 있게 만드는 장치다.
rubric에서 가장 중요한 한 가지는 차원의 개수가 아니라 이 anchor 예시다. LLM-as-judge 연구들이 일관되게 가리키는 사실 — "가독성 0-10점으로 매겨라"만 주면 모델이 6-8 범위에 점수를 몰아 넣는다(scale collapse). 각 차원의 1점·5점·10점이 어떤 페이지인지 anchor를 함께 박으면 모델이 자기 판단을 그 anchor와 대조하면서 분포가 다시 펴진다.
// ~/projects/deep-wiki/rubric.json — 4 차원 채점표 { "version": "2026-05-17.1", "scale": [1, 10], "composite_formula": "weighted_mean", "dims": { "readability": { "weight": 0.30, "anchors": { "10": "문장이 짧고 한 단락이 한 가지만 말한다. 도메인 용어를 처음 쓸 때 1-2문장 정의가 따른다.", "5": "전반적으로 읽히지만 한두 단락이 너무 길거나 주어가 모호하다.", "1": "한 문장이 3줄 이상이고 무엇을 말하려는지 두 번 읽어도 모호하다." } }, "terminology": { "weight": 0.30, "anchors": { "10": "프로젝트 용어집과 100% 일치. 약어는 처음 등장 시 풀어 쓴다.", "5": "대체로 일치하지만 같은 개념을 두 가지 용어로 번갈아 쓴다.", "1": "도메인 용어가 잘못 쓰였거나 일관성이 없어 의미가 흐려진다." } }, "completeness": { "weight": 0.25, "anchors": { "10": "TODO/placeholder가 없고 헤딩별 본문이 모두 작성됨.", "5": "본문은 있지만 1-2개 섹션이 한 줄짜리 stub.", "1": "여러 섹션이 비어 있거나 TODO/lorem ipsum이 남아 있다." } }, "structure": { "weight": 0.15, "anchors": { "10": "페이지 유형(architecture/module/runbook)에 맞는 헤딩 구조 + 다이어그램 1개 이상.", "5": "헤딩 구조는 맞지만 다이어그램이 없거나 코드 블록만 가득.", "1": "헤딩 위계가 무너졌거나 type-appropriate 섹션이 누락." } } } }가중치는 readability·terminology가 각각 0.30, completeness가 0.25, structure가 0.15다. 의미 차원(가독성·용어)이 60%, 형식 차원이 40%로 묶여 있다. LLM이 정량 형식(헤딩 수, 다이어그램 수)보다 의미 차원에서 더 신뢰할 만한 판단을 한다는 가정 위에 가중치가 설계됐다 — 형식은 결정적 검증이 더 잘 잡는다.
또 하나 핵심 디테일은 각 차원의 점수가 가중 평균에 들어가기 전에 5점 이하 절단 규칙이 한 줄 더 있다는 점이다. 어떤 차원이라도 3점 이하면 composite를 무조건 7.0 이하로 강제한다. 차원 하나가 망가지면 평균이 가려 주는 일을 막는다 — readability 10, terminology 10, completeness 10, structure 2인 페이지가 평균 8.0으로 통과되면 곤란하다.
4. 안티패턴 셋 — LLM judge가 빠지기 쉬운 함정
LLM-as-judge의 실패 모드는 의외로 예측 가능하다. LLM-as-judge 연구들이 반복해서 지적해 온 세 가지가 있고, deep-wiki에서도 실제로 같은 순서로 겪었다.
세 안티패턴은 각자의 mitigation이 prompt·rubric·아키텍처 세 층에 흩어져 있다. scale collapse는 rubric에 anchor 예시를 박는 것으로 해결되고, length bias는 페이지 유형별 길이 정책을 rubric에 명시해 보정한다. self-evaluation trap은 prompt를 어떻게 짜도 풀리지 않는다 — 검증자와 생성자가 같은 모델 라인이면 채점 prompt를 아무리 정교하게 짜도 자기 편향이 새어 들어온다. 그래서 deep-wiki는 검증자를 생성 모델과 다른 라인으로 묶는다.
또 한 가지 실전 디테일은 채점 결과의 분산을 모니터링하는 것이다. nightly cron이 같은 페이지에 10번 채점을 던져 표준편차를 잰다. 표준편차가 0.5 이상이면 rubric이 충분히 anchored되지 않았다는 신호 — rubric 갱신 트리거가 된다. 모델 업그레이드 직후엔 표준편차가 잠시 튀었다가 며칠 안에 가라앉는 패턴이 보였다.
5. 거절된 대안 — 왜 RAGAS로 가지 않았나
이미 정착된 LLM 평가 프레임워크가 여럿 있다. 그중 가장 잘 알려진 것이 RAGAS — RAG(Retrieval-Augmented Generation, 검색해 온 문서를 LLM에 붙여 답하게 하는 패턴) 시스템의 답변 품질을 표준 점수로 계산해 주는 라이브러리다. 처음엔 RAGAS를 그대로 쓰는 게 자연스러워 보였다. 하지만 deep-wiki에서는 안 썼다. 이유는 단순하다.
- RAGAS가 채점하는 시나리오가 우리 시나리오와 다르다. RAGAS는 "사용자가 질문했을 때, LLM이 검색해 온 문서를 충실하게 반영해서 답했는가"를 본다. 즉 [질문] [검색 결과] [LLM 답변] 세 가지가 함께 있어야 점수가 나온다. 그런데 우리는 사용자 질문도 없고 검색 결과도 없다 — 위키 페이지 자체의 품질만 보면 된다. RAGAS의 metric 대부분이 input 단계에서 적용할 게 없어진다.
- 프레임워크의 추상화가 우리가 손보고 싶은 디테일을 가린다. "한 차원이라도 3점 이하면 composite를 7.0으로 강제" 같은 정책 한 줄을 추가하려면 RAGAS의 클래스를 상속해 메서드를 override해야 한다. 자체 rubric.json + 짧은 파이썬 함수로 짜면 같은 정책이 한눈에 보이고, rubric을 자주 갱신해도 부담이 없다.
요약하면 — RAGAS는 좋은 도구지만 우리 도메인이 RAG가 아니다. 위키 페이지 검증이라는 시나리오에는 자체 rubric이 더 잘 맞는다. 정착된 프레임워크의 가치는 표준화에서 오는데, deep-wiki는 운영하면서 rubric을 자주 갱신했다 — 표준화의 이득보다 자유도의 이득이 컸다.
마무리 — judge를 시스템에 박을 때 가장 먼저 결정할 두 가지
LLM-as-judge를 처음 박는다면, 모델 선택보다 먼저 결정할 두 가지가 있다.
첫째, rubric에 anchor 예시를 박을 것. 1점·5점·10점이 어떤 페이지인지 구체 예시 없이 점수만 요구하면 모델은 안전한 중간값에 몰린다. anchor 예시 한 세트가 분포를 정상으로 펴 준다. 이게 LLM-as-judge에서 단일 결정 중 가장 큰 효과를 낸다.
둘째, 생성자와 채점자의 모델 라인을 분리할 것. 같은 모델이 자기 출력을 채점하면 거의 항상 9-10이 나온다. 생성에 쓴 라인과 다른 라인(예: 생성 GPT-4 / 채점 Claude)을 채점에 쓰면 trainset이 달라 자기 편향이 새어 들어오지 않는다. deep-wiki는 생성에 여러 모델을 섞고, 채점은 Anthropic 라인으로만 통일했다.
이 두 가지를 박은 위에 system prompt·output schema·few-shot 예시를 얹으면 LLM-as-judge가 운영 가능한 수준의 일관성을 갖는다. 비용 최적화는 그 다음 문제다 — 안정된 채점이 먼저고, 안정된 채점을 싸게 받는 게 그 위에 얹힌다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
같은 소스에서 매번 같은 위키가 나와야 한다 — 위키 생성 파이프라인의 흔들림 제어 (0) 2026.05.31 외부 API 100%, 내부는 trend — 양적 검증을 두 층으로 가르는 이유 (0) 2026.05.30 위키가 거짓말하지 않게 — 모든 코드 인용에 file:line을 강제하는 doctrine (0) 2026.05.30 file.py:LINE anchor가 진짜 그 줄을 가리키는가 — 매일 AST와 대조해서 RAG 거짓말 끊기 (0) 2026.05.30 코드 위키의 빈 박스와 깨진 다이어그램 — mermaid 검증 2중 안전망 (0) 2026.05.30 LLM 검증을 싸게 — haiku 1차 + sonnet 재검증 + Redis SHA 캐시 (1) 2026.05.29 AI 에이전트가 보는 surface를 8개로 좁히다 — deep-wiki MCP gateway (0) 2026.05.28 Redis — 메모리 안의 작은 사전, 그리고 우리가 그것을 쓰는 자리들 (0) 2026.05.28 NetworkX 대표 알고리즘 3선 — 코드 베이스 분석에서 한 줄로 끝나는 일들 (0) 2026.05.27 가벼운 그래프 데이터 처리 — NetworkX + SQLite WAL 조합의 정체와 효과 (0) 2026.05.27