본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 02. 22:27

Laravel AI SDK 서브 에이전트(Sub-Agents): 에이전트를 오케스트레이션 계층으로 전환하기

요약

Laravel AI SDK의 서브 에이전트 기능을 활용하여 멀티 에이전트 오케스트레이션 패턴을 최적화하는 방법을 다룹니다. 에이전트 루프의 내부 작동 원리와 토큰 사용량의 복리 증가 문제를 설명하며 효율적인 에이전트 설계의 중요성을 강조합니다.

핵심 포인트

  • 에이전트 루프는 모델 호출, 도구 실행, 결과 추가의 반복 사이클로 구성됨
  • MaxSteps는 도구 호출 횟수가 아닌 총 요청-응답 사이클을 의미함
  • 대화 기록이 누적됨에 따라 토큰 사용량은 복리로 증가함
  • 멀티 에이전트 설계 시 올바른 패턴 선택이 비용과 안정성의 핵심임

긴 연휴 동안 4개의 에이전트 파이프라인(pipeline)이 계속 실행되고 있다고 상상해 보십시오. 두 에이전트가 서로를 루프(loop)로 호출하기 시작합니다. Analyzer가 Verifier를 호출하고, Verifier가 다시 Analyzer를 호출합니다. 그들의 중단 조건(stopping conditions)은 모호하고 예산 집행(budget enforcement)은 비동기적(asynchronous)이기 때문입니다. 아무도 계량기를 지켜보고 있지 않습니다. 월요일 아침에 누군가 이를 알아차렸을 때, 청구 금액은 이미 5자릿수(만 달러 단위)를 훌쩍 넘어선 상태입니다.

도구는 존재했습니다. 에이전트도 존재했습니다. 패턴이 잘못되었을 뿐입니다.

멀티 에이전트 오케스트레이션(Multi-agent orchestration)이 어려운 이유는 API가 복잡해서가 아닙니다. 올바른 코드를 작성하는 것보다 올바른 패턴을 선택하는 것이 더 중요하기 때문에 어렵습니다. Laravel AI SDK, 특히 2026년 5월 12일에 추가된 서브 에이전트(sub-agent) 기능은 각 패턴에 대한 PHP 네이티브 프리미티브(primitives)를 제공합니다. 하지만 이 SDK는 언제 어떤 패턴을 사용해야 하는지, 내부적으로 비용이 어떻게 발생하는지, 또는 데모에서는 작동하지만 프로덕션(production) 환경에서는 무엇이 망가지는지에 대해서는 알려주지 않습니다.

이 글이 바로 그 부분을 다루기 위한 것입니다.

Prism의 에이전트 루프(Agentic Loop)가 내부적으로 작동하는 방식

서브 에이전트를 이해하기 전에, 먼저 그들이 실행되는 내부 루프를 이해해야 합니다.

Laravel AI 에이전트에서 ->prompt()를 호출하거나(또는 Prism 요청에서 ->asText()를 호출할 때), 반환되는 값이 반드시 첫 번째 API 호출의 응답이라는 보장은 없습니다. 만약 에이전트 클래스에 #[MaxSteps(10)]을 설정했다면, SDK는 루프를 돌게 됩니다.

각 사이클에서 루프가 어떻게 작동하는지는 다음과 같습니다:

  1. 모델이 사용자의 프롬프트(prompt)와 시스템 지침(system instructions)을 받습니다.
  2. 모델이 도구(tool)를 호출하기로 결정하면, 최종 텍스트 응답 대신 tool_use 블록을 반환합니다.
  3. Prism이 도구를 실행하거나, 만약 .concurrent()로 표시했다면 모든 동시 도구들을 병렬로 실행합니다.
  4. Prism은 그 결과를 ToolResultMessage로서 대화 내용에 추가합니다.
  5. 전체 대화 내용(원래의 프롬프트, 도구 호출, 도구 결과)이 모델로 다시 전송됩니다.
  6. 모델이 텍스트 응답을 반환하거나 MaxSteps에 도달할 때까지 이 과정을 반복합니다.

Prism 직접 사용 예시:

use Prism\Prism\Prism;
use Prism\Prism\Enums\Provider;
use Prism\Prism\Facades\Tool;
...

여기서 이해해야 할 두 가지 사항이 있습니다. 첫째, withMaxSteps(5)는 도구 호출(tool calls)의 횟수가 아니라 총 요청-응답 사이클(request-response cycles)의 횟수입니다. 각 단계(step)에는 하나의 모델 호출, 그에 따른 모든 도구 호출, 그리고 해당 도구들로부터 얻은 모든 결과가 포함됩니다. 둘째, 늘어나는 대화 기록(conversation history)은 매 단계마다 다시 전송됩니다. 5단계에 도달하면, 모델은 이전 4단계에서 발생한 모든 도구 호출과 모든 결과가 포함된 전체 스레드(thread)를 받게 됩니다. 따라서 토큰 수(token count)는 선형적으로 증가하지 않습니다. 각 단계마다 복리로 증가합니다.

Laravel AI SDK의 클래스 기반 에이전트(class-based agents)는 PHP 어트리뷰트(attributes)에 의해 제어되는 동일한 루프를 사용합니다:

use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Attributes\MaxTokens;
use Laravel\Ai\Attributes\Model;
...

Vertical cycle diagram of the Prism agentic loop: prompt, model returns a tool_use block, Prism executes tools, a ToolResultMessage is appended, the full conversation is re-sent, then it repeats, with a steps-remaining counter and a growing conversation-history blob.

이 루프는 이 SDK에 있는 모든 에이전트의 기초입니다. 서브 에이전트(Sub-agents)는 단지 그 루프의 한 단계를 자신만의 지침(instructions), 도구(tools), 그리고 컨텍스트(context)를 가진 다른 에이전트에게 위임하는 방법일 뿐입니다.

에이전트를 도구로 전환하기

서브 에이전트 기능은 보기보다 매우 단순합니다. 다른 에이전트의 tools() 메서드에서 에이전트 클래스 인스턴스를 반환하기만 하면 됩니다.

use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
...

오케스트레이터(orchestrator)가 고객의 메시지를 받습니다. 모델은 지침과 도구 정의(각 서브 에이전트의 이름과 설명을 포함하여)를 읽고, 어떤 전문가를 호출할지 결정합니다. 서브 에이전트가 실행되어 응답을 반환하면, 그 응답은 도구 결과(tool result)로서 오케스트레이터에게 돌아옵니다. 오케스트레이터는 이를 자신의 최종 답변에 통합합니다.

SDK가 서브 에이전트를 무엇이라 부를지, 그리고 언제 호출할지를 알 수 있도록, 서브 에이전트는 CanActAsTool을 구현해야 합니다:

use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
...

name()description() 메서드는 오케스트레이터(orchestrator)의 모델이 도구 목록(tool list)에서 보게 되는 정보입니다. 여기서 대부분의 개발자가 실수를 범합니다. 만약 설명(description)이 모호하다면("Handles refunds"), 모델은 어떤 입력을 전달해야 할지 또는 어떤 결과가 돌아올지를 알 수 없습니다. 설명은 잘 작성된 독스트링(docstring)처럼 작성되어야 합니다. 즉, 이 에이전트가 무엇을 입력받는지, 무엇을 반환하는지, 그리고 형제 도구(sibling tool) 대신 언제 이 에이전트를 호출해야 하는지를 명시해야 합니다.

사람들을 당황하게 만드는 한 가지 설계 결정이 있습니다. 바로 서브 에이전트(sub-agent)는 부모의 대화 기록(conversation history)을 전달받지 않는다는 점입니다. 오케스트레이터가 RefundsAgent를 호출할 때, 해당 에이전트는 깨끗한 컨텍스트(context)를 받게 됩니다. 고객이 무엇을 말했든, 오케스트레이터가 이미 무엇을 학습했든, 그 어떤 것도 자동으로 흘러 내려가지 않습니다. 오케스트레이터는 관련 컨텍스트를 추출하여 서브 에이전트를 호출할 때 독립적인 작업 설명(task description)으로 전달해야 합니다.

이는 의도된 설계입니다. 만약 오케스트레이터가 20,000 토큰의 대화 기록을 축적한 상태에서 세 개의 서브 에이전트를 호출한다면, 실제 작업이 시작되기도 전에 전체 기록을 각 에이전트에 무분별하게 전달함으로써 컨텍스트 비용(context spend)을 세 배로 늘리게 됩니다. 이러한 격리(isolation)는 비용을 예측 가능하게 유지해 줍니다. 하지만 그만큼 책임이 전가됩니다. 즉, 오케스트레이터의 프롬프트(prompt)는 무엇을 넘겨주고 있는지에 대해 명시적이어야 합니다.

각 서브 에이전트는 설정 측면에서 완전히 독립적입니다. 오케스트레이터가 OpenAI에서 실행되는 동안 RefundsAgent를 Anthropic으로 고정할 수 있습니다. BillingAgent에는 복잡한 조회를 위해 넉넉한 #[MaxSteps(5)]를 부여하는 한편, 단순 쿼리를 위한 ShippingAgent#[MaxSteps(2)]로 유지할 수 있습니다. 설정은 상속되지 않으며 서로 간섭하지도 않습니다.

오케스트레이터-워커 패턴 (The Orchestrator-Workers Pattern)

오케스트레이터-워커(orchestrator-workers) 패턴은 작업의 실행 경로를 사전에 알 수 없을 때 선택하게 되는 방식입니다.

고객 지원 티켓(customer support ticket)은 주문 상태 확인, 부분 환불 처리, 상담원 연결, 또는 이 세 가지 모두를 필요로 할 수 있습니다. 티켓을 읽기 전까지는 무엇이 필요할지 알 수 없습니다. 정적인 코드 경로(static code path)로는 이를 처리할 수 없습니다. 하드코딩된 match ($type) 체인은 요청이 모호해지거나 여러 단계가 필요해지는 순간 무너집니다. 오케스트레이터-워커(orchestrator-workers) 패턴은 라우팅 결정(routing decision)과 시퀀싱 결정(sequencing decision) 모두를 모델에 위임합니다.

오케스트레이터는 상위 수준의 목표를 부여받습니다. 오케스트레이터는 도구(tools)로서 일련의 워커 에이전트(worker agents)에 접근할 수 있습니다. 오케스트레이터는 다음과 같이 계획하고 실행합니다: "먼저 LookupOrderTool을 호출하고, 그 결과에 따라 RefundsAgent를 호출하거나 상담원에게 연결하라." 워커들은 전체 계획을 알지 못합니다. 각 워커는 작업을 전달받아 실행하고 결과를 반환할 뿐입니다.

다음은 코드 리뷰 워크플로(workflow)를 위한 완전한 오케스트레이터 예시입니다:

#[Provider(Lab::Anthropic)]
#[Model('claude-opus-4-8')]
#[MaxSteps(8)]
...

각 워커는 자신의 작업에 적합한 모델에 고정된(pinned) 독립적인 에이전트 클래스입니다:

#[Provider(Lab::Anthropic)]
#[Model('claude-haiku-4-5-20251001')]
#[MaxSteps(2)]
...

각 워커에는 Haiku를, 오케스트레이터에는 Opus를 사용하는 방식입니다. 즉, 워커는 좁고 집중적인 작업을 수행하고, 오케스트레이터는 계획과 종합(synthesis)을 수행합니다. 이러한 모델 계층 구조는 비용 측면에서 중요합니다. Opus는 Haiku보다 토큰당 비용이 대략 5배 더 비쌉니다. 워커를 Haiku로 실행하고 Opus를 오케스트레이터용으로 예약해 두면, 비싼 모델이 꼭 필요한 작업에만 사용되도록 유지할 수 있습니다.

세 가지 리뷰가 독립적이고 오케스트레이터가 순서를 정할 때까지 기다리는 대신 병렬로 실행하고 싶다면, 에이전트 루프(agentic loop)에서 병렬 처리를 분리하여 PHP 코드에 직접 구현할 수 있습니다:

use Illuminate\\\Support\\\Facades\\\Concurrency;

[$security, $performance, $style] = Concurrency::run([
...

이는 정적이고 사전에 계획된 워크플로에 더 빠릅니다. 오케스트레이터가 도구를 통해 워커를 호출하는 오케스트레이터-워커 패턴은 계획 자체를 런타임(runtime)에 계산해야 할 때 사용합니다.

평가자-최적화(Evaluator-Optimizer) 패턴

평가자-최적화(Evaluator-Optimizer) 패턴은 다른 문제를 해결합니다. "어떤 에이전트가 이를 처리해야 하는가?"라고 묻는 대신, "이 출력이 사용하기에 충분히 좋은가?"라고 묻습니다.

이 구조는 루프(loop) 형태입니다: 무언가를 생성하고, 명시적인 기준에 따라 이를 평가하며, 통과하지 못하면 평가자의 구체적인 피드백을 바탕으로 다시 생성합니다. 통과하거나 최대 반복 횟수에 도달할 때까지 이 과정을 반복합니다. 이 패턴은 측정 가능한 품질이 필요한 작업에 효과적입니다. 예를 들어 테스트를 통과해야 하는 코드, 편집 기준을 충족해야 하는 콘텐츠, 기술적 정확성을 유지해야 하는 번역 등이 있습니다. 하지만 기준이 모호하거나 반복 작업이 실제로 출력을 개선하지 못할 때는 제대로 작동하지 않습니다.

구조화된 평가를 포함한 콘텐츠 생성 루프:

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Schema\JsonSchema;
...

그리고 제어 루프(control loop):

$writer    = new TechnicalWriterAgent;
$evaluator = new EditorialEvaluatorAgent;

...

이 패턴이 작동하게 만드는 몇 가지 요소가 있습니다. 평가자는 구조화된 출력(structured output)을 반환합니다. approved: bool은 깔끔한 종료 조건(exit condition)을 제공하며, issues: string은 작성자에게 모호한 비판 대신 실행 가능한 피드백을 제공합니다. 최대 반복 횟수(max iterations) 제한은 평가자가 결코 승인하지 않더라도 루프가 무한히 실행되는 것을 방지합니다. 평가자는 Haiku(#[MaxSteps(1)])에서 실행되는데, 이는 점수를 매기는 작업이 글을 쓰는 것만큼 깊은 추론(reasoning) 능력을 요구하지 않기 때문입니다.

실패 모드는 모호한 평가 기준입니다. 만약 평가자의 지침이 "좋은지 평가하라"라고 되어 있다면, "개선이 필요함"과 같은 피드백을 받게 되고, 이는 약간은 다르지만 똑같이 평범한 두 번째 초안을 만들어낼 뿐입니다. 모든 품질 기준은 반증 가능(falsifiable)해야 합니다. "주요 개념당 최소 하나의 코드 예시 포함"은 반증 가능하지만, "명확하고 매력적인"은 그렇지 않습니다. 모호한 기준은 평가자-최적화 루프가 수렴하지 못한 채 겉도는 가장 흔한 원인입니다.

Side-by-side comparison of two multi-agent patterns: on the left an orchestrator delegates to three worker agents whose results converge into a synthesis step; on the right a writer-evaluator loop regenerates on rejection until the output is approved.

Orchestrator-Workers vs Evaluator-Optimizer: 언제 무엇을 사용할 것인가

두 패턴은 서로 다른 문제를 해결합니다. 잘못된 패턴을 선택하면 품질과 비용 모두에 악영향을 미칩니다.

Orchestrator-Workers (오케스트레이터-워커) 패턴은 실행 경로를 사전에 알 수 없어 모델이 이를 계획해야 할 때 사용합니다. 고객 지원(Customer support) 예시가 교과서적인 사례입니다. 서로 다른 입력값이 정적으로 결정할 수 없는 순서에 따라 서로 다른 전문가에게 라우팅됩니다. 또한 서브태스크(Subtasks)가 진정으로 다른 요구사항(서로 다른 모델, 서로 다른 지침 세트, 서로 다른 도구 액세스 권한)을 가질 때도 사용하십시오. 50개의 도구를 가진 하나의 거대한 에이전트는 해결하는 문제보다 더 많은 문제를 일으킬 수 있습니다(이에 대해서는 곧 자세히 다루겠습니다).

Evaluator-Optimizer (평가자-최적화 도구) 패턴은 측정 가능한 품질 기준이 있는 단일 작업이 있고, 해당 작업이 반복(Iteration)을 통해 이득을 얻을 때 사용합니다. 글쓰기와 번역은 피드백 루프(Feedback loops)를 통해 개선됩니다. 코드 생성(Code generation)은 평가자가 출력이 실제로 컴파일되거나 테스트를 통과하는지 확인할 수 있을 때 개선됩니다. 루프를 실행하기 전에 세 가지의 반증 가능한 수락 기준(Acceptance criteria)을 작성할 수 있다면, Evaluator-Optimizer를 시도해 볼 가치가 있습니다. 만약 그렇지 못하다면, 루프는 수렴하지 못한 채 반복만 하게 될 것입니다.

두 패턴의 상호 보완성: Orchestrator-Workers 설정 내의 개별 워커(Worker) 안에 Evaluator-Optimizer 루프를 포함할 수 있습니다. 오케스트레이터가 환불 요청을 RefundsAgent로 라우팅하면, 내부적으로 RefundsAgent는 결과를 반환하기 전에 환불 자격 확인을 검증하기 위해 평가자 루프를 사용합니다. 패턴은 서로 조합될 수 있습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0