news 2026/9/12 3:26:34

CopilotKit 应用级 Human-in-the-Loop 实战:基于前端工具实现应用级审批弹窗(Claude Agent SDK / TypeScript)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CopilotKit 应用级 Human-in-the-Loop 实战:基于前端工具实现应用级审批弹窗(Claude Agent SDK / TypeScript)

CopilotKit 应用级 Human-in-the-Loop 实战:基于前端工具实现应用级审批弹窗(Claude Agent SDK / TypeScript)

【免费下载链接】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 智能体中,当 Agent 需要执行退款、降级套餐、升级工单等高风险操作时,必须把决策权交还给用户——这就是 Human-in-the-Loop(HITL)。本篇文章以 CopilotKit 仓库中 Claude Agent SDK (TypeScript) 集成示例的hitl-in-app演示为核心,讲解如何通过useFrontendTool注册一个异步阻塞的前端工具,在应用层(而非聊天流内部)弹出一个审批模态框,并将 Approve / Reject 的结果以工具返回值的形式交还给 Agent。读完本文,你将掌握应用级审批弹窗的完整实现链路、后端运行时配置,以及对应的 QA 验证方法与端到端测试断言。

一、什么是“In-App”级 HITL:弹窗在聊天之外

CopilotKit 提供两种 HITL 呈现形态:

  • In-Chat(聊天内):审批 UI 渲染在聊天气泡树内部,属于对话流的一部分;
  • In-App(应用级):审批 UI 以应用级模态框的形式渲染在聊天界面之外,例如createPortal挂载到document.body

本演示对应的 QA 文档位于 qa/hitl-in-app.md,其验收标准明确写道:“Verify the approval dialog renders OUTSIDE the chat (app-level modal)”。应用级模态框的价值在于:审批动作与聊天记录解耦,用户可以一边查看工单面板一边决策,而 Agent 在审批完成之前会一直阻塞等待——这正是异步前端工具(async frontend tool)的核心语义。

二、前置条件

QA 文档给出两条运行前提,它们对复现与验证都至关重要:

  1. Demo 已部署且可访问(Demo is deployed and accessible):即/demos/hitl-in-app页面能够正常打开;
  2. Agent 后端健康(Agent backend is healthy):CopilotKit 运行时需要能够连通 Claude Agent 后端进程。

在 运行时路由 中,GET健康探针会主动请求${AGENT_URL}/health(默认http://localhost:8000),并返回agent_statusANTHROPIC_API_KEY是否设置的诊断信息,可用于快速确认前提 2 是否满足。

三、核心原理:异步前端工具如何阻塞 Agent

整个 In-App HITL 的基石是useFrontendTool。在 packages/react-core/src/v2/hooks/use-frontend-tool.tsx 中可以看到它的行为:组件挂载时通过copilotkit.addTool(tool)将工具注册进 CopilotKit 运行时;卸载时移除;若同名工具已存在会先移除再覆盖注册。也就是说,工具的“存在性”完全由前端页面决定,后端 Agent 通过 AG-UI 协议透明地把工具调用转发给前端。

Demo 页面 src/app/demos/hitl-in-app/page.tsx 注册了唯一的审批工具request_user_approval

useFrontendTool({ name: "request_user_approval", description: "Ask the operator to approve or reject an action before you take it. " + "The operator will respond via an in-app modal dialog that appears " + "OUTSIDE the chat surface. The tool returns an object of the shape " + "{ approved: boolean, reason?: string }.", parameters: z.object({ message: z .string() .describe( "Short summary of the action needing approval (include concrete numbers / IDs).", ), context: z .string() .optional() .describe( "Optional extra context — e.g. the ticket ID or policy rule.", ), }), handler: async ({ message, context }) => { return await new Promise<{ approved: boolean; reason?: string }>( (resolve) => { setDialog((current) => { if (current.open) { resolve({ approved: false, reason: "Another approval request is already pending.", }); return current; } return { open: true, pending: { message, context }, resolve }; }); }, ); }, });

关键设计有四点:

  1. 参数 Schema 用 zod 声明message(必须,包含具体数字 / ID 的操作摘要)与context(可选,如工单 ID 或策略规则)。Agent 会依据description决定何时调用该工具。
  2. Handler 返回一个悬而未决的 Promiseresolve被存进组件 state(DialogState),弹窗里用户点击 Approve / Reject 时才调用它,从而“解封”handler,把结果作为工具返回值交还给 Agent。
  3. 并发防护:如果已有审批弹窗打开(current.open为真),新请求会被立即拒绝,返回approved: false与原因 “Another approval request is already pending.”,保证同一时刻只有一个挂起的审批。
  4. 返回值契约{ approved: boolean, reason?: string },reason 可选,用户填写的备注会透传给 Agent。

从源码结构看,DialogState被建模为可辨识联合类型{ open: false } | { open: true; pending: PendingApproval; resolve: ResolveFn },其中ResolveFn正是从 Promise 捕获到的完成函数——这是“前端 Promise ↔ 模态框 ↔ Agent 工具结果”三者之间的唯一接线点。

四、审批弹窗:Portal 到<body>的应用级模态

审批弹窗组件位于 approval-dialog.tsx,它的核心是最后一行的createPortal(content, document.body)

export function ApprovalDialog({ pending, onResolve }: Props) { const [reason, setReason] = useState(""); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); if (!mounted) return null; const content = ( <div >const handleResolve = (result: { approved: boolean; reason?: string }) => { if (dialog.open) { dialog.resolve(result); setDialog({ open: false }); } };

至此闭环完成:用户点击 → resolve 被调用 → handler 的 Promise 完成 → 工具结果经运行时回传 Agent → Agent 依据approved分支决策

五、后端与运行时:透传式 Claude HttpAgent

与“后端拥有工具”的演示不同,本演示采用透传(pass-through)后端。在 claude-http-agent.ts 中,createClaudeHttpAgent只是new HttpAgent(...)的封装,本身不声明任何工具:

export function createClaudeHttpAgent(url: string): HttpAgent { return new HttpAgent(claudeHttpAgentConfig(url)); }

运行时路由 的注释对此解释得很清楚:这个后端“转发 AG-UI 客户端提供的任何工具(前端通过useFrontendTool/useRenderTool注册的,以及运行时注入的)给 Claude。因此不同 demo 的行为差异来自前端,而不是每个 demo 一个后端图”。hitl-in-app被注册为共享 agent 之一,AGENT_URL默认指向http://localhost:8000的独立 TypeScript 进程;createCopilotRuntimeHandlersingle-route模式、basePath: "/api/copilotkit"承载请求。也就是说,只要后端健康且提供 Anthropic 密钥,审批工具的存在与行为完全由前端页面驱动——这也解释了 QA 前置条件为何只要求“demo 可访问 + 后端健康”。

Demo 页面还通过 suggestions.ts 的useConfigureSuggestions预置了三条建议卡片(pill),分别对应三个工单:

  • “Approve refund for #12345”——批准 Jordan Rivera 的 $50 重复扣款退款;
  • “Downgrade plan for #12346”——将 Priya Shah 降级到 Starter 套餐;
  • “Escalate ticket #12347”——将 Morgan Lee 卡在 pending 的支付工单升级给支付团队。

工单数据是硬编码在 tickets-panel.tsx 的SUPPORT_TICKETS中,为 Agent 提供了“真实可见”的上下文,方便测试者一键触发审批。

六、QA 测试步骤与验证要点

以下按 QA 文档逐条展开,并补充每个断言对应的实现依据:

  1. 导航到/demos/hitl-in-app页面由HitlInAppDemo渲染,外层以<CopilotKit runtimeUrl="/api/copilotkit" agent="hitl-in-app">包裹,内层是Layout+CopilotPopupagentId="hitl-in-app"defaultOpen为 true)和工单面板。E2E 测试beforeEach中执行page.goto("/demos/hitl-in-app")后,会断言三个工单卡片(ticket-12345/ticket-12346/ticket-12347)、聊天输入框(占位符 “Type a message”)可见,且初始状态下approval-dialog-overlay数量为 0——即没有任何审批弹窗。

  2. 让 Agent 执行需要审批的动作点击任一建议卡片即可。例如点击 “Approve refund for #12345” 后,Agent 会调用request_user_approval工具。三条建议卡片均被 E2E 断言为可见(见 tests/e2e/hitl-in-app.spec.ts),且卡片文本精确引用各工单。

  3. 验证审批弹窗渲染在聊天之外(应用级模态框)这是本用例的独有断言。测试用body > [data-testid="approval-dialog-overlay"]定位弹窗,确认它是<body>的直接子节点——这正是createPortal的契约。同时断言approval-dialog与可选备注输入框approval-dialog-reason可见。QA 文档要求“dialog renders OUTSIDE the chat”,E2E 用 DOM 层级把这一条固化成可自动验证的标准。

  4. 批准操作,验证 Agent 按用户决定继续点击approval-dialog-approve后,断言弹窗在 5 秒内消失,然后断言助手消息包含分支专属的前导句,例如批准退款 #12345 后出现 “I am processing the $50 refund”。这里体现了分支语义:不同结果走不同的确定性夹具分支(fixture branch),消息文本与 approve/reject 一一对应。

  5. 重复执行并拒绝,验证 Agent 尊重拒绝结果对同一建议卡片再点一次,这次点击approval-dialog-reject,随后助手消息应包含拒绝分支的前导句,例如 “refund request was not approved”。升级工单场景同理:批准后出现 “Escalated ticket #12347”,拒绝后出现 “Not escalated ...”。

  6. 验证无控制台错误QA 文档要求 “Verify no console errors”,对应 Playwright 默认收集页面 console / pageerror 的机制,测试全程无 UI 报错即通过。

预期结果(Expected Results)与自动化对应

QA 文档的三条预期结果,逐一对应测试断言:

  • 异步前端工具阻塞直到用户解决模态框(Async frontend tool blocks until user resolves the modal):handler 返回的 Promise 悬而未决,E2E 用 60 秒超时等待body > [data-testid="approval-dialog-overlay"]出现,再等待点击后弹窗消失,验证了“先阻塞、后解封”的时序;
  • Agent 结果在批准与拒绝之间不同(Agent outcome differs between approve and reject):同一建议卡片跑两条测试,approve 分支断言 “processing the $50 refund”,reject 分支断言 “not approved”——文本不对称意味着至少一个分支会失败,从而证明结果确实由用户选择驱动;
  • 无 UI 错误(No UI errors):见上一条。

七、测试设计中的工程细节

理解 E2E 测试的断言策略,能帮你更稳地复现与验证该演示:

  1. 串行模式(serial mode)是承重的:夹具匹配器(aimocksequenceIndex)在整个进程内计数,approve 测试(sequenceIndex 0)必须严格先于 reject 测试(sequenceIndex 1)运行,因此test.describe.configure({ mode: "serial" })必不可少;
  2. 多任务并发审批回归:测试 “approve refund then click escalate — each pill mounts its own approval dialog” 验证了连点两个建议卡片时,每个 pill 都会挂载自己独立的审批弹窗(分别指向 #12345 与 #12347)。这正是页面中if (current.open) resolve({ approved: false, ... })并发保护在单线程场景下的正确行为;
  3. 已注释的降级用例:文件中downgrade #12346的用例被有意跳过(上游 demo 在降级提示词下未稳定触发request_user_approval),注释明确了“待上游修复后重新启用”——这是仓库对已知上游缺陷的诚实标注,复现时不必把它当作失败;
  4. hasToolResult回归教训:注释中记录了 aimock 多 pill 缺陷的修复——通过request_user_approvaltoolCallId链式串联后续夹具,并去掉工具发射夹具上的hasToolResult: false,避免第二个 pill 在首次工具结果已存在时被跳过。这条历史经验提醒我们:HITL 场景中夹具对工具调用的建模必须考虑线程已有的工具结果状态。

八、可复用的实现模式小结

从本演示可以提炼出一个通用的“应用级审批”模式,共五步:

  1. 注册工具:用useFrontendTool声明工具名、语义化description、zod 参数 Schema 与 async handler;
  2. 捕获 resolve:handler 返回new Promise((resolve) => { setDialog(...) }),把resolve存入 state;
  3. 渲染应用级 UI:用createPortal(content, document.body)将审批模态框挂到<body>,脱离聊天树;
  4. 用户决策:模态框的 Approve / Reject 回调调用onResolve({ approved, reason? }),最终触发dialog.resolve(...)
  5. Agent 分支决策:运行时把{ approved, reason? }作为工具结果回传,Agent 依据approved字段继续执行或终止操作。

整个过程对用户透明、对 Agent 阻塞、对前端可控,配合 hitl-in-app.spec.ts 的断言矩阵(approve/reject × 多个工单 × 多任务串行),足以作为任何需要“高风险操作必须人工确认”的智能体前端(客服退款、审批流、运维变更等)的落地蓝本。若要进一步探索聊天内 HITL 的差异实现,可对照同一目录下的 qa/hitl-in-chat.md 及其对应的tests/e2e/hitl-in-chat.spec.ts

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 3:24:30

STM32嵌入式AI编程:开发流程断点与人工校验红线

1. 这不是“用AI写代码”&#xff0c;而是重构嵌入式开发的认知边界我第一次在Keil里把AI生成的UART初始化函数直接粘贴进工程时&#xff0c;编译器报了17个错误——不是语法错&#xff0c;是硬件抽象层&#xff08;HAL&#xff09;版本不匹配、时钟树配置冲突、GPIO复用功能未…

作者头像 李华
网站建设 2026/9/12 3:24:09

COMSOL偶极子天线散射体仿真:远场方向图畸变与参数分析

做射频仿真的人&#xff0c;十有八九会遇到这么个情况&#xff1a;天线单独仿真时指标漂亮得不行&#xff0c;一放进整机里就立马翻脸。增益掉了、方向图歪了、谐振点飘了&#xff0c;怎么看怎么别扭。最典型的困扰之一&#xff0c;就是天线旁边多了个金属散射体之后&#xff0…

作者头像 李华
网站建设 2026/9/12 3:23:38

Fay 数字人框架:从克隆到跑通只需 3 步,把大模型接进数字人

Fay 数字人框架&#xff1a;从克隆到跑通只需 3 步&#xff0c;把大模型接进数字人 【免费下载链接】Fay fay是一个帮助数字人&#xff08;2.5d、3d、移动、pc、网页&#xff09;或大语言模型&#xff08;openai兼容、deepseek&#xff09;连通业务系统的agent框架。 项目地址…

作者头像 李华
网站建设 2026/9/12 3:23:20

OBS训练营:技术人才培养的创新实践与效果评估

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 3:21:23

VSCode+Continue+Ollama:本地化AI编程助手全栈开发实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 3:20:52

激光开袋机技术解析:YC-03系列如何变革服装开袋工序

干服装制造这行的人都知道&#xff0c;开袋是上衣和裤装生产里最考验车工手艺的工序之一。袋口要直、袋角要方正、唇边要齐、缝线要均匀&#xff0c;稍有偏差整件衣服就成了B品。传统工艺靠熟手车工凭手感操作&#xff0c;培训周期长、质量波动大&#xff0c;遇到换款换袋型又是…

作者头像 李华