🤖

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

⚠️

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

이미지 로딩 중...

실전 에이전트 구축 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 12. · 14 Views

실전 에이전트 구축 완벽 가이드

LangGraph를 활용하여 계산기 에이전트를 처음부터 끝까지 구축하는 실전 가이드입니다. 도구 정의부터 라우팅까지, 에이전트의 핵심 개념을 실무 중심으로 배웁니다.


목차

  1. 계산기_에이전트_설계
  2. 도구_정의_multiply_add_divide
  3. LLM_노드_구현
  4. Tool_노드_구현
  5. should_continue_라우팅
  6. 그래프_빌드_및_테스트

1. 계산기 에이전트 설계

어느 날 김개발 씨는 회사에서 새로운 프로젝트를 맡았습니다. "AI 에이전트를 만들어보세요"라는 과제였죠.

막막했습니다. 에이전트가 뭔지는 알겠는데, 어디서부터 시작해야 할지 감이 잡히지 않았습니다.

에이전트 설계는 AI가 스스로 판단하고 도구를 사용하는 시스템의 청사진을 그리는 작업입니다. 마치 건축가가 집을 짓기 전에 설계도를 그리는 것처럼, 에이전트도 먼저 전체 구조를 설계해야 합니다.

좋은 설계는 확장 가능하고 유지보수하기 쉬운 에이전트를 만드는 첫걸음입니다.

다음 코드를 살펴봅시다.

from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage
import operator

# 에이전트의 상태를 정의합니다
class AgentState(TypedDict):
    # messages: 대화 히스토리를 저장합니다
    messages: Annotated[Sequence[BaseMessage], operator.add]

# 상태는 에이전트의 기억 역할을 합니다
# operator.add는 새 메시지를 기존 목록에 추가합니다

김개발 씨는 선배 박시니어 씨를 찾아갔습니다. "선배님, 에이전트를 어떻게 만들어야 할까요?" 박시니어 씨는 화이트보드를 꺼내 들었습니다.

"먼저 설계부터 해봅시다." 에이전트 설계란 정확히 무엇일까요? 쉽게 비유하자면, 에이전트 설계는 마치 레스토랑 운영 매뉴얼을 만드는 것과 같습니다.

주방장이 어떤 도구를 사용할지, 주문을 어떻게 처리할지, 어떤 순서로 요리를 만들지 정하는 것처럼, 에이전트도 어떤 도구를 쓸지, 어떻게 판단할지, 어떤 순서로 작동할지를 미리 정해야 합니다. 에이전트가 없던 시절에는 어땠을까요?

개발자들은 모든 경우의 수를 if-else로 직접 처리해야 했습니다. "사용자가 이렇게 물으면 이렇게 답하고, 저렇게 물으면 저렇게 답하고..." 코드가 수백 줄로 늘어났습니다.

더 큰 문제는 새로운 기능을 추가할 때마다 전체 로직을 다시 손봐야 한다는 점이었습니다. 유지보수 비용이 천문학적으로 증가했죠.

바로 이런 문제를 해결하기 위해 에이전트 패턴이 등장했습니다. 에이전트를 사용하면 AI가 스스로 판단하여 필요한 도구를 선택할 수 있습니다.

또한 새로운 도구를 추가하기만 하면 에이전트가 자동으로 활용할 수 있습니다. 무엇보다 복잡한 비즈니스 로직을 간단한 구조로 표현할 수 있다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 AgentState라는 클래스를 정의합니다.

이것이 에이전트의 기억 장치입니다. TypedDict를 상속받아 타입 안정성을 확보합니다.

messages 필드는 대화 히스토리를 저장하는데, Annotated와 operator.add를 사용하여 새 메시지가 기존 목록에 자동으로 추가되도록 설정합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 고객 상담 챗봇을 개발한다고 가정해봅시다. 고객이 "환불하고 싶어요"라고 물으면, 에이전트는 먼저 주문 조회 도구를 사용하고, 환불 가능 여부를 확인한 후, 환불 처리 도구를 실행합니다.

이 모든 과정이 사람의 개입 없이 자동으로 이루어집니다. 네이버, 카카오 같은 대기업들이 이런 패턴을 적극 활용하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 상태를 너무 복잡하게 설계하는 것입니다.

처음에는 필요한 최소한의 정보만 상태에 담아야 합니다. 너무 많은 필드를 추가하면 나중에 관리가 어려워집니다.

따라서 간단하게 시작해서 필요할 때마다 확장하는 방식으로 접근해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 상태 정의가 중요하군요!" 에이전트 설계를 제대로 이해하면 확장 가능하고 유지보수하기 쉬운 AI 시스템을 구축할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 상태는 최소한으로 시작하고 필요할 때 확장하세요

  • TypedDict를 사용하면 타입 안정성을 확보할 수 있습니다
  • operator.add를 활용하면 상태 업데이트가 간편해집니다

2. 도구 정의 multiply add divide

설계가 끝나자 박시니어 씨가 다음 단계를 알려주었습니다. "이제 에이전트가 사용할 도구를 만들어봅시다." 김개발 씨는 궁금했습니다.

도구라니, 어떻게 만드는 걸까요?

도구 정의는 에이전트가 실제로 실행할 수 있는 함수를 만드는 작업입니다. 마치 목수에게 망치와 톱을 주는 것처럼, 에이전트에게도 계산, 검색, 데이터 처리 등의 도구를 제공해야 합니다.

LangChain의 @tool 데코레이터를 사용하면 일반 함수를 에이전트 도구로 변환할 수 있습니다.

다음 코드를 살펴봅시다.

from langchain_core.tools import tool

@tool
def multiply(a: int, b: int) -> int:
    """두 숫자를 곱합니다."""
    return a * b

@tool
def add(a: int, b: int) -> int:
    """두 숫자를 더합니다."""
    return a + b

@tool
def divide(a: int, b: int) -> float:
    """두 숫자를 나눕니다."""
    return a / b

# 도구 목록 생성
tools = [multiply, add, divide]

김개발 씨는 처음으로 도구를 만들어보기로 했습니다. 박시니어 씨가 화면을 가리키며 말했습니다.

"도구는 생각보다 간단해요. 그냥 함수를 만들고 데코레이터를 붙이면 됩니다." 도구 정의란 정확히 무엇일까요?

쉽게 비유하자면, 도구는 마치 스위스 아미 나이프의 각 기능과 같습니다. 나이프, 가위, 병따개 등 각각의 기능이 독립적으로 존재하지만, 필요할 때 꺼내 쓸 수 있죠.

에이전트도 마찬가지로 여러 도구를 가지고 있다가 상황에 맞게 적절한 도구를 선택합니다. 도구가 제대로 정의되지 않던 시절에는 어땠을까요?

개발자들은 모든 기능을 하나의 거대한 함수에 때려 넣었습니다. 계산도 하고, 검색도 하고, 데이터도 처리하는 천 줄짜리 함수가 탄생했죠.

당연히 테스트하기도 어렵고, 버그 찾기도 힘들었습니다. 더 큰 문제는 새로운 기능을 추가할 때마다 기존 코드를 건드려야 한다는 점이었습니다.

바로 이런 문제를 해결하기 위해 도구 패턴이 등장했습니다. 도구를 분리하면 각 기능을 독립적으로 테스트할 수 있습니다.

또한 새로운 도구를 추가할 때 기존 코드를 전혀 건드리지 않아도 됩니다. 무엇보다 에이전트가 필요에 따라 도구를 조합하여 복잡한 작업을 수행할 수 있다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 @tool 데코레이터를 함수 위에 붙입니다.

이것이 핵심입니다. 이 데코레이터가 일반 함수를 LangChain이 이해할 수 있는 도구로 변환해줍니다.

multiply 함수는 두 정수를 받아 곱셈 결과를 반환합니다. 독스트링에 "두 숫자를 곱합니다"라고 적혀 있는데, 이것이 중요합니다.

에이전트는 이 설명을 읽고 언제 이 도구를 사용해야 할지 판단합니다. add 함수와 divide 함수도 같은 패턴을 따릅니다.

각각 덧셈과 나눗셈을 수행하죠. 마지막으로 tools 리스트에 세 함수를 모아 넣습니다.

이 리스트가 에이전트의 도구 상자가 됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 금융 서비스를 개발한다고 가정해봅시다. 환율 조회 도구, 계좌 잔액 확인 도구, 송금 실행 도구 등을 각각 정의합니다.

사용자가 "미국에 100달러 송금해줘"라고 요청하면, 에이전트는 먼저 환율 조회 도구로 현재 환율을 확인하고, 계좌 잔액 확인 도구로 잔액이 충분한지 체크한 후, 송금 실행 도구를 호출합니다. 카카오뱅크, 토스 같은 핀테크 기업들이 이런 방식으로 서비스를 구축합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 독스트링을 대충 쓰는 것입니다.

"계산합니다"처럼 모호하게 쓰면 에이전트가 언제 이 도구를 써야 할지 헷갈립니다. "두 숫자를 곱합니다"처럼 명확하고 구체적으로 작성해야 합니다.

또한 타입 힌트를 반드시 붙여야 합니다. 그래야 에이전트가 어떤 값을 전달해야 할지 알 수 있습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 독스트링이 이렇게 중요하군요!" 도구 정의를 제대로 이해하면 재사용 가능하고 테스트하기 쉬운 에이전트 시스템을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 독스트링은 명확하고 구체적으로 작성하세요

  • 타입 힌트를 반드시 붙여야 에이전트가 제대로 작동합니다
  • 하나의 도구는 하나의 기능만 수행하도록 설계하세요

3. LLM 노드 구현

도구를 만들고 나자 김개발 씨는 다음 단계가 궁금했습니다. "이제 뭘 해야 하죠?" 박시니어 씨가 웃으며 대답했습니다.

"이제 두뇌를 만들어야죠. LLM 노드 말이에요."

LLM 노드는 에이전트의 두뇌 역할을 하는 부분입니다. 사용자의 요청을 분석하고, 어떤 도구를 사용할지 결정하며, 최종 답변을 생성합니다.

마치 사람이 문제를 보고 어떤 방법으로 풀지 생각하는 것처럼, LLM 노드도 상황을 판단하여 다음 행동을 결정합니다.

다음 코드를 살펴봅시다.

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

# LLM에 도구를 바인딩합니다
llm = ChatOpenAI(model="gpt-4", temperature=0)
llm_with_tools = llm.bind_tools(tools)

def call_model(state: AgentState):
    """LLM을 호출하여 다음 행동을 결정합니다."""
    messages = state["messages"]
    # LLM이 도구를 선택하거나 답변을 생성합니다
    response = llm_with_tools.invoke(messages)
    # 응답을 상태에 추가합니다
    return {"messages": [response]}

김개발 씨는 코드를 보며 고민에 빠졌습니다. "LLM 노드가 정확히 뭘 하는 거죠?" 박시니어 씨가 커피를 한 모금 마시고 설명을 시작했습니다.

LLM 노드란 정확히 무엇일까요? 쉽게 비유하자면, LLM 노드는 마치 프로젝트 매니저와 같습니다.

고객의 요청을 듣고, 어떤 팀원에게 일을 시킬지 결정하고, 각 팀원의 결과물을 종합하여 최종 보고서를 만듭니다. 에이전트에서도 LLM 노드가 이와 똑같은 역할을 합니다.

사용자 요청을 받아서 어떤 도구를 쓸지 판단하고, 도구 실행 결과를 바탕으로 답변을 만들어냅니다. LLM 노드가 없던 시절에는 어땠을까요?

개발자들은 규칙 기반 시스템을 만들어야 했습니다. "만약 사용자가 '곱하기'라는 단어를 쓰면 multiply 도구를 호출하고..." 이런 식으로 수백 개의 규칙을 작성했죠.

문제는 사용자가 "5와 3을 곱한 값에 2를 더해줘"처럼 복잡하게 요청하면 대응할 수 없었다는 점입니다. 규칙이 기하급수적으로 늘어났습니다.

바로 이런 문제를 해결하기 위해 LLM 노드 패턴이 등장했습니다. LLM을 사용하면 자연어로 된 복잡한 요청도 정확히 이해할 수 있습니다.

또한 여러 도구를 순차적으로 사용해야 하는 복잡한 작업도 자동으로 계획을 세워 실행합니다. 무엇보다 새로운 도구를 추가할 때 기존 로직을 전혀 수정하지 않아도 된다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 ChatOpenAI로 GPT-4 모델을 초기화합니다.

temperature를 0으로 설정하여 일관된 결과를 얻습니다. 핵심은 bind_tools 메서드입니다.

이 메서드가 LLM에게 "이런 도구들을 사용할 수 있어"라고 알려줍니다. LLM은 이 정보를 바탕으로 필요할 때 도구를 호출합니다.

call_model 함수는 실제 LLM을 호출하는 부분입니다. 상태에서 messages를 꺼내서 LLM에 전달하고, LLM의 응답을 다시 상태에 추가합니다.

이렇게 하면 대화 히스토리가 계속 쌓이면서 컨텍스트가 유지됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 여행 예약 서비스를 개발한다고 가정해봅시다. 사용자가 "다음 주 금요일에 부산 가는 KTX 표 2장 예매하고, 호텔도 같이 잡아줘"라고 요청합니다.

LLM 노드는 이 요청을 분석하여 먼저 KTX 예매 도구를 호출하고, 그 다음 호텔 검색 도구를 호출한 후, 결과를 사용자에게 친절하게 정리해서 보여줍니다. 야놀자, 여기어때 같은 플랫폼들이 이런 방식으로 사용자 경험을 개선하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 temperature를 너무 높게 설정하는 것입니다.

창의적인 답변이 필요한 경우가 아니라면 0 또는 0.1로 낮게 유지해야 일관된 결과를 얻을 수 있습니다. 또한 bind_tools를 빼먹으면 LLM이 도구 사용법을 몰라서 제대로 작동하지 않습니다.

반드시 도구를 바인딩해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, LLM이 매니저 역할을 하는 거군요!" LLM 노드를 제대로 이해하면 복잡한 비즈니스 로직을 간단하게 구현할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - temperature는 일관성이 필요하면 0에 가깝게, 창의성이 필요하면 높게 설정하세요

  • bind_tools를 반드시 호출해야 LLM이 도구를 사용할 수 있습니다
  • 상태를 업데이트할 때는 항상 리스트로 감싸서 반환하세요

4. Tool 노드 구현

LLM 노드를 만들고 나니 김개발 씨는 뿌듯했습니다. 하지만 박시니어 씨가 말했습니다.

"아직 끝나지 않았어요. 도구를 실제로 실행할 노드가 필요합니다."

Tool 노드는 LLM이 선택한 도구를 실제로 실행하는 부분입니다. LLM은 "이 도구를 사용해야겠다"고 결정만 하고, Tool 노드가 실제 실행을 담당합니다.

마치 매니저가 지시를 내리면 실무자가 일을 처리하는 것과 같습니다.

다음 코드를 살펴봅시다.

from langgraph.prebuilt import ToolNode

# Tool 노드를 생성합니다
tool_node = ToolNode(tools)

# Tool 노드는 다음 작업을 수행합니다:
# 1. LLM의 도구 호출 요청을 파싱합니다
# 2. 해당 도구를 실행합니다
# 3. 실행 결과를 메시지로 변환합니다
# 4. 상태에 결과를 추가합니다

# 사용 예시
def tool_executor(state: AgentState):
    """도구를 실행하고 결과를 반환합니다."""
    return tool_node.invoke(state)

김개발 씨는 궁금했습니다. "LLM 노드가 도구를 선택하는데, 왜 또 Tool 노드가 필요한가요?" 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.

Tool 노드란 정확히 무엇일까요? 쉽게 비유하자면, Tool 노드는 마치 레스토랑의 주방과 같습니다.

매니저(LLM 노드)가 "스테이크 하나 만들어줘"라고 주문하면, 주방(Tool 노드)이 실제로 고기를 굽고 요리를 완성합니다. 매니저는 요리법을 알고 있지만 직접 요리하지는 않습니다.

마찬가지로 LLM은 어떤 도구를 써야 할지 알고 있지만 직접 실행하지는 않습니다. Tool 노드가 분리되지 않던 시절에는 어땠을까요?

LLM이 도구 선택과 실행을 동시에 처리했습니다. 문제는 도구 실행 중 에러가 발생하면 전체 프로세스가 멈춘다는 점이었습니다.

또한 도구 실행 로그를 따로 관리하기도 어려웠습니다. 디버깅할 때 어디서 문제가 생긴 건지 찾기가 힘들었죠.

바로 이런 문제를 해결하기 위해 Tool 노드 분리 패턴이 등장했습니다. Tool 노드를 분리하면 도구 실행 로직을 독립적으로 관리할 수 있습니다.

또한 에러 핸들링을 Tool 노드에서 집중적으로 처리할 수 있습니다. 무엇보다 도구 실행 전후에 로깅, 모니터링, 캐싱 등의 부가 기능을 쉽게 추가할 수 있다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. ToolNode는 LangGraph에서 제공하는 미리 만들어진 노드입니다.

생성자에 tools 리스트를 전달하면 됩니다. 이 노드는 내부적으로 복잡한 작업을 수행하는데, LLM이 생성한 도구 호출 요청을 파싱하고, 적절한 도구를 찾아서 실행하고, 결과를 메시지 형태로 변환합니다.

tool_executor 함수는 Tool 노드를 호출하는 래퍼입니다. 상태를 받아서 tool_node.invoke를 호출하고, 결과를 반환합니다.

실제로는 이 래퍼 없이 tool_node를 직접 사용해도 되지만, 명시적으로 함수를 만들어두면 나중에 커스터마이징하기 편합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 물류 관리 시스템을 개발한다고 가정해봅시다. LLM이 "재고 확인" 도구를 선택하면, Tool 노드는 실제로 데이터베이스를 조회하고 결과를 반환합니다.

이때 Tool 노드에서 데이터베이스 연결 풀링, 쿼리 타임아웃, 에러 재시도 등의 로직을 처리할 수 있습니다. 쿠팡, 마켓컬리 같은 이커머스 기업들이 이런 방식으로 복잡한 물류 시스템을 안정적으로 운영합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 Tool 노드를 커스터마이징하려다가 내부 구조를 망가뜨리는 것입니다.

ToolNode는 이미 잘 만들어진 클래스이므로, 특별한 이유가 없다면 그대로 사용하는 것이 좋습니다. 커스터마이징이 필요하면 상속보다는 래퍼 패턴을 사용하세요.

또한 도구 실행 중 발생하는 예외를 반드시 처리해야 합니다. 그렇지 않으면 전체 에이전트가 멈춥니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 역할 분리가 중요하군요!" Tool 노드를 제대로 이해하면 안정적이고 확장 가능한 에이전트를 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - ToolNode는 그대로 사용하는 것이 가장 안전합니다

  • 커스터마이징이 필요하면 래퍼 패턴을 활용하세요
  • 도구 실행 중 예외 처리를 반드시 구현하세요

5. should continue 라우팅

이제 거의 다 왔습니다. 김개발 씨가 안도의 한숨을 쉬려는 순간, 박시니어 씨가 말했습니다.

"마지막 퍼즐이 남았어요. 라우팅 로직입니다."

라우팅은 에이전트가 다음에 어디로 가야 할지 결정하는 교통 정리 역할입니다. LLM이 도구를 호출했으면 Tool 노드로, 최종 답변을 냈으면 종료로 보내야 합니다.

마치 교차로에서 신호등이 차량의 방향을 안내하는 것처럼, 라우팅 함수가 에이전트의 흐름을 제어합니다.

다음 코드를 살펴봅시다.

from langchain_core.messages import ToolMessage

def should_continue(state: AgentState):
    """다음 노드를 결정합니다."""
    messages = state["messages"]
    last_message = messages[-1]

    # LLM이 도구를 호출했는지 확인합니다
    if last_message.tool_calls:
        # 도구 노드로 이동합니다
        return "tools"

    # 도구 호출이 없으면 종료합니다
    return "end"

김개발 씨는 코드를 보며 고개를 갸우뚱했습니다. "이게 왜 필요한가요?" 박시니어 씨가 웃으며 대답했습니다.

"라우팅이 없으면 에이전트가 길을 잃습니다." 라우팅이란 정확히 무엇일까요? 쉽게 비유하자면, 라우팅은 마치 택배 분류 센터와 같습니다.

택배 상자를 보고 "이건 서울로, 저건 부산으로" 분류하는 것처럼, 라우팅 함수는 메시지를 보고 "이건 도구 노드로, 저건 종료로" 결정합니다. 올바른 라우팅이 없으면 택배가 엉뚱한 곳으로 가듯이, 에이전트도 엉뚱한 노드로 가서 오작동합니다.

라우팅이 제대로 구현되지 않던 시절에는 어땠을까요? 에이전트가 무한 루프에 빠지는 경우가 흔했습니다.

LLM이 도구를 호출했는데 Tool 노드로 가지 않고 다시 LLM 노드로 돌아가거나, 반대로 답변을 냈는데도 계속 도구를 실행하려고 했습니다. 디버깅하기도 어려웠죠.

"왜 안 멈추지?"라고 고민하다가 시간을 낭비하는 경우가 많았습니다. 바로 이런 문제를 해결하기 위해 명시적 라우팅 패턴이 등장했습니다.

명시적 라우팅을 사용하면 에이전트의 흐름을 정확히 제어할 수 있습니다. 또한 조건부 로직을 한곳에 모아두어 관리가 쉬워집니다.

무엇보다 에이전트의 동작을 예측 가능하게 만들어 안정성을 크게 향상시킬 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

should_continue 함수는 상태를 받아서 다음 노드 이름을 문자열로 반환합니다. 먼저 messages에서 마지막 메시지를 꺼냅니다.

이것이 LLM이 방금 생성한 응답입니다. 핵심은 tool_calls 속성을 확인하는 부분입니다.

LLM이 도구를 호출했으면 이 속성에 값이 들어 있습니다. tool_calls가 있으면 "tools"를 반환합니다.

이것은 Tool 노드로 가라는 신호입니다. tool_calls가 없으면 "end"를 반환하여 에이전트를 종료합니다.

간단하지만 강력한 로직입니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 고객 지원 챗봇을 개발한다고 가정해봅시다. 고객이 "주문 취소하고 싶어요"라고 요청하면, LLM은 주문 조회 도구를 호출합니다.

라우팅 함수는 tool_calls를 확인하고 Tool 노드로 보냅니다. 도구가 실행되고 결과가 돌아오면, LLM이 다시 답변을 생성합니다.

이번엔 tool_calls가 없으므로 라우팅 함수가 대화를 종료합니다. 네이버 클로바, 카카오 i 같은 AI 비서들이 이런 방식으로 작동합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 라우팅 로직을 너무 복잡하게 만드는 것입니다.

"이 경우는 A로, 저 경우는 B로, 또 다른 경우는 C로..." 조건이 열 개가 넘어가면 관리가 불가능해집니다. 대부분의 경우 "도구 호출이 있으면 tools, 없으면 end" 정도면 충분합니다.

처음에는 간단하게 시작하세요. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 라우팅이 흐름 제어를 하는 거군요!" 라우팅을 제대로 이해하면 예측 가능하고 안정적인 에이전트를 만들 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 라우팅 로직은 최대한 간단하게 유지하세요

  • tool_calls 속성을 확인하는 것만으로 대부분 충분합니다
  • 복잡한 조건이 필요하면 여러 라우팅 함수로 분리하세요

6. 그래프 빌드 및 테스트

드디어 마지막 단계입니다. 김개발 씨는 긴장했습니다.

"이제 모든 걸 조합해서 실제로 작동하는 에이전트를 만드는 거죠?" 박시니어 씨가 고개를 끄덕였습니다. "맞아요.

퍼즐 조각들을 맞출 시간입니다."

그래프 빌드는 지금까지 만든 모든 구성 요소를 하나로 연결하는 작업입니다. 노드를 추가하고, 엣지로 연결하고, 시작점과 끝점을 정의합니다.

마치 레고 블록들을 조립하여 완성품을 만드는 것처럼, 그래프 빌드를 통해 완전히 작동하는 에이전트가 탄생합니다.

다음 코드를 살펴봅시다.

from langgraph.graph import StateGraph, END

# 그래프를 생성합니다
workflow = StateGraph(AgentState)

# 노드를 추가합니다
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)

# 시작점을 설정합니다
workflow.set_entry_point("agent")

# 조건부 엣지를 추가합니다
workflow.add_conditional_edges(
    "agent",
    should_continue,
    {"tools": "tools", "end": END}
)

# Tool 노드에서 다시 agent로 돌아갑니다
workflow.add_edge("tools", "agent")

# 그래프를 컴파일합니다
app = workflow.compile()

# 테스트 실행
result = app.invoke({
    "messages": [HumanMessage(content="5 곱하기 3은?")]
})
print(result["messages"][-1].content)

김개발 씨는 마지막 코드를 보며 가슴이 두근거렸습니다. "이게 정말 작동할까요?" 박시니어 씨가 엔터 키를 눌렀고, 화면에 "15"라는 답변이 나타났습니다.

성공이었습니다. 그래프 빌드란 정확히 무엇일까요?

쉽게 비유하자면, 그래프 빌드는 마치 전기 회로를 구성하는 것과 같습니다. 배터리, 전구, 스위치를 전선으로 연결하여 완전한 회로를 만듭니다.

각 부품은 개별적으로는 아무 일도 하지 못하지만, 올바르게 연결되면 불이 켜집니다. 에이전트도 마찬가지로 LLM 노드, Tool 노드, 라우팅 함수를 그래프로 연결해야 비로소 작동합니다.

그래프가 제대로 정의되지 않던 시절에는 어땠을까요? 개발자들은 함수를 직접 호출하는 방식으로 에이전트를 만들었습니다.

"call_model을 호출하고, 결과를 확인하고, 필요하면 tool_node를 호출하고..." 코드가 스파게티처럼 얽혔습니다. 새로운 노드를 추가하려면 기존 코드를 전부 뜯어고쳐야 했습니다.

유지보수 비용이 천문학적이었죠. 바로 이런 문제를 해결하기 위해 그래프 기반 에이전트 패턴이 등장했습니다.

그래프를 사용하면 에이전트의 구조를 시각적으로 이해할 수 있습니다. 또한 새로운 노드나 엣지를 추가할 때 기존 코드를 건드리지 않아도 됩니다.

무엇보다 복잡한 멀티 에이전트 시스템도 간단하게 구성할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 StateGraph를 생성합니다. AgentState를 전달하여 이 그래프가 어떤 상태를 사용하는지 알려줍니다.

add_node로 "agent"와 "tools" 노드를 추가합니다. 각각 call_model과 tool_node 함수를 실행합니다.

set_entry_point로 시작점을 "agent"로 설정합니다. 에이전트가 실행되면 가장 먼저 이 노드가 호출됩니다.

핵심은 add_conditional_edges입니다. "agent" 노드 다음에 should_continue 함수를 실행하여 다음 노드를 결정합니다.

반환값이 "tools"면 Tool 노드로, "end"면 종료됩니다. add_edge로 Tool 노드에서 다시 agent 노드로 돌아가는 엣지를 추가합니다.

이렇게 하면 도구 실행 후 LLM이 결과를 보고 다음 행동을 결정할 수 있습니다. 마지막으로 compile을 호출하여 그래프를 실행 가능한 앱으로 변환합니다.

테스트 부분을 보면 HumanMessage로 사용자 질문을 만들고, app.invoke로 에이전트를 실행합니다. 결과 상태에서 마지막 메시지를 꺼내면 에이전트의 최종 답변을 볼 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 복잡한 데이터 분석 서비스를 개발한다고 가정해봅시다.

사용자가 "지난달 매출을 분석하고, 상위 10개 제품을 차트로 보여줘"라고 요청하면, 에이전트는 먼저 데이터베이스 조회 도구를 사용하고, 그 다음 통계 계산 도구를 실행하고, 마지막으로 차트 생성 도구를 호출합니다. 각 단계가 자동으로 연결되어 복잡한 작업이 순식간에 완료됩니다.

구글, 아마존 같은 빅테크 기업들이 내부적으로 이런 방식의 에이전트 시스템을 운영합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 엣지를 빼먹는 것입니다. 특히 Tool 노드에서 agent 노드로 돌아가는 엣지를 추가하지 않으면 도구를 한 번만 실행하고 멈춥니다.

반드시 모든 노드가 적절히 연결되어 있는지 확인하세요. 또한 무한 루프를 방지하기 위해 최대 반복 횟수를 설정하는 것도 좋은 방법입니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨가 웃으며 말했습니다.

"축하합니다. 첫 에이전트를 만들었어요!" 김개발 씨는 뿌듯했습니다.

처음엔 막막했지만, 단계별로 차근차근 배우니 이해할 수 있었습니다. 그래프 빌드를 제대로 이해하면 복잡한 멀티 에이전트 시스템도 자신 있게 구축할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요. 작은 계산기 에이전트에서 시작하여 점점 더 복잡한 시스템으로 확장해 나가세요.

에이전트의 세계는 무한합니다.

실전 팁

💡 - 노드 간 엣지를 빠짐없이 연결했는지 확인하세요

  • 무한 루프 방지를 위해 최대 반복 횟수를 설정하세요
  • 처음에는 간단한 그래프로 시작해서 점진적으로 확장하세요

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

#LangGraph#Agent#ToolCalling#StateGraph#Routing#AI,LLM,Python,LangGraph

댓글 (0)

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