create_agent 속을 직접 열어 그래프로 다시 짓기

조립 완성품 장난감을 사면 설명서 없이도 놀 수 있지만 부품을 낱개로 받으면 원하는 대로 바꿔 끼울 수 있습니다. create_agent는 완성품이고 LangGraph는 그 부품 서랍입니다. 앞 글에서 본 model과 tools 두 노드, 그리고 길 고르기가 어떤 부품으로 되어 있는지 하나씩 직접 올려 보겠습니다.

부품 이름은 LangGraph 1.2의 이름이고 앞 글의 create_agent 안쪽과 같은 모양입니다.

MessagesState는 쪽지가 쌓이는 칸입니다

에이전트의 State에서 가장 중요한 칸은 messages입니다. 사용자 질문, 모델의 응답, 도구 결과가 모두 이 칸에 쪽지로 쌓입니다. LangGraph에는 이 칸을 미리 만들어 둔 MessagesState가 있어서 그대로 가져다 씁니다.

앞 글에서 본 reducer가 이 칸에 달려 있습니다. 메시지용 reducer인 add_messages는 새 메시지를 뒤에 붙이고 번호표(id)가 같은 메시지가 오면 그 자리를 새것으로 바꿉니다. 그래서 노드는 새 쪽지만 돌려주면 되고 앞의 대화가 사라질 걱정은 없습니다.

모델 노드와 ToolNode와 길 고르기를 이어 붙입니다

모델 노드는 State의 메시지를 모델에게 보내고 응답을 새 쪽지로 돌려주는 함수입니다. 도구를 실행하는 노드는 직접 짜지 않고 LangGraph가 만들어 둔 ToolNode에 도구 목록을 넘기면 됩니다. 이 노드는 마지막 응답에 도구 요청이 있으면 도구를 실행해 결과 쪽지를 돌려줍니다.

길 고르기는 조건부 엣지(conditional edge)로 합니다. 일반 엣지는 늘 같은 곳으로 가고 조건부 엣지는 함수를 불러 그때그때 갈 곳을 정합니다. 마지막 쪽지에 도구 요청이 있으면 "tools"로 가고 없으면 끝으로 가는 규칙은 tools_condition이라는 이름으로 이미 만들어져 있습니다.

model 도구 요청있나? 있으면 tools 없으면끝 tools 다음에는 늘 model로 돌아옴
agent_graph.pypython
model = init_chat_model("anthropic:claude-sonnet-5-5").bind_tools([get_ticket])

# 1 모델 노드: 쪽지를 모델에 보내고 응답을 새 쪽지로 돌려준다
def call_model(state: MessagesState):
    res = model.invoke([SystemMessage("너는 IT 헬프데스크 상담원이다.")] + state["messages"])
    return {"messages": [res]}

# 2 노드와 화살표를 등록한다
b = StateGraph(MessagesState)
b.add_node("model", call_model)
b.add_node("tools", ToolNode([get_ticket]))
b.add_edge(START, "model")
# 3 도구 요청이 있으면 tools로, 없으면 끝으로
b.add_conditional_edges("model", tools_condition, {"tools": "tools", "__end__": END})
b.add_edge("tools", "model")
graph = b.compile()
터미널
$ python agent_graph.py model → ai [{'name': 'get_ticket', 'args': {'ticket_id': 'T-1042'}, ...}] tools → tool T-1042: 부품 대기, 도착 예정 10월 18일 model → ai T-1042는 부품 대기 중입니다. ['human', 'ai', 'tool', 'ai']

출력은 예시입니다. 코드는 langgraph 1.2와 langchain 1.4 기준이고 import와 도구 정의는 생략했습니다. stream_mode="updates"로 단계마다 찍은 결과입니다.

  1. 모델 노드입니다. bind_tools는 모델이 이 도구를 부를 수 있다고 알려 주는 일입니다. 노드는 쪽지 목록을 모델에 보내고 응답 하나를 {"messages": [res]}로 돌려줍니다. 역할 안내(system)는 State에 쌓지 않고 호출할 때만 앞에 붙였습니다.
  2. 부품을 올립니다. ToolNode에 도구 목록을 주면 도구를 실행하는 노드가 됩니다. 시작 화살표는 model로 이어 둡니다.
  3. 길을 고릅니다. add_conditional_edges는 출발 노드, 길 고르는 함수, 결과 이름이 가리키는 도착 노드를 받습니다. "__end__"는 END를 뜻합니다. tools 다음에는 늘 model로 돌아가는 일반 엣지를 답니다. 이 둘이 루프를 만듭니다.
직접 쓴 길 고르기 함수와 비교하기
route.pypython
def should_continue(state: MessagesState) -> Literal["tools", "__end__"]:
    last = state["messages"][-1]
    return "tools" if last.tool_calls else "__end__"

b.add_conditional_edges("model", should_continue)

tools_condition이 하는 일을 함수로 직접 쓴 것입니다. 반환 타입에 갈 수 있는 곳을 적어 두면 도착 노드 목록을 따로 주지 않아도 됩니다. 직접 짜면 조건을 바꿀 수 있습니다. 예를 들어 세 바퀴를 넘기면 도구가 있어도 끝으로 보내는 규칙을 넣을 수 있습니다.

길 고르기 함수는 마지막 쪽지만 봅니다

조건부 엣지의 함수는 State를 받아 도착 노드의 이름을 돌려줍니다. 위 규칙은 마지막 쪽지 하나만 봅니다. 모델의 응답에 도구 요청이 있는지만 확인하면 되기 때문입니다. 스크롤하며 모델이 쪽지를 낼 때마다 길이 어떻게 갈리는지 보세요.

모델 응답 1: 도구 요청이 있음

모델이 get_ticket을 부르는 쪽지를 냈습니다. 길 고르기 함수가 마지막 쪽지에서 도구 요청을 찾아 "tools"를 돌려줍니다.

tools를 거쳐 model로 돌아옴

ToolNode가 도구를 실행해 결과 쪽지를 붙이고 일반 엣지를 따라 model로 돌아갑니다. 이번에는 쪽지가 세 장입니다.

모델 응답 2: 도구 요청이 없음

모델이 글로만 답했습니다. 길 고르기 함수가 "__end__"를 돌려주고 그래프가 끝납니다. 마지막 쪽지가 최종 답입니다.

ToolNode는 도구를 실행하고 결과 쪽지를 돌려줍니다

모델이 도구 요청 여러 개를 한 번에 하면 ToolNode가 그 요청들을 함께 실행하고 요청마다 결과 쪽지를 하나씩 돌려줍니다. 결과 쪽지에는 어느 요청의 결과인지 번호가 붙어 있어서 모델이 짝을 맞출 수 있습니다.

도구 안에서 예외가 나면 기본 설정에서는 그 예외를 그대로 위로 던지고 그래프가 멈춥니다. handle_tool_errors=True로 켜면 예외를 오류 표시가 붙은 결과 쪽지로 바꿔 모델에게 돌려줍니다. 앞서 wrap_tool_call로 직접 하던 일을 ToolNode 옵션 하나가 해 줍니다. 직접 실행해 확인한 결과입니다.

따로 정하는 방식 call_model쪽지만 돌려줌 조건부 엣지길 고르기 함수가 정함 Command로 한 번에 call_model돌려주는 것 = Command(update: 새 쪽지goto: 다음 노드 tools 갱신과 이동을 한 노드가 함께 정합니다.

Command를 쓰는 노드는 갈 수 있는 곳을 반환 타입에 적어 두어야 그래프가 길을 그릴 수 있습니다.

Command는 쪽지와 갈 곳을 한꺼번에 돌려줍니다

길 고르기를 노드 밖의 조건부 엣지에 두지 않고 노드가 직접 정할 수도 있습니다. Command는 State 갱신(update)과 다음에 갈 노드(goto)를 한 번에 담는 객체입니다. 모델 노드가 응답을 보고 바로 어디로 갈지 정하면 길 고르기 함수가 따로 필요 없습니다.

노드 안에서 얻은 값에 따라 길이 갈릴 때는 이 방식이 편합니다. 다만 노드가 길 정보를 품게 되어 그림이 노드 안으로 숨는 단점이 있습니다. 순서가 눈에 보이게 하려던 그래프의 목적에는 조건부 엣지가 더 잘 맞습니다. 두 방식이 같은 그래프를 만든다는 것은 직접 실행해 확인했습니다.

command.pypython
def model_cmd(state: MessagesState) -> Command[Literal["tools", "__end__"]]:
    res = model.invoke(state["messages"])
    # 4 쪽지를 갱신하면서 갈 곳도 함께 정한다
    return Command(update={"messages": [res]},
                   goto="tools" if res.tool_calls else END)
  1. 갱신과 이동을 함께 돌려줍니다. 반환 타입의 Command[Literal["tools", "__end__"]]는 이 노드가 갈 수 있는 곳을 그래프에 알려 줍니다. 이 노드에는 조건부 엣지를 달지 않고 tools에서 model로 돌아오는 일반 엣지만 답니다.

직접 짜면 한도와 오류 처리를 원하는 자리에 둘 수 있습니다

그래프를 직접 짜면 create_agent가 정해 주던 것들을 원하는 곳에 둘 수 있습니다. 바퀴 수가 필요하면 State에 칸을 하나 더 만들고 모델 노드가 그 수를 올리게 한 뒤 길 고르기 함수가 보게 하면 됩니다. 도구 오류는 ToolNode 옵션이나 노드 감싸기로 처리합니다.

그래프 전체에 거는 단계 한도(recursion_limit)도 그대로 적용됩니다. 이 글의 그래프에서 모델이 도구만 부르는 상황을 한도 6으로 돌렸더니 GraphRecursionError가 났습니다. 직접 만든 그래프라면 기본 한도부터 확인한 뒤 낮추는 편이 안전합니다.

단계 한도 recursion_limit = 6 · 눈금 하나가 노드 한 번 1model 2tools 3model 4tools 5model 6tools 여기서 GraphRecursionError모델이 도구만 계속 부르는 상황을 한도 6으로 돌린 결과입니다 따로 만든 turns 칸 · 이 글의 예시 State에 칸을 하나 더 두고 model 노드가 1씩 올립니다길 고르기 함수가 turns 3이면 끝으로 보냅니다 두 한도는 별개입니다. 바깥 눈금은 그래프가, turns는 내가 정합니다.

설명을 위한 그림입니다. turns 칸은 이 글에서 직접 만든 예시 State입니다.

다음 글에서는 체이닝과 라우팅이라는 워크플로 패턴으로 정해진 순서의 일을 처리하는 법을 알아보겠습니다.

참고 자료

출처 5개 펼치기