Skip to main content
某些工具操作可能较为敏感,在执行前需要人工审批。Deep agent 通过 LangGraph 的中断能力支持 human-in-the-loop 工作流。您可以使用 interrupt_on 参数配置哪些工具需要审批。

基本配置

interrupt_on 参数接受一个将工具名称映射到中断配置的字典。每个工具可以配置为:
  • True:以默认行为启用中断(允许审批、编辑、拒绝)
  • False:禁用该工具的中断
  • {"allowed_decisions": [...]}:使用特定允许决策的自定义配置
from langchain.tools import tool
from deepagents import create_deep_agent
from langgraph.checkpoint.memory import MemorySaver

@tool
def delete_file(path: str) -> str:
    """Delete a file from the filesystem."""
    return f"Deleted {path}"

@tool
def read_file(path: str) -> str:
    """Read a file from the filesystem."""
    return f"Contents of {path}"

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email."""
    return f"Sent email to {to}"

# Checkpointer is REQUIRED for human-in-the-loop
checkpointer = MemorySaver()

agent = create_deep_agent(
    model="claude-sonnet-4-6",
    tools=[delete_file, read_file, send_email],
    interrupt_on={
        "delete_file": True,  # Default: approve, edit, reject
        "read_file": False,   # No interrupts needed
        "send_email": {"allowed_decisions": ["approve", "reject"]},  # No editing
    },
    checkpointer=checkpointer  # Required!
)

决策类型

allowed_decisions 列表控制人工审查工具调用时可以采取的操作:
  • "approve":使用 agent 提出的原始参数执行工具
  • "edit":在执行前修改工具参数
  • "reject":完全跳过执行此工具调用
您可以为每个工具自定义可用的决策:
interrupt_on = {
    # 敏感操作:允许所有选项
    "delete_file": {"allowed_decisions": ["approve", "edit", "reject"]},

    # 中等风险:仅允许审批或拒绝
    "write_file": {"allowed_decisions": ["approve", "reject"]},

    # 必须审批(不允许拒绝)
    "critical_operation": {"allowed_decisions": ["approve"]},
}

处理中断

触发中断时,agent 暂停执行并返回控制权。检查结果中的中断并相应处理。
import uuid
from langgraph.types import Command

# 使用 thread_id 创建配置以持久化状态
config = {"configurable": {"thread_id": str(uuid.uuid4())}}

# 调用 agent
result = agent.invoke({
    "messages": [{"role": "user", "content": "Delete the file temp.txt"}]
}, config=config)

# 检查执行是否被中断
if result.get("__interrupt__"):
    # 提取中断信息
    interrupts = result["__interrupt__"][0].value
    action_requests = interrupts["action_requests"]
    review_configs = interrupts["review_configs"]

    # 创建从工具名称到审查配置的查找映射
    config_map = {cfg["action_name"]: cfg for cfg in review_configs}

    # 向用户显示待处理的操作
    for action in action_requests:
        review_config = config_map[action["name"]]
        print(f"Tool: {action['name']}")
        print(f"Arguments: {action['args']}")
        print(f"Allowed decisions: {review_config['allowed_decisions']}")

    # 获取用户决策(每个 action_request 一个,按顺序)
    decisions = [
        {"type": "approve"}  # 用户批准了删除操作
    ]

    # 使用决策恢复执行
    result = agent.invoke(
        Command(resume={"decisions": decisions}),
        config=config  # 必须使用相同的 config!
    )

# 处理最终结果
print(result["messages"][-1].content)

多个工具调用

当 agent 调用多个需要审批的工具时,所有中断会被批量合并到单个中断中。您必须按顺序为每个工具提供决策。
config = {"configurable": {"thread_id": str(uuid.uuid4())}}

result = agent.invoke({
    "messages": [{
        "role": "user",
        "content": "Delete temp.txt and send an email to admin@example.com"
    }]
}, config=config)

if result.get("__interrupt__"):
    interrupts = result["__interrupt__"][0].value
    action_requests = interrupts["action_requests"]

    # 两个工具需要审批
    assert len(action_requests) == 2

    # 按照 action_requests 的相同顺序提供决策
    decisions = [
        {"type": "approve"},  # 第一个工具:delete_file
        {"type": "reject"}    # 第二个工具:send_email
    ]

    result = agent.invoke(
        Command(resume={"decisions": decisions}),
        config=config
    )

编辑工具参数

allowed_decisions 中包含 "edit" 时,您可以在执行前修改工具参数:
if result.get("__interrupt__"):
    interrupts = result["__interrupt__"][0].value
    action_request = interrupts["action_requests"][0]

    # 来自 agent 的原始参数
    print(action_request["args"])  # {"to": "everyone@company.com", ...}

    # 用户决定编辑收件人
    decisions = [{
        "type": "edit",
        "edited_action": {
            "name": action_request["name"],  # 必须包含工具名称
            "args": {"to": "team@company.com", "subject": "...", "body": "..."}
        }
    }]

    result = agent.invoke(
        Command(resume={"decisions": decisions}),
        config=config
    )

子 agent 中断

使用子 agent 时,您可以在工具调用上工具调用内使用中断。

工具调用上的中断

每个子 agent 可以有自己的 interrupt_on 配置,覆盖主 agent 的设置:
agent = create_deep_agent(
    tools=[delete_file, read_file],
    interrupt_on={
        "delete_file": True,
        "read_file": False,
    },
    subagents=[{
        "name": "file-manager",
        "description": "Manages file operations",
        "system_prompt": "You are a file management assistant.",
        "tools": [delete_file, read_file],
        "interrupt_on": {
            # 覆盖:在此子 agent 中对读取操作也要求审批
            "delete_file": True,
            "read_file": True,  # 与主 agent 不同!
        }
    }],
    checkpointer=checkpointer
)
当子 agent 触发中断时,处理方式相同——检查 __interrupt__ 并使用 Command 恢复。

工具调用内的中断

子 agent 工具可以直接调用 interrupt() 来暂停执行并等待审批:
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from langchain.messages import HumanMessage
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command, interrupt

from deepagents.graph import create_deep_agent
from deepagents.middleware.subagents import CompiledSubAgent


@tool(description="Request human approval before proceeding with an action.")
def request_approval(action_description: str) -> str:
    """Request human approval using the interrupt() primitive."""
    # interrupt() pauses execution and returns the value passed to Command(resume=...)
    approval = interrupt({
        "type": "approval_request",
        "action": action_description,
        "message": f"Please approve or reject: {action_description}",
    })

    if approval.get("approved"):
        return f"Action '{action_description}' was APPROVED. Proceeding..."
    else:
        return f"Action '{action_description}' was REJECTED. Reason: {approval.get('reason', 'No reason provided')}"


def main():
    checkpointer = InMemorySaver()
    model = ChatAnthropic(
        model_name="claude-sonnet-4-6",
        max_tokens=4096,
    )

    compiled_subagent = create_agent(
        model=model,
        tools=[request_approval],
        name="approval-agent",
    )

    parent_agent = create_deep_agent(
        checkpointer=checkpointer,
        subagents=[
            CompiledSubAgent(
                name="approval-agent",
                description="An agent that can request approvals",
                runnable=compiled_subagent,
            )
        ],
    )

    thread_id = "test_interrupt_directly"
    config = {"configurable": {"thread_id": thread_id}}

    print("Invoking agent - sub-agent will use request_approval tool...")

    result = parent_agent.invoke(
        {
            "messages": [
                HumanMessage(
                    content="Use the task tool to launch the approval-agent sub-agent. "
                    "Tell it to use the request_approval tool to request approval for 'deploying to production'."
                )
            ]
        },
        config=config,
    )

    # Check for interrupt
    if result.get("__interrupt__"):
        interrupt_value = result["__interrupt__"][0].value
        print(f"\nInterrupt received!")
        print(f"  Type: {interrupt_value.get('type')}")
        print(f"  Action: {interrupt_value.get('action')}")
        print(f"  Message: {interrupt_value.get('message')}")

        print("\nResuming with Command(resume={'approved': True})...")
        result2 = parent_agent.invoke(
            Command(resume={"approved": True}),
            config=config,
        )

        if "__interrupt__" not in result2:
            print("\nExecution completed!")
            # Find the tool response
            tool_msgs = [m for m in result2.get("messages", []) if m.type == "tool"]
            if tool_msgs:
                print(f"  Tool result: {tool_msgs[-1].content}")
        else:
            print("\nAnother interrupt occurred")
    else:
        print("\n  No interrupt - the model may not have called request_approval")


if __name__ == "__main__":
    main()
运行后,将产生以下输出:
Invoking agent - sub-agent will use request_approval tool...

Interrupt received!
  Type: approval_request
  Action: deploying to production
  Message: Please approve or reject: deploying to production

Resuming with Command(resume={'approved': True})...

Execution completed!
  Tool result: Great! The approval request has been processed. The action **"deploying to production"** was **APPROVED**. You can now proceed with the production deployment.

最佳实践

始终使用 checkpointer

Human-in-the-loop 需要 checkpointer 在中断和恢复之间持久化 agent 状态:
from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
agent = create_deep_agent(
    tools=[...],
    interrupt_on={...},
    checkpointer=checkpointer  # HITL 所必需
)

使用相同的 thread ID

恢复时,必须使用相同 thread_id 的相同 config:
# 第一次调用
config = {"configurable": {"thread_id": "my-thread"}}
result = agent.invoke(input, config=config)

# 恢复(使用相同的 config)
result = agent.invoke(Command(resume={...}), config=config)

决策顺序与操作顺序一致

decisions 列表必须与 action_requests 的顺序匹配:
if result.get("__interrupt__"):
    interrupts = result["__interrupt__"][0].value
    action_requests = interrupts["action_requests"]

    # 按顺序为每个操作创建一个决策
    decisions = []
    for action in action_requests:
        decision = get_user_decision(action)  # 您的逻辑
        decisions.append(decision)

    result = agent.invoke(
        Command(resume={"decisions": decisions}),
        config=config
    )

根据风险级别定制配置

根据工具的风险级别配置不同的中断策略:
interrupt_on = {
    # 高风险:完全控制(审批、编辑、拒绝)
    "delete_file": {"allowed_decisions": ["approve", "edit", "reject"]},
    "send_email": {"allowed_decisions": ["approve", "edit", "reject"]},

    # 中等风险:不允许编辑
    "write_file": {"allowed_decisions": ["approve", "reject"]},

    # 低风险:无中断
    "read_file": False,
    "list_files": False,
}