AI 에이전트 개발 실전 #5 서브에이전트로 일 나누기
4편에서 컨텍스트를 줄이고 비우고 요약했습니다. 그런데 더 근본적인 해법이 하나 있습니다. 애초에 한 컨텍스트에 다 담지 않는 것입니다. 일의 일부를 별도의 에이전트에게 떼어 주고 결과만 돌려받으면, 그 일의 중간 과정은 본 에이전트의 컨텍스트에 아예 들어오지 않습니다. 이번 글에서는 서브에이전트를 다룹니다.
왜 나누는가 #
서브에이전트의 이점은 세 가지로 정리됩니다.
- 컨텍스트 격리 — 조사 작업은 검색 결과 수십 건을 뒤지는 과정이 따라옵니다. 서브에이전트가 그 과정을 자기 컨텍스트에서 치르고 결론만 보고하면, 메인 에이전트는 결론만 받습니다. 4편의 기법들이 “쌓인 것을 줄이는” 방법이라면 이것은 “쌓이지 않게 하는” 방법입니다.
- 역할 분리 — 서브에이전트마다 다른 시스템 프롬프트와 다른 도구 목록을 줄 수 있습니다. 조사 담당에게는 검색 도구만, 작성 담당에게는 파일 도구만 주는 식입니다. 2편에서 본 “도구 목록은 좁을수록 좋다"는 원칙을 역할 단위로 실현하는 셈입니다.
- 병렬 실행 — 서로 독립적인 작업이라면 서브에이전트 여러 개를 동시에 돌릴 수 있습니다.
delegate 도구 만들기 #
구현의 핵심은 의외로 단순합니다. 서브에이전트는 결국 별도의 messages 배열로 도는 또 하나의 루프입니다. 1편의 run_agent를 거의 그대로 쓰되, 역할과 도구를 파라미터로 받게 만듭니다.
def run_subagent(system: str, tools: list, task: str, max_steps: int = 15) -> str:
"""독립 컨텍스트에서 작업을 수행하고 최종 보고만 반환한다."""
messages = [{"role": "user", "content": task}]
for _ in range(max_steps):
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
system=system,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
return next(b.text for b in response.content if b.type == "text")
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": run_tools(response)})
return "(서브에이전트가 단계 한도 안에 작업을 끝내지 못했습니다)"그리고 이것을 메인 에이전트의 도구로 노출합니다.
{
"name": "delegate_research",
"description": (
"조사 전문 서브에이전트에게 조사 작업을 맡긴다. "
"여러 문서를 검색하고 읽어야 답이 나오는 질문이면 직접 검색하지 말고 이 도구를 쓴다. "
"task에는 무엇을 알아낼지와 보고 형식을 구체적으로 적는다."
),
"input_schema": {
"type": "object",
"properties": {
"task": {"type": "string", "description": "조사할 내용과 기대하는 보고 형식"},
},
"required": ["task"],
},
}
def delegate_research(task: str) -> str:
return run_subagent(
system="너는 조사 전문 에이전트다. 요청받은 내용을 조사해 근거와 함께 간결히 보고한다.",
tools=research_tools, # 검색·읽기 도구만
task=task,
)메인 에이전트 입장에서 서브에이전트는 그냥 도구 하나입니다. 루프 구조는 아무것도 바뀌지 않습니다.
보고 형식을 계약으로 만들기 #
서브에이전트에서 흔한 실패는 위임이 아니라 보고에서 납니다. 메인 에이전트는 서브에이전트의 최종 텍스트만 받으므로, 그 텍스트에 필요한 정보가 빠져 있으면 위임 전체가 헛수고가 됩니다. 그래서 task에 보고 형식을 명시하는 것이 중요합니다.
2026년 5월의 환불 정책 변경 내용을 조사해라.
보고 형식:
- 변경 요점 (3줄 이내)
- 근거 문서의 제목과 위치
- 확인하지 못한 것이 있으면 명시“확인하지 못한 것을 명시하라"는 줄이 특히 유용합니다. 이게 없으면 서브에이전트는 빈손일 때 그럴듯한 추정으로 보고서를 채우는 경향이 있습니다.
오케스트레이터-워커와 병렬 실행 #
서브에이전트가 여럿이 되면 메인 에이전트의 역할이 바뀝니다. 직접 일하는 대신 일을 나누고 결과를 합치는 오케스트레이터(orchestrator, 오케스트라의 지휘처럼 전체 진행을 조율하는 역할)가 됩니다. 서로 독립적인 위임이라면 동시에 실행할 수 있습니다. Claude가 한 응답에서 delegate_research를 세 번 부르면, 그 세 호출을 스레드로 병렬 처리하는 식입니다.
from concurrent.futures import ThreadPoolExecutor
def run_tools(response) -> list:
blocks = [b for b in response.content if b.type == "tool_use"]
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(execute_tool, blocks))
return results1편에서 “병렬 호출의 결과를 전부 돌려줘야 한다"고 했던 것이 여기서 그대로 적용됩니다. 서브에이전트 세 개가 각자 몇 분씩 걸리는 조사를 동시에 하면, 전체 시간은 가장 느린 하나로 줄어듭니다.
위임이 과해지지 않게 #
서브에이전트는 공짜가 아닙니다. 호출마다 별도의 루프가 통째로 돌기 때문에 비용과 시간이 듭니다. 두 가지 기준을 권합니다.
- 한 번에 끝날 일은 직접 한다. 조회 한 번이면 답이 나오는 일에 서브에이전트를 띄우는 것은 낭비입니다. delegate 도구의 description에 “여러 문서를 검색하고 읽어야 하는 경우에만"처럼 조건을 적으면 과한 위임이 줄어듭니다.
- 위임의 깊이는 한 단계로 제한한다. 서브에이전트가 또 서브에이전트를 부르기 시작하면 비용과 동작을 추적하기 어려워집니다. 서브에이전트에게는 delegate 도구를 주지 않는 것으로 구조적으로 막을 수 있습니다.
흔히 걸려 넘어지는 곳 #
- 서브에이전트가 맥락을 공유한다고 가정한다 — 서브에이전트는 메인 대화를 전혀 모릅니다. task에 적힌 것이 아는 것의 전부입니다. 필요한 배경은 task에 다 담아야 합니다.
- 보고 형식 없이 위임한다 — 형식 없는 보고는 핵심이 빠지거나 장황해집니다. 기대하는 출력을 task에 명시합니다.
- 모든 것을 위임한다 — 위임 자체가 목적이 되면 단순 작업에도 루프 하나씩을 태웁니다. 직접 하는 게 빠른 일의 기준을 description에 적어 둡니다.
마무리 #
이번 글에서는 일을 나눠 맡기는 서브에이전트를 다뤘습니다.
- 서브에이전트는 별도 messages로 도는 또 하나의 루프이고, 메인 에이전트에게는 도구 하나로 보입니다.
- 가치의 핵심은 컨텍스트 격리입니다. 중간 과정은 서브에이전트가 치르고 결론만 돌아옵니다.
- task에 보고 형식을 계약처럼 명시하고, 위임 조건과 깊이를 제한해 비용을 관리합니다.
지금까지 도구는 모두 우리 코드 안의 파이썬 함수였습니다. 다음 글인 “AI 에이전트 개발 실전 #6 MCP 서버 직접 만들기"에서는 도구를 표준 프로토콜의 서버로 분리해서, 어떤 에이전트에서든 재사용하는 방법을 다룹니다.