当前位置: 首页 > news >正文

20、LangChain 前端:模式 => 人工审核

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

文章目录

    • 1. HITL 工作原理
    • 2. 环境搭建:useStream 配置
      • 2.1 类型定义(TypeScript)
      • 2.2 基础流式组件(React 示例)
    • 3. 核心概念:中断负载(Interrupt Payload)
      • 3.1 类型结构
      • 3.2 字段说明
    • 4. 三大决策类型:批准/拒绝/编辑
      • 4.1 批准(Approve)
      • 4.2 拒绝(Reject)
      • 4.3 编辑(Edit)
    • 5. 核心组件:审批卡片(ApprovalCard)实现
      • 组件特性
    • 6. 工作流:中断恢复流程
    • 7. 常见应用场景
    • 8. 多待办动作处理方案
      • 8.1 多动作审批组件
      • 8.2 单个动作子卡片(SingleActionCard)
    • 9. 生产环境最佳实践

1. HITL 工作原理

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

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

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


2. 环境搭建:useStream 配置

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

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

2.1 类型定义(TypeScript)

importtype{BaseMessage}from"@langchain/core/messages";// 匹配 Agent 状态结构的接口interfaceAgentState{messages:BaseMessage[];// 消息列表(包含 AI 消息、人类消息)}// HITL 核心类型(从 LangChain 导入或自定义)importtype{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 类型结构

interfaceHITLRequest{actionRequests:ActionRequest[];// 待审批动作列表reviewConfigs:ReviewConfig[];// 审批配置(允许的决策类型)}interfaceActionRequest{action:string;// 动作名称(如 "send_email"、"delete_record")args:Record<string,unknown>;// 动作参数(结构化数据)description?:string;// 动作描述(人类可读)}interfaceReviewConfig{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)

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

// 决策格式constapproveResponse:HITLResponse={decision:"approve",// 决策类型};// 提交决策(触发 Agent 继续执行)stream.submit(null,{command:{resume:approveResponse}});

4.2 拒绝(Reject)

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

// 决策格式constrejectResponse:HITLResponse={decision:"reject",reason:"转账金额超出限额,请核实后重新提交",// 可选拒绝原因};// 提交决策stream.submit(null,{command:{resume:rejectResponse}});

4.3 编辑(Edit)

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

// 原始参数(从 action.args 获取)constoriginalArgs={amount:1000,to:"user123"};// 编辑后的参数consteditedArgs={...originalArgs,amount:800,// 修改金额remark:"月度补贴",// 新增参数};// 决策格式consteditResponse: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. 响应式设计:审批卡片需适配移动端,确保参数展示和操作按钮不溢出

http://www.jsqmd.com/news/536692/

相关文章:

  • 探索Comsol中的奇妙光学现象:远场偏振图、能带图与本征手性观察
  • 避坑指南:在Ubuntu 20.04上搞定VINS-Fusion依赖(Ceres、Eigen、gflags报错全解决)
  • Vue3 + TypeScript 类型工具封装与复用:从重复到高效,让你的代码类型安全又优雅
  • 2026年热门的深圳AI搜索推广靠谱公司推荐 - 品牌宣传支持者
  • PLC、上位机、下位机与嵌入式系统:工业自动化中的角色定位与协同应用
  • nanobot镜像深度优化:OpenClaw启动时间缩短70%
  • OpenClaw技能扩展:基于nanobot镜像开发自定义自动化工作流
  • PaunaStepper库详解:28BYJ-48步进电机精准控制实战
  • 实战指南:如何用Python绘制强化学习中的Reward曲线(无阴影版)
  • 突破组织变革困境:两本不可错过的实战书籍推荐
  • OpenClaw对接ollama GLM-4.7-Flash实战:本地AI助手自动化配置指南
  • CMake的find_package机制详解:为什么你的ROS2项目总提示找不到serial库?
  • 无GPU方案:OpenClaw调用云端百川2-13B-4bits模型API实战
  • 自动化思维培养:OpenClaw+GLM-4.7-Flash解决日常问题的10个案例
  • 计算机毕设 java 基于 Android 的 “课堂管理助手” 移动应用开发 SpringBoot 安卓智能课堂管理移动应用 JavaAndroid 师生互动与教学管理平台
  • 零刻EQ12/EQ12Pro原厂系统安装全攻略:从U盘制作到一键安装(附资源下载)
  • 百川2-13B量化版调优指南:提升OpenClaw任务成功率的关键参数
  • 别再到处找了!2013到2018年亚马逊评论数据集最全下载与使用指南
  • 避坑指南:海康SDK+JNA开发中那些意想不到的Structure陷阱
  • OpenClaw进阶配置:GLM-4.7-Flash模型参数调优实战
  • 一键切换模型:OpenClaw快速对比nanobot与Qwen3-32B效果
  • 为什么顶尖量化团队集体弃用Pandas?Polars 2.0清洗基准测试结果刚解禁(含12类真实业务场景压测数据)
  • palera1n越狱完全解决方案:突破iOS 15.0+设备限制的实战指南
  • OpenClaw自动化测试报告:GLM-4.7-Flash生成可视化结果
  • 告别弹窗!保姆级SecureCRT 9.x 永久激活教程(附防火墙设置与注册机使用避坑指南)
  • OpenClaw实战案例:Qwen3.5-9B自动化处理电商客服问答
  • ChatGPT Pro版充值技术解析:从API接入到支付安全的最佳实践
  • ChatTTS 本地部署性能优化实战:从生成缓慢到高效推理的解决方案
  • OpenClaw监控告警:GLM-4.7-Flash任务异常自动通知设置
  • YOLO系列实战指南:从v1到v9,如何选择最适合你的目标检测模型?