하나의 역할 대신 세 가지의 서로 다른 비평가(Critic) 역할을 사용한 이유 (그리고 평가를 통해 배운 점)
요약
단일 모델의 셀프 리뷰 한계를 극복하기 위해 세 가지 특화된 비평가 에이전트(정확성, 논리, 완전성)와 판정관을 활용하는 Crucible 시스템 구축 사례를 소개합니다. 병렬 처리를 통해 검토의 전문성을 높이고, LangGraph 대신 asyncio를 사용하여 경량화된 오케스트레이션을 구현하는 방법을 다룹니다.
핵심 포인트
- 모델은 자신의 오류를 스스로 인지하기 어려워 다각도 비평이 필요함
- 정확성, 논리, 완전성으로 역할을 분리하여 비평의 신호 강도를 높임
- 판정관(Adjudicator)이 비평가 간의 의견을 종합하여 최종 품질 점수 산출
- 단순 병렬 구조에서는 LangGraph보다 asyncio.gather가 효율적임
하나의 역할 대신 세 가지의 서로 다른 비평가(Critic) 역할을 사용한 이유 (그리고 평가를 통해 배운 점)
저는 주말 동안 Crucible을 구축했습니다: 모든 LLM(Large Language Model) 출력을 병렬로 감사하는 세 가지 특화된 비평가 에이전트(Critic agents), 이들의 비평을 신뢰도 점수가 포함된 판결로 합성하는 심판관(Adjudicator), 그리고 이 전체 시스템이 단순히 단일 모델에게 스스로를 확인하도록 요청하는 것보다 실제로 더 잘 작동하는지 측정하는 평가 하네스(Eval harness)로 구성됩니다.
솔직한 답변이 "기대만큼은 아니었다"는 부분을 포함하여, 제가 배운 점들을 소개합니다.
문제점: 모델은 자신의 사각지대를 신뢰성 있게 감사할 수 없다
언어 모델(Language model)이 출력을 생성할 때, 모델은 이미 특정 방향으로 결정을 내린 상태입니다. 모델에게 셀프 리뷰(Self-review)를 요청하면, 모델은 방금 저지른 똑같은 확신에 찬 실수를 그대로 승인하는 경우가 많습니다. 이는 모델이 게을러서가 아니라
- 정확성 비평가 (Accuracy critic): 주장들이 사실이며 내부적으로 일관성이 있는가? 환각된 엔티티 (Hallucinated entities), 잘못된 숫자, 존재하지 않는 인용문 등을 확인합니다.
- 논리 비평가 (Logic critic): 추론이 논리적인가? 결론이 주어진 전제에 의해 실제로 뒷받침되는가?
- 완전성 비평가 (Completeness critic): 무엇이 누락되었는가? 프롬프트(Prompt)에서 요구한 내용 중 출력물에서 생략된 것은 무엇인가?
각 비평가는 다른 차원으로 벗어나지 말라는 명시적인 지침을 포함하여 좁은 권한 (Mandate)을 가집니다. 정확성 비평가에게는 다음과 같이 지시됩니다: "논리적 흐름이나 완전성에 대해 언급하지 마십시오. 오직 사실적 정확성에만 집중하십시오." 이는 의도적인 설계입니다. 집중된 비평가들이 더 깨끗한 신호 (Signal)를 생성하기 때문입니다. 모든 것을 한꺼번에 검토하는 일반론적인 비평가는 가장 명백한 문제에만 집중하는 경향이 있어 다른 문제들을 놓치기 쉽습니다.
그 후 판정관 (Adjudicator)은 세 가지 비평을 모두 읽고 유형화된 판결을 내립니다: confirmed_issues (판정관이 실제적이고 중대한 것으로 판단한 문제. 비평가 간의 합의가 강력한 신호가 되지만, 단일 비평가의 명확하고 심각도가 높은 플래그도 자격이 있음), dismissed_flags (비평가가 제기했으나 판정관이 범위를 벗어났거나, 지나치게 까다롭거나, 근거가 불충분하다고 판단하여 기각한 문제), 그리고 confidence 등급이 포함된 quality_score입니다.
실무에서는 dismissed_flags 필드가 가장 유용한 요소 중 하나임이 밝혀졌습니다. 단 한 명의 비평가만이 무언가에 대해 반응할 경우, 이는 해당 차원 내에서 비평가가 과도하게 열성적인 상태인 위양성 (False positive)인 경우가 많습니다. 판정관의 역할은 단순히 모든 플래그를 합치는 것이 아니라, 비평가 간의 가중치를 적용하는 것입니다.
asyncio.gather 결정: 왜 LangGraph가 아닌가
LangGraph를 살펴보았습니다. 3개의 노드 팬아웃 (Fan-out, 세 명의 비평가를 병렬로 실행하고 결과를 수집하여 판정관에게 전달)의 경우, LangGraph를 사용하는 것은 지나치게 복잡한 절차 (Ceremony)입니다. Crucible에서의 실제 오케스트레이션 (Orchestration)은 다음과 같습니다:
raw = await asyncio.gather(
*(critic.run(output_text, original_prompt, model) for critic in CRITICS),
return_exceptions=True,
...
겨우 다섯 줄입니다. LangGraph를 사용했다면 그래프 정의, 노드 등록, 상태 관리(state management), 그리고 제가 절대 열어보지 않을 디버깅 UI까지 제공했을 것입니다. 이 정도 규모에서는 추상화(abstraction) 비용이 절감되는 비용보다 더 큽니다.
이 프로젝트를 위해 작성한 결정 기록(decision record)에는 제가 계속해서 되새기는 문장이 하나 있습니다: "3개 노드의 팬아웃(fan-out) 구조를 구현하는 데 LangGraph는 과한 절차(ceremony)다. asyncio.gather는 약 5줄이면 충분하며 면접에서도 설명하기 더 쉽다." LangGraph를 비하하려는 것이 아닙니다. LangGraph는 더 큰 규모에서는 진정으로 제 가치를 합니다. 하지만 5분 안에 설명할 수 없는 것을 만드는 것은 기능(feature)이 아닙니다.
프로바이더(Provider) 문제: Claude 3개, 그리고 다양성으로 가는 경로
여기 제가 투명하게 밝히고 싶은 결정 사항이 있습니다. Crucible의 브리프(brief)는 세 가지 서로 다른 프로바이더를 요구했습니다: 정확성을 위한 GPT-4o, 논리를 위한 Claude, 완결성을 위한 Gemini입니다. 이론은 타당합니다. 훈련 데이터(training data)가 다르면 실패 모드(failure modes)도 다르기 때문에, 세 명의 비평가(critic)가 모두 동일한 사각지대를 공유할 가능성이 낮아집니다.
저는 이를 지원할 수 있도록 아키텍처를 구축했습니다. 프로바이더 결정 로직은 src/providers.py에 있습니다: OPENAI_API_KEY가 설정되어 있으면 정확성 비평가(accuracy critic)가 GPT-4o로 업그레이드됩니다. GEMINI_API_KEY가 설정되어 있으면 완결성 비평가(completeness critic)가 Gemini로 업그레이드됩니다. 그렇지 않으면 세 비평가 모두 Claude에서 실행됩니다.
하지만 저는 멀티 프로바이더(multi-provider)를 v1 요구 사항에서 명시적으로 제외했습니다. 결정 기록은 다음과 같습니다:
"하나의 강력한 모델 위에서 세 개의 서로 다른 비평가 프롬프트(critic prompts)를 사용하는 것만으로도 이미 관점의 다양성(lens diversity)을 만들어낼 수 있으며, 멀티 프로바이더 의존성을 제거함으로써 리뷰어가 단일 API 키만으로 데모를 실행할 수 있게 한다."
이것은 솔직한 트레이드오프 (tradeoff)입니다. Claude에서 잘 정의된 세 가지 비평가 (critic) 프롬프트를 사용하는 것은 구조적으로 작업 (task) 자체가 다르기 때문에 진정으로 다른 결과물을 생성합니다. 하나는 잘못된 사실을 찾아내고, 하나는 논리적 구조를 평가하며, 하나는 명시된 목표에 대한 완결성을 확인합니다. 이것이 진정한 관점의 다양성 (lens diversity)입니다. 서로 다른 프로바이더 (provider)를 통해 얻는 추가적인 다양성도 실재하지만 점진적인 수준이며, 다음과 같은 실제 비용이 따릅니다: 세 세트의 API 키, 세 가지의 서로 다른 속도 제한 (rate limits), 세 가지의 서로 다른 지연 시간 (latency) 프로필, 세 가지의 서로 다른 가격 모델 (pricing models).
출시와 데모 시연이 필요한 v1 단계에서는 더 단순한 버전을 선택했습니다. 아키텍처는 업그레이드를 위한 준비가 되어 있습니다.
평가 (eval)를 통해 배운 점 (솔직한 버전)
저는 12개의 테스트 케이스로 구성된 평가 하네스 (eval harness)를 구축했습니다: 10개는 의도적으로 오류를 심어두었으며 (정확성, 논리, 완결성 차원에서 총 15개의 오류), 2개는 오류가 없는 깨끗한 케이스입니다. 심어진 각 오류에는 "포착됨"으로 간주되는 키워드 목록이 있습니다.
결과: 패널 (panel)에 의해 심어진 15개 오류 중 15개 모두 포착, 깨끗한 케이스에서 오탐 (false positives) 0건.
여기 제가 예상하지 못했던 부분이 있습니다: 단일 모델 베이스라인 (baseline) 또한 15개 오류를 모두 포착했다는 점입니다.
저의 첫 반응은 평가 (eval)가 잘못되었다는 것이었습니다. 하지만 케이스들을 살펴본 후, 결과가 맞으며 이 결과가 알려주는 바가 제가 처음에 생각했던 것보다 더 구체적이라고 판단했습니다.
패널은 잘 설계된 골든 세트 (golden set) 상에서 단일 모델의 자기 평가 (self-eval)보다 탐지 (detection) 측면에서 극적으로 뛰어나지는 않습니다. 패널이 더 뛰어난 점은 바로 구조 (structure) 입니다. 패널의 출력은 다음을 알려줍니다:
- 어떤 특정 차원에서 실패하고 있는지 (정확성 vs 논리 vs 완결성)
- 어떤 비평가들이 동의했고 어떤 비평가가 반대했는지
- 어떤 플래그 (flags)가 기각되었는지와 그 이유
- 신뢰도 (confidence)별로 세분화된 품질 점수
베이스라인은 발견 사항을 평면적인 목록으로 제공합니다. 동일한 오류를 잡아낼 수는 있지만, 모델이 확신하고 있는지, 두 개의 독립적인 관점이 일치했는지, 혹은 특정 차원에서는 과도하게 주의를 기울이고 다른 차원에서는 주의가 부족한지 여부는 알 수 없습니다.
출력값이 잘못되었는지에 대해 빠른 예/아니오(yes/no) 답변이 필요하다면, 잘 설계된 프롬프트를 가진 단일 모델만으로도 충분할 수 있습니다. 하지만 차원별 책임 소재와 신뢰 수준을 갖춘 구조화되고 감사 가능한(auditable) 신호가 필요하다면, 패널(panel) 방식의 복잡성은 그만한 가치가 있습니다.
진심으로 놀랐던 한 가지
판정관(adjudicator)의 dismissed_flags 리스트입니다.
저는 비평가(critics)들이 서로 동의하거나, 혹은 독립적으로 서로 다른 문제들을 찾아낼 것이라고 예상했습니다. 제가 예상하지 못했던 점은, 한 비평가가 지적한 사항을 나머지 두 비평가가 명시적으로 지적하지 않는 경우가 빈번하게 발생한다는 것이었습니다. 판정관이 단순히 모든 플래그를 합치는(unioning) 것이 아니라 해당 케이스를 올바르게 처리하는 것이 제가 예상했던 것보다 훨씬 더 중요하다는 사실이 밝혀졌습니다.
평가(eval) 과정에서, 몇몇 케이스에서는 논리(logic) 비평가가 무언가를 지적했으나 정확도(accuracy) 및 완전성(completeness) 비평가는 이를 무시하는 경우가 있었습니다. 그런 경우 판정관의 역할은 비평가 간의 상호 확증(cross-critic corroboration)을 적용하여, 이를 확인하거나(논리적 문제가 실제 존재하지만 다른 비평가들의 범위 밖이었던 경우) 혹은 기각하는 것(지나치게 조심스러운 지적으로 보였던 경우)이었습니다. 이를 제대로 수행하기 위해서는 단순히 투표수를 세는 것이 아니라, 각 비평가의 임무(mandate)를 이해해야 했습니다.
이러한 구조(좁은 범위를 가진 비평가들과 전체 문맥을 가진 판정관)는 개별 비평가의 프롬프트보다 출력 품질에 더 중요한 역할을 하게 되었습니다.
다르게 시도했을 점
평가 하네스(eval harness)의 키워드 일치(keyword-match) 탐지는 결정론적(deterministic)이고 비용이 저렴하지만, 실제 벤치마크로 쓰기에는 너무 취약합니다. 비평가가 다른 용어를 사용하여 문제를 정확히 식별하더라도 일치 여부 확인에 실패할 수 있기 때문입니다. v2에서는 부분 문자열(substring)의 존재 여부가 아니라 의미론적 동등성(semantic equivalence)을 평가하는 LLM-as-judge 매처(matcher)가 필요합니다. 현재의 하네스는 깔끔한 수치를 제공하지만, 아마도 실제보다 약간 적게 집계하고 있을 것입니다.
또한, 저는 제공자 다양성(provider diversity)을 더 일찍 강력하게 추진했을 것입니다. 아키텍처는 이미 갖춰져 있습니다. 다음으로 의미 있는 평가 질문은 동일한 케이스에 대해 GPT-4o가 Claude가 놓치는 정확도 오류를 잡아내는지 여부이며, 이를 위해서는 이론적인 추측이 아니라 실제로 실행해 보는 과정이 필요합니다.
코드
전체 프로젝트는 github.com/bhj37193/crucible에서 확인할 수 있습니다. 진입점(Entry point)은 python -m src.runner "<output text>"입니다. 평가는 python -m evals.run_eval로 실행됩니다. FastAPI와 Anthropic SDK 외의 프레임워크 의존성은 없습니다.
의사 결정 기록은 /planning/decisions에 있습니다. v1을 위한 컷 리스트(Cut list)에는 무엇이 왜 제거되었는지 명시되어 있습니다. 만약 이 글을 읽으면서 "그런데 왜 그냥 LangGraph를 사용하지 않았나요?"라고 생각하신다면, 해당 문서가 그에 대한 답이 될 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기