에이전트가 에러를 분석하고 코드 수정을 제안할 때, 그 결과가 실제로 올바른지 어떻게 보장할 수 있을까?
self-reflection(자기 검증)을 이미 구현했지만 이건 같은 모델이 자기 출력을 검토하는 구조다. 생성한 사람과 검토하는 사람이 같으니 자기 편향(self-evaluation bias)이 생길 수 있다. 이 문제를 외부 LLM을 Judge로 활용하는 방식으로 해결했다.
아이디어: 생성과 검토를 분리한다
1
2
3
4
5
[분석 에이전트] (Ollama — gemma4:12b)
에러 로그 → 수정 제안 생성
[Judge] (Gemini — 외부 LLM)
에러 로그 + 수정 제안 → 품질 평가 (1~5점)
같은 모델을 쓰더라도 역할을 분리하면 의미가 있지만, 아예 다른 모델이 검토하면 더 독립적인 평가가 가능하다. 여기서는 Gemini API를 무료로 활용해 비용 없이 외부 검증을 추가했다.
평가 프롬프트 설계
Judge에게 줄 정보는 세 가지다.
- 원본 에러 로그 (무엇이 문제인가)
- 분석 에이전트의 결론 (원인, 병목, 수정 설명)
- 실제 코드 변경 (Before / After)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def _build_prompt(error_log: str, suggestion: dict) -> str:
fp = suggestion.get("file_patch", {})
return f"""당신은 시니어 백엔드 엔지니어입니다. 아래 에러와 수정 제안을 검토해주세요.
## 에러 로그
{error_log[:1500]}
## 분석 결과
- 에러 원인: {suggestion.get("error_cause", "")}
- 수정 설명: {suggestion.get("suggested_fix", "")}
## 코드 수정
- Before: {fp.get("before", "")}
- After: {fp.get("after", "")}
## 평가 기준
1. 수정이 에러 원인을 실제로 해결하는가?
2. After 코드가 논리적으로 올바른가?
3. 부작용이나 새로운 버그를 유발할 가능성이 있는가?
JSON만 응답하세요:
score"""
응답을 JSON으로 강제하고 temperature=0.1로 낮게 설정해 일관된 평가를 받도록 했다.
구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async def judge_fix(error_log: str, llm_suggestion: str) -> dict | None:
if not settings.gemini_api_key:
return None # API 키 없으면 조용히 건너뜀
suggestion = json.loads(llm_suggestion)
genai.configure(api_key=settings.gemini_api_key)
model = genai.GenerativeModel("gemini-2.5-flash-lite")
response = model.generate_content(
prompt,
generation_config=genai.GenerationConfig(
temperature=0.1,
max_output_tokens=256,
),
)
result = json.loads(response.text.strip())
return {
"score": int(result["score"]), # 1~5
"confidence": result["confidence"], # high / medium / low
"reason": result["reason"], # 한 문장 평가
}
실패해도 None을 반환할 뿐 파이프라인은 정상 진행된다. Judge는 보조 기능이지 핵심 흐름을 막아선 안 된다.
파이프라인 통합
분석 완료 직후, DB 저장 전에 Judge를 호출한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
# 분석 완료
suggestion = await ollama_service.analyze_log(server.id, raw_log, stack_trace)
# Judge 호출
judge_result = await judge_service.judge_fix(raw_log, suggestion)
# DB 저장 (judge 결과 포함)
record = AnalysisRecord(
llm_suggestion=suggestion,
judge_score=judge_result["score"] if judge_result else None,
judge_confidence=judge_result["confidence"] if judge_result else None,
judge_reason=judge_result["reason"] if judge_result else None,
)
Slack 메시지에 표시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def _build_judge_block(record: AnalysisRecord) -> dict:
score = record.judge_score or 0
filled = "⭐" * score + "☆" * (5 - score)
confidence_emoji = {"high": "🟢", "medium": "🟡", "low": "🔴"}.get(record.judge_confidence, "⚪")
return {
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"*🤖 Gemini Judge*\n"
f"{filled} ({score}/5) {confidence_emoji} {record.judge_confidence}\n"
f"_{record.judge_reason}_"
),
},
}
Slack에서 보면 이렇게 나온다.
1
2
3
4
5
🤖 Gemini Judge
⭐⭐⭐⭐⭐ (5/5) 🟢 high
제시된 수정안은 NullPointerException의 근본 원인을 정확히 파악하고,
null 값에 대한 방어 로직을 추가하여 에러를 효과적으로 해결하며,
새로운 부작용이나 버그를 유발할 가능성이 낮습니다.
실제 동작 로그
1
2
3
4
[judge] score=5 confidence=high reason=제시된 수정안은 NullPointerException의
근본 원인을 정확히 파악하고, null 값에 대한 방어 로직을 추가하여 에러를
효과적으로 해결하며, 새로운 부작용이나 버그를 유발할 가능성이 낮습니다.
[pipeline] judge score=5 confidence=high
설계 포인트
왜 같은 모델을 안 쓰나? 같은 모델이 생성한 결과를 같은 모델이 검토하면 자기 편향이 생긴다. 자신이 만든 답을 좋게 평가하는 경향이 있기 때문이다. 다른 모델(여기서는 Gemini)이 독립적으로 검토하면 더 신뢰할 수 있는 평가가 된다.
왜 실패해도 파이프라인을 멈추지 않나? Judge는 보조 지표다. API 장애나 할당량 초과로 Judge가 실패해도 에러 분석과 Slack 알림은 정상적으로 동작해야 한다. try/except로 감싸고 실패 시 None을 반환해 파이프라인에 영향을 주지 않는다.
비용은? Gemini API 무료 티어(하루 1,500회)를 사용한다. 에러 분석 1건당 Judge 호출이 1회이므로 하루 1,500건의 에러까지 무료로 검증할 수 있다.