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 → 다음 스텝 계속 │ ││ └──────────┬─────────────────┘ ││ ▼ ││ 최종 답변 생성 │└─────────────────────────────────────────────────────────────────┘Python 구현 예시
섹션 제목: “Python 구현 예시”개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다.
from dataclasses import dataclassfrom typing import Any
@dataclassclass 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}]).textStale 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 없이 계획 그대로 수행, 에러는 무시 | 낮음 |
Plan-and-Execute vs. ReAct 선택 기준
섹션 제목: “Plan-and-Execute vs. ReAct 선택 기준”두 패턴은 상호 배타적이 아니다. LangGraph 같은 프레임워크에서는 두 가지를 조합해 “Planner가 계획을 세우고, 각 단계 내부에서 ReAct 루프를 돌린다”는 계층 구조를 만든다.
┌───────────────────────────────────────────────┐│ 태스크 특성에 따른 패턴 선택 │├─────────────────────┬─────────────────────────┤│ ReAct 적합 │ Plan-and-Execute 적합 │├─────────────────────┼─────────────────────────┤│ 탐색적·즉흥적 태스크 │ 절차가 사전에 명확한 태스크 ││ 단기 목표 │ 장기 다단계 목표 ││ 계획 수립 비용 큼 │ 실행 전 전체 맥락이 중요 ││ 실시간 피드백 의존 │ 계획 공유·감사 필요 │└─────────────────────┴─────────────────────────┘한 가지 실용적 판단 기준: 계획을 인간이 미리 검토할 수 있어야 한다면 Plan-and-Execute를 선택하라. Planner의 출력을 Human-in-the-loop로 승인한 뒤 Executor를 돌리는 패턴은 위험이 높은 자동화 태스크에서 특히 유용하다.
다음 챕터에서는 단일 시도 안에서 스스로 비판하고 개선하는 Self-Refine 패턴과, 여러 독립 답변의 다수결로 신뢰도를 높이는 Self-Consistency를 함께 살펴본다.
참고 자료
- Wang et al. — Plan-and-Solve Prompting (arXiv:2305.04091) — 접속 2026-06-30
- Anthropic — Building Effective AI Agents — 접속 2026-06-30
- LangGraph — Plan-and-Execute — 접속 2026-06-30