LangGraph부터 시작해서 LangChain, DeepAgent 등 여러개를 배우다보니, 개념이 자주 헷갈리게 된다. (LangChain부터 하지 않아서 그럴수도..) 그래서 공부한 내용들을 정리해보려고 한다.
LangGraph의 역할
LangGraph는 실행 흐름을 제어하는 프레임워크고, 각 노드가 LLM을 호출하든, 뭘 하든 관심이 없다.
LangGraph는 단지 state를 받아서 state를 반환하고 업데이트하는 것에 관심이 있다.
서브에이전트는 개념적 용어이고, LangGraph에서는 이를 서브그래프나 툴 호출 형태로 구현할 수 있다.
- LangChain의 create_agent()로 생성한 에이전트도 단순히 말하자면 그냥 툴노드 - 메인노드를 루핑하는 그래프다. (물론 단순화하면 그렇단거고, 실제로는 미들웨어 등 여러 요소가 들어갈 수 있다.)
Subgraph
서브그래프는 에이전트일 수도 있고, 아래처럼 단순 워크플로우일 수도 있다.
class SubState(TypedDict):
value: int
def double_node(state: SubState):
return {"value": state["value"] * 2}
sub_workflow = StateGraph(SubState)
sub_workflow.add_node("double", double_node)
sub_workflow.add_edge(START, "double")
sub_workflow.add_edge("double", END)
sub_graph = sub_workflow.compile()
서브그래프 연결 방식
LangGraph 공식 문서 기준으로 두 가지 방식이 있다.
Subgraphs - Docs by LangChain
Documentation IndexFetch the complete documentation index at: https://docs.langchain.com/llms.txtUse this file to discover all available pages before exploring further. This guide explains the mechanics of using subgraphs. A subgraph is a graph that is use
docs.langchain.com
1. 노드 안에서 서브그래프 호출
When to use: 부모와 서브그래프의 state schema가 다를 때, 또는 둘 사이에서 state를 변환해야할 때
class ParentState(TypedDict):
query: str
result: str
class SubState(TypedDict):
messages: Annotated[List, operator.add]
def call_sub_node(state: ParentState):
# 부모 state → 서브그래프 input 변환
sub_input = {"messages": [HumanMessage(content=state["query"])]}
# 서브그래프 직접 invoke
sub_result = sub_graph.invoke(sub_input)
# 서브그래프 output → 부모 state 변환
return {"result": sub_result["messages"][-1].content}
parent_workflow = StateGraph(ParentState)
parent_workflow.add_node("call_sub", call_sub_node)
2. 서브그래프를 노드로 연결
When to use: 부모와 서브그래프가 shared key/channel을 가질 때(서브그래프만의 별도의 key를 가질수도있음)
class SharedState(TypedDict):
messages: Annotated[List, operator.add] # 부모/서브 모두 동일한 key
# 컴파일된 서브그래프를 그대로 노드로 추가
parent_workflow = StateGraph(SharedState)
parent_workflow.add_node("sub_agent", sub_graph) # 변환 없이 바로 추가
parent_workflow.add_edge(START, "sub_agent")
parent_workflow.add_edge("sub_agent", END)
서브에이전트
서브그래프 안에 LLM 호출과 툴 루프가 있으면 서브에이전트라고 볼 수 있을 것 같다.
from typing import Annotated, List, TypedDict
from langchain_core.tools import tool
from langchain_core.messages import SystemMessage
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
import operator
@tool
def search_book(title: str) -> str:
"""책 제목으로 간단한 도서 정보를 검색합니다."""
book_data = {
"데미안": "헤르만 헤세의 성장소설입니다.",
"1984": "조지 오웰의 디스토피아 소설입니다.",
"어린 왕자": "생텍쥐페리의 철학적 동화입니다."
}
return book_data.get(title, "도서 정보를 찾을 수 없습니다.")
class SubAgentState(TypedDict):
messages: Annotated[List, operator.add]
def book_node(state: SubAgentState):
llm_with_tools = llm.bind_tools([search_book])
system_msg = SystemMessage(content="당신은 도서 추천 및 책 정보 전문가입니다.")
messages = [system_msg] + state["messages"]
return {"messages": [llm_with_tools.invoke(messages)]}
sub_workflow = StateGraph(SubAgentState)
sub_workflow.add_node("book_specialist", book_node)
sub_workflow.add_node("tools", ToolNode([search_book]))
sub_workflow.add_edge(START, "book_specialist")
sub_workflow.add_conditional_edges(
"book_specialist",
lambda s: "tools" if s["messages"][-1].tool_calls else END
)
sub_workflow.add_edge("tools", "book_specialist")
book_graph = sub_workflow.compile()

위 컴파일된 그래프를 다른 그래프에서 book_graph.invoke()로 호출하거나, 아예 노드로 추가하여(스키마가 동일한경우) 서브에이전트처럼 활용이 가능해진다.
book_graph.invoke()로 호출하여 사용한다면,
result = book_graph.invoke(state)
final_response = result["messages"][-1]
return {
"messages": [final_response]
}
이렇게 마지막 메시지를 추출해 사용하게 될 것이다.
서브에이전트를 Tool처럼 호출하기
서브그래프를 Tool로 만들면 메인 에이전트가 직접 호출할 수 있다.
# 서브그래프를 툴로 감싸기
@tool
def book_expert(description: str) -> str:
"""도서 관련 작업을 전문 에이전트에게 위임합니다."""
result = book_graph.invoke({
"messages": [HumanMessage(content=description)]
})
return result["messages"][-1].content
# 메인 에이전트 state
class MainState(TypedDict):
messages: Annotated[List, operator.add]
# 메인 노드
def main_node(state: MainState):
llm_with_tools = llm.bind_tools([book_expert])
return {"messages": [llm_with_tools.invoke(state["messages"])]}
# 그래프 연결
main_workflow = StateGraph(MainState)
main_workflow.add_node("agent", main_node)
main_workflow.add_node("tools", ToolNode([book_expert]))
main_workflow.add_edge(START, "agent")
main_workflow.add_conditional_edges(
"agent",
lambda s: "tools" if s["messages"][-1].tool_calls else END
)
main_workflow.add_edge("tools", "agent")
agent = main_workflow.compile()
SubAgentMiddleware로 서브에이전트 등록
위처럼 직접 Tool로 등록할 필요 없이, DeepAgent에서 제공하는 Middleware를 사용할 수도 있다.
# 서브그래프를 CompiledSubAgent로 래핑 (TypedDict)
book_subagent: CompiledSubAgent = {
"name": "book_expert",
"description": "책 제목으로 도서 정보를 조회하고 책에 대한 설명이나 추천을 제공하는 전문 에이전트입니다.",
"runnable": book_graph,
}
agent = create_agent(
model=llm,
system_prompt="당신은 도서 큐레이터입니다.",
middleware=[
SubAgentMiddleware(
backend=StateBackend,
subagents=[book_subagent]
)
],
)
agent.invoke()를 하고 LangSmith에서 보면 다음과 같이 task라는 툴로 등록된 것이 보인다.
(다른 예제 실행 결과라 서브에이전트는 약간 다르다.)

SubAgentMiddleware 내부 동작
1. task 툴을 자동으로 에이전트에 주입
└─ tools = [task_tool] 형태로 미들웨어가 주입
2. LLM이 서브에이전트 호출 판단:
AIMessage(tool_calls=[{"name": "task", "args": {
"subagent_type": "weather_expert",
"description": "서울 날씨 조회해줘"
}}])
3. subagent_graphs[subagent_type].invoke(subagent_state) 실행
└─ subagent_state = {부모 state에서 _EXCLUDED_STATE_KEYS 제거}
└─ subagent_state["messages"] = [HumanMessage(content=description)]
4. result["messages"][-1].text 추출 → ToolMessage로 감싸 Command로 반환
└─ 단순 ToolMessage가 아니라 Command(update={...}) 형태
└─ 부모 state에 subagent state 일부도 함께 merge됨 (_EXCLUDED_STATE_KEYS 제외)
툴 방식 vs SubAgentMiddleware 비교
| 툴로 직접 감싸기 | SubAgentMiddleware | |
| 의존성 | LangChain/LangGraph | deepagents 추가 필요 |
| 보일러플레이트 | 많음 | 적음 |
| 동작 가시성 | 명확 | 추상화됨 |
| 툴 이름 | 직접 지정 | task 고정 |
| 반환값 가공 | 툴 함수 안에서 자유롭게 | messages[-1] 고정 |
| 서브에이전트 여러 개 | 툴 여러 개 등록 | subagents=[...] 리스트로 관리 |
외에도, langgraph의 supervisor 패턴이나 handoff/swarm 패턴 등이 있다고 하는데 나중에 더 알아봐야겠다.
'에이전트' 카테고리의 다른 글
| LangChain: 미들웨어 동작 뜯어보기, 클로저 (1) | 2026.06.02 |
|---|