반성과 자기 개선
실패한 시도를 평가하고 그 원인을 말로 정리해 다음 시도에 넘기는 Reflexion의 구조를 이해하고, 함수 생성·테스트·반성·재시도 루프를 직접 만든다.
시행착오와 반성
- 실제 업무는 한 번에 성공하기 어려움. 시도 → 실패 → 원인 파악 → 다시 시도가 불가피.
- 작업마다 모델을 다시 학습시키기는 어려움. 대안: 가중치는 그대로 두고, 실패의 원인과 교훈을 글로 남겨 다음 시도의 컨텍스트에 넣기.
- 반성(reflection): 지난 기록을 다시 보여 주는 데서 그치지 않고 "무엇이 왜 틀렸고 다음엔 어떻게 할지"라는 추상적인 정보를 뽑아내는 것.
Reflexion
- Reflexion: 가중치 갱신 대신 언어 피드백으로 에이전트를 강화. 언어적 강화(verbal reinforcement).
- 구성: 행위자(Actor, 행동 생성) → 평가자(Evaluator, 테스트·규칙·LLM으로 채점) → 자기 반성(Self-Reflection, 원인과 고칠 점을 글로) → 기억(쌓인 반성을 다음 시도에서 참조).
- 결과: HumanEval 코딩 벤치마크에서 pass@1 91%. 같은 시점 GPT-4의 80%보다 높음.
Mermaid 다이어그램 원문
flowchart LR
T[과제] --> A[행위자<br/>코드 생성]
A --> E[평가자<br/>테스트 실행]
E -->|통과| D[완료]
E -->|실패 메시지| R[자기 반성<br/>원인과 고칠 점]
R --> M[(경험<br/>반성 목록)]
M --> A대화형 다이어그램을 불러오기 전에는 원문으로 표시됩니다.
출처: Shinn 외(2023), Reflexion: Language Agents with Verbal Reinforcement Learning.
생성자-평가자 루프
- 생성자(generator)가 결과를 만들고 평가자(evaluator)가 검토해 되돌려 주는 구조. Reflexion도 그 하나.
- 평가 신호: 실행 결과(단위 테스트, 스키마 검증. 확실하고 쌈), 규칙(글자 수, 반복 감지), 모델 평가(LLM 평가자. 범용이지만 평가 모델의 오류를 물려받음).
- 정답을 찾기는 어려워도 맞는지 확인하기는 쉬운 문제에서 잘 작동. 예: 자율 에이전트의 AlphaEvolve.
- 평가자가 약하면 루프도 약함. 테스트가 빈약하면 테스트만 통과하는 엉뚱한 코드가 나옴.
실습 준비
- 과제: 정수 금액을 한국식 단위로 바꾸는 함수
format_won. 예:12345→"1만 2,345원". - 모델에는 예시 하나만 주고, 세부 규칙(0원, 억 단위, 빈 단위 생략, 음수)은 테스트에만 담음. 요구사항이 다 적혀 있지 않은 실제 업무를 흉내 냄.
!pip install -q openai
import builtins
from pathlib import Path
from openai import OpenAI
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=Path("openrouter-key.txt").read_text().strip(),
)
model = "openai/gpt-4.1-mini"
task = """정수 금액을 한국식 단위로 바꾸는 파이썬 함수 format_won(amount)을 작성하라.
예: format_won(12345) == "1만 2,345원"
- 함수 정의만 반환하라. 마크다운 코드 펜스 없이 코드만.
- import는 쓰지 마라."""
tests = [(0, "0원"), (10000, "1만원"), (12345, "1만 2,345원"),
(123456789, "1억 2,345만 6,789원"), (100000000, "1억원"), (100005000, "1억 5,000원"),
(-12345, "-1만 2,345원")]
평가자: 테스트 실행
exec()에 허용할 내장 함수만 담은 이름 공간을 넘겨 실행.open,__import__가 없어 파일 읽기나 import가 실패함.- 주의: 이름 공간 제한은 샌드박스가 아님. 우회 방법이 알려져 있어 악의적인 코드를 막지 못함. 실제 서비스에서는 코드 실행의 Monty나 외부 샌드박스 사용.
SAFE_BUILTINS = {name: getattr(builtins, name) for name in [
"abs", "divmod", "enumerate", "format", "int", "len", "list", "map", "max", "min",
"range", "reversed", "str", "sum", "zip", "isinstance"]}
def run_tests(code):
namespace = {"__builtins__": SAFE_BUILTINS}
try:
exec(code, namespace)
return [f"format_won({n}): 기대 {want!r}, 실제 {got!r}" for n, want in tests
if (got := namespace["format_won"](n)) != want]
except Exception as e:
return [f"실행 실패: {e!r}"]
run_tests는 실패 메시지 목록을 반환(빈 목록이면 모두 통과). 메시지에 입력·기대값·실제값이 함께 있어 다음 단계의 반성 재료가 됨.
반성 루프
- 생성(행동) → 테스트(관찰) → 반성 → 다시 생성. 종료 조건은 테스트 전부 통과, 안전장치는 최대 시도 횟수.
- 반성 단계에서는 코드를 쓰지 말고 원인 진단과 고칠 점만 쓰게 함. 고친 코드를 바로 쓰게 하지 않고 진단을 먼저 글로 남기는 것이 Reflexion의 핵심.
def chat(prompt):
return client.chat.completions.create(
model=model, messages=[{"role": "user", "content": prompt}]).choices[0].message.content
def reflexion(task, max_trials=4):
reflections, code = [], "" # 장기 기억
for trial in range(1, max_trials + 1):
code = chat(f"{task}\n[이전 코드]\n{code}\n[지난 반성]\n" + "\n".join(reflections)) # 행위자
failures = run_tests(code) # 평가자
print(f"시도 {trial}: 실패 {len(failures)}개", *failures[:2], sep="\n ")
if not failures:
return code
reflection = chat( # 자기 반성
"다음 함수가 테스트에 실패했다. 원인과 고칠 점을 두 문장으로 써라. 코드는 쓰지 마라.\n\n"
f"[코드]\n{code}\n\n[실패한 테스트]\n" + "\n".join(failures))
print("반성:", reflection)
reflections.append(reflection)
return code
final_code = reflexion(task)
실행 결과
시도 1: 실패 2개
format_won(123456789): 기대 '1억 2,345만 6,789원', 실제 '1억 2345만 6,789원'
format_won(-12345): 기대 '-1만 2,345원', 실제 '-12,345원'
반성: 음수에 대한 처리가 없고, 단위별 금액을 계산할 때 절댓값이 아닌 원래 값을 사용하여 음수나 단위 내 숫자의 자리수를 제대로 나누지 못한다. 따라서 음수를 고려해 절댓값으로 단위를 나눈 후, 마지막에 부호를 붙이는 방식을 적용해야 한다.
시도 2: 실패 1개
format_won(123456789): 기대 '1억 2,345만 6,789원', 실제 '1억 2345만 6,789원'
반성: 문제는 10,000 단위로 자를 때 천 단위 구분 쉼표를 붙이지 않아서 '2345만'처럼 출력된 것이다. 각 단위 금액에 천 단위 구분 쉼표를 넣어 출력하도록 수정해야 한다.
시도 3: 실패 0개
- 위 실행에서는 첫 시도에 2개 실패(만 단위 앞 숫자에 쉼표 없음, 음수 처리 없음. 실패 메시지는 앞의 2개만 출력). 첫 반성 뒤 음수는 고쳤지만 쉼표 문제가 남아 1개 실패. 두 번째 반성 뒤 세 번째 시도에서 모두 통과.
- 반성은 실패 메시지를 일반적인 규칙으로 바꿈. "
-12,345원이 나온다" → "절댓값으로 단위를 나눈 뒤 마지막에 부호를 붙인다". - 첫 반성은 음수만 짚고 쉼표 문제의 원인은 흐릿하게 지나감. 두 번째 반성에서야 "각 단위 금액에 쉼표를 넣는다"로 정확해짐. 반성이 늘 완전하지는 않음.
- 실행마다 실수의 종류와 통과까지의 시도 횟수가 달라짐.
print(final_code)로 최종 코드를,run_tests(final_code)로 통과 여부를 직접 확인해 볼 것.
반성의 한계
- 평가자가 전부: 테스트가 요구사항을 다 담지 못하면 통과했지만 틀린 코드가 나옴. 위 실습도 테스트 7개 밖의 입력(예: 1조 이상)은 확인하지 않음.
- 테스트 맞추기: 입력마다 정답을 하드코딩하는 코드가 나올 수 있음. 숨겨 둔 테스트로 따로 확인하는 것이 안전.
- 틀린 반성: 원인을 잘못 짚으면 엉뚱한 방향으로 감. 같은 실패가 반복되면 사람에게 넘기는 장치 필요.
- 비용과 컨텍스트: 시도마다 모델 호출 두 번. 반성 목록이 길어지면 컨텍스트를 차지함(12주 기억과 컨텍스트 관리).
퀴즈
Reflexion이 실패에서 배우는 방식은?
퀴즈를 풀려면 대화형 기능을 불러와야 합니다.