资讯动态

CopilotKit 人工介入(Human-in-the-Loop)实战指南:用 useHumanInTheLoop 为 Agent 工具调用加装审批闸门

发布时间:2026/9/10 2:10:13 来源:尧图企业网站定制
CopilotKit 人工介入Human-in-the-Loop实战指南用 useHumanInTheLoop 为 Agent 工具调用加装审批闸门【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKitCopilotKit 的useHumanInTheLoop是构建生成式 UI 应用时最关键的安全阀它把浏览器端工具useFrontendTool的handler拿掉换成一个由respond(result)驱动、基于 Promise 的合成处理器从而让 Agent 发起的破坏性操作删除、部署、写库等必须等待用户确认后才能继续。本文以 react-core 技能包中的 human-in-the-loop 参考文档 为主体结合copilotkit/react-core/v2的源码与端到端测试完整讲解该 hook 的状态模型、典型实现模式与全部常见陷阱读完后你可以在自己的 React 应用中直接落地人工审批闸门运行中收集结构化输入等能力。本文对应的技能建立在copilotkit/provider-setup、copilotkit/client-side-tools与copilotkit/rendering-tool-calls三个技能之上因此建议先掌握 Provider 挂载 与 浏览器端工具注册再阅读本文。核心模型useHumanInTheLoop 的本质一句话概括useHumanInTheLoop就是useFrontendTool减去handler再加一个接收respond函数的render。hook 内部会合成一个基于 Promise 的处理器——这个 Promise 只有在调用respond(result)时才会 resolve如果没有任何地方调用respond则该 Promise 永远不会 resolveAgent 运行会无限挂起服务端线程被锁死。状态机与 respond 的生命周期工具调用的status采用camelCase枚举inProgress | executing | complete状态含义argsresultrespondinProgress工具调用参数仍在流式传输中PartialT可能不完整undefinedundefinedexecuting参数就绪等待用户决策T完整参数undefined(result: unknown) Promisevoidcomplete已调用respond(result)Promise 已 resolveTstring决策结果序列化后的字符串undefined关键约束respond在除executing之外的所有状态下都是undefined。这一约定被完整编码在 human-in-the-loop 类型定义 的可辨识联合discriminated union中export type ReactHumanInTheLoop T extends Recordstring, unknown Recordstring, unknown, OmitFrontendToolT, handler { render: React.ComponentType | { args: PartialT; status: ToolCallStatus.InProgress; result: undefined; respond: undefined } | { args: T; status: ToolCallStatus.Executing; result: undefined; respond: (result: unknown) Promisevoid } | { args: T; status: ToolCallStatus.Complete; result: string; respond: undefined } ; };从这份类型可以看到render组件在三个状态下接收完全不同的 props 契约——TypeScript 会强制你在executing分支里才使用respond这正是不要在错误状态调用 respond的编译期防线。与 useFrontendTool 相同的注册语义useHumanInTheLoopT(tool, deps?)的第二个可选参数deps与useFrontendTool完全一致只有当tool.name、tool.available或deps中任一元素变化时hook 才会重新注册。如果render闭包引用了组件内的 React 状态必须把该状态放入deps否则渲染器会捕获首次挂载时的旧值详见 客户端工具文档 中对 stale closure 的讨论。快速上手一个完整的删除确认DeleteConfirmHITL最典型的应用场景是破坏性操作确认。下面的组件注册了一个名为confirmDelete的人工介入工具当 Agent 调用它时界面弹出一个基于shadcn AlertDialog的确认框use client; import { useHumanInTheLoop } from copilotkit/react-core/v2; import { z } from zod; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from /components/ui/alert-dialog; export function DeleteConfirmHITL() { useHumanInTheLoop({ name: confirmDelete, description: Confirm a destructive delete with the user, parameters: z.object({ id: z.string(), label: z.string() }), render: ({ status, args, respond }) ( AlertDialog open AlertDialogContent AlertDialogHeader AlertDialogTitleDelete {args.label}?/AlertDialogTitle AlertDialogDescription This action cannot be undone. /AlertDialogDescription /AlertDialogHeader AlertDialogFooter AlertDialogCancel disabled{status ! executing} onClick{() respond?.(denied)} Cancel /AlertDialogCancel AlertDialogAction disabled{status ! executing} onClick{() respond?.(approved)} Delete /AlertDialogAction /AlertDialogFooter /AlertDialogContent /AlertDialog ), }); return null; }注意两个细节按钮在非executing状态被禁用——respond只在executing时存在未执行时按钮不应可点击parameters使用 zod schema 声明——zod是copilotkit/react-core的硬性 peer dependency必须随包一起安装pnpm install copilotkit/react-core zod。UI-kit 探测规则先检查再用在编写任何审批 UI 之前先检查消费者应用的package.json中是否已有 UI 组件库并优先复用其对话框原语UI Kit对话框原语shadcncomponents/ui/*AlertDialogMUImui/materialDialogChakrachakra-ui/reactModalAnt DesignantdModalMantinemantine/coreModal如果应用已有这些原语却手写遮罩层会丢失这些组件内置的焦点陷阱focus trapping、Esc 关闭与无障碍语义详见下文常见错误。三种核心实现模式模式一每一个分支都必须调用 respond无论用户点了批准拒绝还是跳过渲染逻辑的每个分支最终都要把决策通过respond传回给 Agentrender: ({ status, args, respond }) { if (status ! executing || !respond) { return divAwaiting decision…/div; } return ( div button onClick{() respond(approved)}Approve/button button onClick{() respond(denied)}Reject/button button onClick{() respond({ action: skip, reason: timeout })} Skip /button /div ); };respond的入参可以是任意值——字符串、布尔值或包含多个字段的对象。CopilotKit 会将该值序列化后作为工具调用结果返回给 Agent因此你可以把跳过原因这类结构化信息一并传回去让 Agent 的下一步行动拥有足够上下文。模式二组件卸载时 abort run释放被锁住的线程HITL 提示 UI 如果放在会被路由切换卸载的组件里一旦用户在 Agent 等待respond时离开页面pending 的 Promise 就会被遗弃运行将永久挂起。解决思路有两个把 HITL UI 提升到跨路由持久存在的布局层或者在宿主组件卸载时主动agent.abortRun()。后者的推荐实现使用 ref 记录运行状态避免清理函数在每次状态翻转时都触发import { useAgent, UseAgentUpdate } from copilotkit/react-core/v2; import { useEffect, useRef } from react; function HITLHost() { const { agent } useAgent({ agentId: default, updates: [UseAgentUpdate.OnRunStatusChanged], }); // Track isRunning in a ref so the unmount cleanup reads the latest value // without re-firing on every transition. const runningRef useRef(false); useEffect(() { runningRef.current agent.isRunning; }, [agent.isRunning]); useEffect(() { return () { if (runningRef.current) agent.abortRun(); }; }, [agent]); return DeleteConfirmHITL /; }这里的要点是useAgent解构出的{ agent }上挂着agent.isRunning。不要把卸载清理 effect 直接依赖在agent.isRunning上——那会让清理函数在每次运行状态翻转而非仅卸载时执行从而误杀正在进行的运行。ref 模式在清理只在真正卸载时触发的同时仍能读到最新的运行状态。模式三运行中收集结构化用户输入HITL 不仅用于批准/拒绝还可以在 Agent 运行途中向用户收集结构化数据。下面的例子让用户在low / medium / high三档优先级中做出选择并把选择结果连同原始taskId一起作为工具结果返回useHumanInTheLoop({ name: askUserForPriority, parameters: z.object({ taskId: z.string() }), render: ({ status, args, respond }) { if (status ! executing || !respond) return divWaiting…/div; return ( div {[low, medium, high].map((p) ( button key{p} onClick{() respond({ taskId: args.taskId, priority: p })} {p} /button ))} /div ); }, });由于args在executing阶段已经是完整的T类型你可以安全地读取args.taskId并把它原样带回结果中从而让 Agent 明确这个决策是针对哪个任务。常见错误清单按严重程度排序CRITICAL —— 永远不调用 respond()错误写法渲染了确认框但按钮的点击事件没有绑定responduseHumanInTheLoop({ name: confirmDelete, parameters: z.object({ id: z.string() }), render: ({ args, status, respond }) ( div pDelete {args.id}?/p buttonOK/button /div ), });正确写法每个交互路径都调用respond包括拒绝/取消路径useHumanInTheLoop({ name: confirmDelete, parameters: z.object({ id: z.string() }), render: ({ args, status, respond }) ( div pDelete {args.id}?/p button onClick{() respond?.(approved)}OK/button button onClick{() respond?.(denied)}Cancel/button /div ), });合成处理器返回的 Promise只在respond被调用时 resolve。永远不调用包括拒绝/取消路径会导致运行无限期挂起、服务端线程被锁死。参见 use-human-in-the-loop.tsx 中的respond回调与 Promise 合成逻辑。CRITICAL —— 应用已有 Dialog 原语时手写自定义遮罩错误写法用内联样式手写全屏遮罩render: ({ respond }) ( div style{{ position: fixed, inset: 0, background: rgba(0,0,0,0.5) }} … /div );正确写法复用应用已安装的对话框原语import { AlertDialog, AlertDialogContent, AlertDialogAction, } from /components/ui/alert-dialog; render: ({ respond }) ( AlertDialog open AlertDialogContent … AlertDialogAction onClick{() respond?.(approved)} OK /AlertDialogAction /AlertDialogContent /AlertDialog );在写遮罩之前先检查package.json里是否有 shadcn / MUI / Chakra / Ant / Mantine。它们的对话框原语自带焦点陷阱、Esc 关闭和无障碍支持——裸 JSX 会把这些能力全部跳过同时产出自定义样式不一致的界面。HIGH —— 在 inProgress 或 complete 状态下调用 respond错误写法把respond强转成any后无条件使用render: ({ status, respond }) ( button onClick{() (respond as any)(yes)}Yes/button );正确写法只在executing状态下渲染可交互按钮render: ({ status, respond }) status executing respond ? ( button onClick{() respond(yes)}Yes/button ) : ( pWaiting…/p );respond在非executing状态下是undefined。将其强转成any会静默空操作no-op——按钮点击看似有效但没有任何东西去 resolve 那个 Promise。该约束的类型定义见 human-in-the-loop.ts。HIGH —— 在 executing 中途卸载渲染器错误写法用户正在等待respond()时切换到其他路由HITL 组件随之卸载// User clicks away to a different route while the agent is waiting on respond()正确写法把 HITL 提示放在跨路由持久存在的布局层或者卸载时 abort run// Keep the HITL prompt at a layout level that persists across route changes, OR abort on unmount: const { agent } useAgent({ agentId: default, updates: [UseAgentUpdate.OnRunStatusChanged], }); const runningRef useRef(false); useEffect(() { runningRef.current agent.isRunning; }, [agent.isRunning]); useEffect( () () { if (runningRef.current) agent.abortRun(); }, [agent], );与useFrontendTool渲染器会为历史消息保留不同useHumanInTheLoop在卸载时会移除自己的渲染器参见 use-human-in-the-loop.tsx 中的清理逻辑。如果渲染器在executing中途卸载pending 的 Promise 会被遗弃、运行挂起。要么把 HITL UI 提升到布局层组件要么在卸载时 abort 运行。MEDIUM —— 使用连字符形式 in-progress 状态值错误写法render: ({ status }) (status in-progress ? Spinner / : Form /);正确写法render: ({ status }) (status inProgress ? Spinner / : Form /);与rendering-tool-calls中同样的 camelCase 规则状态可辨识联合只匹配inProgress | executing | complete三种字面量写错字符串会永远落入else分支。源码级原理Promise 如何被合成、状态如何驱动渲染阅读 use-human-in-the-loop.tsx 可以看清整个机制respond回调L23-L30通过resolvePromiseRef保存的 resolve 函数在调用respond(result)时把结果交给 pending 的 Promise并清空 ref 与中止监听器防止重复触发或泄漏。合成处理器L32-L60返回一个new Promise。如果传入的context.signal已被中止会立即 reject 并抛错让 core 记录一个显式的错误工具结果而不是静默 resolve 成空字符串否则把 resolve 存入 ref并为AbortSignal注册一次性abort监听——stopAgent/agent.abortRun()正是通过这条路径中止挂起的 HITL 交互。渲染分派L62-L115按inProgress / executing / complete三个状态分别构建增强 props用注册时的name、description、agentId覆盖传入值且只有executing分支注入真正的respond。特别的当工具以通配名*WILDCARD_TOOL_NAME注册成兜底工具时name保留为实际被调用的工具名这样单个兜底渲染器可以服务 N 个工具。类型穷尽检查L111-L112const exhaustiveCheck: never props会把未来新增的状态枚举变成编译错误强制为新状态补充分支避免静默渲染时没有 respond。卸载清理L135-L139使用useLayoutEffect调用copilotkit.removeHookRenderToolCall(tool.name, tool.agentId)。它必须保持为 layout effect注册发生在useFrontendTool的 layout effect 中若清理是 passive effectkeyed 重挂载会先执行新实例的注册、再执行旧实例的移除把刚注册好的渲染器删掉。进阶能力toolCallId 与 agentId 归因除了文档主线提到的 propsrender还接收两个可用于归因的字段定义在 human-in-the-loop.tstoolCallId—— AG-UI 工具调用 ID每个中断唯一。它是把本次中断与运行时子Agent 归因关联起来的稳定键例如onToolExecutionStart上报的agentId或事件流上按 run 打戳的归因信息UI 可以据此给中断标注到底是哪个 Agent 发起的。agentId—— 工具注册时绑定的 Agent工具自身的agentId未绑定作用域的工具为undefined。注意这是静态的注册作用域并不一定是运行时发起中断的子Agent要做运行时归因请用toolCallId去关联事件流 /onToolExecutionStart中的 Agent ID。该行为在端到端测试中得到了验证use-human-in-the-loop.e2e.test.tsx 中的 HITL attribution (toolCallId agentId) 用例断言作用域工具在渲染 props 中同时拿到正确的toolCallId与agentId: research-agent而未作用域工具的agentId在进入Executing后依然保持undefined。测试验证从端到端用例反推正确行为use-human-in-the-loop.e2e.test.tsx共 1554 行从多个角度固化了本文提到的所有行为是最佳的行为规范参照状态迁移断言 HITL 工具经历InProgress → Executing → Complete的完整旅程且respond之后result中出现用户决策内容交互式响应只有status Executing respond时才渲染响应按钮点击后状态推进到Complete多工具并存同一时刻多个 HITL 工具reviewToolconfirmTool并行注册与渲染多 hook 实例隔离两个useHumanInTheLoop注册互不干扰——点击主工具响应时副工具仍停留在InProgress各自独立推进动态注册/注销HITL 组件卸载后同名工具调用不再渲染任何 UI对应卸载即移除渲染器的语义deps 驱动更新传入[version]作为depssetVersion后渲染器立即捕获新值线程重连带 pending HITL 的线程在刷新重连后仍能显示Executing状态并可响应该用例同时验证了executingToolCallIds在 Provider 层面被提前捕获的修复逻辑。小结useHumanInTheLoop把浏览器端工具注册与人工审批之间的空隙用 Promise 桥接起来Agent 发起调用 → 参数流式到达inProgress→ 完整参数就绪并等待决策executing此时respond可用→ 用户决策回传、Promise resolve、运行继续complete。掌握本文的核心模式全分支调用respond、卸载时 abort、运行中收集结构化输入与五条常见错误规避即可安全地在删除确认、发布审批、权限复核等场景中为 Agent 行为加上人类可控的闸门。建议将本文与 rendering-tool-calls 参考 对照阅读二者在状态命名与 UI-kit 复用规则上保持一致。【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价