ARCH · PLANNING

Planning 전략

📋 Plan-and-Execute 🌳 LATS 🔄 동적 재계획

복잡한 다단계 작업에서 LLM이 전략을 수립하고 실행하는 Planning 패턴들을 비교하고, LangGraph로 구현합니다.

Plan-and-Execute 아키텍처

Plan-and-Execute는 강력한 LLM이 먼저 전체 계획을 수립하고, 각 단계를 순서대로 실행하는 패턴입니다. ReAct보다 장기 일관성이 높고, 사용자에게 계획을 미리 보여줄 수 있습니다.

🧠 Planner 강력한 LLM 전체 계획 수립 (1-N 단계) 📋 실행 계획 1. 검색: AI 논문 수집 2. 분석: 트렌드 추출 3. 작성: 보고서 생성 ⚡ Executor 각 단계별 ReAct Agent 도구 사용 + 결과 반환 🔄 Replanner 오류/불충분 결과 감지 계획 수정 또는 재생성 오류 발생 시 ✅ 최종 결과 모든 단계 완료 시 결과 통합 + 반환 성공 Planner: claude-opus-4-7 / Executor: claude-haiku-4-5 (비용 최적화) 그림 1. Plan-and-Execute 아키텍처 — Replanner로 동적 재계획 지원
pythonplan_and_execute.py — LangGraph 구현
from langgraph.graph import StateGraph, END
from langchain_anthropic import ChatAnthropic
from pydantic import BaseModel
from typing import TypedDict, Annotated
from operator import add

class Plan(BaseModel):
    steps: list[str]

class PlanExecuteState(TypedDict):
    input: str
    plan: list[str]
    past_steps: Annotated[list[tuple], add]  # (step, result) 누적
    response: str

# 강력한 모델이 계획 수립
planner_llm = ChatAnthropic(model="claude-opus-4-7")
# 경량 모델이 각 단계 실행 (비용 절감)
executor_llm = ChatAnthropic(model="claude-haiku-4-5-20251001")

async def planner_node(state: PlanExecuteState) -> dict:
    """전체 계획 수립"""
    structured_llm = planner_llm.with_structured_output(Plan)
    plan = await structured_llm.ainvoke([{
        "role": "user",
        "content": f"""다음 목표를 달성하기 위한 단계별 계획을 수립하세요.
각 단계는 단일 도구 호출이나 분석 작업으로 완료 가능해야 합니다.

목표: {state['input']}"""
    }])
    return {"plan": plan.steps}

async def executor_node(state: PlanExecuteState) -> dict:
    """현재 계획의 첫 번째 단계 실행"""
    if not state["plan"]:
        return {}

    current_step = state["plan"][0]
    past_context = "\n".join(
        f"단계: {s}\n결과: {r}" for s, r in state["past_steps"]
    )

    executor_with_tools = executor_llm.bind_tools(tools)
    result = await executor_with_tools.ainvoke([{
        "role": "user",
        "content": f"""전체 목표: {state['input']}

이전 완료 단계:
{past_context or '없음'}

현재 실행할 단계: {current_step}

이 단계만 완료하세요."""
    }])

    return {
        "past_steps": [(current_step, result.content)],
        "plan": state["plan"][1:]  # 완료된 단계 제거
    }

async def replanner_node(state: PlanExecuteState) -> dict:
    """계획 재평가 — 완료 or 수정"""
    class ReplannedOutput(BaseModel):
        action: str  # "complete" | "replan"
        new_steps: list[str] = []
        final_response: str = ""

    structured_llm = planner_llm.with_structured_output(ReplannedOutput)
    past_context = "\n".join(
        f"✅ {s}: {r[:200]}" for s, r in state["past_steps"]
    )

    output = await structured_llm.ainvoke([{
        "role": "user",
        "content": f"""목표: {state['input']}
완료된 단계: {past_context}
남은 계획: {state['plan']}

목표 달성 여부를 평가하세요:
- complete: 목표 달성, final_response에 최종 답변
- replan: 계획 수정 필요, new_steps에 수정된 계획"""
    }])

    if output.action == "complete":
        return {"response": output.final_response}
    return {"plan": output.new_steps}

def should_end(state: PlanExecuteState) -> str:
    return "end" if state.get("response") else "execute"

builder = StateGraph(PlanExecuteState)
builder.add_node("planner",   planner_node)
builder.add_node("executor",  executor_node)
builder.add_node("replanner", replanner_node)
builder.set_entry_point("planner")
builder.add_edge("planner",  "executor")
builder.add_edge("executor", "replanner")
builder.add_conditional_edges("replanner", should_end, {
    "end":     END,
    "execute": "executor"
})
graph = builder.compile()

LATS — LLM 증강 트리 탐색

LATS(Language Agent Tree Search)는 Monte Carlo Tree Search를 LLM에 적용한 패턴입니다. 여러 경로를 동시에 탐색하고, 각 경로의 가치를 LLM이 평가해 최적 경로를 선택합니다.

Root State 경로 A (score: 7.2) 경로 B (score: 8.9) 경로 C (score: 4.1) B-1 (score: 9.3) ✨ B-2 (score: 7.8) 가지치기 가지치기 ✅ 최적 답변 UCT 점수 = 가치 + 탐색 보너스 높은 점수 노드 우선 확장 그림 2. LATS 트리 탐색 — UCT 점수로 최적 경로 선택, 낮은 점수 가지치기
pythonlats_node.py — LATS 핵심 로직
import math
from dataclasses import dataclass, field
from langchain_anthropic import ChatAnthropic

@dataclass
class TreeNode:
    state: str
    parent: "TreeNode | None" = None
    children: list = field(default_factory=list)
    visits: int = 0
    value: float = 0.0

    def uct_score(self, exploration_c: float = 1.4) -> float:
        """Upper Confidence Bound for Trees"""
        if self.visits == 0:
            return float("inf")
        exploitation = self.value / self.visits
        exploration = exploration_c * math.sqrt(
            math.log(self.parent.visits) / self.visits
        ) if self.parent else 0
        return exploitation + exploration

evaluator_llm = ChatAnthropic(model="claude-opus-4-7")

async def evaluate_state(node: TreeNode, goal: str) -> float:
    """LLM이 현재 상태의 가치를 0-10으로 평가"""
    response = await evaluator_llm.ainvoke([{
        "role": "user",
        "content": f"""목표: {goal}
현재 상태: {node.state}

이 상태가 목표 달성에 얼마나 가까운지 0-10 점수를 숫자만 답하세요."""
    }])
    try:
        return float(response.content.strip())
    except:
        return 5.0

async def lats_search(goal: str, max_iterations: int = 20) -> str:
    root = TreeNode(state=f"초기 상태: {goal}")
    best_node = root

    for _ in range(max_iterations):
        # Selection: UCT 점수가 가장 높은 노드 선택
        node = select_node(root)

        # Expansion: 새 자식 노드 생성
        children = await expand_node(node, goal)

        # Evaluation: 각 자식 평가
        for child in children:
            score = await evaluate_state(child, goal)
            child.value = score
            child.visits = 1
            if score > (best_node.value / max(1, best_node.visits)):
                best_node = child

        # Backpropagation
        backpropagate(node, sum(c.value for c in children))

        if best_node.value >= 9.0:  # 충분히 좋은 해 발견
            break

    return best_node.state

Planning 전략 선택 가이드

시나리오권장 전략이유
간단한 Q&A, 1~3 단계ReAct빠르고 저비용
5~10단계 구조화 작업Plan-and-Execute일관성, 사용자에게 계획 공유 가능
창의적 문제, 최적 해 탐색LATS여러 경로 탐색, 높은 품질
외부 변수 많은 장기 작업Plan-and-Execute + Replanner적응적 재계획
실시간 응답 필요ReAct + 병렬 도구지연 최소화