1. 도입부 (Introduction)
단순히 LLM에게 질문을 던지고 텍스트 응답을 받는 시대는 지났다. 이제는 LLM이 스스로 필요한 정보를 찾고, 계산기를 두드리며, 이메일을 보내고, 데이터베이스를 조회하는 등 '도구(Tool)'를 직접 조작하는 에이전트(Agent)의 시대다.
하지만 많은 개발자가 LangChain을 활용해 에이전트를 구현하다가 복잡한 메시지 관리와 제어 흐름의 한계에 부딪힌다. 이 문제를 아키텍처 수준에서 해결하기 위해 등장한 패러다임이 바로 LangGraph다.
이번 포스팅에서는 Anthropic이 정의하는 '진정한 에이전트'의 의미를 짚어보고, LangChain과 LangGraph에서 도구를 활용하는 방식의 결정적인 차이점을 비교해 본다. 나아가 웹 검색, 로컬 RAG, 그리고 Gmail API까지 결합한 강력한 멀티 도메인 하이브리드 에이전트를 구축하는 실전 코드까지 완벽하게 파헤쳐 보겠다.
2. Workflow vs. "찐" Agent: LangGraph의 본질 이해하기
LangGraph를 다루기 전에 먼저 짚고 넘어가야 할 철학적인 경계선이 있다. 우리가 만드는 시스템은 워크플로우(Workflow)인가, 아니면 진정한 에이전트(Agent)인가?
[워크플로우 (Flow)] ────────────────> [결정론적 경로 (개발자가 노드와 엣지로 고정)]
[진정한 에이전트 (True Agent)] ─────> [자율적 루프 (LLM이 스스로 탐색, 검증 및 도구 선택)]
LangGraph의 구조는 워크플로우에 가깝다
LangGraph에서 개발된 에이전트들은 주로 LLM이 분기점에서 "Yes" 또는 "No"를 판단하거나 특정 도구를 호출할지 여부만 결정한다. 실제 동작의 흐름은 개발자가 미리 정의해 둔 노드(Node)와 엣지(Edge)의 뼈대 위에서만 움직인다. 즉, 시스템 전체의 통제권이 개발자의 기획 하에 안전하게 묶여 있는 구조다.
Anthropic이 정의하는 "진정한 에이전트(True Agent)"
반면, Anthropic은 에이전트를 "스스로 다음 단계를 판단하고 유연하게 문제를 해결하는 시스템"으로 정의한다. 진정한 에이전트는 고정된 그래프에 얽매이지 않고, 스스로 확보한 정보의 무결성을 검증하며, 정보가 부족하거나 오류가 발생했을 때 스스로 답변을 수정하거나 재작성하는 자율적인 루프(Autonomous Loop)를 수행한다.
LLM 역할의 패러다임 전환: Function Calling에서 Tool Calling으로
과거에는 LLM이 특정 함수를 실행하기 위한 인자(Arguments)를 정형화된 JSON 형태로 파싱해 주는 'Function Calling'에 머물렀다. 그러나 최신 모델들은 여러 개의 도구를 한 번에 다중 호출(Parallel Tool Calling)하거나, 복잡한 상태를 추적하며 적절한 도구를 유기적으로 선택하는 'Tool Calling' 개념으로 완전히 전환되었다.
LangGraph는 이러한 LLM의 도구 활용 능력을 극대화하면서도, 자율성이 폭주하여 발생할 수 있는 오류(비용 과다 청구, 무한 루프 등)를 결정론적인 그래프 아키텍처로 안전하게 감싸 통제할 수 있게 해주는 최고의 프레임워크다.
3. LangChain에서 도구(Tool) 활용 방법: 클래식한 수동 제어
LangChain에서 도구를 활용하기 위해서는 도구의 정의, LLM 바인딩, 그리고 복잡한 메시지 상태관리를 개발자가 직접(Manual) 처리해 주어야 한다.

코드 구현을 통해 보는 수동 제어의 실체
# 1. 간단한 사칙연산 도구 정의 (@tool 데코레이터 사용)
from langchain_core.tools import tool
@tool
def add(a: int, b: int) -> int:
"""숫자 a와 b를 더합니다."""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""숫자 a와 b를 곱합니다."""
return a * b
# 2. LLM에 도구 바인딩 (bind_tools 메서드 사용)
from langchain_openai import ChatOpenAI
small_llm = ChatOpenAI(model='gpt-4o-mini')
llm_with_tools = small_llm.bind_tools([add, multiply])
# 3. 메시지 히스토리 관리 및 수동 루프 구동
from langchain_core.messages import HumanMessage, AIMessage
query = '3 곱하기 5는?'
message_list = [HumanMessage(content=query)]
# 1차 호출: LLM이 질문을 분석하고 곱셈 도구 호출(tool_calls)을 결정
ai_message = llm_with_tools.invoke(message_list)
message_list.append(ai_message)
print("LLM의 판단 (tool_calls):", ai_message.tool_calls)
# 출력 예시: [{'name': 'multiply', 'args': {'a': 3, 'b': 5}, 'id': '...'}]
# 2차 호출: 개발자가 직접 도구 실행 및 결과를 ToolMessage로 만들어 히스토리에 주입
tool_call = ai_message.tool_calls[0]
tool_message = multiply.invoke(tool_call) # 도구 실행 (ToolMessage 반환)
message_list.append(tool_message)
# 3차 호출: 도구 결과를 전달받은 LLM이 최종 사람이 이해할 수 있는 답변 생성
final_response = llm_with_tools.invoke(message_list)
print("최종 응답:", final_response.content)
# 출력 예시: 3 곱하기 5는 15입니다.
⚠️ LangChain 방식의 한계점
- 메시지 순서의 엄격성: 대화 기록에 HumanMessage -> AIMessage (with tool_calls) -> ToolMessage 순서가 조금이라도 어긋나면 LLM은 즉시 예외를 뱉어내거나 정상적인 추론을 멈춘다.
- 보일러플레이트 코드: 도구가 여러 개로 늘어나고, 다중 도구 호출이 발생할 경우 루프 내에서 분기 처리를 위한 코드가 수백 줄로 늘어나 메인 비즈니스 로직을 오염시킨다.
4. LangGraph에서 도구(Tool) 활용 방법: 자동화된 상태 기반 루프
LangGraph는 위와 같은 번잡한 과정을 상태 그래프(StateGraph)와 프리빌트 도구 노드(ToolNode)를 통해 완벽히 자동화한다.

구조적 매커니즘 분석
- MessagesState (상태 선언): 메시지 리스트를 전역적으로 관리하며, 새로운 메시지가 들어오면 add_messages 함수가 기존 리스트 뒤에 안전하게 덧붙여(Append) 준다.
- ToolNode (도구 노드): LangGraph가 제공하는 프리빌트 노드로, 바인딩된 도구 리스트를 주입하기만 하면 알아서 AIMessage 속의 tool_calls를 찾아 도구를 병렬로 실행하고 그 결과(ToolMessage)를 반환한다.
- Conditional Edge (조건부 엣지): LLM이 도구를 더 실행해야 하는지(tool_calls 존재 여부), 아니면 여기서 멈추고 유저에게 답을 내어줘야 하는지를 판별하는 제어 장치다.
코드 구현: 깔끔하게 제어되는 LangGraph 에이전트
from langgraph.prebuilt.tool_node import ToolNode
from langgraph.graph import MessagesState, StateGraph, START, END
# 1. 도구 노드 정의
tool_list = [add, multiply]
llm_with_tools = small_llm.bind_tools(tool_list)
tool_node = ToolNode(tool_list, name="tool_node")
# 2. 에이전트 노드 생성: 오직 LLM 호출과 결과 반환만 책임진다
def agent(state: MessagesState):
messages = state['messages']
response = llm_with_tools.invoke(messages)
# 반환되는 딕셔너리는 MessagesState의 'messages' 키에 자동 누적된다
return {'messages': [response]}
# 3. 조건부 노드 (Conditional Edge) 구현: 루프를 지속할지 종료할지 판단
def should_continue(state: MessagesState):
messages = state['messages']
last_ai_message = messages[-1]
# 마지막 AI 메시지에 tool_calls가 존재한다면 도구 실행 노드로 라우팅
if last_ai_message.tool_calls:
return 'tools'
# 도구 호출이 필요 없다면 그래프를 종료(END)
return END
# 4. 그래프 아키텍처 조립
graph_builder = StateGraph(MessagesState)
# 노드 등록
graph_builder.add_node('agent', agent)
graph_builder.add_node('tools', tool_node) # 프리빌트 도구 노드 배치
# 엣지 연결 (흐름 설계)
graph_builder.add_edge(START, 'agent')
graph_builder.add_conditional_edges(
'agent',
should_continue, # 분기 판단 함수
['tools', END] # 갈 수 있는 목적지 후보군 목록
)
graph_builder.add_edge('tools', 'agent') # 도구 실행 완료 후 다시 agent로 회귀
# 컴파일
graph = graph_builder.compile()
이 구조의 뛰어난 점은 복잡한 메시지 관리나 실행 제어 루프를 개발자가 직접 작성한 와일(while) 루프로 때우지 않고, 선언형 그래프(Declarative Graph)로 시각화 및 추적할 수 있다는 것이다.
5. 실전! 멀티 도메인 하이브리드 에이전트 구축
실무에서는 단순 사칙연산 도구만 쓰지 않는다. 실제 비즈니스 환경에서는 웹 검색, 로컬 RAG 시스템, 사내 협업 도구(이메일, 슬랙) 등이 복합적으로 작용하여 의사결정을 내려야 한다.
여기서는 아래 4가지 이종 도구를 결합하여, 세금 질문에 대한 RAG 조회 -> 부족한 정보 웹 검색 -> 결과를 요약하여 본인 승인 없이 Gmail로 발송하는 무시무시하고도 정교한 하이브리드 에이전트를 조립해 보겠다.

하이브리드 에이전트 구현 코드
from langchain_community.tools import DuckDuckGoSearchRun
from langchain_google_community import GmailToolkit
from langchain_google_community.gmail.utils import get_gmail_credentials, build_resource_service
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.tools.retriever import create_retriever_tool
from langgraph.prebuilt import tools_condition, ToolNode
from langgraph.graph import MessagesState, StateGraph, START
# ==========================================
# 1. 다양한 멀티 도메인 도구 정의 및 초기화
# ==========================================
# 툴 1: 웹 검색 도구 (DuckDuckGo Search)
search_tool = DuckDuckGoSearchRun()
# 툴 2: 구글 Gmail API 툴킷 (OAuth 인증 필요)
credentials = get_gmail_credentials(
token_file="../google/gmail_token.json",
scopes=["[https://mail.google.com/](https://mail.google.com/)"],
client_sercret_file="../google/gmail_credentials.json",
)
api_resource = build_resource_service(credentials=credentials)
gmail_toolkit = GmailToolkit(api_resource=api_resource)
gmail_tool_list = gmail_toolkit.get_tools() # 메일 전송, 조회 등 다수 도구 포함
# 툴 3: 로컬 벡터 데이터베이스 기반 세금 RAG Retriever 도구
embedding_function = OpenAIEmbeddings(model='text-embedding-3-large')
vector_store = Chroma(
embedding_function=embedding_function,
collection_name='income_tax_collection',
persist_directory='./income_tax_collection'
)
retriever = vector_store.as_retriever(search_kwargs={'k': 3})
retriever_tool = create_retriever_tool(
retriever=retriever,
name='real_estate_tax_retriever',
description='2024년 12월 기준 부동산 종합부동산세(종부세) 관련 공식을 포함하고 있습니다.',
)
# 툴 4: 로컬 수식 연산 도구 (@tool 데코레이터 선언 버전)
@tool
def add(a: int, b: int) -> int:
"""숫자 a와 b를 더합니다."""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""숫자 a와 b를 곱합니다."""
return a * b
# ==========================================
# 2. 모든 도구 병합 및 LLM 바인딩
# ==========================================
all_tool_list = [add, multiply, search_tool, retriever_tool] + gmail_tool_list
# 모델 인스턴스 생성 및 바인딩
llm = ChatOpenAI(model='gpt-4o-mini')
llm_with_tools = llm.bind_tools(all_tool_list)
# 프리빌트 도구 노드 구축
tool_node = ToolNode(all_tool_list, name="tool_node")
# ==========================================
# 3. LangGraph를 이용한 에이전트 그래프 조립
# ==========================================
graph_builder = StateGraph(MessagesState)
# 에이전트 노드 정의
def agent(state: MessagesState):
messages = state['messages']
response = llm_with_tools.invoke(messages)
return {'messages': [response]}
# 노드 등록
graph_builder.add_node('agent', agent)
graph_builder.add_node('tools', tool_node)
# START 엣지 연결
graph_builder.add_edge(START, 'agent')
# 조건부 엣지 등록: 프리빌트 도우미 'tools_condition' 사용
# (마지막 메시지에 tool_calls가 있으면 'tools'로 가고 없으면 종료하는 구조를 자동으로 처리함)
graph_builder.add_conditional_edges(
'agent',
tools_condition
)
# 도구 실행 후 다시 agent 노드로 돌아오도록 고정 연결
graph_builder.add_edge('tools', 'agent')
# 최종 그래프 컴파일
graph = graph_builder.compile()
💡 실전 시나리오 추적
위 그래프에 대고 "qkrwlsdud18@gmail.com으로 집이 15억일 때 종합부동산세를 계산해 줄 수 있나요라는 질문에 대한 답을 제 승인 없이 보내주세요"라고 질문을 입력하면 다음과 같은 기막힌 동적 오케스트레이션이 일어난다.
- [agent]: 질문을 읽고 종부세 공식을 찾아야 함을 파악하고 real_estate_tax_retriever 도구 호출을 선언한다.
- [tools_condition]: 도구 호출 정보가 있으므로 tools 노드로 방향을 꺾는다.
- [tools]: 로컬 Chroma DB에서 15억 기준 종부세 산정 공식을 인출해 온다.
- [agent]: 가져온 공식을 기반으로 계산해야 하므로 multiply 및 add 도구 호출을 다시 예약한다.
- [tools]: 연산을 안전하게 실행하여 결과를 상태값에 전달한다.
- [agent]: 계산된 결과를 확인하고 이메일을 발송하기 위해 Gmail Toolkit 중 send_gmail 도구를 찾아 실행 명령을 내린다.
- [tools]: 지정된 수신자에게 메일을 완전히 은밀하게 발송하고 결과를 반환한다.
- [agent]: 최종적으로 "성공적으로 계산하고 이메일을 보냈다"는 마무리 멘트와 함께 END 처리된다.
이 모든 순서 분기를 하드코딩하지 않고, 단 몇 줄의 그래프 선언과 LLM의 자율적인 판단만으로 완벽하게 조율해 낸다.
6. 결론: 에이전트를 넘어 안전한 자동화 시스템으로
LangGraph의 진정한 가치는 자율성과 예측 가능성 사이의 황금비율을 제공한다는 점이다.
만약 완벽하게 열려 있는 프롬프트 중심의 자율 에이전트("찐 Agent")를 만든다면 예외 상황에서 길을 잃고 무한 루프에 빠져 지갑(API 비용)을 거덜 낼 위험이 있다. 그러나 개발자가 구성한 안전한 노드와 엣지의 워크플로우 틀 안에서 도구(Tool)를 활용하게 통제한다면, 우리는 "사고 치지 않고 일 잘하는 스마트한 AI 동료"를 안전하게 현업에 투입할 수 있다.
이제 단순한 파이썬 함수에 @tool 데코레이터를 붙이는 일부터 시작해 보자. 그 작은 시작이 여러분의 서비스 인프라와 LLM을 세상 모든 API 생태계와 촘촘하게 묶어주는 거대한 에이전트 시스템의 초석이 될 것이다.