LangGraph 에이전트와 커스텀 그래프
LangGraph를 사용하면 State를 공유하는 Node를 만들고, Edge·Command·Send로 실행 흐름을 직접 제어할 수 있습니다. Checkpointer를 붙이면 thread 단위의 실행 상태를 지속할 수 있고, Store를 사용하면 thread 밖에서도 다시 사용할 장기 데이터를 별도로 관리할 수 있습니다.
하지만 이런 기능을 사용할 수 있다고 해서 모든 agent를 StateGraph로 직접 그리는 것이 좋은 설계는 아닙니다. 모델이 도구를 선택하고, tool result를 다시 읽은 뒤 필요하면 모델을 다시 호출하는 일반적인 tool-calling loop는 LangChain의 create_agent()가 이미 LangGraph 위에서 구성해 줍니다.
반대로 다음 실행 단계가 모델의 판단이 아니라 애플리케이션의 업무 규칙으로 정해져 있다면 직접 그래프를 표현할 이유가 생깁니다. 그리고 그 중간에는 표준 agent loop는 유지하면서 model call, tool call, context, human approval 같은 동작만 바꾸는 middleware가 있습니다.
따라서 핵심은 create_agent()와 StateGraph 중 어느 것이 더 강력한지를 고르는 것이 아닙니다. 어떤 실행 결정을 framework의 표준 agent loop에 맡기고, 어디부터 애플리케이션의 명시적인 상태 전이로 표현할 것인지를 구분하는 것입니다.
1장. 표준 tool-calling loop
도구를 사용하는 agent의 기본 흐름은 다음과 같습니다.
사용자 입력
↓
모델
├─ tool call 없음 → 최종 응답
│
└─ tool call 있음
↓
도구
↓
ToolMessage
↓
모델
모델이 도구를 한 번만 호출한다는 보장은 없습니다. 첫 번째 tool result를 확인한 뒤 다른 도구를 호출할 수도 있고, 필요한 정보가 모일 때까지 같은 흐름을 여러 번 반복할 수도 있습니다.
이 구조를 직접 그래프로 표현하려면 적어도 다음과 같은 요소가 필요합니다.
messages를 보관할 State
모델을 호출하는 Node
tool call을 실행하는 Node
tool_calls 존재 여부에 따른 분기
tools → model로 돌아오는 반복 경로
종료 조건
이 구조는 특정 애플리케이션의 고유한 업무 흐름이라기보다 tool-calling agent에서 반복해서 등장하는 실행 패턴입니다. 따라서 매번 같은 그래프를 직접 만드는 것보다 이를 상위 API로 감싸는 것이 자연스럽습니다.
2장. create_agent()가 표준 루프를 만든다
현재 LangChain에서는 일반적인 tool-calling agent의 기본 진입점으로 create_agent()를 제공합니다.
from langchain.agents import create_agent
from langchain.tools import tool
@tool
def search(query: str) -> str:
"""검색 결과를 반환한다."""
return f"검색 결과: {query}"
agent = create_agent(
model="openai:gpt-5.4",
tools=[search],
system_prompt="필요하면 검색 도구를 사용하세요.",
)
실행할 때는 messages를 포함한 State를 전달합니다.
result = agent.invoke({
"messages": [
{"role": "user", "content": "이 주제를 조사해줘"}
]
})
create_agent()가 만드는 graph에서는 모델을 호출한 뒤 AIMessage에 tool_calls가 있으면 해당 도구를 실행하고, 결과를 ToolMessage로 messages에 추가한 뒤 모델을 다시 호출합니다. 더 이상 tool call이 나오지 않으면 반복을 종료합니다.
따라서 create_agent()의 의미는 단순히 몇 줄의 코드를 줄이는 데 있지 않습니다. model → tools → model이라는 반복 구조와 messages 기반 상태를 표준 agent graph로 제공하고, 그 위에 structured output, middleware, checkpointer, store 같은 기능을 연결할 수 있게 하는 것이 핵심입니다.
그리고 create_agent()의 반환값은 CompiledStateGraph입니다. 즉 LangChain agent와 LangGraph는 서로 경쟁하는 별개의 실행 방식이 아니라, 고수준 agent API와 그 아래의 graph runtime이라는 관계에 가깝습니다.
3장. create_react_agent()는 어디로 갔나
기존 LangGraph 코드를 보면 다음 API가 자주 등장합니다.
from langgraph.prebuilt import create_react_agent
langgraph.prebuilt.create_react_agent()는 model과 tools를 받아 반복적인 tool-calling agent graph를 만들어 주던 prebuilt factory입니다.
현재 이 함수는 deprecated되었고, 공식 reference도 같은 용도의 고수준 agent factory로 langchain.agents.create_agent()를 사용하도록 안내합니다.
기존
langgraph.prebuilt.create_react_agent()
↓
현재
langchain.agents.create_agent()
여기서 주의할 점이 하나 있습니다. create_react_agent라는 이름의 함수가 하나만 있었던 것은 아닙니다.
langchain_classic.agents.create_react_agent()도 존재하는데, 이것은 ReAct 논문의 Thought → Action → Observation 형식을 prompt와 output parser로 구성하는 이전 LangChain API입니다. 현재의 tool-calling agent factory와 같은 API가 아닙니다.
langgraph.prebuilt.create_react_agent
tool-calling graph factory
현재 deprecated
langchain_classic.agents.create_react_agent
전통적인 ReAct prompting 기반 agent
legacy 계층
langchain.agents.create_agent
현재 표준 agent factory
따라서 오래된 코드를 볼 때는 create_react_agent라는 함수 이름만 보는 것이 아니라 어느 package에서 import했는지를 확인해야 합니다.
4장. 상위 agent API 안에도 State가 있다
create_agent()를 사용한다고 LangGraph의 State 개념이 사라지는 것은 아닙니다.
기본 agent state에는 messages가 들어 있고, 필요한 경우 애플리케이션 전용 필드를 추가할 수도 있습니다.
from langchain.agents import AgentState, create_agent
class MyState(AgentState):
user_id: str
request_type: str
agent = create_agent(
model="openai:gpt-5.4",
tools=[search],
state_schema=MyState,
)
현재 state_schema로 추가하는 custom state는 AgentState를 확장한 TypedDict 형태를 사용합니다.
다만 custom state가 특정 middleware의 동작을 위해 필요한 값이라면, 현재 LangChain 문서는 create_agent(state_schema=...)에 모든 필드를 모으기보다 해당 middleware의 state schema로 필요한 필드를 함께 정의하는 방식을 권장합니다. 이렇게 하면 상태 필드와 그것을 사용하는 로직의 범위를 같이 묶을 수 있습니다.
즉 state_schema는 여전히 사용할 수 있지만, 모든 확장을 agent 최상위 State에 넣어야 하는 것은 아닙니다.
Checkpointer와 Store도 create_agent()에 연결할 수 있습니다.
create_agent
├─ model
├─ tools
├─ middleware
├─ custom state
├─ checkpointer
└─ store
따라서 상위 agent API를 사용하면서도 LangGraph의 persistence와 state 관리 모델을 그대로 활용할 수 있습니다.
5장. Middleware는 표준 루프의 동작을 바꾼다
Custom requirement가 생겼다고 바로 StateGraph를 직접 만들 필요는 없습니다.
LangChain agent middleware는 표준적인 model-tool 반복 구조는 유지하면서 그 실행 과정의 특정 지점에 정책이나 동작을 추가하는 계층입니다.
Middleware는 model 호출 전후와 tool 실행 주변에 개입할 수 있습니다.
Agent 시작
↓
before_model
↓
Model
↓
after_model
↓
Tool 호출이 있다면
↓
wrap_tool_call
↓
Tool
↓
다시 Model
현재 LangChain에는 긴 대화를 압축하는 summarization, model/tool 호출 횟수 제한, tool retry, model fallback, human-in-the-loop 같은 middleware도 제공됩니다.
예를 들어 다음 요구들은 표준 agent loop의 구조 자체를 새로 만들기보다 middleware에서 해결하기 좋은 문제입니다.
요청마다 system prompt 변경
model 호출 전 context 정리
상황에 따라 사용할 model 변경
tool argument 검사
tool error retry
특정 tool 실행 전 사람 승인
model/tool 호출 횟수 제한
PII 검사
custom state 추가
특히 HumanInTheLoopMiddleware를 사용하면 특정 tool call이 실행되기 전에 graph를 interrupt하고 사람에게 승인·수정·거절 같은 결정을 요청할 수 있습니다. 중단된 실행을 나중에 이어야 하므로 이런 흐름에서는 checkpointer도 함께 구성해야 합니다.
따라서 설계 경계는 다음처럼 잡을 수 있습니다.
표준 tool-calling loop 그대로
→ create_agent()
표준 loop는 유지
+ 실행 지점에 정책 추가
→ create_agent() + middleware
실행 경로 자체를 변경
→ custom StateGraph
6장. create_agent()가 잘 맞는 문제
모델이 상황을 보고 필요한 도구를 선택하는 것이 문제의 핵심이라면 일반적으로 create_agent()에서 시작하는 편이 단순합니다.
예를 들어 검색, 계산, DB 조회 같은 여러 도구를 가진 assistant가 있다고 해 보겠습니다.
사용자 요청
↓
모델 판단
↓
필요한 tool 선택
↓
결과 관찰
↓
다음 행동 판단
여기서 정확히 어떤 tool을 몇 번 사용할지는 실행 전에 고정되어 있지 않습니다. 모델이 현재 context와 tool result를 보고 결정합니다.
검색과 계산을 결합한 질의응답, 여러 업무 도구를 사용하는 assistant, 간단한 문서 작업 agent처럼 행동 순서는 열려 있지만 agent loop 자체는 표준적인 경우가 여기에 해당합니다.
Structured output, custom state, middleware, checkpointer 같은 기능을 붙인 뒤에도 이 기본 구조가 유지된다면 굳이 동일한 model-tool loop를 직접 StateGraph로 다시 만들 이유는 크지 않습니다.
7장. 실행 순서가 업무 규칙이면 직접 그래프로 표현한다
Custom StateGraph의 가치가 커지는 시점은 다음 실행 위치 자체가 애플리케이션의 중요한 규칙이 될 때입니다.
예를 들어 보험 심사 workflow라면 모델에게 자유롭게 다음 단계를 선택하게 하기보다 실제 업무 절차를 코드로 명시해야 할 수 있습니다.
신청 접수
↓
서류 검증
↓
위험도 평가
├─ 낮음 → 자동 승인
├─ 중간 → 사람 심사
└─ 높음 → 거절
여기서는 서류 검증 다음에는 위험도 평가를 수행한다는 사실 자체가 제품의 규칙입니다. Node와 Edge가 단순 구현 세부사항이 아니라 업무 정책을 표현합니다.
생성-평가-수정처럼 정해진 반복 구조도 마찬가지입니다.
생성
↓
평가
├─ 통과 → 종료
│
└─ 실패
↓
수정
↓
평가
모델이 자유롭게 tool을 선택하는 것이 아니라 평가 결과와 retry count 같은 State가 다음 경로를 결정합니다.
여러 작업을 fan-out한 뒤 다시 합쳐야 할 때도 Graph API가 자연스럽습니다.
┌→ 웹 조사 ─┐
요청 ───┤ ├→ 결과 통합 → 작성
└→ DB 조사 ─┘
또 특정 단계에서 interrupt()를 걸고 사람의 결정을 받은 뒤 여러 경로로 이동해야 한다면, 승인 위치와 이후 transition을 graph에 직접 드러내는 편이 이해하기 쉽습니다.
핵심은 복잡하다는 이유만으로 custom graph를 쓰는 것이 아니라, 실행 topology 자체를 애플리케이션이 소유해야 하는가입니다.
8장. Agent와 custom graph는 함께 사용할 수 있다
create_agent()와 StateGraph는 서로 배타적인 선택이 아닙니다.
create_agent()가 LangGraph graph를 반환하기 때문에 표준 tool-calling agent를 더 큰 workflow의 subgraph나 Node로 조합할 수 있습니다.
예를 들어 조사 과정은 agent에게 맡기고, 그 앞뒤의 업무 절차만 명시적인 graph로 관리할 수 있습니다.
입력 검증
↓
Research Agent
↓
결과 검증
├─ 실패 → Research Agent
│
└─ 성공
↓
사람 승인
↓
보고서 생성
Parent graph와 agent가 같은 State key를 공유한다면 compiled agent를 graph의 Node로 직접 추가할 수 있습니다.
반대로 parent와 child의 State schema가 다르다면 wrapper Node에서 두 State 사이를 변환하는 방식이 자연스럽습니다.
research_agent = create_agent(
model="openai:gpt-5.4",
tools=[search],
name="research_agent",
)
def research(state: State):
result = research_agent.invoke({
"messages": [
{
"role": "user",
"content": state["question"],
}
]
})
return {
"research_result": result["messages"][-1].content
}
이 방식에서는 바깥 StateGraph가 제품 수준의 workflow를 제어하고, research_agent 내부에서는 모델이 자유롭게 여러 검색 도구를 사용할 수 있습니다.
명시적으로 제어할 영역
→ Parent StateGraph
열린 tool selection이 필요한 영역
→ create_agent subgraph
여러 agent를 조합할 때도 각 agent의 model-tool 반복을 다시 직접 그릴 필요는 없습니다. 각 agent를 하나의 실행 단위로 두고 바깥 graph에서 routing이나 handoff를 관리할 수 있습니다.
create_agent()의 name은 생성되는 compiled graph의 이름으로 사용되며, 다른 graph에 subgraph로 넣을 때 식별에도 활용할 수 있습니다.
9장. Runnable, Agent, StateGraph는 추상화 수준이 다르다
LangChain과 LangGraph의 실행 방식을 다음 세 수준으로 보면 경계를 잡기 쉽습니다.
Runnable / LCEL
prompt → model → parser 같은
선형·병렬 dataflow 조합
create_agent
model이 다음 행동을 결정하는
표준 tool-calling loop
StateGraph
애플리케이션이 명시적으로 정의하는
state transition과 workflow
물론 이 경계가 API의 절대적인 기능 한계라는 뜻은 아닙니다.
Runnable에도 조건 분기를 넣을 수 있고, middleware도 agent 실행에 상당한 제어를 추가할 수 있으며, custom graph 안에도 agent를 넣을 수 있습니다.
따라서 어떤 기능이 “가능한가”보다 어떤 구조로 표현했을 때 실행 규칙이 가장 명확하게 드러나는가를 기준으로 선택하는 편이 좋습니다.
표준 agent로 충분한 문제를 StateGraph로 처음부터 직접 구현하면 관리해야 하는 코드가 늘어납니다. 반대로 고정된 업무 절차를 agent prompt와 tool 선택 안에 숨기면 중요한 실행 규칙이 모델의 판단 속으로 들어가 버립니다.
10장. 실행 결정을 누가 소유하는가
세 가지 수준의 차이는 결국 다음 행동을 누가 결정하는가로 정리할 수 있습니다.
정해진 데이터 변환을 순서대로 실행
→ Runnable / LCEL
모델이 context를 보고 다음 tool을 선택
→ create_agent()
표준 agent loop에 정책을 추가
→ create_agent() + middleware
애플리케이션이 다음 Node와 경로를 결정
→ custom StateGraph
큰 workflow의 일부만 agent에게 위임
→ StateGraph + create_agent subgraph
LangGraph를 배웠다고 모든 agent를 직접 graph로 만들 필요는 없습니다. 표준 tool-calling loop는 create_agent()에 맡기고, 그 반복 구조를 유지하면서 필요한 정책은 middleware로 추가할 수 있습니다.
반대로 단계의 순서, 분기, 반복, 합류, 사람 개입 위치 같은 실행 흐름 자체가 제품 요구사항이라면 custom StateGraph로 그 구조를 명시적으로 표현하는 것이 자연스럽습니다.
그리고 실제 시스템에서는 두 방법을 함께 사용할 수 있습니다. 전체 workflow의 중요한 상태 전이는 StateGraph가 관리하고, 조사나 도구 선택처럼 열린 판단이 필요한 일부 Node만 create_agent()로 만든 agent에게 맡기는 방식입니다.
LangGraph를 직접 사용한다는 것은 단순히 더 낮은 수준의 API를 선택한다는 뜻이 아닙니다. 실행 흐름의 중요한 결정을 모델에게 맡길 것인지, framework에 맡길 것인지, 애플리케이션이 직접 소유할 것인지를 구분하는 설계 선택이라고 이해하면 됩니다.
참고자료
'AI Agent > Frameworks' 카테고리의 다른 글
| LiteLLM (3) - Proxy와 AI Gateway 운영 (0) | 2024.11.08 |
|---|---|
| LiteLLM (2) - 폴백과 라우팅 (0) | 2024.10.21 |
| LiteLLM (1) - 전체 구조와 SDK (0) | 2024.08.29 |
| LangGraph (3) - 체크포인터, Interrupt와 Memory (0) | 2024.08.22 |
| LangGraph (2) - State, Node, Edge (0) | 2024.08.05 |
댓글