并非每个代理动作都应该在无人监督的情况下运行。当代理即将发送电子邮件、删除记录、执行金融交易或执行任何不可逆操作时,你需要先让人工审核并批准该动作。Human-in-the-Loop(HITL)模式让代理暂停执行,向用户展示待处理动作,并且只有在明确批准后才恢复执行。 因为 HITL 构建在 LangGraph interrupts 和 checkpoints 之上,所以暂停是持久的。用户可以刷新页面,审核者可以从另一个组件回答,代理仍会从执行停止的确切位置恢复,而不是重放整个运行。

Interrupts 的工作原理

LangGraph 代理支持 interrupts,即代理将控制权交还给客户端的显式暂停点。当代理遇到 interrupt 时:
  1. 代理停止执行并发出 interrupt payload
  2. useStream hook 通过 stream.interrupt 暴露 interrupt
  3. 你的 UI 渲染一个包含 approve/reject/edit 选项的审核卡片
  4. 用户做出决策
  5. 你的代码使用 resume 命令调用 stream.submit()
  6. 代理从停止处继续执行
前端 SDK 会将 interrupt 与线程的其余状态一起保存,因此你的 UI 可以在合适的位置渲染它:对话记录内联位置、审核队列、管理员仪表板,或在做出决策前阻塞用户下一个动作的模态框中。

设置 useStream

useStream 连接到你的 human-in-the-loop 代理。当图遇到 interrupt 时,hook 会在 stream.interrupt 上暴露待处理 payload。该值存在时渲染审批卡片,然后在用户批准、拒绝或编辑该动作后,通过 stream.submit(null, { command: { resume: response } }) 恢复运行。
The code examples use useStream<typeof myAgent> for type-safe stream state. See Type inference for Python or JavaScript backends.
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>
  );
}

Interrupt payload

代理暂停时,stream.interrupt 包含一个结构如下的 HITLRequest
interface HITLRequest {
  actionRequests: ActionRequest[];
  reviewConfigs: ReviewConfig[];
}

interface ActionRequest {
  name: string;
  args: Record<string, unknown>;
  description?: string;
}

interface ReviewConfig {
  allowedDecisions: ("approve" | "reject" | "edit" | "respond")[];
}
属性描述
actionRequests代理想要执行的待处理动作数组
actionRequests[].name动作名称(例如 "send_email""delete_record"
actionRequests[].args动作的结构化参数
actionRequests[].description可选的人类可读描述,说明该动作会做什么
reviewConfigs每个动作的配置,用于控制允许哪些决策
reviewConfigs[].allowedDecisions要显示哪些按钮:"approve""reject""edit""respond"

决策类型

HITL 模式支持四种决策类型:

批准

用户确认该动作应按原样继续执行:
const response: HITLResponse = {
  decisions: [{ type: "approve" }],
};

stream.submit(null, { command: { resume: response } });

拒绝

用户拒绝该动作,并可选择提供原因。工具不会执行:
const response: HITLResponse = {
  decisions: [
    {
      type: "reject",
      message: "The email tone is too aggressive. Do not send it.",
    },
  ],
};

stream.submit(null, { command: { resume: response } });
动作被拒绝时,代理会收到拒绝原因,并可以决定如何继续。如果省略 message,后端会使用默认消息,告诉模型该工具未执行,并且除非用户要求,否则不要重试同一个工具调用。对于有副作用的工具,请传入清晰消息,告诉代理是放弃该动作、询问后续问题,还是尝试更安全的替代方案。

编辑

用户在批准前修改动作参数:
const response: HITLResponse = {
  decisions: [
    {
      type: "edit",
      editedAction: {
        name: actionRequest.name,
        args: {
          ...actionRequest.args,
          subject: "Updated subject line",
          body: "Revised email body with softer language.",
        },
      },
    },
  ],
};

stream.submit(null, { command: { resume: response } });

响应

用户为“ask user”风格的工具提供直接回复。message 会成为工具结果,工具本身不会执行:
const response: HITLResponse = {
  decisions: [{ type: "respond", message: "Blue." }],
};

stream.submit(null, { command: { resume: response } });
当工具有意作为人工输入占位符时使用 respond,例如提示代理向用户收集信息的 ask_user 工具。不要用 respond 拒绝拟议动作,因为它会作为成功的工具结果返回给模型。

构建 ApprovalCard

下面是审批卡片使用的决策连接方式。UI 可以将每个动作拆成单独卡片,但 resume payload 是单个 HITLResponse,其中每个待处理动作对应一个决策:
async function approveAll() {
  const resume: HITLResponse = {
    decisions: actionRequests.map(() => ({ type: "approve" })),
  };
  await stream.submit(null, { command: { resume } });
}

async function rejectOne(index: number, message: string) {
  const resume: HITLResponse = {
    decisions: actionRequests.map((_, i) =>
      i === index
        ? { type: "reject", message }
        : { type: "reject", message: "Rejected along with other actions" },
    ),
  };
  await stream.submit(null, { command: { resume } });
}

async function editOne(index: number, editedArgs: Record<string, unknown>) {
  const originalAction = actionRequests[index];
  const resume: HITLResponse = {
    decisions: actionRequests.map((_, i) =>
      i === index
        ? {
            type: "edit",
            editedAction: { name: originalAction.name, args: editedArgs },
          }
        : { type: "approve" },
    ),
  };
  await stream.submit(null, { command: { resume } });
}

Resume 流程

用户做出决策后,完整周期如下:
  1. 调用 stream.submit(null, { command: { resume: hitlResponse } })
  2. useStream hook 将 resume 命令发送到 LangGraph 后端
  3. 代理接收 HITLResponse 并继续执行。decisions 中的每个条目可以是:
    • { type: "approve" }:代理继续执行该动作
    • { type: "reject", message }:工具不会执行,代理会先收到拒绝消息,再决定下一步
    • { type: "edit", editedAction }:代理使用编辑后的参数运行工具
    • { type: "respond", message }:人工消息不执行工具,直接作为工具结果返回
  4. 代理恢复流式传输时,interrupt 属性重置为 null
你可以在单次代理运行中串联多个 HITL checkpoints。例如,代理可能先请求批准搜索,然后在发送包含结果的电子邮件前再次请求批准。每个 interrupt 都会独立处理。

处理多个待处理动作

当代理想要一次执行多个动作时,一个 interrupt 可以包含多个 actionRequests。为每个动作渲染一张卡片,并在恢复前收集所有决策:
function MultiActionReview({
  interrupt,
  onRespond,
}: {
  interrupt: { value: HITLRequest };
  onRespond: (response: HITLResponse) => void;
}) {
  const [decisions, setDecisions] = useState<Record<number, HITLResponse["decisions"][number]>>({});
  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({
              decisions: request.actionRequests.map((_, i) => decisions[i]),
            })
          }
        >
          Submit All Decisions
        </button>
      )}
    </div>
  );
}

最佳实践

实现 HITL 工作流时,请记住这些准则:
  • 显示清晰上下文。始终展示代理想做什么以及为什么要这么做。包含动作描述和完整参数。
  • 让批准成为最简单路径。如果动作看起来正确,批准应该只需单击。将多步流程留给拒绝/编辑。
  • 验证编辑后的 args。用户编辑动作参数时,在发送前验证 JSON 结构。对格式错误的输入显示行内错误。
  • 持久化 interrupt 状态。如果用户刷新页面,interrupt 应该仍然可见。useStream 通过线程 checkpoint 处理这一点。
  • 记录所有决策。为了审计跟踪,记录每个 approve/reject/edit 决策,包括时间戳和做出决策的用户。
  • 谨慎设置超时。长时间运行的代理不应无限期阻塞在人工审核上。可以考虑显示代理已经等待了多长时间。