Skip to content

필사 모드: 프로덕션 에이전트 설계 패턴 2026 — Supervisor·CodeAct·Plan-Execute·Self-RAG·Handoff·Subagent를 실제로 조합하는 법

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

프롤로그 — 입문서의 패턴 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:

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).

최소 스케치

codeact_executor.py — 격리된 환경에서 모델의 코드를 실행

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"""

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 `customers` table"이라고 했는데 그런 테이블이 없다. 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장 · 안티패턴 — 흔히 보는 실패

1. **모든 패턴 합치기.** "Supervisor 안에 또 supervisor, subagent 안에 plan-execute…". 디버깅이 불가능해진다. **추가하기 전에 빼라**가 황금률이다.

2. **Hooks 없이 CodeAct.** 며칠 안에 누군가 `rm -rf` 또는 secret 누수를 만든다. CodeAct는 sandbox + hooks가 prerequisite이다.

3. **Supervisor 컨텍스트에 모든 specialist raw output을 누적.** 5라운드 안에 240k 토큰을 다 쓴다.

4. **Reflexion을 hard cap 없이.** 12번 재시도가 청구서 3,000달러로 돌아온다.

5. **Plan-Execute를 plan에 너무 의존시킴.** Plan은 가설일 뿐이다. Execute 단계에서 plan과 다른 관찰이 나오면 replan을 허용해야 한다.

6. **Handoff 시 컨텍스트를 그대로 전달.** 매번 새 에이전트가 같은 컨텍스트를 다시 읽으니 비용이 N배가 된다. **요약 + 변수 패킷**만 넘긴다.

7. **Self-RAG의 grader를 자신과 같은 모델로.** Grader는 비싸지 않은 별도 모델이 낫다.

8. **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...

작성 글자: 0원문 글자: 22,341작성 단락: 0/500