콘텐츠로 이동

Plan-and-Execute: 계획과 실행의 분리

ReAct 루프는 매 이터레이션마다 “지금 무엇을 해야 하는가”를 즉흥적으로 결정한다. 이 방식은 단기 태스크에 효율적이지만, 수십 단계가 필요한 복잡한 작업에서 문제가 생긴다. 모델이 현재 상태에만 집중하다 보면 전체 목표를 잃어버리는 목표 표류(goal drift) 현상이 나타나고, 작업이 길어질수록 이전 단계의 결정이 왜 내려졌는지 맥락이 희미해진다.

Plan-and-Execute 패턴은 이 문제를 역할 분리로 해결한다. Planner는 태스크를 받아 전체 실행 계획을 수립하고, Executor는 그 계획의 각 단계를 순서대로 수행한다. 두 역할이 분리되면 Planner는 글로벌 시야를 유지하고, Executor는 각 단계의 세부 사항에 집중할 수 있다.

단순 Plan-and-Solve 프롬프팅 vs. 에이전틱 Plan-and-Execute

섹션 제목: “단순 Plan-and-Solve 프롬프팅 vs. 에이전틱 Plan-and-Execute”

먼저 두 개념을 구분해야 한다.

Plan-and-Solve 프롬프팅 (Wang et al., ACL 2023, arXiv:2305.04091)은 단일 LLM 호출 내에서 “먼저 계획을 세우고, 그 계획에 따라 풀어라”는 지시를 시스템 프롬프트에 넣는 기법이다. 도구 호출 없이 텍스트 생성만으로 추론 품질을 높이는 프롬프트 엔지니어링 기법에 가깝다.

에이전틱 Plan-and-Execute는 이를 멀티스텝 루프로 확장한다. Planner가 구조화된 계획을 출력하고, Executor가 각 단계를 실제 도구 호출로 수행하며, 필요하면 Replanner가 계획을 수정한다.

┌─────────────────────────────────────────────────────────────────┐
│ Plan-and-Execute 흐름 │
│ │
│ 태스크 입력 │
│ ▼ │
│ ┌──────────┐ 계획 (JSON 스텝 배열) │
│ │ Planner │──────────────────────────────────┐ │
│ └──────────┘ │ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ Step 1 실행 (Executor) │ │
│ │ → 도구 호출 → 결과 │ │
│ └──────────┬─────────────────┘ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ Step 2 실행 (Executor) │ │
│ │ → 도구 호출 → 결과 │ │
│ └──────────┬─────────────────┘ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ 계획과 현실 불일치? │ │
│ │ YES → Replanner 호출 │ │
│ │ NO → 다음 스텝 계속 │ │
│ └──────────┬─────────────────┘ │
│ ▼ │
│ 최종 답변 생성 │
└─────────────────────────────────────────────────────────────────┘

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

from dataclasses import dataclass
from typing import Any
@dataclass
class Step:
index: int
description: str
tool: str
arguments: dict[str, Any]
result: str = ""
completed: bool = False
def plan_and_execute(model, tools: dict, task: str, max_replan: int = 2) -> str:
# 1. Planner: 전체 계획 수립
plan_prompt = (
f"태스크: {task}\n\n"
"단계별 실행 계획을 JSON 배열로 작성하라. 각 항목은 "
"{'step': 번호, 'description': 설명, 'tool': 도구명, 'args': {{...}}} 형식."
)
plan_response = model.generate([{"role": "user", "content": plan_prompt}])
steps = parse_plan(plan_response.text) # JSON 파싱
replan_count = 0
results = []
for i, step in enumerate(steps):
# 2. Executor: 각 단계 수행
fn = tools.get(step.tool)
if fn is None:
step_result = f"ERROR: 도구 '{step.tool}'를 찾을 수 없음"
else:
try:
step_result = fn(**step.arguments)
except Exception as e:
step_result = f"ERROR: {e}"
step.result = step_result
step.completed = True
results.append(step)
# 3. 계획-현실 불일치 감지 및 Replanning
if "ERROR" in step_result and replan_count < max_replan:
replan_prompt = (
f"원래 태스크: {task}\n"
f"완료된 스텝: {[s.description for s in results if s.completed]}\n"
f"현재 스텝 실패: {step.description}\n"
f"에러: {step_result}\n"
f"남은 스텝을 수정하거나 새로 계획을 세워라. JSON 배열로 반환."
)
replan_resp = model.generate([{"role": "user", "content": replan_prompt}])
remaining_steps = parse_plan(replan_resp.text)
# 남은 계획을 재설정
steps = list(results) + remaining_steps
replan_count += 1
# 4. 최종 종합
context = "\n".join(
f"스텝 {s.index}: {s.description}{s.result}" for s in results
)
final_prompt = f"태스크: {task}\n\n실행 결과:\n{context}\n\n최종 답변을 작성하라."
return model.generate([{"role": "user", "content": final_prompt}]).text

Stale Plan: 계획이 낡아버리는 문제

섹션 제목: “Stale Plan: 계획이 낡아버리는 문제”

Plan-and-Execute의 가장 전형적인 실패 모드는 **스테일 플랜(stale plan)**이다. 계획은 태스크 시작 시점의 정보를 기반으로 수립된다. 그런데 실행 도중 새로운 정보가 나오면 계획이 더 이상 유효하지 않을 수 있다.

예를 들어, “경쟁사 A와 B의 최신 가격을 비교하라”는 태스크로 “1단계: A 가격 검색, 2단계: B 가격 검색, 3단계: 비교표 작성”이라는 계획을 세웠다고 하자. 1단계에서 A가 서비스를 종료했다는 사실을 발견하면, 2단계와 3단계는 의미를 잃는다. Replanning 없이 계속 진행하면 무의미한 비교표가 생성된다.

이 문제에 대한 접근 방법은 두 가지다.

전략 설명 비용
조건부 Replanning 각 스텝 후 “계획 수정 필요 여부”를 판단하는 가드를 둔다 중간
Dynamic Plan 계획을 고정하지 않고 매 스텝마다 다음 스텝을 결정 (ReAct에 가까워짐) 높음
Rigid Plan Replanning 없이 계획 그대로 수행, 에러는 무시 낮음

두 패턴은 상호 배타적이 아니다. LangGraph 같은 프레임워크에서는 두 가지를 조합해 “Planner가 계획을 세우고, 각 단계 내부에서 ReAct 루프를 돌린다”는 계층 구조를 만든다.

┌───────────────────────────────────────────────┐
│ 태스크 특성에 따른 패턴 선택 │
├─────────────────────┬─────────────────────────┤
│ ReAct 적합 │ Plan-and-Execute 적합 │
├─────────────────────┼─────────────────────────┤
│ 탐색적·즉흥적 태스크 │ 절차가 사전에 명확한 태스크 │
│ 단기 목표 │ 장기 다단계 목표 │
│ 계획 수립 비용 큼 │ 실행 전 전체 맥락이 중요 │
│ 실시간 피드백 의존 │ 계획 공유·감사 필요 │
└─────────────────────┴─────────────────────────┘

한 가지 실용적 판단 기준: 계획을 인간이 미리 검토할 수 있어야 한다면 Plan-and-Execute를 선택하라. Planner의 출력을 Human-in-the-loop로 승인한 뒤 Executor를 돌리는 패턴은 위험이 높은 자동화 태스크에서 특히 유용하다.

다음 챕터에서는 단일 시도 안에서 스스로 비판하고 개선하는 Self-Refine 패턴과, 여러 독립 답변의 다수결로 신뢰도를 높이는 Self-Consistency를 함께 살펴본다.

참고 자료