왜 단순 재시도는 실패하는가

LLM 에이전트가 도구 호출이나 코드 생성에 실패했을 때, 같은 프롬프트로 그냥 다시 부르면 대부분 같은 실수를 반복한다. 온도(temperature)를 올려도 우연에 기댈 뿐이다. 문제의 본질은 모델이 무엇이 왜 틀렸는지를 다음 시도의 입력으로 전달받지 못한다는 데 있다. Reflexion은 실행 결과에서 얻은 구체적 피드백을 언어화해 다음 시도에 주입하는 루프다. 즉 재시도(retry)가 아니라 반영 기반 재시도(reflect-then-retry)다.

루프의 4단계 구조

Reflexion 루프는 다음 순서를 최대 N회 반복한다. 핵심은 3번의 검증기(verifier)가 사람의 판단이 아니라 결정론적이거나 저비용이어야 한다는 점이다.

  • Act: 모델이 행동(코드, 도구 호출, 답변)을 생성한다.
  • Execute: 실제로 실행해 관측 결과를 얻는다(테스트, 컴파일, API 응답).
  • Evaluate: 성공/실패를 판정한다. 통과하면 즉시 종료한다.
  • Reflect: 실패 원인을 짧은 자연어로 요약해 메모리에 쌓고, 다음 Act의 입력에 붙인다.

최소 구현

아래는 코드 생성 에이전트에 적용한 뼈대다. 검증기는 실제 테스트 실행이고, 반영은 별도 호출로 분리해 "무엇을 다르게 할지"만 뽑아낸다.

import anthropic

client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"

def run(system, task, verify, max_iters=4):
    memory = []  # 누적된 반영 노트
    for i in range(max_iters):
        hint = "\n".join(f"- 지난 시도 교훈: {m}" for m in memory)
        code = ask(system, f"{task}\n{hint}")
        ok, log = verify(code)          # 결정론적 검증 (예: pytest)
        if ok:
            return code, i + 1
        note = reflect(task, code, log) # 실패 로그 → 한 줄 교훈
        memory.append(note)
    raise RuntimeError("max_iters 도달")

def reflect(task, code, log):
    msg = client.messages.create(
        model=MODEL, max_tokens=200,
        messages=[{"role": "user", "content":
            f"과제:{task}\n코드:{code}\n실패로그:{log}\n"
            "다음 시도에서 바꿔야 할 핵심 한 가지를 한 문장으로."}])
    return msg.content[0].text.strip()

검증기 없이는 성립하지 않는다

Reflexion의 성능은 전적으로 Evaluate 단계의 신뢰도에 달려 있다. 검증기가 틀린 실패를 통과시키거나(false negative) 옳은 답을 막으면 루프는 잘못된 방향으로 수렴한다. 우선순위는 다음과 같다.

검증 방식신뢰도비용적용 예
단위 테스트/컴파일높음낮음코드 생성
스키마·타입 검사높음낮음구조화 출력, SQL
규칙 기반 assertion중간낮음도구 호출 인자
LLM-judge낮음~중간높음자유형 텍스트

가능하면 상단의 결정론적 방식을 쓰고, LLM-judge는 최후에만 쓴다. 판정 기준을 프롬프트가 아니라 코드로 고정하는 것이 재현성의 핵심이다.

메모리는 짧고 구체적으로

반영 노트를 원본 로그째로 쌓으면 컨텍스트가 폭발하고 모델이 노이즈에 묻힌다. 노트는 "인덱스가 아니라 키로 조회해야 함"처럼 행동 지침 한 문장으로 압축한다. 또한 오래된 노트가 새 실패와 충돌할 수 있으므로, 성공 시 메모리를 비우고 유사 과제에는 요약본만 재사용한다. 아래처럼 설정으로 한계를 명시해 두면 운영이 안전하다.

reflexion:
  max_iters: 4          # 무한 루프 방지
  memory_max_notes: 5   # 오래된 교훈은 폐기
  note_max_chars: 160
  stop_on_pass: true
  budget:
    max_tokens_total: 40000   # 회당 총 토큰 상한
    max_wall_seconds: 90
  fallback: escalate_to_human  # 소진 시 사람에게 이관

주의점과 실패 모드

실무에서 자주 겪는 함정을 정리한다.

  • 비용 폭증: 반영은 추가 호출이다. 반드시 반복 상한과 토큰 예산을 걸고, 조기 성공 시 즉시 탈출한다.
  • 수렴 실패: 검증기가 모호하면 모델이 두 오답 사이를 오간다. 진동이 감지되면 조기 중단하고 이관한다.
  • 과적합 반영: 특정 실패에만 맞춘 교훈이 다음 과제를 오염시킨다. 노트는 과제 범위 안에서만 유지한다.
  • 부작용: 실행 단계가 파일·DB를 변경한다면 재시도마다 격리(임시 워크스페이스, 트랜잭션 롤백)가 필요하다.

언제 쓰고 언제 피할까

Reflexion은 실패를 값싸고 명확하게 판정할 수 있는 과제에서 가장 효과가 크다. 코드 생성, SQL 작성, 구조화된 도구 호출이 대표적이다. 반대로 정답 검증이 사람 판단에만 의존하거나 실행 부작용을 되돌릴 수 없는 작업에서는 루프가 비용만 늘리고 품질은 보장하지 못한다. 도입 전에 "무엇으로 실패를 자동 판정할 것인가"에 먼저 답할 수 있어야 한다. 그 답이 없다면 Reflexion이 아니라 검증기 설계부터 시작해야 한다.