4장 — 리플렉션

패턴 개요

앞선 세 장은 순차 실행을 위한 체이닝, 동적 경로 선택을 위한 라우팅, 동시 작업 실행을 위한 병렬화를 살펴봤습니다. 이 패턴들 덕분에 에이전트는 복잡한 작업을 더 효율적이고 유연하게 수행할 수 있습니다. 그러나 워크플로가 아무리 정교하더라도 에이전트의 초기 출력이나 계획이 최적이 아니거나 정확하지 않을 수 있습니다. 바로 이 지점에서 리플렉션 패턴이 등장합니다.

원서의 Reflection은 ‘자기 검토’, ‘성찰’ 등으로 옮길 수 있으나, 에이전틱 패턴의 핵심 용어로 기능하고 음차 표기가 비교적 널리 쓰인다는 점을 고려해 이 책에서는 ‘리플렉션’으로 옮겼다고 옮긴이 주가 달려 있습니다.

리플렉션 패턴은 에이전트가 자신의 작업이나 출력, 내부 상태를 스스로 평가하고, 그 평가를 활용해 성능이나 응답을 개선(refine) 하는 방식입니다. 일종의 자기 교정 또는 자기 개선이라 할 수 있습니다.

출력을 다음 단계로 곧바로 넘기는 단순한 순차 체인이나 경로를 선택하는 라우팅과 달리, 리플렉션은 검토와 개선의 순환 구조(feedback loop)를 도입합니다. 에이전트는 단순히 출력을 만드는 데 그치지 않고, 그 출력(또는 출력을 만들어 낸 과정)을 다시 검토하고, 잠재적 문제나 개선 여지를 파악한 뒤, 이를 바탕으로 더 나은 버전을 만들거나 이후 행동을 수정합니다.

네 단계

단계 하는 일
1. 실행 에이전트가 작업을 수행하거나 초기 출력을 생성한다
2. 평가 / 비평 이전 단계의 결과를 분석한다. 이때 별도의 LLM 호출이나 사전 정의한 규칙을 활용하는 경우가 많다. 사실적 정확성, 일관성, 문체, 누락 여부, 지시 준수 여부 등을 점검한다
3. 리플렉션 / 개선 비평 결과를 바탕으로 개선 방향을 결정한다. 개선된 출력을 새로 생성하거나, 다음 단계의 매개변수를 조정하거나, 심지어 전체 계획 자체를 수정하는 것까지 포함한다
4. 반복 (선택적이지만 일반적) 개선된 출력이나 조정된 접근 방식을 다시 실행하고, 만족스러운 결과가 나오거나 종료 조건이 충족될 때까지 반복한다

생성자-비평자 모델

리플렉션 패턴을 구현하는 핵심이자 매우 효과적인 방식은, 이 과정을 작성자(Producer)와 비평자(Critic)라는 두 가지 논리적 역할로 나누는 것입니다. 흔히 “생성자-비평자(Generator-Critic)” 또는 “작성자-검토자(Producer-Reviewer)” 모델이라 불립니다.

단일 에이전트가 스스로 리플렉션을 수행할 수도 있지만, 전문화된 에이전트 두 개(또는 서로 다른 시스템 프롬프트로 LLM을 두 번 별도로 호출)를 활용하면 더 견고하고 편향 없는 결과를 얻는 경우가 많습니다.

  • 작성 에이전트 (Producer Agent) — 작업을 처음 실행합니다. 코드 작성이든, 블로그 게시물 초안 작성이든, 계획 수립이든, 콘텐츠 생성에만 온전히 집중합니다. 초기 프롬프트를 받아 첫 번째 출력 버전을 만들어 냅니다.
  • 비평 에이전트 (Critic Agent)오직 생성 에이전트가 만든 출력을 평가하는 역할만 맡습니다. 별도의 지시가 주어지며 고유한 페르소나(“당신은 시니어 소프트웨어 엔지니어입니다”, “당신은 꼼꼼한 팩트체커입니다”)를 부여받는 경우가 많습니다. 사실 정확성, 코드 품질, 문체 요구사항, 누락 여부 등 특정 기준에 비추어 결과물을 분석하고, 결함을 찾아내고, 개선점을 제안하며, 구조화된 피드백을 제공합니다.

이러한 역할 분리가 강력한 이유는, 에이전트가 자기 결과물을 검토할 때 생길 수 있는 “인지 편향(cognitive bias)”을 방지하기 때문입니다. 비평 에이전트는 새로운 시각으로 결과물을 검토하며, 오류와 개선할 부분을 찾는 데 전적으로 집중합니다. 비평 에이전트의 피드백은 생성 에이전트에 다시 전달되고, 생성 에이전트는 이를 길잡이 삼아 새롭게 개선된 출력을 만들어 냅니다.

다른 패턴과 만나는 지점

  • 목표 설정과 모니터링(11장) — 목표는 에이전트가 자기 평가를 할 때 궁극적인 기준점을 제공하고, 모니터링은 진행 상황을 추적합니다. 리플렉션은 교정 엔진 역할을 할 수 있는데, 모니터링으로 수집한 피드백을 분석해 이탈 지점을 파악하고 전략을 조정하는 것입니다.
  • 대화 기억(8장) — 리플렉션의 효과는 LLM이 대화 기억을 유지할 때 크게 향상됩니다. 기억이 없으면 각 리플렉션은 단일 이벤트에 불과합니다. 기억이 있으면 리플렉션은 누적되는 과정이 되어, 각 사이클이 이전 사이클 위에 쌓이면서 더 지능적이고 맥락에 맞는 개선이 가능해집니다. 에이전트는 과거 비평으로부터 학습하고 같은 오류를 반복하지 않을 수 있습니다.

실제 적용 및 활용 사례

리플렉션 패턴은 출력 품질이나 정확성, 복잡한 제약 조건 준수가 중요한 상황에서 특히 유용합니다.

영역 리플렉션이 하는 일 효과
창작 글쓰기와 콘텐츠 생성 — 블로그 게시물 작성 에이전트 초안을 생성하고 흐름·어조·명확성 측면에서 비평한 뒤, 비평 내용을 바탕으로 다시 작성한다. 품질 기준을 충족할 때까지 반복 더 다듬어지고 효과적인 콘텐츠
코드 생성과 디버깅 — 파이썬 함수 작성 에이전트 초기 코드를 작성하고, 테스트나 정적 분석을 실행해 오류·비효율을 파악한 뒤, 발견 결과를 바탕으로 코드를 수정한다 더 견고하고 제대로 작동하는 코드
복잡한 문제 해결 — 논리 퍼즐을 푸는 에이전트 한 단계를 제안한 뒤, 그것이 해결에 더 가까이 다가가는지 아니면 모순이 생기는지 평가하고, 필요하면 되돌리거나 다른 단계를 선택한다 복잡한 문제를 탐색하고 풀어 가는 능력 향상
요약과 정보 종합 — 긴 문서를 요약하는 에이전트 초기 요약을 생성하고, 원본 문서의 핵심 포인트와 비교한 뒤, 누락된 정보를 포함하거나 정확성을 높이는 방향으로 개선한다 더 정확하고 문서 전반을 아우르는 요약
계획 수립과 전략 — 목표 달성 행동을 계획하는 에이전트 계획을 생성하고, 실행을 시뮬레이션하거나 제약 조건에 비추어 실현 가능성을 평가한 뒤, 평가 결과를 바탕으로 계획을 수정한다 더 효과적이고 현실적인 계획
대화형 에이전트 — 고객 지원 챗봇 사용자가 응답한 뒤 대화 이력과 마지막으로 생성한 메시지를 검토해, 대화의 일관성을 유지하고 사용자의 최신 입력에 정확히 응답하도록 한다 더 자연스럽고 효과적인 대화

실습 코드 예제 (LangChain)

완전한 반복적 리플렉션 과정을 구현하려면 상태 관리와 순환 실행을 위한 메커니즘이 필요합니다. LangGraph 같은 그래프 기반 프레임워크에서는 이러한 기능을 기본으로 지원하고, 사용자 정의 절차 코드로도 처리할 수 있습니다.

아래 예제는 팩토리얼을 계산하는 파이썬 함수를 반복적으로 생성하고 개선하는 리플렉션 순환 구조입니다. 작업 프롬프트에서 출발해 초기 코드를 생성한 뒤, 시니어 소프트웨어 엔지니어 역할을 시뮬레이션한 비평을 바탕으로 코드를 반복 개선합니다. 비평 단계에서 코드가 완벽하다고 판단되거나 최대 반복 횟수에 도달하면 순환이 종료됩니다.

pip install langchain langchain-community langchain-openai
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import SystemMessage, HumanMessage

# --- 설정 ---
# .env 파일에서 환경 변수를 로드한다(OPENAI_API_KEY)
load_dotenv()

# API 키가 설정되어 있는지 확인한다
if not os.getenv("OPENAI_API_KEY"):
    raise ValueError("OPENAI_API_KEY not found in .env file. Please add it.")

# Chat LLM을 초기화한다. 더 나은 추론 성능을 위해 gpt-4o를 사용한다.
# 더 일관된 출력을 얻기 위해 temperature를 낮게 설정한다.
llm = ChatOpenAI(model="gpt-4o", temperature=0.1)


def run_reflection_loop():
    """
    다단계 AI 리플렉션 순환 구조를 시연하여
    Python 함수를 점진적으로 개선한다.
    """
    # --- 핵심 작업 ---
    task_prompt = """
Your task is to create a Python function named `calculate_factorial`.
This function should do the following:
1. Accept a single integer `n` as input.
2. Calculate its factorial (n!).
3. Include a clear docstring explaining what the function does.
4. Handle edge cases: The factorial of 0 is 1.
5. Handle invalid input: Raise a ValueError if the input is a negative number.
"""

    # --- 리플렉션 순환 구조 ---
    max_iterations = 3
    current_code = ""
    # 각 단계에 맥락을 제공하기 위해 대화 이력을 누적한다.
    message_history = [HumanMessage(content=task_prompt)]

    for i in range(max_iterations):
        print("\n" + "="*25 + f" REFLECTION LOOP: ITERATION {i + 1} " + "="*25)

        # --- 1. 생성 / 비평 단계 ---
        # 첫 번째 반복에서는 생성하고, 이후 반복에서는 비평을 통해 내용을 개선한다.
        if i == 0:
            print("\n>>> STAGE 1: GENERATING initial code...")
            # 첫 번째 메시지는 작업 프롬프트뿐이다.
            response = llm.invoke(message_history)
            current_code = response.content
        else:
            print("\n>>> STAGE 1: REFINING code based on previous critique...")
            # 이 시점의 메시지 이력에는 작업,
            # 직전 코드, 그리고 직전 비평이 포함되어 있다.
            # 모델에 비평을 적용하도록 지시한다.
            message_history.append(HumanMessage(content="Please refine the code using the critiques provided."))
            response = llm.invoke(message_history)
            current_code = response.content

        print("\n--- Generated Code (v" + str(i + 1) + ") ---\n" + current_code)
        message_history.append(response)  # 생성된 코드를 이력에 추가한다

        # --- 2. 리플렉션 단계 ---
        print("\n>>> STAGE 2: REFLECTING on the generated code...")

        # 리플렉터 에이전트용 프롬프트를 생성한다.
        # 모델에 시니어 코드 리뷰어 역할을 맡도록 요청한다.
        reflector_prompt = [
            SystemMessage(content="""
You are a senior software engineer and an expert in Python.
Your role is to perform a meticulous code review.
Critically evaluate the provided Python code based on the original task requirements.
Look for bugs, style issues, missing edge cases, and areas for improvement.
If the code is perfect and meets all requirements, respond with the single phrase 'CODE_IS_PERFECT'.
Otherwise, provide a bulleted list of your critiques.
"""),
            HumanMessage(content=f"Original Task:\n{task_prompt}\n\nCode to Review:\n{current_code}")
        ]

        critique_response = llm.invoke(reflector_prompt)
        critique = critique_response.content

        # --- 3. 종료 조건 ---
        if "CODE_IS_PERFECT" in critique:
            print("\n--- Critique ---\nNo further critiques found. The code is satisfactory.")
            break

        print("\n--- Critique ---\n" + critique)

        # 다음 개선 순환을 위해 비평을 이력에 추가한다.
        message_history.append(HumanMessage(content=f"Critique of the previous code:\n{critique}"))

    print("\n" + "="*30 + " FINAL RESULT " + "="*30)
    print("\nFinal refined code after the reflection process:\n")
    print(current_code)


if __name__ == "__main__":
    run_reflection_loop()

코드 읽는 포인트

  • 같은 LLM이 두 역할을 나눠 맡습니다. 생성은 누적된 message_history 로, 비평은 별도의 reflector_prompt(시니어 엔지니어 시스템 메시지)로 호출합니다. 서로 다른 시스템 프롬프트를 쓰는 것만으로 역할 분리가 성립합니다.
  • 비평 호출에는 대화 이력을 주지 않습니다. reflector_prompt 는 원래 작업과 현재 코드만 담습니다. 자기가 방금 쓴 코드라는 사실을 모르는 상태에서 보게 하는 것이 인지 편향을 줄이는 장치입니다.
  • 종료 조건이 두 개입니다. 비평이 CODE_IS_PERFECT 를 반환하거나, max_iterations 에 도달하면 멈춥니다. 순환 패턴에서 종료 조건이 없으면 무한 루프가 됩니다.
  • 대화 이력이 각 단계마다 유지되어 생성/개선 단계와 리플렉션 단계 모두에 맥락을 제공합니다.

실습 코드 예제 (구글 ADK)

ADK 예제는 생성 에이전트와 검토 에이전트를 명시적으로 정의합니다. LangChain 예제가 전용 프롬프트로 비평자 페르소나를 만든 것과 대비됩니다.

from google.adk.agents import SequentialAgent, LlmAgent

# 첫 번째 에이전트가 초안을 생성한다.
generator = LlmAgent(
    name="DraftWriter",
    description="Generates initial draft content on a given subject.",
    instruction="Write a short, informative paragraph about the user's subject.",
    output_key="draft_text"   # 출력이 이 상태 키에 저장된다.
)

# 두 번째 에이전트가 첫 번째 에이전트의 초안을 비평한다.
reviewer = LlmAgent(
    name="FactChecker",
    description="Reviews a given text for factual accuracy and provides a structured critique.",
    instruction="""
You are a meticulous fact-checker.
1. Read the text provided in the state key 'draft_text'.
2. Carefully verify the factual accuracy of all claims.
3. Your final output must be a dictionary containing two keys:
   - "status": A string, either "ACCURATE" or "INACCURATE".
   - "reasoning": A string providing a clear explanation for your status, citing specific issues if any are found.
""",
    output_key="review_output"   # 구조화된 딕셔너리가 여기에 저장된다.
)

# SequentialAgent는 generator가 reviewer보다 먼저 실행되도록 보장한다.
review_pipeline = SequentialAgent(
    name="WriteAndReview_Pipeline",
    sub_agents=[generator, reviewer]
)

# 실행 흐름:
# 1. generator가 실행된다 -> 생성한 문단을 state['draft_text']에 저장한다.
# 2. reviewer가 실행된다 -> state['draft_text']를 읽고
#    딕셔너리 출력을 state['review_output']에 저장한다.

reviewer 의 출력은 status("ACCURATE" 또는 "INACCURATE")와 reasoning(판정 이유) 두 키로 구성된 딕셔너리이며, 상태 키 review_output 에 저장됩니다. 비평을 자유 텍스트가 아니라 구조화된 형태로 받아 두면 후속 단계에서 조건 분기를 걸기 쉽습니다.

참고로 이 예제는 한 번의 생성-검토로 끝나는 순차 파이프라인입니다. ADK의 LoopAgent 를 활용하면 반복적 구현도 가능합니다.

대가 — 반드시 짚고 넘어갈 트레이드오프

리플렉션 패턴이 출력 품질을 크게 향상시키지만, 중요한 트레이드오프가 수반됩니다.

  • 비용과 지연 시간 — 개선 순환을 돌 때마다 새로운 LLM 호출이 필요할 수 있어 비용과 지연 시간이 증가합니다. 시간에 민감한 애플리케이션에는 적합하지 않을 수 있습니다.
  • 메모리 소모 — 반복이 거듭될수록 초기 출력, 비평, 후속 개선 내용이 포함된 대화 이력이 계속 누적됩니다. 모델의 컨텍스트 윈도를 초과하거나 API 서비스의 속도 제한에 걸릴 위험도 커집니다.

리플렉션 패턴 개요도

자기 성찰 — 하나의 에이전트가 출력을 만들고, 그 출력을 자신에게 되먹여 다시 다듬습니다.

  사용자 ──► 프롬프트 ──► 에이전트 ──► 출력(1)
     ▲                       ▲   │          │
     │                       └───┘          │
     │                     리플렉션          │
     └───────────────────────────────────────┘

그림 4.1 — 리플렉션 디자인 패턴, 자기 성찰

생성 에이전트와 비평 에이전트 — 만드는 쪽과 평가하는 쪽을 나눕니다.

  사용자 ──► 프롬프트 ──► 에이전트: 공급자 ──► 에이전트: 비평가
     ▲                          │                    │
     │                          ▼                    │
     └──────────────────────  출력  ◄─── 리플렉션 ───┘

그림 4.2 — 리플렉션 디자인 패턴, 생성 에이전트와 비평 에이전트

정리

리플렉션이란 무엇인가?

에이전트의 초기 출력은 부정확하거나, 불완전하거나, 복잡한 요구사항을 충족하지 못하는 등 최적이 아닌 경우가 많습니다. 기본적인 에이전틱 워크플로에는 에이전트가 자신의 오류를 인식하고 수정하는 과정이 내장되어 있지 않습니다. 이 문제는 에이전트가 자신의 결과물을 스스로 평가하게 하거나, 별도의 논리 에이전트를 비평자로 도입하는 좀 더 견고한 방법으로 해결할 수 있습니다. 이렇게 하면 품질과 무관하게 초기 응답이 곧바로 최종 결과가 되는 것을 방지할 수 있습니다.

왜 사용하는가?

리플렉션 패턴은 자기 교정과 개선 메커니즘을 도입해 이 문제를 해결합니다. “생성자” 에이전트가 출력을 만들면, “비평자” 에이전트(또는 생성자 자신)가 사전에 정의된 기준에 따라 이를 평가하는 순환 구조를 구성합니다. 이 비평 결과를 바탕으로 개선된 버전이 만들어집니다. 생성, 평가, 개선을 반복하면서 최종 결과물 품질이 점진적으로 향상되어, 더 정확하고 일관되며 신뢰할 수 있는 결과물을 얻을 수 있습니다.

언제 사용해야 하는가?

최종 출력의 품질, 정확성, 상세함이 속도와 비용보다 중요할 때 사용합니다. 다듬어진 장문 콘텐츠 생성, 코드 작성 및 디버깅, 상세한 계획 수립 같은 작업에 특히 효과적입니다. 높은 객관성이 요구되거나, 범용 생성 에이전트가 놓칠 수 있는 전문적 평가가 필요한 작업에는 별도의 비평 에이전트를 활용하는 것이 좋습니다.

핵심 정리

  • 리플렉션 패턴의 가장 큰 장점은 반복적으로 자기 교정하고 출력을 개선할 수 있다는 점이며, 그 결과 품질·정확성·복잡한 지시 준수도가 크게 향상됩니다.
  • 이 패턴은 실행, 평가/비평, 개선으로 이어지는 순환 구조를 포함합니다. 고품질이거나 정확하거나 섬세한 차이를 반영해야 하는 출력이 필요한 작업에서 리플렉션은 필수적입니다.
  • 강력한 구현 방식은 생성자-비평자 모델로, 별도의 에이전트(또는 프롬프트로 지정된 별도의 역할)가 초기 출력을 평가합니다. 생성 역할과 검토 역할을 분리하면 객관성이 높아지고, 더 전문화되고 구조화된 피드백이 가능해집니다.
  • 다만 이러한 이점에는 지연 시간과 연산 비용 증가라는 대가가 따르며, 모델의 컨텍스트 윈도를 초과하거나 API 서비스의 속도 제한에 걸릴 위험도 커집니다.
  • 완전한 반복적 리플렉션에는 보통 LangGraph 같은 상태 기반 워크플로가 필요하지만, 단일 리플렉션 단계는 LangChain에서 LCEL을 사용해 구현할 수 있습니다.
  • 구글 ADK에서는 한 에이전트의 출력을 다른 에이전트가 비평하는 순차적 워크플로를 통해 리플렉션을 구현할 수 있으며, 후속 개선 단계를 추가할 수 있습니다.
  • 이 패턴을 활용하면 에이전트가 자기 교정을 수행하고, 시간이 지나면서 성능을 향상시킬 수 있습니다.

결론

리플렉션 패턴은 에이전트 워크플로 안에서 자기 교정의 핵심 메커니즘을 제공하며, 일회성 실행을 넘어 반복적 개선을 가능하게 합니다. 이는 시스템이 출력을 생성하고, 특정 기준에 따라 평가한 뒤, 그 평가 결과를 활용해 개선된 결과물을 만들어 내는 순환 구조를 통해 달성됩니다. 이 평가는 에이전트 스스로 수행할 수도 있고, 별도의 비평 에이전트가 맡을 때 더 효과적인 경우도 많은데, 이는 이 패턴에서 핵심적인 아키텍처 선택에 해당합니다.

완전히 자율적인 다단계 리플렉션 과정을 구현하려면 상태 관리를 위한 견고한 아키텍처가 필요하지만, 핵심 원리는 단일 생성-비평-개선 사이클만으로도 효과적으로 보여줄 수 있습니다. 리플렉션을 제어 구조로 활용하면 다른 기본 패턴과 결합하여 더 견고하고 기능적으로 복잡한 에이전틱 시스템을 구축할 수 있습니다.

참고 문헌