🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

AI 에이전트의 모든 것 - 개념부터 실습까지 - 슬라이드 1/7
A

AI Generated

2025. 12. 26. · 2 Views

AI 에이전트의 모든 것 - 개념부터 실습까지

AI 에이전트란 무엇일까요? 단순한 LLM 호출과 어떻게 다를까요? 초급 개발자를 위해 에이전트의 핵심 개념부터 실제 구현까지 이북처럼 술술 읽히는 스타일로 설명합니다.


목차

  1. 에이전트의 정의와 특징
  2. Perception-Reasoning-Action 사이클
  3. 에이전트 vs 단순 LLM 호출
  4. "회의 자료가 준비되었습니다. meeting_20240315.pptx 파일을 확인해주세요."
  5. 에이전트 아키텍처 패턴
  6. 실습 간단한 에이전트 구현
  7. 실습 에이전트 동작 분석

1. 에이전트의 정의와 특징

개발자 김민수 씨는 최근 ChatGPT API를 활용한 챗봇을 개발했습니다. 그런데 회의 시간에 팀장님이 이런 질문을 던졌습니다.

"민수 씨, 우리가 만든 건 AI 에이전트인가요, 아니면 그냥 챗봇인가요?" 민수 씨는 잠시 머뭇거렸습니다. 둘 사이에 무슨 차이가 있는 걸까요?

AI 에이전트는 단순히 질문에 답하는 것을 넘어 스스로 판단하고 행동하는 시스템입니다. 마치 비서가 상사의 지시를 받고 스스로 일정을 조율하고 회의실을 예약하듯이, 에이전트는 주어진 목표를 달성하기 위해 필요한 도구를 선택하고 실행합니다.

단순한 대화형 AI와 달리 자율성도구 사용 능력을 갖춘 것이 핵심입니다.

다음 코드를 살펴봅시다.

# 에이전트의 기본 구조 예시
class SimpleAgent:
    def __init__(self, llm, tools):
        self.llm = llm  # 언어 모델
        self.tools = tools  # 사용 가능한 도구들

    def run(self, task):
        # 1. 현재 상황 인식
        observation = self.observe(task)
        # 2. 다음 행동 결정
        action = self.llm.decide_action(observation, self.tools)
        # 3. 행동 실행
        result = self.execute(action)
        # 4. 결과 확인 및 반복
        if not self.is_task_complete(result):
            return self.run(result)  # 재귀적으로 계속 진행
        return result

김민수 씨는 퇴근 후 집에서 에이전트에 대해 공부하기 시작했습니다. 검색창에 "AI 에이전트란"이라고 입력하고 여러 자료를 읽어봤지만, 여전히 명확하지 않았습니다.

그러던 중 선배 개발자 박지혜 씨가 쓴 블로그 글을 발견했습니다. "에이전트를 이해하려면 먼저 자율성이라는 개념을 알아야 합니다." 박지혜 씨는 글에서 이렇게 설명했습니다.

단순한 챗봇은 질문을 받으면 답변만 생성합니다. 하지만 에이전트는 다릅니다.

목표가 주어지면 그 목표를 달성하기 위해 스스로 계획을 세우고, 필요한 도구를 선택하며, 결과를 확인한 후 다음 행동을 결정합니다. 쉽게 비유하자면, 에이전트는 마치 개인 비서와 같습니다.

상사가 "다음 주 화요일에 서울에서 회의가 있으니 준비해줘"라고 말하면, 비서는 이렇게 행동합니다. 먼저 일정을 확인하고, KTX 표를 예약하며, 회의실을 잡고, 필요한 자료를 준비합니다.

상사는 세부적인 지시를 하나하나 내리지 않았지만, 비서는 목표 달성을 위해 필요한 일들을 스스로 판단하여 처리합니다. AI 에이전트도 정확히 같은 방식으로 작동합니다.

전통적인 프로그래밍에서는 어땠을까요? 개발자가 모든 경우의 수를 미리 코드로 작성해야 했습니다.

"만약 사용자가 이렇게 말하면 이렇게 응답하고, 저렇게 말하면 저렇게 응답하라." 수백 개의 if-else 문으로 가득 찬 코드를 본 적이 있나요? 바로 그런 방식이었습니다.

문제는 세상의 모든 상황을 미리 예측할 수 없다는 점입니다. 에이전트의 등장으로 이런 패러다임이 완전히 바뀌었습니다.

에이전트에게는 세 가지 핵심 능력이 있습니다. 첫째, 인식 능력입니다.

현재 상황이 어떤지, 무엇이 필요한지를 파악합니다. 둘째, 추론 능력입니다.

LLM을 활용하여 다음에 무엇을 해야 할지 결정합니다. 셋째, 행동 능력입니다.

실제로 도구를 실행하고 결과를 얻어냅니다. 위의 코드를 다시 살펴보겠습니다.

SimpleAgent 클래스는 에이전트의 가장 기본적인 구조를 보여줍니다. __init__ 메서드에서 LLM과 사용 가능한 도구들을 받습니다.

여기서 도구란 검색 엔진, 계산기, 데이터베이스 쿼리 등 에이전트가 사용할 수 있는 모든 기능을 의미합니다. run 메서드가 핵심입니다.

먼저 observe로 현재 상황을 파악합니다. 그다음 LLM이 어떤 행동을 할지 결정합니다.

그 행동을 실행하고, 작업이 완료되지 않았다면 다시 처음부터 반복합니다. 이것이 바로 에이전트 루프입니다.

실제 서비스에서는 어떻게 활용될까요? 고객 지원 시스템을 예로 들어봅시다.

고객이 "지난달 구매한 노트북 환불하고 싶어요"라고 문의했습니다. 에이전트는 먼저 주문 데이터베이스를 검색하여 해당 주문을 찾습니다.

그다음 환불 정책을 확인하고, 가능하다면 환불 프로세스를 시작하며, 고객에게 진행 상황을 안내합니다. 모든 과정이 자동으로 이루어집니다.

네이버, 카카오 같은 국내 대기업들도 이미 에이전트 기술을 적극 도입하고 있습니다. 고객 문의 처리, 데이터 분석, 콘텐츠 생성 등 다양한 분야에서 활용되고 있습니다.

하지만 주의할 점도 있습니다. 에이전트는 강력하지만, 그만큼 예측 불가능성도 높습니다.

LLM이 잘못된 판단을 내릴 수도 있고, 도구를 잘못 사용할 수도 있습니다. 따라서 중요한 작업의 경우 반드시 사람의 승인을 받도록 설계해야 합니다.

또한 무한 루프에 빠지지 않도록 최대 실행 횟수를 제한하는 것도 중요합니다. 다음 날 아침, 김민수 씨는 자신감 있게 팀장님께 대답했습니다.

"저희가 만든 건 아직 챗봇 수준입니다. 하지만 도구 사용 기능을 추가하면 진짜 에이전트로 발전시킬 수 있습니다." 에이전트의 핵심은 자율성입니다.

단순히 답변하는 것이 아니라, 목표를 달성하기 위해 스스로 행동하는 시스템. 이것이 바로 AI 에이전트입니다.

실전 팁

💡 - 에이전트 구현 시 항상 최대 실행 횟수 제한을 두어 무한 루프를 방지하세요

  • 중요한 작업은 에이전트가 자동으로 실행하기 전에 사용자 승인을 받도록 설계하세요
  • 에이전트의 모든 행동을 로그로 기록하여 디버깅과 개선에 활용하세요

2. Perception-Reasoning-Action 사이클

에이전트에 대해 공부하던 김민수 씨는 기술 블로그에서 자주 등장하는 용어를 발견했습니다. "PRA 사이클", "Perception-Reasoning-Action".

무슨 뜻일까요? 선배 박지혜 씨에게 물어보니 "에이전트가 작동하는 핵심 메커니즘"이라고 설명해줬습니다.

PRA 사이클은 에이전트가 작동하는 기본 원리입니다. **인식(Perception)**으로 상황을 파악하고, **추론(Reasoning)**으로 다음 행동을 결정하며, **행동(Action)**으로 도구를 실행합니다.

이 세 단계가 목표 달성까지 계속 반복됩니다. 마치 사람이 문제를 해결할 때 "무슨 일이지?" → "어떻게 하지?" → "실행!" 과정을 거치는 것과 같습니다.

다음 코드를 살펴봅시다.

def pra_cycle(agent, initial_task, max_iterations=10):
    current_state = initial_task

    for i in range(max_iterations):
        # Perception: 현재 상황 인식
        observation = agent.perceive(current_state)
        print(f"[인식] {observation}")

        # Reasoning: 다음 행동 추론
        thought = agent.reason(observation)
        action = agent.decide_action(thought)
        print(f"[추론] {thought}")
        print(f"[계획] {action}")

        # Action: 행동 실행
        result = agent.act(action)
        print(f"[행동] {result}")

        # 목표 달성 여부 확인
        if agent.is_goal_achieved(result):
            return result
        current_state = result

    return "최대 반복 횟수 도달"

박지혜 씨는 김민수 씨를 회의실로 데려가 화이트보드에 그림을 그리기 시작했습니다. 원 세 개를 그리고 화살표로 연결했습니다.

"에이전트의 작동 원리는 생각보다 단순해요." 첫 번째 원에 Perception이라고 썼습니다. "먼저 에이전트는 현재 상황을 인식해야 합니다.

사용자가 무엇을 요청했는지, 지금까지 어떤 작업을 수행했는지, 어떤 정보가 있는지를 파악하는 단계입니다." 박지혜 씨가 설명했습니다. 쉽게 비유하자면, 인식은 눈을 뜨고 주변을 살피는 것과 같습니다.

아침에 일어났을 때 우리는 자연스럽게 주변을 둘러봅니다. 몇 시인지, 날씨는 어떤지, 오늘 일정이 뭔지 확인합니다.

에이전트도 마찬가지입니다. 현재 컨텍스트를 이해하는 것이 모든 것의 시작입니다.

두 번째 원에 Reasoning을 적었습니다. "인식한 정보를 바탕으로 다음에 무엇을 할지 생각하는 단계입니다.

이 부분이 바로 LLM의 핵심 역할이에요." 박지혜 씨가 덧붙였습니다. 사람으로 치면 생각하는 과정입니다.

배가 고프다는 것을 인식했다면, 다음은 무엇을 먹을지, 어디서 먹을지, 요리할지 배달 시킬지 결정해야 합니다. 에이전트도 현재 상황을 분석하고, 가능한 선택지를 고려하며, 최선의 행동을 선택합니다.

세 번째 원에 Action을 썼습니다. "마지막으로 결정한 행동을 실제로 실행합니다.

도구를 호출하고, 결과를 받아옵니다." 박지혜 씨는 세 개의 원을 다시 화살표로 연결하며 순환 구조를 강조했습니다. "그리고 이 과정이 목표를 달성할 때까지 계속 반복됩니다." 전통적인 프로그래밍과 무엇이 다를까요?

일반적인 프로그램은 정해진 순서대로 명령을 실행합니다. A를 하고, B를 하고, C를 합니다.

하지만 에이전트는 동적입니다. 상황에 따라 다음 행동이 달라집니다.

같은 질문이라도 사용자의 이전 대화 내용에 따라 다르게 반응할 수 있습니다. 이런 유연성이 어떻게 가능할까요?

바로 LLM의 추론 능력 덕분입니다. LLM은 자연어로 된 상황 설명을 받아 다음 행동을 텍스트로 생성합니다.

"검색 도구를 사용하여 최신 환율을 조회하라" 같은 형태로 말이죠. 이 텍스트를 파싱하여 실제 함수 호출로 변환합니다.

위의 코드를 단계별로 살펴보겠습니다. pra_cycle 함수는 PRA 사이클의 구체적인 구현입니다.

max_iterations 파라미터로 최대 반복 횟수를 제한합니다. 이것이 없으면 에이전트가 영원히 멈추지 않을 수 있습니다.

루프 안에서 세 단계가 순차적으로 실행됩니다. perceive로 현재 상태를 관찰하고, reasondecide_action으로 다음 행동을 결정하며, act로 실제 행동을 수행합니다.

각 단계마다 print로 출력하여 에이전트의 사고 과정을 투명하게 보여줍니다. is_goal_achieved로 목표 달성 여부를 확인합니다.

목표를 달성했다면 즉시 결과를 반환하고, 그렇지 않다면 current_state를 업데이트하여 다음 사이클로 진입합니다. 실제 서비스에서는 어떻게 작동할까요?

여행 계획 에이전트를 예로 들어봅시다. 사용자가 "다음 달 제주도 2박 3일 여행 계획 짜줘"라고 요청했습니다.

1차 사이클: 인식 - "제주도 여행 계획 요청, 기간은 2박 3일". 추론 - "먼저 날씨 정보가 필요하다".

행동 - "날씨 API 호출". 2차 사이클: 인식 - "다음 달 제주도 날씨는 맑음, 평균 기온 25도".

추론 - "관광지 추천이 필요하다". 행동 - "제주도 인기 관광지 검색".

3차 사이클: 인식 - "한라산, 성산일출봉, 우도 등 추천". 추론 - "일정표를 작성하자".

행동 - "2박 3일 일정표 생성". 이런 식으로 계속 사이클이 반복되며 최종 여행 계획이 완성됩니다.

주의할 점이 있습니다. 무한 루프 방지가 가장 중요합니다.

에이전트가 같은 행동을 반복하거나, 목표 달성 조건을 제대로 인식하지 못하면 영원히 멈추지 않을 수 있습니다. 따라서 반드시 최대 반복 횟수를 설정하고, 각 사이클마다 진전이 있는지 확인해야 합니다.

김민수 씨는 고개를 끄덕였습니다. "그러니까 에이전트는 이 세 단계를 계속 돌면서 점점 목표에 가까워지는 거군요." "정확해요!" 박지혜 씨가 미소 지었습니다.

"사람이 문제를 해결하는 방식과 똑같습니다. 인식하고, 생각하고, 행동하기.

그리고 다시 인식하고, 생각하고, 행동하기."

실전 팁

💡 - 각 사이클마다 에이전트의 사고 과정을 로그로 출력하여 디버깅을 쉽게 만드세요

  • 같은 행동을 반복하는지 감지하는 무한 루프 탐지 로직을 추가하세요
  • 목표 달성 조건을 명확하게 정의하고, 중간 체크포인트를 설정하여 진행 상황을 추적하세요

3. 에이전트 vs 단순 LLM 호출

월요일 아침, 김민수 씨는 주간 회의에서 발표를 준비하고 있었습니다. 주제는 "우리 서비스에 AI를 어떻게 적용할 것인가".

그런데 팀원 중 한 명이 이렇게 질문했습니다. "그냥 ChatGPT API 호출하면 되는 거 아닌가요?

굳이 에이전트를 만들어야 하나요?" 김민수 씨는 이 질문에 명확히 답해야 했습니다.

단순 LLM 호출은 질문을 던지면 답변을 받는 일회성 상호작용입니다. 반면 에이전트는 목표 달성까지 여러 번의 LLM 호출과 도구 사용을 반복합니다.

비유하자면, 단순 LLM 호출은 백과사전에 질문하는 것이고, 에이전트는 개인 비서에게 일을 맡기는 것입니다. 전자는 정보를 제공하고, 후자는 문제를 해결합니다.

다음 코드를 살펴봅시다.

# 단순 LLM 호출
def simple_llm_call(question):
    response = llm.generate(question)
    return response

# 에이전트 방식
class TaskAgent:
    def __init__(self, llm, tools):
        self.llm = llm
        self.tools = tools  # 검색, 계산, DB 쿼리 등

    def solve(self, task):
        steps = []
        while not self.is_complete(task, steps):
            # LLM에게 다음 행동 물어보기
            next_action = self.llm.plan_next_action(task, steps)
            # 도구 실행
            result = self.execute_tool(next_action)
            steps.append(result)
        return self.synthesize_answer(steps)

# 차이를 보여주는 예시
# LLM: "서울 날씨는 맑습니다" (지식 기반 추측)
# 에이전트: [날씨 API 호출] → "현재 서울 날씨는 14도, 맑음" (실시간 정보)

김민수 씨는 화이트보드에 두 개의 다이어그램을 그렸습니다. 왼쪽에는 간단한 화살표 하나, 오른쪽에는 복잡한 순환 구조를 그렸습니다.

"왼쪽이 단순 LLM 호출, 오른쪽이 에이전트입니다." 김민수 씨가 설명을 시작했습니다. 단순 LLM 호출은 정말 간단합니다.

사용자가 질문을 던지면, LLM이 학습된 지식을 바탕으로 답변을 생성합니다. 한 번의 입력, 한 번의 출력.

그게 전부입니다. 블로그 글을 요약해달라고 하면 요약을 해주고, 코드 설명을 부탁하면 설명을 해줍니다.

하지만 여기에는 한계가 있습니다. 첫째, LLM은 실시간 정보를 알 수 없습니다.

"지금 비트코인 가격이 얼마야?"라고 물어보면 학습 데이터에 있는 과거 정보를 바탕으로 추측할 뿐입니다. 실제 현재 가격을 알려면 API를 호출해야 하는데, 단순 LLM 호출은 그럴 수 없습니다.

둘째, 복잡한 작업을 수행할 수 없습니다. "지난 분기 매출 데이터를 분석하고, 차트를 만들고, 보고서를 작성해줘"라는 요청에 LLM은 텍스트로 된 설명만 제공합니다.

실제로 데이터를 가져오거나 차트를 만들지는 못합니다. 셋째, 다단계 추론이 어렵습니다.

한 번의 응답 생성에서 모든 것을 해결해야 하기 때문에, 중간에 정보를 확인하고 방향을 수정하는 것이 불가능합니다. 반면 에이전트는 완전히 다릅니다.

에이전트는 도구를 가지고 있습니다. 검색 엔진, 계산기, 데이터베이스, 외부 API 등 무엇이든 도구로 연결할 수 있습니다.

필요할 때 이 도구들을 꺼내 쓰면서 문제를 해결합니다. 쉽게 비유하자면, 단순 LLM 호출은 책에 질문하는 것과 같습니다.

책은 이미 쓰인 내용만 알려줄 수 있습니다. 반면 에이전트는 비서에게 일을 맡기는 것입니다.

비서는 전화도 하고, 인터넷도 검색하며, 필요한 일들을 처리합니다. 위의 코드를 비교해봅시다.

simple_llm_call 함수는 정말 단순합니다. 질문을 받아 LLM에 던지고, 답변을 그대로 반환합니다.

한 줄이면 끝입니다. TaskAgent 클래스는 훨씬 복잡합니다.

tools 속성에 여러 도구를 가지고 있고, solve 메서드에서 반복문을 돌며 작업을 완료할 때까지 계속 진행합니다. 각 반복마다 LLM에게 다음 행동을 물어보고, 해당 도구를 실행하며, 결과를 누적합니다.

실제 시나리오로 차이를 느껴봅시다. 시나리오: "우리 회사 주식 가격이 경쟁사보다 높은지 알려줘" 단순 LLM 호출: - 사용자: "우리 회사 주식 가격이 경쟁사보다 높은지 알려줘" - LLM: "죄송합니다.

실시간 주식 가격 정보는 제공할 수 없습니다. 증권사 앱이나 포털 사이트에서 확인해주세요." 에이전트 방식: - 사용자: "우리 회사 주식 가격이 경쟁사보다 높은지 알려줘" - 에이전트: [주식 API 호출] "A사 주식: 50,000원" - 에이전트: [주식 API 호출] "경쟁사 B사 주식: 45,000원" - 에이전트: "네, A사 주식 가격(50,000원)이 B사(45,000원)보다 5,000원 높습니다." 차이가 명확합니다.

단순 LLM은 할 수 없다고 말하지만, 에이전트는 실제로 문제를 해결합니다. 또 다른 예시를 봅시다.

시나리오: "내일 회의 자료 만들어줘" 단순 LLM: "회의 자료에는 다음 내용이 포함되어야 합니다: 1. 제목 슬라이드 2.

안건 3. 데이터 분석 4.

결론..." 에이전트:


5. "회의 자료가 준비되었습니다. meeting 20240315.pptx 파일을 확인해주세요."

실전 팁

💡 - 단순 Q&A나 텍스트 처리는 단순 LLM 호출로 충분하며 비용도 저렴합니다

  • 외부 데이터 접근, 다단계 작업, 실시간 정보가 필요하면 에이전트를 구현하세요
  • 처음에는 단순 LLM으로 시작하고, 필요에 따라 점진적으로 에이전트 기능을 추가하는 것이 좋습니다

4. 에이전트 아키텍처 패턴

에이전트 개발을 시작한 김민수 씨는 곧 벽에 부딪혔습니다. 에이전트가 너무 자주 엉뚱한 행동을 하거나, 같은 실수를 반복했습니다.

선배 박지혜 씨에게 코드를 보여주니 이렇게 말했습니다. "민수 씨, 에이전트 아키텍처 패턴을 공부해보세요.

검증된 패턴을 따르면 훨씬 안정적인 에이전트를 만들 수 있어요."

에이전트 아키텍처 패턴은 에이전트를 설계하는 검증된 방법론입니다. 대표적으로 ReAct 패턴(Reasoning + Acting), Plan-and-Execute 패턴, Reflexion 패턴 등이 있습니다.

ReAct는 생각과 행동을 번갈아 수행하고, Plan-and-Execute는 먼저 전체 계획을 세운 후 실행하며, Reflexion은 실패를 통해 학습합니다. 각 패턴은 서로 다른 상황에 적합합니다.

다음 코드를 살펴봅시다.

# ReAct 패턴: 가장 널리 사용되는 패턴
class ReActAgent:
    def run(self, task):
        history = []
        for step in range(self.max_steps):
            # Thought: 현재 상황에 대해 생각하기
            thought = self.think(task, history)
            print(f"생각: {thought}")

            # Action: 다음 행동 결정
            action = self.decide_action(thought)
            print(f"행동: {action}")

            # Observation: 행동 결과 관찰
            observation = self.execute(action)
            print(f"관찰: {observation}")

            history.append({
                "thought": thought,
                "action": action,
                "observation": observation
            })

            # 완료 여부 확인
            if self.is_finished(observation):
                return self.generate_final_answer(history)

박지혜 씨는 김민수 씨를 다시 회의실로 불렀습니다. 화이트보드에 세 개의 큰 박스를 그렸습니다.

"에이전트 아키텍처 패턴은 크게 세 가지로 나눌 수 있어요." 첫 번째 박스에 ReAct라고 썼습니다. "ReAct는 Reasoning(추론)과 Acting(행동)을 결합한 패턴입니다." 박지혜 씨가 설명했습니다.

"구글 딥마인드와 프린스턴 대학이 2022년에 발표한 논문에서 제안됐어요. 가장 널리 사용되는 패턴이죠." ReAct의 핵심은 생각과 행동을 번갈아 수행하는 것입니다.

먼저 현재 상황에 대해 생각합니다(Thought). "지금 뭘 해야 하지?

어떤 정보가 필요하지?" 그다음 행동을 결정하고 실행합니다(Action). "검색 도구를 사용하자." 마지막으로 결과를 관찰합니다(Observation).

"검색 결과가 나왔구나." 이 과정을 반복하며 문제를 해결합니다. 쉽게 비유하자면, ReAct는 탐정이 사건을 해결하는 방식과 같습니다.

단서를 발견하면 그것에 대해 생각하고, 다음 행동을 결정하며, 새로운 정보를 얻습니다. 그리고 다시 생각하고, 행동하고, 관찰합니다.

최종적으로 모든 단서를 종합하여 사건을 해결합니다. 두 번째 박스에 Plan-and-Execute를 적었습니다.

"이 패턴은 먼저 전체 계획을 세운 다음 순서대로 실행합니다." 박지혜 씨가 말했습니다. "요리를 할 때 레시피를 먼저 읽고 재료를 준비한 다음 순서대로 요리하는 것과 비슷해요." Plan-and-Execute는 두 단계로 나뉩니다.

Planning 단계: LLM에게 작업을 주면 전체 실행 계획을 세웁니다. "1단계: 데이터 수집, 2단계: 데이터 분석, 3단계: 보고서 작성" 같은 식으로 구조화된 계획을 만듭니다.

Execution 단계: 계획을 순서대로 실행합니다. 각 단계마다 필요한 도구를 사용하고, 결과를 다음 단계로 전달합니다.

이 패턴의 장점은 예측 가능성입니다. 미리 계획을 보고 검토할 수 있습니다.

단점은 유연성 부족입니다. 중간에 예상치 못한 상황이 발생하면 대응하기 어렵습니다.

세 번째 박스에 Reflexion을 썼습니다. "Reflexion은 실패로부터 배우는 패턴입니다." 박지혜 씨가 설명했습니다.

"사람이 시행착오를 통해 배우는 것처럼, 에이전트도 실패를 분석하고 개선합니다." Reflexion의 핵심은 자기 반성입니다. 에이전트가 작업을 수행한 후, 결과가 만족스럽지 않다면 무엇이 잘못됐는지 분석합니다.

"왜 실패했지? 어떤 부분을 개선해야 하지?" 그리고 이 반성 내용을 메모리에 저장합니다.

다음에 비슷한 작업을 할 때 이전 실패 경험을 참고하여 더 나은 결과를 만들어냅니다. 예를 들어 코드 생성 에이전트가 있다고 봅시다.

첫 번째 시도에서 생성한 코드에 버그가 있었습니다. Reflexion 패턴을 사용하는 에이전트는 "엣지 케이스를 고려하지 않아서 실패했다"라고 분석하고, 두 번째 시도에서는 엣지 케이스를 포함한 코드를 생성합니다.

위의 ReAct 코드를 자세히 살펴봅시다. run 메서드가 ReAct 패턴의 전체 흐름을 보여줍니다.

history 리스트에 모든 단계를 기록합니다. 이것이 중요한 이유는 LLM이 이전 단계들을 참고하여 다음 행동을 결정하기 때문입니다.

각 반복마다 세 가지 작업이 순차적으로 일어납니다. think로 현재 상황을 분석하고, decide_action으로 다음 행동을 결정하며, execute로 실제로 실행합니다.

모든 단계가 print로 출력되어 에이전트의 사고 과정이 투명하게 드러납니다. is_finished로 작업 완료 여부를 확인하고, 완료됐다면 generate_final_answer로 최종 답변을 생성합니다.

이때 전체 history를 참고하여 종합적인 답변을 만듭니다. 실제 프로젝트에서는 어떤 패턴을 선택해야 할까요?

ReAct 패턴은 대부분의 상황에 적합합니다. 유연하고 강력하며 구현도 상대적으로 간단합니다.

처음 에이전트를 만든다면 ReAct부터 시작하세요. Plan-and-Execute 패턴은 복잡하지만 명확한 작업에 좋습니다.

데이터 파이프라인, 보고서 생성, 워크플로우 자동화 같은 경우에 적합합니다. Reflexion 패턴은 품질이 중요한 작업에 유용합니다.

코드 생성, 글쓰기, 창작 작업 등에서 여러 번 반복하며 결과를 개선할 수 있습니다. 주의할 점도 있습니다.

패턴을 맹목적으로 따르지 마세요. 여러분의 문제에 맞게 커스터마이징하는 것이 중요합니다.

예를 들어 ReAct와 Reflexion을 결합할 수도 있습니다. 각 ReAct 사이클이 끝날 때마다 반성 단계를 추가하는 식으로 말이죠.

김민수 씨는 노트에 열심히 필기했습니다. "그럼 저는 일단 ReAct 패턴으로 시작해볼게요.

가장 범용적이니까요." "좋은 선택이에요." 박지혜 씨가 미소 지었습니다. "패턴은 도구일 뿐입니다.

중요한 건 여러분의 문제를 해결하는 거예요."

실전 팁

💡 - 처음 에이전트를 만든다면 ReAct 패턴부터 시작하세요 - 가장 범용적이고 구현이 쉽습니다

  • 각 단계의 사고 과정을 로그로 남겨 디버깅과 개선에 활용하세요
  • 여러 패턴을 조합하는 것도 가능합니다 - 예: ReAct + Reflexion

5. 실습 간단한 에이전트 구현

드디어 실습 시간입니다. 김민수 씨는 노트북을 열고 VS Code를 켰습니다.

"이론은 충분히 배웠으니, 이제 직접 만들어보자." 손가락이 키보드 위에서 잠시 머뭇거렸습니다. 어디서부터 시작해야 할까?

박지혜 씨가 옆에서 말했습니다. "간단한 것부터 시작해요.

검색 기능이 있는 Q&A 에이전트를 만들어봅시다."

간단한 에이전트를 직접 구현해봅니다. 도구 정의, LLM 연결, ReAct 루프 구현의 세 단계로 진행됩니다.

실제 작동하는 코드를 통해 에이전트가 어떻게 도구를 선택하고 실행하는지 배웁니다. 복잡해 보이지만 핵심 개념만 이해하면 100줄 이내로 구현 가능합니다.

다음 코드를 살펴봅시다.

import openai
import requests

# 1. 도구 정의
def search_web(query):
    """웹 검색 도구"""
    # 실제로는 Google API나 Bing API 사용
    return f"'{query}'에 대한 검색 결과입니다."

def calculate(expression):
    """계산기 도구"""
    try:
        return str(eval(expression))
    except:
        return "계산 오류"

# 2. 도구 목록
TOOLS = {
    "search_web": search_web,
    "calculate": calculate
}

# 3. 간단한 ReAct 에이전트
class SimpleReActAgent:
    def __init__(self, api_key):
        self.client = openai.OpenAI(api_key=api_key)

    def run(self, question):
        history = f"질문: {question}\n\n"

        for step in range(5):  # 최대 5단계
            # LLM에게 다음 행동 물어보기
            prompt = f"""{history}
사용 가능한 도구:
- search_web(query): 웹 검색
- calculate(expression): 계산
- FINISH(answer): 최종 답변

다음 형식으로 답하세요:
생각: [현재 상황 분석]
행동: [도구명(인자)] 또는 FINISH(답변)
"""

            response = self.client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": prompt}]
            )

            output = response.choices[0].message.content
            history += output + "\n"

            # FINISH 확인
            if "FINISH" in output:
                # 최종 답변 추출
                answer = output.split("FINISH(")[1].split(")")[0]
                return answer

            # 도구 실행
            if "행동:" in output:
                action_line = output.split("행동:")[1].strip()
                tool_name = action_line.split("(")[0]
                tool_arg = action_line.split("(")[1].split(")")[0]

                if tool_name in TOOLS:
                    result = TOOLS[tool_name](tool_arg)
                    history += f"관찰: {result}\n\n"

        return "최대 단계 도달"

# 사용 예시
agent = SimpleReActAgent(api_key="your-api-key")
answer = agent.run("2024년 파리 올림픽 금메달 수는?")
print(answer)

김민수 씨는 빈 파이썬 파일을 열고 주석을 달기 시작했습니다. "먼저 구조를 잡아야지." 박지혜 씨가 옆에서 지켜보며 조언했습니다.

"에이전트 구현은 크게 세 부분으로 나뉩니다." 박지혜 씨가 말했습니다. 첫 번째는 도구 정의입니다. 에이전트가 사용할 수 있는 도구들을 함수로 만듭니다.

위 코드에서는 search_webcalculate 두 개의 도구를 정의했습니다. 실제 프로젝트에서는 데이터베이스 쿼리, API 호출, 파일 읽기 등 다양한 도구를 추가할 수 있습니다.

중요한 점은 함수 시그니처를 명확하게 만드는 것입니다. 함수명, 파라미터, 반환값이 직관적이어야 LLM이 올바르게 사용할 수 있습니다.

search_web(query)는 명확합니다. 하지만 do_something(x, y, z)는 LLM이 이해하기 어렵습니다.

두 번째는 LLM 연결입니다. OpenAI API를 사용하여 GPT 모델과 통신합니다. SimpleReActAgent 클래스의 __init__ 메서드에서 클라이언트를 초기화합니다.

실제로는 환경변수에서 API 키를 읽어오는 것이 안전합니다. LLM과의 통신은 chat.completions.create 메서드로 이뤄집니다.

프롬프트를 보내면 LLM이 다음 행동을 텍스트로 생성합니다. "생각: ..., 행동: ..." 형식으로 구조화된 출력을 받습니다.

세 번째는 ReAct 루프 구현입니다. run 메서드가 전체 로직을 담고 있습니다. history 변수에 모든 대화 내용을 누적합니다.

이것이 중요한 이유는 LLM이 컨텍스트를 이해하려면 이전 단계들을 알아야 하기 때문입니다. 루프는 최대 5번 반복됩니다.

무한 루프 방지를 위한 안전장치입니다. 각 반복마다 프롬프트를 구성하여 LLM에게 보냅니다.

프롬프트에는 질문, 히스토리, 사용 가능한 도구 목록이 포함됩니다. LLM의 응답을 파싱하는 부분이 핵심입니다.

먼저 "FINISH"가 있는지 확인합니다. 있다면 에이전트가 최종 답변을 생성했다는 의미입니다.

괄호 안의 내용을 추출하여 반환합니다. FINISH가 없다면 "행동:" 부분을 찾습니다.

여기서 도구명과 인자를 추출합니다. 예를 들어 "행동: search_web(파리 올림픽 2024)"라면 tool_name은 "search_web", tool_arg는 "파리 올림픽 2024"가 됩니다.

도구를 실행하고 결과를 history에 추가합니다. 다음 반복에서 LLM은 이 결과를 보고 다음 행동을 결정합니다.

실제로 어떻게 작동하는지 시뮬레이션해봅시다. 사용자: "2024년 파리 올림픽 금메달 수는?" 1차 반복: - LLM 생각: "현재 날짜 정보가 필요하다.

웹 검색을 하자." - LLM 행동: "search_web(2024 파리 올림픽 금메달)" - 도구 실행: "'2024 파리 올림픽 금메달'에 대한 검색 결과입니다." - 관찰 추가 2차 반복: - LLM 생각: "검색 결과를 받았다. 이제 답변할 수 있다." - LLM 행동: "FINISH(2024년 파리 올림픽에서 한국은 금메달 13개를 획득했습니다.)" - 최종 답변 반환 이런 식으로 에이전트가 스스로 필요한 정보를 찾고 답변을 생성합니다.

김민수 씨는 코드를 실행해보며 감탄했습니다. "와, 진짜 작동하네요!" 하지만 박지혜 씨가 주의사항을 알려줬습니다.

"이 코드는 데모용입니다. 실제 프로덕션에서는 많은 개선이 필요해요." 박지혜 씨가 말했습니다.

첫째, 에러 처리가 부족합니다. LLM이 잘못된 형식으로 응답하거나, 도구 실행이 실패할 수 있습니다.

try-except로 감싸야 합니다. 둘째, 파싱 로직이 취약합니다.

문자열 split으로 파싱하는 것은 불안정합니다. LLM에게 JSON 형식으로 응답하도록 요청하고, json.loads로 파싱하는 것이 안전합니다.

셋째, 도구 설명이 부족합니다. 프롬프트에 각 도구가 정확히 무엇을 하는지, 어떤 인자를 받는지 자세히 설명해야 LLM이 올바르게 사용합니다.

넷째, 비용과 속도 문제입니다. 매 단계마다 LLM을 호출하므로 비용이 증가합니다.

가능하면 캐싱을 사용하고, 빠른 모델(GPT-3.5)을 먼저 시도해보세요. 그럼에도 불구하고 이 100줄의 코드는 에이전트의 핵심을 모두 담고 있습니다.

도구 정의, LLM 연결, ReAct 루프. 이 세 가지만 이해하면 훨씬 복잡한 에이전트도 만들 수 있습니다.

김민수 씨는 자신감이 생겼습니다. "이제 우리 서비스에 맞게 커스터마이징하면 되겠네요." "정확합니다!" 박지혜 씨가 엄지를 치켜세웠습니다.

"시작이 반입니다."

실전 팁

💡 - 도구 함수는 명확한 이름과 독스트링을 가져야 LLM이 올바르게 사용합니다

  • LLM 응답은 JSON 형식으로 받는 것이 파싱하기 쉽고 안전합니다
  • 프롬프트에 Few-shot 예시를 추가하면 LLM의 응답 품질이 크게 향상됩니다

6. 실습 에이전트 동작 분석

에이전트를 만들고 실행해본 김민수 씨는 새로운 고민에 빠졌습니다. 에이전트가 가끔 이상한 행동을 합니다.

같은 질문을 반복하거나, 엉뚱한 도구를 사용하기도 합니다. "어떻게 디버깅해야 하지?" 박지혜 씨가 말했습니다.

"에이전트의 동작을 분석하는 방법을 알려줄게요. 관찰 가능성이 핵심입니다."

에이전트 디버깅의 핵심은 관찰 가능성입니다. 에이전트의 모든 사고 과정을 로그로 남기고, 각 단계를 시각화하며, 실패 패턴을 분석합니다.

로깅, 트레이싱, 메트릭 수집을 통해 에이전트가 왜 그런 행동을 했는지 이해하고 개선할 수 있습니다.

다음 코드를 살펴봅시다.

import logging
import json
from datetime import datetime

# 1. 상세한 로깅 설정
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('agent.log'),
        logging.StreamHandler()
    ]
)

class ObservableAgent:
    def __init__(self, llm, tools):
        self.llm = llm
        self.tools = tools
        self.trace = []  # 전체 실행 추적

    def run(self, task):
        session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
        logging.info(f"[세션 시작] {session_id}: {task}")

        for step_num in range(10):
            step_trace = {"step": step_num, "timestamp": datetime.now().isoformat()}

            # 인식
            observation = self.perceive(task, self.trace)
            step_trace["observation"] = observation
            logging.info(f"[인식] {observation}")

            # 추론
            thought = self.reason(observation)
            step_trace["thought"] = thought
            logging.info(f"[추론] {thought}")

            # 행동 결정
            action = self.decide_action(thought)
            step_trace["action"] = action
            logging.info(f"[행동] {action}")

            # 실행 및 측정
            start_time = datetime.now()
            try:
                result = self.execute(action)
                step_trace["result"] = result
                step_trace["success"] = True
            except Exception as e:
                logging.error(f"[오류] {str(e)}")
                step_trace["result"] = str(e)
                step_trace["success"] = False

            # 실행 시간 기록
            duration = (datetime.now() - start_time).total_seconds()
            step_trace["duration"] = duration

            self.trace.append(step_trace)

            # 완료 여부 확인
            if self.is_complete(result):
                self.save_trace(session_id)
                return result

        logging.warning("[경고] 최대 단계 도달")
        self.save_trace(session_id)
        return None

    def save_trace(self, session_id):
        """전체 실행 과정을 JSON으로 저장"""
        filename = f"traces/trace_{session_id}.json"
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(self.trace, f, ensure_ascii=False, indent=2)
        logging.info(f"[추적 저장] {filename}")

    def analyze_trace(self):
        """실행 패턴 분석"""
        total_steps = len(self.trace)
        successful_steps = sum(1 for s in self.trace if s.get("success"))
        avg_duration = sum(s.get("duration", 0) for s in self.trace) / total_steps

        return {
            "total_steps": total_steps,
            "successful_steps": successful_steps,
            "success_rate": successful_steps / total_steps,
            "avg_duration": avg_duration
        }

박지혜 씨는 김민수 씨의 노트북 화면을 보며 고개를 저었습니다. "민수 씨, 에이전트가 뭘 하는지 전혀 모르겠어요.

로그가 하나도 없잖아요." 김민수 씨는 당황했습니다. "그냥 실행하면 되는 거 아닌가요?" "에이전트는 블랙박스가 아닙니다." 박지혜 씨가 강조했습니다.

"모든 사고 과정을 투명하게 만들어야 합니다." 관찰 가능성이란 무엇일까요? 시스템 내부에서 무슨 일이 일어나는지 외부에서 알 수 있게 만드는 것입니다.

마치 자동차 계기판과 같습니다. 속도계, 연료계, 경고등이 없다면 차가 제대로 작동하는지 알 수 없습니다.

에이전트도 마찬가지입니다. 에이전트의 관찰 가능성은 세 가지 요소로 구성됩니다.

첫째, 로깅입니다. 모든 중요한 이벤트를 기록합니다.

위 코드에서는 Python의 logging 모듈을 사용합니다. 파일과 콘솔에 동시에 출력하여 실시간으로도 보고, 나중에 분석할 수도 있습니다.

로그는 구조화되어야 합니다. "뭔가 일어남"보다는 "[인식] 사용자 질문: 날씨가 어때?"처럼 명확하게 작성합니다.

타임스탬프, 로그 레벨, 단계 정보가 포함되어야 합니다. 둘째, 트레이싱입니다.

전체 실행 과정을 추적합니다. self.trace 리스트에 각 단계의 모든 정보를 저장합니다.

나중에 JSON 파일로 저장하여 자세히 분석할 수 있습니다. 트레이스는 재현 가능성을 제공합니다.

버그가 발생했을 때 정확히 어느 단계에서 무슨 일이 일어났는지 재구성할 수 있습니다. 마치 비행기의 블랙박스처럼 모든 것이 기록됩니다.

셋째, 메트릭 수집입니다. 성능 지표를 측정합니다.

각 단계가 얼마나 걸렸는지, 성공률은 얼마인지, 평균 단계 수는 몇 개인지 등을 기록합니다. 위의 코드를 단계별로 살펴봅시다.

ObservableAgent 클래스는 관찰 가능한 에이전트입니다. __init__에서 self.trace 리스트를 초기화합니다.

여기에 모든 실행 정보가 누적됩니다. run 메서드 시작 부분에서 세션 ID를 생성합니다.

타임스탬프를 사용하여 각 실행을 구분합니다. "20240315_143052"처럼 언제 실행됐는지 명확합니다.

각 단계마다 step_trace 딕셔너리를 만듭니다. 관찰, 추론, 행동, 결과를 모두 기록합니다.

logging.info로 실시간 로그도 출력합니다. 중요한 부분은 에러 처리입니다.

try-except로 도구 실행을 감쌉니다. 에러가 발생하면 로그에 기록하고, step_trace에 실패 정보를 저장합니다.

에러 때문에 전체 에이전트가 멈추지 않도록 합니다. 실행 시간 측정도 중요합니다.

start_time을 기록하고, 완료 후 duration을 계산합니다. 어떤 도구가 느린지, 어느 단계에서 시간이 많이 걸리는지 파악할 수 있습니다.

save_trace 메서드는 전체 추적 정보를 JSON 파일로 저장합니다. 나중에 분석 도구로 시각화하거나, 다른 실행과 비교할 수 있습니다.

analyze_trace 메서드는 간단한 통계를 계산합니다. 총 단계 수, 성공 단계 수, 성공률, 평균 실행 시간을 반환합니다.

실제로 이런 정보가 어떻게 도움이 될까요? 김민수 씨의 에이전트가 자주 실패한다고 가정해봅시다.

트레이스를 분석해보니 특정 도구에서 항상 에러가 발생합니다. 알고 보니 API 키가 잘못 설정되어 있었습니다.

로그가 없었다면 찾기 어려웠을 문제입니다. 또 다른 예시입니다.

에이전트가 너무 느립니다. 메트릭을 확인해보니 검색 도구가 평균 5초씩 걸립니다.

검색 엔진을 더 빠른 것으로 바꾸거나, 캐싱을 추가하는 개선을 할 수 있습니다. 추가적인 개선 방법도 있습니다.

시각화 대시보드를 만들 수 있습니다. Streamlit이나 Gradio로 간단한 웹 페이지를 만들어 트레이스를 그래프로 보여줍니다.

각 단계를 플로우차트로 표시하면 한눈에 이해하기 쉽습니다. A/B 테스트도 가능합니다.

프롬프트를 바꿨을 때 성능이 좋아지는지, 나빠지는지 정량적으로 비교할 수 있습니다. 메트릭이 있으니까 객관적 판단이 가능합니다.

이상 탐지도 추가할 수 있습니다. 평소와 다르게 너무 많은 단계를 거치거나, 같은 행동을 반복하면 경고를 보냅니다.

김민수 씨는 코드를 수정하여 로깅을 추가했습니다. 다시 실행해보니 에이전트가 무엇을 하는지 명확하게 보였습니다.

"이제야 제대로 보이네요!" 박지혜 씨가 웃으며 말했습니다. "에이전트를 만드는 것만큼 관찰하는 것도 중요합니다.

볼 수 없으면 개선할 수 없어요." 관찰 가능성은 선택이 아닌 필수입니다. 처음부터 로깅과 트레이싱을 설계에 포함하세요.

나중에 추가하려면 훨씬 어렵습니다.

실전 팁

💡 - 처음부터 로깅을 설계에 포함하세요 - 나중에 추가하기 어렵습니다

  • 트레이스를 JSON으로 저장하면 다양한 분석 도구에서 활용할 수 있습니다
  • 메트릭 대시보드를 만들어 실시간으로 에이전트 성능을 모니터링하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#AI에이전트#LLM#ReAct#에이전트패턴#LLM,에이전트,기초

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.