이미지 로딩 중...
AI Generated
2025. 11. 8. · 2 Views
AI 에이전트 완벽 가이드 - 개념과 종류
AI 에이전트의 핵심 개념부터 다양한 종류까지 실무 관점에서 상세히 알아봅니다. Reflexion Agent, ReAct Agent, Tool-using Agent 등 실제 구현 가능한 예제와 함께 AI 에이전트의 세계를 탐험해보세요.
목차
- AI 에이전트의 기본 개념
- ReAct 에이전트 - 추론과 행동의 결합
- Reflexion 에이전트 - 자기 반성과 개선
- Tool-using 에이전트 - 외부 도구 활용
- Plan-and-Execute 에이전트 - 계획 기반 실행
- Multi-Agent 시스템 - 협업하는 에이전트들
- Memory-augmented 에이전트 - 장기 기억 활용
- Hierarchical 에이전트 - 계층적 작업 분해
- Autonomous 에이전트 - 완전 자율 실행
1. AI 에이전트의 기본 개념
시작하며
여러분이 ChatGPT에게 복잡한 작업을 맡겼을 때, "이건 한 번에 답하기 어려운데..." 하고 생각해본 적 있나요? 예를 들어, "최근 3년간 우리 회사 매출 데이터를 분석하고, 경쟁사와 비교한 후, 내년 전략을 제안해줘" 같은 요청 말이죠.
이런 복잡한 작업은 단순히 텍스트를 생성하는 것을 넘어서, 계획을 세우고, 데이터를 찾고, 분석하고, 결론을 내리는 '사고 과정'이 필요합니다. 전통적인 LLM은 이런 다단계 작업에서 한계를 보입니다.
바로 이럴 때 필요한 것이 AI 에이전트입니다. AI 에이전트는 단순히 답변을 생성하는 것이 아니라, 스스로 계획을 세우고, 도구를 사용하며, 피드백을 받아 개선하는 '자율적인 시스템'입니다.
마치 실제 직원처럼 작업을 완수할 수 있는 것이죠.
개요
간단히 말해서, AI 에이전트는 LLM(Large Language Model)을 두뇌로 사용하면서, 외부 도구를 활용하고 자율적으로 작업을 수행하는 시스템입니다. 왜 이것이 필요할까요?
실무에서는 단순한 질문-답변을 넘어서는 작업이 대부분입니다. 예를 들어, 고객 문의를 받아 데이터베이스를 조회하고, 관련 문서를 찾아 종합적인 답변을 작성하고, 필요시 다른 부서에 티켓을 생성하는 작업 같은 경우에 AI 에이전트가 매우 유용합니다.
기존에는 이런 작업을 위해 복잡한 if-else 로직과 여러 API를 하드코딩으로 연결했다면, 이제는 AI 에이전트가 상황을 이해하고 스스로 적절한 도구를 선택해 작업을 수행할 수 있습니다. AI 에이전트의 핵심 특징은 세 가지입니다: 첫째, 자율성(Autonomy) - 최소한의 개입으로 작업을 완수합니다.
둘째, 도구 사용(Tool Use) - API, 데이터베이스, 외부 서비스 등을 활용합니다. 셋째, 반성과 개선(Reflection) - 자신의 결과를 평가하고 개선합니다.
이러한 특징들이 AI 에이전트를 단순한 챗봇과 구별짓는 핵심 요소입니다.
코드 예제
# 기본적인 AI 에이전트 구조
class SimpleAgent:
def __init__(self, llm, tools):
self.llm = llm # LLM을 두뇌로 사용
self.tools = tools # 사용 가능한 도구들
self.memory = [] # 작업 기록
def run(self, task):
# 1. 작업을 이해하고 계획 수립
plan = self.llm.generate(f"다음 작업의 계획을 세워줘: {task}")
# 2. 계획에 따라 도구 사용
for step in plan.steps:
tool = self.select_tool(step) # 적절한 도구 선택
result = tool.execute(step) # 도구 실행
self.memory.append((step, result)) # 결과 기록
# 3. 최종 답변 생성
return self.llm.generate(f"기억: {self.memory}, 최종 답변 작성")
설명
이것이 하는 일: 이 코드는 AI 에이전트의 가장 기본적인 구조를 보여줍니다. LLM을 중심으로 도구를 활용하고, 작업 기록을 관리하며, 최종 목표를 달성하는 전체 프로세스를 담고 있습니다.
첫 번째로, __init__ 메서드에서 에이전트의 핵심 구성요소를 초기화합니다. llm은 GPT-4나 Claude 같은 언어 모델이고, tools는 계산기, 웹 검색, 데이터베이스 조회 같은 외부 도구들입니다.
memory는 작업 과정을 기록해 나중에 참조하거나 학습하는 데 사용됩니다. 이렇게 세 가지 요소를 분리함으로써 각각을 독립적으로 교체하거나 확장할 수 있습니다.
두 번째로, run 메서드의 첫 부분에서 LLM에게 작업을 분석하고 계획을 세우도록 요청합니다. 이는 단순히 즉각적으로 답하는 것이 아니라, "이 작업을 완수하려면 어떤 단계가 필요한가?"를 먼저 생각하는 과정입니다.
예를 들어, "이번 달 매출 보고서 작성"이라는 작업이 주어지면, "1. 매출 데이터 조회, 2.
전월 대비 분석, 3. 차트 생성, 4.
보고서 작성" 같은 계획을 수립합니다. 세 번째로, 계획의 각 단계를 순회하며 적절한 도구를 선택하고 실행합니다.
select_tool 메서드는 현재 단계에 가장 적합한 도구를 찾아줍니다. 각 도구의 실행 결과는 memory에 기록되어, 다음 단계나 최종 답변 생성 시 참조됩니다.
이는 마치 사람이 메모를 하며 작업하는 것과 유사합니다. 마지막으로, 모든 단계가 완료되면 LLM이 기록된 메모리를 바탕으로 최종 답변을 생성합니다.
이때 단순히 마지막 결과만 보는 것이 아니라, 전체 작업 과정을 종합해 일관된 답변을 만들어냅니다. 여러분이 이 코드를 사용하면 복잡한 다단계 작업을 자동화할 수 있습니다.
단순한 API 호출이나 스크립트와 달리, 상황에 맞게 유연하게 대응하며, 예상치 못한 문제가 발생해도 LLM의 추론 능력으로 해결책을 찾을 수 있습니다. 또한 메모리 기능으로 작업 과정을 추적하고 디버깅할 수 있으며, 나중에 유사한 작업을 더 효율적으로 처리할 수 있습니다.
실전 팁
💡 LLM 선택이 성능을 좌우합니다. GPT-4나 Claude 3.5 Sonnet 같은 고성능 모델을 사용하면 더 정교한 계획과 도구 선택이 가능합니다. 하지만 간단한 작업에는 GPT-3.5나 Llama 같은 가벼운 모델도 충분합니다.
💡 무한 루프를 방지하세요. 에이전트가 같은 도구를 반복 실행하거나 계획을 끝없이 수정하는 경우가 있습니다. 최대 반복 횟수나 타임아웃을 설정해 비용과 시간을 제어하세요.
💡 도구 설명이 명확해야 합니다. LLM이 도구를 선택할 때 각 도구의 기능, 입력/출력 형식, 사용 시점을 정확히 알아야 합니다. 도구마다 상세한 docstring이나 schema를 제공하세요.
💡 메모리 관리가 중요합니다. 모든 작업 내용을 저장하면 컨텍스트 길이가 초과될 수 있습니다. 중요한 정보만 요약해서 저장하거나, 벡터 DB를 활용해 필요한 정보만 검색하세요.
💡 에러 핸들링을 구현하세요. 도구 실행이 실패하거나 예상치 못한 결과가 나올 때, 에이전트가 스스로 재시도하거나 대안을 찾도록 설계하면 안정성이 크게 향상됩니다.
2. ReAct 에이전트 - 추론과 행동의 결합
시작하며
여러분이 복잡한 문제를 해결할 때 어떻게 하시나요? 아마도 "먼저 이걸 확인하고, 그 결과를 보고 다음 단계를 결정하고..." 이런 식으로 생각과 행동을 번갈아가며 진행하실 겁니다.
그런데 전통적인 AI 시스템은 모든 것을 한 번에 계획하거나, 생각 없이 행동만 합니다. 이런 문제는 특히 동적인 환경에서 심각합니다.
예를 들어, 실시간으로 변하는 주식 시장 데이터를 분석하거나, 사용자 피드백에 따라 전략을 조정해야 하는 경우, 사전에 모든 것을 계획하는 것은 불가능합니다. 결과를 보고 다음 단계를 결정해야 하죠.
바로 이럴 때 필요한 것이 ReAct(Reasoning + Acting) 에이전트입니다. ReAct는 "생각(Thought) → 행동(Action) → 관찰(Observation)" 사이클을 반복하며, 마치 사람처럼 단계적으로 문제를 해결합니다.
각 단계에서 얻은 정보를 바탕으로 다음 행동을 결정하기 때문에, 예측 불가능한 상황에서도 유연하게 대응할 수 있습니다.
개요
간단히 말해서, ReAct 에이전트는 사고(Reasoning)와 행동(Acting)을 명시적으로 분리하고 반복하는 AI 에이전트 패턴입니다. 각 단계에서 "지금 뭘 해야 할까?"를 생각하고, 행동하고, 결과를 관찰한 후 다시 생각하는 과정을 거칩니다.
왜 이것이 필요할까요? 실무에서는 불확실성이 가득합니다.
API가 예상과 다른 데이터를 반환하거나, 데이터베이스에 원하는 정보가 없거나, 중간 결과가 예상과 달라 전략을 바꿔야 할 수도 있습니다. 예를 들어, "고객의 최근 구매 이력을 바탕으로 추천 상품을 제안하라"는 작업에서, 만약 구매 이력이 없다면?
ReAct 에이전트는 이를 관찰하고 "대신 유사 고객의 데이터를 찾아보자"라고 생각을 전환할 수 있습니다. 기존에는 모든 가능한 경우의 수를 미리 프로그래밍했다면, 이제는 LLM이 상황을 보고 스스로 판단합니다.
이는 코드 복잡도를 크게 줄이고, 예상치 못한 상황에도 대응할 수 있게 만듭니다. ReAct의 핵심 특징은 명시적인 사고 과정과 단계별 피드백입니다.
각 단계에서 "왜 이 행동을 하는지"를 명확히 기록하므로 디버깅과 설명이 쉽고, 관찰 결과를 즉시 다음 사고에 반영하므로 적응력이 뛰어납니다. 이러한 특징들이 ReAct를 현재 가장 널리 사용되는 에이전트 패턴으로 만들었습니다.
코드 예제
# ReAct 패턴 구현
class ReActAgent:
def __init__(self, llm, tools):
self.llm = llm
self.tools = tools
def run(self, question, max_steps=5):
context = f"질문: {question}\n\n"
for step in range(max_steps):
# Thought: 현재 상황을 분석하고 다음 행동 결정
thought = self.llm.generate(context + "Thought:")
context += f"Thought: {thought}\n"
# Action: 도구 선택 및 실행
action = self.llm.generate(context + "Action:")
context += f"Action: {action}\n"
# Observation: 도구 실행 결과 관찰
observation = self.execute_tool(action)
context += f"Observation: {observation}\n\n"
# 답변을 찾았는지 확인
if self.is_final_answer(thought):
return self.llm.generate(context + "Final Answer:")
return "최대 단계에 도달했습니다."
설명
이것이 하는 일: 이 코드는 ReAct 패턴의 핵심인 "생각 → 행동 → 관찰" 루프를 구현합니다. 각 단계에서 컨텍스트를 축적하며, LLM이 이전 모든 정보를 바탕으로 다음 행동을 결정하도록 합니다.
첫 번째로, run 메서드가 시작되면 질문을 컨텍스트에 저장하고 반복 루프를 시작합니다. max_steps는 무한 루프를 방지하는 안전장치입니다.
실무에서는 5~10 정도가 적당하며, 너무 크면 비용이 증가하고 너무 작으면 복잡한 작업을 완수하지 못할 수 있습니다. 두 번째로, Thought 단계에서 LLM에게 현재까지의 컨텍스트를 제공하고 "지금 무엇을 해야 할까?"를 물어봅니다.
이때 LLM은 질문, 이전 사고들, 그리고 관찰 결과들을 모두 고려해 답변합니다. 예를 들어, "사용자의 구매 이력을 확인했지만 비어있으므로, 이제 유사 사용자를 찾아야 한다"라는 식의 추론을 합니다.
이 사고 과정을 명시적으로 기록하는 것이 ReAct의 핵심입니다. 세 번째로, Action 단계에서는 방금 생각한 내용을 바탕으로 실제로 실행할 도구와 파라미터를 결정합니다.
예를 들어, "search_database(user_id=similar_users, limit=10)" 같은 형식으로 구체적인 명령을 생성합니다. 그런 다음 execute_tool이 이를 파싱하고 실제 도구를 실행합니다.
네 번째로, Observation 단계에서는 도구 실행 결과를 받아 컨텍스트에 추가합니다. 이 관찰 결과가 다음 사이클의 Thought에 영향을 줍니다.
만약 예상과 다른 결과가 나왔다면, 다음 Thought에서 "예상과 달리 X가 나왔으므로, 이제 Y를 시도해야 한다"라는 식으로 전략을 수정할 수 있습니다. 마지막으로, 각 Thought를 분석해 최종 답변에 도달했는지 판단합니다.
is_final_answer는 LLM이 "충분한 정보를 수집했다"거나 "답변할 준비가 됐다"는 신호를 보냈는지 확인합니다. 준비가 됐다면 최종 답변을 생성하고, 그렇지 않으면 다음 사이클을 계속합니다.
여러분이 이 코드를 사용하면 복잡한 조사 작업, 데이터 분석, 문제 해결 등을 자동화할 수 있습니다. 특히 결과를 예측할 수 없는 작업에서 빛을 발합니다.
예를 들어, "최근 급증한 고객 불만의 원인을 찾아라"는 작업에서, 여러 데이터 소스를 탐색하고, 가설을 세우고, 검증하는 과정을 자동으로 수행할 수 있습니다. 또한 모든 사고 과정이 기록되므로, 왜 그런 결론에 도달했는지 추적하고 설명할 수 있어 신뢰성을 확보할 수 있습니다.
실전 팁
💡 프롬프트에 예시를 포함하세요. ReAct 패턴은 Few-shot learning과 결합될 때 가장 효과적입니다. "Thought: X를 확인해야 한다 → Action: search(X) → Observation: Y 발견" 같은 예시를 2-3개 제공하면 LLM이 패턴을 훨씬 잘 따릅니다.
💡 도구 실패를 Observation에 포함하세요. API 에러나 빈 결과도 중요한 정보입니다. "Observation: 에러 - 데이터베이스 연결 실패"라고 명시하면 LLM이 대안을 찾을 수 있습니다.
💡 컨텍스트 길이를 모니터링하세요. 매 사이클마다 컨텍스트가 늘어나므로 LLM의 토큰 한계에 빠르게 도달할 수 있습니다. 중요하지 않은 Observation은 요약하거나 제거하는 로직을 추가하세요.
💡 조기 종료 조건을 명확히 하세요. "Final Answer:"라는 키워드를 사용하거나, 특정 신뢰도 임계값을 설정해 불필요한 반복을 줄이세요. 이는 비용 절감과 응답 속도 향상에 직결됩니다.
💡 각 단계의 타임아웃을 설정하세요. 특정 도구가 응답하지 않으면 전체 프로세스가 멈출 수 있습니다. 각 도구 실행에 타임아웃을 두고, 초과 시 "Observation: 타임아웃"을 반환하도록 하세요.
3. Reflexion 에이전트 - 자기 반성과 개선
시작하며
여러분이 코드를 작성하고 테스트를 돌렸는데 실패했을 때, 어떻게 하시나요? 그냥 포기하거나 같은 시도를 반복하지는 않으실 겁니다.
대신 "왜 실패했지? 어디가 잘못됐지?"라고 분석하고, 개선된 방법을 시도하죠.
이것이 바로 학습과 성장의 핵심입니다. 그런데 대부분의 AI 에이전트는 실패하면 그냥 끝입니다.
같은 실수를 반복하거나, 약간만 바꿔서 재시도할 뿐, 근본적인 문제를 분석하고 개선하지 못합니다. 이는 특히 코드 생성, 복잡한 추론, 창의적 문제 해결 같은 작업에서 심각한 한계로 작용합니다.
바로 이럴 때 필요한 것이 Reflexion 에이전트입니다. Reflexion은 자신의 실패를 분석하고, 무엇이 잘못됐는지 반성(Reflection)하며, 그 교훈을 다음 시도에 반영하는 자기 개선 사이클을 구현합니다.
이는 AI 에이전트에게 '학습 능력'을 부여하는 혁신적인 접근법입니다.
개요
간단히 말해서, Reflexion 에이전트는 "실행 → 평가 → 반성 → 개선" 사이클을 반복하며 스스로 성능을 향상시키는 에이전트입니다. 실패는 끝이 아니라 학습의 기회가 됩니다.
왜 이것이 필요할까요? 많은 작업은 한 번에 완벽하게 해결되지 않습니다.
특히 코드 생성, 복잡한 수학 문제, 전략 수립 같은 작업은 여러 번의 시도와 개선이 필요합니다. 예를 들어, "특정 알고리즘을 구현하라"는 작업에서 첫 시도가 틀렸다면, 단순히 다시 생성하는 것보다 "왜 틀렸는지" 분석하고 그 인사이트를 반영하는 것이 훨씬 효과적입니다.
기존 에이전트는 피드백을 받아도 표면적으로만 수정했다면, Reflexion은 실패의 근본 원인을 언어로 명시화합니다. "테스트 케이스 3이 실패했다"가 아니라 "엣지 케이스에서 인덱스 범위를 체크하지 않아서 실패했다"라고 구체적으로 분석합니다.
Reflexion의 핵심 특징은 세 가지입니다: 첫째, 명시적 반성(Explicit Reflection) - 실패 원인을 자연어로 기록합니다. 둘째, 반복적 개선(Iterative Refinement) - 반성 내용을 다음 시도에 직접 활용합니다.
셋째, 장기 기억(Long-term Memory) - 여러 작업에 걸쳐 교훈을 축적합니다. 이러한 특징들이 Reflexion을 단순한 재시도 메커니즘이 아닌, 진정한 학습 시스템으로 만듭니다.
코드 예제
# Reflexion 패턴 구현
class ReflexionAgent:
def __init__(self, llm, evaluator):
self.llm = llm
self.evaluator = evaluator # 결과를 평가하는 함수/모델
self.reflections = [] # 과거 반성 기록
def run(self, task, max_iterations=3):
for iteration in range(max_iterations):
# 1. 과거 반성 내용을 참고해 작업 수행
context = self.build_context(task)
result = self.llm.generate(context)
# 2. 결과 평가
evaluation = self.evaluator.evaluate(result, task)
# 3. 성공하면 반환
if evaluation.success:
return result
# 4. 실패 시 반성 수행
reflection = self.llm.generate(
f"작업: {task}\n결과: {result}\n"
f"평가: {evaluation.feedback}\n"
f"무엇이 잘못됐고, 어떻게 개선해야 할까?"
)
self.reflections.append(reflection)
return f"최대 시도 후에도 실패. 반성 내용: {self.reflections}"
설명
이것이 하는 일: 이 코드는 Reflexion의 핵심 메커니즘인 자기 반성 루프를 구현합니다. 단순히 재시도하는 것이 아니라, 왜 실패했는지 분석하고 그 교훈을 명시적으로 다음 시도에 반영합니다.
첫 번째로, run 메서드는 최대 반복 횟수만큼 시도합니다. 각 반복은 독립적인 시도가 아니라, 이전 실패로부터 학습한 내용을 포함합니다.
max_iterations는 보통 3~5 정도가 적당합니다. 연구에 따르면 대부분의 작업은 3번의 반성 사이클 내에 크게 개선되며, 그 이상은 수익 체감이 발생합니다.
두 번째로, build_context는 현재 작업과 함께 과거의 모든 반성 내용을 LLM에게 제공합니다. 이는 "이전에 X를 시도했지만 Y 때문에 실패했으므로, 이번에는 Z를 고려해야 한다"라는 맥락을 만들어줍니다.
예를 들어, 첫 시도에서 "빈 리스트를 처리하지 못했다"는 반성이 있었다면, 두 번째 시도에서는 자연스럽게 빈 리스트 처리 로직을 포함하게 됩니다. 세 번째로, evaluator가 결과를 객관적으로 평가합니다.
이는 단순한 성공/실패를 넘어 구체적인 피드백을 제공해야 합니다. 코드 생성 작업이라면 테스트 케이스 결과, 수학 문제라면 단계별 검증, 창의적 작업이라면 기준 충족 여부 등을 명시합니다.
평가가 구체적일수록 반성도 구체적이고 유용해집니다. 네 번째로, 평가가 실패를 나타내면 LLM에게 반성을 요청합니다.
이때 작업, 결과, 평가 피드백을 모두 제공해 "무엇을 시도했고, 어떻게 실패했으며, 왜 실패했는지"를 종합적으로 분석하도록 합니다. LLM은 이를 바탕으로 "X 때문에 실패했으므로 다음에는 Y를 고려해야 한다"는 형태의 교훈을 도출합니다.
이 반성 내용은 reflections 리스트에 추가되어 다음 시도에 활용됩니다. 마지막으로, 성공하면 즉시 결과를 반환하고, 모든 시도가 실패하면 축적된 반성 내용을 함께 반환합니다.
이는 디버깅에 매우 유용합니다. "왜 실패했는가?"에 대한 에이전트 자신의 분석을 볼 수 있기 때문입니다.
여러분이 이 코드를 사용하면 복잡하고 어려운 작업의 성공률을 크게 높일 수 있습니다. 실제 연구에 따르면 Reflexion은 코드 생성 작업에서 기존 방법 대비 20-30% 성능 향상을 보였습니다.
특히 첫 시도로는 해결하기 어려운 복잡한 문제에서 더 큰 효과를 발휘합니다. 또한 반성 내용을 데이터베이스에 저장하면 장기적으로 에이전트가 점점 더 똑똑해지는 '진화하는 시스템'을 만들 수 있습니다.
실전 팁
💡 평가자를 신중히 설계하세요. Reflexion의 품질은 평가자의 품질에 달려있습니다. 가능하면 자동화된 테스트, 명확한 메트릭, 또는 강력한 LLM을 평가자로 사용하세요. 모호한 평가는 모호한 반성을 낳습니다.
💡 반성 포맷을 구조화하세요. "1. 무엇이 잘못됐는가, 2. 왜 잘못됐는가, 3. 어떻게 개선할 것인가"처럼 명확한 구조를 제공하면 LLM이 더 체계적인 반성을 합니다.
💡 반성을 필터링하세요. 모든 반성이 유용한 것은 아닙니다. 반복되는 내용이나 너무 일반적인 조언은 제거하고, 구체적이고 실행 가능한 인사이트만 유지하세요.
💡 성공 사례도 기록하세요. 실패한 반성만이 아니라 성공했을 때 "무엇이 효과적이었는가"도 기록하면 더 빠른 학습이 가능합니다. 긍정적 패턴도 중요한 지식입니다.
💡 비용을 고려하세요. Reflexion은 여러 번의 LLM 호출을 수반하므로 비용이 높습니다. 간단한 작업에는 과도할 수 있으니, 복잡도에 따라 max_iterations를 동적으로 조정하세요.
4. Tool-using 에이전트 - 외부 도구 활용
시작하며
여러분이 복잡한 계산을 해야 할 때, 머릿속으로만 하시나요? 당연히 계산기를 사용하실 겁니다.
최신 뉴스를 알아야 할 때는 웹을 검색하고, 데이터를 저장해야 할 때는 데이터베이스를 사용합니다. 인간의 능력은 도구를 사용함으로써 무한히 확장됩니다.
그런데 전통적인 LLM은 텍스트 생성만 가능합니다. 실시간 정보에 접근할 수 없고, 정확한 계산을 못하며, 외부 시스템과 상호작용할 수 없습니다.
이는 실무에서 치명적인 한계입니다. 아무리 똑똑한 AI라도 "지금 주가는 얼마야?"라는 질문에 답할 수 없다면 쓸모가 제한적이죠.
바로 이럴 때 필요한 것이 Tool-using 에이전트입니다. 이는 LLM에게 "손과 발"을 제공하는 것과 같습니다.
계산기, 웹 검색, 데이터베이스 조회, API 호출 등 수십 가지 도구를 자유롭게 사용할 수 있게 하여, LLM의 능력을 현실 세계로 확장합니다.
개요
간단히 말해서, Tool-using 에이전트는 LLM이 상황에 맞게 적절한 외부 도구를 선택하고 사용할 수 있도록 하는 시스템입니다. LLM은 '언제, 어떤 도구를, 어떻게 사용할지'를 결정하는 두뇌 역할을 합니다.
왜 이것이 필요할까요? 실무 작업의 대부분은 정보 조회, 계산, 데이터 처리 등 도구 없이는 불가능한 것들입니다.
예를 들어, "이번 분기 매출을 분석하고 보고서를 작성하라"는 작업은 데이터베이스 조회, 엑셀 분석, 차트 생성, 문서 작성 등 여러 도구를 순차적으로 사용해야 합니다. Tool-using 에이전트는 이 모든 과정을 자동화할 수 있습니다.
기존에는 각 도구를 언제 사용할지 하드코딩했다면, 이제는 LLM이 문맥을 이해하고 동적으로 결정합니다. "날씨가 어때?"라는 질문에는 날씨 API를, "3252 곱하기 7849는?"에는 계산기를 자동으로 선택합니다.
Tool-using의 핵심 특징은 세 가지입니다: 첫째, 동적 도구 선택 - 수십 개의 도구 중에서 상황에 맞는 것을 고릅니다. 둘째, 파라미터 생성 - 도구에 필요한 입력값을 자동으로 추출하고 형식화합니다.
셋째, 결과 통합 - 도구의 출력을 이해하고 최종 답변에 통합합니다. 이러한 특징들이 Tool-using을 현대 AI 에이전트의 필수 요소로 만들었습니다.
코드 예제
# Function Calling 기반 Tool-using 에이전트
import json
class ToolUsingAgent:
def __init__(self, llm):
self.llm = llm
self.tools = {
"calculator": {"func": self.calculator, "desc": "수학 계산 수행"},
"search": {"func": self.search, "desc": "웹에서 정보 검색"},
"db_query": {"func": self.db_query, "desc": "데이터베이스 조회"}
}
def run(self, query):
# LLM에게 도구 목록과 함께 쿼리 전달
tool_descriptions = json.dumps({k: v["desc"] for k, v in self.tools.items()})
response = self.llm.generate(
f"도구: {tool_descriptions}\n질문: {query}\n어떤 도구를 사용할까?"
)
# LLM이 선택한 도구와 파라미터 파싱
tool_name = response.get("tool")
params = response.get("parameters")
# 도구 실행
result = self.tools[tool_name]["func"](**params)
# 결과를 바탕으로 최종 답변 생성
return self.llm.generate(f"질문: {query}\n도구 결과: {result}\n답변:")
설명
이것이 하는 일: 이 코드는 LLM이 여러 도구 중에서 적절한 것을 선택하고, 올바른 파라미터로 실행하며, 결과를 답변에 통합하는 전체 프로세스를 구현합니다. 첫 번째로, __init__에서 사용 가능한 도구들을 등록합니다.
각 도구는 실제 실행 함수와 설명을 포함합니다. 이 설명이 매우 중요한데, LLM이 이를 읽고 어떤 도구를 언제 사용할지 판단하기 때문입니다.
실무에서는 "수학 계산"보다 "정수 및 소수의 사칙연산, 제곱근, 지수 계산 등을 수행. 예: (123 * 456) / 789"처럼 구체적으로 작성하는 것이 좋습니다.
두 번째로, run 메서드에서 사용자의 질문과 함께 모든 도구의 설명을 LLM에게 제공합니다. 이는 LLM에게 "당신이 사용할 수 있는 능력들"을 알려주는 것입니다.
OpenAI의 Function Calling이나 Anthropic의 Tool Use 같은 최신 API는 이를 구조화된 방식으로 지원하여, LLM이 JSON 형식으로 도구 선택과 파라미터를 반환하도록 합니다. 세 번째로, LLM의 응답을 파싱해 어떤 도구를 어떤 파라미터로 실행할지 추출합니다.
예를 들어, "345 곱하기 789는?"이라는 질문에 LLM은 {"tool": "calculator", "parameters": {"operation": "multiply", "a": 345, "b": 789}}를 반환할 수 있습니다. 이때 파라미터 추출의 정확성이 중요한데, LLM이 문맥에서 필요한 값을 정확히 찾아내야 합니다.
네 번째로, 선택된 도구를 실제로 실행합니다. self.tools[tool_name]["func"](**params)는 해당 도구 함수를 동적으로 호출합니다.
이 부분에서 에러 핸들링이 중요합니다. 도구가 실패하면 "도구 실행 실패: [이유]"를 LLM에게 반환하여 대안을 찾도록 할 수 있습니다.
마지막으로, 도구의 결과를 LLM에게 다시 제공해 자연어 답변을 생성합니다. 단순히 "272505"라는 숫자를 반환하는 것이 아니라, "345와 789를 곱하면 272,505입니다"처럼 사용자 친화적인 답변을 만듭니다.
또한 필요시 여러 도구를 순차적으로 사용할 수도 있습니다. 여러분이 이 코드를 사용하면 LLM의 한계를 극복하고 실용적인 애플리케이션을 만들 수 있습니다.
예를 들어, 고객 지원 봇에 주문 조회 도구, 환불 처리 도구, 티켓 생성 도구를 연결하면 실제로 업무를 처리할 수 있는 에이전트가 됩니다. 또한 도구를 쉽게 추가/제거할 수 있어 확장성이 뛰어나고, LLM이 자동으로 새 도구를 학습해 사용하므로 코드 변경이 최소화됩니다.
실전 팁
💡 도구 설명은 예시를 포함하세요. "데이터베이스 조회"보다 "데이터베이스 조회: 사용자 정보, 주문 내역 등을 조회. 예: db_query(table='users', filter={'id': 123})"처럼 구체적으로 작성하면 LLM이 올바르게 사용할 확률이 높아집니다.
💡 Function Calling API를 활용하세요. OpenAI, Anthropic, Google 등 주요 LLM 제공자들은 도구 사용을 위한 구조화된 API를 제공합니다. 이를 사용하면 파싱 오류가 줄고 안정성이 높아집니다.
💡 도구 실행은 샌드박스에서 하세요. 특히 코드 실행, 파일 시스템 접근 같은 위험한 도구는 격리된 환경에서 실행해야 합니다. Docker 컨테이너나 제한된 권한의 프로세스를 사용하세요.
💡 도구 결과를 요약하세요. 데이터베이스 조회가 1000행을 반환했다면 모두 LLM에게 보내지 말고, 요약하거나 처음 N개만 보내세요. 토큰 한계와 비용을 절약할 수 있습니다.
💡 도구 체인을 지원하세요. 복잡한 작업은 여러 도구를 순차적으로 사용해야 합니다. "검색 → 계산 → 저장" 같은 체인을 자동으로 실행할 수 있도록 설계하면 훨씬 강력해집니다.
5. Plan-and-Execute 에이전트 - 계획 기반 실행
시작하며
여러분이 대형 프로젝트를 시작할 때, 바로 코딩부터 하시나요? 아마 아닐 겁니다.
먼저 요구사항을 분석하고, 아키텍처를 설계하고, 태스크를 나누고, 순서를 정한 후에 구현을 시작하실 겁니다. 계획 없는 실행은 비효율적이고 실패하기 쉽습니다.
AI 에이전트도 마찬가지입니다. ReAct처럼 매 단계마다 다음 행동을 즉흥적으로 결정하는 방식은 간단한 작업에는 적합하지만, 복잡하고 긴 작업에서는 방향을 잃거나 중복된 작업을 하거나 중요한 단계를 빠뜨릴 수 있습니다.
전체적인 로드맵 없이 한 걸음씩만 보는 것이죠. 바로 이럴 때 필요한 것이 Plan-and-Execute 에이전트입니다.
이는 먼저 전체 작업을 분석해 상세한 계획을 수립하고, 그 계획에 따라 체계적으로 실행하는 두 단계 접근법입니다. 마치 프로젝트 매니저가 먼저 계획을 세우고 팀이 실행하는 것처럼 작동합니다.
개요
간단히 말해서, Plan-and-Execute 패턴은 작업을 "계획(Planning)" 단계와 "실행(Execution)" 단계로 명확히 분리하는 에이전트 아키텍처입니다. 계획 단계에서는 전체 작업을 분석하고 단계별 하위 작업으로 분해하며, 실행 단계에서는 이 계획을 충실히 따릅니다.
왜 이것이 필요할까요? 복잡한 작업일수록 전체적인 시각이 중요합니다.
예를 들어, "회사의 마케팅 전략을 분석하고 개선안을 제시하라"는 작업은 시장 조사, 경쟁사 분석, 내부 데이터 검토, 트렌드 파악, 전략 수립 등 수십 개의 하위 작업으로 구성됩니다. 이를 계획 없이 즉흥적으로 진행하면 중요한 부분을 빠뜨리거나 비효율적인 순서로 진행할 수 있습니다.
기존의 즉흥적 접근법은 "지금 뭐 하지?"를 매번 고민했다면, Plan-and-Execute는 처음에 한 번만 깊이 고민하고 나머지는 실행에 집중합니다. 이는 특히 LLM 호출 비용을 줄이는 데도 효과적입니다.
매 단계마다 "다음 뭐 하지?"를 묻지 않으니까요. Plan-and-Execute의 핵심 특징은 세 가지입니다: 첫째, 계층적 분해(Hierarchical Decomposition) - 큰 작업을 작은 작업들로 체계적으로 나눕니다.
둘째, 의존성 관리(Dependency Management) - 어떤 작업이 먼저 완료되어야 하는지 파악합니다. 셋째, 진행 상황 추적(Progress Tracking) - 계획 대비 현재 진행률을 명확히 알 수 있습니다.
이러한 특징들이 대규모 자동화 작업에서 Plan-and-Execute를 필수적으로 만듭니다.
코드 예제
# Plan-and-Execute 패턴 구현
class PlanAndExecuteAgent:
def __init__(self, llm, tools):
self.llm = llm
self.tools = tools
def run(self, task):
# Phase 1: Planning - 전체 계획 수립
plan = self.create_plan(task)
print(f"계획 수립 완료: {len(plan)} 단계")
# Phase 2: Execution - 계획에 따라 실행
results = []
for i, step in enumerate(plan):
print(f"[{i+1}/{len(plan)}] {step['description']}")
# 각 단계 실행
result = self.execute_step(step, results)
results.append({"step": step, "result": result})
# 필요시 계획 재조정
if self.should_replan(step, result):
plan = self.update_plan(plan, i, result)
# Phase 3: Synthesis - 결과 종합
return self.synthesize_results(task, results)
def create_plan(self, task):
# LLM에게 상세한 계획 수립 요청
prompt = f"""다음 작업을 완수하기 위한 상세한 계획을 수립하세요:
작업: {task}
각 단계는 다음 정보를 포함해야 합니다:
- description: 무엇을 할 것인가
- tool: 어떤 도구를 사용할 것인가
- dependencies: 어떤 이전 단계의 결과가 필요한가
"""
return self.llm.generate(prompt, format="json")
설명
이것이 하는 일: 이 코드는 복잡한 작업을 먼저 분석해 상세한 실행 계획을 만들고, 그 계획을 단계별로 실행하며, 필요시 계획을 동적으로 조정하는 전체 프로세스를 구현합니다. 첫 번째로, run 메서드는 세 가지 주요 단계로 구성됩니다: Planning, Execution, Synthesis.
이 명확한 분리가 Plan-and-Execute의 핵심입니다. 계획 단계에서는 실행을 고려하지 않고 오직 "무엇을 어떤 순서로 해야 하는가"에만 집중하고, 실행 단계에서는 사고를 최소화하고 계획을 따르는 데 집중합니다.
두 번째로, create_plan 메서드에서 LLM에게 작업 전체를 분석하고 상세한 로드맵을 작성하도록 요청합니다. 이때 중요한 것은 각 단계에 대한 메타데이터입니다.
단순히 "데이터 조회"가 아니라 "어떤 도구로, 어떤 이전 결과를 사용해, 무엇을 조회할 것인가"를 명시합니다. 또한 의존성 정보를 통해 병렬 처리 가능한 단계를 식별할 수 있습니다.
예를 들어, "시장 조사"와 "내부 데이터 분석"은 독립적이므로 동시에 실행할 수 있습니다. 세 번째로, 실행 단계에서는 계획의 각 단계를 순회하며 실행합니다.
각 단계의 결과는 results 리스트에 누적되어, 다음 단계에서 참조할 수 있습니다. 예를 들어, 3단계가 "1단계의 결과를 바탕으로 계산"이라면, results[0]['result']를 사용합니다.
또한 진행 상황을 출력해 사용자에게 투명성을 제공합니다. 네 번째로, should_replan을 통해 계획이 현실과 맞지 않을 때를 감지합니다.
예를 들어, 3단계에서 "데이터가 없음"이라는 결과가 나왔는데 4-5단계가 그 데이터에 의존한다면, 계획을 수정해야 합니다. update_plan은 현재 상황을 LLM에게 설명하고 나머지 계획을 재조정하도록 요청합니다.
이는 완전히 고정된 계획이 아니라 "적응형 계획"을 가능하게 합니다. 마지막으로, synthesize_results는 모든 단계의 결과를 종합해 최종 답변을 생성합니다.
단순히 마지막 결과만 반환하는 것이 아니라, "1단계에서 A를 발견했고, 3단계에서 B를 확인했으며, 이를 종합하면 C라는 결론에 도달한다"처럼 전체 스토리를 구성합니다. 여러분이 이 코드를 사용하면 수십 단계로 구성된 복잡한 워크플로우를 자동화할 수 있습니다.
특히 데이터 파이프라인, 보고서 생성, 종합 분석 같은 작업에서 효과적입니다. 또한 계획이 명시적이므로 실행 전에 사용자에게 승인을 받거나, 중간에 개입하거나, 특정 단계만 재실행하는 등의 제어가 가능합니다.
연구에 따르면 Plan-and-Execute는 복잡한 작업에서 ReAct보다 30-40% 더 효율적이며, 성공률도 높습니다.
실전 팁
💡 계획 수립에 강력한 모델을 사용하세요. 계획은 한 번만 수립되므로 GPT-4나 Claude 3 Opus 같은 고성능 모델을 사용해도 전체 비용이 크게 증가하지 않습니다. 반면 계획 품질은 전체 성공에 결정적입니다.
💡 계획을 사용자에게 보여주세요. 실행 전에 "이런 계획으로 진행하겠습니다"를 보여주면 신뢰가 높아지고, 사용자가 문제를 미리 발견할 수 있습니다. 승인 단계를 추가하는 것도 좋습니다.
💡 체크포인트를 저장하세요. 각 단계의 결과를 디스크에 저장하면 중간에 실패해도 처음부터 다시 시작하지 않아도 됩니다. 특히 오래 걸리는 작업에서 필수적입니다.
💡 병렬 실행을 고려하세요. 의존성이 없는 단계들은 동시에 실행할 수 있습니다. 계획에 의존성 그래프를 포함시키고, 병렬 실행 가능한 단계를 자동 감지하면 속도를 크게 향상시킬 수 있습니다.
💡 계획의 추상화 수준을 조절하세요. 너무 상세하면 유연성이 떨어지고, 너무 추상적이면 실행이 어렵습니다. "데이터베이스에서 최근 1주일 매출 조회"처럼 실행 가능하지만 구체적인 SQL은 실행 시점에 생성하는 수준이 적당합니다.
6. Multi-Agent 시스템 - 협업하는 에이전트들
시작하며
여러분의 회사를 생각해보세요. 한 명이 모든 일을 하나요?
아니죠. 개발팀, 디자인팀, 마케팅팀, 영업팀이 각자 전문 분야에서 협업합니다.
복잡한 프로젝트는 전문가들의 협업으로만 완수할 수 있습니다. 한 사람이 모든 것을 잘하기는 불가능하니까요.
AI 에이전트도 마찬가지입니다. 하나의 거대한 에이전트가 모든 작업을 처리하려 하면 혼란스럽고 비효율적입니다.
코드 작성, 디버깅, 테스트, 문서화, 배포를 하나의 에이전트가 동시에 고민하면 각 작업의 품질이 떨어집니다. 전문화와 분업이 필요합니다.
바로 이럴 때 필요한 것이 Multi-Agent 시스템입니다. 여러 개의 전문화된 에이전트가 각자의 역할을 수행하며 협업하는 구조입니다.
예를 들어, Researcher 에이전트가 정보를 수집하면, Analyst 에이전트가 분석하고, Writer 에이전트가 보고서를 작성하는 식이죠. 마치 실제 팀처럼 작동합니다.
개요
간단히 말해서, Multi-Agent 시스템은 여러 개의 전문화된 AI 에이전트가 서로 소통하고 협업하며 복잡한 작업을 완수하는 아키텍처입니다. 각 에이전트는 특정 역할에 최적화되어 있고, 협업 프로토콜을 통해 조율됩니다.
왜 이것이 필요할까요? 실무의 복잡한 프로젝트는 여러 전문 영역을 넘나듭니다.
예를 들어, "새로운 기능 개발"은 요구사항 분석, 설계, 구현, 테스트, 문서화, 코드 리뷰 등 각기 다른 전문성이 필요한 단계들로 구성됩니다. 하나의 범용 에이전트보다 각 분야에 특화된 에이전트들이 협업하는 것이 훨씬 효과적입니다.
기존에는 하나의 복잡한 프롬프트로 모든 것을 처리하려 했다면, 이제는 "Planner가 계획을 세우면 → Coder가 구현하고 → Reviewer가 검토하고 → Tester가 테스트한다"처럼 명확한 역할 분담이 가능합니다. 각 에이전트는 자신의 역할에만 집중하므로 품질이 높아집니다.
Multi-Agent의 핵심 특징은 세 가지입니다: 첫째, 전문화(Specialization) - 각 에이전트는 특정 작업에 최적화됩니다. 둘째, 소통(Communication) - 에이전트 간 메시지 전달 프로토콜이 있습니다.
셋째, 조율(Orchestration) - 누가 언제 작업하는지 조율하는 메커니즘이 있습니다. 이러한 특징들이 Multi-Agent를 복잡한 실무 작업의 핵심 솔루션으로 만들고 있습니다.
코드 예제
# 간단한 Multi-Agent 시스템
class Agent:
def __init__(self, name, role, llm):
self.name = name
self.role = role # 에이전트의 전문 분야
self.llm = llm
def process(self, task, context):
# 자신의 역할에 맞게 작업 처리
prompt = f"당신은 {self.role}입니다.\n이전 작업: {context}\n현재 작업: {task}"
return self.llm.generate(prompt)
class MultiAgentSystem:
def __init__(self, llm):
self.agents = {
"researcher": Agent("연구원", "정보 수집 및 조사 전문가", llm),
"analyst": Agent("분석가", "데이터 분석 및 인사이트 도출 전문가", llm),
"writer": Agent("작가", "명확하고 설득력 있는 글쓰기 전문가", llm)
}
self.message_history = []
def run(self, task):
# 1. Researcher가 정보 수집
research = self.agents["researcher"].process(task, self.message_history)
self.message_history.append({"agent": "researcher", "output": research})
# 2. Analyst가 분석
analysis = self.agents["analyst"].process("수집된 정보를 분석하라", self.message_history)
self.message_history.append({"agent": "analyst", "output": analysis})
# 3. Writer가 보고서 작성
report = self.agents["writer"].process("분석을 바탕으로 보고서 작성", self.message_history)
return report
설명
이것이 하는 일: 이 코드는 여러 전문화된 에이전트가 순차적으로 작업을 이어받아 최종 결과물을 만드는 협업 파이프라인을 구현합니다. 각 에이전트는 이전 에이전트의 작업을 바탕으로 자신의 전문성을 더합니다.
첫 번째로, Agent 클래스는 개별 에이전트의 기본 구조를 정의합니다. 각 에이전트는 이름, 역할, 그리고 LLM을 가집니다.
role이 핵심인데, 이는 에이전트의 정체성을 정의합니다. "정보 수집 전문가"로 정의된 에이전트는 자연스럽게 검색, 조사, 데이터 수집에 집중하고, "분석가"는 패턴 발견, 통계, 인사이트 도출에 집중합니다.
같은 LLM을 사용하더라도 역할 정의에 따라 행동이 달라지는 것이죠. 두 번째로, MultiAgentSystem은 여러 에이전트를 관리하고 조율합니다.
message_history는 에이전트 간 소통의 핵심으로, 모든 에이전트의 작업 내용이 기록됩니다. 이는 마치 팀의 공유 문서나 슬랙 채널처럼 작동합니다.
각 에이전트는 이를 읽고 "이전에 무슨 일이 있었는지" 파악한 후 자신의 작업을 수행합니다. 세 번째로, run 메서드는 에이전트들의 협업 순서를 정의합니다.
이 예시에서는 선형적 파이프라인(Researcher → Analyst → Writer)을 사용하지만, 실제로는 더 복잡한 협업 패턴도 가능합니다. 예를 들어, 여러 Researcher가 병렬로 정보를 수집하고, Analyst가 종합하고, Writer와 Editor가 협업해 최종 문서를 다듬는 식으로 확장할 수 있습니다.
네 번째로, 각 에이전트는 process 메서드를 통해 작업을 수행합니다. 이때 현재 작업뿐 아니라 전체 컨텍스트를 받습니다.
예를 들어, Analyst는 단순히 "분석하라"는 지시만 받는 것이 아니라, Researcher가 어떤 정보를 수집했는지, 어떤 출처를 사용했는지 등을 모두 알 수 있습니다. 이를 바탕으로 더 맥락에 맞는 분석을 할 수 있습니다.
다섯 번째로, 각 단계의 결과가 message_history에 추가됩니다. 이는 추적성과 디버깅에 매우 유용합니다.
최종 결과가 마음에 들지 않는다면, 어느 에이전트가 어떤 결정을 내렸는지 단계별로 확인할 수 있습니다. 또한 필요시 특정 에이전트의 작업만 다시 실행할 수도 있습니다.
여러분이 이 코드를 사용하면 복잡한 지식 작업을 자동화할 수 있습니다. 예를 들어, 시장 조사 보고서, 기술 분석 문서, 경쟁사 분석 등을 여러 전문가 역할의 에이전트가 협업해 생성할 수 있습니다.
실제로 AutoGen, CrewAI 같은 프레임워크는 이 패턴을 확장해 수십 개의 에이전트가 협업하는 시스템을 지원합니다. 연구에 따르면 Multi-Agent는 단일 에이전트 대비 복잡한 작업에서 40-60% 더 높은 품질을 보입니다.
실전 팁
💡 에이전트의 역할을 명확히 정의하세요. "개발자"보다는 "Python 백엔드 개발자, FastAPI와 PostgreSQL 전문"처럼 구체적으로 정의하면 더 나은 결과를 얻습니다. 역할이 명확할수록 에이전트의 행동도 일관됩니다.
💡 협업 패턴을 선택하세요. 순차(Sequential), 병렬(Parallel), 계층(Hierarchical), 토론(Debate) 등 다양한 패턴이 있습니다. 작업 특성에 맞게 선택하세요. 창의적 작업은 토론 패턴이, 분석 작업은 순차 패턴이 효과적입니다.
💡 Orchestrator를 두세요. 에이전트가 많아지면 누가 언제 작업하는지 조율하는 역할이 필요합니다. 별도의 Manager 에이전트를 두거나, 규칙 기반 스케줄러를 사용하세요.
💡 컨텍스트 관리에 신경 쓰세요. 모든 에이전트에게 전체 히스토리를 주면 토큰이 폭발합니다. 각 에이전트에게 필요한 컨텍스트만 필터링해서 제공하거나, 요약해서 전달하세요.
💡 에이전트 간 피드백을 허용하세요. Reviewer 에이전트가 Coder 에이전트의 코드를 검토하고 수정 요청을 하는 식의 양방향 소통을 지원하면 품질이 크게 향상됩니다.
7. Memory-augmented 에이전트 - 장기 기억 활용
시작하며
여러분이 고객과 장기적인 관계를 유지할 때 어떻게 하시나요? 매번 "처음 뵙겠습니다"라고 하지는 않을 겁니다.
이전 대화, 선호도, 거래 내역을 기억하고 참조하죠. 기억이 없다면 신뢰를 쌓을 수 없고, 맞춤형 서비스도 불가능합니다.
그런데 기본 LLM은 각 세션이 독립적입니다. 어제 나눈 대화를 오늘은 기억하지 못하고, 사용자의 선호도나 이전 작업 결과도 누적되지 않습니다.
매번 백지 상태에서 시작하는 것이죠. 이는 실무에서 큰 한계입니다.
반복적인 작업을 하거나, 장기 프로젝트를 진행하거나, 개인화된 서비스를 제공하려면 기억이 필수적입니다. 바로 이럴 때 필요한 것이 Memory-augmented 에이전트입니다.
이는 단기 기억(현재 대화)뿐 아니라 장기 기억(과거 모든 상호작용)을 활용하는 에이전트입니다. 데이터베이스나 벡터 스토어에 정보를 저장하고, 필요할 때 검색해 사용함으로써 지속적으로 학습하고 개선되는 시스템을 만듭니다.
개요
간단히 말해서, Memory-augmented 에이전트는 외부 메모리 시스템(데이터베이스, 벡터 DB 등)을 활용해 과거 정보를 저장하고 검색하는 에이전트입니다. 모든 상호작용, 학습 내용, 사용자 선호도 등이 누적되어 시간이 갈수록 똑똑해집니다.
왜 이것이 필요할까요? 많은 실무 작업은 맥락과 이력이 중요합니다.
예를 들어, 코드 리뷰 에이전트가 "이전에 유사한 패턴에서 버그를 발견했었다"는 것을 기억한다면 더 정확한 리뷰를 할 수 있습니다. 고객 지원 봇이 "이 사용자는 3주 전에도 비슷한 문제를 겪었다"를 안다면 더 빠른 해결이 가능합니다.
기존에는 매번 모든 컨텍스트를 프롬프트에 포함시켜야 했다면, 이제는 관련된 정보만 동적으로 검색해 사용합니다. 이는 토큰 한계를 극복하고, 수개월~수년에 걸친 정보를 활용 가능하게 만듭니다.
Memory-augmented의 핵심 특징은 세 가지입니다: 첫째, 지속성(Persistence) - 정보가 세션을 넘어 영구 저장됩니다. 둘째, 검색성(Retrieval) - 필요한 정보를 의미적으로 검색합니다.
셋째, 업데이트(Update) - 새로운 경험으로 메모리가 지속적으로 갱신됩니다. 이러한 특징들이 진정으로 진화하는 AI 시스템을 가능하게 합니다.
코드 예제
# Vector DB를 활용한 Memory-augmented 에이전트
import chromadb
from chromadb.utils import embedding_functions
class MemoryAgent:
def __init__(self, llm):
self.llm = llm
# 벡터 DB 초기화
self.client = chromadb.Client()
self.embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction()
self.memory = self.client.create_collection(
name="agent_memory",
embedding_function=self.embedding_fn
)
def run(self, query):
# 1. 관련 기억 검색
relevant_memories = self.memory.query(
query_texts=[query],
n_results=5 # 상위 5개 관련 기억
)
# 2. 기억을 컨텍스트에 포함하여 응답 생성
context = f"관련 과거 경험:\n{relevant_memories}\n\n현재 질문: {query}"
response = self.llm.generate(context)
# 3. 현재 상호작용을 메모리에 저장
self.memory.add(
documents=[f"Q: {query}\nA: {response}"],
ids=[str(time.time())] # 타임스탬프 ID
)
return response
def remember(self, key_info, metadata=None):
"""중요한 정보를 명시적으로 기억"""
self.memory.add(
documents=[key_info],
metadatas=[metadata or {}],
ids=[str(uuid.uuid4())]
)
설명
이것이 하는 일: 이 코드는 벡터 데이터베이스를 활용해 모든 상호작용을 의미적으로 저장하고, 현재 질문과 관련된 과거 경험을 자동으로 검색해 활용하는 장기 기억 시스템을 구현합니다. 첫 번째로, __init__에서 ChromaDB라는 벡터 데이터베이스를 초기화합니다.
벡터 DB는 텍스트를 수치 벡터로 변환(임베딩)해 저장하고, 의미적 유사성으로 검색할 수 있게 합니다. 예를 들어, "비밀번호 재설정"과 "패스워드를 잊어버렸어요"는 단어는 다르지만 의미가 유사하므로 벡터 공간에서 가깝게 위치합니다.
SentenceTransformerEmbeddingFunction은 이런 임베딩을 자동으로 생성합니다. 두 번째로, run 메서드의 첫 단계에서 현재 질문과 의미적으로 유사한 과거 기억을 검색합니다.
query_texts=[query]는 현재 질문을 벡터로 변환하고, n_results=5는 가장 유사한 상위 5개를 가져옵니다. 이는 마치 "이 상황과 비슷한 경우를 과거에 본 적 있나?"라고 자신의 기억을 뒤지는 것과 같습니다.
수백만 개의 기억 중에서도 밀리초 내에 관련된 것만 찾아냅니다. 세 번째로, 검색된 기억을 LLM의 컨텍스트에 포함시킵니다.
이는 LLM에게 "이전에 유사한 질문에 이렇게 답했었고, 그때 이런 정보가 유용했었다"는 힌트를 제공합니다. LLM은 이를 참고해 더 일관되고 정확한 답변을 생성할 수 있습니다.
예를 들어, 사용자가 "저번에 추천해준 방법 말이야..."라고 하면, 그때의 대화를 검색해 이해할 수 있습니다. 네 번째로, 현재 상호작용(질문과 답변)을 메모리에 저장합니다.
이는 미래의 유사한 상황에서 참조할 수 있게 됩니다. 타임스탬프를 ID로 사용해 시간 순서도 추적할 수 있습니다.
또한 메타데이터를 추가하면 "성공/실패", "사용자 만족도", "카테고리" 등으로 필터링도 가능합니다. 다섯 번째로, remember 메서드는 특정 정보를 명시적으로 저장합니다.
일반 대화가 아니라 "사용자 선호도", "중요한 발견", "규칙" 등을 저장할 때 사용합니다. 예를 들어, "이 사용자는 TypeScript를 선호함"이나 "프로젝트 X에서 패턴 Y가 효과적이었음" 같은 메타 지식을 축적할 수 있습니다.
여러분이 이 코드를 사용하면 시간이 지날수록 개선되는 AI 시스템을 만들 수 있습니다. 개인 비서, 고객 지원, 코드 리뷰, 프로젝트 관리 등 장기적 맥락이 중요한 모든 작업에서 효과적입니다.
실제 사례로, Notion AI는 사용자의 모든 문서를 벡터로 저장해 질문에 답하고, Cursor IDE는 코드베이스 전체를 임베딩해 관련 코드를 자동으로 참조합니다. 연구에 따르면 메모리 활용은 반복 작업의 효율을 50% 이상 향상시킵니다.
실전 팁
💡 임베딩 모델을 도메인에 맞게 선택하세요. 일반 텍스트는 Sentence Transformers, 코드는 CodeBERT, 다국어는 multilingual 모델을 사용하면 검색 정확도가 높아집니다.
💡 메타데이터를 적극 활용하세요. 날짜, 카테고리, 성공 여부, 사용자 ID 등을 메타데이터로 저장하면 "최근 1주일", "성공한 경우만" 같은 필터링이 가능해집니다.
💡 메모리를 주기적으로 정리하세요. 오래되거나 관련 없는 기억은 검색 품질을 떨어뜨립니다. 중요도 점수를 매기고, 낮은 것은 아카이브하거나 삭제하세요.
💡 계층적 메모리를 고려하세요. 단기(세션 내), 중기(프로젝트별), 장기(전체 이력)로 나누어 관리하면 더 효율적입니다. 단기는 전체 저장, 장기는 요약만 저장하는 식입니다.
💡 개인정보를 주의하세요. 사용자 데이터를 저장할 때는 동의를 받고, 민감 정보는 마스킹하거나 암호화하세요. GDPR 등 규제 준수도 중요합니다.
8. Hierarchical 에이전트 - 계층적 작업 분해
시작하며
여러분이 대형 프로젝트를 관리할 때, 모든 세부 작업을 직접 하시나요? 아니죠.
큰 목표를 중간 목표로 나누고, 각 중간 목표를 팀에 위임하고, 팀은 다시 개인 작업으로 분해합니다. 이런 계층적 구조가 복잡성을 관리하는 핵심입니다.
AI 에이전트도 복잡한 작업을 한 번에 처리하려 하면 압도당합니다. "회사의 디지털 전환 전략을 수립하라" 같은 거대한 작업은 수백 개의 하위 작업으로 구성되어 있습니다.
하나의 에이전트가 이 모든 것을 동시에 고려하기는 불가능합니다. 바로 이럴 때 필요한 것이 Hierarchical 에이전트입니다.
이는 작업을 계층적으로 분해하고, 각 계층에서 전문화된 에이전트가 처리하는 구조입니다. Manager 에이전트가 큰 그림을 보고 중간 목표를 설정하면, Worker 에이전트들이 구체적인 작업을 수행합니다.
필요시 여러 단계의 계층을 만들 수도 있습니다.
개요
간단히 말해서, Hierarchical 에이전트는 작업을 상위-하위 관계로 분해하고, 각 계층에서 적절한 추상화 수준으로 처리하는 에이전트 아키텍처입니다. 고수준 에이전트는 전략과 계획을, 저수준 에이전트는 실행을 담당합니다.
왜 이것이 필요할까요? 추상화 수준을 분리하지 않으면 사고가 혼란스러워집니다.
전략을 짜면서 동시에 구현 세부사항을 고민하면 둘 다 잘 안 됩니다. 예를 들어, "신규 서비스 런칭" 작업에서 Manager는 "1.
시장 조사, 2. MVP 개발, 3.
베타 테스트, 4. 정식 출시"를 계획하고, 각 단계를 전문 Worker에게 위임합니다.
Worker는 자신의 단계만 깊게 파고들면 됩니다. 기존의 평면적 접근은 모든 작업을 같은 수준에서 나열했다면, Hierarchical은 "왜(Why) → 무엇(What) → 어떻게(How)"의 계층을 만듭니다.
상위는 "왜 이것이 필요한가"를, 하위는 "구체적으로 어떻게 하는가"를 담당합니다. Hierarchical의 핵심 특징은 세 가지입니다: 첫째, 추상화 계층(Abstraction Layers) - 각 계층은 다른 수준의 세부사항을 다룹니다.
둘째, 위임(Delegation) - 상위 에이전트는 하위 에이전트에게 작업을 위임합니다. 셋째, 집계(Aggregation) - 하위 결과를 상위로 보고하고 종합합니다.
이러한 특징들이 매우 복잡한 프로젝트를 관리 가능하게 만듭니다.
코드 예제
# Hierarchical Agent 구현
class HierarchicalAgent:
def __init__(self, llm, level="manager"):
self.llm = llm
self.level = level # manager or worker
self.workers = []
def add_worker(self, worker_agent):
"""하위 에이전트 추가"""
self.workers.append(worker_agent)
def run(self, task):
if self.level == "manager":
# Manager: 작업을 하위 작업으로 분해
subtasks = self.decompose_task(task)
# 각 하위 작업을 Worker에게 위임
results = []
for i, subtask in enumerate(subtasks):
worker = self.workers[i % len(self.workers)] # 라운드 로빈
result = worker.run(subtask)
results.append({"subtask": subtask, "result": result})
# 결과 집계 및 종합
return self.synthesize(task, results)
else: # worker
# Worker: 구체적인 작업 실행
return self.execute_task(task)
def decompose_task(self, task):
"""고수준 작업을 중수준 하위 작업들로 분해"""
prompt = f"""다음 작업을 3-5개의 독립적인 하위 작업으로 분해하세요:
작업: {task}
각 하위 작업은 구체적이고 실행 가능해야 합니다."""
return self.llm.generate(prompt, format="list")
설명
이것이 하는 일: 이 코드는 작업을 고수준과 저수준으로 분리하고, Manager 에이전트가 작업을 분해해 Worker 에이전트들에게 위임한 후, 결과를 집계하는 계층적 시스템을 구현합니다. 첫 번째로, __init__에서 에이전트의 계층 수준을 정의합니다.
level="manager"는 이 에이전트가 전략적 사고와 위임을 담당함을 의미하고, level="worker"는 구체적 실행을 담당함을 의미합니다. 실제 시스템에서는 3단계 이상의 계층도 가능합니다: Director(최상위) → Manager(중간) → Worker(실행) 같은 구조로요.
두 번째로, add_worker를 통해 Manager에게 Worker들을 등록합니다. 이는 조직도를 만드는 것과 같습니다.
각 Worker는 특정 영역에 전문화될 수 있습니다. 예를 들어, 하나는 데이터 수집 전문, 다른 하나는 분석 전문, 또 다른 하나는 시각화 전문일 수 있습니다.
Manager는 각 Worker의 능력을 알고 적절한 작업을 할당합니다. 세 번째로, Manager의 run 메서드는 먼저 decompose_task로 큰 작업을 중간 크기의 하위 작업들로 분해합니다.
이때 중요한 것은 각 하위 작업이 독립적이고 명확해야 한다는 것입니다. "데이터 분석"처럼 모호한 것이 아니라 "2023년 매출 데이터를 CSV로 추출하고 월별 합계 계산"처럼 구체적이어야 합니다.
이는 Worker가 추가 지시 없이 바로 실행할 수 있게 합니다. 네 번째로, 분해된 각 하위 작업을 Worker들에게 분배합니다.
이 예시에서는 라운드 로빈 방식으로 순환 배정하지만, 실제로는 각 Worker의 전문성, 현재 부하, 작업 유형 등을 고려해 최적의 Worker를 선택할 수 있습니다. 각 Worker는 자신에게 할당된 작업만 집중적으로 처리하므로 품질과 효율이 높아집니다.
다섯 번째로, Worker의 execute_task는 실제 작업을 수행합니다. 이는 도구를 사용하거나, 데이터를 처리하거나, 분석을 수행하는 등 구체적인 행동입니다.
Worker는 "왜 이것을 하는가"에 대해 깊이 고민하지 않고, "어떻게 잘 수행할 것인가"에만 집중합니다. 이런 역할 분리가 효율성의 핵심입니다.
마지막으로, 모든 Worker의 결과가 모이면 Manager의 synthesize가 이를 종합합니다. 단순히 나열하는 것이 아니라, 각 결과 간의 관계를 파악하고, 일관성을 확인하고, 최종 인사이트를 도출합니다.
예를 들어, 3개의 시장 조사 결과를 종합해 "공통 트렌드는 X이고, 차이점은 Y이며, 우리의 기회는 Z이다"라는 전략적 결론을 냅니다. 여러분이 이 코드를 사용하면 수십~수백 개의 작업으로 구성된 대규모 프로젝트를 자동화할 수 있습니다.
특히 컨설팅, 연구, 전략 수립 같은 복잡한 지식 작업에서 효과적입니다. Google의 DeepMind는 계층적 강화학습으로 복잡한 게임을 마스터했고, OpenAI는 계층적 에이전트로 복잡한 코딩 작업을 해결합니다.
연구에 따르면 Hierarchical 구조는 평면 구조 대비 50% 이상 복잡한 작업을 처리할 수 있습니다.
실전 팁
💡 계층의 깊이를 제한하세요. 3단계 이상은 오버헤드가 커집니다. 대부분의 작업은 Manager-Worker 2단계로 충분하며, 매우 복잡한 경우에만 3단계(Director-Manager-Worker)를 사용하세요.
💡 하위 작업의 독립성을 확보하세요. Worker 간 의존성이 많으면 조율 비용이 증가합니다. 가능하면 각 하위 작업이 병렬로 실행될 수 있도록 분해하세요.
💡 컨텍스트 전달을 최소화하세요. Manager는 Worker에게 필요한 정보만 전달하고, Worker는 Manager에게 핵심 결과만 보고하세요. 전체 컨텍스트를 계속 전달하면 비효율적입니다.
💡 Worker의 전문화를 활용하세요. 모든 Worker가 범용적이면 계층의 이점이 줄어듭니다. 각 Worker에게 명확한 전문 분야를 부여하고, Manager가 이를 고려해 작업을 배정하도록 하세요.
💡 실패 처리를 구현하세요. Worker가 실패하면 Manager가 다른 Worker에게 재할당하거나, 작업을 더 작게 분해하거나, 대안 전략을 사용하도록 설계하세요.
9. Autonomous 에이전트 - 완전 자율 실행
시작하며
여러분이 직원에게 "이번 분기 목표는 매출 20% 증가야. 알아서 해봐"라고 하면 어떻게 될까요?
유능한 직원이라면 스스로 계획을 세우고, 필요한 자원을 찾고, 문제를 해결하며 목표를 달성할 겁니다. 매 단계마다 지시를 기다리지 않고 자율적으로 움직이죠.
그런데 대부분의 AI 에이전트는 매 단계마다 인간의 확인을 받거나, 미리 정의된 계획을 따르거나, 제한된 범위 내에서만 작동합니다. 이는 안전하지만 진정한 자동화는 아닙니다.
실시간 모니터링, 지속적 개입, 세밀한 가이드가 필요하다면 효율이 제한적입니다. 바로 이럴 때 필요한 것이 Autonomous 에이전트입니다.
이는 최소한의 인간 개입으로 장기간 독립적으로 작동하는 에이전트입니다. 목표만 주어지면 스스로 계획하고, 실행하고, 문제를 해결하며, 예상치 못한 상황에도 대응합니다.
AutoGPT, BabyAGI 같은 시스템이 이 범주에 속합니다.
개요
간단히 말해서, Autonomous 에이전트는 고수준 목표만 주어지면 스스로 하위 목표를 생성하고, 작업을 계획하고, 실행하고, 결과를 평가하며, 필요시 계획을 수정하는 완전 자율 시스템입니다. 왜 이것이 필요할까요?
많은 작업은 장기간에 걸쳐 지속적으로 수행되어야 합니다. 예를 들어, "경쟁사의 동향을 모니터링하고 중요한 변화가 있으면 보고하라"는 작업은 몇 주, 몇 달 동안 계속됩니다.
매번 인간이 개입하기는 비현실적입니다. Autonomous 에이전트는 이런 "설정하고 잊어버리기(set-and-forget)" 작업을 가능하게 합니다.
기존 에이전트는 미리 정의된 워크플로우나 스크립트를 따랐다면, Autonomous는 상황을 보고 스스로 결정합니다. 새로운 기회를 발견하면 탐색하고, 막히면 우회 경로를 찾고, 실패하면 다른 접근법을 시도합니다.
Autonomous의 핵심 특징은 네 가지입니다: 첫째, 자기 주도성(Self-direction) - 인간의 지시 없이 다음 행동을 결정합니다. 둘째, 장기 실행(Long-running) - 몇 시간, 며칠, 심지어 몇 주간 작동합니다.
셋째, 적응성(Adaptability) - 예상치 못한 상황에 유연하게 대응합니다. 넷째, 목표 지향성(Goal-oriented) - 세부 방법이 아닌 최종 목표에 집중합니다.
이러한 특징들이 진정한 AI 워커를 가능하게 합니다.
코드 예제
# Autonomous Agent의 기본 루프
import time
class AutonomousAgent:
def __init__(self, llm, tools):
self.llm = llm
self.tools = tools
self.goal = None
self.task_queue = [] # 해야 할 작업들
self.completed = [] # 완료된 작업들
self.running = False
def start(self, goal, max_iterations=100):
"""목표를 주고 자율 실행 시작"""
self.goal = goal
self.running = True
self.task_queue = [{"task": "목표 분석 및 초기 계획 수립", "priority": 1}]
iteration = 0
while self.running and iteration < max_iterations:
# 1. 다음 작업 선택 (우선순위 기반)
if not self.task_queue:
self.generate_new_tasks() # 작업이 없으면 생성
current_task = self.task_queue.pop(0)
# 2. 작업 실행
result = self.execute_task(current_task)
self.completed.append({"task": current_task, "result": result})
# 3. 결과 평가 및 새 작업 생성
self.reflect_and_plan(result)
# 4. 목표 달성 여부 확인
if self.is_goal_achieved():
self.running = False
return self.generate_final_report()
iteration += 1
time.sleep(1) # Rate limiting
return "최대 반복 도달 또는 중단됨"
def generate_new_tasks(self):
"""현재 상황을 분석하고 새로운 작업 생성"""
context = f"목표: {self.goal}\n완료: {self.completed}\n"
new_tasks = self.llm.generate(
f"{context}\n다음에 해야 할 작업 3개를 생성하세요",
format="list"
)
self.task_queue.extend(new_tasks)
설명
이것이 하는 일: 이 코드는 고수준 목표를 받아 스스로 작업을 생성하고, 우선순위를 정하고, 실행하며, 결과를 평가해 새 작업을 만드는 자율적 루프를 구현합니다. 목표를 달성하거나 최대 반복에 도달할 때까지 계속됩니다.
첫 번째로, start 메서드는 목표와 함께 자율 실행을 시작합니다. max_iterations는 폭주를 방지하는 안전장치입니다.
실제 AutoGPT 같은 시스템은 수백~수천 번의 반복을 수행할 수 있으며, 각 반복마다 LLM을 호출하고 도구를 실행하므로 비용과 시간이 상당합니다. 따라서 명확한 종료 조건과 한계 설정이 중요합니다.
두 번째로, 메인 루프는 "작업 선택 → 실행 → 평가 → 계획" 사이클을 반복합니다. 이는 인간의 작업 방식과 유사합니다.
할 일 목록에서 다음 작업을 선택하고, 수행하고, 결과를 보고, "이제 뭘 해야 하지?"를 생각하는 과정이죠. task_queue가 비어있으면 generate_new_tasks로 새로운 작업을 생성하므로, 에이전트는 절대 "할 일이 없어서" 멈추지 않습니다.
세 번째로, execute_task는 현재 작업에 필요한 도구를 선택하고 실행합니다. 이는 앞서 설명한 Tool-using이나 ReAct 패턴을 내부적으로 사용할 수 있습니다.
핵심은 인간의 확인 없이 자동으로 진행된다는 것입니다. 물론 위험한 작업(파일 삭제, 금전 거래 등)에는 안전장치를 추가해야 합니다.
네 번째로, reflect_and_plan은 방금 수행한 작업의 결과를 분석하고 다음 단계를 결정합니다. 예를 들어, "경쟁사 웹사이트 스크래핑"을 수행했는데 새로운 제품을 발견했다면, "해당 제품의 가격 조사", "유사 제품과 비교", "보고서에 추가" 같은 새 작업을 생성할 수 있습니다.
이는 정적 계획이 아닌 동적 계획으로, 실행 중 발견한 정보에 따라 계획이 진화합니다. 다섯 번째로, is_goal_achieved는 목표 달성 여부를 판단합니다.
이는 Autonomous 에이전트의 가장 어려운 부분입니다. "매출 20% 증가"처럼 정량적 목표는 쉽지만, "경쟁력 있는 제품 개발"처럼 정성적 목표는 판단이 어렵습니다.
보통 LLM에게 현재 상태와 목표를 제시하고 달성 여부를 평가하도록 합니다. 마지막으로, 목표를 달성하면 generate_final_report로 전체 과정과 결과를 종합합니다.
이는 단순히 "완료"가 아니라 "무엇을 했고, 어떤 어려움이 있었고, 최종 결과는 무엇인가"를 상세히 기록합니다. 이는 인간에게 투명성을 제공하고, 향후 유사 작업의 참고 자료가 됩니다.
여러분이 이 코드를 사용하면 장기적이고 복잡한 프로젝트를 최소 개입으로 자동화할 수 있습니다. 예를 들어, "매주 업계 뉴스를 모니터링하고 중요한 트렌드를 요약하라", "코드베이스의 보안 취약점을 지속적으로 스캔하고 발견하면 이슈 생성하라" 같은 작업에 유용합니다.
하지만 완전 자율은 위험도 높으므로, 중요한 결정에는 인간 승인을 요구하는 "semi-autonomous" 모드도 고려하세요.
실전 팁
💡 안전장치를 필수로 구현하세요. 비용 한도, 시간 한도, 위험한 작업의 화이트리스트, 주기적인 인간 체크인 등을 설정해 에이전트가 폭주하지 않도록 하세요.
💡 작업 우선순위를 관리하세요. 모든 작업이 동등하지 않습니다. 목표에 직접적으로 기여하는 작업을 우선하고, 탐색적 작업은 후순위로 두는 우선순위 큐를 사용하세요.
💡 진행 상황을 로깅하세요. 모든 작업, 결정, 결과를 상세히 로그로 남기면 디버깅과 감사(audit)가 가능합니다. 특히 실패 시 왜 실패했는지 추적하는 데 필수적입니다.
💡 컨텍스트 윈도우를 관리하세요. 수백 번의 반복 후에는 컨텍스트가 폭발합니다. 중요한 정보만 요약해 유지하고, 세부사항은 외부 메모리(벡터 DB)에 저장하세요.
💡 목표를 SMART하게 정의하세요. Specific(구체적), Measurable(측정 가능), Achievable(달성 가능), Relevant(관련성), Time-bound(시간 제한)한 목표를 설정하면 에이전트가 달성 여부를 더 정확히 판단할 수 있습니다.