某些工具操作可能涉及敏感操作,需要在执行前获得人工审批。Deep Agents 通过 LangGraph 的中断功能支持人在环工作流。您可以使用以下方式配置需要审批的工具 interrupt_on 参数。当 interrupt_on 被设置时, HumanInTheLoopMiddleware 被添加到 默认中间件堆栈。如果运行在工具返回结果之前被取消或中断,则 PatchToolCallsMiddleware 同一堆栈中的会自动修复消息历史。
graph LR
Agent[Agent] --> Check{Interrupt?}
Check --> |no| Execute[Execute]
Check --> |yes| Human{Human}
Human --> |approve| Execute
Human --> |edit| Execute
Human --> |reject| ToolMessage[ToolMessage]
Human --> |respond| ToolMessage
Execute --> Agent
ToolMessage --> Agent
classDef trigger fill:#F6FFDB,stroke:#6E8900,stroke-width:2px,color:#2E3900
classDef process fill:#E5F4FF,stroke:#006DDD,stroke-width:2px,color:#030710
classDef decision fill:#FDF3FF,stroke:#7E65AE,stroke-width:2px,color:#504B5F
classDef alert fill:#F8E8E6,stroke:#B27D75,stroke-width:2px,color:#634643
class Agent trigger
class Check,Human decision
class Execute process
class ToolMessage process
基本配置
该 interrupt_on 参数接受一个将工具名称映射到中断配置的字典。每个工具可以配置:
- **True**:启用中断,具有默认行为(允许批准、编辑、拒绝、响应) - **False**:禁用此工具的中断 - **InterruptOnConfig**:自定义配置。设置 allowed_decisions 来控制审查选项。 在 Python 中,添加一个可选的 when 谓词以仅中断特定调用(参见 条件中断).
决策类型
该 allowed_decisions 列表控制人类在审查工具调用时可以采取的操作:
当人类拒绝提议的操作时使用 reject 当人类拒绝提议的操作时使用 respond 仅当人类作为工具行事时使用,例如回答 ask_user 提示。不要使用 respond 来拒绝有副作用的工具,因为模型可能会将其消息视为成功的工具结果。
您可以为每个工具自定义可用的决策:
interrupt_on = {
# Sensitive operations: allow all options
"delete_file": {"allowed_decisions": ["approve", "edit", "reject"]},
# Moderate risk: approval or rejection only
"write_file": {"allowed_decisions": ["approve", "reject"]},
# Must approve (no rejection allowed)
"critical_operation": {"allowed_decisions": ["approve"]},
}
条件中断
默认情况下,列为 interrupt_on 的每个工具调用都会暂停以供审核。若仅暂停部分调用,请添加 when 到工具的 InterruptOnConfig。谓词接收 ToolCallRequest 并返回 True 以中断,或 False 以自动批准,这样您可以根据工具的参数进行筛选。
当 when 谓词返回 False时,调用会继续运行而不会中断。当它返回 True时,或者当您省略 when时,调用会照常暂停。计算结果为 False 的操作不会被添加到中断批次中,因此审核者只会看到需要做出决策的操作。
请参阅 LangChain 人工介入文档 以获取更多配置选项和示例。
处理中断
当触发中断时,代理会暂停执行并返回控制权。检查结果中的中断并相应处理。如果用户拒绝某个操作,请包含一条清晰的 message 消息,告知代理该工具未被执行以及下一步该怎么做。
from langchain_core.utils.uuid import uuid7
from langgraph.types import Command
# Create config with thread_id for state persistence
config = {"configurable": {"thread_id": str(uuid7())}}
# Invoke the agent
result = agent.invoke(
{"messages": [{"role": "user", "content": "Delete the file temp.txt"}]},
config=config,
version="v2", # [!code highlight]
)
# Check if execution was interrupted
if result.interrupts: # [!code highlight]
# Extract interrupt information
interrupt_value = result.interrupts[0].value # [!code highlight]
action_requests = interrupt_value["action_requests"]
review_configs = interrupt_value["review_configs"]
# Create a lookup map from tool name to review config
config_map = {cfg["action_name"]: cfg for cfg in review_configs}
# Display the pending actions to the user
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']}")
# Get user decisions (one per action_request, in order)
decisions = [
{
"type": "reject",
"message": "User rejected deleting temp.txt. Do not retry deletion.",
}
]
# Resume execution with decisions
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config, # Must use the same config!
version="v2",
)
# Process final result
print(result.value["messages"][-1].content) # [!code highlight]
多次工具调用
当代理调用多个需要审批的工具时,所有中断都会被合并到单个中断中。您必须按顺序为每个中断提供决策。
config = {"configurable": {"thread_id": str(uuid7())}}
result = agent.invoke(
{"messages": [{
"role": "user",
"content": "Delete temp.txt and send an email to admin@example.com"
}]},
config=config,
version="v2", # [!code highlight]
)
if result.interrupts: # [!code highlight]
interrupt_value = result.interrupts[0].value # [!code highlight]
action_requests = interrupt_value["action_requests"]
# Two tools need approval
assert len(action_requests) == 2
# Provide decisions in the same order as action_requests
decisions = [
{"type": "approve"}, # First tool: delete_file
{
"type": "reject",
"message": "User rejected this action. Do not retry this tool call.",
} # Second tool: send_email
]
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config,
version="v2",
)
拒绝消息
当审阅者返回 reject 决策时,Deep Agents会跳过工具调用并向代理发送拒绝反馈。如果省略 message,默认反馈会告诉模型该工具未被执行,并且除非用户要求,否则不要重试相同的工具调用。
对于敏感或有副作用的工具,请传递一个特定领域的 message 以及决策。请明确说明代理应该放弃该操作、提出后续问题,还是尝试更安全的替代方案。
decisions = [
{
"type": "reject",
"message": "User rejected deleting this file. Do not retry deletion. Ask which file to archive instead.",
}
]
编辑工具参数
当 "edit" 在允许的决策中时,您可以在执行前修改工具参数:
if result.interrupts: # [!code highlight]
interrupt_value = result.interrupts[0].value # [!code highlight]
action_request = interrupt_value["action_requests"][0]
# Original args from the agent
print(action_request["args"]) # {"to": "everyone@company.com", ...}
# User decides to edit the recipient
decisions = [{
"type": "edit",
"edited_action": {
"name": action_request["name"], # Must include the tool name
"args": {"to": "team@company.com", "subject": "...", "body": "..."}
}
}]
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config,
version="v2",
)
子代理中断
使用子代理时,您可以在工具调用上使用中断 和 工具调用内部 工具调用上的中断.
每个子代理可以有自己的
配置来覆盖主代理的设置: interrupt_on 当子代理触发中断时,处理方式相同——检查结果中的
agent = create_deep_agent(
model="google_genai:gemini-3.5-flash",
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": {
# Override: require approval for reads in this subagent
"delete_file": True,
"read_file": True, # Different from main agent!
}
}],
checkpointer=checkpointer
)
并使用 interrupts 恢复执行 Command.
工具调用内部的中断
子代理工具可以直接调用 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(
model="google_genai:gemini-3.5-flash",
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,
version="v2", # [!code highlight]
)
# Check for interrupt
if result.interrupts: # [!code highlight]
interrupt_value = result.interrupts[0].value # [!code highlight]
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,
version="v2", # [!code highlight]
)
if not result2.interrupts: # [!code highlight]
print("\nExecution completed!")
# Find the tool response
tool_msgs = [m for m in result2.value.get("messages", []) if m.type == "tool"] # [!code highlight]
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.
文件系统权限中断
除了 interrupt_on之外,您还可以通过将 权限规则 标记为 mode="interrupt"来暂停内置文件系统工具。当代理在匹配的路径上调用 write_file or edit_file 时,如果路径匹配中断模式规则, create_deep_agent 会引发与已配置工具相同的人机交互中断,并使用文件系统工具的名称作为操作名称。
from deepagents import FilesystemPermission, create_deep_agent
from langgraph.checkpoint.memory import MemorySaver
agent = create_deep_agent(
model=model,
permissions=[
FilesystemPermission(
operations=["write"],
paths=["/secrets/**"],
mode="interrupt",
),
],
checkpointer=MemorySaver(), # Required to pause and resume
)
以与工具调用中断相同的方式处理和恢复中断:运行直到暂停,检查请求,然后使用决策恢复执行。
from langgraph.types import Command
config = {"configurable": {"thread_id": "fs-thread-1"}}
result = agent.invoke(
{"messages": [{"role": "user", "content": "Save the API key to /secrets/key.txt"}]},
config=config,
version="v2",
)
if result.interrupts:
action = result.interrupts[0].value["action_requests"][0]
print(f"Approve {action['name']} on {action['args']}?")
# Resume with the human decision (approve, edit, or reject).
result = agent.invoke(
Command(resume={"decisions": [{"type": "approve"}]}),
config=config, # Same thread ID
version="v2",
)
文件系统权限中断会与您传递的任何 interrupt_on 合并,这样单个审查步骤可以同时覆盖自定义工具和受保护的文件系统路径。
最佳实践
始终使用检查点
人机交互需要检查点在中断和恢复之间持久化代理状态:
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
agent = create_deep_agent(
model="google_genai:gemini-3.5-flash",
tools=[...],
interrupt_on={...},
checkpointer=checkpointer # Required for HITL
)
使用相同的线程ID
恢复时,您必须使用包含相同 thread_id:
# First call
config = {"configurable": {"thread_id": "my-thread"}}
result = agent.invoke(input, config=config, version="v2")
# Resume (use same config)
result = agent.invoke(Command(resume={...}), config=config, version="v2")
使决策顺序与操作相匹配
决策列表必须与以下顺序相匹配 action_requests:
if result.interrupts: # [!code highlight]
interrupt_value = result.interrupts[0].value # [!code highlight]
action_requests = interrupt_value["action_requests"]
# Create one decision per action, in order
decisions = []
for action in action_requests:
decision = get_user_decision(action) # Your logic
decisions.append(decision)
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config,
version="v2",
)
按风险定制配置
根据不同工具的风险级别进行配置:
interrupt_on = {
# High risk: full control (approve, edit, reject)
"delete_file": {"allowed_decisions": ["approve", "edit", "reject"]},
"send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
# Medium risk: no editing allowed
"write_file": {"allowed_decisions": ["approve", "reject"]},
# Low risk: no interrupts
"read_file": False,
"ls": False,
}