langchain / / 2025. 11. 8. 21:13

[LangChain v1.0] Middleware

각 단계에서 에이전트 실행을 제어하고 커스터마이징

Middleware는 에이전트 내부에서 발생하는 일을 더욱 세밀하게 제어할 수 있는 방법을 제공합니다.

핵심 에이전트 루프는 모델을 호출하고, 모델이 실행할 도구를 선택하게 한 다음, 더 이상 도구를 호출하지 않을 때 종료하는 과정을 포함합니다:

Middleware는 이러한 각 단계 전후에 훅(hook)을 노출합니다:

Middleware가 할 수 있는 일은?

Monitor (모니터링)

로깅, 분석 및 디버깅을 통해 에이전트 동작을 추적합니다

Modify (수정)

프롬프트, 도구 선택 및 출력 형식을 변환합니다

Control (제어)

재시도, 폴백 및 조기 종료 로직을 추가합니다

Enforce (강제 적용)

속도 제한, 가드레일 및 PII 탐지를 적용합니다

Middleware는 create_agent에 전달하여 추가할 수 있습니다:

from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware, HumanInTheLoopMiddleware


agent = create_agent(
    model="gpt-4o",
    tools=[...],
    middleware=[SummarizationMiddleware(), HumanInTheLoopMiddleware()],
)

내장 Middleware

LangChain은 일반적인 사용 사례를 위한 사전 구축된 middleware를 제공합니다:

Summarization (요약)

토큰 제한에 도달할 때 대화 기록을 자동으로 요약합니다.

다음과 같은 경우에 완벽:

  • 컨텍스트 윈도우를 초과하는 장기 실행 대화
  • 광범위한 기록이 있는 다회차 대화
  • 전체 대화 컨텍스트를 보존하는 것이 중요한 애플리케이션
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware


agent = create_agent(
    model="gpt-4o",
    tools=[weather_tool, calculator_tool],
    middleware=[
        SummarizationMiddleware(
            model="gpt-4o-mini",
            max_tokens_before_summary=4000,  # 4000 토큰에서 요약 트리거
            messages_to_keep=20,  # 요약 후 최근 20개 메시지 유지
            summary_prompt="Custom prompt for summarization...",  # 선택사항
        ),
    ],
)

설정 옵션

매개변수 타입 설명
model string (필수) 요약을 생성하기 위한 모델
max_tokens_before_summary number 요약을 트리거하는 토큰 임계값
messages_to_keep number (기본값: "20") 보존할 최근 메시지
token_counter function 사용자 정의 토큰 계산 함수. 기본적으로 문자 기반 계산을 사용합니다.
summary_prompt string 사용자 정의 프롬프트 템플릿. 지정하지 않으면 내장 템플릿을 사용합니다.
summary_prefix string (기본값: "## Previous conversation summary:") 요약 메시지의 접두사

Human-in-the-loop (휴먼 인 더 루프)

도구 호출이 실행되기 전에 사람의 승인, 편집 또는 거부를 위해 에이전트 실행을 일시 중지합니다.

다음과 같은 경우에 사용:

  • 사람의 승인이 필요한 고위험 작업(데이터베이스 쓰기, 금융 거래)
  • 사람의 감독이 의무인 규정 준수 워크플로우
  • 사람의 피드백이 에이전트를 안내하는 데 사용되는 장기 실행 대화

from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver


agent = create_agent(
    model="gpt-4o",
    tools=[read_email_tool, send_email_tool],
    checkpointer=InMemorySaver(),
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                # 이메일 전송에 대한 승인, 편집 또는 거부 필요
                "send_email_tool": {
                    "allowed_decisions": ["approve", "edit", "reject"],
                },
                # 이메일 읽기는 자동 승인
                "read_email_tool": False,
            }
        ),
    ],
)

설정 옵션

매개변수 타입 설명
interrupt_on dict (필수) 도구 이름과 승인 설정의 매핑. 값은 True(기본 설정으로 중단), False(자동 승인) 또는 InterruptOnConfig 객체일 수 있습니다.
description_prefix string (기본값: "Tool execution requires approval") 작업 요청 설명의 접두사

InterruptOnConfig 옵션:

매개변수 타입 설명
allowed_decisions list[string] 허용되는 결정 목록: "approve", "edit" 또는 "reject"
description string | callable 사용자 정의 설명을 위한 정적 문자열 또는 호출 가능 함수

중요: Human-in-the-loop middleware는 중단 간 상태를 유지하기 위해 checkpointer가 필요합니다.

완전한 예제와 통합 패턴은 human-in-the-loop 문서를 참조하세요.

Anthropic prompt caching (Anthropic 프롬프트 캐싱)

Anthropic 모델에서 반복적인 프롬프트 접두사를 캐싱하여 비용을 절감합니다.

다음과 같은 경우에 사용:

  • 길고 반복되는 시스템 프롬프트가 있는 애플리케이션
  • 호출 간 동일한 컨텍스트를 재사용하는 에이전트
  • 대량 배포에서 API 비용 절감

Anthropic Prompt Caching 전략 및 제한 사항에 대해 자세히 알아보세요.

from langchain_anthropic import ChatAnthropic
from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware
from langchain.agents import create_agent


LONG_PROMPT = """
Please be a helpful assistant.

<Lots more context ...>
"""

agent = create_agent(
    model=ChatAnthropic(model="claude-sonnet-4-5-20250929"),
    system_prompt=LONG_PROMPT,
    middleware=[AnthropicPromptCachingMiddleware(ttl="5m")],
)

# 캐시 저장
agent.invoke({"messages": [HumanMessage("Hi, my name is Bob")]})

# 캐시 히트, 시스템 프롬프트가 캐시됨
agent.invoke({"messages": [HumanMessage("What's my name?")]})

설정 옵션

매개변수 타입 설명
type string (기본값: "ephemeral") 캐시 타입. 현재 "ephemeral"만 지원됩니다.
ttl string (기본값: "5m") 캐시된 콘텐츠의 유지 시간. 유효한 값: "5m" 또는 "1h"
min_messages_to_cache number (기본값: "0") 캐싱 시작 전 최소 메시지 수
unsupported_model_behavior string (기본값: "warn") 비-Anthropic 모델 사용 시 동작. 옵션: "ignore", "warn" 또는 "raise"

Model call limit (모델 호출 제한)

무한 루프나 과도한 비용을 방지하기 위해 모델 호출 수를 제한합니다.

다음과 같은 경우에 완벽:

  • 너무 많은 API 호출을 하는 폭주 에이전트 방지
  • 프로덕션 배포에 대한 비용 제어 시행
  • 특정 호출 예산 내에서 에이전트 동작 테스트
from langchain.agents import create_agent
from langchain.agents.middleware import ModelCallLimitMiddleware


agent = create_agent(
    model="gpt-4o",
    tools=[...],
    middleware=[
        ModelCallLimitMiddleware(
            thread_limit=10,  # 스레드당 최대 10회 호출(실행 전체)
            run_limit=5,  # 실행당 최대 5회 호출(단일 호출)
            exit_behavior="end",  # 또는 예외를 발생시키려면 "error"
        ),
    ],
)

설정 옵션

매개변수 타입 설명
thread_limit number 스레드의 모든 실행에서 최대 모델 호출 수. 기본값은 제한 없음입니다.
run_limit number 단일 호출당 최대 모델 호출 수. 기본값은 제한 없음입니다.
exit_behavior string (기본값: "end") 제한에 도달했을 때의 동작. 옵션: "end"(정상 종료) 또는 "error"(예외 발생)

Tool call limit (도구 호출 제한)

특정 도구 또는 모든 도구에 대한 도구 호출 수를 제한합니다.

다음과 같은 경우에 사용:

  • 값비싼 외부 API에 대한 과도한 호출 방지
  • 웹 검색 또는 데이터베이스 쿼리 제한
  • 특정 도구 사용에 대한 속도 제한 시행

from langchain.agents import create_agent
from langchain.agents.middleware import ToolCallLimitMiddleware


# 모든 도구 호출 제한
global_limiter = ToolCallLimitMiddleware(thread_limit=20, run_limit=10)

# 특정 도구 제한
search_limiter = ToolCallLimitMiddleware(
    tool_name="search",
    thread_limit=5,
    run_limit=3,
)

agent = create_agent(
    model="gpt-4o",
    tools=[...],
    middleware=[global_limiter, search_limiter],
)

설정 옵션

매개변수 타입 설명
tool_name string 제한할 특정 도구. 제공하지 않으면 모든 도구에 제한이 적용됩니다.
thread_limit number 스레드의 모든 실행에서 최대 도구 호출 수. 기본값은 제한 없음입니다.
run_limit number 단일 호출당 최대 도구 호출 수. 기본값은 제한 없음입니다.
exit_behavior string (기본값: "end") 제한에 도달했을 때의 동작. 옵션: "end"(정상 종료) 또는 "error"(예외 발생)

Model fallback (모델 폴백)

기본 모델이 실패할 때 자동으로 대체 모델로 폴백합니다.

다음과 같은 경우에 완벽:

  • 모델 중단을 처리하는 복원력 있는 에이전트 구축
  • 더 저렴한 모델로 폴백하여 비용 최적화
  • OpenAI, Anthropic 등의 제공자 이중화
from langchain.agents import create_agent
from langchain.agents.middleware import ModelFallbackMiddleware


agent = create_agent(
    model="gpt-4o",  # 기본 모델
    tools=[...],
    middleware=[
        ModelFallbackMiddleware(
            "gpt-4o-mini",  # 오류 시 먼저 시도
            "claude-3-5-sonnet-20241022",  # 그 다음 이것
        ),
    ],
)

설정 옵션

매개변수 타입 설명
first_model string | BaseChatModel (필수) 기본 모델이 실패할 때 시도할 첫 번째 폴백 모델. 모델 문자열(예: "openai:gpt-4o-mini") 또는 BaseChatModel 인스턴스일 수 있습니다.
*additional_models string | BaseChatModel 이전 모델이 실패한 경우 순서대로 시도할 추가 폴백 모델

PII detection (PII 탐지)

대화에서 개인 식별 정보를 탐지하고 처리합니다.

다음과 같은 경우에 사용:

  • 규정 준수 요구 사항이 있는 의료 및 금융 애플리케이션
  • 로그를 정리해야 하는 고객 서비스 에이전트
  • 민감한 사용자 데이터를 처리하는 모든 애플리케이션

from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware


agent = create_agent(
    model="gpt-4o",
    tools=[...],
    middleware=[
        # 사용자 입력에서 이메일 삭제
        PIIMiddleware("email", strategy="redact", apply_to_input=True),
        # 신용 카드 마스킹(마지막 4자리 표시)
        PIIMiddleware("credit_card", strategy="mask", apply_to_input=True),
        # 정규식을 사용한 사용자 정의 PII 타입
        PIIMiddleware(
            "api_key",
            detector=r"sk-[a-zA-Z0-9]{32}",
            strategy="block",  # 탐지되면 오류 발생
        ),
    ],
)

설정 옵션

매개변수 타입 설명
pii_type string (필수) 탐지할 PII 타입. 내장 타입(email, credit_card, ip, mac_address, url) 또는 사용자 정의 타입 이름일 수 있습니다.
strategy string (기본값: "redact") 탐지된 PII를 처리하는 방법. 옵션: "block" - 탐지 시 예외 발생, "redact" - [REDACTED_TYPE]으로 대체, "mask" - 부분 마스킹(예: ****-****-****-1234), "hash" - 결정론적 해시로 대체
detector function | regex 사용자 정의 탐지 함수 또는 정규식 패턴. 제공하지 않으면 PII 타입에 대한 내장 탐지기를 사용합니다.
apply_to_input boolean (기본값: "True") 모델 호출 전 사용자 메시지 확인
apply_to_output boolean (기본값: "False") 모델 호출 후 AI 메시지 확인
apply_to_tool_results boolean (기본값: "False") 실행 후 도구 결과 메시지 확인

To-do list

복잡한 multi-step 작업을 위해 에이전트에 task planningtracking 기능을 갖추세요.

다음에 특히 적합합니다:

  • 여러 tools 간 조율이 필요한 복잡한 multi-step 작업
  • 진행 상황의 가시성이 중요한 long-running operations

사람이 할 일(to-do)을 적고 추적할수록 더 효율적인 것처럼, agents도 복잡한 문제를 쪼개고, 새 정보에 따라 계획을 수정하며, 워크플로우의 투명성을 높이기 위해 structured task management의 이점을 얻습니다.
예를 들어 Claude Code가 복잡하고 다단계의 작업을 진행하기 전에 to-do list를 먼저 작성하는 패턴을 본 적이 있을 겁니다.

middleware는 에이전트에게 write_todos tool과 효과적인 task planning을 유도하는 system prompts를 자동으로 제공합니다.

from langchain.agents import create_agent
from langchain.agents.middleware import TodoListMiddleware
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool


@tool
def read_file(file_path: str) -> str:
    """Read contents of a file."""
    with open(file_path) as f:
        return f.read()


@tool
def write_file(file_path: str, content: str) -> str:
    """Write content to a file."""
    with open(file_path, 'w') as f:
        f.write(content)
    return f"Wrote {len(content)} characters to {file_path}"


@tool
def run_tests(test_path: str) -> str:
    """Run tests and return results."""
    # Simplified for example
    return "All tests passed!"


agent = create_agent(
    model="gpt-4o",
    tools=[read_file, write_file, run_tests],
    middleware=[TodoListMiddleware()],
)

result = agent.invoke({
    "messages": [HumanMessage("Refactor the authentication module to use async/await and ensure all tests pass")]
})

# The agent will use write_todos to plan and track:
# 1. Read current authentication module code
# 2. Identify functions that need async conversion
# 3. Refactor functions to async/await
# 4. Update function calls throughout codebase
# 5. Run tests and fix any failures

print(result["todos"])  # Track the agent's progress through each step

LLM tool selector (LLM 도구 선택기)

메인 모델을 호출하기 전에 LLM을 사용하여 관련 도구를 지능적으로 선택합니다.

다음과 같은 경우에 유용:

  • 대부분이 쿼리당 관련이 없는 많은 도구(10개 이상)가 있는 에이전트
  • 관련 없는 도구를 필터링하여 토큰 사용량 감소
  • 모델 집중도 및 정확도 향상

from langchain.agents import create_agent
from langchain.agents.middleware import LLMToolSelectorMiddleware


agent = create_agent(
    model="gpt-4o",
    tools=[tool1, tool2, tool3, tool4, tool5, ...],  # 많은 도구
    middleware=[
        LLMToolSelectorMiddleware(
            model="gpt-4o-mini",  # 선택을 위해 더 저렴한 모델 사용
            max_tools=3,  # 가장 관련성 높은 3개 도구로 제한
            always_include=["search"],  # 항상 특정 도구 포함
        ),
    ],
)

설정 옵션

매개변수 타입 설명
model string | BaseChatModel 도구 선택을 위한 모델. 모델 문자열 또는 BaseChatModel 인스턴스일 수 있습니다. 기본값은 에이전트의 메인 모델입니다.
system_prompt string 선택 모델에 대한 지침. 지정하지 않으면 내장 프롬프트를 사용합니다.
max_tools number 선택할 최대 도구 수. 기본값은 제한 없음입니다.
always_include list[string] 선택에 항상 포함할 도구 이름 목록

Tool retry (도구 재시도)

설정 가능한 지수 백오프로 실패한 도구 호출을 자동으로 재시도합니다.

다음과 같은 경우에 유용:

  • 외부 API 호출의 일시적 실패 처리
  • 네트워크 종속 도구의 안정성 향상
  • 일시적 오류를 우아하게 처리하는 복원력 있는 에이전트 구축

from langchain.agents import create_agent
from langchain.agents.middleware import ToolRetryMiddleware


agent = create_agent(
    model="gpt-4o",
    tools=[search_tool, database_tool],
    middleware=[
        ToolRetryMiddleware(
            max_retries=3,  # 최대 3번 재시도
            backoff_factor=2.0,  # 지수 백오프 승수
            initial_delay=1.0,  # 1초 지연으로 시작
            max_delay=60.0,  # 지연을 60초로 제한
            jitter=True,  # thundering herd를 피하기 위해 무작위 지터 추가
        ),
    ],
)

설정 옵션

매개변수 타입 설명
max_retries number (기본값: "2") 초기 호출 후 최대 재시도 시도 횟수(기본값으로 총 3회 시도)
tools list[BaseTool | str] 재시도 로직을 적용할 도구 또는 도구 이름의 선택적 목록. None이면 모든 도구에 적용됩니다.
retry_on tuple[type[Exception], ...] | callable (기본값: "(Exception,)") 재시도할 예외 타입의 튜플 또는 예외를 받아 재시도해야 하면 True를 반환하는 호출 가능 객체입니다.
on_failure string | callable (기본값: "return_message") 모든 재시도가 소진되었을 때의 동작. 옵션: "return_message" - 오류 세부 정보가 있는 ToolMessage 반환(LLM이 실패를 처리할 수 있음), "raise" - 예외 다시 발생(에이전트 실행 중지), Custom callable - 예외를 받아 ToolMessage 콘텐츠의 문자열을 반환하는 함수
backoff_factor number (기본값: "2.0") 지수 백오프의 승수. 각 재시도는 initial_delay * (backoff_factor ** retry_number) 초 동안 대기합니다. 일정한 지연을 위해 0.0으로 설정합니다.
initial_delay number (기본값: "1.0") 첫 번째 재시도 전 초기 지연(초)
max_delay number (기본값: "60.0") 재시도 간 최대 지연 시간(초)(지수 백오프 증가 제한)
jitter boolean (기본값: "true") thundering herd를 피하기 위해 지연에 무작위 지터(±25%)를 추가할지 여부

LLM tool emulator (LLM 도구 에뮬레이터)

테스트 목적으로 LLM을 사용하여 도구 실행을 에뮬레이트하고, 실제 도구 호출을 AI가 생성한 응답으로 대체합니다.

다음과 같은 경우에 유용:

  • 실제 도구를 실행하지 않고 에이전트 동작 테스트
  • 외부 도구를 사용할 수 없거나 비용이 많이 들 때 에이전트 개발
  • 실제 도구를 구현하기 전에 에이전트 워크플로 프로토타이핑

from langchain.agents import create_agent
from langchain.agents.middleware import LLMToolEmulator


agent = create_agent(
    model="gpt-4o",
    tools=[get_weather, search_database, send_email],
    middleware=[
        # 기본적으로 모든 도구 에뮬레이트
        LLMToolEmulator(),

        # 또는 특정 도구 에뮬레이트
        # LLMToolEmulator(tools=["get_weather", "search_database"]),

        # 또는 에뮬레이션을 위한 사용자 정의 모델 사용
        # LLMToolEmulator(model="claude-sonnet-4-5-20250929"),
    ],
)

설정 옵션

매개변수 타입 설명
tools list[str | BaseTool] 에뮬레이트할 도구 이름(str) 또는 BaseTool 인스턴스의 목록. None(기본값)이면 모든 도구가 에뮬레이트됩니다. 빈 목록이면 에뮬레이트되는 도구가 없습니다.
model string | BaseChatModel (기본값: "anthropic:claude-3-5-sonnet-latest") 에뮬레이트된 도구 응답을 생성하는 데 사용할 모델. 모델 식별자 문자열 또는 BaseChatModel 인스턴스일 수 있습니다.

Context editing (컨텍스트 편집)

도구 사용을 자르거나, 요약하거나, 지워서 대화 컨텍스트를 관리합니다.

다음과 같은 경우에 유용:

  • 주기적인 컨텍스트 정리가 필요한 긴 대화
  • 컨텍스트에서 실패한 도구 시도 제거
  • 사용자 정의 컨텍스트 관리 전략

from langchain.agents import create_agent
from langchain.agents.middleware import ContextEditingMiddleware, ClearToolUsesEdit


agent = create_agent(
    model="gpt-4o",
    tools=[...],
    middleware=[
        ContextEditingMiddleware(
            edits=[
                ClearToolUsesEdit(trigger=1000),  # 오래된 도구 사용 지우기
            ],
        ),
    ],
)

설정 옵션

매개변수 타입 설명
edits list[ContextEdit] (기본값: "[ClearToolUsesEdit()]") 적용할 ContextEdit 전략 목록
token_count_method string (기본값: "approximate") 토큰 계산 방법. 옵션: "approximate" 또는 "model"

ClearToolUsesEdit 옵션:

매개변수 타입 설명
trigger number (기본값: "100000") 편집을 트리거하는 토큰 수
clear_at_least number (기본값: "0") 회수할 최소 토큰
keep number (기본값: "3") 보존할 최근 도구 결과 수
clear_tool_inputs boolean (기본값: "False") 도구 호출 매개변수를 지울지 여부
exclude_tools list[string] (기본값: "()") 지우기에서 제외할 도구 이름 목록
placeholder string (기본값: "[cleared]") 지워진 출력의 자리표시자 텍스트

커스텀 Middleware

에이전트 실행 흐름의 특정 지점에서 실행되는 훅을 구현하여 커스텀 middleware를 구축할 수 있습니다.

Middleware는 두 가지 방법으로 생성할 수 있습니다:

  • Decorator 기반 - 단일 훅 middleware를 위한 빠르고 간단한 방법
  • Class 기반 - 여러 훅이 있는 복잡한 middleware를 위한 더 강력한 방법

Decorator 기반 Middleware

단일 훅만 필요한 간단한 middleware의 경우, 데코레이터가 기능을 추가하는 가장 빠른 방법을 제공합니다:

from langchain.agents.middleware import before_model, after_model, wrap_model_call
from langchain.agents.middleware import AgentState, ModelRequest, ModelResponse, dynamic_prompt
from langchain.messages import AIMessage
from langchain.agents import create_agent
from langgraph.runtime import Runtime
from typing import Any, Callable


# Node-style: 모델 호출 전 로깅
@before_model
def log_before_model(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    print(f"About to call model with {len(state['messages'])} messages")
    return None

# Node-style: 모델 호출 후 검증
@after_model(can_jump_to=["end"])
def validate_output(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    last_message = state["messages"][-1]
    if "BLOCKED" in last_message.content:
        return {
            "messages": [AIMessage("I cannot respond to that request.")],
            "jump_to": "end"
        }
    return None

# Wrap-style: 재시도 로직
@wrap_model_call
def retry_model(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    for attempt in range(3):
        try:
            return handler(request)
        except Exception as e:
            if attempt == 2:
                raise
            print(f"Retry {attempt + 1}/3 after error: {e}")

# Wrap-style: 동적 프롬프트
@dynamic_prompt
def personalized_prompt(request: ModelRequest) -> str:
    user_id = request.runtime.context.get("user_id", "guest")
    return f"You are a helpful assistant for user {user_id}. Be concise and friendly."

# 에이전트에서 데코레이터 사용
agent = create_agent(
    model="gpt-4o",
    middleware=[log_before_model, validate_output, retry_model, personalized_prompt],
    tools=[...],
)

사용 가능한 데코레이터

Node-style (특정 실행 지점에서 실행):

  • @before_agent - 에이전트 시작 전(호출당 한 번)
  • @before_model - 각 모델 호출 전
  • @after_model - 각 모델 응답 후
  • @after_agent - 에이전트 완료 후(호출당 최대 한 번)

Wrap-style (실행을 가로채고 제어):

  • @wrap_model_call - 각 모델 호출 주변
  • @wrap_tool_call - 각 도구 호출 주변

편의 데코레이터:

  • @dynamic_prompt - 동적 시스템 프롬프트 생성(프롬프트를 수정하는 @wrap_model_call과 동등)

데코레이터를 사용하는 경우

데코레이터를 사용하는 경우:

  • 단일 훅이 필요한 경우
  • 복잡한 설정이 없는 경우

클래스를 사용하는 경우:

  • 여러 훅이 필요한 경우
  • 복잡한 설정이 있는 경우
  • 프로젝트 전체에서 재사용(초기화 시 설정)

Class 기반 Middleware

두 가지 훅 스타일

Node-style 훅:
특정 실행 지점에서 순차적으로 실행됩니다. 로깅, 검증 및 상태 업데이트에 사용합니다.

Wrap-style 훅:
핸들러 호출에 대한 완전한 제어로 실행을 가로챕니다. 재시도, 캐싱 및 변환에 사용합니다.

Node-style 훅

실행 흐름의 특정 지점에서 실행됩니다:

  • before_agent - 에이전트 시작 전(호출당 한 번)
  • before_model - 각 모델 호출 전
  • after_model - 각 모델 응답 후
  • after_agent - 에이전트 완료 후(호출당 최대 한 번)

예제: 로깅 middleware

from langchain.agents.middleware import AgentMiddleware, AgentState
from langgraph.runtime import Runtime
from typing import Any

class LoggingMiddleware(AgentMiddleware):
    def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        print(f"About to call model with {len(state['messages'])} messages")
        return None

    def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        print(f"Model returned: {state['messages'][-1].content}")
        return None

예제: 대화 길이 제한

from langchain.agents.middleware import AgentMiddleware, AgentState
from langchain.messages import AIMessage
from langgraph.runtime import Runtime
from typing import Any

class MessageLimitMiddleware(AgentMiddleware):
    def __init__(self, max_messages: int = 50):
        super().__init__()
        self.max_messages = max_messages

    def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        if len(state["messages"]) == self.max_messages:
            return {
                "messages": [AIMessage("Conversation limit reached.")],
                "jump_to": "end"
            }
        return None

Wrap-style 훅

실행을 가로채고 핸들러가 호출되는 시기를 제어합니다:

  • wrap_model_call - 각 모델 호출 주변
  • wrap_tool_call - 각 도구 호출 주변

핸들러가 0번(단락), 1번(정상 흐름) 또는 여러 번(재시도 로직) 호출되는지 결정합니다.

예제: 모델 재시도 middleware

from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
from typing import Callable

class RetryMiddleware(AgentMiddleware):
    def __init__(self, max_retries: int = 3):
        super().__init__()
        self.max_retries = max_retries

    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        for attempt in range(self.max_retries):
            try:
                return handler(request)
            except Exception as e:
                if attempt == self.max_retries - 1:
                    raise
                print(f"Retry {attempt + 1}/{self.max_retries} after error: {e}")

예제: 동적 모델 선택

from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
from langchain.chat_models import init_chat_model
from typing import Callable

class DynamicModelMiddleware(AgentMiddleware):
    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        # 대화 길이에 따라 다른 모델 사용
        if len(request.messages) > 10:
            request.model = init_chat_model("gpt-4o")
        else:
            request.model = init_chat_model("gpt-4o-mini")

        return handler(request)

예제: 도구 호출 모니터링

from langchain.tools.tool_node import ToolCallRequest
from langchain.agents.middleware import AgentMiddleware
from langchain_core.messages import ToolMessage
from langgraph.types import Command
from typing import Callable

class ToolMonitoringMiddleware(AgentMiddleware):
    def wrap_tool_call(
        self,
        request: ToolCallRequest,
        handler: Callable[[ToolCallRequest], ToolMessage | Command],
    ) -> ToolMessage | Command:
        print(f"Executing tool: {request.tool_call['name']}")
        print(f"Arguments: {request.tool_call['args']}")

        try:
            result = handler(request)
            print(f"Tool completed successfully")
            return result
        except Exception as e:
            print(f"Tool failed: {e}")
            raise

커스텀 상태 스키마

Middleware는 커스텀 속성으로 에이전트의 상태를 확장할 수 있습니다. 커스텀 상태 타입을 정의하고 state_schema로 설정합니다:

from langchain.agents.middleware import AgentState, AgentMiddleware
from typing_extensions import NotRequired
from typing import Any

class CustomState(AgentState):
    model_call_count: NotRequired[int]
    user_id: NotRequired[str]

class CallCounterMiddleware(AgentMiddleware[CustomState]):
    state_schema = CustomState

    def before_model(self, state: CustomState, runtime) -> dict[str, Any] | None:
        # 커스텀 상태 속성 접근
        count = state.get("model_call_count", 0)

        if count > 10:
            return {"jump_to": "end"}

        return None

    def after_model(self, state: CustomState, runtime) -> dict[str, Any] | None:
        # 커스텀 상태 업데이트
        return {"model_call_count": state.get("model_call_count", 0) + 1}
agent = create_agent(
    model="gpt-4o",
    middleware=[CallCounterMiddleware()],
    tools=[...],
)

# 커스텀 상태로 호출
result = agent.invoke({
    "messages": [HumanMessage("Hello")],
    "model_call_count": 0,
    "user_id": "user-123",
})

실행 순서

여러 middleware를 사용할 때 실행 순서를 이해하는 것이 중요합니다:

agent = create_agent(
    model="gpt-4o",
    middleware=[middleware1, middleware2, middleware3],
    tools=[...],
)

실행 흐름:

  1. Before 훅은 순서대로 실행됩니다:

    • middleware1.before_agent()
    • middleware2.before_agent()
    • middleware3.before_agent()
    • 에이전트 루프 시작
    • middleware1.before_model()
    • middleware2.before_model()
    • middleware3.before_model()
  2. Wrap 훅은 함수 호출처럼 중첩됩니다:

    • middleware1.wrap_model_call()middleware2.wrap_model_call()middleware3.wrap_model_call() → 모델
  3. After 훅은 역순으로 실행됩니다:

    • middleware3.after_model()
    • middleware2.after_model()
    • middleware1.after_model()
    • 에이전트 루프 종료
    • middleware3.after_agent()
    • middleware2.after_agent()
    • middleware1.after_agent()

핵심 규칙:

  • before_* 훅: 처음부터 마지막까지
  • after_* 훅: 마지막부터 처음까지(역순)
  • wrap_* 훅: 중첩됨(첫 번째 middleware가 다른 모든 것을 감쌈)

에이전트 점프

Middleware에서 조기에 종료하려면 jump_to가 있는 딕셔너리를 반환합니다:

class EarlyExitMiddleware(AgentMiddleware):
    def before_model(self, state: AgentState, runtime) -> dict[str, Any] | None:
        # 일부 조건 확인
        if should_exit(state):
            return {
                "messages": [AIMessage("Exiting early due to condition.")],
                "jump_to": "end"
            }
        return None

사용 가능한 점프 대상:

  • "end": 에이전트 실행의 끝으로 점프
  • "tools": 도구 노드로 점프
  • "model": 모델 노드로 점프(또는 첫 번째 before_model 훅)

중요: before_model 또는 after_model에서 점프할 때, "model"로 점프하면 모든 before_model middleware가 다시 실행됩니다.

점프를 활성화하려면 @hook_config(can_jump_to=[...])로 훅을 데코레이트합니다:

from langchain.agents.middleware import AgentMiddleware, hook_config
from typing import Any

class ConditionalMiddleware(AgentMiddleware):
    @hook_config(can_jump_to=["end", "tools"])
    def after_model(self, state: AgentState, runtime) -> dict[str, Any] | None:
        if some_condition(state):
            return {"jump_to": "end"}
        return None

모범 사례

  • Middleware를 집중적으로 유지 - 각각은 한 가지 일을 잘해야 합니다
  • 오류를 우아하게 처리 - middleware 오류로 인해 에이전트가 충돌하지 않도록 합니다
  • 적절한 훅 타입 사용:
    • 순차적 로직을 위한 Node-style(로깅, 검증)
    • 제어 흐름을 위한 Wrap-style(재시도, 폴백, 캐싱)
  • 커스텀 상태 속성을 명확하게 문서화합니다
  • 통합하기 전에 middleware를 독립적으로 단위 테스트합니다
  • 실행 순서를 고려 - 중요한 middleware를 목록의 첫 번째에 배치합니다
  • 가능하면 내장 middleware를 사용하고, 바퀴를 재발명하지 마세요

예제

동적으로 도구 선택

런타임에 관련 도구를 선택하여 성능과 정확도를 향상시킵니다.

이점:

  • 더 짧은 프롬프트 - 관련 도구만 노출하여 복잡성 감소
  • 더 나은 정확도 - 모델이 더 적은 옵션에서 올바르게 선택
  • 권한 제어 - 사용자 액세스에 따라 동적으로 도구 필터링

from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ModelRequest
from typing import Callable


class ToolSelectorMiddleware(AgentMiddleware):
    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        """상태/컨텍스트에 따라 관련 도구를 선택하는 middleware."""
        # 상태/컨텍스트에 따라 작고 관련성 높은 도구 하위 집합 선택
        relevant_tools = select_relevant_tools(request.state, request.runtime)
        request.tools = relevant_tools
        return handler(request)

agent = create_agent(
    model="gpt-4o",
    tools=all_tools,  # 모든 사용 가능한 도구를 사전에 등록해야 합니다
    # Middleware를 사용하여 주어진 실행에 관련된 더 작은 하위 집합을 선택할 수 있습니다.
    middleware=[ToolSelectorMiddleware()],
)

확장 예제: GitHub vs GitLab 도구 선택

from dataclasses import dataclass
from typing import Literal, Callable

from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
from langchain_core.tools import tool


@tool
def github_create_issue(repo: str, title: str) -> dict:
    """GitHub 저장소에 이슈를 생성합니다."""
    return {"url": f"https://github.com/{repo}/issues/1", "title": title}

@tool
def gitlab_create_issue(project: str, title: str) -> dict:
    """GitLab 프로젝트에 이슈를 생성합니다."""
    return {"url": f"https://gitlab.com/{project}/-/issues/1", "title": title}

all_tools = [github_create_issue, gitlab_create_issue]

@dataclass
class Context:
    provider: Literal["github", "gitlab"]

class ToolSelectorMiddleware(AgentMiddleware):
    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        """VCS 제공자에 따라 도구를 선택합니다."""
        provider = request.runtime.context.provider

        if provider == "gitlab":
            selected_tools = [t for t in request.tools if t.name == "gitlab_create_issue"]
        else:
            selected_tools = [t for t in request.tools if t.name == "github_create_issue"]

        request.tools = selected_tools
        return handler(request)

agent = create_agent(
    model="gpt-4o",
    tools=all_tools,
    middleware=[ToolSelectorMiddleware()],
    context_schema=Context,
)

# GitHub 컨텍스트로 호출
agent.invoke(
    {
        "messages": [{"role": "user", "content": "Open an issue titled 'Bug: where are the cats' in the repository `its-a-cats-game`"}]
    },
    context=Context(provider="github"),
)

핵심 사항:

  • 모든 도구를 사전에 등록
  • Middleware가 요청당 관련 하위 집합 선택
  • 설정 요구 사항을 위해 context_schema 사용

추가 리소스


출처: https://docs.langchain.com/oss/python/langchain/middleware


Langchain v1.0

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유