LangChain

create_agent & 미들웨어 — LangChain 1.0 에이전트 표준

빌드 피벗(Build Pivot) 2026. 6. 29. 06:58
반응형
LANGCHAIN 실전 1.0 · 04
create_agent & 미들웨어 — LangChain 1.0 에이전트 표준
AgentExecutor·텍스트 ReAct가 폐기되고 create_agent로 일원화된 이유, 도구 정의·bind_tools·6훅 미들웨어·HITL·멀티에이전트까지
🟡 중급 ⏱️ 약 14분 🔧 LangChain 1.x 🗓️ 최종 검토 2026-06-21

TL;DR — LangChain 1.0의 에이전트 표준은 langchain.agents.create_agent 하나입니다. 옛 AgentExecutor · initialize_agent · 텍스트 ReAct(hub.pull)와 중간세대 create_tool_calling_agent는 모두 여기로 모였습니다. 기본 설치에서 from langchain.agents import AgentExecutor는 ImportError가 납니다. create_agent는 LangGraph 런타임 위에서 model → tool-call → final 루프를 영속성·스트리밍과 함께 돌립니다. 1.0의 핵심 신개념인 미들웨어(6훅)로 PII 마스킹·히스토리 압축·사람 승인(HITL)을 선언적으로 끼워 넣습니다.

이 글은 옛 AgentExecutor 자료로 에이전트를 익혔거나 1.0 에이전트를 처음 보는 개발자create_agent와 미들웨어가 무엇을 어떻게 바꿨는지를 이해하도록 돕습니다. 나아가 도구·모델·미들웨어를 조립해 실무용 에이전트를 짜는 기준을 얻도록 합니다.

이 글에서 다루는 것

  • 도구 정의(@tool · docstring=라우팅 · Pydantic v2 인자)
  • bind_tools — 모델 레벨 툴콜(AIMessage.tool_calls → ToolMessage)
  • create_agent 표준 파라미터와 LangGraph 런타임
  • 미들웨어 6훅 + 프리빌트 3종 · HITL · 멀티에이전트

이 글에서 다루지 않는 것

  • LangGraph 저수준 StateGraph·interrupt() 상세 (3편에서)
  • RAG 검색·Agentic RAG (5편에서) · 관측·평가(LangSmith) (6편에서)

1. 왜 create_agent인가 — 에이전트 진입점의 일원화

LangChain의 에이전트는 역사가 복잡합니다. 0.x 시절에만 해도 initialize_agentAgentExecutor + 텍스트 ReAct → create_tool_calling_agentlanggraph.prebuilt.create_react_agent까지, 에이전트를 만드는 길이 세대마다 갈렸습니다. 옛 자료를 따라 치면 어느 세대 코드인지에 따라 import부터 달라 헷갈리기 쉽습니다.

1.0은 이 갈래를 langchain.agents.create_agent 하나로 모았습니다. 위에 나열한 진입점은 전부 create_agent로 일원화됐습니다. 텍스트 ReAct는 모델 네이티브 tool-calling 이전의 방식이라 사실상 역사가 됐습니다. 그래서 새 코드의 출발점은 더 고민할 것 없이 create_agent 한 줄입니다.

비유하면 에이전트는 도구 상자를 든 신입 직원입니다. 옛 방식(텍스트 ReAct)은 직원에게 "생각하고(Thought), 행동하고(Action), 결과를 보고(Observation), 다시 생각하라"는 절차를 글로 적어 외우게 한 것입니다. 모델이 함수 호출을 네이티브로 지원하기 전이라 어쩔 수 없었습니다. 지금은 모델이 "어떤 도구를 어떤 인자로 부를지"를 구조화된 형태로 직접 내놓습니다. create_agent는 그 구조화된 호출을 받아 실제 도구를 실행하고 결과를 되돌리는 루프(model → tool-call → final)를 표준화한 것입니다. 그리고 이 루프는 LangGraph 런타임 위에서 돌아, 영속성·스트리밍·체크포인팅을 공짜로 얻습니다.

2. 도구 정의 — docstring이 곧 라우팅

에이전트의 재료는 도구입니다. 가장 단순한 정의는 함수에 @tool 데코레이터를 붙이는 것입니다. 아래는 이 글의 첫 코드이므로 환경부터 짚습니다.

환경

  • 언어/런타임: Python 3.10 이상(1.x 요구)
  • 핵심: langchain · langchain-core · langgraph(create_agent 런타임) · 파트너 패키지(예: langchain-openai)
  • 스키마: Pydantic v2(도구 인자·구조화 출력 전제)
PYTHON · @tool 데코레이터로 도구 정의
from langchain.tools import tool from pydantic import BaseModel, Field # 인자 스키마는 Pydantic v2 @tool def get_weather(city: str) -> str: """도시의 현재 날씨를 조회한다.""" # docstring = 에이전트가 읽는 라우팅 설명 return f"{city}는 맑음 22도"

여기서 핵심은 docstring입니다. 도구의 name과 description이 곧 라우팅 로직이기 때문입니다. 에이전트는 코드를 보는 게 아니라 도구의 설명을 읽고 "언제 이 도구를 쓸지"를 판단합니다. 그래서 docstring을 모호하게 쓰면 모델이 엉뚱한 도구를 고르거나 아예 도구를 안 부르는 일이 생깁니다. 함정은 여기 있습니다 — 도구가 동작하지 않을 때 코드부터 의심하기 쉽습니다. 그러나 실제 원인은 설명 문장이 부실한 경우가 많습니다.

create_agent의 tools는 세 형태를 받습니다 — ① @tool/BaseTool, ② 타입힌트와 docstring을 갖춘 순수 callable, ③ 프로바이더 내장 도구 dict. 인자가 여러 개거나 검증이 필요하면 Pydantic v2 모델을 인자 스키마로 붙여 입력을 구조화합니다. 즉 도구는 이름·설명·인자 스키마의 묶음이며 그 셋이 모델의 선택 품질을 좌우합니다.

3. bind_tools — 모델 레벨 툴 호출

create_agent를 쓰면 도구 호출 루프는 대부분 자동입니다. 하지만 그 밑바닥에서 무슨 일이 벌어지는지 알아두면, 에이전트가 도구를 잘못 부를 때 어디를 봐야 할지가 보입니다. 그 밑바닥이 bind_tools입니다.

핵심 원리는 이렇습니다 — 모델은 도구를 결정만 한다. 실행은 우리가 한다. 모델에 도구 목록을 묶어주면, 모델은 코드를 실행하는 대신 "이 도구를 이 인자로 부르라"는 지시를 내놓습니다.

PYTHON · bind_tools → tool_calls → ToolMessage
model_with_tools = model.bind_tools([get_weather], tool_choice="any") # 강제: 'any'/특정 툴명 ai = model_with_tools.invoke("서울 날씨?") ai.tool_calls # [{'name':'get_weather','args':{'city':'서울'},'id':'call_..'}] # → 실제 실행 후 ToolMessage(tool_call_id=...)로 결과를 모델에 되돌린다

흐름을 메시지로 풀면 AIMessage.tool_calls → (도구 실행) → ToolMessage(tool_call_id로 연결) → 다시 모델입니다. 모델이 도구 호출을 담은 AIMessage를 냅니다. 우리가 그 도구를 실행해 결과를 ToolMessage로 만들어 같은 tool_call_id에 묶어 되돌리면, 모델이 그 결과를 보고 다음 행동을 정합니다. 옛 자료에 보이는 FunctionMessage는 옛 OpenAI functions API 잔재입니다. 현행 tools API는 이 tool_calls → ToolMessage 흐름을 씁니다(2편의 메시지 설명과 같은 줄기입니다).

create_agent는 바로 이 왕복을 자동으로 반복하는 루프입니다. 그래서 실무에서는 bind_tools를 직접 다루기보다 create_agent에 도구를 넘기는 쪽이 일반적입니다. 다만 "왜 이 도구가 안 불렸지?"를 디버깅할 때는 결국 이 tool_calls를 들여다보게 됩니다.

4. create_agent 표준 — 한 함수로 잡는 에이전트

이제 본론입니다. create_agent는 모델·도구·시스템 프롬프트에 더해 구조화 출력·미들웨어·영속성을 한 자리에서 받습니다.

PYTHON · create_agent 표준 형태
from langchain.agents import create_agent from langgraph.checkpoint.memory import InMemorySaver agent = create_agent( model="openai:gpt-5.5", # init_chat_model 규칙('provider:model') tools=[get_weather, get_time], system_prompt="너는 친절한 비서다.", # response_format=WeatherReport, # 구조화 출력(선택) # middleware=[...], # 미들웨어(선택, 5절) checkpointer=InMemorySaver(), # 영속성(선택) ) out = agent.invoke( {"messages": [{"role": "user", "content": "서울 날씨랑 시간 알려줘"}]}, config={"configurable": {"thread_id": "u1"}}, )

파라미터를 하나씩 보면 의도가 분명합니다. model은 1편의 init_chat_model 규칙을 그대로 따라 "provider:model" 문자열을 받으므로 프로바이더 교체가 한 줄로 끝납니다. response_format에 스키마를 주면 에이전트 루프 안에서 검증된 객체를 받습니다. checkpointer는 대화 상태를 저장해 thread_id별로 이어가게 합니다.

📌 반환은 컴파일된 그래프 — create_agent는 LangGraph 런타임 위에서 model → tool-call → final 루프를 durable execution·스트리밍·체크포인팅과 함께 제공합니다. 반환값은 컴파일된 LangGraph 그래프라서, 3편에서 본 invoke/stream(stream_mode=...)를 그대로 씁니다. 에이전트를 쓰면 langgraph가 함께 깔리는 이유입니다.

에이전트 루프 안에서의 구조화 출력에는 두 전략이 있습니다. ToolStrategy는 추가 LLM 호출 없이 메인 루프 안에서 결과를 구조화합니다. 반면 ProviderStrategy는 프로바이더 네이티브 구조화 출력을 씁니다. 단순 호출에서의 with_structured_output(1편)이 에이전트 루프 안으로 들어온 형태로 이해하면 됩니다.

5. 미들웨어 6훅 — 1.0의 핵심 신개념

미들웨어는 1.0이 에이전트에 새로 들인 가장 큰 개념입니다. AgentExecutor가 못 하던 분기·HITL·컨텍스트 관리를, 루프 코드를 직접 고치지 않고 선언적으로 끼워 넣는 장치입니다. 비유하면 에이전트 루프라는 컨베이어 벨트의 특정 지점마다 검사·가공 스테이션을 끼우는 것입니다 — 모델 호출 직전에 민감정보를 마스킹하고 히스토리가 길어지면 압축합니다. 위험한 도구 실행 직전에는 사람의 승인을 받는 식입니다.

끼울 수 있는 자리는 6개 훅입니다. AgentMiddleware를 서브클래싱해 필요한 훅만 구현합니다.

① before_agent · 에이전트 시작 전
전체 루프가 돌기 직전 1회 — 초기 컨텍스트 준비·검증
② before_model · 모델 호출 전
매 모델 호출 직전 — 입력 메시지 정리·PII 마스킹·히스토리 손질
③ wrap_model_call · 모델 호출 감싸기
모델 호출 전후를 감싸 재시도·캐싱·관측 등을 한 자리에서
④ wrap_tool_call · 도구 호출 감싸기
도구 실행 전후를 감싸 승인·검증·예외 처리(HITL의 토대)
⑤ after_model · 모델 응답 후
모델 응답을 받은 직후 — 출력 검증·필터·후처리
⑥ after_agent · 에이전트 종료 후
루프 종료 직후 1회 — 최종 결과 정리·로깅
그림 1. 미들웨어 6훅 — model → tool-call → final 루프에 끼우는 자리

직접 서브클래싱하지 않아도 자주 쓰는 패턴은 프리빌트 3종으로 제공됩니다. 아래는 송금 도구를 쓰는 에이전트에 세 미들웨어를 한꺼번에 끼운 예입니다.

PYTHON · 프리빌트 미들웨어 3종
from langchain.agents.middleware import ( HumanInTheLoopMiddleware, SummarizationMiddleware, PIIMiddleware, ) agent = create_agent( model="anthropic:claude-...", tools=[transfer_money], middleware=[ PIIMiddleware("email", strategy="redact"), # PII 마스킹/제거 SummarizationMiddleware(model="openai:gpt-5.5", trigger=("tokens", 4000), keep=("messages", 20)), # 히스토리 압축 HumanInTheLoopMiddleware(interrupt_on={"transfer_money": True}), # 민감 툴 승인 ], checkpointer=InMemorySaver(), )

세 미들웨어의 역할은 이렇습니다. PIIMiddleware는 email·credit_card 같은 민감정보를 redact/mask 합니다. SummarizationMiddleware는 히스토리를 압축하는데, trigger/keep("tokens",N)·("messages",N)·("fraction",0.3) 튜플로 받습니다. 다만 ("tokens",N)의 토큰 수는 기본값이 모델 토크나이저가 아닌 문자 기반 근사라, 정밀한 임계가 필요하면 token_counter에 실제 토크나이저를 넘기는 게 안전합니다. 이 미들웨어가 폐기된 ConversationSummaryMemory를 대체합니다. HumanInTheLoopMiddleware는 민감한 도구 호출을 사람이 승인/편집/거부하게 하는데, 이 부분은 비중이 커서 다음 절에서 따로 봅니다.

6. Human-in-the-Loop — checkpointer가 필수인 이유

DB 쓰기·결제·메일 발송처럼 한 번 실행하면 되돌릴 수 없는 도구가 있습니다. 에이전트가 자동으로 이런 도구를 부르게 두는 건 위험합니다. HumanInTheLoopMiddleware는 그런 도구의 실행 에 멈춰 사람의 승인을 받습니다.

PYTHON · HITL — 민감 도구 승인/편집/거부
HumanInTheLoopMiddleware( interrupt_on={ "transfer_money": {"allowed_decisions": ["approve", "edit", "reject"]}, } ) # 단순 승인만이면: interrupt_on={"transfer_money": True} # ★ checkpointer 필수 — 멈춘 지점을 저장해야 재개할 수 있다

여기서 빠지기 쉬운 함정이 checkpointer 필수입니다. HITL은 도구 실행 직전에 루프를 중단(interrupt)했다가, 사람이 결정을 내리면 그 지점부터 재개합니다. 중단된 상태를 어딘가 저장해 두지 않으면 재개할 수 없으므로, checkpointer 없이 HITL을 켜면 동작하지 않습니다. 이것은 3편의 영속성(checkpointer + thread_id)이 에이전트에서 왜 중요한지를 보여주는 가장 실무적인 사례입니다.

고수준 API는 이렇게 미들웨어 한 줄로 끝납니다. 더 세밀한 제어가 필요하면 3편에서 본 저수준 interrupt()를 직접 쓸 수 있습니다. 대부분의 "민감 도구 승인" 시나리오는 이 미들웨어로 충분합니다.

7. 멀티에이전트 — supervisor / subagent

하나의 에이전트가 모든 일을 다 하기보다, 역할을 나눈 여러 에이전트를 조율하는 편이 나을 때가 있습니다. 1.0에서는 create_agent로 만든 서브에이전트를 다시 도구처럼 묶어 상위 supervisor가 오케스트레이션합니다.

supervisor 에이전트
서브에이전트를 도구처럼 호출해 작업 분배
calendar_agent
일정 도구 + 자체 미들웨어
email_agent
메일 도구 + 자체 미들웨어(HITL)
checkpointer로 상태 공유 · 각 서브에이전트는 독립 도구·미들웨어를 가진다
그림 2. supervisor가 create_agent 서브에이전트를 도구처럼 오케스트레이션

예컨대 calendar_agent·email_agent를 각자 도구와 미들웨어를 가진 create_agent로 만듭니다. 그러면 supervisor가 이들을 호출해 일을 나눕니다. 상태는 checkpointer로 공유합니다. 더 복잡한 토폴로지가 필요하면 LangGraph StateGraph를 직접 짜거나 보조 패키지 langgraph-supervisor·langgraph-swarm을 씁니다. 정리하면, 간단한 위계는 create_agent 조합으로, 복잡한 그래프는 LangGraph 직접 작성으로 갑니다.

8. 🕰️ 레거시 에이전트 vs 현행 — 같은 작업 두 시대

🕰️ 레거시 에이전트(역사)
AgentExecutor·initialize_agent(0.2 deprecated → langchain-classic), 텍스트 ReAct(create_react_agent + hub.pull("hwchase17/react"), Thought→Action→Observation→Final), 중간세대 create_tool_calling_agent, langgraph.prebuilt.create_react_agent(v1 deprecated)는 모두 langchain.agents.create_agent로 일원화됐습니다. from langchain.agents import AgentExecutor는 1.x 기본 설치에서 ImportError입니다.

같은 도구 쓰는 에이전트를 두 시대의 코드로 나란히 보면 차이가 한눈에 들어옵니다.

❌ 레거시 (0.1~0.2 · 텍스트 ReAct)
from langchain.agents import AgentExecutor
from langchain import hub
prompt = hub.pull("hwchase17/react")
agent = create_react_agent(llm, tools, prompt)
ex = AgentExecutor(agent=agent, tools=tools)
✅ 현행 (1.x · create_agent)
from langchain.agents import create_agent
agent = create_agent(
  "openai:gpt-5.5", tools=tools,
  middleware=[...], checkpointer=saver)
# LangGraph 런타임 · 영속·스트리밍 기본
그림 3. 같은 에이전트, 텍스트 ReAct 시절 코드 vs 1.0 create_agent
레거시 (옛 자료) 현행 (1.x)
AgentExecutor + initialize_agent langchain.agents.create_agent
텍스트 ReAct(hub.pull + create_react_agent) 모델 네이티브 tool-calling 루프
create_tool_calling_agent(중간세대) create_agent로 흡수
FunctionMessage AIMessage.tool_calls → ToolMessage
ConversationSummaryMemory SummarizationMiddleware
콜백·커스텀 루프로 승인 구현 HumanInTheLoopMiddleware(+checkpointer)

당장 옛 코드를 살려야 한다면 langchain-classic을 설치해 import 경로만 바꾸면 AgentExecutor도 동작은 합니다. 다만 그건 임시방편이므로 신규 코드는 오른쪽(현행)으로 쓰는 것을 권합니다. 텍스트 ReAct는 모델이 tool-calling을 네이티브로 못 하던 시절의 우회였습니다. 지금은 그럴 이유가 없기 때문입니다.

시리즈: 01 LangChain 1.0 시작하기 → 02 LCEL & Runnable → 03 LangGraph → 04 create_agent & 미들웨어 → 05 현대 RAG → 06 LangSmith

9. 정리 · 다음 편

✅ 1.0 에이전트인지 가르는 체크리스트
에이전트를 create_agent로 만드는가 (AgentExecutor ❌)
도구의 docstring·name이 라우팅 설명으로 충실한가
도구 결과를 tool_calls → ToolMessage로 다루는가 (FunctionMessage ❌)
PII·요약·승인을 미들웨어로 선언하는가
HITL을 쓸 때 checkpointer를 함께 두는가
★ 마치며

1.0의 에이전트는 갈래가 사라진 대신 조립 방식이 또렷해졌습니다. create_agent로 model → tool-call → final 루프를 잡습니다. 미들웨어로 PII·요약·승인 같은 횡단 관심사를 선언적으로 끼우고 도구의 docstring으로 라우팅을 다듬습니다. 그 밑에는 3편의 LangGraph 런타임이 영속성·스트리밍을 받쳐 줍니다. 다음 편에서는 에이전트가 외부 지식을 다루는 방식 — 현대 RAG, 고급 검색부터 Agentic RAG까지를 봅니다.

#LangChain #create_agent #미들웨어 #AI에이전트 #HITL #LangGraph #생성형AI #기술블로그
📚 참고 자료 · 공식 문서
본문 버전·API는 공식 문서 기반이며 LangChain API는 버전에 따라 변동될 수 있습니다(검증: LangChain 1.x, 2026-06).
안내 · 본 글은 LangChain 공식 문서·저장소를 기준으로 정리한 교육용 콘텐츠입니다. 코드는 개념 설명용 예시이며 LangChain API는 버전에 따라 달라질 수 있습니다.
반응형