✍️ 필사 모드: 프로덕션 에이전트 설계 패턴 2026 — Supervisor·CodeAct·Plan-Execute·Self-RAG·Handoff·Subagent를 실제로 조합하는 법
한국어- 프롤로그 — 입문서의 패턴 vs 프로덕션의 패턴
- 1장 · Supervisor / Orchestrator 패턴
- 2장 · CodeAct — 액션 언어로 JSON 대신 Python 쓰기
- 3장 · Plan-Execute — 계획을 먼저 세우고, 그다음 실행
- 4장 · Self-RAG / Corrective RAG — 검색 여부 자체를 결정한다
- 5장 · Reflexion / Self-Critique — verbal-RL 식 재시도 루프
- 6장 · Handoff — swarm 스타일 멀티에이전트
- 7장 · Specialist routing — intent classifier가 작업 분배
- 8장 · Subagent isolation — Claude Code의 컨텍스트 격리 패턴
- 9장 · Hooks + 권한 게이트 — LLM 스텝 사이에 결정론적 가드레일
- 10장 · 실제 시스템은 이들을 어떻게 조합하는가
- 11장 · 안티패턴 — 흔히 보는 실패
- 12장 · 패턴 선택 체크리스트
- 에필로그 — 패턴 카탈로그가 아니라 시스템 사고
- 참고 / References
프롤로그 — 입문서의 패턴 vs 프로덕션의 패턴
2023~2024년에 쏟아진 "LLM Agent 설계 패턴" 글들의 카탈로그는 대개 비슷하다. ReAct, Chain-of-Thought, Reflection, Plan-and-Solve, Tree of Thoughts, 그리고 마지막 챕터로 "Multi-Agent". 다섯 개의 깔끔한 박스. 데모는 잘 돈다.
문제는 그 패턴들 대부분이 벤치마크용 토이 환경에서 검증된 것이라는 점이다. HotpotQA 두세 홉, GSM8K 산수, ALFWorld 텍스트 게임. 거기서 의미 있던 것들은 실제 프로덕션 — 5,000줄짜리 코드베이스를 수정하고, 사내 데이터를 30개 시스템에서 끌어오고, 결제 API를 호출하고, 30분짜리 태스크 도중에 OOM이 나는 환경 — 에서는 다른 식으로 무너진다.
2026년 프로덕션 에이전트의 진실 한 줄: 단일 패턴으로 굴러가는 시스템은 거의 없다. Supervisor + CodeAct + Subagent isolation + Hooks를 합쳐서 굴리거나, Plan-Execute + Self-RAG + Reflexion + Handoff를 합쳐서 굴린다. 책임은 분리되어 있고, 각 자리에서 다른 패턴이 일을 한다.
이 글은 2024~2026년 실제 워크로드에서 살아남은 9가지 패턴을 정리한다. 각 패턴마다 (1) 무엇인지, (2) 언제 빛나는지, (3) 어떻게 무너지는지, (4) 30~50줄짜리 최소 스케치. 그리고 마지막에 — 이들을 실제로 어떻게 조합하는지.
1장 · Supervisor / Orchestrator 패턴
한 줄 정의: 상위(supervisor) 에이전트 하나가 작업을 받고, 그것을 여러 전문(specialist) 에이전트에게 분배하고, 결과를 모아 다음 스텝을 결정한다.
LangGraph의 공식 "Supervisor" 가이드에서 권장하는 구조이고, 실제로 가장 많이 보는 멀티에이전트 토폴로지다. 자유분방한 swarm과 다르게, 누가 다음에 일할지를 하나의 supervisor가 결정하기 때문에 통제 가능하다.
토폴로지
┌────────────┐
user task ────► │ Supervisor │
└─────┬──────┘
│ route by intent / state
┌───────────┼───────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Research │ │ Coder │ │ Reviewer │
│ agent │ │ agent │ │ agent │
└────┬─────┘ └────┬─────┘ └────┬─────┘
└──────┬─────┴──────┬─────┘
▼ ▼
return to Supervisor (loop)
언제 빛나는가
- 워크플로의 단계가 이질적일 때 — 검색은 길고 비싸고, 코딩은 도구가 다르고, 리뷰는 짧고 결정론적이다. 한 에이전트가 다 하면 system prompt가 누더기가 된다.
- 상태 관찰이 필요할 때. Supervisor는 매 스텝마다 전체 상태를 보고 "이번엔 누구"를 결정한다.
- 비용 비대칭이 큰 모델 믹스를 쓰고 싶을 때 (Opus를 supervisor에, Haiku를 specialist에).
어떻게 무너지는가
- Supervisor가 컨텍스트 블랙홀이 된다. 모든 specialist 출력이 supervisor의 context로 누적되어, 5번째 라운드쯤 되면 context window가 터진다. → 해법: specialist 결과를 supervisor에게 돌려줄 때 강제로 요약하거나, "툴 리턴값"으로만 보내고 raw chain은 버린다.
- 무한 라우팅 루프. Supervisor가 "Research → Coder → Research → Coder" 를 6번 반복한 뒤 같은 결론에 도달한다. → 해법: hop budget을 결정론적 카운터로 둔다 (LangGraph의
recursion_limit같은 것). - Specialist 출력 형식 불일치. Coder가 raw diff를 주는데 Supervisor는 "PR description"을 기대한다. → 해법: 각 specialist의 return type을 schema로 못박는다.
최소 스케치
# supervisor.py — LangGraph 스타일의 최소 supervisor
from typing import Literal, TypedDict
from langgraph.graph import StateGraph, END
class State(TypedDict):
task: str
messages: list
next: Literal["research", "code", "review", "done"]
hop: int
def supervisor(state: State) -> dict:
if state["hop"] >= 6:
return {"next": "done"}
prompt = f"Task: {state['task']}\nHistory: {state['messages'][-3:]}\nWho works next? research/code/review/done"
decision = llm(prompt) # 강제로 enum 중 하나만 리턴
return {"next": decision, "hop": state["hop"] + 1}
def specialist(name):
def run(state: State) -> dict:
result = run_agent(name, state["task"], state["messages"])
# 핵심: raw output을 통째로 넣지 말고 요약본만
return {"messages": state["messages"] + [{"agent": name, "summary": summarize(result)}]}
return run
g = StateGraph(State)
g.add_node("supervisor", supervisor)
g.add_node("research", specialist("research"))
g.add_node("code", specialist("code"))
g.add_node("review", specialist("review"))
g.set_entry_point("supervisor")
g.add_conditional_edges("supervisor", lambda s: s["next"],
{"research": "research", "code": "code", "review": "review", "done": END})
for s in ["research", "code", "review"]:
g.add_edge(s, "supervisor")
graph = g.compile()
핵심은 supervisor 노드가 결정 함수라는 것이다 — 일을 하지 않고, 누가 일할지만 정한다.
2장 · CodeAct — 액션 언어로 JSON 대신 Python 쓰기
한 줄 정의: 에이전트의 "Action" 출력 형식을 tool_name(arg) JSON 객체가 아니라 실행 가능한 Python 코드 블록으로 만든다. 한 스텝에서 변수에 담고, 분기하고, 루프 돌리고, 여러 툴을 한 번에 호출할 수 있다.
2024년 6월 Yang et al.의 "Executable Code Actions Elicit Better LLM Agents" (CodeAct) 논문이 출발점이고, 2025년 Manus 팀이 자사 에이전트의 액션 레이어로 채택했다고 공개 글에서 밝힌 패턴이다. HuggingFace smolagents의 CodeAgent도 같은 철학으로 만들어졌다.
비교
[기존 ReAct + JSON tool calls]
Action: {"tool": "search", "args": {"q": "kubernetes pod oom"}}
Observation: [50개 결과]
Action: {"tool": "search", "args": {"q": "kubernetes pod oom limits"}}
Observation: [30개 결과]
Action: {"tool": "summarize", "args": {"text": "..."}}
→ 3 LLM 호출, 3 라운드 트립
[CodeAct]
Action:
```python
results_a = search("kubernetes pod oom")
results_b = search("kubernetes pod oom limits")
merged = list(set(results_a + results_b))[:20]
print(summarize("\n".join(r.snippet for r in merged)))
Observation: [요약 1개] → 1 LLM 호출, 1 라운드 트립
### 언제 빛나는가
- **여러 툴을 조합해야 하는 태스크.** 데이터 fetch → 변환 → 집계 → 차트 같은 파이프라인이 한 블록에 들어간다.
- **반복이 자연스러운 태스크** — 100개 파일을 처리해야 하는데 JSON tool call로는 100번 라운드 트립이 필요하다. CodeAct는 `for f in files:` 한 번.
- **수치 연산이 섞이는 태스크.** LLM이 직접 산수하지 않고 `numpy` 한 줄로 끝낸다.
- **상태가 누적되는 태스크** — 변수를 통해 이전 스텝의 결과를 자연스럽게 다음 스텝에서 쓸 수 있다.
### 어떻게 무너지는가
- **샌드박스가 새는 순간 보안이 끝난다.** Python 코드를 실행한다는 것은 임의 코드 실행이라는 뜻이다. → 해법: Docker, Firecracker, E2B, gVisor, 혹은 적어도 Python의 `restricted exec` + 시스템콜 필터.
- **`import os; os.system('rm -rf /')` 한 줄로 모든 게 끝난다.** LLM이 이런 코드를 쓰지 않는다는 보장은 없다. → 해법: allowlist 기반 import + 네트워크 격리 + ephemeral 파일시스템.
- **무한 루프.** `while True: search("x")` 한 번이면 토큰 청구서가 폭발한다. → 해법: 실행 타임아웃 + 출력 토큰 캡.
- **디버깅이 어렵다.** 한 코드 블록이 7개 툴을 호출하고 5번째에서 실패하면, 모델은 "왜 실패했는지"를 다시 파악해야 한다. → 해법: 실행 trace를 모델에게 그대로 돌려준다 (line-by-line stdout).
### 최소 스케치
```python
# codeact_executor.py — 격리된 환경에서 모델의 코드를 실행
import subprocess, json, textwrap
ALLOWED_IMPORTS = {"json", "re", "math", "datetime", "tools"}
def execute_codeact(code: str, tools: dict, timeout_s: int = 30) -> str:
# tools는 모델이 호출할 수 있는 함수 dict — 컨테이너 안에 미리 주입
wrapper = textwrap.dedent(f"""
import json, sys
from tools import {", ".join(tools.keys())}
try:
{textwrap.indent(code, " ")}
except Exception as e:
print(f"__ERROR__: {{type(e).__name__}}: {{e}}", file=sys.stderr)
""")
result = subprocess.run(
["docker", "run", "--rm", "-i", "--network=none",
"--memory=512m", "--cpus=1.0", "codeact-sandbox:latest", "python", "-"],
input=wrapper.encode(),
capture_output=True,
timeout=timeout_s,
)
stdout = result.stdout.decode()[-4000:] # 출력 캡
stderr = result.stderr.decode()[-1000:]
return f"STDOUT:\n{stdout}\n\nSTDERR:\n{stderr}"
# 모델 루프
while not done:
code = llm_extract_codeact(messages) # ```python ... ``` 블록만 뽑기
obs = execute_codeact(code, tools)
messages.append({"role": "user", "content": f"Execution result:\n{obs}"})
핵심 디테일: 출력 캡 (마지막 4KB만), 네트워크 격리 (--network=none 이후 필요한 도메인만 추가), 메모리/CPU 제한, 타임아웃.
3장 · Plan-Execute — 계획을 먼저 세우고, 그다음 실행
한 줄 정의: 한 번의 큰 LLM 호출로 N단계 plan을 만든 뒤, plan을 따라 결정론적으로(또는 더 작은 모델로) 실행한다. ReAct가 "한 스텝 생각 → 한 스텝 행동"을 반복한다면, Plan-Execute는 "한 번에 N스텝 생각 → N스텝 행동"이다.
LangGraph의 공식 튜토리얼에 "plan-and-execute" 예제가 있고, Wang et al.의 "Plan-and-Solve Prompting"(2023) 이후로 가장 안정적인 구조로 자리 잡았다.
토폴로지
[Plan phase] [Execute phase]
┌──────────────┐ ┌──────────────┐
│ Planner LLM │ → step list → │ Executor LLM │ → results
│ (one shot) │ │ (per step) │
└──────────────┘ └──────┬───────┘
│
optional re-plan ◄─────┘
(if step fails)
언제 빛나는가
- 단계 수가 미리 예측 가능한 태스크 — "리포트 작성"은 (자료 수집 → 정리 → 초안 → 검토 → 다듬기)로 거의 항상 분해된다.
- 각 스텝이 독립적이어서 작은 모델로 실행해도 되는 태스크. 비용이 절반 이하로 떨어진다.
- 재현 가능성이 중요한 워크플로. Plan을 저장해두면 같은 입력에 같은 plan이 나온다 (temperature=0일 때).
어떻게 무너지는가
- 계획 단계에서 환각. Planner가 "Step 3: query the
customerstable"이라고 했는데 그런 테이블이 없다. Execution이 시작되어야 발견된다. → 해법: planner에게 가능한 툴/리소스의 schema를 먼저 주거나, planner가 plan을 만들기 전에 "discover" phase를 거치게 한다. - 세상은 plan대로 안 움직인다. 3번째 스텝에서 외부 API가 401을 뱉으면? 4번째 스텝은 무의미하다. → 해법: re-planning 트리거를 둔다 (failure 또는 unexpected observation).
- 너무 잘게 쪼개면 plan-overhead가 커진다. 4단계로 충분한 일을 12단계로 만든다. → 해법: planner system prompt에 "make plans at most N steps unless absolutely necessary"를 명시.
최소 스케치
# plan_execute.py
def plan(task: str, tools_schema: dict) -> list[dict]:
prompt = f"""You will be given a task. Decompose it into 3-7 ordered steps.
Each step: {{"id": int, "goal": str, "tool": str | null, "depends_on": [int]}}
Available tools: {list(tools_schema.keys())}
Task: {task}
Return a JSON array."""
return json.loads(llm(prompt, temperature=0))
def execute_step(step: dict, prior_results: dict) -> dict:
context = "\n".join(f"step{i}: {prior_results[i]}" for i in step["depends_on"])
return run_executor_agent(step["goal"], context, tool=step["tool"])
def plan_execute(task: str, tools_schema: dict, max_replans: int = 2):
steps = plan(task, tools_schema)
results, replans = {}, 0
i = 0
while i < len(steps):
try:
results[steps[i]["id"]] = execute_step(steps[i], results)
i += 1
except UnexpectedObservation as e:
if replans >= max_replans: raise
steps = plan(f"{task}\nSo far: {results}\nProblem: {e}", tools_schema)
replans += 1
i = 0 # 새 plan으로 다시
return results
ReAct 대비 LLM 호출 수가 1/3 ~ 1/5로 떨어지는 게 일반적이다. 그래서 비용이 가장 큰 이점이다.
4장 · Self-RAG / Corrective RAG — 검색 여부 자체를 결정한다
한 줄 정의: 모든 쿼리에 검색을 붙이는 일반 RAG와 달리, 에이전트가 "검색할지 말지", "결과가 충분한지", "다시 검색해야 할지" 를 스스로 판단한다.
원조는 Asai et al.의 "Self-RAG"(2023). 이후 Yan et al.의 "Corrective Retrieval Augmented Generation"(CRAG, 2024)이 "검색 결과가 나쁘면 웹 검색으로 보완"하는 형태로 확장했다. 2025~2026년에는 LangGraph 튜토리얼의 "Adaptive RAG" 예제가 사실상 표준 reference가 되었다.
그래프
┌─────────────┐
query ───────────► │ Route LLM │
└──────┬──────┘
retrieve? │ direct?
┌────────────────┴────────────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ Retrieve │ │ Answer │
└────┬─────┘ │ directly │
▼ └──────────┘
┌──────────┐
│ Grade │ ──→ irrelevant ──→ rewrite query, retry
│ docs LLM │ ──→ partial ──→ web search supplement
└────┬─────┘ ──→ relevant ──→ answer
▼
┌──────────┐
│ Answer │ ──→ hallucinated? ──→ retry
│ + check │ ──→ grounded ──→ return
└──────────┘
언제 빛나는가
- 질문이 다양한 시스템. 어떤 질문은 retrieval이 필요 없고("2 + 2는?"), 어떤 질문은 여러 검색이 필요하다 ("우리 회사 환불 정책과 2025 EU 소비자법 비교").
- retrieval 품질이 들쭉날쭉한 환경. 사내 위키 + 슬랙 + 노션 + 깃허브 이슈를 다 뒤지는 경우. 한 번에 다 나오지 않는다.
- 할루시네이션 비용이 큰 도메인 — 법무·의료·금융. "검색 결과가 충분하지 않다"라고 말할 수 있는 게 중요하다.
어떻게 무너지는가
- 루프가 길어진다. "다시 검색", "다시 검색", "다시 검색" → 토큰 청구서 폭발. → 해법: retrieval hop 캡 (보통 3).
- grader가 grader 본인의 환각을 한다. 문서가 답을 충분히 담고 있는데 "irrelevant"라고 판단. → 해법: grader를 작은 분류 모델로 분리하고 confidence threshold를 둔다.
- rewrite가 원래 의도를 흐린다. "환불 정책" → rewrite → "환불 절차 단계별 안내"로 바뀌면서 정작 정책의 예외 조항을 못 찾는다. → 해법: rewrite 시 원본 쿼리를 같이 보낸다.
최소 스케치
# self_rag.py
def adaptive_rag(query: str, max_hops: int = 3):
route = llm_classify(query, choices=["direct", "retrieve"])
if route == "direct":
return llm_answer(query)
for hop in range(max_hops):
docs = retrieve(query, k=5)
grade = llm_grade(query, docs) # {"relevant", "partial", "irrelevant"}
if grade == "relevant":
answer = llm_answer_with_context(query, docs)
if llm_check_grounding(answer, docs) == "grounded":
return answer
# 환각이면 재시도
continue
if grade == "partial":
web_docs = web_search(query, k=3)
return llm_answer_with_context(query, docs + web_docs)
# irrelevant → rewrite
query = llm_rewrite(query, hint=f"prior docs were off-topic: {summarize(docs)}")
return llm_answer_with_context(query, docs) # 마지막 시도
5장 · Reflexion / Self-Critique — verbal-RL 식 재시도 루프
한 줄 정의: 태스크를 시도하고, 실패하면, "왜 실패했는지" 자연어 reflection을 만들어 메모리에 저장하고, 다음 시도 때 이 reflection을 system prompt에 주입한다. 가중치는 안 바뀐다 — "verbal reinforcement learning"이다.
원조는 Shinn et al.의 "Reflexion: Language Agents with Verbal Reinforcement Learning"(NeurIPS 2023). HumanEval에서 GPT-4의 코딩 성공률을 80% → 91%로 올렸다.
사이클
┌──────────────────────────────────────┐
│ │
▼ │
Actor ──► Action ──► Env ──► Reward │
(LLM) │
▲ │
│ │
Evaluator (success/fail판단) │
│ │
▼ │
Self-Reflection LLM ──► "why I failed" ───┘
(memory buffer)
언제 빛나는가
- 명확한 실패 신호가 있는 태스크. 코드 테스트 통과/실패, 수치 답 정/오, 사용자 거절. 시그널이 있으면 reflection이 잡을 게 있다.
- 시도가 싸고 빠른 환경 — 코드 한 줄 고치고 unit test 돌리기. 시도 비용이 시간당 100달러짜리 외부 API라면 안 맞는다.
- 같은 종류의 태스크를 반복할 때. Reflection memory가 누적되면서 학습 효과가 생긴다.
어떻게 무너지는가
- 잘못된 reflection이 오히려 다음 시도를 망친다. "지난번엔 X 때문에 실패했어"라는 잘못된 결론이 다음 시도를 잘못된 방향으로 끌고 간다. → 해법: reflection에 confidence를 붙이고, 같은 reflection이 두 번 연속 나오면 다른 방향을 강제한다.
- 무한 시도. "또 실패했네, 또 reflection, 또 시도…". → 해법: hard retry cap (보통 3~5).
- memory bloat. Reflection이 누적되면 context window를 다 잡아먹는다. → 해법: 최근 N개만 유지하거나, 의미적 유사도가 낮은 것만 남긴다.
최소 스케치
# reflexion.py
def reflexion_loop(task: str, max_trials: int = 5):
reflections = []
for trial in range(max_trials):
sys_prompt = base_prompt + "\n\nPast reflections:\n" + "\n".join(reflections[-3:])
result = actor_llm(task, system=sys_prompt)
evaluation = evaluator(result, task) # {"success", "fail", reason}
if evaluation["status"] == "success":
return result
reflection = reflect_llm(task=task, attempt=result, why_failed=evaluation["reason"])
# 핵심: 중복 reflection 제거
if reflection not in reflections:
reflections.append(reflection)
else:
# 같은 깨달음 반복 → 강제 변형
reflection = reflect_llm(task=task, attempt=result, hint="try a completely different approach")
reflections.append(reflection)
return result # 마지막 시도 반환
6장 · Handoff — swarm 스타일 멀티에이전트
한 줄 정의: Supervisor가 없다. 각 에이전트가 자기 일이 끝나면 다음 에이전트에게 직접 "handoff"한다. OpenAI가 2024년 10월에 공개한 Swarm 라이브러리의 핵심 패턴이고, Anthropic의 computer-use 데모도 결과적으로 비슷한 형태를 띤다.
Supervisor vs Handoff
[Supervisor] [Handoff]
┌─────────┐ A
│ Sup │ │ handoff_to(B)
└──┬──┬──┬┘ ▼
▼ ▼ ▼ B
A B C │ handoff_to(C)
▼
중앙집중 결정 C
"누가 다음에?" 분산 결정
"다음은 누구한테 넘길지 내가 안다"
OpenAI Swarm에서 handoff는 그냥 "다른 함수처럼 보이는 tool call"이다. transfer_to_billing() 같은 함수를 부르면 대화 컨텍스트가 billing 에이전트로 넘어간다.
언제 빛나는가
- 태스크가 명확하게 단계가 나뉘는 워크플로 — "트리아지 에이전트가 받음 → 환불이면 billing으로, 기술 문제면 tech-support로 → 거기서 끝".
- 각 에이전트의 책임이 좁고 잘 정의되어 있을 때. 좁은 책임 = 좋은 system prompt.
- 상태 누적이 작은 대화. handoff 시점에 컨텍스트가 모두 다음 에이전트에게 넘어가기 때문에, 양이 적어야 한다.
어떻게 무너지는가
- handoff 루프. A → B → A → B. → 해법: handoff 횟수 카운터 + 같은 페어로 2번 이상 핑퐁 시 escalation.
- 컨텍스트가 모두 따라가서 비용 폭발. 대화가 길어지면 매 handoff마다 같은 컨텍스트를 새 에이전트가 다시 처리한다. → 해법: handoff 시점에 요약 + 변수 패킷만 넘긴다.
- system prompt 충돌. Tech-support 에이전트가 "공감하라"인데 billing 에이전트는 "정확한 숫자만"이다. handoff 직후 톤이 갑자기 바뀐다. → 해법: 공통 베이스 prompt + 역할별 오버레이.
최소 스케치
# swarm.py — OpenAI Swarm 스타일
def make_agent(name, instructions, tools, handoffs):
return {
"name": name,
"instructions": instructions,
"tools": tools + [{"name": f"transfer_to_{h.name}", "fn": lambda h=h: h} for h in handoffs],
}
triage = make_agent("triage",
"Route user to billing or tech_support based on intent.",
tools=[], handoffs=[billing, tech])
def run(agent, user_msg, history):
while True:
history.append({"role": "user", "content": user_msg})
out = llm(agent["instructions"], history)
if out.tool_call and out.tool_call.name.startswith("transfer_to_"):
agent = out.tool_call.fn() # handoff!
continue
return out, history
7장 · Specialist routing — intent classifier가 작업 분배
한 줄 정의: 사용자 입력이 들어오면, 먼저 싸고 빠른 분류기가 intent를 잡고, 거기에 해당하는 specialist 에이전트로 라우팅한다. Supervisor와 비슷해 보이지만, 차이는 분류기가 LLM이 아닌 작은 분류 모델이거나 결정론적 규칙이라는 점이다.
대부분의 프로덕션 챗봇이 이미 이렇게 한다. Anthropic의 customer-facing 에이전트, OpenAI ChatGPT의 GPTs 라우팅, Slack/Discord 봇의 명령어 라우팅.
토폴로지
user msg
│
▼
┌─────────────┐
│ Intent │ ← 작은 BERT 분류기, 혹은 regex 룰, 혹은 Haiku 같은 싼 LLM
│ classifier │
└──┬─────┬────┘
│ │
▼ ▼
agent_a agent_b ... agent_n
(각자 자기 system prompt, 자기 도구만)
언제 빛나는가
- intent가 셀 수 있게 유한할 때 ("환불 / 배송 조회 / 계정 / 기타").
- 각 intent의 응답이 매우 다른 시스템 — 같은 prompt로 못 한다.
- latency가 critical한 시스템. 분류기를 BERT-base 같은 거 쓰면 50ms 안에 끝난다.
어떻게 무너지는가
- 잘못 분류된 입력은 영원히 잘못된 에이전트로 간다. "결제가 안 됐는데 환불 안 해줘서 화났어"는 billing? complaint? → 해법: confidence가 낮으면 generalist로, 분류기 출력에 항상 "uncertain" 옵션을 둔다.
- 새 intent가 생기면 분류기 재학습 필요. → 해법: 작은 LLM에게 "기존 N개 클래스 + other" 형태로 zero-shot 분류시키고, "other" 비율을 모니터링한다.
- multi-intent. "주문 취소하고 새로 주문하고 싶어" → cancel + create. → 해법: 분류를 multi-label로 두거나, generalist가 직접 처리한다.
최소 스케치
# routing.py
INTENT_AGENTS = {
"billing": billing_agent,
"shipping": shipping_agent,
"account": account_agent,
"general": general_agent,
}
def classify(text: str) -> tuple[str, float]:
# 작은 분류기 — bert-base finetune 또는 Haiku zero-shot
logits = small_classifier(text)
intent, conf = top1(logits)
return intent, conf
def route(user_msg: str):
intent, conf = classify(user_msg)
if conf < 0.7 or intent == "uncertain":
return general_agent(user_msg) # fallback
return INTENT_AGENTS[intent](user_msg)
8장 · Subagent isolation — Claude Code의 컨텍스트 격리 패턴
한 줄 정의: 메인 에이전트가 큰 태스크 중간에, 완전히 새 컨텍스트 윈도를 가진 자식 에이전트(subagent)를 띄워서 좁은 서브태스크를 시키고, 결과 요약만 받는다. 자식의 컨텍스트는 부모에 누적되지 않는다.
Claude Code의 핵심 패턴 중 하나로, Anthropic의 공식 문서가 "subagent / Task tool"로 다룬다. 비슷한 개념이 OpenAI의 "Agents SDK"의 subagent, Cursor의 "background agents"에도 있다.
그림
[main agent context]
┌────────────────────────────────────────────────┐
│ user task, system prompt, history (240k tok) │
│ │
│ ── spawn(subagent, "summarize 18 PRs") ──► │
│ │
│ [subagent context] │
│ ┌───────────────────┐ │
│ │ fresh window │ │
│ │ subtask + tools │ │
│ │ (returns: 400 tok)│ │
│ └─────────┬─────────┘ │
│ │ │
│ ◄────── result summary (400 tok) ── │
│ │
│ continues main task (context grew by 400 tok) │
└────────────────────────────────────────────────┘
언제 빛나는가
- 큰 검색/탐색이 필요한데 raw 결과를 부모가 알 필요는 없을 때. "이 18개 PR을 다 읽고 stale한 거 찾아"는 subagent로 보내고, 부모는 결과 리스트만 받는다.
- context rot 방지가 중요한 긴 세션. 부모 컨텍스트가 80%를 넘기 전에 일을 위임한다.
- 병렬화 가능한 독립 서브태스크. 3개 subagent를 동시에 띄울 수 있다.
어떻게 무너지는가
- 자식이 부모의 의도를 모른다. "stale PR 찾아"라고만 하면 자식은 "왜?" 모른다. → 해법: 부모가 prompt 작성 시 충분한 컨텍스트를 명시적으로 패킹.
- 자식이 무한히 일을 키운다. "PR 18개" → 자식이 PR마다 또 subagent를 띄움 → 재귀. → 해법: subagent 깊이 제한 + token budget 제한.
- 결과 통합이 어렵다. 3개 자식이 서로 다른 포맷의 결과를 돌려준다. → 해법: subagent prompt에 출력 schema를 명시.
최소 스케치
# subagent.py
def spawn_subagent(parent_state, subtask: str, tools: list,
max_tokens: int = 8000, depth: int = 0):
if depth >= 2:
raise RuntimeError("subagent depth limit reached")
sub_context = {
"system": SUB_AGENT_SYSTEM, # 부모와 다른 system prompt
"messages": [{"role": "user", "content": subtask}],
}
while not done(sub_context) and tokens(sub_context) < max_tokens:
sub_context = step(sub_context, tools, depth=depth + 1)
summary = final_summary(sub_context) # 자식이 자기 결과를 짧게 요약
return summary # 부모는 이것만 받는다
# 부모 루프 안에서
if parent_decides_to_delegate():
summary = spawn_subagent(state, "summarize the 18 open PRs in the repo", tools=[git_tool, gh_tool])
state["messages"].append({"role": "tool", "name": "subagent", "content": summary})
핵심은 부모의 messages에 자식의 raw chain이 안 들어간다는 것. 8,000 토큰짜리 자식 작업이 400 토큰짜리 요약으로 들어온다.
9장 · Hooks + 권한 게이트 — LLM 스텝 사이에 결정론적 가드레일
한 줄 정의: 에이전트 루프의 매 스텝 전/후에 **결정론적 코드(hook)**가 끼어들어 검사/변형/차단을 한다. LLM은 빠르고 유연하지만 신뢰할 수 없고, hook은 느리지만 100% 결정론적이다. 둘이 교대로 쓰인다.
Claude Code의 hook system (PreToolUse, PostToolUse, UserPromptSubmit)이 대표적이고, LangGraph의 interrupt_before/interrupt_after, OpenAI Agents SDK의 guardrails, Anthropic Agent Skills의 permission gates가 같은 카테고리다.
어디에 끼어드는가
┌──────────────────────────────────────┐
│ Agent loop │
│ │
user │ ┌──── UserPromptSubmit hook ───┐ │
msg ──┼─►│ check policy, strip secrets │ │
│ └──┬────────────────────────────┘ │
│ ▼ │
│ ┌──── LLM step ──────────┐ │
│ │ propose tool call │ │
│ └──┬─────────────────────┘ │
│ ▼ │
│ ┌── PreToolUse hook ────┐ │
│ │ allow? deny? mutate? │ │
│ └──┬────────────────────┘ │
│ ▼ │
│ ┌── Tool execution ─────┐ │
│ └──┬────────────────────┘ │
│ ▼ │
│ ┌── PostToolUse hook ───┐ │
│ │ redact, audit │ │
│ └──┬────────────────────┘ │
│ ▼ │
│ loop │
└──────────────────────────────────────┘
언제 빛나는가
- 돌이킬 수 없는 액션에 대한 인간 승인.
rm -rf, 결제, 외부 이메일 발송. LLM이 "괜찮을 거야"라고 하든 말든, hook이 차단한다. - 시크릿 누수 방지 — LLM 입력에 API 키, 토큰, PII가 들어오는지 매번 정규식으로 검사.
- 정책 강제 — "특정 폴더 외부 파일은 못 건드림", "특정 도메인 외부 URL은 못 요청함".
- audit log — 모든 tool call을 PostHook에서 영구 저장. 사고 났을 때 추적 가능.
어떻게 무너지는가
- hook이 너무 빡빡하면 에이전트가 멈춰버린다. "모든 파일 쓰기 거부" → 에이전트가 아무것도 못 한다. → 해법: deny-by-default 대신 allowlist + ask-user for borderline.
- hook이 LLM에게 안 보이면 같은 시도 반복. 거부당했는데 모델은 이유를 모르고 또 시도. → 해법: hook의 reject 메시지를 다음 LLM 입력에 포함.
- hook 안에서 LLM 호출 = 비결정론. "이게 위험한지 LLM에게 물어봄"은 hook의 의미를 깬다. → 해법: hook은 정적 규칙으로만, 모호하면 차단하고 사람에게 묻는다.
최소 스케치
# hooks.py
def pre_tool_hook(tool: str, args: dict, state) -> tuple[bool, str | None]:
if tool == "bash":
cmd = args.get("command", "")
if "rm -rf" in cmd or "curl http" in cmd:
return False, "blocked: dangerous command pattern"
if "$SECRET" in cmd or re.search(r"sk-[a-zA-Z0-9]{20,}", cmd):
return False, "blocked: secret leak detected"
if tool == "write_file":
if not args["path"].startswith(state["allowed_root"]):
return False, f"blocked: outside allowed root {state['allowed_root']}"
return True, None
def agent_step(state):
proposal = llm_propose_action(state)
allowed, reason = pre_tool_hook(proposal.tool, proposal.args, state)
if not allowed:
state["messages"].append({"role": "tool", "content": f"BLOCKED: {reason}"})
return state
result = execute(proposal)
audit_log(proposal, result) # post hook
state["messages"].append({"role": "tool", "content": result})
return state
10장 · 실제 시스템은 이들을 어떻게 조합하는가
여기서부터가 진짜다. 단일 패턴으로 굴러가는 시스템은 거의 없다. 2026년의 잘 만들어진 프로덕션 에이전트는 3~4개의 패턴을 합성한다.
케이스 1 · 코딩 에이전트 (Claude Code, Cursor, Devin 류)
[user task]
│
▼
┌────────────────────────────────────────────────┐
│ Main agent (loop) — Plan-Execute │
│ - Reads task, builds a 3-7 step plan │
│ - Per step: │
│ ┌─── PreToolUse hook ────┐ │
│ │ permission + policy │ │
│ └─────────┬──────────────┘ │
│ ▼ │
│ ┌─── Tool / CodeAct exec ────┐ │
│ └─────────┬──────────────────┘ │
│ ▼ │
│ ┌─── PostToolUse hook ───┐ │
│ │ audit, redact │ │
│ └─────────┬──────────────┘ │
│ ▼ │
│ need deep exploration? ──► spawn subagent │
│ (isolated ctx)│
│ ▼ │
│ - On step failure: Reflexion-style retry │
└────────────────────────────────────────────────┘
조합: Plan-Execute + Subagent isolation + Hooks + Reflexion (4개).
케이스 2 · 고객 지원 에이전트 (Anthropic, Intercom Fin, Linear Asks)
[user msg]
│
▼
┌──── Specialist routing (intent classifier) ────┐
│ ▼ ▼ ▼ ▼ │
│ billing shipping account general │
│ │ │ │ │ │
└──────┼──────────┼─────────┼───────────┼────────┘
▼ ▼ ▼ ▼
[each specialist: Self-RAG over internal KB]
│
▼
can resolve? ──── yes ──► reply
│
no
▼
Handoff to human OR another specialist
조합: Specialist routing + Self-RAG + Handoff (3개).
케이스 3 · 데이터 분석 에이전트 (Manus, OpenAI Code Interpreter)
[user question over CSV / DB]
│
▼
Supervisor (Manus-style)
│
├─► Researcher subagent (subagent isolation)
│ └─ Self-RAG over schemas/docs
│
├─► Analyst subagent
│ └─ CodeAct (writes pandas / SQL in Python blocks)
│ └─ executes in sandbox with Hooks (network=none, fs scoped)
│
└─► Reporter subagent
└─ writes markdown summary
조합: Supervisor + Subagent isolation + CodeAct + Self-RAG + Hooks (5개).
케이스 4 · OS 자동화 에이전트 (Anthropic computer use)
[user goal: "book a flight"]
│
▼
Plan phase: high-level plan (find site → search → fill form → confirm)
│
▼
Execute phase: action loop
- screenshot
- decide click/type/key
- PreActionHook: dangerous-zone? (banking, settings) → halt + ask user
- execute
- PostActionHook: screenshot diff
- on unexpected screen → Reflexion-style replan
조합: Plan-Execute + Hooks + Reflexion.
패턴 조합 매트릭스
단순 | 중간복잡 | 매우복잡
입문서 ReAct만 | ReAct+Reflection | (벤치마크 거기서 멈춤)
실제 프로덕션 추천 Routing+ | Plan-Exec+ | Supervisor+Subagent+
Self-RAG | Hooks+Subagent| CodeAct+Hooks+Reflexion
11장 · 안티패턴 — 흔히 보는 실패
- 모든 패턴 합치기. "Supervisor 안에 또 supervisor, subagent 안에 plan-execute…". 디버깅이 불가능해진다. 추가하기 전에 빼라가 황금률이다.
- Hooks 없이 CodeAct. 며칠 안에 누군가
rm -rf또는 secret 누수를 만든다. CodeAct는 sandbox + hooks가 prerequisite이다. - Supervisor 컨텍스트에 모든 specialist raw output을 누적. 5라운드 안에 240k 토큰을 다 쓴다.
- Reflexion을 hard cap 없이. 12번 재시도가 청구서 3,000달러로 돌아온다.
- Plan-Execute를 plan에 너무 의존시킴. Plan은 가설일 뿐이다. Execute 단계에서 plan과 다른 관찰이 나오면 replan을 허용해야 한다.
- Handoff 시 컨텍스트를 그대로 전달. 매번 새 에이전트가 같은 컨텍스트를 다시 읽으니 비용이 N배가 된다. 요약 + 변수 패킷만 넘긴다.
- Self-RAG의 grader를 자신과 같은 모델로. Grader는 비싸지 않은 별도 모델이 낫다.
- Routing의 confidence 무시. 분류기가 0.51의 confidence로 보낸 input도 그대로 specialist로 보낸다. → fallback 트랙 필수.
12장 · 패턴 선택 체크리스트
새 에이전트를 만들 때 던져야 할 질문:
- 태스크의 단계 수가 미리 예측 가능한가? → 가능하면 Plan-Execute, 아니면 ReAct + Reflexion.
- 이질적인 도구/지식 영역이 섞이는가? → Specialist routing 또는 Supervisor.
- 계산/조합이 많은 액션이 있는가? → CodeAct (단, sandbox는 prerequisite).
- 외부 지식이 답에 필수인가? → 모두 RAG가 아니라 Self-RAG (필요할 때만).
- 명확한 success/fail 신호가 있는가? → Reflexion 도입 검토.
- 돌이킬 수 없는 액션이 있는가? → Hooks + 권한 게이트, 필수.
- 긴 세션 / 큰 탐색이 있는가? → Subagent isolation.
- 여러 에이전트가 자연스럽게 핸드오프되는 워크플로인가? → Handoff (단, hop cap 필수).
조합 가능 매트릭스를 직접 그려라. 패턴은 도구일 뿐이다.
에필로그 — 패턴 카탈로그가 아니라 시스템 사고
2024년의 글들이 "ReAct를 써라"라고 말했다면, 2026년의 답은 더 차분하다 — "당신의 워크로드에 어떤 실패가 잦은가?" 로 시작한다. 컨텍스트가 폭주하면 Subagent. 검색이 들쭉날쭉하면 Self-RAG. 단계가 흩어지면 Plan-Execute. 돌이킬 수 없는 액션이 있으면 Hooks. 책임이 너무 넓으면 Specialist routing이나 Supervisor.
패턴은 카드 덱이고, 당신은 손을 짠다. 손은 시스템에 따라 다르다. 그리고 한 번에 한 카드씩만 더하라 — 그게 가장 큰 교훈이다.
다음 글에서는 이 패턴들의 평가를 다룬다. Inspect AI, Phoenix, LangSmith로 multi-pattern 에이전트의 회귀를 어떻게 잡는지, 그리고 swarm/handoff 같은 비결정론적 토폴로지에서 "정답"이 무엇인지를 정의하는 법.
참고 / References
- Yao et al., "ReAct: Synergizing Reasoning and Acting in Language Models" (ICLR 2023). https://arxiv.org/abs/2210.03629
- Wang et al., "Plan-and-Solve Prompting" (ACL 2023). https://arxiv.org/abs/2305.04091
- Asai et al., "Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection" (ICLR 2024). https://arxiv.org/abs/2310.11511
- Yan et al., "Corrective Retrieval Augmented Generation" (CRAG, 2024). https://arxiv.org/abs/2401.15884
- Shinn et al., "Reflexion: Language Agents with Verbal Reinforcement Learning" (NeurIPS 2023). https://arxiv.org/abs/2303.11366
- Yang et al., "Executable Code Actions Elicit Better LLM Agents" (CodeAct, 2024). https://arxiv.org/abs/2402.01030
- LangGraph — Multi-Agent Supervisor tutorial. https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/
- LangGraph — Plan-and-Execute. https://langchain-ai.github.io/langgraph/tutorials/plan-and-execute/plan-and-execute/
- LangGraph — Adaptive RAG. https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_adaptive_rag/
- OpenAI Swarm — minimal multi-agent orchestration framework. https://github.com/openai/swarm
- Anthropic — "Computer use" (Claude 3.5 Sonnet). https://www.anthropic.com/news/3-5-models-and-computer-use
- Anthropic — Claude Code Subagents docs. https://docs.claude.com/en/docs/claude-code/sub-agents
- Anthropic — Claude Code Hooks docs. https://docs.claude.com/en/docs/claude-code/hooks
- HuggingFace
smolagents— CodeAgent. https://huggingface.co/docs/smolagents/index - Manus — agent architecture write-up. https://manus.im/blog
- OpenAI Agents SDK. https://github.com/openai/openai-agents-python
현재 단락 (1/500)
2023~2024년에 쏟아진 "LLM Agent 설계 패턴" 글들의 카탈로그는 대개 비슷하다. ReAct, Chain-of-Thought, Reflection, Plan-and-S...