LangGraph는 노드 간 상태를 공유하고, 노드들은 상태를 입력 → 처리 → 상태 업데이트 → 출력의 흐름으로 동작합니다. 출력은 다음 노드로 전달되며, 초기 상태는 시작 노드에서 주어지고, 최종 출력은 엔드 노드에서 생성됩니다.
a=10 설정 a=a+10 → a=20 (기존 값 10은 사라짐)이 방식은 기본 리듀서(Reducer)로 동작하며, 명시하지 않으면 이 방식이 사용됩니다.
addReducer를 사용해 리스트 필드에 값 추가 가능.Annotated를 사용해 필드에 add 리듀서 지정:from langgraph.graph import State
from operator import add
from typing import Annotated, List
class MyState(State):
document: Annotated[List[str], add]
None['doc1', 'doc2', 'doc3']['doc1', 'doc2', 'doc3', 'doc2', 'doc4', 'doc5']이 실습은 State Reducer를 활용하여 상태를 업데이트하는 방법을 실습해보는 과정입니다. 기본 덮어쓰기 방식, addReducer를 통한 데이터 추가 방식, 그리고 사용자 정의 커스텀 Reducer를 통해 중복 제거까지 세 가지 방식으로 상태를 관리하는 예제를 따라갑니다.
프로젝트 03번 메시지 그래프 파일 실행
기본 Reducer 실습 (덮어쓰기 방식)
DocumentState 정의 (query, documents)query만 갱신됨 → documents는 업데이트 없음addReducer 사용
Annotated로 addReducer 적용 → 기존 리스트에 appendCustom Reducer 실습 (중복 제거)
reduceUniqueDocument 정의def reduceUniqueDocument(left, right):
return list(set(left + right))Annotated로 해당 함수 등록2번) 제거됨 → 총 5개 문서만 최종 저장됨| 구분 | 설명 |
|---|---|
| 기본 Reducer | 별도 설정이 없으면, 새 값으로 상태를 덮어씀 |
addReducer | 기존 값에 추가(append)하는 방식 |
| Custom Reducer | 사용자가 원하는 방식대로 상태 병합 가능 (예: 중복 제거, 조건 필터링 등) |
Annotated | 상태 필드에 reducer를 붙이는 데 사용되는 데코레이터 역할 |
| 그래프 흐름 | start → Node1 → Node2 → Node3 → end 순서대로 선형 연결 |
.
messagesmessages라는 키에 리스트 형태로 저장됨addReducer를 사용addMessages라는 기본 Reducer 제공add 함수는 단순 리스트 추가만 지원messages: list[BaseMessage] 필드 추가@reduce 데코레이터로 addMessages 함수를 연결MessageState 클래스를 미리 제공messages 키가 기본 포함document, grade, num_generation 등)는 별도로 정의 가능Retrieve and Respond 노드
Grade Answer 노드
조건부 엣지 사용
End 노드로 종료Retrieve and Respond로 피드백 루프from typing import Annotated, List
from langchain_core.messages import AnyMessage
from langgraph.graph import State
from langgraph.utils import add_messages
class ChatState(State):
messages: Annotated[List[AnyMessage], add_messages]
messages: 모든 종류의 메시지를 저장할 수 있는 필드.add_messages: 리스트에 메시지를 누적 저장하는 리듀서.MessageState를 기본으로 제공.State를 상속하지 않고 MessageState를 상속하면 messages 필드 자동 포함됨.from typing import List
from langchain_core.documents import Document
class GraphState(MessageState):
documents: List[Document] # 검색된 문서
grade: float # 답변 점수 (0~1)
num_generations: int # 답변 생성 횟수
documents: 벡터 DB에서 검색된 문서들 (덮어쓰기 방식).grade: 답변 품질 점수.num_generations: 루프 제어를 위한 생성 횟수 카운트.Chroma, 임베딩 모델: Ollama/BGE-M3RunnableParallel을 통해 다음 수행:question: 사용자 입력 → PassThroughdocuments: 유사도 검색 후 → format_docs 함수로 문자열 병합prompt: 시스템 메시지 + 사용자 쿼리로 구성AIMessage로 변환되어 messages 상태에 추가됨Pydantic 스키마 정의:class GradeResponse(BaseModel):
score: float # 0 ~ 1
explanation: str
평가 프롬프트 구성:
질문/답변/컨텍스트 기반으로 평가 요청 → 구조화된 출력 (GradeResponse) 생성
평가 노드에서는:
messages[-1]: 최근 AI 응답messages[-2]: 이전 사용자 질문documents: 컨텍스트 문서grade, num_generations 상태 필드 업데이트def route(state):
if state.num_generations > 2:
return "generate"
elif state.grade < 0.7:
return "retrieve_and_respond"
else:
return "generate"
노드 연결 흐름:
start → retrieve_and_respond → grade_answer ────┐
▼
조건 분기 (route)
┌────────────┬────────────┐
▼ ▼
retrieve_and_respond generate(end)
그래프 실행 예시:
initial_state = {
"messages": [HumanMessage(content="채식주의자를 위한 메뉴 추천해줘")]
}
final_state = graph.invoke(initial_state)
이번 실습에서는 피드백 루프가 포함된 Graph 구조(LangGraph)를 기반으로, 이를 Gradio 챗봇 인터페이스로 확장하여 구현하는 과정을 다룹니다.
기존 Gradio 챗봇 구조에서 크게 달라진 부분은 없고, 상태(state)를 초기화하고 메시지를 전달하는 방식에서 LangGraph와 연동된 부분이 핵심입니다.
Gradio 임포트 및 기본 설정
gradio as gr 임포트챗봇 동작 함수 정의 (answer_invoke)
historyhistory는 튜플 리스트: (HumanMessage, AIMessage)initial_state로 구성initial_state를 LangGraph에 전달content 속성 (문자열)Gradio 인터페이스 구성
gr.ChatInterface에 함수 등록실행
| 요소 | 설명 |
|---|---|
| LangGraph | 메시지 흐름과 상태 업데이트를 관리하는 피드백 루프 구조 기반 |
| initial_state | history의 마지막 두 메시지 + 사용자 입력 메시지를 포함 |
| 출력 | state["messages"][-1].content를 추출하여 챗봇 응답으로 반환 |
| Gradio 챗봇 | 입력과 출력을 문자열로 구성해야 하므로 content만 추출 |
| 예시 질문 | "파스타에 어울리는 음료가 뭐예요?" 등 3가지 질문 추가 가능 |
| 추론 방식 | 외부 검색 기반일 수도 있고, AI의 내재 지식 기반일 수도 있음 |