)
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-LoopHITL。本篇文章以 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 文档给出两条运行前提它们对复现与验证都至关重要Demo 已部署且可访问Demo is deployed and accessible即/demos/hitl-in-app页面能够正常打开Agent 后端健康Agent backend is healthyCopilotKit 运行时需要能够连通 Claude Agent 后端进程。在 运行时路由 中GET健康探针会主动请求${AGENT_URL}/health默认http://localhost:8000并返回agent_status与ANTHROPIC_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_approvaluseFrontendTool({ 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 }; }); }, ); }, });关键设计有四点参数 Schema 用 zod 声明message必须包含具体数字 / ID 的操作摘要与context可选如工单 ID 或策略规则。Agent 会依据description决定何时调用该工具。Handler 返回一个悬而未决的 Promiseresolve被存进组件 stateDialogState弹窗里用户点击 Approve / Reject 时才调用它从而“解封”handler把结果作为工具返回值交还给 Agent。并发防护如果已有审批弹窗打开current.open为真新请求会被立即拒绝返回approved: false与原因 “Another approval request is already pending.”保证同一时刻只有一个挂起的审批。返回值契约{ 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 进程createCopilotRuntimeHandler以single-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 文档逐条展开并补充每个断言对应的实现依据导航到/demos/hitl-in-app页面由HitlInAppDemo渲染外层以CopilotKit runtimeUrl/api/copilotkit agenthitl-in-app包裹内层是LayoutCopilotPopupagentIdhitl-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——即没有任何审批弹窗。让 Agent 执行需要审批的动作点击任一建议卡片即可。例如点击 “Approve refund for #12345” 后Agent 会调用request_user_approval工具。三条建议卡片均被 E2E 断言为可见见 tests/e2e/hitl-in-app.spec.ts且卡片文本精确引用各工单。验证审批弹窗渲染在聊天之外应用级模态框这是本用例的独有断言。测试用body [data-testidapproval-dialog-overlay]定位弹窗确认它是body的直接子节点——这正是createPortal的契约。同时断言approval-dialog与可选备注输入框approval-dialog-reason可见。QA 文档要求“dialog renders OUTSIDE the chat”E2E 用 DOM 层级把这一条固化成可自动验证的标准。批准操作验证 Agent 按用户决定继续点击approval-dialog-approve后断言弹窗在 5 秒内消失然后断言助手消息包含分支专属的前导句例如批准退款 #12345 后出现 “I am processing the $50 refund”。这里体现了分支语义不同结果走不同的确定性夹具分支fixture branch消息文本与 approve/reject 一一对应。重复执行并拒绝验证 Agent 尊重拒绝结果对同一建议卡片再点一次这次点击approval-dialog-reject随后助手消息应包含拒绝分支的前导句例如 “refund request was not approved”。升级工单场景同理批准后出现 “Escalated ticket #12347”拒绝后出现 “Not escalated ...”。验证无控制台错误QA 文档要求 “Verify no console errors”对应 Playwright 默认收集页面 console / pageerror 的机制测试全程无 UI 报错即通过。预期结果Expected Results与自动化对应QA 文档的三条预期结果逐一对应测试断言异步前端工具阻塞直到用户解决模态框Async frontend tool blocks until user resolves the modalhandler 返回的 Promise 悬而未决E2E 用 60 秒超时等待body [data-testidapproval-dialog-overlay]出现再等待点击后弹窗消失验证了“先阻塞、后解封”的时序Agent 结果在批准与拒绝之间不同Agent outcome differs between approve and reject同一建议卡片跑两条测试approve 分支断言 “processing the $50 refund”reject 分支断言 “not approved”——文本不对称意味着至少一个分支会失败从而证明结果确实由用户选择驱动无 UI 错误No UI errors见上一条。七、测试设计中的工程细节理解 E2E 测试的断言策略能帮你更稳地复现与验证该演示串行模式serial mode是承重的夹具匹配器aimocksequenceIndex在整个进程内计数approve 测试sequenceIndex 0必须严格先于 reject 测试sequenceIndex 1运行因此test.describe.configure({ mode: serial })必不可少多任务并发审批回归测试 “approve refund then click escalate — each pill mounts its own approval dialog” 验证了连点两个建议卡片时每个 pill 都会挂载自己独立的审批弹窗分别指向 #12345 与 #12347。这正是页面中if (current.open) resolve({ approved: false, ... })并发保护在单线程场景下的正确行为已注释的降级用例文件中downgrade #12346的用例被有意跳过上游 demo 在降级提示词下未稳定触发request_user_approval注释明确了“待上游修复后重新启用”——这是仓库对已知上游缺陷的诚实标注复现时不必把它当作失败hasToolResult回归教训注释中记录了 aimock 多 pill 缺陷的修复——通过request_user_approval的toolCallId链式串联后续夹具并去掉工具发射夹具上的hasToolResult: false避免第二个 pill 在首次工具结果已存在时被跳过。这条历史经验提醒我们HITL 场景中夹具对工具调用的建模必须考虑线程已有的工具结果状态。八、可复用的实现模式小结从本演示可以提炼出一个通用的“应用级审批”模式共五步注册工具用useFrontendTool声明工具名、语义化description、zod 参数 Schema 与 async handler捕获 resolvehandler 返回new Promise((resolve) { setDialog(...) })把resolve存入 state渲染应用级 UI用createPortal(content, document.body)将审批模态框挂到body脱离聊天树用户决策模态框的 Approve / Reject 回调调用onResolve({ approved, reason? })最终触发dialog.resolve(...)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),仅供参考