왜 LangGraph를 사용했는가?

하나둘셋·2024년 11월 8일
post-thumbnail

LangGraph 공식 문서

저번 게시물에서 프로그램에 대해 간단하게 소개하며 개발하면서 까다로웠던 점들을 이야기했다. 다시 한번 그 부분을 살펴보며 LangChain과 LangGraph 라이브러리로 어떻게 구현할 예정인지 살펴보겠다.

( 아직 개발 능력이 부족해 잘못 판단한 부분이 있을 수 있습니다 🥲)



개발하면서 까다로웠던 부분


프로그램에 사용되는 LLM은..

1. 이전 대화 내용을 기억할 수 있어야 한다.
자연어 처리 모델를 이용해 대화를 하다 보면 대화 내용이 쌓이게 되는데, 앞선 대화 속에 사용자의 요구사항을 모델이 기억하고 알맞은 답변을 하기 위해서는 대화 내용을 기억할 수 있어야 한다.

👉 LangChain과 LangGraph의 memory를 사용할 수 있다.


2. 도구를 호출할 수 있어야 한다.
사용자의 다양한 요구에 맞는 능동적인 답변을 생성하기 위해서 사용자의 질문과 관련된 함수를 작동시킬 수 있도록 구성하였다. 따라서 위에서 설명한 여행지 추천, 여행 계획 생성, 특정 장소 검색 기능은 각각 다른 함수들로 구성되어 있고, 사용자 질문의 의도를 파악한 후 모델이 관련 함수를 호출한 후 답변을 생성한다.

EX. 사용자 질문: 강릉에서 아이와 함께 갈 수 있는 장소 알려줄래?
    -> 모델은 여행지 추천 함수를 작동 후 작동 결과를 다음 단계로 전달

👉 LangChain의 AgentExecutor를 통해 가능
👉 LangGraph의 사전 구축된 create_react_agent 를 통해 가능


3. 항상 JSON 형식으로 답변을 생성해야 한다.
LLM의 답변을 JSON 형식으로 만들어 웹 서버로 전달해야 하며 FastAPI를 사용한다. 웹 서버에서는 JSON을 받아 필요한 값을 추출해 질문에 가장 효과적인 방식으로 답변을 재구성한다. JSON의 key는 answerplace가 존재한다.

👉 LangChain with_structured_output() 메서드를 이용한 모델의 답변을 구조적 데이터로 반환 가능
👉 JSON 스키마 설정을 출력 형태로 전달해 구조적 출력이 가능한 모델이 있음 (EX. OpenAI)


4. 상황에 따라 생성해야 하는 JSON 값이 다르다.
간단하게 예를 들어 설명하면,

  1. 여행지 추천의 경우
    장소의 그림과 정보를 전달해 카드 형식으로 사용자에게 보여줘야 하기 때문에 다음과 같이 구성된다. answer 값으로 짧은 추천의 말과, place에는 장소의 정보가 전달된다.
	{
		"answer" : "AI의 짧은 추천의 말",
    	"place" :   
    		[
    			{
 					"place_name" : "장소 이름",
                	"description" : "장소 설명",
                	"redirection_url" : "장소의 상세페이지 이동 경로"
    			},
            	...
        	]
	}
  1. 여행 계획 생성의 경우
    사용자에게 전달되는 답변은 긴 문자열이기 때문에
    여행 계획의 긴 내용이 answer 키의 값으로 들어가고, place 값은 null로 된다.
	{
		"answer" : "1박 2일의 긴 여행 계획 내용",
    	"place" : null
    }

👉 Pydantic Class , TypedDict, Json-Schema 등의 방식으로 원하는 형태의 답변 출력을 의도할 수 있음



어떻게 구현해볼까?

1. LangChain과 LangGraph의 사전 구축 Agent 사용

import getpass
import os

from typing import Optional
from langgraph.prebuilt import create_react_agent
from langchain_core.pydantic_v1 import BaseModel, Field
from available_functions import callable_tools


os.environ["OPENAI_API_KEY"] = getpass.getpass()

from langchain_openai import ChatOpenAI

# Pydantic
class Joke(BaseModel):
    """Joke to tell user."""

    setup: str = Field(description="The setup of the joke")
    punchline: str = Field(description="The punchline to the joke")
    rating: Optional[int] = Field(
        default=None, description="How funny the joke is, from 1 to 10"
    )

llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(Joke)

# 호출할 함수 리스트 가져오기
tools_of_list = callable_tools

agent = create_react_agent(
    model=structured_llm,
    tools=tools_of_list
)

response = agent.invoke({"messages": [("human", "안녕?")]})
print(response["messages"])

간단하게 코드를 짜서 가능성만 확인해본다.
create_react_agent 메서드는 Langgraph의 사전 구축 Agent로 이를 통해 쉽게 도구를 호출하고 작동시킬 수 있다. 또한 LLM의 답변이 JSON 형식으로 나오도록 with_structured_output()를 사용하고 싶었다.

하지만 이 코드는 오류가 발생해 작동될 수 없다.
구조적 출력이 가능한 모델을 create_react_agent의 model 파라미터로 전달할 수 없기 때문이다. 오류 메시지는 다음과 같다.

무슨 오류인가?

AttributeError: 'RunnableSequence' object has no attribute 'bind_tools 

Create_react_agent는 내부적으로 모델의 bind_tools 메서드를 호출한다.
with_structured_output()의 리턴값은 RunnableSequence 객체이기 때문에 이 객체는 bind_tools 메서드를 작동시킬 수 없기 때문에 일어나는 오류이다.

LangChain의 기존 AgentExecutor를 사용해도 같은 문제가 일어나게 되었다.

create_react_agent의 결과를 이용한 후처리 단계를 chain에 달면 되지만 우리 프로그램의 복잡한 로직을 구현하기에 한계가 있다고 생각했다.




2. LangGraph를 통해 나만의 Agent를 만들어보자

LangChain의 AgentExecutor와 사전 구축되어 제공되는 agent에 한계를 느낄 때쯤, 구조적 출력이 가능한 agent 생성 글을 보게 되었다.

How to return structured output with a ReAct style agent

LangChain은 보통 도구 호출 후 결과가 LLM에게 다시 반환되어 이 결과를 참고하여 답변을 생성한다. 이 과정이 일정하게 묶여있어 도구 호출 결과에 따라 다른 후처리 과정을 구현하기가 어렵다고 생각되었다.

LangGraph를 사용하면 도구 호출 결과에 따라 다른 과정을 구현하고 검토하기 편리하며, 에이전트의 흐름을 직접 지정하고 제어할 수 있다는 점에서 유연하다고 판단했다.

또한 좋은 점은 프로그램 각 단계의 상태를 저장하고 확인할 수 있기 때문에 데이터의 일관성과 올바른 실행 순서를 보장할 수 있다는 점이다.




나만의 Agent는?


현재 흐름은 단순하다.

  1. 언어 모델이 로드되어 있는 agent 노드에 사용자 메시지 전달

  2. 사용자 메시지가 호출할 수 있는 함수와 관련이 있는지 판단 후 다음 단계 진입, 관련이 있는 경우 AI가 tool_calls 변수를 가진 메시지 생성

  3. 호출 가능한 함수가 존재한다면, tools 노드로 진입 후 작동, 이때 질문과 관련된 정보 검색도 호출된 함수 안에서 실행
    호출 가능한 함수가 없다면, AI의 일반적인 답변을 생성한 후 다음 노드로 전달

  4. 호출 결과는 respond 노드로 전달되어 호출된 함수에 따라 다른 지시사항과 함께 적용되어 답변 생성 (답변은 JSON 형태로 생성)

  5. 생성된 답변이 지정된 JSON 형식으로 잘 생성되고, 올바른 JSON인지 확인하는 단계를 json-processing 노드에서 진행 후 답변을 사용자에게 전달한다.

이제 이러한 흐름의 자세한 코드 내용은 다음 게시물에서 이어진다.

profile
하나씩 뚝딱뚝딱

0개의 댓글