에이전트가 자신의 출력을 평가하고 반복적으로 개선하는 리플렉션 패턴의 원리, 구현 방법, 실전 활용 전략을 다룹니다.
리플렉션(Reflection)은 에이전트가 자신의 출력을 스스로 평가하고, 부족한 점을 식별하여 개선하는 패턴입니다. 사람이 글을 쓴 후 다시 읽으며 퇴고하는 과정과 유사합니다. Andrew Ng은 이 패턴을 에이전틱 AI의 네 가지 핵심 설계 패턴 중 하나로 제시하며, 단순한 프롬프트 추가만으로도 눈에 띄는 품질 향상을 가져올 수 있다고 강조했습니다.
기본 아이디어는 단순합니다. LLM이 초기 응답을 생성한 후, 같은 모델이나 다른 모델이 그 응답을 평가하고, 평가 결과를 바탕으로 개선된 응답을 다시 생성하는 것입니다.
LLM이 단일 패스(single pass)로 생성한 출력에는 여러 문제가 있을 수 있습니다. 사실 오류, 논리적 비약, 불완전한 답변, 형식 불일치 등이 그 예입니다. 리플렉션은 이런 문제를 발견하고 수정할 기회를 제공합니다.
핵심적인 이유는 LLM이 생성 모드와 평가 모드에서 서로 다른 능력을 발휘한다는 점입니다. 생성할 때는 창의적이고 유창한 텍스트를 만들지만, 세부적인 정확성을 놓칠 수 있습니다. 반면 평가할 때는 구체적인 기준에 따라 체계적으로 검토할 수 있습니다. 리플렉션은 이 두 모드를 순환시켜 양쪽의 장점을 결합합니다.
가장 단순한 형태의 리플렉션을 구현해 보겠습니다.
하나의 LLM이 생성과 평가를 모두 수행하는 방식입니다.
import anthropic
client = anthropic.Anthropic()
def generate(prompt: str) -> str:
"""초기 응답을 생성합니다."""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
def evaluate(original_prompt: str, response: str) -> dict:
"""응답을 평가하고 피드백을 반환합니다."""
eval_prompt = (
"다음 질문에 대한 응답을 평가하십시오.\n\n"
"## 원래 질문\n" + original_prompt + "\n\n"
"## 응답\n" + response + "\n\n"
"## 평가 기준\n"
"1. 정확성: 사실적으로 정확한가?\n"
"2. 완전성: 질문의 모든 측면을 다루었는가?\n"
"3. 명확성: 이해하기 쉽게 작성되었는가?\n"
"4. 구조: 논리적으로 구성되었는가?\n\n"
"## 출력 형식\n"
"각 기준에 대해 1-5점으로 평가하고, 구체적인 개선 사항을 제시하십시오.\n"
"마지막에 전체 점수(1-5)를 제시하십시오.\n\n"
"JSON 형식으로 응답하십시오:\n"
'{"scores": {"accuracy": N, "completeness": N, '
'"clarity": N, "structure": N},\n'
' "overall": N,\n'
' "feedback": "구체적인 개선 사항",\n'
' "pass": true/false}'
)
result = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=[{"role": "user", "content": eval_prompt}],
)
import json
try:
return json.loads(result.content[0].text)
except json.JSONDecodeError:
return {"overall": 3, "feedback": result.content[0].text, "pass": False}
def refine(original_prompt: str, response: str, feedback: str) -> str:
"""피드백을 바탕으로 응답을 개선합니다."""
refine_prompt = (
"다음 응답을 피드백에 따라 개선하십시오.\n\n"
"## 원래 질문\n" + original_prompt + "\n\n"
"## 현재 응답\n" + response + "\n\n"
"## 피드백\n" + feedback + "\n\n"
"피드백에서 지적된 모든 사항을 반영하여 개선된 응답을 작성하십시오.\n"
"개선된 응답만 출력하십시오."
)
result = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{"role": "user", "content": refine_prompt}],
)
return result.content[0].text
def reflect_and_improve(prompt: str, max_iterations: int = 3) -> str:
"""리플렉션 루프를 실행합니다."""
response = generate(prompt)
print(f"[초기 생성] 완료")
for i in range(max_iterations):
evaluation = evaluate(prompt, response)
print(f"[평가 {i + 1}] 전체 점수: {evaluation.get('overall', 'N/A')}")
if evaluation.get("pass", False):
print(f"[완료] {i + 1}번째 평가에서 통과")
return response
feedback = evaluation.get("feedback", "")
response = refine(prompt, response, feedback)
print(f"[개선 {i + 1}] 완료")
return response생성과 평가에 서로 다른 모델을 사용하는 방식입니다. 동일한 모델의 편향을 줄이는 효과가 있습니다.
class DualModelReflector:
def __init__(self):
self.client = anthropic.Anthropic()
self.generator_model = "claude-sonnet-4-20250514"
self.evaluator_model = "claude-sonnet-4-20250514"
def generate(self, prompt: str) -> str:
response = self.client.messages.create(
model=self.generator_model,
max_tokens=4096,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
def evaluate(self, prompt: str, response: str) -> dict:
eval_prompt = (
"당신은 엄격한 품질 검토자입니다.\n"
"다음 응답을 비판적으로 평가하십시오.\n\n"
"질문: " + prompt + "\n"
"응답: " + response + "\n\n"
"1. 사실 오류가 있는가? 있다면 구체적으로 지적하십시오.\n"
"2. 누락된 중요한 정보가 있는가?\n"
"3. 논리적 비약이나 모순이 있는가?\n"
"4. 개선이 필요한 부분을 구체적으로 기술하십시오.\n\n"
'JSON으로 응답: {"issues": [...], "suggestions": [...], "pass": true/false}'
)
response = self.client.messages.create(
model=self.evaluator_model,
max_tokens=2048,
messages=[{"role": "user", "content": eval_prompt}],
)
import json
try:
return json.loads(response.content[0].text)
except json.JSONDecodeError:
return {"issues": [], "suggestions": [], "pass": False}코드 생성에 리플렉션을 적용한 예입니다.
CODE_EVAL_PROMPT = (
"다음 코드를 검토하십시오.\n\n"
"## 요구 사항\n{requirements}\n\n"
"## 생성된 코드\n```python\n{code}\n```\n\n"
"## 검토 기준\n"
"1. 정확성: 요구 사항을 올바르게 구현하는가?\n"
"2. 엣지 케이스: 경계 조건을 처리하는가?\n"
"3. 오류 처리: 예외 상황을 적절히 처리하는가?\n"
"4. 성능: 비효율적인 부분이 있는가?\n"
"5. 보안: 보안 취약점이 있는가? (SQL 인젝션, XSS 등)\n"
"6. 가독성: 명확하고 이해하기 쉬운가?\n\n"
"각 기준에 대해 구체적인 문제와 개선 방안을 JSON으로 제시하십시오."
)
class CodeReflector:
def __init__(self):
self.client = anthropic.Anthropic()
def generate_code(self, requirements: str) -> str:
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{
"role": "user",
"content": "다음 요구 사항을 구현하는 Python 코드를 작성하십시오:\n" + requirements
}],
)
return response.content[0].text
def review_code(self, requirements: str, code: str) -> dict:
prompt = CODE_EVAL_PROMPT.format(
requirements=requirements, code=code
)
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
)
import json
try:
return json.loads(response.content[0].text)
except json.JSONDecodeError:
return {"pass": False, "feedback": response.content[0].text}
def improve_code(self, requirements: str, code: str, review: dict) -> str:
prompt = (
"다음 코드를 리뷰 결과에 따라 개선하십시오.\n\n"
"요구 사항: " + requirements + "\n"
"현재 코드:\n```python\n" + code + "\n```\n"
"리뷰 결과: " + json.dumps(review, ensure_ascii=False) + "\n\n"
"개선된 코드만 출력하십시오."
)
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text코드 리플렉션에서는 정적 분석 도구(린터, 타입 체커)의 결과를 평가 프롬프트에 포함시키면 효과가 크게 향상됩니다. LLM만으로는 구문 오류나 타입 오류를 완벽히 잡아내기 어렵기 때문입니다.
문서나 기술 블로그 글 작성에 리플렉션을 적용한 예입니다.
WRITING_CRITERIA = {
"technical_accuracy": "기술적 내용이 정확한가?",
"audience_fit": "대상 독자의 수준에 적합한가?",
"structure": "도입-본문-결론 구조가 논리적인가?",
"examples": "충분하고 적절한 예시를 포함하는가?",
"actionability": "독자가 실제로 적용할 수 있는 내용인가?",
}
class WritingReflector:
def __init__(self):
self.client = anthropic.Anthropic()
self.criteria = WRITING_CRITERIA
def evaluate_writing(self, topic: str, draft: str) -> dict:
criteria_text = "\n".join(
"- " + k + ": " + v for k, v in self.criteria.items()
)
prompt = (
"다음 글을 평가하십시오.\n\n"
"주제: " + topic + "\n"
"초안:\n" + draft + "\n\n"
"평가 기준:\n" + criteria_text + "\n\n"
"각 기준에 대해 1-5점과 구체적 피드백을 제시하십시오.\n"
"전체 점수가 4점 이상이면 pass: true로 표시하십시오."
)
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
)
return {"feedback": response.content[0].text, "pass": False}LLM은 자신이 생성한 텍스트에 대해 편향된 평가를 할 수 있습니다. 특히 자신이 작성한 내용의 사실적 오류를 발견하는 데는 취약합니다.
대응 전략:
리플렉션을 반복해도 품질이 개선되지 않거나, 오히려 악화되는 경우가 있습니다. 특히 평가 기준이 모호하거나 서로 충돌할 때 이런 현상이 발생합니다.
대응 전략:
def check_convergence(scores: list[float], window: int = 3) -> bool:
"""점수 변화가 수렴했는지 확인합니다."""
if len(scores) < window:
return False
recent = scores[-window:]
# 최근 점수 변화가 미미하면 수렴으로 판단
variance = sum((s - sum(recent) / len(recent)) ** 2
for s in recent) / len(recent)
return variance < 0.1
def reflection_with_convergence(prompt: str, max_iterations: int = 5) -> str:
"""수렴 감지가 포함된 리플렉션 루프입니다."""
response = generate(prompt)
scores = []
for i in range(max_iterations):
evaluation = evaluate(prompt, response)
score = evaluation.get("overall", 0)
scores.append(score)
# 통과 조건
if evaluation.get("pass", False):
return response
# 수렴 감지: 더 이상 개선되지 않으면 중단
if check_convergence(scores):
print(f"[수렴] 점수가 더 이상 개선되지 않습니다: {scores}")
return response
# 점수 하락 감지
if len(scores) >= 2 and scores[-1] < scores[-2]:
print(f"[경고] 점수가 하락했습니다: {scores[-2]} -> {scores[-1]}")
feedback = evaluation.get("feedback", "")
response = refine(prompt, response, feedback)
return response리플렉션은 여러 번의 LLM 호출을 수반하므로 비용과 지연이 증가합니다. 3회 반복이면 최소 7번의 API 호출(생성 1 + 평가 3 + 개선 3)이 필요합니다.
대응 전략:
실무에서는 "항상 리플렉션을 사용한다"보다 "리플렉션이 필요한 상황을 식별한다"가 더 중요합니다. 단순한 질답에는 리플렉션이 불필요하지만, 코드 생성, 보고서 작성, 복잡한 분석 등에는 큰 효과를 발휘합니다.
리플렉션은 독립적으로도 유용하지만, 다른 패턴과 결합하면 더욱 강력해집니다.
ReAct 에이전트의 최종 답변에 리플렉션을 적용하여, 도구를 사용해 수집한 정보가 올바르게 종합되었는지 검증합니다.
코드를 생성한 후 실제로 실행하여 결과를 검증하고, 오류가 있으면 수정하는 패턴입니다. 이는 "도구 기반 리플렉션"이라고 할 수 있습니다.
def code_generate_and_test(requirements: str) -> str:
"""코드를 생성하고 테스트로 검증하는 리플렉션 루프입니다."""
code = generate_code(requirements)
for attempt in range(3):
# 코드 실행으로 검증
test_result = run_tests(code)
if test_result["all_passed"]:
return code
# 실패한 테스트 정보로 코드 개선
feedback = format_test_failures(test_result)
code = improve_code(requirements, code, feedback)
return code5장에서는 계획 수립(Planning) 패턴을 다룹니다. 복잡한 작업을 하위 단계로 분해하고, 실행 계획을 수립하며, 실행 중에 계획을 동적으로 수정하는 방법을 살펴보겠습니다. Plan-and-Execute 아키텍처와 적응적 계획 수립 전략을 집중적으로 다룹니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
Plan-and-Execute 아키텍처의 원리와 구현, 적응적 재계획 전략, 그리고 계획 수립 패턴이 에이전트 성능에 미치는 영향을 다룹니다.
AI 에이전트의 도구 정의, 호출, 결과 통합의 전 과정을 다루고, 효과적인 도구 스키마 설계와 복합 도구 조합 전략을 살펴봅니다.
여러 전문화된 에이전트가 협업하는 멀티 에이전트 시스템의 설계 패턴, 감독자/토론/파이프라인 아키텍처를 코드와 함께 다룹니다.