Skip to main content
멀티 에이전트 시스템은 복잡한 애플리케이션을 여러 특화된 에이전트로 분할하여 문제를 해결하도록 협력합니다. 단일 에이전트가 모든 단계를 처리하는 대신, 멀티 에이전트 아키텍처를 사용하면 더 작고 집중된 에이전트들을 조율된 워크플로우로 구성할 수 있습니다. 멀티 에이전트 시스템은 다음과 같은 경우에 유용합니다:
  • 단일 에이전트가 너무 많은 도구를 보유하여 어떤 도구를 사용할지 잘못된 결정을 내리는 경우
  • 컨텍스트나 메모리가 하나의 에이전트가 효과적으로 추적하기에는 너무 커지는 경우
  • 작업에 전문화가 필요한 경우 (예: 계획자, 연구원, 수학 전문가)

멀티 에이전트 패턴

패턴작동 방식제어 흐름사용 사례 예시
Tool Calling슈퍼바이저 에이전트가 다른 에이전트를 도구로 호출합니다. “도구” 에이전트는 사용자와 직접 대화하지 않고 작업을 실행하고 결과를 반환합니다.중앙집중형: 모든 라우팅이 호출 에이전트를 통과합니다.작업 오케스트레이션, 구조화된 워크플로우.
Handoffs현재 에이전트가 다른 에이전트로 제어를 이전할 것을 결정합니다. 활성 에이전트가 변경되며, 사용자는 새 에이전트와 직접 상호작용을 계속할 수 있습니다.분산형: 에이전트가 활성 에이전트를 변경할 수 있습니다.멀티 도메인 대화, 전문가 인수.

튜토리얼: 슈퍼바이저 에이전트 만들기

슈퍼바이저 패턴을 사용하여 개인 비서를 만드는 방법을 배워보세요. 중앙 슈퍼바이저 에이전트가 전문화된 워커 에이전트를 조율합니다. 이 튜토리얼에서는 다음을 다룹니다:
  • 다양한 도메인(캘린더 및 이메일)을 위한 전문화된 서브 에이전트 생성
  • 중앙집중식 오케스트레이션을 위해 서브 에이전트를 도구로 래핑
  • 민감한 작업에 대한 휴먼 인 더 루프 검토 추가

패턴 선택하기

질문Tool CallingHandoffs
워크플로우에 대한 중앙집중식 제어가 필요한가요?✅ 예❌ 아니오
에이전트가 사용자와 직접 상호작용하길 원하나요?❌ 아니오✅ 예
전문가 간의 복잡하고 인간적인 대화가 필요한가요?❌ 제한적✅ 강력함
두 패턴을 혼합할 수 있습니다 — 에이전트 전환을 위해 handoffs를 사용하고, 각 에이전트가 특화된 작업을 위해 서브 에이전트를 도구로 호출하도록 할 수 있습니다.

에이전트 컨텍스트 커스터마이징

멀티 에이전트 설계의 핵심은 컨텍스트 엔지니어링입니다 - 각 에이전트가 어떤 정보를 보는지 결정하는 것입니다. LangChain은 다음에 대한 세밀한 제어를 제공합니다:
  • 대화 또는 상태의 어느 부분을 각 에이전트에게 전달할지
  • 서브 에이전트에 맞춤화된 전문 프롬프트
  • 중간 추론의 포함/제외
  • 에이전트별 입력/출력 형식 커스터마이징
시스템의 품질은 컨텍스트 엔지니어링에 크게 의존합니다. 목표는 각 에이전트가 도구로 작동하든 활성 에이전트로 작동하든 작업을 수행하는 데 필요한 올바른 데이터에 액세스할 수 있도록 하는 것입니다.

Tool calling

Tool calling에서는 하나의 에이전트(“컨트롤러”)가 필요할 때 다른 에이전트를 도구로 취급하여 호출합니다. 컨트롤러는 오케스트레이션을 관리하고, 도구 에이전트는 특정 작업을 수행하고 결과를 반환합니다. 흐름:
  1. 컨트롤러가 입력을 받고 어떤 도구(서브 에이전트)를 호출할지 결정합니다.
  2. 도구 에이전트가 컨트롤러의 지시에 따라 작업을 실행합니다.
  3. 도구 에이전트가 결과를 컨트롤러에게 반환합니다.
  4. 컨트롤러가 다음 단계를 결정하거나 종료합니다.
도구로 사용되는 에이전트는 일반적으로 사용자와 대화를 계속할 것으로 기대되지 않습니다. 이들의 역할은 작업을 수행하고 결과를 컨트롤러 에이전트에게 반환하는 것입니다. 서브 에이전트가 사용자와 대화할 수 있어야 한다면 대신 handoffs를 사용하세요.

구현

다음은 메인 에이전트가 도구 정의를 통해 단일 서브 에이전트에 액세스할 수 있도록 제공하는 최소 예제입니다:
import { createAgent, tool } from "langchain";
import * as z from "zod";

const subagent1 = createAgent({...});

const callSubagent1 = tool(
  async ({ query }) => {
    const result = await subagent1.invoke({
      messages: [{ role: "user", content: query }]
    });
    return result.messages.at(-1)?.text;
  },
  {
    name: "subagent1_name",
    description: "subagent1_description",
    schema: z.object({
      query: z.string().describe("The query to to send to subagent1."),
    }),
  }
);

const agent = createAgent({
  model,
  tools: [callSubagent1]
});
이 패턴에서:
  1. 메인 에이전트는 작업이 서브 에이전트의 설명과 일치한다고 판단할 때 call_subagent1을 호출합니다.
  2. 서브 에이전트는 독립적으로 실행되고 결과를 반환합니다.
  3. 메인 에이전트는 결과를 받고 오케스트레이션을 계속합니다.

커스터마이징 지점

메인 에이전트와 서브 에이전트 간에 컨텍스트가 전달되는 방식을 제어할 수 있는 여러 지점이 있습니다:
  1. 서브 에이전트 이름 ("subagent1_name"): 메인 에이전트가 서브 에이전트를 참조하는 방식입니다. 프롬프팅에 영향을 미치므로 신중하게 선택하세요.
  2. 서브 에이전트 설명 ("subagent1_description"): 메인 에이전트가 서브 에이전트에 대해 “아는” 것입니다. 메인 에이전트가 언제 호출할지 결정하는 방식을 직접적으로 형성합니다.
  3. 서브 에이전트로의 입력: 서브 에이전트가 작업을 해석하는 방식을 더 잘 형성하기 위해 이 입력을 커스터마이징할 수 있습니다. 위 예제에서는 에이전트가 생성한 query를 직접 전달합니다.
  4. 서브 에이전트로부터의 출력: 메인 에이전트에게 다시 전달되는 응답입니다. 메인 에이전트가 결과를 해석하는 방식을 제어하기 위해 무엇이 반환되는지 조정할 수 있습니다. 위 예제에서는 최종 메시지 텍스트를 반환하지만, 추가 상태나 메타데이터를 반환할 수도 있습니다.

서브 에이전트로의 입력 제어

메인 에이전트가 서브 에이전트에게 전달하는 입력을 제어하는 두 가지 주요 레버가 있습니다:
  • 프롬프트 수정 – 메인 에이전트의 프롬프트 또는 도구 메타데이터(즉, 서브 에이전트의 이름과 설명)를 조정하여 언제, 어떻게 서브 에이전트를 호출할지 더 잘 안내합니다.
  • 컨텍스트 주입 – 정적 프롬프트에 캡처하기 어려운 입력(예: 전체 메시지 기록, 이전 결과, 작업 메타데이터)을 에이전트의 상태에서 가져오도록 도구 호출을 조정하여 추가합니다.
import { createAgent, tool, AgentState, ToolMessage } from "langchain";
import { Command } from "@langchain/langgraph";
import * as z from "zod";

// 상태를 통해 전체 대화 기록을 서브 에이전트에게 전달하는 예제
const callSubagent1 = tool(
  async ({query}) => {
    const state = getCurrentTaskInput<AgentState>();
    // 메시지를 적절한 입력으로 변환하는 데 필요한 로직을 적용합니다
    const subAgentInput = someLogic(query, state.messages);
    const result = await subagent1.invoke({
      messages: subAgentInput,
      // 필요에 따라 다른 상태 키도 여기에 전달할 수 있습니다.
      // 메인과 서브 에이전트의 상태 스키마 모두에 이를 정의해야 합니다.
      exampleStateKey: state.exampleStateKey
    });
    return result.messages.at(-1)?.content;
  },
  {
    name: "subagent1_name",
    description: "subagent1_description",
  }
);

서브 에이전트로부터의 출력 제어

메인 에이전트가 서브 에이전트로부터 받는 것을 형성하는 두 가지 일반적인 전략:
  • 프롬프트 수정 – 서브 에이전트의 프롬프트를 개선하여 정확히 무엇이 반환되어야 하는지 지정합니다.
    • 출력이 불완전하거나, 너무 장황하거나, 주요 세부 정보가 누락된 경우 유용합니다.
    • 일반적인 실패 모드는 서브 에이전트가 도구 호출이나 추론을 수행하지만 최종 메시지에 결과를 포함하지 않는 것입니다. 컨트롤러(및 사용자)는 최종 출력만 보므로 모든 관련 정보가 거기에 포함되어야 함을 상기시키세요.
  • 커스텀 출력 형식 – 메인 에이전트에게 전달하기 전에 코드에서 서브 에이전트의 응답을 조정하거나 강화합니다.
    • 예: 최종 텍스트 외에 특정 상태 키를 메인 에이전트에게 다시 전달합니다.
    • 이를 위해서는 서브 에이전트의 응답과 커스텀 상태를 병합할 수 있도록 결과를 Command (또는 동등한 구조)로 래핑해야 합니다.
import { tool, ToolMessage } from "langchain";
import { Command } from "@langchain/langgraph";
import * as z from "zod";

const callSubagent1 = tool(
  async ({ query }, config) => {
    const result = await subagent1.invoke({
      messages: [{ role: "user", content: query }]
    });

    // 여러 상태 키를 업데이트하기 위해 Command를 반환합니다
    return new Command({
      update: {
        // 서브 에이전트에서 추가 상태를 다시 전달합니다
        exampleStateKey: result.exampleStateKey,
        messages: [
          new ToolMessage({
            content: result.messages.at(-1)?.text,
            tool_call_id: config.toolCall?.id!
          })
        ]
      }
    });
  },
  {
    name: "subagent1_name",
    description: "subagent1_description",
    schema: z.object({
      query: z.string().describe("The query to send to subagent1")
    })
  }
);

Handoffs

Handoffs에서 에이전트는 서로에게 직접 제어를 전달할 수 있습니다. “활성” 에이전트가 변경되며, 사용자는 현재 제어권을 가진 에이전트와 상호작용합니다. 흐름:
  1. 현재 에이전트가 다른 에이전트의 도움이 필요하다고 판단합니다.
  2. 제어(및 상태)를 다음 에이전트에게 전달합니다.
  3. 새 에이전트가 다시 전달하거나 종료할 때까지 사용자와 직접 상호작용합니다.

구현 (곧 제공 예정)


Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.
I