LabHub

블로그

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

한국어English日本語

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

언제 빛나는가

어떻게 무너지는가

최소 스케치

# 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 smolagentsCodeAgent도 같은 철학으로 만들어졌다.

비교

[기존 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_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
        └──────────┘

언제 빛나는가

어떻게 무너지는가

최소 스케치

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

언제 빛나는가

어떻게 무너지는가

최소 스케치

# 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 에이전트로 넘어간다.

언제 빛나는가

어떻게 무너지는가

최소 스케치

# 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, 자기 도구만)

언제 빛나는가

어떻게 무너지는가

최소 스케치

# 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)  │
  └────────────────────────────────────────────────┘

언제 빛나는가

어떻게 무너지는가

최소 스케치

# 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                                │
       └──────────────────────────────────────┘

언제 빛나는가

어떻게 무너지는가

최소 스케치

# 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장 · 패턴 선택 체크리스트

새 에이전트를 만들 때 던져야 할 질문:

조합 가능 매트릭스를 직접 그려라. 패턴은 도구일 뿐이다.

에필로그 — 패턴 카탈로그가 아니라 시스템 사고

2024년의 글들이 "ReAct를 써라"라고 말했다면, 2026년의 답은 더 차분하다 — "당신의 워크로드에 어떤 실패가 잦은가?" 로 시작한다. 컨텍스트가 폭주하면 Subagent. 검색이 들쭉날쭉하면 Self-RAG. 단계가 흩어지면 Plan-Execute. 돌이킬 수 없는 액션이 있으면 Hooks. 책임이 너무 넓으면 Specialist routing이나 Supervisor.

패턴은 카드 덱이고, 당신은 손을 짠다. 손은 시스템에 따라 다르다. 그리고 한 번에 한 카드씩만 더하라 — 그게 가장 큰 교훈이다.

다음 글에서는 이 패턴들의 평가를 다룬다. Inspect AI, Phoenix, LangSmith로 multi-pattern 에이전트의 회귀를 어떻게 잡는지, 그리고 swarm/handoff 같은 비결정론적 토폴로지에서 "정답"이 무엇인지를 정의하는 법.

참고 / References

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다