LangChain 문서의 정의를 쉽게 풀면 이렇게 말할 수 있습니다. ([LangChain Docs][1])
에이전트는
언어 모델(LLM) 에게
여러 도구를 쥐여주고,
그걸 언제 어떻게 쓸지 스스로 판단하게 만든 시스템이다.
보통 LLM에게 그냥 “오늘 서울 날씨 알려줘”라고 하면, 모델은 자기가 알고 있는 과거 지식만 가지고 답을 만듭니다.
하지만 에이전트는 이렇게 행동할 수 있습니다.
이런 패턴을 LangChain에서는 ReAct (Reason + Act) 루프라고 부릅니다. ([LangChain Docs][1])
에이전트의 구조를 간단히 그림으로 표현하면 대략 이런 흐름입니다.

조금 더 풀어보면
User: “무선 헤드폰 중 지금 인기 많은 모델 찾아서 재고도 확인해줘.”
Agent
Tools
search_products : 인기 상품을 검색check_inventory : 특정 상품 재고 확인문서에도 비슷한 예시가 나오는데,
“무선 헤드폰 인기 모델을 찾고 재고를 확인하는” 도구 호출 흐름을 보여줍니다. ([LangChain Docs][1])
LangChain Agents 문서에서 말하는 핵심 요소는 다음 네 가지입니다. ([LangChain Docs][1])
각각을 아주 기초부터 볼게요.
가장 기본적인 방식입니다.
에이전트 생성 시 한 번만 모델을 정해두고, 이후에도 계속 그 모델만 사용하는 방식입니다. ([LangChain Docs][1])
문서에서는 이런 식으로 예시를 들고 있습니다.
from langchain.agents import create_agent
agent = create_agent(
"gpt-5", # 또는 "openai:gpt-5" 식의 모델 식별자
tools=tools
)
또는 조금 더 세밀하게 설정하고 싶다면 실제 모델 인스턴스를 직접 만들어서 넘깁니다. ([LangChain Docs][1])
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="gpt-5",
temperature=0.1,
max_tokens=1000,
timeout=30,
)
agent = create_agent(model, tools=tools)
여기서 중요한 포인트
조금 더 고급: 상황에 따라 모델을 바꾸고 싶을 때 사용합니다.
예를 들어
이걸 LangChain에서는 미들웨어(middleware) 를 통해 구현합니다. 문서에서는 @wrap_model_call 데코레이터를 사용해, 대화 길이에 따라 gpt-4o-mini와 gpt-4o를 바꾸는 예시를 보여줍니다. ([LangChain Docs][1])
구조는 대략 이런 느낌입니다.
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
basic_model = ChatOpenAI(model="gpt-4o-mini")
advanced_model = ChatOpenAI(model="gpt-4o")
@wrap_model_call
def dynamic_model_selection(request: ModelRequest, handler) -> ModelResponse:
message_count = len(request.state["messages"])
if message_count > 10:
request.model = advanced_model # 긴 대화 → 강한 모델
else:
request.model = basic_model # 짧은 대화 → 저렴한 모델
return handler(request)
agent = create_agent(
model=basic_model,
tools=tools,
middleware=[dynamic_model_selection]
)
정리하면
LLM은 원래 “텍스트를 생성하는 모델”일 뿐,
직접 데이터베이스를 조회하거나 API를 호출하지 못합니다.
에이전트에서 Tools는 다음 역할을 합니다. ([LangChain Docs][1])
가장 기본적인 형태는 “파이썬 함수에 @tool 데코레이터를 붙이는 것”입니다. ([LangChain Docs][1])
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def search_product(query: str) -> str:
"""상품 검색"""
return f"검색 결과: {query} 관련 상위 5개 상품을 찾았습니다."
@tool
def get_weather(location: str) -> str:
"""날씨 조회"""
return f"{location}의 현재 날씨: 맑음, 23도"
tools = [search_product, get_weather]
agent = create_agent(model, tools=tools)
이렇게 등록해 두면, LLM은 대화 중에
와 같은 판단을 하고 search_product, get_weather를 호출할 수 있습니다. ([LangChain Docs][1])
실제 서비스에서는
같은 에러가 자주 납니다.
아래는@wrap_tool_call 미들웨어로 에러를 잡아서 LLM이 이해할 수 있는 형태의 메시지로 바꿔 주는 예시를 보여줍니다. ([LangChain Docs][1])
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call, ToolRequest, ToolResponse
from langchain_core.messages import ToolMessage
from langchain_openai import ChatOpenAI
# 1) 원래의 도구 정의
@tool
def divide(a: float, b: float) -> float:
"""a를 b로 나누어 결과를 반환합니다."""
return a / b
tools = [divide]
# 2) wrap_tool_call 미들웨어 정의
@wrap_tool_call
def safe_tool_executor(request: ToolRequest, handler) -> ToolResponse:
"""
- request: 어떤 도구를 어떤 인자로 호출하려는지 정보가 들어 있음
- handler: 실제로 도구를 실행하는 함수
"""
try:
# 실제 도구 실행
response = handler(request)
return response
except ZeroDivisionError as e:
# 0으로 나누기 같은 에러를 잡아서 LLM이 이해할 수 있는 메시지로 변환
error_msg = ToolMessage(
content="0으로 나누기는 불가능합니다. 분모(b)에 0이 아닌 값을 넣어주세요.",
tool_call_id=request.id,
)
return ToolResponse(output=[error_msg])
except Exception as e:
# 그 외 에러에 대한 일반적인 처리
error_msg = ToolMessage(
content=f"도구 실행 중 에러가 발생했습니다: {str(e)}",
tool_call_id=request.id,
)
return ToolResponse(output=[error_msg])
# 3) 에이전트 생성
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent = create_agent(
model=model,
tools=tools,
middleware=[safe_tool_executor], # 여기서 wrap_tool_call 미들웨어를 등록
)
# 4) 에이전트 호출 예시
result = agent.invoke({
"messages": [
{"role": "user", "content": "10을 0으로 나눠줘"}
]
})
print(result["messages"][-1].content)
핵심 아이디어는
ToolMessage 형태로 “이런 에러가 났다”고 모델에게 알려주기문서에서는 에이전트가 도구를 어떻게 사용하는지 ReAct 루프 예시로 설명합니다. ([LangChain Docs][1])
예를 들어:
사용자: “지금 인기 많은 무선 헤드폰 찾아주고, 재고 있는지도 알려줘.”
모델의 내부 단계
search_products("무선 헤드폰") 호출도구 응답: “WH-1000XM5, …”
모델의 내부 단계
check_inventory("WH-1000XM5") 호출도구 응답: “재고 10개”
최종 답변 생성: “현재 가장 인기 있는 모델은 WH-1000XM5이고, 재고는 10개입니다.”
이 과정을 시각적으로 그리면:

이게 바로 Agents 문서에서 말하는 “도구를 루프 안에서 반복적으로 사용하는 에이전트”입니다. ([LangChain Docs][1])
에이전트에게 “어떤 방식으로 답해야 하는지”를 알려주는 설명서입니다. ([LangChain Docs][1])
가장 단순하게는 system_prompt 문자열 하나를 넘깁니다.
agent = create_agent(
model,
tools,
system_prompt="당신은 간결하고 정확하게 답하는 유능한 비서입니다."
)
조금 더 고급: “상황에 따라 system prompt를 바꾸고 싶다”면 @dynamic_prompt 데코레이터를 사용합니다. ([LangChain Docs][1])
문서에서는 예를 들어
user_role 이 "expert" → 기술적으로 깊게 설명"beginner" → 최대한 쉽게 설명처럼 runtime context에 따라 프롬프트를 다르게 만드는 예시를 보여줍니다.
아이디어만 간단히 표현하면:
from typing import TypedDict
from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest
class Context(TypedDict):
user_role: str
@dynamic_prompt
def user_role_prompt(request: ModelRequest) -> str:
role = request.runtime.context.get("user_role", "user")
base = "You are a helpful assistant."
if role == "expert":
return base + " Provide detailed technical responses."
elif role == "beginner":
return base + " Explain concepts simply and avoid jargon."
return base
agent = create_agent(
model="gpt-4o",
tools=[...],
middleware=[user_role_prompt],
context_schema=Context
)
이렇게 하면
context={"user_role": "expert"} / "beginner"에 따라 행동 방식이 달라짐 ([LangChain Docs][1])invoke에이전트는 상태(State) 안에 messages라는 대화 기록을 가지고 있습니다. ([LangChain Docs][1])
가장 기본적인 호출은:
result = agent.invoke(
{"messages": [{"role": "user", "content": "서울 날씨 알려줘"}]}
)
result를 반환합니다. ([LangChain Docs][1])stream에이전트가 여러 단계(도구 호출 → reasoning → 또 도구 호출)를 거치는 동안,
중간 상황을 실시간으로 보고 싶다면 stream을 사용합니다. ([LangChain Docs][1])
문서 예시는 이런 식입니다. ([LangChain Docs][1])
for chunk in agent.stream(
{
"messages": [{"role": "user", "content": "AI 관련 뉴스를 찾고 요약해줘"}]
},
stream_mode="values",
):
latest = chunk["messages"][-1]
if latest.content:
print("Agent:", latest.content)
elif latest.tool_calls:
print("Calling tools:", [tc["name"] for tc in latest.tool_calls])
이렇게 하면
때로는 “자연어 문장”이 아니라, 딱 정해진 필드로 결과를 받고 싶을 때가 있습니다.
예:
“이 문장에서 이름, 이메일, 전화번호를 추출해줘.”
이럴 때는 LangChain이 제공하는 structured output 기능을 사용합니다. ([LangChain Docs][1])
ToolStrategy는 “가상의 도구 호출”을 이용해서 구조화된 결과를 만드는 방법입니다.
어떤 tool-calling 지원 모델이든 사용할 수 있습니다. ([LangChain Docs][1])
from pydantic import BaseModel
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
class ContactInfo(BaseModel):
name: str
email: str
phone: str
agent = create_agent(
model="gpt-4o-mini",
tools=[search_tool],
response_format=ToolStrategy(ContactInfo)
)
result = agent.invoke({
"messages": [{
"role": "user",
"content": "John Doe, john@example.com, (555) 123-4567에서 연락처를 뽑아줘"
}]
})
structured = result["structured_response"] # ContactInfo 인스턴스
ProviderStrategy는 OpenAI 같은 제공자 자체의 structured output 기능을 활용합니다.
더 안정적인 대신, 해당 기능을 지원하는 모델에서만 사용할 수 있습니다. ([LangChain Docs][1])
from langchain.agents.structured_output import ProviderStrategy
agent = create_agent(
model="gpt-4o",
response_format=ProviderStrategy(ContactInfo)
)
참고: langchain 1.0부터는 단순히 response_format=ContactInfo처럼 직접 스키마만 넘기는 방식은 지원되지 않고, 반드시 ToolStrategy 또는 ProviderStrategy를 써야 합니다. ([LangChain Docs][1])
Agents 문서에서 말하는 “Memory”는 사실 에이전트의 State 개념과 연결됩니다. ([LangChain Docs][1])
기본적으로
AgentState 안에 messages를 포함여기에 더해서
등을 함께 저장하고 싶다면 커스텀 state schema를 정의할 수 있습니다. ([LangChain Docs][1])
두 가지 방식이 있습니다.
Middleware로 state 정의 (권장)
state_schema 인자로 AgentState를 상속한 TypedDict 전달
예시 구조는 문서에 다음과 같이 나옵니다. ([LangChain Docs][1])
CustomState(AgentState)에 user_preferences: dict 추가{"user_preferences": {...}} 같이 넘겨서 사용마지막으로, 문서 끝에서 미들웨어(Middleware) 를 중요한 확장 지점으로 소개합니다. ([LangChain Docs][1])
미들웨어로 할 수 있는 일들:
@wrap_tool_call)@wrap_model_call)@dynamic_prompt)즉, 에이전트의 생명주기 전체에서 “가로채서 가공할 수 있는 훅(hook)”을 제공하는 레이어라고 보면 됩니다.
정리해보면, LangChain Agents 문서의 핵심은 다음 네 줄로 요약할 수 있습니다.
create_agent(model, tools, ...) 로 생산 준비된 에이전트를 만들 수 있다. ([LangChain Docs][1])invoke로 한 번에 결과를 받거나, stream으로 중간 상태를 스트리밍 받을 수 있다. ([LangChain Docs][1])