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이 시작될 때, 모델은 원래 태스크뿐 아니라 지금까지의 모든 반성문을 컨텍스트로 받는다. 이것이 “파라미터를 바꾸지 않고 컨텍스트를 통해 학습”하는 방식이다.
Python 구현 예시
섹션 제목: “Python 구현 예시”개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다.
from dataclasses import dataclass, field
@dataclassclass 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}"수치로 본 성능: HumanEval 91%
섹션 제목: “수치로 본 성능: HumanEval 91%”Reflexion 논문은 코딩 벤치마크 HumanEval에서 91% pass@1을 달성했다고 보고했다. 이는 당시 GPT-4의 약 80%에 비해 11%p 높은 수치다. “pass@1”은 한 번의 시도로 올바른 코드를 생성하는 비율이다. 단, Reflexion은 여러 Trial을 거친 뒤의 수치이므로 단순 비교가 아님을 주의해야 한다.
코딩 태스크가 Reflexion과 궁합이 좋은 이유가 있다. 단위 테스트라는 **명확한 외부 평가기(evaluator)**가 존재하기 때문이다. 테스트가 통과하면 성공, 실패하면 에러 메시지가 자동으로 Reflector에 전달된다. 이처럼 기계적으로 검증 가능한 도메인에서 Reflexion의 효과가 극대화된다.
Reflexion의 한계와 주의점
섹션 제목: “Reflexion의 한계와 주의점”에피소드 메모리의 선형 증가: 반성문이 쌓일수록 컨텍스트가 길어진다. ReAct의 컨텍스트 증가 문제가 Trial 차원에서 재발한다. 실용적으로는 최근 N개의 반성문만 유지하거나, 반성문 자체를 압축(요약)하는 전략이 필요하다.
평가기 의존성: Evaluator가 없으면 Reflexion은 작동하지 않는다. “답이 맞는가”를 외부에서 판단해줄 신호가 없는 오픈엔드 태스크(예: 창의적 글쓰기)에는 적용이 까다롭다. LLM-as-Judge를 Evaluator로 사용할 수 있지만, 그 자체의 편향 문제가 생긴다.
반성문의 질: Reflector가 잘못된 진단을 내리면 다음 Trial이 오히려 나빠질 수 있다. “검색 쿼리가 너무 길었다”고 반성했지만 실제 원인은 다른 곳에 있었다면, 이후 모든 Trial이 잘못된 방향으로 수렴한다.
공개 가중치 미변경: Reflexion은 파라미터를 업데이트하지 않으므로 “진정한 의미의 학습”은 아니다. 컨텍스트 창이 닫히면 경험이 사라진다. 장기 지식 보존이 필요하다면 반성문을 외부 파일에 직렬화하거나, 11-2-dspy 챕터에서 다룰 DSPy처럼 프롬프트를 직접 최적화하는 방식을 고려해야 한다.
언제 Reflexion을 선택하는가
섹션 제목: “언제 Reflexion을 선택하는가”| 조건 | Reflexion 추천 여부 |
|---|---|
| 명확한 기계적 평가기 존재 (테스트, 스코어) | 강력 추천 |
| Trial 횟수가 3~5회로 제한 가능 | 적합 |
| 태스크가 반복적으로 유사한 실패 패턴을 보임 | 적합 |
| 오픈엔드 창의적 태스크 | 부적합 |
| 레이턴시가 중요한 실시간 요청 | 부적합 (Trial만큼 지연 배수) |
| 컨텍스트 창이 매우 제한적 | 주의 필요 |
다음 챕터에서는 “실행 전에 먼저 계획을 세운다”는 다른 전략, Plan-and-Execute를 다룬다. Reflexion이 “실패 후 반성”이라면, Plan-and-Execute는 “실패 예방을 위한 사전 분해”에 초점을 맞춘다.