langchain-middleware
작성자: langchain-ai
LangChain 에이전트를 위한 인간 개입 승인, 커스텀 미들웨어 및 구조화된 출력 패턴. HumanInTheLoopMiddleware는 위험한 도구 호출 전에 실행을 일시 중지하여 인간이 승인, 인수 편집 또는 피드백과 함께 거부할 수 있도록 합니다. 도구별 인터럽트 정책을 통해 위험 수준에 따라 다른 승인 규칙을 구성할 수 있으며, 상태 지속성을 위해 체크포인터와 thread_id가 필요합니다. 명령 재개 패턴은 인간의 결정 후 실행을 계속하며, 도구 인수 편집을 지원합니다...
npx skills add https://github.com/langchain-ai/langchain-skills --skill langchain-middleware
Middleware patterns for production LangChain agents:
- HumanInTheLoopMiddleware / humanInTheLoopMiddleware: Pause before dangerous tool calls for human approval
- Custom middleware: Intercept tool calls for error handling, logging, retry logic
- Command resume: Continue execution after human decisions (approve, edit, reject)
Requirements: Checkpointer + thread_id config for all HITL workflows.
Human-in-the-Loop
Set up an agent with HITL middleware that pauses before sending emails for approval.from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import MemorySaver
from langchain.tools import tool
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
return f"Email sent to {to}"
agent = create_agent(
model="gpt-4.1",
tools=[send_email],
checkpointer=MemorySaver(), # Required for HITL
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
}
)
],
)
Set up an agent with HITL that pauses before sending emails for human approval.
import { createAgent, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const sendEmail = tool(
async ({ to, subject, body }) => `Email sent to ${to}`,
{
name: "send_email",
description: "Send an email",
schema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
}
);
const agent = createAgent({
model: "anthropic:claude-sonnet-4-5",
tools: [sendEmail],
checkpointer: new MemorySaver(),
middleware: [
humanInTheLoopMiddleware({
interruptOn: { send_email: { allowedDecisions: ["approve", "edit", "reject"] } },
}),
],
});
Run the agent, detect an interrupt, then resume execution after human approval.
from langgraph.types import Command
config = {"configurable": {"thread_id": "session-1"}}
# Step 1: Agent runs until it needs to call tool
result1 = agent.invoke({
"messages": [{"role": "user", "content": "Send email to [email protected]"}]
}, config=config)
# Check for interrupt
if "__interrupt__" in result1:
print(f"Waiting for approval: {result1['__interrupt__']}")
# Step 2: Human approves
result2 = agent.invoke(
Command(resume={"decisions": [{"type": "approve"}]}),
config=config
)
Run the agent, detect an interrupt, then resume execution after human approval.
import { Command } from "@langchain/langgraph";
const config = { configurable: { thread_id: "session-1" } };
// Step 1: Agent runs until it needs to call tool
const result1 = await agent.invoke({
messages: [{ role: "user", content: "Send email to [email protected]" }]
}, config);
// Check for interrupt
if (result1.__interrupt__) {
console.log(`Waiting for approval: ${result1.__interrupt__}`);
}
// Step 2: Human approves
const result2 = await agent.invoke(
new Command({ resume: { decisions: [{ type: "approve" }] } }),
config
);
Edit the tool arguments before approving when the original values need correction.
# Human edits the arguments — edited_action must include name + args
result2 = agent.invoke(
Command(resume={
"decisions": [{
"type": "edit",
"edited_action": {
"name": "send_email",
"args": {
"to": "[email protected]", # Fixed email
"subject": "Project Meeting - Updated",
"body": "...",
},
},
}]
}),
config=config
)
Edit the tool arguments before approving when the original values need correction.
// Human edits the arguments — editedAction must include name + args
const result2 = await agent.invoke(
new Command({
resume: {
decisions: [{
type: "edit",
editedAction: {
name: "send_email",
args: {
to: "[email protected]", // Fixed email
subject: "Project Meeting - Updated",
body: "...",
},
},
}]
}
}),
config
);
Reject a tool call and provide feedback explaining why it was rejected.
# Human rejects
result2 = agent.invoke(
Command(resume={
"decisions": [{
"type": "reject",
"feedback": "Cannot delete customer data without manager approval",
}]
}),
config=config
)
Configure different HITL policies for each tool based on risk level.
agent = create_agent(
model="gpt-4.1",
tools=[send_email, read_email, delete_email],
checkpointer=MemorySaver(),
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
"delete_email": {"allowed_decisions": ["approve", "reject"]}, # No edit
"read_email": False, # No HITL for reading
}
)
],
)
### What You CAN Configure
- Which tools require approval (per-tool policies)
- Allowed decisions per tool (approve, edit, reject)
- Custom middleware hooks:
before_model,after_model,wrap_tool_call,before_agent,after_agent - Tool-specific middleware (apply only to certain tools)
Custom Middleware Hooks
Six decorator hooks are available. Two patterns:
- Wrap hooks (
wrap_tool_call,wrap_model_call):(request, handler)— callhandler(request)to proceed, or return early to short-circuit. - Before/after hooks (
before_model,after_model,before_agent,after_agent):(state, runtime)— inspect or modify state. ReturnNoneor a dict of state updates.
from langchain.agents.middleware import wrap_tool_call
@wrap_tool_call
def retry_middleware(request, handler):
for attempt in range(3):
try:
return handler(request)
except Exception:
if attempt == 2:
raise
@wrap_tool_call
def guard_middleware(request, handler):
if request.tool_call["name"] == "dangerous_tool":
return "This tool is disabled" # short-circuit
return handler(request)
`createMiddleware({ wrapToolCall })` intercepts tool execution.
import { createMiddleware } from "langchain";
const retryMiddleware = createMiddleware({
wrapToolCall: async (request, handler) => {
for (let attempt = 0; attempt < 3; attempt++) {
try { return await handler(request); }
catch (e) { if (attempt === 2) throw e; }
}
},
});
`before_model` / `after_model` / `before_agent` / `after_agent` all share `(state, runtime)` signature.
from langchain.agents.middleware import before_model, after_model
@before_model
def log_calls(state, runtime):
print(f"Calling model with {len(state['messages'])} messages")
@after_model
def check_output(state, runtime):
print(f"Model responded")
All before/after hooks share the same `(state, runtime)` signature via `createMiddleware`.
import { createMiddleware } from "langchain";
const loggingMiddleware = createMiddleware({
beforeModel: (state, runtime) => {
console.log(`Calling model with ${state.messages.length} messages`);
},
afterModel: (state, runtime) => {
console.log("Model responded");
},
});
### What You CANNOT Configure
- Interrupt after tool execution (must be before)
- Skip checkpointer requirement for HITL
# WRONG
agent = create_agent(model="gpt-4.1", tools=[send_email], middleware=[HumanInTheLoopMiddleware({...})])
# CORRECT
agent = create_agent(
model="gpt-4.1", tools=[send_email],
checkpointer=MemorySaver(), # Required
middleware=[HumanInTheLoopMiddleware({...})]
)
HITL requires a checkpointer to persist state.
// WRONG: No checkpointer
const agent = createAgent({
model: "anthropic:claude-sonnet-4-5", tools: [sendEmail],
middleware: [humanInTheLoopMiddleware({ interruptOn: { send_email: true } })],
});
// CORRECT: Add checkpointer
const agent = createAgent({
model: "anthropic:claude-sonnet-4-5", tools: [sendEmail],
checkpointer: new MemorySaver(),
middleware: [humanInTheLoopMiddleware({ interruptOn: { send_email: true } })],
});
Always provide thread_id when using HITL to track conversation state.
# WRONG
agent.invoke(input) # No config!
# CORRECT
agent.invoke(input, config={"configurable": {"thread_id": "user-123"}})
Use Command class to resume execution after an interrupt.
# WRONG
agent.invoke({"resume": {"decisions": [...]}})
# CORRECT
from langgraph.types import Command
agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=config)
Use Command class to resume execution after an interrupt.
// WRONG
await agent.invoke({ resume: { decisions: [...] } });
// CORRECT
import { Command } from "@langchain/langgraph";
await agent.invoke(new Command({ resume: { decisions: [{ type: "approve" }] } }), config);
langchain-ai의 다른 스킬
arxiv-search
langchain-ai
주제별로 arXiv에서 프리프린트와 학술 논문을 검색하고 초록을 가져옵니다. 물리학, 수학, 컴퓨터 과학, 생물학, 통계학 및 관련 분야를 쿼리 기반으로 검색합니다. 결과 개수를 설정할 수 있으며(기본값 10개), 관련성 순으로 정렬됩니다. 각 일치하는 논문의 제목과 초록을 반환합니다. arxiv Python 패키지가 필요하며, 설치되어 있지 않은 경우 pip를 통해 설치해야 합니다.
official
blog-post
langchain-ai
긴 형식의 블로그 포스트 작성 기능으로, 연구 위임, 구조화된 콘텐츠 템플릿, AI 생성 커버 이미지를 포함합니다. 작성 전에 하위 에이전트에 연구를 위임하고, 결과물을 참조 및 맥락을 위해 마크다운으로 저장합니다. 후크, 맥락, 주요 콘텐츠(3~5개 섹션), 실용적 적용, 행동 촉구가 포함된 결론의 다섯 부분 포스트 구조를 적용합니다. 주제, 스타일, 구성, 색상, 조명을 다루는 상세 프롬프트를 사용하여 SEO 최적화된 커버 이미지를 생성합니다. 포스트를 다음으로 출력합니다...
official
code-review
langchain-ai
변경 사항에 대해 구조화된 코드 리뷰를 수행하여 정확성, 스타일, 테스트 및 잠재적 문제를 확인합니다.
official
coding-prefs
langchain-ai
사용자의 코딩 선호도를 /memory/coding-prefs.md에서 읽어 중요한 스타일 결정을 내리기 전에 참고하고, 사용자가 새로운 선호도를 제시하면 이를 추가합니다.
official
competitor-analysis
langchain-ai
경쟁사를 분석하라는 요청을 받았을 때:
official
cudf-analytics
langchain-ai
GPU 가속 데이터 분석을 위해 데이터셋, CSV 또는 테이블 형식 데이터에 NVIDIA cuDF를 사용합니다. groupby 집계, 통계…와 관련된 작업에서 트리거됩니다.
official
cuml-machine-learning
langchain-ai
GPU 가속 머신러닝을 위해 NVIDIA cuML을 사용하여 표 형식 데이터를 처리합니다. 분류, 회귀, 클러스터링, 차원 축소와 관련된 작업에서 실행됩니다.
official
data-visualization
langchain-ai
출판 품질의 차트와 다중 패널 분석 요약을 생성하는 데 사용됩니다. 데이터 시각화, 결과 플로팅, 생성 작업이 포함된 작업에서 활성화됩니다.
official