本文聚焦高风险操作的人工审批场景,提供 React/Vue/Svelte/Angular 全框架适配代码,包含审批卡片、决策流转、多动作处理等核心功能。


1. HITL 工作原理

Human-in-the-Loop(人工介入)是针对高风险操作的审批机制,适用于邮件发送、数据删除、资金转账等不可逆操作。核心流程如下:

  1. Agent 触发中断:当 Agent 要执行高风险操作时,主动暂停执行并发送「中断请求」
  2. 前端接收中断useStream 钩子通过 stream.interrupt 暴露中断信息
  3. 用户决策:前端渲染审批卡片,提供「批准/拒绝/编辑」选项
  4. 提交决策:用户操作后,前端调用 stream.submit() 提交决策结果
  5. Agent 恢复执行:Agent 接收决策后,继续执行(批准)、调整(编辑)或终止(拒绝)操作

核心价值:在 Agent 自动化流程中插入人工校验,降低操作风险,提升系统可靠性


2. 环境搭建:useStream 配置

首先安装核心依赖(以 npm 为例):

# 核心依赖
npm install @langchain/core @langchain/react
# 类型支持(TypeScript 项目)
npm install -D @types/langchain__core

2.1 类型定义(TypeScript)

import type { BaseMessage } from "@langchain/core/messages";

// 匹配 Agent 状态结构的接口
interface AgentState {
  messages: BaseMessage[]; // 消息列表(包含 AI 消息、人类消息)
}

// HITL 核心类型(从 LangChain 导入或自定义)
import type { HITLRequest, HITLResponse } from "@langchain/react/dist/types";

2.2 基础流式组件(React 示例)

import { useStream } from "@langchain/react";
import { BaseMessage } from "@langchain/core/messages";
import { Message } from "./Message"; // 自定义消息组件
import { ApprovalCard } from "./ApprovalCard"; // 后续实现的审批卡片
import type { HITLRequest, HITLResponse } from "@langchain/react/dist/types";

// Agent 服务地址(替换为你的实际地址)
const AGENT_URL = "http://localhost:2024";

export function HITLChat() {
  // 初始化流式连接(指定 HITL 专属 Agent ID)
  const stream = useStream<AgentState>({
    apiUrl: AGENT_URL,
    assistantId: "human_in_the_loop", // 替换为你的 HITL Agent ID
  });

  // 获取当前中断请求(Agent 暂停时非 null)
  const interrupt = stream.interrupt as { value: HITLRequest } | null;

  // 提交决策结果,恢复 Agent 执行
  const handleRespond = (response: HITLResponse) => {
    stream.submit(null, { command: { resume: response } });
  };

  return (
    <div className="chat-container max-w-3xl mx-auto p-4">
      {/* 渲染历史消息 */}
      {stream.messages.map((msg: BaseMessage) => (
        <Message key={msg.id} message={msg} />
      ))}

      {/* 有中断时渲染审批卡片 */}
      {interrupt && (
        <ApprovalCard
          interrupt={interrupt.value}
          onRespond={handleRespond}
        />
      )}
    </div>
  );
}

3. 核心概念:中断负载(Interrupt Payload)

当 Agent 暂停时,stream.interrupt.value 包含 HITLRequest 类型的完整中断信息,定义如下:

3.1 类型结构

interface HITLRequest {
  actionRequests: ActionRequest[]; // 待审批动作列表
  reviewConfigs: ReviewConfig[];    // 审批配置(允许的决策类型)
}

interface ActionRequest {
  action: string;                  // 动作名称(如 "send_email"、"delete_record")
  args: Record<string, unknown>;   // 动作参数(结构化数据)
  description?: string;            // 动作描述(人类可读)
}

interface ReviewConfig {
  allowedDecisions: ("approve" | "reject" | "edit")[]; // 允许的决策类型
}

3.2 字段说明

字段 描述
actionRequests 待审批的动作数组(支持单个或多个动作)
actionRequests[].action 动作唯一标识(如 “transfer_funds” 对应资金转账)
actionRequests[].args 动作的结构化参数(如转账的 { amount: 1000, to: "user123" }
actionRequests[].description 可选,动作的自然语言描述(如 “向用户 user123 转账 1000 元”)
reviewConfigs 每个动作的审批配置(与 actionRequests 一一对应)
reviewConfigs[].allowedDecisions 该动作支持的决策类型(如仅允许 “approve”/“reject”,不允许编辑)

4. 三大决策类型:批准/拒绝/编辑

HITL 支持三种核心决策,前端需根据 reviewConfigs.allowedDecisions 动态渲染对应按钮:

4.1 批准(Approve)

用户确认动作按原参数执行:

// 决策格式
const approveResponse: HITLResponse = {
  decision: "approve", // 决策类型
};

// 提交决策(触发 Agent 继续执行)
stream.submit(null, { command: { resume: approveResponse } });

4.2 拒绝(Reject)

用户拒绝执行动作,可附带拒绝原因(Agent 可根据原因调整流程):

// 决策格式
const rejectResponse: HITLResponse = {
  decision: "reject",
  reason: "转账金额超出限额,请核实后重新提交", // 可选拒绝原因
};

// 提交决策
stream.submit(null, { command: { resume: rejectResponse } });

4.3 编辑(Edit)

用户修改动作参数后批准执行:

// 原始参数(从 action.args 获取)
const originalArgs = { amount: 1000, to: "user123" };

// 编辑后的参数
const editedArgs = {
  ...originalArgs,
  amount: 800, // 修改金额
  remark: "月度补贴", // 新增参数
};

// 决策格式
const editResponse: HITLResponse = {
  decision: "edit",
  args: editedArgs, // 编辑后的参数
};

// 提交决策
stream.submit(null, { command: { resume: editResponse } });

5. 核心组件:审批卡片(ApprovalCard)实现

审批卡片是 HITL 交互的核心,需支持「查看动作详情、选择决策、编辑参数、提交结果」全流程。以下是完整 React 实现:

import { useState } from "react";
import type { HITLRequest, HITLResponse } from "@langchain/react/dist/types";
import { FaCheckCircle, FaTimesCircle, FaEdit, FaSave } from "react-icons/fa";

interface ApprovalCardProps {
  interrupt: HITLRequest; // 中断请求数据
  onRespond: (response: HITLResponse) => void; // 决策提交回调
}

export function ApprovalCard({ interrupt, onRespond }: ApprovalCardProps) {
  // 取第一个待审批动作(单动作场景,多动作场景见 8 节)
  const [action] = interrupt.actionRequests;
  const [config] = interrupt.reviewConfigs;

  // 状态管理
  const [mode, setMode] = useState<"review" | "reject" | "edit">("review");
  const [rejectReason, setRejectReason] = useState("");
  const [editedArgs, setEditedArgs] = useState(
    JSON.stringify(action.args, null, 2) // 格式化参数为 JSON 字符串,方便编辑
  );
  const [isInvalidJson, setIsInvalidJson] = useState(false);

  // 验证编辑后的 JSON 格式
  const validateEditedArgs = (): Record<string, unknown> | null => {
    try {
      const parsed = JSON.parse(editedArgs);
      setIsInvalidJson(false);
      return parsed;
    } catch (err) {
      setIsInvalidJson(true);
      return null;
    }
  };

  // 提交批准决策
  const handleApprove = () => {
    onRespond({ decision: "approve" });
  };

  // 提交拒绝决策
  const handleReject = () => {
    onRespond({
      decision: "reject",
      reason: rejectReason.trim() || "用户拒绝执行该动作",
    });
  };

  // 提交编辑后的决策
  const handleEditSubmit = () => {
    const parsedArgs = validateEditedArgs();
    if (parsedArgs) {
      onRespond({
        decision: "edit",
        args: parsedArgs,
      });
    }
  };

  if (!action || !config) return null;

  return (
    <div className="rounded-lg border-2 border-amber-400 bg-amber-50 p-5 mb-4 shadow-md">
      {/* 卡片标题 */}
      <div className="flex items-center gap-2 mb-4">
        <span className="inline-block w-3 h-3 rounded-full bg-amber-500 animate-pulse"></span>
        <h3 className="text-lg font-semibold text-amber-800">需要人工审核</h3>
      </div>

      {/* 动作描述 */}
      <div className="mb-4 text-gray-700">
        <p className="font-medium">动作:{action.action}</p>
        {action.description && (
          <p className="mt-1 text-sm text-gray-600">
            描述:{action.description}
          </p>
        )}
      </div>

      {/* 动作参数(JSON 格式化展示) */}
      <div className="mb-4 rounded-lg bg-white p-3 font-mono text-sm overflow-x-auto">
        {mode === "edit" ? (
          // 编辑模式:文本域允许修改参数
          <textarea
            className={`w-full h-40 p-2 border rounded ${
              isInvalidJson ? "border-red-500" : "border-gray-300"
            }`}
            value={editedArgs}
            onChange={(e) => setEditedArgs(e.target.value)}
            placeholder="输入 JSON 格式的参数..."
          />
        ) : (
          // 查看模式:展示格式化 JSON
          <pre>{JSON.stringify(action.args, null, 2)}</pre>
        )}
        {isInvalidJson && (
          <p className="mt-1 text-xs text-red-500">JSON 格式无效,请检查</p>
        )}
      </div>

      {/* 决策操作区 */}
      {mode === "review" && (
        <div className="flex gap-3">
          {/* 批准按钮 */}
          {config.allowedDecisions.includes("approve") && (
            <button
              className="flex items-center gap-1 rounded bg-green-600 px-4 py-2 text-white hover:bg-green-700 transition-colors"
              onClick={handleApprove}
            >
              <FaCheckCircle size={16} /> 批准
            </button>
          )}

          {/* 拒绝按钮 */}
          {config.allowedDecisions.includes("reject") && (
            <button
              className="flex items-center gap-1 rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700 transition-colors"
              onClick={() => setMode("reject")}
            >
              <FaTimesCircle size={16} /> 拒绝
            </button>
          )}

          {/* 编辑按钮 */}
          {config.allowedDecisions.includes("edit") && (
            <button
              className="flex items-center gap-1 rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 transition-colors"
              onClick={() => setMode("edit")}
            >
              <FaEdit size={16} /> 编辑
            </button>
          )}
        </div>
      )}

      {/* 拒绝模式:输入拒绝原因 */}
      {mode === "reject" && (
        <div className="space-y-3">
          <textarea
            className="w-full h-24 p-2 border rounded border-gray-300"
            value={rejectReason}
            onChange={(e) => setRejectReason(e.target.value)}
            placeholder="请输入拒绝原因(可选)..."
          />
          <div className="flex gap-3">
            <button
              className="flex items-center gap-1 rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700 transition-colors"
              onClick={handleReject}
            >
              <FaTimesCircle size={16} /> 确认拒绝
            </button>
            <button
              className="rounded bg-gray-200 px-4 py-2 text-gray-700 hover:bg-gray-300 transition-colors"
              onClick={() => setMode("review")}
            >
              取消
            </button>
          </div>
        </div>
      )}

      {/* 编辑模式:提交/取消 */}
      {mode === "edit" && (
        <div className="flex gap-3">
          <button
            className="flex items-center gap-1 rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 transition-colors"
            onClick={handleEditSubmit}
          >
            <FaSave size={16} /> 提交编辑
          </button>
          <button
            className="rounded bg-gray-200 px-4 py-2 text-gray-700 hover:bg-gray-300 transition-colors"
            onClick={() => {
              setMode("review");
              setEditedArgs(JSON.stringify(action.args, null, 2)); // 重置编辑内容
            }}
          >
            取消
          </button>
        </div>
      )}
    </div>
  );
}

组件特性

  1. 动态适配:根据 allowedDecisions 只渲染允许的决策按钮
  2. 格式校验:编辑参数时自动校验 JSON 格式,避免无效输入
  3. 用户体验:分「查看/拒绝/编辑」三种模式,操作流程清晰
  4. 样式友好:使用语义化颜色(绿色批准、红色拒绝、蓝色编辑),适配聊天场景

6. 工作流:中断恢复流程

完整的 HITL 工作流包含「Agent 中断 → 前端展示 → 用户决策 → Agent 恢复」四个阶段,具体步骤如下:

  1. Agent 触发中断:执行高风险操作前,Agent 发送 HITLRequest 并暂停
  2. 前端接收中断stream.interrupt 变为非 null,触发审批卡片渲染
  3. 用户决策:用户查看动作详情,选择「批准/拒绝/编辑」并提交
  4. 前端提交决策:调用 stream.submit(null, { command: { resume: response } })
  5. Agent 接收决策:LangGraph 后端传递决策结果给 Agent
  6. Agent 恢复执行
    • 批准:按原参数执行动作
    • 拒绝:接收拒绝原因,调整流程(如重新询问用户)
    • 编辑:按修改后的参数执行动作
  7. 前端重置状态stream.interrupt 变为 null,审批卡片消失,继续流式渲染后续内容

支持多轮中断:一个 Agent 流程中可插入多个 HITL 检查点(如先审批搜索权限,再审批邮件发送)


7. 常见应用场景

HITL 适用于所有需要人工校验的高风险操作,以下是典型场景及配置:

应用场景 动作名称(action) 允许的决策类型(allowedDecisions) 核心参数(args)
邮件发送 send_email ["approve", "reject", "edit"] { to: string[], subject: string, body: string }
数据库记录修改 update_record ["approve", "reject"] { table: string, id: string, data: Record<string, unknown> }
资金转账 transfer_funds ["approve", "reject"] { amount: number, to: string, currency: string }
文件删除 delete_files ["approve", "reject"] { filePaths: string[], force: boolean }
外部 API 调用 call_external_api ["approve", "reject", "edit"] { url: string, method: string, params: Record<string, unknown> }

8. 多待办动作处理方案

Agent 可能同时触发多个待审批动作(如批量删除多个文件),此时需渲染多个审批卡片,收集所有决策后统一提交:

8.1 多动作审批组件

import { useState } from "react";
import type { HITLRequest, HITLResponse } from "@langchain/react/dist/types";
import { SingleActionCard } from "./SingleActionCard"; // 单个动作审批卡片(复用 5 节逻辑)

interface MultiActionReviewProps {
  interrupt: HITLRequest;
  onRespond: (responses: HITLResponse[]) => void; // 接收多个决策结果
}

export function MultiActionReview({ interrupt, onRespond }: MultiActionReviewProps) {
  // 存储每个动作的决策结果(key: 动作索引,value: 决策)
  const [decisions, setDecisions] = useState<Record<number, HITLResponse>>({});
  const { actionRequests, reviewConfigs } = interrupt;

  // 检查是否所有动作都已决策
  const isAllDecided = Object.keys(decisions).length === actionRequests.length;

  // 处理单个动作的决策
  const handleActionDecide = (index: number, response: HITLResponse) => {
    setDecisions((prev) => ({ ...prev, [index]: response }));
  };

  // 提交所有决策
  const handleSubmitAll = () => {
    // 按动作顺序整理决策结果
    const responses = actionRequests.map((_, index) => decisions[index]);
    onRespond(responses);
  };

  return (
    <div className="rounded-lg border-2 border-amber-400 bg-amber-50 p-5 mb-4 shadow-md">
      <div className="flex items-center gap-2 mb-4">
        <span className="inline-block w-3 h-3 rounded-full bg-amber-500 animate-pulse"></span>
        <h3 className="text-lg font-semibold text-amber-800">
          批量动作审核(共 {actionRequests.length} 个动作)
        </h3>
      </div>

      {/* 渲染每个动作的审批卡片 */}
      <div className="space-y-4 mb-4">
        {actionRequests.map((action, index) => (
          <SingleActionCard
            key={index}
            action={action}
            config={reviewConfigs[index]}
            onDecide={(response) => handleActionDecide(index, response)}
            isDecided={!!decisions[index]} // 标记是否已决策
          />
        ))}
      </div>

      {/* 所有动作决策完成后,显示提交按钮 */}
      {isAllDecided && (
        <button
          className="rounded bg-green-600 px-6 py-2 text-white hover:bg-green-700 transition-colors"
          onClick={handleSubmitAll}
        >
          提交所有决策
        </button>
      )}
    </div>
  );
}

8.2 单个动作子卡片(SingleActionCard)

import { useState } from "react";
import type { ActionRequest, ReviewConfig, HITLResponse } from "@langchain/react/dist/types";

interface SingleActionCardProps {
  action: ActionRequest;
  config: ReviewConfig;
  onDecide: (response: HITLResponse) => void;
  isDecided: boolean; // 是否已决策
}

export function SingleActionCard({ action, config, onDecide, isDecided }: SingleActionCardProps) {
  const [mode, setMode] = useState<"review" | "reject" | "edit">("review");
  const [editedArgs, setEditedArgs] = useState(JSON.stringify(action.args, null, 2));
  const [rejectReason, setRejectReason] = useState("");
  const [isInvalidJson, setIsInvalidJson] = useState(false);

  // 验证 JSON 格式(复用 5 节逻辑)
  const validateEditedArgs = () => { /* ... */ };

  // 决策处理函数(复用 5 节逻辑)
  const handleApprove = () => { /* ... */ };
  const handleReject = () => { /* ... */ };
  const handleEditSubmit = () => { /* ... */ };

  return (
    <div className={`rounded-lg bg-white p-4 border ${isDecided ? "border-green-300" : "border-gray-300"}`}>
      {/* 动作标题 + 已决策标记 */}
      <div className="flex items-center justify-between mb-2">
        <h4 className="font-medium">{action.action}</h4>
        {isDecided && (
          <span className="text-xs text-green-600">已决策</span>
        )}
      </div>

      {/* 参数展示/编辑 + 决策按钮 */}
      {/* (复用 ApprovalCard 中的参数展示、模式切换、按钮逻辑) */}
      {/* ... 此处省略重复代码,直接复用 5 节中的对应逻辑 ... */}
    </div>
  );
}

9. 生产环境最佳实践

  1. 清晰展示上下文:必须显示动作名称、描述、完整参数,让用户明确「Agent 要做什么」
  2. 简化批准流程:批准操作应一步完成,复杂流程(拒绝/编辑)放在次要位置
  3. 参数校验不可少:编辑参数时必须校验 JSON 格式,显示明确的错误提示
  4. 持久化中断状态:用户刷新页面后,中断状态应保留(useStream 已通过线程 checkpoint 实现)
  5. 审计日志:记录所有决策(谁、何时、批准/拒绝/编辑了哪个动作),用于合规审计
  6. 超时处理:避免 Agent 无限期等待人工决策,设置超时时间(如 24 小时),超时后自动拒绝
  7. 权限控制:高敏感操作(如资金转账)应限制审批人权限,仅授权用户可审批
  8. 批量动作优化:多动作审批时,支持「全选批准/全选拒绝」,提升操作效率
  9. 错误降级:Agent 中断失败时,前端应显示友好提示,支持手动重试或取消
  10. 响应式设计:审批卡片需适配移动端,确保参数展示和操作按钮不溢出

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐