并非所有代理操作都应在无人监督的情况下运行。当代理即将发送电子邮件、删除记录、执行金融交易或执行任何不可逆操作时,您需要人工先审核并批准该操作。人机回环(HITL)模式让您的代理暂停执行,向用户呈现待处理操作,并在获得明确批准后才恢复执行。
中断如何工作
LangGraph 代理支持中断,这是代理将控制权交还给客户端的显式暂停点。当代理遇到中断时:
- 代理停止执行并发出中断有效载荷
useStream 钩子通过 stream.interrupt 显示中断
- 您的 UI 渲染一个审核卡片,提供批准/拒绝/编辑选项
- 用户做出决定
- 您的代码使用恢复命令调用
stream.submit()
- 代理从暂停处继续执行
为 HITL 设置 useStream
导入您的代理,并将 typeof myAgent 作为类型参数传递给 useStream,以类型安全地访问状态值:
import type { myAgent } from "./agent";
import { useStream } from "@langchain/react";
const AGENT_URL = "http://localhost:2024";
export function Chat() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "human_in_the_loop",
});
const interrupt = stream.interrupt;
return (
<div>
{stream.messages.map((msg) => (
<Message key={msg.id} message={msg} />
))}
{interrupt && (
<ApprovalCard
interrupt={interrupt}
onRespond={(response) =>
stream.submit(null, { command: { resume: response } })
}
/>
)}
</div>
);
}
中断有效载荷
当代理暂停时,stream.interrupt 包含一个 HITLRequest,其结构如下:
interface HITLRequest {
actionRequests: ActionRequest[];
reviewConfigs: ReviewConfig[];
}
interface ActionRequest {
action: string;
args: Record<string, unknown>;
description?: string;
}
interface ReviewConfig {
allowedDecisions: ("approve" | "reject" | "edit")[];
}
| 属性 | 描述 |
|---|
actionRequests | 代理想要执行的待处理操作数组 |
actionRequests[].action | 操作名称(例如 "send_email", "delete_record") |
actionRequests[].args | 操作的结构化参数 |
actionRequests[].description | 操作作用的可选人类可读描述 |
reviewConfigs | 控制允许哪些决策的每操作配置 |
reviewConfigs[].allowedDecisions | 要显示的按钮:"approve", "reject", "edit" |
决策类型
HITL 模式支持三种决策类型:
用户确认操作应按原样进行:
const response: HITLResponse = {
decision: "approve",
};
stream.submit(null, { command: { resume: response } });
用户拒绝操作,并可提供可选原因:
const response: HITLResponse = {
decision: "reject",
reason: "The email tone is too aggressive. Please revise.",
};
stream.submit(null, { command: { resume: response } });
当操作被拒绝时,代理会收到拒绝原因,并可以决定如何继续。它可能会重新措辞、提出澄清问题或完全放弃该操作。
用户在批准前修改操作的参数:
const response: HITLResponse = {
decision: "edit",
args: {
...originalArgs,
subject: "Updated subject line",
body: "Revised email body with softer language.",
},
};
stream.submit(null, { command: { resume: response } });
构建 ApprovalCard
这是一个完整的审批卡片组件,处理所有三种决策类型:
function ApprovalCard({
interrupt,
onRespond,
}: {
interrupt: { value: HITLRequest };
onRespond: (response: HITLResponse) => void;
}) {
const request = interrupt.value;
const [editedArgs, setEditedArgs] = useState(
request.actionRequests[0]?.args ?? {}
);
const [rejectReason, setRejectReason] = useState("");
const [mode, setMode] = useState<"review" | "edit" | "reject">("review");
const action = request.actionRequests[0];
const config = request.reviewConfigs[0];
if (!action || !config) return null;
return (
<div className="rounded-lg border-2 border-amber-300 bg-amber-50 p-4">
<h3 className="font-semibold text-amber-800">Action Review Required</h3>
<p className="mt-1 text-sm text-amber-700">
{action.description ?? `The agent wants to perform: ${action.action}`}
</p>
<div className="mt-3 rounded bg-white p-3 font-mono text-sm">
<pre>{JSON.stringify(action.args, null, 2)}</pre>
</div>
{mode === "review" && (
<div className="mt-4 flex gap-2">
{config.allowedDecisions.includes("approve") && (
<button
className="rounded bg-green-600 px-4 py-2 text-white"
onClick={() => onRespond({ decision: "approve" })}
>
Approve
</button>
)}
{config.allowedDecisions.includes("reject") && (
<button
className="rounded bg-red-600 px-4 py-2 text-white"
onClick={() => setMode("reject")}
>
Reject
</button>
)}
{config.allowedDecisions.includes("edit") && (
<button
className="rounded bg-blue-600 px-4 py-2 text-white"
onClick={() => setMode("edit")}
>
Edit
</button>
)}
</div>
)}
{mode === "reject" && (
<div className="mt-4 space-y-2">
<textarea
className="w-full rounded border p-2"
placeholder="Reason for rejection..."
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
/>
<button
className="rounded bg-red-600 px-4 py-2 text-white"
onClick={() =>
onRespond({ decision: "reject", reason: rejectReason })
}
>
Confirm Rejection
</button>
</div>
)}
{mode === "edit" && (
<div className="mt-4 space-y-2">
<textarea
className="w-full rounded border p-2 font-mono text-sm"
value={JSON.stringify(editedArgs, null, 2)}
onChange={(e) => {
try {
setEditedArgs(JSON.parse(e.target.value));
} catch {
// allow invalid JSON while editing
}
}}
/>
<button
className="rounded bg-blue-600 px-4 py-2 text-white"
onClick={() =>
onRespond({ decision: "edit", args: editedArgs })
}
>
Submit Edits
</button>
</div>
)}
</div>
);
}
恢复流程
用户做出决定后,完整周期如下:
- 调用
stream.submit(null, { command: { resume: hitlResponse } })
useStream 钩子将恢复命令发送到 LangGraph 后端
- 代理收到
HITLResponse 并继续执行
- 如果批准,工具将使用原始(或编辑后的)参数运行
- 如果拒绝,代理会收到原因并决定下一步
interrupt 属性重置为 null,代理恢复流式传输
您可以在单个代理运行中链接多个 HITL 检查点。例如,代理可能请求批准进行搜索,然后在发送包含结果的电子邮件之前再次请求批准。每个中断都是独立处理的。
常见用例
| 用例 | 操作 | 审核配置 |
|---|
| 发送电子邮件 | send_email | ["approve", "reject", "edit"] |
| 数据库写入 | update_record | ["approve", "reject"] |
| 金融交易 | transfer_funds | ["approve", "reject"] |
| 文件删除 | delete_files | ["approve", "reject"] |
| 调用外部服务 API | call_api | ["approve", "reject", "edit"] |
处理多个待处理操作
当代理想要同时执行多个操作时,中断可以包含多个 actionRequests。为每个操作渲染一个卡片,并在恢复前收集所有决策:
function MultiActionReview({
interrupt,
onRespond,
}: {
interrupt: { value: HITLRequest };
onRespond: (responses: HITLResponse[]) => void;
}) {
const [decisions, setDecisions] = useState<Record<number, HITLResponse>>({});
const request = interrupt.value;
const allDecided =
Object.keys(decisions).length === request.actionRequests.length;
return (
<div className="space-y-4">
{request.actionRequests.map((action, i) => (
<SingleActionCard
key={i}
action={action}
config={request.reviewConfigs[i]}
onDecide={(response) =>
setDecisions((prev) => ({ ...prev, [i]: response }))
}
/>
))}
{allDecided && (
<button
className="rounded bg-green-600 px-4 py-2 text-white"
onClick={() =>
onRespond(
request.actionRequests.map((_, i) => decisions[i])
)
}
>
Submit All Decisions
</button>
)}
</div>
);
}
最佳实践
在实现 HITL 工作流时,请牢记以下准则:
- 显示清晰的上下文。始终显示代理想要做什么以及为什么。包括操作描述和完整参数。
- 让批准成为最简单的路径。如果操作看起来正确,批准应该只需单击一次。将多步骤流程留给拒绝/编辑。
- 验证编辑后的参数。当用户编辑操作参数时,在发送前验证 JSON 结构。为格式错误的输入显示内联错误。
- 持久化中断状态。如果用户刷新页面,中断应仍然可见。
useStream 通过线程的检查点处理此问题。
- 记录所有决策。为了审计跟踪,记录每个批准/拒绝/编辑决策以及时间戳和做出决策的用户。
- 合理设置超时。长时间运行的代理不应无限期地阻塞在人工审核上。考虑显示代理已等待多长时间。