Create React Agent - Prebuilt
- Download Notebook on GitHub
- Slides:Deep Agents.pdf

이 프로젝트에서는 Deep Agent를 구축하게 됩니다. LangGraph의 ‘사전 구축된(pre-built)’ 에이전트 추상화 위에 이를 구축할 것이며, 이는 코드를 상당히 단순화합니다. 이번 강의에서는 사전 구축된 ReAct 에이전트에 대해 배울 것입니다. 학습 내용은 다음과 같습니다:
- ReAct 에이전트란 무엇인가
- 구현의 기능과 추가 정보 확인 방법
- 도구를 활용한 에이전트 구축
- 그래프, 상태 및 메시지
- 도구를 통한 상태 접근 및 수정
- 🪝 훅! 및 구조화된 응답
ReAct 에이전트란 무엇인가

ReAct 에이전트는 “추론과 행동”(ReAct) 프레임워크를 사용하여 사고의 사슬(CoT) 추론과 외부 도구 사용을 결합하는 AI 에이전트입니다. 이 개념은 논문 ‘ReAct: 언어 모델에서 추론과 행동의 시너지 효과’를 통해 널리 알려졌습니다.
이 에이전트는 세 가지 구성 요소로 이루어집니다: 대규모 언어 모델(LLM), 사용 가능한 도구 세트, 지시를 제공하는 프롬프트입니다.
LLM은 반복 루프 방식으로 작동합니다. 각 반복에서 LLM은 사용 가능한 도구 목록을 포함한 컨텍스트를 검토합니다. 도구를 호출할 필요가 있는지 결정합니다. 호출할 도구를 선택하고 도구 호출을 구성합니다. 이 호출은 실행을 위해 도구 노드로 전송됩니다. 도구 노드는 도구를 실행하고 결과(관측값)를 LLM으로 되돌려 보냅니다. LLM은 관측값을 수신하여 다음 행동을 결정하는 데 활용합니다. 이 루프는 중지 조건이 충족될 때까지 계속됩니다. 일반적으로 에이전트가 더 이상 도구를 호출할 필요가 없다고 판단할 때 중지됩니다.
LangGraph 사전 구축 create_agent의 주요 기능
다음은 create_agent 에서 제공하는 주요 기능들입니다.
- 메모리 통합: 단기(세션 기반) 및 장기(세션 간 지속적) 메모리에 대한 네이티브 지원으로, 챗봇 및 어시스턴트에서 상태 유지 동작을 가능하게 합니다.
- 휴먼-인-더-루프: 웹소켓 기반 솔루션이 실시간 상호작용으로 제한되는 것과 달리, 실행을 무기한 일시 중지하여 인간 피드백을 기다릴 수 있습니다. 이를 통해 워크플로우의 어느 단계에서든 비동기적 승인, 수정 또는 개입이 가능합니다.
- 스트리밍 지원: 에이전트 상태, 모델 토큰, 도구 출력 또는 결합된 스트림의 실시간 스트리밍.
- 배포 도구: 인프라 없이 사용 가능한 배포 도구 포함. LangGraph 플랫폼은 테스트, 디버깅 및 배포를 지원합니다.
- Studio: 워크플로우 검사 및 디버깅을 위한 시각적 IDE.
- LangSmith: 추적 및 평가 도구.
- 프로덕션 환경을 위한 다양한 배포 옵션 지원.
create-agent는 매우 정교하여 다양한 입력 형식을 수용하고 풍부한 커스터마이징을 허용합니다. 단순한 에이전트-툴 루프 형태로 사용할 수 있으며, 프리/포스트 훅 및/또는 구조화된 출력을 통해 커스터마이징을 추가할 수 있습니다.
| simple agent | complex agent |
|---|---|
![]() | ![]() |
환경 변수 로드
from dotenv import load_dotenv
load_dotenv(".env", override=True)
# 새 코드를 실행하기 전에 모든 모듈을 자동으로 재로드합니다. 로컬 패키지의 변경 사항을 반영합니다.
%load_ext autoreload
%autoreload 2The autoreload extension is already loaded. To reload it, use:
%reload_ext autoreload
도구로 에이전트 만들기
시작하기 위해 간단한 계산기 도구로 에이전트를 만들어 보겠습니다. 구성 방식을 파악한 후 더 자세한 내용을 살펴보겠습니다.
from typing import Literal
from langchain_core.tools import tool
from pydantic import BaseModel, Field
class CalculatorSchema(BaseModel):
operation: Literal["add", "subtract", "multiply", "divide"] = Field(
description="수행할 연산('add', 'subtract', 'multiply', 'divide'"
)
a: int | float = Field(description="첫 번째 숫자")
b: int | float = Field(description="두 번째 숫자")
@tool("calculator", args_schema=CalculatorSchema)
def calculator(
operation: Literal["add", "subtract", "multiply", "divide"],
a: int | float,
b: int | float,
) -> int | float:
"""두 개의 입력값을 받는 계산기 도구"""
if operation == "divide" and b == 0:
raise ValueError("0으로 나눌 수 없습니다.")
if operation == "add":
return a + b
if operation == "subtract":
return a - b
if operation == "multiply":
return a * b
if operation == "divide":
return a / b
raise TypeError("unknown operation")from langchain_core.runnables import RunnableConfig
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
SYSTEM_PROMPT = "당신은 계산기 사용에 능숙한 유능한 산술 도우미입니다."
model = init_chat_model(
"openai:gpt-4.1-nano",
temperature=0.0,
)
tools = [calculator]
agent = create_agent(
model,
tools=tools,
system_prompt=SYSTEM_PROMPT,
).with_config(RunnableConfig(recursion_limit=20))
agent
type(agent)langgraph.graph.state.CompiledStateGraph
그래프, 상태 및 메시지
에이전트 정의
에이전트를 정의할 때 다음을 제공합니다: 모델(필수), 하나 이상의 도구, 시스템 프롬프트, 그리고 기본값이 AgentState인 상태 스키마. 내부적으로는 위에서 보여준 LangGraph 그래프를 정의하고 컴파일하는 작업입니다. 중요한 점은 도구 노드(tools node)가 또 다른 사전 구축된 항목인 ToolNode라는 점입니다. 툴 노드는 LLM에서 보낸 메시지에 명시된 모든 툴을 실행하고 결과를 반환합니다.
에이전트 호출
에이전트를 호출해 결과를 확인해 보겠습니다!
from langchain_core.messages import HumanMessage
from utils import format_messages
# 사용 예시
result1 = agent.invoke(
{
"messages": [HumanMessage("3.1 * 4.2는 얼마입니까?")],
}
)
format_messages(result1["messages"])╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮ │ 3.1 * 4.2는 얼마입니까? │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ │ │ │ │ 🔧 Tool Call: calculator │ │ Args: { │ │ "operation": "multiply", │ │ "a": 3.1, │ │ "b": 4.2 │ │ } │ │ ID: call_44XHwWgz1qsfYXxQ2cND3QCm │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮ │ 13.020000000000001 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ 3.1 곱하기 4.2는 13.02입니다. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
에이전트 호출:
시퀀스 다이어그램은 모델이 호출될 때 발생하는 과정을 살펴보는 데 매우 유용한 방법입니다.
sequenceDiagram participant U as User participant A as LLM participant T as Tools Note over A: System message<br/>"당신은 계산기 사용에 능숙한..." U->>A: Initial input,<br/>"3.1 * 4.2는 얼마입니까?" loop while tool_calls present A->>T: AIMessage(id="call_123", tool_calls=[...]) T-->>A: ToolMessage(tool_call_id="call_123", content="xx") end A->>U: Return final state
이 예시에서 사용자의 입력은 “3.1 * 4.2는 얼마입니까?”입니다. 이 입력은 시스템 프롬프트 및 도구 설명과 함께 LLM으로 전송됩니다.
LLM은 계산기 도구를 호출해야 한다고 결정합니다. messages에 AIMessage를 추가합니다:
AIMessage(
content="",
tool_calls=[{
"id": "call_123",
"name": "calculator",
"args": {"a": 3.1, "b": 4.2, "operation": "multiply"}}
]
)도구 노드는 AIMessage를 수신하고 모든 도구 호출을 처리합니다. tool_call_ids를 추적합니다. messages에 ToolMessage로 응답합니다:
ToolMessage(
content="13.02", # 도구 실행 결과.
tool_call_id="call_123" # AIMessage.tool_calls의 ID와 일치합니다.
)LLM은 messages 내 응답을 검토하고 완료된 것으로 판단한 후, 사용자에게 보낼 AIMessage를 생성합니다.
도구 내에서 상태 접근 및 수정
상태(State)
LangGraph의 유용한 기능 중 하나는 상태입니다. 그래프는 각 노드가 그래프 실행 기간 동안 사용할 수 있고 장기 저장소에 지속 저장될 수 있는 타입화된 데이터 구조를 가집니다. 이를 통해 노드 간 공유할 정보를 저장하거나, 그래프를 디버깅하거나, 장시간 실행 중인 그래프를 이전 시점으로 재설정할 수 있습니다.
그래프에 상태를 정의할 때는 데이터 유형과 ‘리듀서’ 함수를 지정합니다. 리듀서는 해당 요소에 정보가 추가되는 방식을 설명합니다. 이는 특히 작업이 여러 노드에 매핑되어 병렬로 실행되며 상태를 동시에 업데이트할 때 유용합니다.
이 예제에서는 기본 AgentState가 사용되었습니다.
class AgentState(TypedDict, Generic[ResponseT]):
"""State schema for the agent."""
messages: Required[Annotated[list[AnyMessage], add_messages]]
jump_to: NotRequired[Annotated[JumpTo | None, EphemeralValue, PrivateStateAttr]]
structured_response: NotRequired[Annotated[ResponseT, OmitFromInput]]messages는BaseMessage의 리스트로, LLM과의 메시지를 포함합니다.- typing.Annotated는 타입 힌트에 임의의 메타데이터를 첨부할 수 있게 합니다. 구문: Annotated[Type, metadata1, metadata2, …]
add_messages리듀서는 메시지 목록 끝에 새 메시지를 추가합니다.
간단히 살펴보겠습니다.
from IPython.display import JSON
from langchain_core.messages import messages_to_dict
JSON({"messages": messages_to_dict(result1["messages"])})<IPython.core.display.JSON object>
사용자 정의 상태
계산기에 수행된 모든 연산 목록을 저장하도록 확장해 보겠습니다. 이를 위해 상태에 목록을 추가하고, 상태를 목록에 추가하는 리듀서 함수가 필요합니다. 이렇게 하면 목록이나 연산이 비어 있는 경우도 안전하게 처리할 수 있습니다.
from typing import Annotated
from langchain.agents import AgentState
def reduce_list(left: list | None, right: list | None) -> list:
"""두 개의 리스트를 안전하게 결합하며, 입력값 중 하나 또는 둘 다 None일 수 있는 경우를 처리합니다.
인수:
left (list | None): 결합할 첫 번째 리스트 또는 None.
right (list | None): 결합할 두 번째 리스트 또는 None.
반환값:
list: 두 입력 리스트의 모든 요소를 포함하는 새로운 리스트.
입력값이 None인 경우 빈 리스트로 처리됩니다."""
if not left:
left = []
if not right:
right = []
return left + right
class CalcState(AgentState):
"""Graph State."""
ops: Annotated[list[str], reduce_list]상태 접근
이제 계산기에 업데이트 기능을 추가할 수 있습니다. 여기서 문제가 발생합니다! 이제 상태가 계산기 도구의 인수가 되었습니다.
도표에서 명확히 알 수 있듯이, LLM은 도구 호출을 생성하는 임무를 맡고 있지만, 그 컨텍스트에 state 인자가 없기 때문에 이를 구성할 수 없습니다!
해결책은 LLM 이후에 상태를 주입하는 것입니다.
이는 아래와 같이 InjectedState 와 InjectedToolCallId 어노테이션을 사용하여 구현됩니다.
@tool
def calculator_wstate(
operation: Literal["add","subtract","multiply","divide"],
a: Union[int, float],
b: Union[int, float],
state: Annotated[CalcState, InjectedState], # ← LLM에 전송되지 않음
tool_call_id: Annotated[str, InjectedToolCallId], # ← LLM에 전송되지 않음
) -> Union[int, float]:⚠️ InjectedState, InjectedStore, get_runtime, InjectedToolCallId annotations를 사용하는 이전 패턴이 ToolRuntime 로 대체되었습니다. ToolRuntime는 tools가 state, context, store, streaming, config, tool call ID에 액세스할 수 있도록 하는 통합 매개변수입니다.
이는 LLM에 제공되는 설명에서 state를 제거하고, ToolNode에서 도구를 호출할 때 이를 주입합니다. tool_call_id도 포함됩니다. 이에 대해서는 다음 섹션에서 설명합니다.
상태 업데이트
도구는 일반적으로 상태의 messages 필드에 포함된 ToolMessage를 통해 관측 결과를 LLM에 반환합니다. 상태의 추가 멤버를 업데이트하려면 이 업데이트를 확장해야 합니다. 아래 반환 예시와 같이 Command를 사용하여 수행합니다.
return Command(
update={
"ops": ops,
"messages": [
ToolMessage(f"{result}", tool_call_id=tool_call_id)
]
}
)참고:
ToolMessage를 생성하려면tool_call_id가 필요합니다.
from langchain_core.messages import ToolMessage
from langchain_core.tools import InjectedToolCallId
from langgraph.prebuilt import InjectedState
from langgraph.types import Command
@tool
def calculator_with_state(
operation: Literal["add", "subtract", "multiply", "divide"],
a: int | float,
b: int | float,
state: Annotated[CalcState, InjectedState], # LLM으로 전송되지 않음
tool_call_id: Annotated[str, InjectedToolCallId], # LLM으로 전송되지 않음
) -> Command:
"""두 개의 입력값을 받는 계산기 도구"""
if operation == "divide" and b == 0:
raise ValueError("0으로 나눌 수 없습니다.")
result = "unknown operation"
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
result = a / b
ops = [f"({operation}, {a}, {b}),"]
return Command(
update={
"ops": ops,
"messages": [
ToolMessage(f"{result}", tool_call_id=tool_call_id),
],
},
)tools = [calculator_with_state] # new tool
agent = create_agent(
model,
tools=tools,
system_prompt=SYSTEM_PROMPT,
state_schema=CalcState,
).with_config({"recursion_limit": 20})# 사용 예시
result2 = agent.invoke(
{
"messages": [HumanMessage("3.1 * 4.2는 얼마입니까?")],
}
)
format_messages(result2["messages"])╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮ │ 3.1 * 4.2는 얼마입니까? │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ │ │ │ │ 🔧 Tool Call: calculator_with_state │ │ Args: { │ │ "operation": "multiply", │ │ "a": 3.1, │ │ "b": 4.2 │ │ } │ │ ID: call_NYErnC0N8LZVUupESiTMPRvI │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮ │ 13.020000000000001 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ 3.1 곱하기 4.2는 13.02입니다. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
# 응답에 ops 필드가 포함되었음을 확인합니다.
JSON(result2)<IPython.core.display.JSON object>
한 가지 예시를 더 살펴보겠습니다. 이 예시에서 이중 도구 호출을 확인합니다. 도구 노드는 이들을 병렬로 실행합니다.
# Example usage
result3 = agent.invoke(
{
"messages": [
HumanMessage("3.1 × 4.2 + 5.5 × 6.5는 얼마입니까?"),
],
}
)
format_messages(result3["messages"])╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮ │ 3.1 × 4.2 + 5.5 × 6.5는 얼마입니까? │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ │ │ │ │ 🔧 Tool Call: calculator_with_state │ │ Args: { │ │ "operation": "multiply", │ │ "a": 3.1, │ │ "b": 4.2 │ │ } │ │ ID: call_rBMD0skUF8cWy2yPfIAgGeMl │ │ │ │ 🔧 Tool Call: calculator_with_state │ │ Args: { │ │ "operation": "multiply", │ │ "a": 5.5, │ │ "b": 6.5 │ │ } │ │ ID: call_UQY3fjpE4LT7sH5ugekdlxnJ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮ │ 13.020000000000001 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮ │ 35.75 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ │ │ │ │ 🔧 Tool Call: calculator_with_state │ │ Args: { │ │ "operation": "add", │ │ "a": 13.02, │ │ "b": 35.75 │ │ } │ │ ID: call_HwCpj1EwrIDrI20EoRTaqWEw │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮ │ 48.769999999999996 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ 3.1 × 4.2 + 5.5 × 6.5는 약 48.77입니다. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
JSON(result3)<IPython.core.display.JSON object>
# Example usage - create your own
result4 = agent.invoke(
{
"messages": [HumanMessage("직접 예시를 만들어 보시겠습니까?")],
}
)
format_messages(result4["messages"])╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮ │ 직접 예시를 만들어 보시겠습니까? │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ │ │ │ │ 🔧 Tool Call: calculator_with_state │ │ Args: { │ │ "operation": "add", │ │ "a": 10, │ │ "b": 5 │ │ } │ │ ID: call_ILsbfUCXb4Ascbuza7Fi2UHp │ │ │ │ 🔧 Tool Call: calculator_with_state │ │ Args: { │ │ "operation": "subtract", │ │ "a": 20, │ │ "b": 4 │ │ } │ │ ID: call_FSLJOkXIbNgo4FtMIJkZTVUQ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮ │ 15 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮ │ 16 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ 물론입니다. 예를 들어, 10과 5를 더하면 15이고, 20에서 4를 빼면 16이 됩니다. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
ToolRuntime
아래는 ToolRuntime를 사용하는 예시입니다.
from langchain.tools import ToolRuntime, tool
@tool("calculator")
def calculator_with_runtime(
operation: Literal["add", "subtract", "multiply", "divide"],
a: int | float,
b: int | float,
runtime: ToolRuntime, # ToolRuntime 매개변수는 모델에 표시되지 않습니다.
) -> Command:
"""두 개의 입력값을 받는 계산기 도구"""
if operation == "divide" and b == 0:
raise ValueError("0으로 나눌 수 없습니다.")
result = "unknown operation"
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
result = a / b
ops = [f"({operation}, {a}, {b}),"]
return Command(
update={
"ops": ops,
"messages": [
ToolMessage(f"{result}", tool_call_id=runtime.tool_call_id),
],
},
)tools = [calculator_with_runtime] # new tool
agent = create_agent(
model,
tools=tools,
system_prompt=SYSTEM_PROMPT,
state_schema=CalcState,
).with_config({"recursion_limit": 20})result5 = agent.invoke(
{
"messages": [HumanMessage("직접 예시를 만들어 보시겠습니까?")],
}
)
format_messages(result5["messages"])╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮ │ 직접 예시를 만들어 보시겠습니까? │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ │ │ │ │ 🔧 Tool Call: calculator │ │ Args: { │ │ "operation": "add", │ │ "a": 5, │ │ "b": 3 │ │ } │ │ ID: call_f1q3GRc9O4446X7uPSKrDCKM │ │ │ │ 🔧 Tool Call: calculator │ │ Args: { │ │ "operation": "subtract", │ │ "a": 10, │ │ "b": 4 │ │ } │ │ ID: call_k9jQyeKRPKq892XS59Yop1sN │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮ │ 8 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮ │ 6 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮ │ 물론입니다. 예를 들어, 5와 3을 더하면 8이 되고, 10에서 4를 빼면 6이 됩니다. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

