ARCH · PLANNING
Planning 전략
복잡한 다단계 작업에서 LLM이 전략을 수립하고 실행하는 Planning 패턴들을 비교하고, LangGraph로 구현합니다.
Plan-and-Execute 아키텍처
Plan-and-Execute는 강력한 LLM이 먼저 전체 계획을 수립하고, 각 단계를 순서대로 실행하는 패턴입니다. ReAct보다 장기 일관성이 높고, 사용자에게 계획을 미리 보여줄 수 있습니다.
그림 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이 평가해 최적 경로를 선택합니다.
그림 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 + 병렬 도구 | 지연 최소화 |