langgraph의 공식문서를 번역해 놓은 자료입니다. 이해하기 쉽게 예제는 변경하였습니다. 또한 필요한 경우 부연 설명을 추가하였습니다. 문제가 되면 삭제하겠습니다.
https://langchain-ai.github.io/langgraph/tutorials/introduction/
Part 6: 상태 커스트마이징하기
지금까지 우리는 간단한 상태에 의존해 왔다(그것은 단순히 메시지 목록일 뿐이다). 이 간단한 상태로도 많은 것을 할 수 있지만, 메시지 목록에 의존하지 않고 복잡한 동작을 정의하려면 상태에 추가 필드를 추가할 수 있다. 이 섹션에서는 이를 설명하기 위해 우리의 챗봇을 새로운 노드로 확장할 것이다.
위의 예에서 우리는 사람을 포함시켰고 그래프는 도구가 호출될 때마다 항상 중단되었다. 만약 우리의 챗봇이 사람에게 의존할 선택권을 가지도록 하고 싶다면 어떻게 해야 할까?
이를 수행하는 한 가지 방법은 그래프가 항상 중단될 "human" 노드를 생성하는 것이다. 이 노드는 LLM이 "human" 도구를 호출할 때만 실행된다. 편의상, 우리는 이 도구가 호출될 경우 "ask_human" 플래그를 그래프 상태에 포함할 것이다.
아래에서 이 새로운 그래프와 업데이트된 상태를 정의하자.
from typing import Annotated
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
class State(TypedDict):
messages: Annotated[list, add_messages]
# 이 부분이 추가되었다
ask_human: bool
다음으로, 모델이 도움을 요청할지 결정할 수 있도록 스키마를 정의하자.
Langchain에서 Pydantic 사용
이 노트북은 Pydantic v2의
BaseModel
을 사용하며, 이는langchain-core >= 0.3
이 필요하다.langchain-core < 0.3
을 사용하면 Pydantic v1과 v2의BaseModel
혼합으로 인해 오류가 발생한다.
from pydantic import BaseModel
class RequestAssistance(BaseModel):
"""Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.
To use this function, relay the user's 'request' so the expert can provide the right guidance.
"""
request: str
다음으로, 챗봇 노드를 정의하자. 여기서 주요 수정 사항은 챗봇이 RequestAssistance
플래그를 호출한 경우 ask_human
플래그를 전환하는 것이다.
tool = TavilySearchResults(max_results=2)
tools = [tool]
llm = ChatOpenAI(model="gpt-3.5-turbo")
# We can bind the llm to a tool definition, a pydantic model, or a json schema
llm_with_tools = llm.bind_tools(tools + [RequestAssistance])
def chatbot(state: State):
response = llm_with_tools.invoke(state["messages"])
ask_human = False
if (
response.tool_calls
and response.tool_calls[0]["name"] == RequestAssistance.__name__
):
ask_human = True
return {"messages": [response], "ask_human": ask_human}
다음으로, 그래프 빌더를 생성하고 이전과 동일하게 챗봇 노드와 도구 노드를 그래프에 추가하자.
graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", ToolNode(tools=[tool]))
다음으로, "human" 노드
를 생성하자. 이 노드
함수는 주로 그래프에서 인터럽트(interrupt)를 발생시키는 placeholder 역할을 한다. 만약 사람이 인터럽트 중에 상태를 수동으로 업데이트하지 않으면, LLM이 사용자가 요청되었지만 응답하지 않았다는 것을 알 수 있도록 도구 메시지를 삽입한다. 이 노드는 또한 ask_human
플래그를 해제하여 그래프가 추가 요청이 없는 한 다시 이 노드를 방문하지 않도록 한다.
from langchain_core.messages import AIMessage, ToolMessage
def create_response(response: str, ai_message: AIMessage):
return ToolMessage(
content=response,
tool_call_id=ai_message.tool_calls[0]["id"],
)
def human_node(state: State):
new_messages = []
if not isinstance(state["messages"][-1], ToolMessage):
# 일반적으로, 사용자는 interrupt 중에 상태를 업데이트할 것이다.
# 그렇지 않으면, LLM이 계속할 수 있도록 placeholder ToolMessage를 포함할 것이다.
new_messages.append(
create_response("No response from human.", state["messages"][-1])
)
return {
# 새 메시지 추가
"messages": new_messages,
# 플래그 해제
"ask_human": False,
}
graph_builder.add_node("human", human_node)
다음으로, 조건 논리를 정의하자. select_next_node
는 플래그가 설정되어 있으면 human 노드로 라우팅한다. 그렇지 않으면, 미리 작성된 tools_condition
함수가 다음 노드를 선택하게 한다.
tools_condition
함수는 챗봇이 응답 메시지에서 도구 호출로 응답했는지를 간단히 확인한다. 만약 그렇다면, 액션 노드로 라우팅한다. 그렇지 않으면 그래프를 종료한다.
def select_next_node(state: State):
if state["ask_human"]:
return "human"
# 그렇지 않으면, 전과 같이 라우팅한다.
return tools_condition(state)
graph_builder.add_conditional_edges(
"chatbot",
select_next_node,
{"human": "human", "tools": "tools", END: END},
)
마지막으로, 간단한 방향 엣지를 추가하고 그래프를 컴파일하자. 이 엣지는 그래프가 노드 a에서 b로 항상 흐르도록 지시하며, a가 실행을 마칠 때마다 적용된다.
# The rest is the same
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge("human", "chatbot")
graph_builder.add_edge(START, "chatbot")
memory = MemorySaver()
graph = graph_builder.compile(
checkpointer=memory,
# human 노드가 실행되기 전에 interrupt한다.
interrupt_before=["human"],
)
시각화 의존성이 설치되어 있다면, 아래에서 그래프 구조를 볼 수 있다.
챗봇은 사람에게 도움을 요청할 수 있다 (chatbot->select->human), 검색 엔진 도구를 호출할 수 있다 (chatbot->select->action), 또는 직접 응답할 수 있다 (chatbot->select->end). 한 번 액션이나 요청이 이루어지면, 그래프는 챗봇 노드로 다시 전환되어 작업을 계속한다.
이제 이 그래프가 실제로 어떻게 작동하는지 살펴보자. 우리의 그래프를 설명하기 위해 전문가의 도움을 요청할 것이다.
user_input = "AI Agent 만드는 데 전문가 가이드가 필요해. 도움 요청할 수 있어?"
config = {"configurable": {"thread_id": "1"}}
events = graph.stream(
{"messages": [("user", user_input)]}, config, stream_mode="values"
)
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
================================ Human Message =================================
AI Agent 만드는 데 전문가 가이드가 필요해. 도움 요청할 수 있어?
================================== Ai Message ==================================
Tool Calls:
RequestAssistance (call_dVJccYNfISvVM4c9uhh0fEFS)
Call ID: call_dVJccYNfISvVM4c9uhh0fEFS
Args:
request: I need expert guidance on creating an AI Agent.
Notice: LLM이 우리가 제공한 "RequestAssistance" 도구를 호출했으며, 인터럽트가 설정되었다. 그래프 상태를 검사하여 확인해 보자.
snapshot = graph.get_state(config)
print(snapshot.next)
('human',)
그래프 상태는 실제로 'human' 노드 이전에 인터럽트되었다. 우리는 이 시나리오에서 "전문가" 역할을 하여 새 ToolMessage
를 추가하여 상태를 수동으로 업데이트할 수 있다.
다음으로, 챗봇의 요청에 응답하기 위해 다음을 수행한다.
- 우리의 응답으로
ToolMessage
를 생성한다. 이 메시지는 챗봇에 전달될 것이다. update_state
를 호출하여 그래프 상태를 수동으로 업데이트한다.
ai_message = snapshot.values["messages"][-1]
human_response = (
"여기 울트라 캡숑 짱 전문가가 왔어요! AI 에이전트를 만들기 위해 열심히 공부하세요."
"간단한 자율 에이전트보다 더 신뢰할 수 있고 확장 가능합니다."
)
tool_message = create_response(human_response, ai_message)
graph.update_state(config, {"messages": [tool_message]})
{'configurable': {'thread_id': '1',
'checkpoint_ns': '',
'checkpoint_id': '1ef7d092-bb30-6bee-8002-015e7e1c56c0'}}
상태를 검사하여 우리의 응답이 추가되었는지 확인할 수 있다.
print(graph.get_state(config).values["messages"])
[
HumanMessage(content='AI Agent 만드는 데 전문가 가이드가 필요해. 도움 요청할 수 있어?', additional_kwargs={}, response_metadata={}, id='a3425e47-186a-4627-b562-31f38fbf3a81'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_n8AUB1Kl7EdRfS1tXFkYgYml', 'function': {'arguments': '{"request":"I need expert guidance on creating an AI agent."}', 'name': 'RequestAssistance'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 24, 'prompt_tokens': 178, 'total_tokens': 202, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-928512ed-694d-4fc5-9e09-3d0bb5292fd8-0', tool_calls=[{'name': 'RequestAssistance', 'args': {'request': 'I need expert guidance on creating an AI agent.'}, 'id': 'call_n8AUB1Kl7EdRfS1tXFkYgYml', 'type': 'tool_call'}], usage_metadata={'input_tokens': 178, 'output_tokens': 24, 'total_tokens': 202, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
ToolMessage(content='여기 울트라 캡숑 짱 전문가가 왔어요! AI 에이전트를 만들기 위해 열심히 공부하세요.간단한 자율 에이전트보다 더 신뢰할 수 있고 확장 가능합니다.', id='62275780-547f-4bd6-8468-c31fb6a4d7e8', tool_call_id='call_n8AUB1Kl7EdRfS1tXFkYgYml')]
다음으로, 입력으로 None을 사용하여 그래프를 재개하자.
events = graph.stream(None, config, stream_mode="values")
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
================================= Tool Message =================================
여기 울트라 캡숑 짱 전문가가 왔어요! AI 에이전트를 만들기 위해 열심히 공부하세요.간단한 자율 에이전트보다 더 신뢰할 수 있고 확장 가능합니다.
================================= Tool Message =================================
여기 울트라 캡숑 짱 전문가가 왔어요! AI 에이전트를 만들기 위해 열심히 공부하세요.간단한 자율 에이전트보다 더 신뢰할 수 있고 확장 가능합니다.
================================== Ai Message ==================================
울트라 캡숑 짱 전문가가 도와줄 준비가 되어 있습니다! AI 에이전트를 만들기 위해 열심히 공부하고 자율 에이전트보다 더 신뢰할 수 있고 확장 가능한 것을 목표로 하세요. 필요하신 경우 언제든지 질문해 주세요.
챗봇이 최종 응답에 업데이트된 상태를 반영한 것을 확인하자. 모든 것이 체크포인트로 저장되었기 때문에, "전문가"인 인간이 그래프 실행에 영향을 주지 않고 언제든지 업데이트를 수행할 수 있었다.
이제 챗봇이 실행을 중단할 필요가 있는지 스스로 결정할 수 있도록 어시스턴트 그래프에 추가 노드를 추가했다. 이는 그래프 상태를 새 ask_human
필드로 업데이트하고 그래프를 컴파일할 때 인터럽트 로직을 수정함으로써 이루어졌다. 이를 통해 그래프를 실행할 때마다 전체 메모리를 유지하면서 동적으로 사람을 루프에 포함할 수 있다.
튜토리얼이 거의 끝났지만, 체크포인팅과 상태 업데이트를 연결하는 마지막 개념을 검토할 것이다.