1. 이번 단계에서 달라지는 것
고정된 단방향 흐름에서 벗어나, LLM이 "직접 답할지 / 도구를 쓸지"를 스스로 판단하는 구조로 발전시킴.
①편의 그래프는 경로가 코드에 박혀 있었다. 이번에는 add_conditional_edges로 분기를 주고, LLM이 도구 호출 여부를 결정하게 만든다. 핵심 추가 요소는 다음과 같음.
- LLM 노드 — AWS Bedrock 모델을 호출해 추론하는 "판단 노드"임.
- Tool 노드 — 실제 기능(여기서는 곱셈)을 수행하는 "행동 노드"임.
- 조건부 엣지 — LLM의 응답이 도구 호출인지 텍스트인지에 따라 경로가 갈림.
①편이 직선 도로였다면, 이번 단계는 LLM이 갈림길에서 핸들을 잡는 구조다.
2. 준비 — 모델과 환경변수
Bedrock LLM 객체를 만들고 `.env`에서 모델 정보를 로드함.
from langgraph.graph import StateGraph, END, MessagesState, START
from langchain_core.tools import tool # 툴 정의용 데코레이터
from langchain_core.messages import HumanMessage # 사용자 메시지를 편하게 구성
from langchain_aws import ChatBedrock, ChatBedrockConverse # AWS Bedrock LLM 호출
from langgraph.prebuilt import ToolNode, tools_condition # 툴->노드 변환, 조건부 분기
from dotenv import load_dotenv
import os, boto3
load_dotenv()
# LLM 추론용 객체(전역). 모델별로 ChatBedrock / ChatBedrockConverse 교체 적용
# 예: us.anthropic.claude-haiku-4-5-20251001-v1:0
llm = ChatBedrockConverse(
model=os.getenv('MODEL_ID'),
client=boto3.client('bedrock-runtime', region_name=os.getenv('AWS_REGION'))
)
`MessagesState`는 LangGraph가 미리 제공하는 상태로, 대화 메시지 누적에 특화되어 있음.
2.1 MessagesState를 쓰는 이유
①편에서는 TypedDict로 상태를 직접 정의했음. 대화형 에이전트는 메시지 리스트를 계속 누적해야 하므로, 라이브러리가 제공하는 MessagesState(내부에 messages 키 보유)를 그대로 사용함.
3. 도구 정의와 등록
`@tool` 데코레이터로 함수를 LLM이 이해할 수 있는 도구로 변환함.
@tool # LLM이 이해할 수 있는 형식으로 자동 변환됨
def multiply(a: int, b: int) -> int:
'''두 수를 곱한 후 반환'''
print(f' [TOOL 실행] {a}x{b} 계산중..')
return a * b
# 여러 툴을 모아 LLM에 등록(여기서는 1개)
tools = [multiply]
llm_with_tools = llm.bind_tools(tools) # "이런 도구를 쓸 수 있다"고 LLM에 알림
`bind_tools`는 LLM에게 도구의 존재와 사용법을 알릴 뿐, 강제하지는 않음. 사용 여부는 LLM이 판단함.
함수의 docstring과 타입 힌트가 곧 LLM이 읽는 "도구 설명서"가 된다. 그래서 docstring을 명확히 쓰는 게 중요하다.
4. 노드 구성
사용자 입력을 LLM에 전달해 "직접 답변" 또는 "도구 호출"을 받아오는 판단 노드를 만듦.
def chatbot_node(state: MessagesState):
print('[chatbot_node 호출 전 상태값]', state)
# 사용자 프롬프트를 LLM에 전달 -> LLM이 직접 해결할지/도구를 쓸지 판단 -> 응답
res = llm_with_tools.invoke(state['messages']) # messages 키는 MessagesState에 정의됨
new_state = {"messages": [res]} # MessagesState 형식에 맞춤
print('[chatbot_node 호출 후 상태값]', new_state)
return new_state
LLM이 도구가 필요하다고 판단하면, `res` 안에 `tool_calls` 정보가 담겨 돌아옴.
5. 조건부 엣지로 그래프 구성
`tools_condition`이 LLM 응답을 보고 도구 노드로 보낼지 종료할지 자동 분기함.
workflow = StateGraph(MessagesState)
# 노드 추가
workflow.add_node('chatbot', chatbot_node) # 생각/판단 노드
workflow.add_node('tools', ToolNode(tools)) # 도구 수행(곱하기) 행동 노드
# 시작점 (= set_entry_point)
workflow.add_edge(START, 'chatbot') # 데이터 주입 시 가장 먼저 작동
# 조건부 엣지: 이전 노드 응답에 따라 경로 분기
workflow.add_conditional_edges(
'chatbot', # 텍스트 응답이면 -> END
tools_condition # 도구가 필요하다는 응답이면 -> tools 노드
)
# 도구 사용 -> 결과 -> 다시 chatbot으로 (순환 구조)
workflow.add_edge('tools', 'chatbot')
app = workflow.compile()
이 구조가 만들어내는 두 가지 시나리오는 다음과 같음.
| 시나리오 | 흐름 |
| 직접 응답 | 질의 → chatbot → LLM 추론 → 응답 → END |
| 도구 사용 | 질의 → chatbot → 도구 필요 판단 → tools → 결과 → chatbot → 응답 → END |
①편의 단방향 엣지가 여기서 "순환 가능한" 엣지로 진화했다. tools → chatbot이 다시 이어지면서 사이클이 생긴다.
6. 실행 — 스트리밍 루프
`stream` 모드로 응답을 실시간 출력하며 대화함.
if __name__ == '__main__':
while True:
user_input = input('\n유저:').lower()
if user_input == 'q':
break
# 프롬프트 구성
prompt = {"messages": [HumanMessage(content=user_input)]}
print(prompt)
# invoke: 동기식 / stream: 비동기식(점진 출력)
for evt in app.stream(prompt, stream_mode='values'):
msg = evt['messages'][-1] # 마지막에 추가된 응답
print("Agent", msg.content)
`stream_mode='values'`는 매 노드 실행 후 갱신된 전체 상태를 흘려보내, 진행 과정을 실시간으로 볼 수 있게 함.
6.1 모델별 동작 차이
같은 그래프라도 LLM에 따라 도구 사용 성향이 달랐음.
- OpenAI 계열 — LLM이 직접 추론해 응답. 도구를 쓰지 않는 경우가 있었음.
- Claude 계열 — 도구를 적극 사용. 도구 호출 후 재추론까지 LLM이 2회 추론하는 경우가 있었음.
요약
도구와 조건부 엣지를 더해, LLM이 경로를 스스로 결정하는 순환형 에이전트로 발전시켰음.
| 항목 | LangGraph 1 | LangGraph 2 |
| 상태 | TypedDict 직접 정의 | MessagesState 활용 |
| 흐름 | 고정 단방향 | 조건부 분기 + 순환 |
| 판단 주체 | 코드(개발자) | LLM |
| 외부 기능 | 없음 | @tool 도구 호출 |
이제 한 번의 질의는 잘 처리한다. 그러나 새 질문마다 기억이 초기화되어 이전 대화를 모른다. 다음 단계에서 단기기억을 붙인다.
'SK플래닛 ai활용 데이터엔지니어 과정 2기 > ML & DL' 카테고리의 다른 글
| LangGraph 4 - RAG 에이전트 (0) | 2026.06.02 |
|---|---|
| LangGraph 3 - 단기기억 (0) | 2026.06.02 |
| LangGraph 1 - 상태 그래프 기초 (0) | 2026.06.02 |
| LLM 2 - 서비스 구축 실습 (0) | 2026.05.29 |
| LLM 1 - 프롬프트, 컨텍스트 (1) | 2026.05.28 |