人在回路 (Human-in-the-loop)
人在回路(HITL)中间件允许你为智能体(Agent)的工具调用增加人类监管。 当模型提议执行可能需要审核的操作时(例如写入文件或执行 SQL),该中间件可以暂停执行并等待决策。
它的工作原理是将每次工具调用与可配置的策略进行比对。如果需要介入,该中间件会发出一个暂停执行的中断 (interrupt)。图状态会使用 LangGraph 的持久化层进行保存,以便执行过程可以安全暂停并在稍后恢复。
随后,人类的决策将决定接下来的走向:操作可以按原样批准(approve)、在运行前进行修改(edit)、附带反馈予以驳回(reject),或者对于“向用户提问”风格的工具直接进行答复(respond)。
中断决策类型
该中间件定义了人类响应中断的四种内置方式:
| 决策类型 | 描述 | 示例用例 |
|---|---|---|
✅ approve | 使用智能体提议的原始参数执行工具。 | 原样发送起草的电子邮件 |
✏️ edit | 在执行前修改工具参数。 | 在发送电子邮件前更改收件人 |
❌ reject | 完全跳过执行此工具调用,并向智能体返回驳回反馈。 | 拒绝删除文件并说明原因 |
💬 respond | 针对“向用户提问”风格的工具,直接返回人类的消息作为合成工具结果,跳过执行。 | 直接回复答复 "ask_user" 提示 |
每个工具可用的决策类型取决于你在 interruptOn 中配置的策略。
当多个工具调用同时暂停时,每个操作都需要一个单独的决策。
在默认的批处理模式下,按照操作在中断请求中出现的相同顺序提供决策。在单次调用模式下,每个工具调用都有各自的中断,并接受一个决策。
当人类拒绝请求的操作时,请使用 reject。仅当人类充当工具时(例如回答 ask_user 提示)才使用 respond。不要使用 respond 来拒绝有副作用的工具,因为其消息会被视为成功的工具执行结果。
TIP
在编辑工具参数时,请保持保守的修改。对原始参数进行重大修改可能会导致模型重新评估其处理方法,并可能多次执行该工具或采取非预期的操作。
配置中断
要使用 HITL,请在创建智能体时将中间件添加到智能体的 middleware 列表中。
你需要通过工具操作到每个操作所允许决策类型的映射来进行配置。当工具调用匹配映射中的操作时,中间件将中断执行。
该示例使用默认的批处理模式。如需每次工具调用暂停一次,请参阅设置中断模式。
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 执行需要 DBA 审批",
},
// 安全操作,无需审批
read_data: false,
},
// 中断消息前缀 - 与工具名称和参数组合形成完整消息
// 例如:"Tool execution pending approval: execute_sql with query='DELETE FROM...'"
// 单个工具可以通过在其中断配置中指定 "description" 来覆盖此项
descriptionPrefix: "Tool execution pending approval",
}),
],
// 人在回路需要检查点机制来处理中断。
// 在生产环境中,请使用像 AsyncPostgresSaver 或 MongoDBSaver 这样的持久化检查点保存器。
checkpointer: new MemorySaver(),
});NOTE
你必须配置一个检查点保存器(checkpointer),以便跨中断持久化图状态。
在生产环境中,请使用持久化检查点保存器,例如 AsyncPostgresSaver 或 MongoDBSaver。对于测试或原型设计,请使用 InMemorySaver。
在调用智能体时,传入包含 线程 ID (thread ID) 的 config,以将执行过程与会话线程相关联。 有关详细信息,请参阅 LangGraph 中断文档。
配置选项
interruptOn
object必填工具名称到审批配置的映射。值可以是
true(使用默认配置中断)、false(自动批准)或InterruptOnConfig对象。descriptionPrefix
string(默认值:Tool execution requires approval)操作请求描述的前缀
interruptMode
string(默认值:batched)智能体暂停等待审核的方式。
"batched"每个模型轮次触发一次中断,并通过有序列表进行答复。"per_call"每个受控调用触发一次中断,并通过中断 ID 进行答复。"per_call"需要langchain>=1.5.18。请参阅设置中断模式。editNotice
string | null在单次调用模式下,添加到审核人员使用
edit决策替换的调用结果前面的文本,以便模型获知该调用已发生更改。默认是一条注明由人工审核人员替换了该调用的说明。传入null则不添加任何内容。
InterruptOnConfig选项:allowedDecisions
string[]允许的决策列表:
'approve'、'edit'、'reject'或'respond'description
string | callable用于自定义描述的静态字符串或可调用函数
when
callable可选的谓词函数,接收一个 ToolCallRequest,并返回
true以中断,或返回false以自动批准。用于根据调用的参数对中断进行门禁控制。需要langchain>=1.4.6。
条件中断
默认情况下,interruptOn 中列出的每个工具调用都会暂停以供审核。若要仅暂停部分调用,可以向工具的 InterruptOnConfig 中添加一个 when 谓词函数。该谓词接收一个 ToolCallRequest,并返回 true 表示中断,或返回 false 表示自动批准,因此你可以根据工具的参数进行门禁控制。
NOTE
条件中断需要 langchain>=1.4.6。
import { createAgent, humanInTheLoopMiddleware } from "langchain";
import type { ToolCallRequest } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
// 暂停对工作区目录以外路径的写入。
const writesOutsideWorkspace = (request: ToolCallRequest) =>
!String(request.toolCall.args.path ?? "").startsWith("/workspace/");
// 暂停非只读 SELECT 的 SQL。
const isWriteQuery = (request: ToolCallRequest) =>
!String(request.toolCall.args.query ?? "")
.trimStart()
.toUpperCase()
.startsWith("SELECT");
const agent = createAgent({
model: "gpt-5.5",
tools: [writeFile, executeSql, readData],
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
write_file: {
allowedDecisions: ["approve", "edit", "reject"],
when: writesOutsideWorkspace,
},
execute_sql: {
allowedDecisions: ["approve", "reject"],
when: isWriteQuery,
},
},
}),
],
checkpointer: new MemorySaver(),
});当 when 谓词返回 false 时,该调用将直接运行而不发生中断。当其返回 true 或省略 when 时,调用将照常暂停。评估结果为 false 的调用永远不会暂停,因此审核人员只会看到需要决策的操作。
智能体恢复执行时谓词会再次运行,因此对于同一个调用它必须返回相同的结果。如果恢复时它返回 false,工具将直接运行,而审核人员的答复将被忽略(即使是 reject 也是如此)。在批处理模式下,如果仅部分调用的结果发生变化,中间件将抛出错误。
设置中断模式
该中间件可以在两种模式之一下暂停,通过 interruptMode 进行设置:
"per_call":每个需要审核的工具调用在开始时都会触发各自的中断。每个中断都带有一个它所接受答复的response_schema。你使用以中断 ID 为键的单个决策来对其进行答复。"batched"(默认值):每个模型轮次产生一个中断,列出每个需要审核的调用。你使用相同顺序的决策列表来进行答复。
对于新智能体,请使用 per-call 模式。LangChain 计划在下一个主要版本中将其设为默认模式。如需横向对比以及 per-call 模式所解决的问题,请参阅人在回路概述。
每次工具调用暂停一次
NOTE
单次调用中断(Per-call interrupts)需要 langchain>=1.5.18 以及默认的 createAgent 版本 "v2"。
设置 interruptMode: "per_call" 可以单独审核每个工具调用。该智能体对 write_file 和 execute_sql 设置了门禁,并允许 read_data 在无需审核的情况下直接运行:
import { MemorySaver } from "@langchain/langgraph";
import { createAgent, humanInTheLoopMiddleware, tool } from "langchain";
import * as z from "zod";
const writeFile = tool(
({ path, content }) => `Wrote ${content.length} characters to ${path}`,
{
name: "write_file",
description: "Write content to a file.",
schema: z.object({ path: z.string(), content: z.string() }),
},
);
const executeSql = tool(() => "Deleted 42 rows", {
name: "execute_sql",
description: "Run a SQL query.",
schema: z.object({ query: z.string() }),
});
const readData = tool(({ table }) => `3 rows from ${table}`, {
name: "read_data",
description: "Read rows from a table.",
schema: z.object({ table: z.string() }),
});
let agent = createAgent({
model: "google:gemini-3.6-flash",
tools: [writeFile, executeSql, readData],
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
write_file: true,
execute_sql: { allowedDecisions: ["approve", "reject"] },
read_data: false,
},
interruptMode: "per_call",
}),
],
checkpointer: new MemorySaver(),
});import { MemorySaver } from "@langchain/langgraph";
import { createAgent, humanInTheLoopMiddleware, tool } from "langchain";
import * as z from "zod";
const writeFile = tool(
({ path, content }) => `Wrote ${content.length} characters to ${path}`,
{
name: "write_file",
description: "Write content to a file.",
schema: z.object({ path: z.string(), content: z.string() }),
},
);
const executeSql = tool(() => "Deleted 42 rows", {
name: "execute_sql",
description: "Run a SQL query.",
schema: z.object({ query: z.string() }),
});
const readData = tool(({ table }) => `3 rows from ${table}`, {
name: "read_data",
description: "Read rows from a table.",
schema: z.object({ table: z.string() }),
});
let agent = createAgent({
model: "openai:gpt-5.5",
tools: [writeFile, executeSql, readData],
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
write_file: true,
execute_sql: { allowedDecisions: ["approve", "reject"] },
read_data: false,
},
interruptMode: "per_call",
}),
],
checkpointer: new MemorySaver(),
});import { MemorySaver } from "@langchain/langgraph";
import { createAgent, humanInTheLoopMiddleware, tool } from "langchain";
import * as z from "zod";
const writeFile = tool(
({ path, content }) => `Wrote ${content.length} characters to ${path}`,
{
name: "write_file",
description: "Write content to a file.",
schema: z.object({ path: z.string(), content: z.string() }),
},
);
const executeSql = tool(() => "Deleted 42 rows", {
name: "execute_sql",
description: "Run a SQL query.",
schema: z.object({ query: z.string() }),
});
const readData = tool(({ table }) => `3 rows from ${table}`, {
name: "read_data",
description: "Read rows from a table.",
schema: z.object({ table: z.string() }),
});
let agent = createAgent({
model: "anthropic:claude-sonnet-5",
tools: [writeFile, executeSql, readData],
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
write_file: true,
execute_sql: { allowedDecisions: ["approve", "reject"] },
read_data: false,
},
interruptMode: "per_call",
}),
],
checkpointer: new MemorySaver(),
});import { MemorySaver } from "@langchain/langgraph";
import { createAgent, humanInTheLoopMiddleware, tool } from "langchain";
import * as z from "zod";
const writeFile = tool(
({ path, content }) => `Wrote ${content.length} characters to ${path}`,
{
name: "write_file",
description: "Write content to a file.",
schema: z.object({ path: z.string(), content: z.string() }),
},
);
const executeSql = tool(() => "Deleted 42 rows", {
name: "execute_sql",
description: "Run a SQL query.",
schema: z.object({ query: z.string() }),
});
const readData = tool(({ table }) => `3 rows from ${table}`, {
name: "read_data",
description: "Read rows from a table.",
schema: z.object({ table: z.string() }),
});
let agent = createAgent({
model: "openrouter:z-ai/glm-5.2",
tools: [writeFile, executeSql, readData],
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
write_file: true,
execute_sql: { allowedDecisions: ["approve", "reject"] },
read_data: false,
},
interruptMode: "per_call",
}),
],
checkpointer: new MemorySaver(),
});import { MemorySaver } from "@langchain/langgraph";
import { createAgent, humanInTheLoopMiddleware, tool } from "langchain";
import * as z from "zod";
const writeFile = tool(
({ path, content }) => `Wrote ${content.length} characters to ${path}`,
{
name: "write_file",
description: "Write content to a file.",
schema: z.object({ path: z.string(), content: z.string() }),
},
);
const executeSql = tool(() => "Deleted 42 rows", {
name: "execute_sql",
description: "Run a SQL query.",
schema: z.object({ query: z.string() }),
});
const readData = tool(({ table }) => `3 rows from ${table}`, {
name: "read_data",
description: "Read rows from a table.",
schema: z.object({ table: z.string() }),
});
let agent = createAgent({
model: "fireworks:accounts/fireworks/models/glm-5p2",
tools: [writeFile, executeSql, readData],
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
write_file: true,
execute_sql: { allowedDecisions: ["approve", "reject"] },
read_data: false,
},
interruptMode: "per_call",
}),
],
checkpointer: new MemorySaver(),
});import { MemorySaver } from "@langchain/langgraph";
import { createAgent, humanInTheLoopMiddleware, tool } from "langchain";
import * as z from "zod";
const writeFile = tool(
({ path, content }) => `Wrote ${content.length} characters to ${path}`,
{
name: "write_file",
description: "Write content to a file.",
schema: z.object({ path: z.string(), content: z.string() }),
},
);
const executeSql = tool(() => "Deleted 42 rows", {
name: "execute_sql",
description: "Run a SQL query.",
schema: z.object({ query: z.string() }),
});
const readData = tool(({ table }) => `3 rows from ${table}`, {
name: "read_data",
description: "Read rows from a table.",
schema: z.object({ table: z.string() }),
});
let agent = createAgent({
model: "baseten:zai-org/GLM-5.2",
tools: [writeFile, executeSql, readData],
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
write_file: true,
execute_sql: { allowedDecisions: ["approve", "reject"] },
read_data: false,
},
interruptMode: "per_call",
}),
],
checkpointer: new MemorySaver(),
});import { MemorySaver } from "@langchain/langgraph";
import { createAgent, humanInTheLoopMiddleware, tool } from "langchain";
import * as z from "zod";
const writeFile = tool(
({ path, content }) => `Wrote ${content.length} characters to ${path}`,
{
name: "write_file",
description: "Write content to a file.",
schema: z.object({ path: z.string(), content: z.string() }),
},
);
const executeSql = tool(() => "Deleted 42 rows", {
name: "execute_sql",
description: "Run a SQL query.",
schema: z.object({ query: z.string() }),
});
const readData = tool(({ table }) => `3 rows from ${table}`, {
name: "read_data",
description: "Read rows from a table.",
schema: z.object({ table: z.string() }),
});
let agent = createAgent({
model: "ollama:north-mini-code-1.0",
tools: [writeFile, executeSql, readData],
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
write_file: true,
execute_sql: { allowedDecisions: ["approve", "reject"] },
read_data: false,
},
interruptMode: "per_call",
}),
],
checkpointer: new MemorySaver(),
});读取中断
当模型在一个轮次中调用全部三个工具时,read_data 会立即运行。两个受控调用各自会触发一个中断:
import { isToolApprovalInterrupt } from "langchain";
let config = { configurable: { thread_id: "1" } };
let result = await agent.invoke(
{
messages: [
{ role: "user", content: "Archive old orders, then delete them." },
],
},
config,
);
// 每个受控工具调用触发一个中断,没有固定顺序
const approvals = (result.__interrupt__ ?? []).filter(isToolApprovalInterrupt);
for (const interrupt of approvals) {
console.log(interrupt.value);
}
// > {
// > type: 'tool_approval',
// > tool_call_id: 'call_write',
// > name: 'write_file',
// > args: { path: 'orders_archive.csv', content: 'id,total\n1,20\n' },
// > description: 'Tool execution requires approval\n\nTool: write_file\nArgs: {\n "path": "orders_archive.csv",\n "content": "id,total\\n1,20\\n"\n}'
// > }
// > {
// > type: 'tool_approval',
// > tool_call_id: 'call_sql',
// > name: 'execute_sql',
// > args: { query: "DELETE FROM orders WHERE created_at < '2025-01-01'" },
// > description: `Tool execution requires approval\n\nTool: execute_sql\nArgs: {\n "query": "DELETE FROM orders WHERE created_at < '2025-01-01'"\n}`
// > }每个中断值都具有 type: "tool_approval",因此客户端可以将其与其他中断区分开来。isToolApprovalInterrupt 函数用于对此进行检查并收窄该值的类型。tool_call_id、name 和 args 字段用于标识正在审核的调用。
读取响应 Schema
每个中断还会携带一个 response_schema:一个 JSON Schema,工具允许的每个决策对应一个分支,基于决策的 type 进行匹配。execute_sql 的 Schema 包含 approve 和 reject 分支。write_file 的 Schema 则包含全部四个分支。对于仅允许一种决策的工具,response_schema 仅为该决策本身的 Schema,不包含 oneOf。
每个选项卡展示了 write_file 的一个分支、与之匹配的答复,以及模型接收到的 ToolMessage。为了提高可读性,以下各分支省略了 additionalProperties 字段。
approve
按提议的内容执行调用。
{"type": "object", "properties": {"type": {"type": "string", "const": "approve"}}, "required": ["type"]}{ type: "approve" }Wrote 14 characters to orders_archive.csvedit
使用新参数执行调用。edited_action.name 必须保持为 write_file,且 edited_action.args 必须符合 write_file 的参数 Schema。对于工具未声明的参数,其处理方式与模型发送它们时相同:z.object Schema 会丢弃它们,而 z.looseObject Schema 会保留它们。
{
"type": "object",
"properties": {
"type": {"type": "string", "const": "edit"},
"edited_action": {
"type": "object",
"properties": {
"name": {"type": "string", "const": "write_file"},
"args": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"}
},
"required": ["path", "content"]
}
},
"required": ["name", "args"]
}
},
"required": ["type", "edited_action"]
}{
type: "edit",
edited_action: {
name: "write_file",
args: { path: "archive/orders_2024.csv", content: "id,total\n1,20\n" },
},
}模型会在一条注明由审核人员替换了调用的说明之后接收到工具的结果。可以在中间件上设置 editNotice 以更改此说明,或者传入 null 将其移除。
Note: a human reviewer replaced this tool call before it ran. The call recorded in your message is the one you produced, not the one that executed. This was intentional and authorized. Do not re-issue your original call. Executed instead: write_file with arguments {"path":"archive/orders_2024.csv","content":"id,total\n1,20\n"}.
Tool response:
Wrote 14 characters to archive/orders_2024.csvreject
跳过该调用。message 为可选参数,用于告知模型原因。
{"type": "object", "properties": {"type": {"type": "string", "const": "reject"}, "message": {"type": "string"}}, "required": ["type"]}{ type: "reject", message: "Write to the archive/ folder instead." }Write to the archive/ folder instead.respond
跳过该调用并代替工具进行回答。message 为必填项。模型会将其作为成功的工具结果来接收,因此若要拒绝调用请使用 reject。
{"type": "object", "properties": {"type": {"type": "string", "const": "respond"}, "message": {"type": "string"}}, "required": ["type", "message"]}{ type: "respond", message: "Saved orders_archive.csv to the shared drive myself." }Saved orders_archive.csv to the shared drive myself.通过 ID 恢复每次调用
通过以其中断 ID 为键的单个决策来答复每个中断:
import { Command } from "@langchain/langgraph";
const decisions = Object.fromEntries(
approvals.map((interrupt) => [
interrupt.id,
interrupt.value.name === "execute_sql"
? { type: "reject", message: "Archive the rows before deleting them." }
: { type: "approve" },
]),
);
result = await agent.invoke(new Command({ resume: decisions }), config);你也可以逐个答复中断。未答复的中断将保持挂起状态,其对应的工具会在获得答复后立即运行。
WARNING
如果一同发送的多个答复中有一个无效,其他答复对应的工具可能会运行,而它们的中断仍显示为挂起状态。再次答复它们会导致这些工具被运行两次。在将它们一同发送之前,请根据 response_schema 检查答复,或者逐个发送答复。
纠正无效答复
LangGraph 在保存答复之前会根据中断的 response_schema 检查每个答复。无效的答复会抛出错误并且不会保存任何内容,因此你可以向同一个中断 ID 发送纠正后的答复。中间件会将该错误包装在 MiddlewareError 中,其 cause 是包含问题列表的 Zod 错误。在这里,编辑操作遗漏了 content:
const writeFileInterrupt = (result.__interrupt__ ?? [])
.filter(isToolApprovalInterrupt)
.find((interrupt) => interrupt.value.name === "write_file")!;
const args: Record<string, string> = { path: "archive/orders_2024.csv" };
const edit = { type: "edit", edited_action: { name: "write_file", args } };
try {
await agent.invoke(
new Command({ resume: { [writeFileInterrupt.id]: edit } }),
config,
);
} catch (error) {
// 中间件包装了错误;其 `cause` 列出了失败的原因
if (!(error instanceof Error && error.cause instanceof z.core.$ZodError)) {
throw error;
}
for (const issue of error.cause.issues) {
console.log(`${issue.path.join(".")} ${issue.message}`);
}
}
// > edited_action.args.content Invalid input: expected string, received undefined
// 没有保存任何内容,因此再次答复同一个中断
args.content = "id,total\n1,20\n";
result = await agent.invoke(
new Command({ resume: { [writeFileInterrupt.id]: edit } }),
config,
);每个 issue 都会指明失败的字段,因此你可以将其展示给审核人员。LangGraph 仅对具有 Zod v4 对象 Schema 的工具检查编辑内容。对于其他工具(例如具有 JSON Schema 的工具),response_schema 会接受任何 args 对象,并在工具运行时检查参数。
单次调用审核的中间件顺序
在 per-call 模式下,审核发生在工具调用开始时,位于工具调用中间件链的内部:
- 将
humanInTheLoopMiddleware列在工具重试或错误处理中间件(例如 toolRetryMiddleware 和 toolErrorMiddleware)之前,以便对其进行包裹。否则,重试的调用会再次询问审核人员,且无效的答复会被重试或作为工具错误返回给模型。 - 当智能体恢复时,排在
humanInTheLoopMiddleware之前的中间件会再次运行直至暂停处,因此在其调用处理程序之前不得产生副作用。工具仅在得到答复后才会运行。 when谓词在每次调用开始时运行,因此它接收到的 ToolCallRequest 包含了该工具。
响应中断 (Responding to interrupts)
调用智能体(agent)时,它会持续运行,直到完成任务或触发中断(interrupt)。当工具调用与你在 interruptOn 中配置的策略相匹配时,就会触发中断。返回结果中包含一个 __interrupt__ 字段,其中包含需要审查的操作。随后你可以将这些操作呈现给审查者,并在提供决策后恢复执行。
本节中的示例以及下方的决策类型均使用默认的批处理模式(batched mode)。对于单次调用模式(per-call mode),请参阅每次工具调用暂停一次。
import { HumanMessage } from "@langchain/core/messages";
import { Command } from "@langchain/langgraph";
// 必须提供 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 请求,附带 actionRequests 和 reviewConfigs
console.log(result.__interrupt__);
// > [
// > Interrupt(
// > value: {
// > actionRequests: [
// > {
// > name: 'execute_sql',
// > args: { 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 // 使用相同的 thread ID 来恢复暂停的对话
);决策类型 (Decision types)
approve
使用 approve 按原样批准工具调用并在不作任何修改的情况下执行它。
await agent.invoke(
new Command({
// 决策以列表形式提供,每个待审查操作对应一项。
// 决策的顺序必须与中断请求中操作的顺序一致。
resume: {
decisions: [
{
type: "approve",
}
]
}
}),
config // 使用相同的 thread ID 来恢复暂停的对话
);edit
使用 edit 在执行前修改工具调用。 提供带有新工具名称和参数的已编辑操作。
await agent.invoke(
new Command({
// 决策以列表形式提供,每个待审查操作对应一项。
// 决策的顺序必须与中断请求中操作的顺序一致。
resume: {
decisions: [
{
type: "edit",
// 包含工具名称和参数的已编辑操作
editedAction: {
// 要调用的工具名称。
// 通常与原始操作相同。
name: "new_tool_name",
// 传递给工具的参数。
args: { key1: "new_value", key2: "original_value" },
}
}
]
}
}),
config // 使用相同的 thread ID 来恢复暂停的对话
);TIP
编辑(editing)工具参数时,请谨慎进行修改。对原始参数的重大修改可能导致模型重新评估其方案,并可能多次执行该工具或采取非预期的操作。
reject
使用 reject 拒绝工具调用,并提供反馈而不是执行。该工具不会被执行。
await agent.invoke(
new Command({
// 决策以列表形式提供,每个待审查操作对应一项。
// 决策的顺序必须与中断请求中操作的顺序一致。
resume: {
decisions: [
{
type: "reject",
// 可选:解释为何拒绝该操作,
// 以及智能体是否应尝试不同的方法。
message: "User rejected this action. Do not retry this tool call.",
}
]
}
}),
config // 使用相同的 thread ID 来恢复暂停的对话
);message 会作为反馈添加到对话中,帮助智能体理解为什么该操作被拒绝以及接下来应该做什么。当省略 message 时,中间件会使用默认的拒绝消息,告知模型该工具未执行,并且除非用户要求,否则不要重试相同的工具调用。对于产生副作用的工具,请提供特定领域的明确消息,说明智能体是应该放弃该操作、提出跟进问题,还是尝试更安全的替代方案。
respond
对于“询问用户”风格的工具(其真正的工具实现就是人类的回复),使用 respond。message 内容将直接作为工具结果返回;工具本身不会被执行。
await agent.invoke(
new Command({
// 决策以列表形式提供,每个待审查操作对应一项。
// 决策的顺序必须与中断请求中操作的顺序一致。
resume: {
decisions: [
{
type: "respond",
// 人类的回复,直接作为工具结果返回
message: "Blue.",
}
]
}
}),
config // 使用相同的 thread ID 来恢复暂停的对话
);message 会作为成功的 ToolMessage 返回给智能体。当工具是有意用作人类输入的占位符时(例如提示澄清信息的 ask_user 工具),请使用 respond。不要使用 respond 来拒绝提议的操作,因为它会告知模型该工具已成功完成。
多个决策 (Multiple decisions)
当有多个操作处于待审查状态时,按照它们在中断中出现的相同顺序为每个操作提供决策:
{
decisions: [
{ type: "approve" },
{
type: "edit",
editedAction: {
name: "tool_name",
args: { param: "new_value" }
}
},
{
type: "reject",
message: "This action is not allowed"
}
]
}结合人在回路的流式传输 (Streaming with human-in-the-loop)
你可以在智能体运行和处理中断时使用 stream_events() 流式传输实时更新。使用 stream.messages 流式传输 LLM token,使用 stream.values 检查智能体状态快照以获取中断信息。
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);
}
}在单次调用模式中,stream.interrupts 为每个受控工具调用保留一个条目,中断值位于 payload 中,且没有 response_schema。使用以 interruptId 为键的对象进行恢复:
const streamDecisions = Object.fromEntries(
stream.interrupts.map((interrupt) => [
interrupt.interruptId,
{ type: "approve" },
]),
);
stream = await agent.streamEvents(new Command({ resume: streamDecisions }), {
...config,
version: "v3",
});
for await (const message of stream.messages) {
for await (const token of message.text) {
process.stdout.write(token);
}
}有关流式模式的更多详细信息,请参阅流式传输 (Streaming)指南。
执行生命周期 (Execution lifecycle)
中间件定义了一个 afterModel 钩子,该钩子在模型生成响应之后但在任何工具调用执行之前运行:
- 智能体调用模型以生成响应。
- 中间件检查响应中是否存在工具调用。
- 如果有任何调用需要人工输入,中间件会构建一个包含
actionRequests和reviewConfigs的 HITLRequest,并调用 interrupt。 - 智能体等待人工决策。
- 根据
HITLResponse中的决策,中间件执行已批准或已编辑的调用,为已拒绝的调用合成 ToolMessage,对于respond决策将人类回复直接作为 ToolMessage 返回,并恢复执行。
在单次调用模式中,afterModel 钩子不执行任何操作。中间件改为在每个受控调用运行时暂停,每个调用对应一个 interrupt,一旦该调用的决策到达便运行该工具。
自定义 HITL 逻辑 (Custom HITL logic)
对于更专门的工作流,你可以直接使用 interrupt 原语和中间件抽象来构建自定义 HITL 逻辑。
查看上文中的执行生命周期,了解如何将中断集成到智能体的运行中。
