에이전틱스
도메인 1. 에이전트와 워크플로

1.1에이전트 아키텍처

예상 학습 시간 30

같은 기능을 워크플로로 짤 수도, 에이전트로 짤 수도 있습니다. 이 수업에서는 그 선택을 코드 관점에서 가르는 기준과, 매니저·서브에이전트 계층에서 무엇이 오가는지를 다룹니다. 설계도를 그리는 관점이 아니라 어느 쪽을 구현할지 정하는 관점입니다.

제어 흐름을 누가 쥐는가

워크플로와 에이전트를 가르는 기준은 규모나 복잡도가 아닙니다. 다음에 무엇을 할지 코드가 정하느냐, 모델이 정하느냐입니다.

워크플로에서는 호출 순서가 소스 코드에 적혀 있습니다. 읽으면 경로를 알 수 있고, 같은 입력에 같은 경로를 탑니다. 에이전트에서는 그 순서가 실행 중에 정해집니다. 소스에는 도구 목록과 루프만 있고, 어떤 도구를 몇 번 쓸지는 응답을 받아야 압니다.

# 워크플로 — 경로가 코드에 있다
text = fetch_document(doc_id)
summary = summarize(text)
tags = classify(summary)
save(doc_id, summary, tags)

# 에이전트 — 경로가 실행 중에 정해진다
while True:
    resp = client.messages.create(model=MODEL, tools=tools, messages=messages)
    if resp.stop_reason != "tool_use":
        break
    messages.append({"role": "assistant", "content": resp.content})
    messages.append({"role": "user", "content": run_tools(resp.content)})

구현 비용도 다릅니다. 워크플로는 각 단계를 따로 테스트할 수 있고 실패 지점이 분명합니다. 에이전트는 같은 입력에도 경로가 달라질 수 있어, 단위 테스트보다 관측과 상한에 기대야 합니다.

그래서 선택 기준이 나옵니다. 처리할 경우의 수를 미리 적을 수 있으면 워크플로로 짜는 편이 싸고 안전합니다. 입력마다 필요한 단계가 달라 미리 적을 수 없을 때만 에이전트가 값을 합니다. 시험에서는 "복잡하니까 에이전트"라는 선택지가 오답으로 자주 나옵니다. 복잡해도 경로가 고정이면 워크플로입니다.

매니저와 서브에이전트

한 에이전트가 도구를 스무 개 들고 있으면 모델이 매번 스무 개를 훑어야 하고, 대화 이력에 모든 중간 결과가 쌓입니다. 이때 쓰는 구조가 매니저(감독자) 계층입니다.

매니저는 목표를 쪼개 서브에이전트에 맡기고, 돌아온 결과를 모아 다음 판단을 합니다. 서브에이전트는 자기 몫에 필요한 도구만 들고, 자기만의 대화 이력에서 작업합니다.

여기서 구현할 때 가장 자주 틀리는 부분이 있습니다. 부모가 돌려받는 것은 서브에이전트의 대화 전체가 아니라 최종 결과 하나입니다. 서브에이전트가 도구를 열 번 호출했든 한 번 호출했든, 그 중간 과정은 부모의 컨텍스트에 들어오지 않습니다.

def run_subagent(goal: str, tools: list) -> str:
    """서브에이전트를 독립된 대화로 돌리고 최종 텍스트만 반환한다."""
    messages = [{"role": "user", "content": goal}]
    for _ in range(MAX_TURNS):
        resp = client.messages.create(model=MODEL, tools=tools, messages=messages)
        if resp.stop_reason != "tool_use":
            return text_of(resp)          # ← 부모에게는 이것만 간다
        messages.append({"role": "assistant", "content": resp.content})
        messages.append({"role": "user", "content": run_tools(resp.content)})
    raise SubagentBudgetExceeded(goal)

이 성질이 곧 이 구조의 이점입니다. 검색 결과 수천 줄을 서브에이전트가 읽고 요약 세 줄만 올려 보내면, 부모의 컨텍스트는 세 줄만 씁니다. 대신 잃는 것도 있습니다. 부모는 서브에이전트가 무엇을 근거로 그 답을 냈는지 모릅니다. 근거가 필요하면 반환 형식에 근거를 포함하도록 목표 문장에 적어야 합니다.

분해를 어디까지 할 것인가

계층을 깊게 만들수록 좋아 보이지만, 단계마다 비용이 붙습니다. 서브에이전트 하나를 부르면 그 안에서 별도의 모델 호출이 여러 번 일어나고, 부모는 그 결과를 기다립니다. 지연은 더해지고 토큰도 더해집니다.

분해가 값을 하는 조건은 둘입니다.

  • 병렬로 돌 수 있을 때. 서로 의존하지 않는 하위 작업 세 개를 동시에 돌리면 지연이 가장 느린 하나로 수렴합니다. 순차로 도는 분해는 지연만 늘립니다.
  • 컨텍스트를 크게 줄일 때. 하위 작업이 큰 입력을 읽고 작은 결과를 낼 때 이득이 큽니다. 반대로 결과가 입력만큼 크면 격리 효과가 없습니다.
import asyncio

async def gather(goals: list[str]) -> list[str]:
    # 서로 의존하지 않는 하위 작업만 병렬로 묶는다
    return await asyncio.gather(*(run_subagent_async(g, tools) for g in goals))

깊이도 통제해야 합니다. 서브에이전트가 다시 서브에이전트를 부르게 두면 호출 트리가 예측 불가능하게 자랍니다. 실무에서는 깊이를 1~2로 제한하고, 더 필요하면 구조를 다시 보는 편이 낫습니다. 깊이 제한은 프롬프트가 아니라 코드에서 걸어야 지켜집니다.

무엇을 코드로 남길 것인가

에이전트를 쓰기로 했더라도 전부를 모델에 맡기지는 않습니다. 실무 구조는 대개 판단은 모델, 실행은 코드로 갈립니다.

  • 모델이 맡는 것: 어떤 도구를 쓸지, 어떤 순서로 갈지, 언제 멈출지
  • 코드가 맡는 것: 도구의 실제 실행, 권한 검사, 재시도, 상한, 결과 기록

이 경계를 지키면 모델이 잘못 판단해도 피해가 코드 안에서 막힙니다. 반대로 권한 검사까지 프롬프트에 맡기면, 모델이 지시를 무시하는 순간 방어가 없습니다.

DESTRUCTIVE = {"delete_record", "send_email"}

def run_tools(blocks):
    results = []
    for b in tool_use_blocks(blocks):
        if b.name in DESTRUCTIVE and not approved(b):
            # 거부도 결과로 돌려줘야 모델이 다음 수를 고를 수 있다
            results.append(tool_result(b.id, "승인되지 않아 실행하지 않았습니다.", is_error=True))
            continue
        results.append(tool_result(b.id, execute(b)))
    return results

거부할 때도 결과를 돌려주는 것이 중요합니다. 아무 것도 돌려주지 않으면 대화가 끊기고, 모델은 왜 실패했는지 모른 채 같은 호출을 반복합니다. 시험에서는 "위험한 도구는 목록에서 빼면 된다"는 선택지가 나오는데, 그러면 그 기능 자체가 사라집니다. 목록에는 두되 실행 직전에 막는 것이 실무 답입니다.

시험 함정

  • 복잡도로 워크플로와 에이전트를 가르는 선택지 — 기준은 복잡도가 아니라 경로를 코드가 정하느냐 모델이 정하느냐입니다.
  • 부모가 서브에이전트의 대화 전체를 돌려받는다는 서술 — 부모에게 가는 것은 최종 결과 하나뿐입니다.
  • 분해하면 항상 빨라진다는 선택지 — 순차로 도는 분해는 지연을 더하기만 합니다. 병렬로 돌 때만 줄어듭니다.
  • 깊이 제한을 프롬프트로 거는 선택지 — 프롬프트는 상한이 아니며 코드에서 걸어야 지켜집니다.
  • 권한 검사를 시스템 프롬프트에 맡기는 선택지 — 모델이 지시를 벗어나는 순간 방어가 없습니다.
  • 거부한 도구 호출에 아무 결과도 돌려주지 않는 처리 — 대화가 끊기고 모델이 같은 호출을 반복합니다.

실습 시나리오

실제 시험과 같은 형식의 시나리오 문제입니다.

문서를 받아 요약하고 태그를 붙여 저장하는 기능을 만듭니다. 문서 종류와 처리 순서는 항상 같고 예외 경로도 미리 정해져 있습니다. 구현 방식으로 가장 적절한 것은 무엇입니까?

빌드 연습 · 같은 기능을 워크플로와 에이전트로 각각 구현해 비교하기

40
  1. 1.워크플로 버전

    문서 요약과 태깅을 순서가 고정된 함수 호출로 구현합니다.

    기대 결과 · 함수 세 개가 순서대로 호출되고 각각 단위 테스트가 붙어 있습니다.

  2. 2.에이전트 버전

    같은 기능을 도구 세 개와 루프로 구현합니다.

    기대 결과 · stop_reason이 tool_use인 동안 도구를 실행하고 결과를 대화에 넣습니다.

  3. 3.토큰과 지연 계측

    같은 문서 다섯 건을 두 버전에 넣고 usage와 소요 시간을 기록합니다.

    기대 결과 · 두 버전의 총 토큰과 총 시간이 표로 남습니다.

  4. 4.예외 입력 투입

    형식이 다른 문서를 넣어 두 버전이 각각 어떻게 실패하는지 봅니다.

    기대 결과 · 워크플로는 특정 단계에서 멈추고, 에이전트는 다른 경로를 시도하는 것이 기록에 남습니다.

  5. 5.선택 기준 정리

    관측한 수치를 근거로 이 기능에 어느 쪽이 맞는지 한 문단으로 적습니다.

    기대 결과 · 복잡도가 아니라 경로의 예측 가능성을 근거로 삼은 문장이 나옵니다.

출처 및 더 읽기