外观
人在回路(HITL)中间件 让你能够为智能体的工具调用添加人工监督。 当模型提出的某个操作可能需要审核时——例如写入文件或执行 SQL——该中间件可以暂停执行并等待决策。
它通过将每次工具调用与可配置的策略进行比对来实现这一点。如果需要人工介入,中间件会发出一个 interrupt(中断)来暂停执行。图状态使用 LangGraph 的持久化层保存,因此执行可以安全地暂停并在之后恢复。
随后由人工决策决定接下来发生什么:操作可以原样批准(approve)、在运行前修改(edit)、附上反馈拒绝(reject),或者直接回复(respond)(用于"询问用户"类的工具)。
中断决策类型
中间件 定义了四种内置的、人类响应中断的方式:
| Decision Type | Description | Example Use Case |
|---|---|---|
✅ approve | Execute the tool with the original arguments as proposed by the agent. | Send an email draft exactly as written |
✏️ edit | Modify the tool arguments before execution. | Change the recipient before sending an email |
❌ reject | Skip executing this tool call entirely and return rejection feedback to the agent. | Deny file deletion and explain why |
💬 respond | Return 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)才能在中断期间持久化图状态。 在生产环境中,使用持久化检查点,例如 AsyncPostgresSaver 或 MongoDBSaver。对于测试或原型开发,使用 InMemorySaver。 在生产环境中,使用持久化检查点,例如 AsyncPostgresSaver 或 MongoDBSaver。对于测试或原型开发,使用 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 钩子,它在模型生成响应之后、任何工具调用执行之前运行:
- 智能体调用模型生成响应。
- 中间件检查响应中的工具调用。
- 如果有任何调用需要人工输入,中间件会构建一个包含
action_requests和review_configs的HITLRequest,并调用 interrupt。 - 智能体等待人工决策。
- 根据
HITLResponse决策,中间件执行已批准或已编辑的调用,为被拒绝的调用合成 ToolMessage,将人类回复直接作为 ToolMessage 返回给respond决策,然后恢复执行。
自定义 HITL 逻辑
对于更专门化的工作流,你可以直接使用 interrupt 原语和中间件抽象来构建自定义的 HITL 逻辑。
请回顾上面的执行生命周期,了解如何将中断集成到智能体的运行中。