계획과 ReAct
에이전트의 계획 모듈을 피드백 유무로 나누어 보고, ReAct와 계획-실행 분리를 비교하며, 추론 시점의 계산 확대와 에이전트의 관계를 정리한다.
- 계획(planning): 복잡한 작업을 하위 작업으로 나누고 순서를 정하는 일.
- 8주의 추론 전략(문제 분해, Plan-and-Solve, 생각의 트리)이 한 번의 답변 안에서 하는 계획이었다면, 에이전트의 계획은 도구를 쓰는 여러 단계에 걸친 계획.
계획 모듈의 유형
| 유형 | 방식 | 특징 |
|---|---|---|
| 피드백 없는 계획 | 단계를 미리 나열하고 차례로 수행(단일 경로), 또는 여러 갈래를 비교·선택(다중 경로) | 구현이 간단. 예상과 다른 결과에 대응하지 못함 |
| 피드백 있는 계획 | 환경(도구 결과·테스트), 사람, 다른 모델의 피드백을 보고 계획 수정 | 설계가 복잡하지만 복잡한 과제에 효과적 |
- 테스트 결과를 반성으로 바꾸는 피드백은 반성과 자기 개선에서 다룸.
출처: Wang 외(2023), A Survey on Large Language Model based Autonomous Agents.
ReAct: 추론과 행동의 교대
- ReAct(Reasoning + Acting): 생각(Thought) → 행동(Action) → 관찰(Observation) → 다음 생각 …을 번갈아 수행.
- 배경: 생각의 연쇄(CoT)는 모델 내부 지식만 쓰는 닫힌 과정. 사실 오류가 생기고 새 정보로 추론을 고칠 수 없음.
- 원 논문은 위키백과 검색을 행동으로 써서 질의응답(HotpotQA)·사실 검증(FEVER)에서 행동만, 추론만 하는 방식보다 정확하고 해석하기 쉬운 궤적을 보고.
- 에이전트 루프 만들기의
run_agent가 바로 ReAct 구조. 요즘 모델은 생각을 글로 쓰지 않아도 도구 호출이 행동, 도구 결과가 관찰.- 예: 주문 두 건의 배송 상태를 물으면 주문 조회 2번, 배송 조회 2번의 순서를 모델이 매 단계 스스로 정함. 이 순서를 미리 볼 방법은 없음.
- 장점: 결과를 보고 바로 다음 행동을 바꿈(출고 전이면 배송 조회 생략).
- 단점: 전체 계획이 드러나지 않아 실행 전에 검토할 수 없음. 단계가 길어지면 목표를 놓치거나 같은 행동을 반복하기도 함.
출처: Yao 외(2022), ReAct: Synergizing Reasoning and Acting in Language Models.
계획-실행 분리
- 계획-실행 분리(plan-and-execute): 계획 담당이 단계 목록을 먼저 만들고, 실행 담당이 단계를 하나씩 수행.
- 계획을 구조화된 출력으로 받으면 코드가 단계를 하나씩 꺼내 쓸 수 있음. 실행 전에 사람이 계획을 검토하거나 단계마다 쓸 도구를 제한할 수 있음.
Mermaid 다이어그램 원문
flowchart LR
Q[요청] --> P[계획 담당<br/>구조화된 출력]
P --> S1[1단계] --> S2[2단계] --> S3[…] --> F[최종 답변]
S1 -.-> L1[그 단계의 도구만 호출]
S2 -.-> L2[그 단계의 도구만 호출]대화형 다이어그램을 불러오기 전에는 원문으로 표시됩니다.
실습 준비
!pip install -q openai pydantic
from pathlib import Path
from typing import Literal
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=Path("openrouter-key.txt").read_text().strip(),
)
model = "openai/gpt-4.1-mini"
에이전트 루프 만들기와 같은 주문·배송 데이터와 도구 함수(주문 12347 추가). 이번에는 모델이 도구를 고르지 않으므로 JSON Schema 정의는 필요 없음.
orders = {"12345": {"status": "출고 완료", "tracking_number": "ZYX987"},
"12347": {"status": "출고 완료", "tracking_number": "QWE456"}}
shipments = {"ZYX987": {"status": "배송 중", "location": "서울 강서 허브", "eta": "2026-11-03"},
"QWE456": {"status": "배송 지연", "location": "대전 허브", "eta": "2026-11-06", "note": "기상 악화"}}
functions = {"find_order": lambda x: orders.get(x), "get_shipping_status": lambda x: shipments.get(x)}
def chat(messages):
return client.chat.completions.create(model=model, messages=messages).choices[0].message.content
계획을 구조화된 출력으로 받기
parse에 Pydantic 모델을 넘기면 JSON Schema로 바꿔 요청하고 결과를 객체로 돌려줌.tool을Literal로 제한: 있지도 않은 도구 이름을 계획에 쓰지 못하게 함.
class Step(BaseModel):
goal: str # 이 단계에서 알아낼 것
tool: Literal["find_order", "get_shipping_status"]
class Plan(BaseModel):
steps: list[Step]
planner_prompt = """고객 요청을 처리할 단계를 순서대로 세우세요. 한 단계에 도구 하나.
도구: find_order(주문 번호 → 운송장 번호), get_shipping_status(운송장 번호 → 배송 상태)"""
question = "주문 12345랑 12347 둘 다 어디쯤 왔는지 알려 주세요. 늦어지는 게 있으면 이유도요."
response = client.chat.completions.parse(model=model, response_format=Plan, messages=[
{"role": "system", "content": planner_prompt}, {"role": "user", "content": question}])
plan = response.choices[0].message.parsed
for i, step in enumerate(plan.steps, 1):
print(i, step.tool, step.goal)
실행 결과
1 find_order 주문 번호 12345에 대한 운송장 번호 찾기
2 find_order 주문 번호 12347에 대한 운송장 번호 찾기
3 get_shipping_status 주문 번호 12345의 운송장 번호로 배송 상태 조회하기
4 get_shipping_status 주문 번호 12347의 운송장 번호로 배송 상태 조회하기
- 위 실행에서는 주문 조회 2단계 → 배송 조회 2단계의 계획. 아직 모르는 운송장 번호를 앞 단계의 결과로 얻도록 배치했는지가 핵심. 계획은 실행마다 달라질 수 있음.
- 이 시점에는 도구를 하나도 실행하지 않았음. 계획만 있는 상태라 사람이 검토하고 고칠 수 있음.
계획에 따라 단계별로 실행하기
- 단계마다 계획에 적힌 도구를 코드가 실행하고, 모델은 인자(주문 번호, 운송장 번호)만 채움. 계획이 행동 공간을 그 단계의 도구 하나로 좁힘.
messages는 단계 사이에 이어서 사용. 앞 단계에서 얻은 운송장 번호를 다음 단계가 봐야 하기 때문.
def execute_plan(question, plan):
messages = [{"role": "user", "content": question}]
for i, step in enumerate(plan.steps, 1):
messages.append({"role": "user", "content": f"[{i}단계] {step.goal}\n{step.tool}에 넣을 값만 답하세요."})
arg = chat(messages).strip() # 모델은 인자만 채움
result = functions[step.tool](arg) # 코드가 계획대로 실행
print(f"{i}단계: {step.tool}({arg})")
messages += [{"role": "assistant", "content": arg}, {"role": "user", "content": f"결과: {result}"}]
messages.append({"role": "user", "content": "위 결과로만 원래 질문에 답하세요."})
return chat(messages)
print(execute_plan(question, plan))
실행 결과
1단계: find_order(12345)
2단계: find_order(12347)
3단계: get_shipping_status(ZYX987)
4단계: get_shipping_status(QWE456)
주문 12345는 현재 서울 강서 허브에서 배송 중이며, 예상 도착일은 2026년 11월 3일입니다.
주문 12347은 현재 대전 허브에 있으며 배송이 지연되고 있습니다. 지연 사유는 기상 악화 때문이며, 예상 도착일은 2026년 11월 6일입니다.
- 위 실행에서는 모델이 단계마다 인자(12345, 12347, ZYX987, QWE456)만 채우고, 코드가 계획대로 도구를 실행. 운송장 번호는 앞 단계의 결과에서 가져옴. 마지막에 결과를 종합해 답변.
- 계획이 틀렸다면(예: 배송 조회를 주문 조회보다 앞에 둠) 실행 단계는 그 잘못을 그대로 따름. 피드백 없는 계획의 한계.
- 보완: 단계 실행 결과를 보고 계획을 다시 세우기(replanning), 실행 전 사람의 계획 승인.
| 구분 | ReAct | 계획-실행 분리 |
|---|---|---|
| 계획 시점 | 매 단계 | 처음에 한 번(필요하면 다시) |
| 계획 검토 | 어려움 | 실행 전에 가능 |
| 예상 밖의 결과 | 바로 대응 | 다시 계획해야 대응 |
| 어울리는 작업 | 짧고 결과에 따라 갈라지는 작업 | 길고 단계가 분명한 작업 |
추론 시점의 계산 확대
- 추론 시점 확장(test-time scaling): 학습이 끝난 모델이 답을 만드는 과정에 계산을 더 써서 결과를 개선. 풀이 연장(추론 모델), 후보 비교(자기 일관성), 경로 탐색(생각의 트리) 등. 자세한 내용은 추론 시점 확장.
- 에이전트 루프도 추론 시점에 계산을 더 쓰는 방법. 모델 호출을 여러 번 하며 도구로 얻은 정보로 풀이를 이어 감.
- 추론 강도(reasoning effort)를 높이면 추론 토큰(출력 토큰으로 과금)과 지연이 늘지만, 정답률 향상은 문제에 따라 다름. 쉬운 문제는 토큰만 더 쓰는 경우가 많음.
- 에이전트에서는 이 비용이 단계 수만큼 곱해짐. 계획처럼 어려운 판단에는 높은 강도, 단순한 도구 호출 단계에는 낮은 강도나 작은 모델.
퀴즈
피드백 없는 계획의 한계로 가장 적절한 것은?
퀴즈를 풀려면 대화형 기능을 불러와야 합니다.