Skip to main content
Human-in-the-Loop(HITL) 미들웨어를 사용하면 에이전트의 도구 호출에 사람의 감독을 추가할 수 있습니다. 모델이 검토가 필요할 수 있는 작업(예: 파일 쓰기 또는 SQL 실행)을 제안하면, 미들웨어가 실행을 일시 중지하고 결정을 기다릴 수 있습니다. 미들웨어는 구성 가능한 정책에 따라 각 도구 호출을 검사합니다. 개입이 필요한 경우, 미들웨어는 실행을 중단하는 interrupt를 발행합니다. 그래프 상태는 LangGraph의 지속성 계층을 사용하여 저장되므로, 실행을 안전하게 일시 중지했다가 나중에 재개할 수 있습니다. 이후 사람의 결정이 다음 동작을 결정합니다. 작업을 그대로 승인(approve)하거나, 실행 전에 수정(edit)하거나, 피드백과 함께 거부(reject)할 수 있습니다.

Interrupt 결정 유형

미들웨어는 사람이 interrupt에 응답할 수 있는 세 가지 내장 방법을 정의합니다:
결정 유형설명사용 사례 예시
approve작업을 그대로 승인하고 변경 없이 실행합니다.작성된 그대로 이메일 초안을 전송
✏️ edit수정 사항을 적용하여 도구 호출을 실행합니다.이메일을 보내기 전에 수신자를 변경
reject도구 호출을 거부하고 대화에 설명을 추가합니다.이메일 초안을 거부하고 다시 작성하는 방법을 설명
각 도구에 사용 가능한 결정 유형은 interrupt_on에서 구성한 정책에 따라 달라집니다. 여러 도구 호출이 동시에 일시 중지되면 각 작업에 대해 별도의 결정이 필요합니다. 결정은 interrupt 요청에 나타나는 작업 순서와 동일한 순서로 제공되어야 합니다.
도구 인수를 수정할 때는 보수적으로 변경하세요. 원래 인수를 크게 수정하면 모델이 접근 방식을 재평가하여 도구를 여러 번 실행하거나 예상치 못한 작업을 수행할 수 있습니다.

Interrupt 구성

HITL을 사용하려면 에이전트를 생성할 때 에이전트의 middleware 목록에 미들웨어를 추가하세요. 각 작업에 허용되는 결정 유형에 대한 도구 작업의 매핑으로 구성합니다. 미들웨어는 도구 호출이 매핑의 작업과 일치할 때 실행을 중단합니다.
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware 
from langgraph.checkpoint.memory import InMemorySaver 


agent = create_agent(
    model="openai:gpt-4o",
    tools=[write_file_tool, execute_sql_tool, read_data_tool],
    middleware=[
        HumanInTheLoopMiddleware( 
            interrupt_on={
                "write_file": True,  # All decisions (approve, edit, reject) allowed
                "execute_sql": {"allowed_decisions": ["approve", "reject"]},  # No editing allowed
                # Safe operation, no approval needed
                "read_data": False,
            },
            # Prefix for interrupt messages - combined with tool name and args to form the full message
            # e.g., "Tool execution pending approval: execute_sql with query='DELETE FROM...'"
            # Individual tools can override this by specifying a "description" in their interrupt config
            description_prefix="Tool execution pending approval",
        ),
    ],
    # Human-in-the-loop requires checkpointing to handle interrupts.
    # In production, use a persistent checkpointer like AsyncPostgresSaver.
    checkpointer=InMemorySaver(),  
)
You must configure a checkpointer to persist the graph state across interrupts. In production, use a persistent checkpointer like AsyncPostgresSaver. For testing or prototyping, use InMemorySaver.When invoking the agent, pass a config that includes the thread ID to associate execution with a conversation thread. See the LangGraph interrupts documentation for details.

Responding to interrupts

When you invoke the agent, it runs until it either completes or an interrupt is raised. An interrupt is triggered when a tool call matches the policy you configured in interrupt_on. In that case, the invocation result will include an __interrupt__ field with the actions that require review. You can then present those actions to a reviewer and resume execution once decisions are provided.
from langgraph.types import Command

# Human-in-the-loop leverages LangGraph's persistence layer.
# You must provide a thread ID to associate the execution with a conversation thread,
# so the conversation can be paused and resumed (as is needed for human review).
config = {"configurable": {"thread_id": "some_id"}} 
# Run the graph until the interrupt is hit.
result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "Delete old records from the database",
            }
        ]
    },
    config=config 
)

# The interrupt contains the full HITL request with action_requests and review_configs
print(result['__interrupt__'])
# > [
# >    Interrupt(
# >       value={
# >          'action_requests': [
# >             {
# >                'name': 'execute_sql',
# >                'arguments': {'query': 'DELETE FROM records WHERE created_at < NOW() - INTERVAL \'30 days\';'},
# >                'description': 'Tool execution pending approval\n\nTool: execute_sql\nArgs: {...}'
# >             }
# >          ],
# >          'review_configs': [
# >             {
# >                'action_name': 'execute_sql',
# >                'allowed_decisions': ['approve', 'reject']
# >             }
# >          ]
# >       }
# >    )
# > ]


# Resume with approval decision
agent.invoke(
    Command( 
        resume={"decisions": [{"type": "approve"}]}  # or "edit", "reject"
    ), 
    config=config # Same thread ID to resume the paused conversation
)

Decision types

  • ✅ approve
  • ✏️ edit
  • ❌ reject
Use approve to approve the tool call as-is and execute it without changes.
agent.invoke(
    Command(
        # Decisions are provided as a list, one per action under review.
        # The order of decisions must match the order of actions
        # listed in the `__interrupt__` request.
        resume={
            "decisions": [
                {
                    "type": "approve",
                }
            ]
        }
    ),
    config=config  # Same thread ID to resume the paused conversation
)

Execution lifecycle

The middleware defines an after_model hook that runs after the model generates a response but before any tool calls are executed:
  1. The agent invokes the model to generate a response.
  2. The middleware inspects the response for tool calls.
  3. If any calls require human input, the middleware builds a HITLRequest with action_requests and review_configs and calls interrupt.
  4. The agent waits for human decisions.
  5. Based on the HITLResponse decisions, the middleware executes approved or edited calls, synthesizes ToolMessage’s for rejected calls, and resumes execution.

Custom HITL logic

For more specialized workflows, you can build custom HITL logic directly using the interrupt primitive and middleware abstraction. Review the execution lifecycle above to understand how to integrate interrupts into the agent’s operation.
Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.
I