20、LangChain 前端:模式 => 人工审核
本文聚焦高风险操作的人工审批场景,提供 React/Vue/Svelte/Angular 全框架适配代码,包含审批卡片、决策流转、多动作处理等核心功能。
文章目录
1. HITL 工作原理
Human-in-the-Loop(人工介入)是针对高风险操作的审批机制,适用于邮件发送、数据删除、资金转账等不可逆操作。核心流程如下:
- Agent 触发中断:当 Agent 要执行高风险操作时,主动暂停执行并发送「中断请求」
- 前端接收中断:
useStream钩子通过stream.interrupt暴露中断信息 - 用户决策:前端渲染审批卡片,提供「批准/拒绝/编辑」选项
- 提交决策:用户操作后,前端调用
stream.submit()提交决策结果 - 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>
);
}
组件特性
- 动态适配:根据
allowedDecisions只渲染允许的决策按钮 - 格式校验:编辑参数时自动校验 JSON 格式,避免无效输入
- 用户体验:分「查看/拒绝/编辑」三种模式,操作流程清晰
- 样式友好:使用语义化颜色(绿色批准、红色拒绝、蓝色编辑),适配聊天场景
6. 工作流:中断恢复流程
完整的 HITL 工作流包含「Agent 中断 → 前端展示 → 用户决策 → Agent 恢复」四个阶段,具体步骤如下:
- Agent 触发中断:执行高风险操作前,Agent 发送
HITLRequest并暂停 - 前端接收中断:
stream.interrupt变为非 null,触发审批卡片渲染 - 用户决策:用户查看动作详情,选择「批准/拒绝/编辑」并提交
- 前端提交决策:调用
stream.submit(null, { command: { resume: response } }) - Agent 接收决策:LangGraph 后端传递决策结果给 Agent
- Agent 恢复执行:
- 批准:按原参数执行动作
- 拒绝:接收拒绝原因,调整流程(如重新询问用户)
- 编辑:按修改后的参数执行动作
- 前端重置状态:
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. 生产环境最佳实践
- 清晰展示上下文:必须显示动作名称、描述、完整参数,让用户明确「Agent 要做什么」
- 简化批准流程:批准操作应一步完成,复杂流程(拒绝/编辑)放在次要位置
- 参数校验不可少:编辑参数时必须校验 JSON 格式,显示明确的错误提示
- 持久化中断状态:用户刷新页面后,中断状态应保留(
useStream已通过线程 checkpoint 实现) - 审计日志:记录所有决策(谁、何时、批准/拒绝/编辑了哪个动作),用于合规审计
- 超时处理:避免 Agent 无限期等待人工决策,设置超时时间(如 24 小时),超时后自动拒绝
- 权限控制:高敏感操作(如资金转账)应限制审批人权限,仅授权用户可审批
- 批量动作优化:多动作审批时,支持「全选批准/全选拒绝」,提升操作效率
- 错误降级:Agent 中断失败时,前端应显示友好提示,支持手动重试或取消
- 响应式设计:审批卡片需适配移动端,确保参数展示和操作按钮不溢出
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)