0

에이전틱 AI - 랭체인코리아 밋업 2025Q1

랭체인 코리아

LangGraph #2 - Chatbot with Tools

AF 김태영
2025.01.09 10:36
208

우리는 ch1에서 LangGraph의 StateGraph를 만들고, 간단한 노드(chatbot)와 엣지(START->chatbot->END)를 연결해, “사용자 입력 -> LLM 응답”의 기초적인 챗봇을 완성했습니다. 그 결과, Claude가 이미 알고 있는 지식 범위 내에서는 문제없이 대화를 이어갈 수 있었죠. 하지만 LLM이 미처 학습하지 못했거나 최신 정보를 요구하는 상황에서는 응답이 부정확해질 수 있었습니다. 이를 해결하기 위해 검색 엔진 등의 외부 도구(tool) 를 연결해 필요한 데이터를 실시간으로 가져오는 방식을 고려해볼 수 있습니다. 이번 ch2에서는 LangGraph에 툴 노드(ToolNode) 를 추가해, LLM이 필요시 스스로 검색 툴을 사용할 수 있도록 만들고, 조건부 분기(conditional edges)를 적용해 LLM이 검색을 원하는 경우와 원치 않는 경우를 구분하여 처리합니다. 이를 통해, 사용자 질문에 대해 LLM이 필요 시 웹 검색을 수행하고, 그 결과를 바탕으로 대답을 보강할 수 있게 됩니다.

1. 사전 준비 사항

가상환경 활성화

python -m venv .venv
source .venv/bin/activate

필요 라이브러리 설치

pip install -U langgraph langsmith langchain_anthropic tavily-python langchain_community

환경변수 설정

  • Anthropic API 키: export ANTHROPIC_API_KEY="..."
  • Tavily API 키: export TAVILY_API_KEY="..."

(Windows PowerShell: $env:ANTHROPIC_API_KEY="..." 식)

2. 코드 전체 (ch2_chatbot_with_tools.py)

아래 코드를 chatbot_with_tools.py로 저장한 뒤, python chatbot_with_tools.py로 실행해보세요.

import os

# Anthropic Claude 연결
from langchain_anthropic import ChatAnthropic

# LangGraph 핵심 구성요소
# - StateGraph: 그래프 정의
# - START, END: 시작/종료 지점
# - add_messages: 새로운 메시지를 State에 append하는 편의함수
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

# 웹 검색 툴 (Tavily)
from langchain_community.tools.tavily_search import TavilySearchResults

# 미리 만들어진 'ToolNode'와 'tools_condition' (LangGraph 제공)
# - ToolNode: 여러 툴(도구) 실행을 자동 처리
# - tools_condition: "LLM이 툴을 호출했는지" 보고 다음 노드를 결정
from langgraph.prebuilt import ToolNode, tools_condition

# 타입 정의
from typing import Annotated
from typing_extensions import TypedDict

################################################################################
# 1) State(상태) 정의
################################################################################
# - messages: 사용자/AI 메시지를 저장해두는 리스트
# - add_messages: 새 메시지를 append

class State(TypedDict):
    messages: Annotated[list, add_messages]

################################################################################
# 2) 그래프 빌더 생성
################################################################################
# - 우리가 만든 State 구조를 기반으로 동작하는 StateGraph
# - 노드와 엣지를 등록한 후, compile() 하면 실행 가능해짐

graph_builder = StateGraph(State)

################################################################################
# 3) LLM + 툴 생성
################################################################################
# (A) Tavily 웹 검색 툴
search_tool = TavilySearchResults(
    max_results=2,  # 검색 결과 최대 개수
    tavily_api_key=os.environ.get("TAVILY_API_KEY")  # 환경변수에서 가져오기
)

# (B) Anthropic Claude
llm = ChatAnthropic(
    model="claude-3-5-sonnet-20241022",  # 원하시는 모델 버전으로 수정 가능
    anthropic_api_key=os.environ.get("ANTHROPIC_API_KEY")
)

# (C) LLM + 툴 바인딩
# - LLM이 JSON 포맷을 통해 툴을 "호출"할 수 있게 설정
llm_with_tools = llm.bind_tools([search_tool])

################################################################################
# 4) 노드(Node) 정의
################################################################################
# (1) chatbot 노드
# - state["messages"]를 보고 LLM이 답변
# - 필요하면 (tool_calls)를 포함하여, 툴을 호출하도록 지시
def chatbot_node(state: State):
    ai_response = llm_with_tools.invoke(state["messages"])
    return {
        "messages": [ai_response]
    }

# (2) tools 노드
# - LLM 메시지에 툴 호출 정보(tool_calls)가 있으면
#   실제 검색 툴을 실행하고, 그 결과를 messages에 append
# -> LangGraph에는 이미 'ToolNode'라는 편의 클래스가 있으므로 직접 구현 필요 없음
#   이번에는 prebuilt된 ToolNode를 사용해보겠습니다.

tool_node = ToolNode(tools=[search_tool])

################################################################################
# 5) 그래프 빌더에 노드 등록
################################################################################
# - "chatbot" 노드 -> chatbot_node(함수)
graph_builder.add_node("chatbot", chatbot_node)

# - "tools" 노드 -> tool_node(객체)
#   => tool_node(...) 실행 시 LLM이 요청한 툴들을 병렬/직렬 등으로 실행 가능
graph_builder.add_node("tools", tool_node)

################################################################################
# 6) 조건부 엣지(conditional_edges) 설정
################################################################################
# - LangGraph의 핵심: "조건부 엣지"로 다음 노드를 동적으로 결정
# - tools_condition:
#   => "LLM이 툴을 호출했으면 tools 노드로, 아니면 END로" 라우팅
# - chatbot 노드가 실행된 후, tools_condition로 분기
#   (예: "search_tool" 호출 요청이 있으면 'tools'로 이동 / 없으면 'END')

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,  # condition 함수
    {
        "tools": "tools",  # condition이 "tools"라 반환하면, 실제로 "tools" 노드로 이동
        END: END           # condition이 END라 반환하면 그래프 종료
    }
)

################################################################################
# 7) 추가 엣지 설정
################################################################################
# - "tools" -> "chatbot" : 툴 실행 후 다시 LLM에 돌아와 응답을 마무리
# - START -> "chatbot"   : 시작하면 곧바로 챗봇 노드부터 실행

graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")

################################################################################
# 8) 컴파일
################################################################################
# - 이렇게 해서 완성된 노드/엣지 구조를 실제로 "실행할 수 있는" 그래프로 변환
graph = graph_builder.compile()

################################################################################
# 그래프 시각화를 위한 ASCII 아트 생성
################################################################################
# - graph.get_graph().draw_ascii()를 호출하여 그래프 구조를 ASCII 아트로 표현
# - 노드와 엣지의 연결 관계를 시각적으로 보여줌

ascii_graph = graph.get_graph().draw_ascii()

print(ascii_graph)

################################################################################
# 9) 메인 함수
################################################################################
def main():
    print("==== [ch2] LangGraph + Tools (Conditional) Demo ====")
    print("Type 'quit' or 'q' to exit.\n")

    while True:
        user_input = input("User: ")
        if user_input.lower() in ["quit", "q", "exit"]:
            print("Goodbye!")
            break

        # 그래프 실행
        # - 입력 형태: {"messages": [("user", user_input)]}
        # - chatbot 노드 -> tools_condition -> (tools or end) -> ...
        # - 툴 실행이 발생하면 "tools" 노드가 검색 수행, 결과를 messages에 저장
        # - 이후 다시 chatbot 노드에서 최종 답변
        events = graph.stream({"messages": [("user", user_input)]})

        # 실행 중 발생하는 event(상태 업데이트)마다 새 메시지가 있다면 출력
        for event in events:
            for node_output in event.values():
                if "messages" in node_output:
                    # 가장 마지막 메시지(새로운 답변)
                    last_msg = node_output["messages"][-1]
                    # AIMessage의 .content 속성 출력
                    print("Assistant:", last_msg.content)

if __name__ == "__main__":
    main()

3. 주요 흐름 설명

  1. chatbot 노드가 사용자 메시지를 보고 Claude에게 질의
    • Claude가 “외부 검색이 필요하다”고 판단하면, tool_callssearch_tool을 호출하는 JSON을 담아 반환
  2. 조건부 엣지 (tools_condition)
    • LLM 응답에 툴 호출이 있으면 "tools" 노드로 이동, 없으면 END로 종료
  3. tools 노드
    • 실제 TavilySearchResults를 호출해 검색 결과(JSON)를 messages에 추가
    • 이후 다시 "chatbot" 노드로 돌아가, 검색 결과를 이용해 최종 답변 생성
  4. 메인 루프
    • 사용자 입력 -> 그래프 stream -> (필요 시 검색) -> 최종 답변 출력
    • “quit” 입력 시 종료

4. 실행 예시

 

$ python ch2_chatbot_with_tools.py
        +-----------+         
        | __start__ |         
        +-----------+         
              *               
              *               
              *               
        +---------+           
        | chatbot |           
        +---------+           
         .         .          
       ..           ..        
      .               .       
+-------+         +---------+ 
| tools |         | __end__ | 
+-------+         +---------+ 
==== [ch2] LangGraph + Tools (Conditional) Demo ====
Type 'quit' or 'q' to exit.

User: LangGraph는 뭐야?
Assistant: (Claude가 답변 중...)
Assistant: ... (검색 필요 판단) tool_calls -> search_tool
Assistant: ... (검색 결과) ...
Assistant: LangGraph는 ~~~ 입니다!
  • Claude가 학습된 지식만으로 답할 수 있으면 툴을 안 쓰고 바로 끝(END)
  • 추가 정보가 필요하면 tool_calls를 통해 search_tool을 실행하고, 검색 결과를 메시지로 받아와 더 풍부한 답변을 구성합니다.

이상으로, LangGraph + Anthropic Claude + TavilySearchResults를 이용한 도구(검색) 활용 챗봇 예시(ch2)를 살펴봤습니다. 소스코드는 간단하지만 add_conditional_edges에 대해 생소하시다면 완벽히 이해가 되지 않을 수 있습니다. 이 부분에는 다음 챕터에서 살펴보겠습니다.

0
0개의 댓글
로그인 후 이용해주세요!