콘텐츠로 이동

Reflexion: 언어적 강화학습 루프

ReAct의 빈틈: 실패를 배우지 못한다

섹션 제목: “ReAct의 빈틈: 실패를 배우지 못한다”

앞 챕터에서 ReAct 루프의 두 가지 약점 중 하나로 “캐스케이딩 에러 자동 회복 없음”을 꼽았다. 모델이 잘못된 Action을 취하면 그 루프 안에서는 방향 전환이 쉽지 않다. 더 근본적인 문제는 실패한 시도 전체를 다음 시도에 활용하지 못한다는 것이다. 루프가 종료되면 그 경험은 사라진다.

인간은 다르게 배운다. 체스를 두다 패배하면 어떤 수에서 틀렸는지 돌아보고, 그 통찰을 다음 판에 가져간다. Reflexion(Shinn et al., NeurIPS 2023)은 이 직관을 LLM 에이전트에 옮겨놓는다. 가중치를 업데이트하지 않고, 언어 텍스트만으로 “이전 시도에서 무엇이 잘못됐는가”를 정리해 다음 시도의 컨텍스트로 주입한다. 이를 **언어적 강화학습(verbal reinforcement learning)**이라고 부른다.

Reflexion은 두 개의 루프가 중첩된다.

┌──────────────────────────────────────────────────────────────────┐
│ Reflexion 이중 루프 │
│ │
│ 외부 루프 (Trial Loop) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Trial N │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 내부 루프 (ReAct Loop) │ │ │
│ │ │ Thought → Action → Observation → ... │ │ │
│ │ │ → Final Answer / 실패 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ ▼ │ │
│ │ Evaluator (외부 신호: 테스트 통과 여부, 점수 등) │ │
│ │ ▼ │ │
│ │ Reflector: "이번 시도에서 무엇이 틀렸나?" → 언어 반성문 │ │
│ │ ▼ │ │
│ │ Episodic Memory에 반성문 추가 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ▼ │
│ Trial N+1 시작 시 Episodic Memory를 컨텍스트 앞에 주입 │
└──────────────────────────────────────────────────────────────────┘

내부 루프는 이전 챕터의 ReAct 루프 그 자체다. 차이는 외부 루프에서 발생한다. 내부 루프가 끝나면(성공이든 실패든) Evaluator가 결과를 평가하고, Reflector가 “무엇이 잘못됐고 다음에는 어떻게 해야 하는가”를 자연어로 작성한다. 이 반성문(reflection)이 에피소드 메모리(episodic memory)에 추가된다.

다음 Trial이 시작될 때, 모델은 원래 태스크뿐 아니라 지금까지의 모든 반성문을 컨텍스트로 받는다. 이것이 “파라미터를 바꾸지 않고 컨텍스트를 통해 학습”하는 방식이다.

개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다.

from dataclasses import dataclass, field
@dataclass
class ReflexionAgent:
model: object
tools: dict
evaluator: object # 성공 여부를 판단하는 외부 함수/모델
max_trials: int = 3
max_steps_per_trial: int = 10
episodic_memory: list[str] = field(default_factory=list)
def _build_system_prompt(self) -> str:
base = "주어진 태스크를 ReAct 방식으로 해결하라."
if not self.episodic_memory:
return base
reflections = "\n".join(
f"[시도 {i+1} 반성] {r}" for i, r in enumerate(self.episodic_memory)
)
return f"{base}\n\n이전 시도에서 학습한 내용:\n{reflections}"
def _reflect(self, task: str, trajectory: list[dict], outcome: str) -> str:
"""Reflector: 실패 원인을 자연어로 작성한다."""
reflection_prompt = (
f"태스크: {task}\n"
f"결과: {outcome}\n"
f"궤적 요약: {trajectory[-3:]}\n" # 마지막 3스텝
"무엇이 잘못됐고 다음에는 어떻게 달리 해야 하는가? "
"한 단락으로 간결하게 작성하라."
)
return self.model.generate(
[{"role": "user", "content": reflection_prompt}]
).text
def run(self, task: str) -> str:
for trial in range(self.max_trials):
system = self._build_system_prompt()
# 내부 ReAct 루프 (이전 챕터 react_loop 재사용)
trajectory, answer = react_loop_with_trajectory(
model=self.model,
tools=self.tools,
task=task,
system=system,
max_steps=self.max_steps_per_trial,
)
# 외부 평가
success, outcome = self.evaluator(task, answer)
if success:
return answer
# 반성문 생성 및 저장
reflection = self._reflect(task, trajectory, outcome)
self.episodic_memory.append(reflection)
return f"최대 시도 횟수({self.max_trials}) 초과. 마지막 답: {answer}"

Reflexion 논문은 코딩 벤치마크 HumanEval에서 91% pass@1을 달성했다고 보고했다. 이는 당시 GPT-4의 약 80%에 비해 11%p 높은 수치다. “pass@1”은 한 번의 시도로 올바른 코드를 생성하는 비율이다. 단, Reflexion은 여러 Trial을 거친 뒤의 수치이므로 단순 비교가 아님을 주의해야 한다.

코딩 태스크가 Reflexion과 궁합이 좋은 이유가 있다. 단위 테스트라는 **명확한 외부 평가기(evaluator)**가 존재하기 때문이다. 테스트가 통과하면 성공, 실패하면 에러 메시지가 자동으로 Reflector에 전달된다. 이처럼 기계적으로 검증 가능한 도메인에서 Reflexion의 효과가 극대화된다.

에피소드 메모리의 선형 증가: 반성문이 쌓일수록 컨텍스트가 길어진다. ReAct의 컨텍스트 증가 문제가 Trial 차원에서 재발한다. 실용적으로는 최근 N개의 반성문만 유지하거나, 반성문 자체를 압축(요약)하는 전략이 필요하다.

평가기 의존성: Evaluator가 없으면 Reflexion은 작동하지 않는다. “답이 맞는가”를 외부에서 판단해줄 신호가 없는 오픈엔드 태스크(예: 창의적 글쓰기)에는 적용이 까다롭다. LLM-as-Judge를 Evaluator로 사용할 수 있지만, 그 자체의 편향 문제가 생긴다.

반성문의 질: Reflector가 잘못된 진단을 내리면 다음 Trial이 오히려 나빠질 수 있다. “검색 쿼리가 너무 길었다”고 반성했지만 실제 원인은 다른 곳에 있었다면, 이후 모든 Trial이 잘못된 방향으로 수렴한다.

공개 가중치 미변경: Reflexion은 파라미터를 업데이트하지 않으므로 “진정한 의미의 학습”은 아니다. 컨텍스트 창이 닫히면 경험이 사라진다. 장기 지식 보존이 필요하다면 반성문을 외부 파일에 직렬화하거나, 11-2-dspy 챕터에서 다룰 DSPy처럼 프롬프트를 직접 최적화하는 방식을 고려해야 한다.

조건 Reflexion 추천 여부
명확한 기계적 평가기 존재 (테스트, 스코어) 강력 추천
Trial 횟수가 3~5회로 제한 가능 적합
태스크가 반복적으로 유사한 실패 패턴을 보임 적합
오픈엔드 창의적 태스크 부적합
레이턴시가 중요한 실시간 요청 부적합 (Trial만큼 지연 배수)
컨텍스트 창이 매우 제한적 주의 필요

다음 챕터에서는 “실행 전에 먼저 계획을 세운다”는 다른 전략, Plan-and-Execute를 다룬다. Reflexion이 “실패 후 반성”이라면, Plan-and-Execute는 “실패 예방을 위한 사전 분해”에 초점을 맞춘다.

참고 자료