Skip to content

人在回路(HITL)中间件 让你能够为智能体的工具调用添加人工监督。 当模型提出的某个操作可能需要审核时——例如写入文件或执行 SQL——该中间件可以暂停执行并等待决策。

它通过将每次工具调用与可配置的策略进行比对来实现这一点。如果需要人工介入,中间件会发出一个 interrupt(中断)来暂停执行。图状态使用 LangGraph 的持久化层保存,因此执行可以安全地暂停并在之后恢复。

随后由人工决策决定接下来发生什么:操作可以原样批准(approve)、在运行前修改(edit)、附上反馈拒绝(reject),或者直接回复(respond)(用于"询问用户"类的工具)。

中断决策类型

中间件 定义了四种内置的、人类响应中断的方式:

Decision TypeDescriptionExample Use Case
approveExecute the tool with the original arguments as proposed by the agent.Send an email draft exactly as written
✏️ editModify the tool arguments before execution.Change the recipient before sending an email
rejectSkip executing this tool call entirely and return rejection feedback to the agent.Deny file deletion and explain why
💬 respondReturn the human's message directly as a synthetic tool result, skipping execution, for "ask user" style tools.Answer an "ask_user" prompt with a direct reply

每个工具可用的决策类型取决于你在 interrupt_on 中配置的策略。 当多个工具调用同时被暂停时,每个操作都需要单独的决策。 决策必须按照操作在中断请求中出现的顺序提供。

当人类拒绝所请求的操作时,使用 reject。仅当人类充当工具本身(例如回答 ask_user 提示词)时,才使用 respond。不要使用 respond 来拒绝有副作用(side-effecting)的工具,因为其消息会被视为一次成功的工具结果。

TIP

编辑工具参数时,请保守地进行修改。对原始参数的重大修改可能会导致模型重新评估其方案,并可能多次执行工具或采取意想不到的操作。

配置中断

要使用 HITL,在创建智能体时将中间件添加到智能体的 middleware 列表中。

你需要使用"工具操作 → 每个操作允许的决策类型"的映射来配置它。当工具调用与映射中的某个操作匹配时,中间件将中断执行。

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

agent = create_agent(
    model="gpt-5.5",
    tools=[write_file, execute_sql, read_data],
    middleware=[
        HumanInTheLoopMiddleware( 
            interrupt_on={
                "write_file": True,  # 所有决策(approve、edit、reject、respond)都允许
                "execute_sql": {"allowed_decisions": ["approve", "reject"]},  # 不允许编辑
                "read_data": False, # 安全操作,无需审批
            },
            # 中断消息的前缀,与工具名称和参数组合成完整消息
            # 例如:"Tool execution pending approval: execute_sql with query='DELETE FROM...'"
            # 各个工具可以通过在其中断配置中指定 "description" 来覆盖此前缀
            description_prefix="Tool execution pending approval",
        ),
    ],
    # 人在回路(HITL)需要检查点(checkpointing)来处理中断。
    # 在生产环境中,使用持久化检查点,例如 AsyncPostgresSaver 或 MongoDBSaver。
    checkpointer=InMemorySaver(),  
)
ts
import { createAgent, humanInTheLoopMiddleware } from "langchain"; 
import { MemorySaver } from "@langchain/langgraph"; 

const agent = createAgent({
    model: "gpt-5.5",
    tools: [writeFileTool, executeSQLTool, readDataTool],
    middleware: [
        humanInTheLoopMiddleware({
            interruptOn: {
                write_file: true, // 所有决策(approve、edit、reject、respond)都允许
                execute_sql: {
                    allowedDecisions: ["approve", "reject"],
                    // 不允许编辑
                    description: "🚨 SQL execution requires DBA approval",
                },
                // 安全操作,无需审批
                read_data: false,
            },
            // 中断消息的前缀,与工具名称和参数组合成完整消息
            // 例如:"Tool execution pending approval: execute_sql with query='DELETE FROM...'"
            // 各个工具可以通过在其中断配置中指定 "description" 来覆盖此前缀
            descriptionPrefix: "Tool execution pending approval",
        }),
    ],
    // 人在回路(HITL)需要检查点(checkpointing)来处理中断。
    // 在生产环境中,使用持久化检查点,例如 AsyncPostgresSaver 或 MongoDBSaver。
    checkpointer: new MemorySaver(), 
});

INFO

你必须配置一个检查点(checkpointer)才能在中断期间持久化图状态。 在生产环境中,使用持久化检查点,例如 AsyncPostgresSaverMongoDBSaver。对于测试或原型开发,使用 InMemorySaver。 在生产环境中,使用持久化检查点,例如 AsyncPostgresSaverMongoDBSaver。对于测试或原型开发,使用 InMemorySaver

调用智能体时,传入包含 thread ID(线程 ID)config,以将执行与会话线程关联起来。 有关详细信息,请参阅 LangGraph 中断文档

配置选项

  • interrupt_on (dict)(必填):工具名称到批准配置的映射。值可以是 True(使用默认配置中断)、False(自动批准)或一个 InterruptOnConfig 对象。

  • description_prefix (string)(默认:Tool execution requires approval):操作请求描述的前缀

InterruptOnConfig 选项:

  • allowed_decisions (list[string]):允许的决策列表:'approve''edit''reject''respond'

  • description (string | callable):用于自定义描述的静态字符串或可调用函数

  • when (callable):可选谓词,接收一个 ToolCallRequest 并返回 True 以中断或 False 以自动批准。使用它来根据调用的参数决定是否中断。需要 langchain>=1.3.3

  • interruptOn (object)(必填):工具名称到批准配置的映射

工具批准配置选项:

  • allowAccept (boolean)(默认:false):是否允许批准

  • allowEdit (boolean)(默认:false):是否允许编辑

  • allowRespond (boolean)(默认:false):是否允许回复/拒绝

条件中断

默认情况下,interrupt_on 中列出的每个工具调用都会暂停等待审核。要仅暂停部分调用,请为工具的 InterruptOnConfig 添加一个 when 谓词。该谓词接收一个 ToolCallRequest 并返回 True 以中断或 False 以自动批准,因此你可以根据工具的参数来决定是否中断。

INFO

条件中断需要 langchain>=1.3.3

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

def writes_outside_workspace(request: ToolCallRequest) -> bool:
    """Pause writes to paths outside the workspace directory."""
    path = request.tool_call["args"].get("path", "")
    return not path.startswith("/workspace/")

def is_write_query(request: ToolCallRequest) -> bool:
    """Pause SQL that isn't a read-only SELECT."""
    query = request.tool_call["args"].get("query", "")
    return not query.lstrip().upper().startswith("SELECT")

agent = create_agent(
    model="gpt-5.5",
    tools=[write_file, execute_sql, read_data],
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "write_file": {
                    "allowed_decisions": ["approve", "edit", "reject"],
                    "when": writes_outside_workspace,
                },
                "execute_sql": {
                    "allowed_decisions": ["approve", "reject"],
                    "when": is_write_query,
                },
            },
        ),
    ],
    checkpointer=InMemorySaver(),
)

when 谓词返回 False 时,调用会在不中断的情况下运行。当它返回 True 时,或者当你省略 when 时,调用会照常暂停。评估为 False 的调用永远不会被添加到中断批次中,因此审核者只会看到需要决策的操作。

条件中断目前仅在 Python 中可用。

响应中断

调用智能体时,它会一直运行到完成或触发中断为止。当工具调用与你在 interrupt_on 中配置的策略匹配时,就会触发中断。使用 version="v2" 时,结果是一个带有 interrupts 属性的 GraphOutput,其中包含需要审核的操作。然后你可以将这些操作呈现给审核者,并在提供决策后恢复执行。

python
from langgraph.types import Command

# 人在回路利用 LangGraph 的持久化层。
# 你必须提供一个线程 ID(thread ID)来将执行与会话线程关联起来,
# 这样对话才能被暂停并恢复(这正是人工审核所需的)。
config = {"configurable": {"thread_id": "some_id"}} 
# 运行图,直到触发中断为止。
result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "Delete old records from the database",
            }
        ]
    },
    config=config, 
    version="v2", 
)

# result 是一个带有 .value 和 .interrupts 属性的 GraphOutput
print(result.interrupts)  
# > (
# >    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']
# >             }
# >          ]
# >       }
# >    ),
# > )

# 使用批准决策恢复执行
agent.invoke(
    Command( 
        resume={"decisions": [{"type": "approve"}]}  # 或 "reject"
    ), 
    config=config, # 使用相同的线程 ID 来恢复暂停的对话
    version="v2",
)
typescript
import { HumanMessage } from "@langchain/core/messages";
import { Command } from "@langchain/langgraph";

// 你必须提供一个线程 ID(thread ID)来将执行与会话线程关联起来,
// 这样对话才能被暂停并恢复(这正是人工审核所需的)。
const config = { configurable: { thread_id: "some_id" } }; 

// 运行图,直到触发中断为止。
const result = await agent.invoke(
    {
        messages: [new HumanMessage("Delete old records from the database")],
    },
    config 
);

// 中断包含完整的 HITL 请求,其中包括 action_requests 和 review_configs
console.log(result.__interrupt__);
// > [
// >    Interrupt(
// >       value: {
// >          actionRequests: [
// >             {
// >                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: {...}'
// >             }
// >          ],
// >          reviewConfigs: [
// >             {
// >                actionName: 'execute_sql',
// >                allowedDecisions: ['approve', 'reject']
// >             }
// >          ]
// >       }
// >    )
// > ]

// 使用批准决策恢复执行
await agent.invoke(
    new Command({ 
        resume: { decisions: [{ type: "approve" }] }, // 或 "reject"
    }), 
    config // 使用相同的线程 ID 来恢复暂停的对话
);

决策类型

✅ approve

使用 approve 原样批准工具调用并执行,不做任何更改。

python
agent.invoke(
    Command(
        # 决策以列表形式提供,每个被审核的操作对应一条决策。
        # 决策的顺序必须与操作在中断请求中出现的顺序一致,
        # 即按照中断请求中列出的顺序提供。
        resume={
            "decisions": [
                {
                    "type": "approve",
                }
            ]
        }
    ),
    config=config,  # 使用相同的线程 ID 来恢复暂停的对话
    version="v2",
)
typescript
await agent.invoke(
    new Command({
        // 决策以列表形式提供,每个被审核的操作对应一条决策。
        // 决策的顺序必须与操作在中断请求中出现的顺序一致,
        // 即按照中断请求中列出的顺序提供。
        resume: {
            decisions: [
                {
                    type: "approve",
                }
            ]
        }
    }),
    config  // 使用相同的线程 ID 来恢复暂停的对话
);

✏️ edit

使用 `edit` 在执行前修改工具调用。
提供包含新工具名称和参数的被编辑操作。
python
agent.invoke(
    Command(
        # 决策以列表形式提供,每个被审核的操作对应一条决策。
        # 决策的顺序必须与操作在中断请求中出现的顺序一致,
        # 即按照中断请求中列出的顺序提供。
        resume={
            "decisions": [
                {
                    "type": "edit",
                    # 已编辑的操作,包含工具名称和参数
                    "edited_action": {
                        # 要调用的工具名称。
                        # 通常与原始操作相同。
                        "name": "new_tool_name",
                        # 传递给工具的参数。
                        "args": {"key1": "new_value", "key2": "original_value"},
                    }
                }
            ]
        }
    ),
    config=config,  # 使用相同的线程 ID 来恢复暂停的对话
    version="v2",
)
typescript
await agent.invoke(
    new Command({
        // 决策以列表形式提供,每个被审核的操作对应一条决策。
        // 决策的顺序必须与操作在中断请求中出现的顺序一致,
        // 即按照中断请求中列出的顺序提供。
        resume: {
            decisions: [
                {
                    type: "edit",
                    // 已编辑的操作,包含工具名称和参数
                    editedAction: {
                        // 要调用的工具名称。
                        // 通常与原始操作相同。
                        name: "new_tool_name",
                        // 传递给工具的参数。
                        args: { key1: "new_value", key2: "original_value" },
                    }
                }
            ]
        }
    }),
    config  // 使用相同的线程 ID 来恢复暂停的对话
);

TIP

编辑工具参数时,请保守地进行修改。对原始参数的重大修改可能会导致模型重新评估其方案,并可能多次执行工具或采取意想不到的操作。

❌ reject

使用 `reject` 拒绝工具调用并提供反馈,而不是执行。该工具不会被执行。
python
agent.invoke(
    Command(
        # 决策以列表形式提供,每个被审核的操作对应一条决策。
        # 决策的顺序必须与操作在中断请求中出现的顺序一致,
        # 即按照中断请求中列出的顺序提供。
        resume={
            "decisions": [
                {
                    "type": "reject",
                    # 可选:说明操作被拒绝的原因
                    # 以及智能体是否应该尝试不同的方法。
                    "message": "User rejected this action. Do not retry this tool call.",
                }
            ]
        }
    ),
    config=config,  # 使用相同的线程 ID 来恢复暂停的对话
    version="v2",
)
typescript
await agent.invoke(
    new Command({
        // 决策以列表形式提供,每个被审核的操作对应一条决策。
        // 决策的顺序必须与操作在中断请求中出现的顺序一致,
        // 即按照中断请求中列出的顺序提供。
        resume: {
            decisions: [
                {
                    type: "reject",
                    // 可选:说明操作被拒绝的原因
                    // 以及智能体是否应该尝试不同的方法。
                    message: "User rejected this action. Do not retry this tool call.",
                }
            ]
        }
    }),
    config  // 使用相同的线程 ID 来恢复暂停的对话
);

message 会作为反馈添加到对话中,以帮助智能体理解为什么操作被拒绝以及它应该改做什么。当你省略 message 时,中间件会使用默认的拒绝消息,告诉模型该工具未被执行,并且除非用户要求,否则不要重试相同的工具调用。对于有副作用的工具,请提供领域特定的消息,明确说明智能体是应该放弃该操作、追问后续问题,还是尝试更安全的替代方案。

💬 respond

对"询问用户"类的工具使用 `respond`,这类工具的真实实现就是人类的回复。`message` 内容会直接作为工具结果返回;工具本身不会被执行。
python
agent.invoke(
    Command(
        # 决策以列表形式提供,每个被审核的操作对应一条决策。
        # 决策的顺序必须与操作在中断请求中出现的顺序一致,
        # 即按照中断请求中列出的顺序提供。
        resume={
            "decisions": [
                {
                    "type": "respond",
                    # 人类的回复,直接作为工具结果返回
                    "message": "Blue.",
                }
            ]
        }
    ),
    config=config,  # 使用相同的线程 ID 来恢复暂停的对话
    version="v2",
)
typescript
await agent.invoke(
    new Command({
        // 决策以列表形式提供,每个被审核的操作对应一条决策。
        // 决策的顺序必须与操作在中断请求中出现的顺序一致,
        // 即按照中断请求中列出的顺序提供。
        resume: {
            decisions: [
                {
                    type: "respond",
                    // 人类的回复,直接作为工具结果返回
                    message: "Blue.",
                }
            ]
        }
    }),
    config  // 使用相同的线程 ID 来恢复暂停的对话
);

message 会作为一条成功的 ToolMessage 返回给智能体。当工具有意作为人类输入的占位符时(例如,用于请求澄清的 ask_user 工具),请使用 respond。不要使用 respond 来拒绝提议的操作,因为它会告诉模型工具已成功完成。


多个决策

当多个操作需要审核时,请按照它们在中断中出现的顺序为每个操作提供决策:

python
{
    "decisions": [
        {"type": "approve"},
        {
            "type": "edit",
            "edited_action": {
                "name": "tool_name",
                "args": {"param": "new_value"}
            }
        },
        {
            "type": "reject",
            "message": "This action is not allowed"
        }
    ]
}
typescript
{
    decisions: [
        { type: "approve" },
        {
            type: "edit",
            editedAction: {
                name: "tool_name",
                args: { param: "new_value" }
            }
        },
        {
            type: "reject",
            message: "This action is not allowed"
        }
    ]
}

带人在回路的流式输出

当智能体运行并处理中断时,你可以使用 stream_events() 流式接收实时更新。使用 stream.messages 流式接收 LLM token,使用 stream.values 检查智能体状态快照以获取中断。

python
from langgraph.types import Command

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

# 流式接收智能体进度和 LLM token,直到触发中断
stream = agent.stream_events(
    {"messages": [{"role": "user", "content": "Delete old records from the database"}]},
    config=config,
    version="v3",  
)
for message in stream.messages:  
    for token in message.text:  
        print(token, end="", flush=True)

# 检查运行是否因等待人工输入而暂停
if stream.interrupted:  
    print(f"\n\nInterrupt: {stream.interrupts}")  

# 在人工决策后以流式方式恢复执行
stream = agent.stream_events(
    Command(resume={"decisions": [{"type": "approve"}]}),
    config=config,
    version="v3",  
)
for message in stream.messages:  
    for token in message.text:
        print(token, end="", flush=True)
typescript
import { Command } from "@langchain/langgraph";

const config = { configurable: { thread_id: "some_id" } };

// 流式接收智能体进度和 LLM token,直到触发中断
const stream = await agent.streamEvents(
    { messages: [{ role: "user", content: "Delete old records from the database" }] },
    { ...config, version: "v3" }  
);
for await (const message of stream.messages) {  
    for await (const token of message.text) {  
        process.stdout.write(token);
    }
}

// 检查运行是否因等待人工输入而暂停
if (stream.interrupted) {  
    console.log(`\n\nInterrupt: ${JSON.stringify(stream.interrupts)}`);  
}

// 在人工决策后以流式方式恢复执行
const resumeStream = await agent.streamEvents(
    new Command({ resume: { decisions: [{ type: "approve" }] } }),
    { ...config, version: "v3" }  
);
for await (const message of resumeStream.messages) {  
    for await (const token of message.text) {
        process.stdout.write(token);
    }
}

有关流式输出模式的更多详细信息,请参阅流式输出指南。

执行生命周期

该中间件定义了一个 after_model 钩子,它在模型生成响应之后、任何工具调用执行之前运行:

  1. 智能体调用模型生成响应。
  2. 中间件检查响应中的工具调用。
  3. 如果有任何调用需要人工输入,中间件会构建一个包含 action_requestsreview_configsHITLRequest,并调用 interrupt。
  4. 智能体等待人工决策。
  5. 根据 HITLResponse 决策,中间件执行已批准或已编辑的调用,为被拒绝的调用合成 ToolMessage,将人类回复直接作为 ToolMessage 返回给 respond 决策,然后恢复执行。

自定义 HITL 逻辑

对于更专门化的工作流,你可以直接使用 interrupt 原语和中间件抽象来构建自定义的 HITL 逻辑。

请回顾上面的执行生命周期,了解如何将中断集成到智能体的运行中。