LangGraph를 활용한 Agent 만들기

김지우·2025년 1월 19일

목차
1. 정리 배경
2. 도구 정의
3. 상태 및 노드 정의
4. 그래프 생성 및 노드 추가
5. 도구 노드(Tool Node)
6. 조건부 엣지(Conditional Edge)
7. 결론


1. 정리 배경

사실 개인적으로 LangGraph의 공부는 Agent 사용을 더 잘하게 하기 위해 공부를 시작한 것이다. 따라서 LangGraph를 기반으로 Agent를 만들어 보는 연습을 하려고 한다.

해당 정리글은 테디노트님의 유료 강의를 듣고 내가 이해하기 편하게 정리한 글이다. 글이 부족하다고 느껴진다면 테디 노트님 유료 강의를 구매하여 수강하면 된다.

2. 도구 정의

우선 기본적으로 Agent의 경우 도구를 사용하기 떄문에 우선 사용할 도구에 대한 정의를 진행하면 된다.

from langchain_community.tools.tavily_search import TavilySearchResults

tool = TavilySearchResults(k=3)

# 도구 목록에 추가
tools = [tool]

# 도구 실행
print(tool.invoke("덱스의 냉터뷰"))

====결과 ======
[{'url': 'https://www.dispatch.co.kr/2314364', 'content': '\'냉터뷰\'는 본래 덱스가 mc로 진행하는 \'덱스의 냉터뷰\'였으나 이번 회차에서는 mc없이 제작진의 질문으로 촬영을 이어나갔다. 앞서 덱스는 지난 8월 채널 \'뜬뜬\'에 출연해 벗아웃이 왔다고 고백했다. 원래 방송인이 아닌 소방관을 꿈
꾸었던 그는 "유재석도 안 오는'}, {'url': 'https://v.daum.net/v/20240927110125666', 'content': '덱스와 고민시 사이에서 묘한 기류가 포착됐다. 지난 26일 \'덱스의 냉터뷰\'에는 고민시가 게스트로 출연했다. 이날 덱스는 \'덱스의 냉터뷰\' 공식 질문으로 고민시의 이상형을 물었다. \'알잘딱깔센
\'(알아서 잘 딱 깔끔하고 센스있게) 잘하는 여자를 이상형으로 꼽은 덱스는 "민시의 이상형은'}, {'url': 'https://www.youtube.com/watch?v=OObQryzR6KU', 'content': '📌 117 공지 📌 덱스의 냉터뷰 시즌2에 보내주신 큰 사랑에 감사드립니다.냉터뷰는 덱스 님과 함께 시즌3로 찾아올 예정이니 구독 & 알람 설정 후'}, {'url': 'https://namu.wiki/w/덱스의+냉터뷰', 'content': '덱스의 냉터뷰 시즌2 첫 게스트로 출연했던 twice의 사나가 시즌3 전 휴식기에 냉터뷰 스페셜 mc를 맡게 되었다. [21] # 이후 덱스의 냉터뷰 사나 편의 조회수가 무려 1,000만 뷰를 넘게 되면서 이를 기념하여 사나의 냉터뷰
에 덱스가 게스트로 초대된 바 있다.'}, {'url': 'https://blog.naver.com/PostView.naver?blogId=spring_hmong&logNo=223511534560', 'content': '💟드디어 돌아온 덱스의 냉터뷰 시즌3💟 여름을 맞이하여 시원청량해진 분위기에. 더욱 화려해진 초..급 게스트 라인업까지!! 올해 무더위는 덱터뷰 보 
면서 완...파💥 얘들아, 나랑 같이 여름방학에 놀러갈래?🌿'}]

3. 상태 및 노드 정의

그럼 여기서부터는 또 기본적인 LangGraph를 통한 그래프 정의 과정을 시작하면 된다. 그리고 순서는 내가 기억하기 좋게 정리 하기 위함이지, 도구 바인딩 전까지는 크게 중요하지 않은 것 같다.

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages


# State 정의
class State(TypedDict):
    # list 타입에 add_messages 적용(list 에 message 추가)
    messages: Annotated[list, add_messages]

이제 노드를 만들어야 하는데, 이제는 이전 처럼 챗봇의 기능을 하는 노드를 만들 것이고, Agent이기 때문에 도구를 바인딩 할 예정이다.

from langchain_openai import ChatOpenAI

# LLM 초기화
llm = ChatOpenAI(model="gpt-4o-mini")

# LLM 에 도구 바인딩
llm_with_tools = llm.bind_tools(tools)

# 노드 함수 정의
def chatbot(state: State):
    answer = llm_with_tools.invoke(state["messages"])
    # 메시지 목록 반환
    return {"messages": [answer]}  # 자동으로 add_messages 적용

4. 그래프 생성 및 노드 추가

from langgraph.graph import StateGraph

# 상태 그래프 초기화
graph_builder = StateGraph(State)

# 노드 추가
graph_builder.add_node("chatbot", chatbot)

여기서 그래프를 정의하고, add_node를 통해 미리 만들어 놓은 노드를 추가한다.

5. 도구 노드(Tool Node)

import json
from langchain_core.messages import ToolMessage


class BasicToolNode:
    """Run tools requested in the last AIMessage node"""

    def __init__(self, tools: list) -> None:
        # 도구 리스트
        self.tools_list = {tool.name: tool for tool in tools}

    def __call__(self, inputs: dict):
        # 메시지가 존재할 경우 가장 최근 메시지 1개 추출
        if messages := inputs.get("messages", []):
            message = messages[-1]
        else:
            raise ValueError("No message found in input")

        # 도구 호출 결과
        outputs = []
        for tool_call in message.tool_calls:
            # 도구 호출 후 결과 저장
            tool_result = self.tools_list[tool_call["name"]].invoke(tool_call["args"])
            outputs.append(
                # 도구 호출 결과를 메시지로 저장
                ToolMessage(
                    content=json.dumps(
                        tool_result, ensure_ascii=False
                    ),  # 도구 호출 결과를 문자열로 변환
                    name=tool_call["name"],
                    tool_call_id=tool_call["id"],
                )
            )

        return {"messages": outputs}


# 도구 노드 생성
tool_node = BasicToolNode(tools=[tool])

# 그래프에 도구 노드 추가
graph_builder.add_node("tools", tool_node)

Agent 이기 때문에 도구를 실행하는 노드를 만들 필요가 있다.
실제로는 LangChain pre-built의 ToolNode를 사용하면 되지만, 전체 flow를 알아볼 필요가 있다.

동작 방식은 for lool를 돌려 tool_calls가 포함되어 있으면 도구를 호출하는 방식. LangGraph없을 때도 ToolBind는 다음과 같은 방법으로 진행헀다.

6. 조건부 엣지(Conditional Edge)

조건부 엣지는 if문을 포함한 함수를 이용해 그래프 상태에 따라 다른 노드로 라우팅하도록 하는 구조다. 위의 이미지처럼 동작한다고 가정하면 코드는 다음과 같다.

from langgraph.graph import START, END


def route_tools(
    state: State,
):
    if messages := state.get("messages", []):
        # 가장 최근 AI 메시지 추출
        ai_message = messages[-1]
    else:
        # 입력 상태에 메시지가 없는 경우 예외 발생
        raise ValueError(f"No messages found in input state to tool_edge: {state}")

    # AI 메시지에 도구 호출이 있는 경우 "tools" 반환
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        # 도구 호출이 있는 경우 "tools" 반환
        return "tools"
    # 도구 호출이 없는 경우 "END" 반환
    return END


# `tools_condition` 함수는 챗봇이 도구 사용을 요청하면 "tools"를 반환하고, 직접 응답이 가능한 경우 "END"를 반환
graph_builder.add_conditional_edges(
    source="chatbot",
    path=route_tools,
    # route_tools 의 반환값이 "tools" 인 경우 "tools" 노드로, 그렇지 않으면 END 노드로 라우팅
    path_map={"tools": "tools", END: END},
)

# tools > chatbot
graph_builder.add_edge("tools", "chatbot")

# START > chatbot
graph_builder.add_edge(START, "chatbot")

# 그래프 컴파일
graph = graph_builder.compile()

=====결과 =====
==============
STEP: messages
==============

content='테디노트 YouTube 채널에 대해서 검색해 줘' additional_kwargs={} response_metadata={} id='3dfa394f-6820-4b18-9d65-f21f0cc7f38f'

==============
STEP: messages
==============

content='' additional_kwargs={'tool_calls': [{'id': 'call_evJ0QCI8X8AzGOJrXTyf9aQ6', 'function': {'arguments': '{"query":"테디노트 YouTube 채널"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}], 'refusal': None} response_metadata={'token_usage': {'completion_tokens': 
26, 'prompt_tokens': 93, 'total_tokens': 119, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_72ed7ab54c', 'finish_reason': 'tool_calls', 'logprobs': None} id='run-1ea3fbe8-122d-4f0a-9921-29bc143a4687-0' tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': '테디노트 YouTube 채널'}, 'id': 'call_evJ0QCI8X8AzGOJrXTyf9aQ6', 'type': 'tool_call'}] usage_metadata={'input_tokens': 93, 'output_tokens': 26, 'total_tokens': 119, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}

==============
STEP: messages
==============

content='[{"url": "https://aisummit.co.kr/workshop-teddy/", "content": "Teddy(이경록) 는 AI 교육 스타트업 브레인크루의 CEO이자 YouTube \'테디노트\' 채널(구독자 3만명)을 운영하며 RAG 기술 콘텐츠를 주로 제작한다. 2024년 LangChain 밋업Q1, Q2 연사와 Gencon2024 에서 Modular RAG 기법을  
주제로 발표했다. \\"랭체인 한국어 튜토리얼\\" 저자로, 관련"}, {"url": "https://blog.naver.com/PostView.naver?blogId=teddysecretnote&logNo=223438344026", "content": "테디의 비밀노트 유튜브 채널 5개 실패 후 6번째 도전에서 성공해 유튜브 조회수로만 약 8000만원의 수입을 올린 8만 구독자 
유튜버가 되었습니다. 일상에서 이 경험을 주변에 공유하니 너도나도 자신들도 유튜브를 한 번 해보고 싶다고 제가 겪은 시행착오를"}, {"url": "https://teddynote.com/", "content": "테디노트.dev Teddy Lee (이경록) 👋. YouTube 테디노트; 블로그 테디노트; LinkedIn; LangChain. LangChain 한국어 
튜토리얼 Github; LangChain 한국어 튜토리얼 위키독스 전자책"}, {"url": "https://linktr.ee/teddynote", "content": "🔥[100% 무료] 테디노트 YouTube 콘텐츠 학습 순서🔥. 유튜브. 📘 랭체인 한국어 튜토리얼🇰🇷. Github. 9/21 테디노트-Gencon2024-ModularRAG-2  0240921.pdf."}, {"url": "https://teddylee777.github.io/lectures/", "content": "텐서플로 개발자 자격증 취득\\n🙏 아래의 내용을 참고해 주세요 🙏\\n📌 VOD 강의 링크\\n💻 한 방으로 끝내는 판다스(Pandas) 데이터 분석 - 전자책 포함\\n🙏 아래의 내용을 참고해 주세요 🙏\\n📌 VOD 강의 링크\\n🌱 한 권으로 끝내는 파이썬(Python) 코딩 입문 - 전자책 포함\\n🙏 아래의 내용을 참고해 주세요 🙏\\n📌 VOD 강의 링크\\n🌱 깃헙 블로그(Github blog)로 차별화 된 나만의 홈페이지 만들기!\\n 🔥알림🔥\\n① 테디노트 유튜브 -\\n구경하러 가기!\\n② 서울대 X 테디노트 ChatGPT & PyTorch 강의\\n강의 커리큘럼 👀\\n강의\\n아래에 나열 
된 강의는 모두 제가 직접 촬영 및 진행한 내 새끼같은 강의들입니다.\\n 주제별로 강의 영상을 시리즈로 모아 놓았어요😊\\n구독, 좋아요👍 그리고 댓글 감사합니다🥰\\n📌  지금 당장 유튜브 보러가기\\n✏️ 머신러닝 스터디 혼자 해보기\\n깃헙(GitHub)에 혼자서 데이터 분석, 머신러닝, 딥러닝을 스터 
디 할 수 있도록 주제별로 정리해 놓았어요✌️\\n저도 혼자서 공부를 했었는데, 이해가 쏙쏙 되는 강의와 블로그 주소들을 메모해 두었다가 정리해 본 내용이에요.\\n 강의 링크\\n🌱 스트림릿(Streamlit)을 활용한 파이썬 웹앱 제작하기\\n🙏 아래의 내용을 참고해 주세요 🙏\\n스트림릿(streamlit)을 활
용하여 파이썬 웹앱 대시보드를 구현해보고 싶으신 분들께 추천 드립니다🤩\\n데이터 분석/대시보드 제작 그리고 머신러닝 모델을 배포해서 서비스화 시키는 과정입니다😊\\n파이썬 기본 문법만 알고 계신다면 부담 없이 도전해 만들어 보고 싶으신 분들께 추천 드립니다🤩\\n코딩은 거의 없기 때문에 코
린이도 문제 없어요😊\\n테디노트와 같은 블로그를 만들어 보고 싶으시다면 지금 바로 도전해 보세요 🥰\\n📌 VOD"}]' name='tavily_search_results_ json' id='fa10376c-269b-4d02-8e8c-e3bff7ae0870' tool_call_id='call_evJ0QCI8X8AzGOJrXTyf9aQ6'

==============
STEP: messages
==============

content='테디노트 YouTube 채널에 대한 정보는 다음과 같습니다:\n\n1. **운영자**: 테디노트 채널은 이경록(Teddy)라는 개인이 운영하고 있으며, AI 교육 스타트업 브레인크루의 CEO입니다. 현재 구독자는 약 3만명입니다. 채널에서는 RAG(다양한 생성적 AI) 기술 콘텐츠를 주로 다룹니다. [자세히 보 
기](https://aisummit.co.kr/workshop-teddy/)\n\n2. **성공적인 도전**: 이경록은 유튜브 채널을 5번 실패한 후 6번째 도전에서 성공을 거두었으며, 현재 8만 구독자를 보유하고 있습니다. 유튜브 조회수로 약 8000만원의 수입을 올렸다고 합니다. [자세히 보기](https://blog.naver.com/PostView.naver?blogId=teddysecretnote&logNo=223438344026)\n\n3. **전문성**: 테디노트 채널은 머신러닝 및 AI와 관련된 다양한 주제를 깊이 있게 다룹니다. 이경록은 "랭체인 한국어 튜토리얼"의 저자이기도 하며, 스터디 및 강의를 온라인으로 제공하고 있습니다. [자세히 보기](https://teddynote.com/)\n\n4. **기타 리소스**: 테디노트는 다양한 학습 자료와 VOD 강의를 제공하며, 관심 있는 주제에 대해 쉽게 접근할 수 있도록 돕습니다. [링크트리](https://linktr.ee/teddynote)에서 더 많은 자료를 찾아볼 수 있습니다.\n\n이외에도 추가적인 정보나 특정 질문이 있다면 말씀해 주세요!' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 386, 'prompt_tokens': 1117, 'total_tokens': 1503, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_72ed7ab54c', 'finish_reason': 'stop', 'logprobs': None} id='run-49306277-534b-4413-a322-07922dc35900-0' usage_metadata={'input_tokens': 1117, 'output_tokens': 386, 'total_tokens': 1503, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}

컴파일 이후 에이전트를 실행하면 다음과 같은 결과를 확인할 수 있다.

7. 결론

다음과 같은 방식으로 Agent를 LangGraph 기반으로 생성할 수 있다. 반복 숙달을 통해 익혀야 하지 않을까 싶다.

이 게시물에서 꼭 기억해야 하는 부분은 조건부 엣지를 구현하는 것과 ToolNode의 원리인 것 같다.

profile
프로그래밍 기록 + 공부 기록

0개의 댓글