일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- RecSys
- conditional_edge
- REACT
- rl
- Python
- toolnode
- tool_binding
- 강화학습의 수학적 기초와 알고리듬 이해
- lcel
- 밑바닥부터 시작하는 딥러닝
- summarize_chat_history
- removemessage
- conditional_edges
- LangChain
- langgrpah
- pinecone
- subgraph
- Ai
- add_subgraph
- tool_calls
- 강화학습의 수학적 기초와 알고리듬의 이해
- update_state
- rag
- human-in-the-loop
- 강화학습
- humannode
- 추천시스템
- chat_history
- 밑바닥부터시작하는딥러닝 #딥러닝 #머신러닝 #신경망
- langgraph
- Today
- Total
타임트리
[LangGraph] ToolNode 본문
LLM에 Tool을 binding 해서 LLM이 tool_calls
를 생성했을 때, 적절한 arguments를 사용해 해당 tool을 실행하도록 하는 ToolNode
에 대해 자세히 알아보자. 방금 작성한 그대로 로직을 작성할 수도 있지만(참고 - [LangGrpah] Tool Binding), LangGraph는 ToolNode를 사전 정의(pre-built)해서 제공한다.
내부적으로는 LLM에게 tool의 목록을 전달하고 (bind_tools), LLM이 사용자의 질문을 기반으로 tool 실행이 필요하다고 판단하면 해당 tool의 이름과 arguments를 반환한다. 그러면 해당 tool과 arguments로 함수를 실행하게 된다. 이때 tool
list를 갖고, LLM이 반환한 tool_calls
를 기반으로 함수를 실행할 수 있도록 노드로 구현한 것이 ToolNode
다.
도구 정의
먼저 파이썬 코드를 실행하는 execute_python
과 location의 따라 서로 다른 문자열을 반환하는get_weather
를 정의하자. 그리고 이 두 함수를 tool 콜백 함수를 사용해서 llm에 binding 하기 좋은 형태로 변환하자.
다음으로, 두 개의 tool을 list 형태로 만든 뒤, ToolNode를 초기화하자.
from langchain_core.messages import AIMessage
from langchain_core.tools import tool
from langchain_experimental.tools.python.tool import PythonAstREPLTool
from langgraph.prebuilt import ToolNode
@tool
def execute_python(code: str):
"""Call to excute python code."""
return PythonAstREPLTool().invoke(code)
@tool
def get_weather(location: str):
"""Call to get the current weather."""
if location.lower() in ["seoul", "busan"]:
return "The temperature is 5 degrees and it's cloudy."
else:
return "The temperature is 30 degrees and it's sunny."
# Tool Node 초기화
tools = [execute_python, get_weather]
tool_node = ToolNode(tools=tools)
ToolNode 수동 호출해보기
ToolNode는 State의 messages list의 마지막 메세지가 tool_calls 인자가 있는지 없는지 여부로 tool을 호출할지를 결정한다.
일반적으로는 AIMessage를 수동으로 생성하지 않고, LangChain의 LLM이 생성하지만, 먼저 ToolNode를 수동으로 호출해 보자.
AIMessage에서 content는 보통 빈 문자열이 들어가고, tool_calls
속성에 List[Dict]
형태로, 호출할 도구의 이름, 인자, ID, 유형의 키 키값으로 들어간다.
message_with_single_tool_call = AIMessage(
content="",
tool_calls=[
{
"name": "get_weather",
"args": {"location": "seoul"},
"id": "tool_call_id",
"type": "tool_call"
}
]
)
print(tool_node.invoke({"messages": [message_with_single_tool_call]}))
ToolNode는 execute_python
과 get_weather
두 개의 tool을 갖고 있지만, invoke 한 결과는 ToolMessage로 반환되며, content에는 get_weather(location="seoul") 을 실행한 결과가 들어간다.
{'messages': [ToolMessage(content="The temperature is 5 degrees and it's cloudy.", name='get_weather', tool_call_id='tool_call_id')]}
병렬 수행
AIMessage의 tool_calls 인자에 리스트로 여러 tool을 전달하면, ToolNode가 병렬적으로 도구 호출을 수행한다. 아래는 execute_python
과 get_weather
두 가지 tool_call을 tool_calls 리스트에 전달했다. 그리고 결과는 예상대로 2개의 ToolMessage가 반환된다.
message_with_multiple_tool_call = AIMessage(
content="",
tool_calls=[
{
"name": "get_weather",
"args": {"location": "busan"},
"id": "tool_call_id_1",
"type": "tool_call"
},
{
"name": "execute_python",
"args": {"code": "3 + 3"},
"id": "tool_call_id_2",
"type": "tool_call"
}
]
)
print(tool_node.invoke({"messages": [message_with_multiple_tool_call]}))
{'messages': [ToolMessage(content="The temperature is 5 degrees and it's cloudy.", name='get_weather', tool_call_id='tool_call_id_1'), ToolMessage(content='6', name='execute_python', tool_call_id='tool_call_id_2')]}
ChatModel과 함께 사용
앞서 말한 것처럼, 일반적으로는 AIMessage를 수동으로 생성하지 않고, LangChain의 LLM이 생성한다. bind_tools
메소드를 호출해서 LLM에 도구를 인식시켜 줄 수 있다.
실제로는 아래와 같은 프롬프트가 들어간다! (참고)
"""You are an assistant that has access to the following set of tools. Here are the names and descriptions for each tool:
{rendered_tools}
Given the user input, return the name and input of the tool to use. Return your response as a JSON blob with 'name' and 'arguments' keys."""
그럼 이번에는 tool을 들고 있는 llm_with_tools
를 정의하고, 서울의 현재 날씨를 물어보자. 그러면 LLM이 get_weather
tool을 호출해야 한다고 판단하고, AIMessage에 tool_calls
인자가 채워져 반환된다.
from langchain_openai import ChatOpenAI
llm_with_tools = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools=tools)
llm_with_tools.invoke("seoul의 현재 날씨는 어때요?").tool_calls
[{'name': 'get_weather',
'args': {'location': 'seoul'},
'id': 'call_7TfQhajj4O3fM1FROdH4gAwC',
'type': 'tool_call'}]
따라서, 해당 반환값을 직접 ToolNode에 전달해 실행할 수 있다.
tool_node.invoke({
"messages": [llm_with_tools.invoke("seoul의 현재 날씨는 어때요?")]
})
{'messages': [ToolMessage(content="The temperature is 5 degrees and it's cloudy.", name='get_weather', tool_call_id='call_jDy3hBVkBKHzHDkzC1UuzyOX')]}
ReACT 에이전트
다음으로 그래프에서 ToolNode를 통합하여 사용하는 방법을 살펴보자. 여기서는 ReAct 에이전트를 구현해 보자. 이 에이전트는 일부 쿼리를 input으로 사용하고, 쿼리에 적절한 답변을 생성하기에 충분한 정보가 수집될 때까지 tool을 반복적으로 호출한다.
이때, RemoveMessage
를 사용해서 최근 5개의 대화까지만 사용하도록 하자.
from typing import Annotated, TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages, RemoveMessage
from langgraph.checkpoint.memory import MemorySaver
# 상태 정의
class State(TypedDict):
messages: Annotated[list, add_messages]
# conditional edge에 사용할 함수 정의
def should_continue(state: State) -> Literal["tools", "delete_message"]:
last_message = state["messages"][-1]
if last_message.tool_calls: # 마지막 메세지에 tool_calls가 있다면
return "tools"
return "delete_message"
# chat_model Node 정의
def chat_model(state: State):
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
# delete_message Node 정의
def delete_message(state: State):
"""메세지 개수가 5개가 넘어갈 경우, 오래된 메세지 삭제해서 최신 5개만 유지"""
if len(state["messages"]) > 5:
return {"messages": [RemoveMessage(id=message.id) for message in state["messages"][:-5]]}
flow = StateGraph(State)
flow.add_node("chat_model", chat_model)
flow.add_node("delete_message", delete_message)
flow.add_node("tools", tool_node)
flow.add_edge(START, "chat_model")
flow.add_edge("tools", "chat_model")
flow.add_edge("delete_message", END)
flow.add_conditional_edges(
"chat_model",
should_continue
)
memory = MemorySaver()
graph = flow.compile(checkpointer=memory)
만들어진 그래프를 시각화해 보자.
from IPython.display import display, Image
display(Image(graph.get_graph().draw_mermaid_png()))
사용자 질문이 들어오면 execute_python
와 get_weather
2개의 도구를 들고 있는 chat_model 노드에서 2가지 액션 중 하나를 취할 수 있다.
- tool 호출
- 파이썬 함수 실행이 필요한 경우
execute_python
- 현재 날씨가 필요한 경우
get_weather
- 파이썬 함수 실행이 필요한 경우
- 직접 답변
- 굳이 Tool 호출이 필요하지 않은 경우 직접 답변
그리고 최종적으로 Tool 호출이 일어나지 않고 답변이 완료됐다면, delete_message 노드로 이동해서 가장 최근 5개의 메세지 이력만 남기고 나머지는 지운다.
테스트해 보자. 먼저, 파이썬 코드를 작성하도록 요청해 보자. (주피터 노트북 환경에서 실행하면 그래프 결과도 확인 가능하다)
input = {"messages": [("user", "y=2x+3 그래프를 파이썬으로 그려주세요.")]}
config = {"configurable": {"thread_id": "a"}}
events = graph.stream(input, config, stream_mode="values")
for event in events:
event["messages"][-1].pretty_print()
================================ Human Message =================================
y=2x+3 그래프를 파이썬으로 그려주세요.
================================== Ai Message ==================================
Tool Calls:
execute_python (call_dyChs2ZSWFvXQbWv1W4DQUvZ)
Call ID: call_dyChs2ZSWFvXQbWv1W4DQUvZ
Args:
code: import matplotlib.pyplot as plt
import numpy as np
# Define the function
def linear_function(x):
return 2 * x + 3
# Generate x values
x_values = np.linspace(-10, 10, 400)
# Generate y values based on the linear function
y_values = linear_function(x_values)
# Create the plot
plt.figure(figsize=(10, 6))
plt.plot(x_values, y_values, label='y = 2x + 3', color='blue')
plt.title('Graph of y = 2x + 3')
plt.xlabel('x')
plt.ylabel('y')
plt.axhline(0, color='black',linewidth=0.5, ls='--') # x-axis
plt.axvline(0, color='black',linewidth=0.5, ls='--') # y-axis
plt.grid(color = 'gray', linestyle = '--', linewidth = 0.5)
plt.legend()
plt.xlim(-10, 10)
plt.ylim(-20, 25)
plt.show()
================================= Tool Message =================================
Name: execute_python
================================== Ai Message ==================================
그래프가 성공적으로 그려졌습니다! 함수 \( y = 2x + 3 \)의 그래프를 확인해 보세요. 이 그래프는 x 값에 따라 y 값이 어떻게 변화하는지를 보여줍니다.
다음으로 seoul의 현재 날씨를 물어보며 get_weather tool을 잘 호출하는지도 확인해 보자.
input = {"messages": [("user", "seoul의 현재 날씨는 어떠한가요?")]}
config = {"configurable": {"thread_id": "b"}}
events = graph.stream(input, config, stream_mode="values")
for event in events:
event["messages"][-1].pretty_print()
================================ Human Message =================================
seoul의 현재 날씨는 어떠한가요?
================================== Ai Message ==================================
Tool Calls:
get_weather (call_0pZ5Ft9AqcpTx5wQMulQJG90)
Call ID: call_0pZ5Ft9AqcpTx5wQMulQJG90
Args:
location: Seoul
================================= Tool Message =================================
Name: get_weather
The temperature is 5 degrees and it's cloudy.
================================== Ai Message ==================================
서울의 현재 날씨는 기온 5도이며, 흐림입니다.
마지막으로, 현재 memory에 해당 thread의 대화 이력이 어떻게 상태에 저장되어 있는지 확인해 보면 앞서 delete_node를 통과하므로 의도한 대로 최근 5개의 메세지가 남아있는 것을 확인할 수 있다.
snapshot = graph.get_state(config)
snapshot.values["messages"]
[
AIMessage(content='그래프가 성공적으로 그려졌습니다! 함수 \\( y = 2x + 3 \\)의 그래프를 확인해 보세요. 이 그래프는 x 값에 따라 y 값이 어떻게 변화하는지를 보여줍니다.', ...),
HumanMessage(content='seoul의 현재 날씨는 어떠한가요?', ...),
ToolMessage(content="The temperature is 5 degrees and it's cloudy.", name='get_weather', ...),
AIMessage(content='서울의 현재 날씨는 기온 5도이며, 흐림입니다.',...)
]
---
출처:
LangGraph. "How to call tools using ToolNode". https://langchain-ai.github.io/langgraph/how-tos/tool-calling/
위키독스 - <랭체인LangChain 노트> - LangChain 한국어 튜토리얼🇰🇷 (https://wikidocs.net/book/14314)
'LLM > LangGraph' 카테고리의 다른 글
[LangGraph] 과거 대화 이력의 요약 (0) | 2025.01.01 |
---|---|
[LangGraph] Branches for parallel node execution(병렬 노드 실행) (0) | 2024.12.31 |
[LangGraph] Delete Messages (0) | 2024.12.31 |
[LangGraph] Human Node (LLM이 판단) (0) | 2024.12.30 |
[LangGraph] Manually Updating the State (0) | 2024.12.30 |