안녕하세요 오니입니다 🍀🐰
요즘은 회사에서 LangGraph를 사용하여 챗봇관련 과제를 하고 있는만큼 LangGraph의 구조에 대해 포스팅해보겠습니다.
🕸️ LangGraph의 특징:LangChain VS LangGraph
LangChain과 비교했을 때 LangGraph의 가장 큰 차이는 구현하는 방식, 즉 구조입니다.
- LangGraph는 그래프 기반 구조이고, LangChain은 체인기반 구조를 사용합니다. LangChain은 순차적으로 실행하지만 LangGraph는 보다 모듈화되어 여러 분기 조건에 대한 비선형적인 구조를 반영하기에 유리합니다.
- 이러한 구조의 차이로 인해 LangChain은 LLM 기반 간단한 애플리케이션을 빠르게 만들기에 좋고 LangGraph는 상태 기반 복잡한 워크플로우를 설계하는 데 강점이 있습니다.
개인 사이드 프로젝트정도라면 LangChain으로도 충분할 수 있지만 어느정도 이상의 규모가 있는 과제나 확장 가능성을 고려한다면 LangGraph가 더 적합할 수 있습니다. 실제로 과제를 진행할 때에도 LangChain으로 시작을 했으나, 현재 LangGraph로 POC를 진행중 입니다.
🕸️ LangGraph 구조
- LangGraph는 크게 3개의 구성요소로 이루어집니다.
- State: 애플리케이션의 현재 상태를 나타내는 공유 데이터 구조. 전체 워크플로우의 컨텍스트 유지, 노드 간 정보 공유
- Node: 실제 작업을 수행하며 상태 업데이트 (point)
- Edge: 노드간의 연결을 정의하는 요소. 흐름 제어 (line)
State는 Node와 Edge로 이루어진 맵을 이동하는 유저로 비유할 수 있습니다.
음식의 정보를 물으면 칼로리정보를 알려주는 에이전트를 랭그래프로 구현한다고 가정해보겠습니다.

위 그래프 예시에서 정의한 노드는 4개(parse_message, clarify_food, get_calorie_info, generate_response)이며, 엣지는 총 4개입니다. 코드로 살펴보면 다음과 같습니다. (LLM 모델을 불러오는 과정은 미포함)
1. 필요한 모듈 임포트 및 함수 정의
from typing import Literal, Optional, TypedDict
from langgraph.graph import StateGraph, END, START
# 데이터 처리를 위한 함수
def extract_food(message: str) -> Optional[str]:
words = message.split()
if "about" in words:
index = words.index("about")
if index + 1 < len(words):
return words[index + 1] # "about" 다음 단어를 음식으로 추출
def is_ambiguous(food: str) -> bool:
return len(food) < 3 # 음식명이 3글자 미만이면 애매한 것으로 간주
def fetch_calorie_info(food: str) -> str:
calorie_data = {
"apple": "52 kcal per 100g",
"banana": "89 kcal per 100g",
"chicken": "239 kcal per 100g",
"salmon": "208 kcal per 100g",
"tofu": "76 kcal per 100g",
}
return calorie_data.get(food, "Calorie data not available")
def generate_food_clarification(food: str) -> str:
return f"Could you please specify more details about '{food}'?"
def format_calorie_response(food: str, info: str) -> str:
return f"The calorie information for {food} is: {info}"
2. state 정의
-
State라는 TypedDict를 정의하여 그래프의 상태 구조를 지정합니다.
class State(TypedDict):
message: str
food: Optional[str]
calorie_info: Optional[str]
response: str
3. 노드 정의
- parse_message(state: State): 사용자의 메시지에서 음식 정보를 추출합니다.
- get_calorie_info(state: State): 추출된 음식에 대한 칼로리 정보를 가져옵니다.
- clarify_food(state: State): 음식 정보가 애매하면 추가 정보를 요청하는 메시지를 생성합니다.
- generate_response(state: State): 음식과 칼로리 정보를 조합해 최종 응답을 생성합니다.
def parse_message(state: State):
food = extract_food(state["message"])
return {"food": food, "calorie_info": state.get("calorie_info", None)}
def get_calorie_info(state: State):
calorie_info = fetch_calorie_info(state["food"])
return {"food": state["food"], "calorie_info": calorie_info}
def clarify_food(state: State):
clarification = generate_food_clarification(state["food"])
return {"message": clarification, "food": state["food"], "calorie_info": state.get("calorie_info", None)}
def generate_response(state: State):
response = format_calorie_response(state["food"], state["calorie_info"])
return {"response": response, "food": state["food"], "calorie_info": state["calorie_info"]}
4. 엣지 함수 정의
- check_food: 음식인지 결정합니다.
def check_food(state: State) -> Literal["valid", "invalid", "ambiguous"]:
if not state["food"]:
return "invalid"
elif is_ambiguous(state["food"]):
return "ambiguous"
else:
return "valid"
5. 그래프 생성
- StateGraph 인스턴스를 생성하고, 상태 타입으로 'State'를 지정합니다.
- 이때 condition_function으로 조건부 노드를 정의해줄 수 있습니다. 현재 상태를 평가하고 다음에 어떤 노드로 이동할지 결정하는 역할을 합니다.
graph_builder = StateGraph(State)
graph_builder.add_node("parse_message", parse_message)
graph_builder.add_node("get_calorie_info", get_calorie_info)
graph_builder.add_node("clarify_food", clarify_food)
graph_builder.add_node("generate_response", generate_response)
graph_builder.add_edge(START, "parse_message")
graph_builder.add_conditional_edges(
"parse_message",
check_food,
{
"valid": "get_calorie_info",
"ambiguous": "clarify_food",
"invalid": END,
},
)
graph_builder.add_edge("get_calorie_info", "generate_response")
graph_builder.add_edge("generate_response", END)
6. 정의한 그래프를 실행 가능한 형태로 변환해줍니다.
app = graph_builder.compile()
7. 실행
test1 = app.invoke({"message": "Tell me about apple"})
print(f'test1:{test1["response"]}') # 52 kcal per 100g
test2 = app.invoke({"message": "Tell me about aple"})
print(f'test2:{test2["response"]}') # Calorie data not available
해당 과정은 LangGraph의 구조파악을 위한 것으로, 실제 활용측면에서는 답변을 함수로 하드코딩하는 것이 아니라 LLM에게 task를 맡기는 방식으로 활용을 하게됩니다. 다음 포스팅에서는 RAG를 활용한 LangGraph 모델 활용에 대해 다뤄보겠습니다.