본문 바로가기

AI

[AI] 멀티 에이전트를 만들어보자!

멀티 에이전트가 뭘까?

이전 글에서는 에이전트 1개가 모든 작업을 처리했습니다.

프로필 읽기 → 업무 경험 읽기 → 목표 직무 읽기 → 이력서 초안 작성 → 결과 저장

작은 예제에서는 이 방식도 충분합니다.

하지만 실제로 이력서와 포트폴리오를 제대로 만들려면 작업이 더 복잡해집니다.

  • 업무 경험을 분석해야 한다.
  • 목표 직무와 연결되는 경험을 찾아야 한다.
  • 이력서 문장으로 바꿔야 한다.
  • 포트폴리오 설명으로도 바꿔야 한다.
  • 과장된 표현이나 부족한 근거를 검토해야 한다.

이 모든 일을 하나의 에이전트에게 맡기면 프롬프트가 길어지고, 역할이 흐려지고, 결과도 흔들릴 수 있습니다.

그래서 이번 글에서는 하나의 큰 에이전트를 여러 개의 작은 에이전트로 나누어보겠습니다.

멀티 에이전트란?

LangChain 공식 문서에서는 subagents 구조를 설명하면서, 중앙의 메인 에이전트가 여러 하위 에이전트를 도구처럼 호출한다고 설명합니다.

이 중앙 에이전트를 보통 Supervisor라고 부릅니다.

간단하게 정리하면 아래와 같습니다.

SupervisorAgent
├─ ExperienceAnalyzerAgent
├─ JobMatcherAgent
├─ ResumeWriterAgent
└─ ResumeReviewAgent

각 에이전트는 자신이 맡은 역할만 수행합니다. 이렇게 역할을 나누면 좋은 점이 있습니다.

멀티 에이전트로 역할 분리의 장점

  • 프롬프트가 역할별로 짧아진다.
  • 각 에이전트의 책임이 명확해진다.
  • 문제가 생겼을 때 어느 단계가 문제인지 찾기 쉽다.
  • 나중에 확장하기 좋다.

멀티 에이전트 만들기

1. 예시 데이터 작성

data/profile.md

# 기본 프로필

- 이름: 홍길동
- 목표: AI Agent Engineer로 성장하기
- 관심 분야: LLM 애플리케이션, LangChain, LangGraph, RAG, 업무 자동화

 

data/work_history.md

# 업무 경험

## AI 교육 플랫폼 프로젝트

- 역할: 백엔드 개발자
- 기간: 2024.03 ~ 2024.12
- 사용 기술: Python, FastAPI, PostgreSQL, LangChain
- 주요 업무:
  - 수강생 질문 데이터를 기반으로 AI 튜터 응답 기능 개발
  - 강의별 Q&A 검색 API 구현
  - 관리자용 학습 리포트 API 개발
- 성과:
  - 반복 질문 대응 시간을 줄이기 위한 자동 응답 흐름 설계
  - 프롬프트 템플릿 관리 구조 개선

## 사내 운영 자동화 프로젝트

- 역할: 백엔드 개발자
- 기간: 2025.01 ~ 2025.04
- 사용 기술: Python, FastAPI, PostgreSQL, Docker
- 주요 업무:
  - 반복 운영 업무를 처리하는 내부 API 개발
  - 관리자 입력 데이터를 검증하는 자동화 로직 구현
  - 작업 실패 로그를 조회하는 관리 기능 개발
- 성과:
  - 수동으로 확인하던 운영 데이터를 API 기반으로 조회할 수 있게 개선

 

data/target_role.md

# 목표 직무

AI Agent Engineer

## 요구사항

- Python 기반 LLM 애플리케이션 개발 경험
- LangChain 또는 LangGraph 사용 경험
- Tool Calling, Agent Workflow 이해
- 백엔드 API 개발 경험
- RAG 또는 검색 기반 응답 시스템 경험
- 문제 해결 과정을 문서화할 수 있는 능력

2. 전체 코드

from __future__ import annotations

import os
from pathlib import Path

from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI


ROOT_DIR = Path(__file__).parent
DATA_DIR = ROOT_DIR / "data"
OUTPUT_DIR = ROOT_DIR / "outputs"


def read_markdown(filename: str) -> str:
    """
    data 폴더 안의 markdown 파일을 읽습니다.
    """
    path = DATA_DIR / filename

    if not path.exists():
        raise FileNotFoundError(f"파일을 찾을 수 없습니다: {path}")

    return path.read_text(encoding="utf-8")


def last_message_text(result: dict) -> str:
    """
    LangChain agent 실행 결과에서 마지막 메시지 내용을 문자열로 꺼냅니다.
    """
    message = result["messages"][-1]
    content = getattr(message, "content", "")

    if isinstance(content, str):
        return content

    return str(content)


@tool
def read_profile() -> str:
    """
    사용자의 기본 프로필을 읽습니다.
    """
    return read_markdown("profile.md")


@tool
def read_work_history() -> str:
    """
    사용자의 업무 경험을 읽습니다.
    """
    return read_markdown("work_history.md")


@tool
def read_target_role() -> str:
    """
    목표 직무 또는 채용공고 내용을 읽습니다.
    """
    return read_markdown("target_role.md")


@tool
def save_resume_draft(filename: str, content: str) -> str:
    """
    최종 이력서 초안을 outputs 폴더에 markdown 파일로 저장합니다.
    """
    OUTPUT_DIR.mkdir(exist_ok=True)

    safe_filename = filename.replace("/", "_").replace("\\", "_")
    if not safe_filename.endswith(".md"):
        safe_filename = f"{safe_filename}.md"

    path = OUTPUT_DIR / safe_filename
    path.write_text(content, encoding="utf-8")

    return f"저장 완료: {path}"


EXPERIENCE_ANALYZER_PROMPT = """
너는 ExperienceAnalyzerAgent다.

역할:
- 사용자의 업무 경험을 분석한다.
- 프로젝트별 역할, 사용 기술, 해결한 문제, 결과를 구조화한다.
- 이력서에 바로 쓰기보다 먼저 재료를 정리한다.

주의:
- 주어진 정보에 없는 수치나 회사명은 만들지 않는다.
- 성과가 모호하면 '근거 보완 필요'라고 표시한다.

출력 형식:

# 업무 경험 분석

## 프로젝트별 요약

## 사용 기술

## 문제 해결 경험

## 성과 후보

## 근거 보완 필요 항목
"""


JOB_MATCHER_PROMPT = """
너는 JobMatcherAgent다.

역할:
- 목표 직무 요구사항과 업무 경험 분석 결과를 비교한다.
- 어떤 경험이 어떤 요구사항과 연결되는지 정리한다.
- 부족한 요구사항도 함께 표시한다.

출력 형식:

# 직무 매칭 분석

## 강하게 연결되는 경험

## 부분적으로 연결되는 경험

## 부족한 경험 또는 추가 확인 필요

## 이력서에서 강조할 키워드
"""


RESUME_WRITER_PROMPT = """
너는 ResumeWriterAgent다.

역할:
- 업무 경험 분석과 직무 매칭 결과를 바탕으로 이력서 초안을 작성한다.
- 문장은 간결하고 성과 중심으로 작성한다.
- 과장하지 않고, 주어진 근거 안에서만 작성한다.

출력 형식:

# 이력서 초안

## 한 줄 소개

## 핵심 역량

## 경력 요약

## 프로젝트 경험

## 포트폴리오에 연결할 포인트
"""


RESUME_REVIEWER_PROMPT = """
너는 ResumeReviewAgent다.

역할:
- 이력서 초안을 검토한다.
- 과장된 표현, 근거가 부족한 표현, 더 구체화해야 할 질문을 찾는다.
- 최종 제출 전 보완 방향을 제안한다.

출력 형식:

# 이력서 리뷰

## 좋은 점

## 과장 가능성이 있는 표현

## 근거 보완이 필요한 부분

## 추가 질문

## 수정 제안
"""


SUPERVISOR_PROMPT = """
너는 SupervisorAgent다.

목표:
- 여러 전문 에이전트를 순서대로 호출해서 목표 직무에 맞는 이력서 초안을 만든다.
- 필요한 입력 데이터는 도구로 읽는다.
- 하위 에이전트의 결과를 종합해서 최종 초안을 저장한다.

작업 순서:
1. read_profile 도구로 기본 프로필을 읽는다.
2. read_work_history 도구로 업무 경험을 읽는다.
3. read_target_role 도구로 목표 직무를 읽는다.
4. analyze_experience 도구로 업무 경험을 분석한다.
5. match_job_requirements 도구로 목표 직무와 경험을 매칭한다.
6. write_resume 도구로 이력서 초안을 작성한다.
7. review_resume 도구로 초안을 검토한다.
8. 최종 결과를 markdown으로 정리한다.
9. save_resume_draft 도구로 결과를 저장한다.

최종 결과에는 아래 내용을 포함한다.
- 업무 경험 분석 요약
- 직무 매칭 요약
- 이력서 초안
- 리뷰 결과
- 추가로 확인할 질문
"""


def create_model() -> ChatOpenAI:
    """
    모든 에이전트가 함께 사용할 언어 모델을 생성합니다.
    """
    model_name = os.getenv("OPENAI_MODEL", "gpt-5.5")

    return ChatOpenAI(
        model=model_name,
        temperature=0.2,
    )


def create_sub_agents(model: ChatOpenAI) -> dict:
    """
    역할별 하위 에이전트를 생성합니다.

    이번 예제의 하위 에이전트들은 별도의 tool 없이
    각자의 system prompt에 맞춰 텍스트를 분석하고 작성합니다.
    """
    experience_agent = create_agent(
        model=model,
        tools=[],
        system_prompt=EXPERIENCE_ANALYZER_PROMPT,
        name="experience_analyzer_agent",
    )

    job_matcher_agent = create_agent(
        model=model,
        tools=[],
        system_prompt=JOB_MATCHER_PROMPT,
        name="job_matcher_agent",
    )

    resume_writer_agent = create_agent(
        model=model,
        tools=[],
        system_prompt=RESUME_WRITER_PROMPT,
        name="resume_writer_agent",
    )

    resume_reviewer_agent = create_agent(
        model=model,
        tools=[],
        system_prompt=RESUME_REVIEWER_PROMPT,
        name="resume_reviewer_agent",
    )

    return {
        "experience": experience_agent,
        "job_matcher": job_matcher_agent,
        "resume_writer": resume_writer_agent,
        "resume_reviewer": resume_reviewer_agent,
    }


def create_supervisor_agent(model: ChatOpenAI, sub_agents: dict):
    """
    하위 에이전트를 tool처럼 호출할 수 있는 SupervisorAgent를 생성합니다.
    """

    @tool
    def analyze_experience(work_history: str) -> str:
        """
        ExperienceAnalyzerAgent를 호출해서 업무 경험을 분석합니다.
        """
        result = sub_agents["experience"].invoke(
            {
                "messages": [
                    {
                        "role": "user",
                        "content": work_history,
                    }
                ]
            }
        )
        return last_message_text(result)

    @tool
    def match_job_requirements(experience_analysis: str, target_role: str) -> str:
        """
        JobMatcherAgent를 호출해서 업무 경험과 목표 직무를 매칭합니다.
        """
        result = sub_agents["job_matcher"].invoke(
            {
                "messages": [
                    {
                        "role": "user",
                        "content": f"""
[업무 경험 분석]
{experience_analysis}

[목표 직무]
{target_role}
""".strip(),
                    }
                ]
            }
        )
        return last_message_text(result)

    @tool
    def write_resume(profile: str, experience_analysis: str, job_match_report: str) -> str:
        """
        ResumeWriterAgent를 호출해서 이력서 초안을 작성합니다.
        """
        result = sub_agents["resume_writer"].invoke(
            {
                "messages": [
                    {
                        "role": "user",
                        "content": f"""
[기본 프로필]
{profile}

[업무 경험 분석]
{experience_analysis}

[직무 매칭 분석]
{job_match_report}
""".strip(),
                    }
                ]
            }
        )
        return last_message_text(result)

    @tool
    def review_resume(resume_draft: str, experience_analysis: str, job_match_report: str) -> str:
        """
        ResumeReviewAgent를 호출해서 이력서 초안을 검토합니다.
        """
        result = sub_agents["resume_reviewer"].invoke(
            {
                "messages": [
                    {
                        "role": "user",
                        "content": f"""
[이력서 초안]
{resume_draft}

[업무 경험 분석]
{experience_analysis}

[직무 매칭 분석]
{job_match_report}
""".strip(),
                    }
                ]
            }
        )
        return last_message_text(result)

    supervisor_agent = create_agent(
        model=model,
        tools=[
            read_profile,
            read_work_history,
            read_target_role,
            analyze_experience,
            match_job_requirements,
            write_resume,
            review_resume,
            save_resume_draft,
        ],
        system_prompt=SUPERVISOR_PROMPT,
        name="supervisor_agent",
    )

    return supervisor_agent


def main() -> None:
    load_dotenv()

    model = create_model()
    sub_agents = create_sub_agents(model)
    supervisor_agent = create_supervisor_agent(model, sub_agents)

    result = supervisor_agent.invoke(
        {
            "messages": [
                {
                    "role": "user",
                    "content": """
목표 직무에 맞는 이력서 초안을 멀티 에이전트 방식으로 작성해줘.

최종 결과는 multi_agent_resume_draft.md 파일로 저장해줘.
""".strip(),
                }
            ]
        }
    )

    print(last_message_text(result))


if __name__ == "__main__":
    main()

 

3. 코드 설명

3.1 에이전트 프롬프트 분리

EXPERIENCE_ANALYZER_PROMPT = """
너는 ExperienceAnalyzerAgent다.
...
"""

 

단일 에이전트에서는 하나의 프롬프트 안에 모든 역할을 넣었습니다.

멀티 에이전트에서는 역할마다 프롬프트를 나눕니다.

ExperienceAnalyzerAgent = 업무 경험 분석
JobMatcherAgent = 직무 요구사항 매칭
ResumeWriterAgent = 이력서 초안 작성
ResumeReviewAgent = 결과 검토

3.2 서브 에이전트 만들기

experience_agent = create_agent(
    model=model,
    tools=[],
    system_prompt=EXPERIENCE_ANALYZER_PROMPT,
    name="experience_analyzer_agent",
)

이번 예제에서는 하위 에이전트가 외부 파일을 직접 읽지 않습니다.

대신 SupervisorAgent가 필요한 데이터를 읽고, 하위 에이전트에게 필요한 내용만 전달합니다.

이 구조는 역할을 깔끔하게 나누는 데 도움이 됩니다.

3.3 서브 에이전트를 도구처럼 감싸기

LangChain의 subagents 패턴에서는 하위 에이전트를 도구처럼 감싸서 메인 에이전트가 호출할 수 있게 만들 수 있습니다.

이번 코드에서는 아래 함수들이 그 역할을 합니다.

@tool
def analyze_experience(work_history: str) -> str:

analyze_experience 도구가 호출되면 내부에서 ExperienceAnalyzerAgent가 실행됩니다.

result = sub_agents["experience"].invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": work_history,
            }
        ]
    }
)

 

즉, Supervisor 입장에서는 analyze_experience라는 도구를 호출하는 것처럼 보입니다.

하지만 실제로는 그 안에서 다른 에이전트가 실행됩니다.

SupervisorAgent
→ analyze_experience tool 호출
→ ExperienceAnalyzerAgent 실행
→ 분석 결과 반환

3.4 SupervisorAgent 만들기

supervisor_agent = create_agent(
    model=model,
    tools=[
        read_profile,
        read_work_history,
        read_target_role,
        analyze_experience,
        match_job_requirements,
        write_resume,
        review_resume,
        save_resume_draft,
    ],
    system_prompt=SUPERVISOR_PROMPT,
    name="supervisor_agent",
)

SupervisorAgent는 전체 흐름을 조율합니다.

이 에이전트가 사용할 수 있는 도구는 두 종류입니다.

첫 번째는 파일을 읽고 저장하는 도구입니다.

  • read_profile
  • read_work_history
  • read_target_role
  • save_resume_draft

두 번째는 하위 에이전트를 호출하는 도구입니다.

  • analyze_experience
  • match_job_requirements
  • write_resume
  • review_resume

이렇게 하면 Supervisor는 전체 작업을 관리하고, 실제 세부 작업은 전문 에이전트에게 넘길 수 있습니다.

3.5 last_messages_text 함수

def last_message_text(result: dict) -> str:

LangChain 에이전트를 실행하면 결과가 messages 형태로 반환됩니다.

최종 응답만 꺼내기 위해 last_message_text 함수를 만들었습니다.

message = result["messages"][-1]
content = getattr(message, "content", "")

이렇게 해두면 하위 에이전트를 호출할 때마다 같은 방식으로 결과를 꺼낼 수 있습니다.

4. 실행 흐름과 결과

실행 흐름

1. 사용자가 SupervisorAgent에게 요청한다.
2. SupervisorAgent가 profile.md를 읽는다.
3. SupervisorAgent가 work_history.md를 읽는다.
4. SupervisorAgent가 target_role.md를 읽는다.
5. ExperienceAnalyzerAgent가 업무 경험을 분석한다.
6. JobMatcherAgent가 목표 직무와 업무 경험을 매칭한다.
7. ResumeWriterAgent가 이력서 초안을 작성한다.
8. ResumeReviewAgent가 초안을 검토한다.
9. SupervisorAgent가 최종 결과를 저장한다.

 

결과 파일

# 멀티 에이전트 이력서 작성 결과

## 업무 경험 분석 요약

## 직무 매칭 요약

## 이력서 초안

## 리뷰 결과

## 추가로 확인할 질문

6. 싱글 에이전트 / 멀티 에이전트 비교

구분 싱글 에이전트 멀티 에이전트
구조 하나의 에이전트 전담 역할별 에이전트가 나누어 처리
프롬프트 하나의 프롬프트가 길어짐 역할별로 짧게 유지 가능
디버깅 어떤 단계가 문제인지 찾기 어려움 문제가 난 에이전트를 분리해서 확인 가능
확장성 기능이 늘어날 수록 복잡 확장성 좋음
타겟 작은 작업, 단순 자동화 여러 전문 역할이 필요한 작업

멀티 에이전트 사용 시 참고할 점

  1. 에이전트를 너무 많이 나누면 오히려 복잡해질 수 있습니다.
  2. Supervisor의 프롬프트가 중요합니다.
  3. 하위 에이전트의 출력 형식을 고정하는 것이 좋습니다.

정리

이번 글에서는 단일 ResumeWriterAgent를 여러 전문 에이전트로 나누어봤습니다.

핵심은 아래와 같습니다.

  • 멀티 에이전트는 역할을 나누기 위한 구조다.
  • SupervisorAgent는 전체 흐름을 조율한다.
  • 하위 에이전트는 하나의 전문 역할에 집중한다.
  • LangChain에서는 하위 에이전트를 tool처럼 감싸서 Supervisor가 호출하게 만들 수 있다.

다음에는 멀티 에이전트 구조를 파이프라인을 형태로 정리해 보겠습니다.

반응형

'AI' 카테고리의 다른 글

[AI] LangGraph 써보기  (0) 2026.05.27
[AI] 멀티 에이전트 파이프라인 with LangChain  (0) 2026.05.26
[AI] 에이전트를 만들어보자!  (0) 2026.05.17
[NEWS] Marrrang Dev News - 26.04.24  (0) 2026.04.24
[AI] Gemma 사용해보기  (1) 2024.03.16