멀티 에이전트가 뭘까?
이전 글에서는 에이전트 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. 싱글 에이전트 / 멀티 에이전트 비교
| 구분 | 싱글 에이전트 | 멀티 에이전트 |
| 구조 | 하나의 에이전트 전담 | 역할별 에이전트가 나누어 처리 |
| 프롬프트 | 하나의 프롬프트가 길어짐 | 역할별로 짧게 유지 가능 |
| 디버깅 | 어떤 단계가 문제인지 찾기 어려움 | 문제가 난 에이전트를 분리해서 확인 가능 |
| 확장성 | 기능이 늘어날 수록 복잡 | 확장성 좋음 |
| 타겟 | 작은 작업, 단순 자동화 | 여러 전문 역할이 필요한 작업 |
멀티 에이전트 사용 시 참고할 점
- 에이전트를 너무 많이 나누면 오히려 복잡해질 수 있습니다.
- Supervisor의 프롬프트가 중요합니다.
- 하위 에이전트의 출력 형식을 고정하는 것이 좋습니다.
정리
이번 글에서는 단일 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 |