CopilotKit × Agno 实战:用前端工具 + 应用级模态框实现 In-App HITL 人工审批

发布时间:2026/11/13 15:51:41

CopilotKit × Agno 实战:用前端工具 + 应用级模态框实现 In-App HITL 人工审批 CopilotKit × Agno 实战用前端工具 应用级模态框实现 In-App HITL 人工审批【免费下载链接】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导读本文以 CopilotKit 开源仓库中 Agno 集成的hitl-in-app演示为骨架完整讲解如何在聊天界面之外、应用页面层级实现 Human-in-the-LoopHITL人工审批Agent 在执行退款、降级套餐、升级工单等影响客户的操作前会通过前端工具挂起执行并在应用顶层弹出审批对话框由操作员点击「Approve / Reject」后把结果回传给 Agent 继续执行。读完本文你将掌握useFrontendTool挂起 Promise、createPortal应用级弹窗、Agno 侧external_execution工具的完整实现链路以及对应的 QA 验证清单与 E2E 断言策略。一、什么是 In-App HITL审批弹窗位于聊天之外hitl-in-app演示的核心特征是「审批发生在应用页面层级而非聊天气泡内部」。原 QA 文档qa/hitl-in-app.md开篇即点明其定位frontend-tool app-level modal。与「在聊天流内呈现按钮」的 in-chat HITL 方案相比In-App HITL 的关键差异在于审批对话框通过 React Portal 渲染为body的直接子节点覆盖整个页面聊天面板右侧的CopilotPopup与工单面板左侧的 Support Inbox同时保持可见操作员可以在不打断对话上下文的前提下完成高风险操作的授权。因此 QA 文档将验证重点放在「approval-dialog是否出现在聊天区域之外并覆盖页面」这一条上这正是该模式与聊天内嵌 HITL 的分水岭。二、整体架构与一次审批的完整链路整个演示由前端 Next.js 应用与 Agno Python Agent 后端两部分组成。运行时通过 CopilotKit 路由 以 AG-UI 协议代理到 Agno 后端// showcase/integrations/agno/src/app/api/copilotkit/route.ts const AGENT_URL process.env.AGENT_URL || http://localhost:8000; function createMainAgent() { return new HttpAgent({ url: ${AGENT_URL}/agui }); }hitl-in-app这一 agent 名称被映射到主 Agentroute.ts 中的mainAgentNames数组前端通过agenthitl-in-app指定路由page.tsx。一次完整审批的调用链为用户点击建议 pill如 Approve refund for #12345消息发送给 Agno AgentAgent 依据 instructions 判断该操作影响客户调用request_user_approval工具该工具在前后端均被声明为「由前端执行」Agno 后端发出工具调用事件后暂停本轮运行前端useFrontendTool的 handler 收到调用参数返回一个挂起的 Promise其resolve被存入 React state页面据此渲染应用级审批弹窗Portal 到body操作员点击 Approve/Reject弹窗调用resolve({ approved, reason? })Promise 完成工具结果Tool Result返回给 AgentAgent 恢复运行按结果输出确认或拒绝的话术。三、前端实现useFrontendTool 挂起 Promise前端核心在 page.tsx 中。首先通过useFrontendTool注册与后端同名的工具并用zod描述参数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({ open: true, pending: { message, context }, resolve }); }, ); }, });这是整个模式的精髓所在handler 返回的 Promise 不立即完成而是把resolve函数劫持进 React state。只要弹窗未操作该 Promise 就保持 pendingAgent 的运行就被前端工具机制挂起操作员点击按钮时resolve被调用Promise 完成工具结果自动回传 Agentpage.tsx。为此页面维护了一个带判别联合discriminated union的 state把resolve与待审批内容绑定在一起type ResolveFn (value: { approved: boolean; reason?: string }) void; type DialogState | { open: false } | { open: true; pending: PendingApproval; resolve: ResolveFn };当用户点击按钮时handleResolve先调用存储的resolve(result)完成 Promise再关闭弹窗const handleResolve (result: { approved: boolean; reason?: string }) { if (dialog.open) { dialog.resolve(result); setDialog({ open: false }); } };页面布局同时渲染三部分左侧工单面板TicketsPanel、右侧CopilotPopup聊天、以及条件渲染的ApprovalDialogpage.tsx。四、应用级模态框ApprovalDialog 与 createPortalApprovalDialogapproval-dialog.tsx负责把审批 UI 提升到应用层级。其注释明确说明模态框被portal 到body而非渲染在聊天气泡树内。关键实现通过createPortal(content, document.body)完成并设置fixed inset-0 z-50全屏遮罩与roledialog、aria-modaltrue无障碍语义。同时为 QA 提供了三个稳定的测试锚点data-testidapproval-dialog-overlay遮罩层验证 Portal 位置与开闭状态data-testidapproval-dialog对话框主体QA 文档验证其出现在聊天之外data-testidapproval-dialog-reason可选备注输入框data-testidapproval-dialog-approve/approval-dialog-reject审批 / 拒绝按钮。组件还包含一个可选备注reason文本框——这对应前端工具返回结构中的reason?: string字段onClick{() onResolve({ approved: true, reason: reason.trim() || undefined, }) }useEffect中先setMounted(true)再返回内容是为了避免 SSR 时document未定义导致的报错——这是 Next.js 中使用createPortal的标准做法。E2E 测试通过body [data-testidapproval-dialog-overlay]这一选择器断言弹窗确实挂载在body下hitl-in-app.spec.ts从测试层面锁定了「应用级弹窗」这一契约。五、后端实现Agno 的 external_execution 工具在 Agno 侧同名工具定义于 main.py其签名与前端的 zod schema 一一对应tool(external_executionTrue, external_execution_silentTrue) def request_user_approval(message: str, context: str ): 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 }. 两个装饰器参数是关键external_executionTrue声明工具由外部前端执行Agno 只负责发出工具调用事件并等待结果而不是自己执行external_execution_silentTrue工具调用过程中不产生多余的事件输出保证前端只收到一次干净的工具调用。该工具被注册进主 Agent 的tools列表main.py并在 Agent 的 instructions 中明确约束了使用时机main.pyUSER APPROVAL (HITL): When asked to take any action that affects a customer — for example issuing a refund, updating a plan, cancelling a subscription, escalating a ticket, or sending a credit — call request_user_approval FIRST with a short summary and optional context. Follow the tool result: if approved, confirm in one short sentence; if rejected, acknowledge and do not retry.此外Agent 配置中db_create_session_db()与tool_call_limit15也与 HITL 相关审批期间 Agent 运行会被挂起等待前端响应需要可写会话存储以支持运行恢复见 main.py 的注释说明。六、运行前提执行 QA 测试前需满足原文档列出的两个前置条件前提说明Demo 已部署在/demos/hitl-in-app对应源码位于 hitl-in-app/page.tsx通过next dev或next start启动Agent 后端健康Agno 后端监听AGENT_URL默认http://localhost:8000路由的GET /api/copilotkit健康检查会返回agent_status字段见 route.ts七、QA 测试步骤In-App HITL 验收清单以下为原 QA 文档hitl-in-app.md的完整验收步骤并补充了可对照的测试锚点与断言细节。7.1 基础功能导航到/demos/hitl-in-app页面加载CopilotKitagenthitl-in-app并默认展开CopilotPopup。验证工单面板与三张工单渲染左侧 Support Inbox 应显示#12345Jordan Rivera退款争议金额 $50.00、#12346Priya Shah降级到 Starter、#12347Morgan Lee升级到支付团队三张工单。工单数据硬编码于 tickets-panel.tsxE2E 通过getByTestId(ticket-12345)等锚点断言可见性。验证右侧聊天正常渲染聊天输入框占位符为 Type a messageCopilotPopup的labels.chatInputPlaceholder配置。同时初始状态下不应存在任何审批弹窗approval-dialog-overlay计数为 0。7.2 功能专项检查批准路径refund #12345点击建议 pillApprove refund for #12345。三个建议 pill 由 suggestions.ts 通过useConfigureSuggestions注册available: always并带有完整消息模板例如Please approve a $50 refund to Jordan Rivera on ticket #12345 for the duplicate charge.。验证审批对话框出现在聊天之外并覆盖页面断言data-testidapproval-dialog可见且其挂载位置为body [data-testidapproval-dialog-overlay]Portal 契约。点击Approveapproval-dialog-approve。验证弹窗关闭且 Agent 继续跟进approval-dialog-overlay数量在数秒内降为 0随后聊天区出现 Agent 的确认消息E2E 断言开头短语 I am processing the $50 refund见 hitl-in-app.spec.ts。拒绝路径downgrade #12346点击建议 pillDowngrade plan for #12346消息模板要求降级到 Starter 计划、下一账单周期生效。对话框出现后点击Rejectapproval-dialog-reject。验证 Agent 确认收到拒绝拒绝结果{ approved: false, reason? }回传后Agent 应acknowledge and do not retry按 instructions 约定不再重试。E2E 中对应的拒绝分支断言短语如 refund request was not approved / Not escalatedhitl-in-app.spec.ts。7.3 错误处理无未捕获的 console 错误全程页面加载、弹窗开合、审批与拒绝控制台不得出现未捕获异常。这一点在 E2E 测试中由test.describe.configure({ mode: serial })与严格断言组合保障。八、E2E 测试要点如何真实地验证审批结果仓库为该 demo 配套了完整的 Playwright 测试 hitl-in-app.spec.ts其设计对理解本模式极有价值Serial 模式是载重结构load-bearingaimock 确定性夹具按sequenceIndex0 approve 分支、1 reject 分支匹配因此批准测试必须在拒绝测试之前运行依赖测试顺序保证分支正确。Portal 契约断言所有弹窗可见性断言都使用body [data-testidapproval-dialog-overlay]直接验证弹窗是body的子节点而非聊天树内部。多轮审批回归测试「先批准 #12345 再升级 #12347」确保每个 pill 都触发自己独立的request_user_approval调用并挂载独立弹窗——这对应一个曾修复的 aimock 多 pill bug夹具需通过toolCallId串联后续分支避免首个审批后第二个 pill 直接跳过弹窗hitl-in-app.spec.ts 的注释详述了该回归案例。已知暂缓项downgrade #12346的审批/拒绝流程测试被显式跳过TODO注释标明上游 bug 于 2026-05-07 存在超出本次改写范围QA 执行时应知悉此限制。九、常见问题与排查建议现象排查方向弹窗未出现在页面顶层而是嵌在聊天内检查是否使用createPortal(content, document.body)E2E 用body [data-testidapproval-dialog-overlay]选择器可直接定位点击 Approve/Reject 后 Agent 无后续响应确认resolve是否被正确调用、handleResolve是否先resolve再关闭弹窗后端检查request_user_approval是否带external_executionTrue审批后对话无工具结果、Agent 卡住检查 Agent 的会话存储db是否可写HITL 挂起依赖运行恢复以及tool_call_limit是否过低导致运行被截断页面首个审批完成后后续 pill 不弹窗参考多轮审批回归确保夹具按toolCallId串联后续分支而不是依赖hasToolResult布尔切换SSR 下document未定义报错使用mountedstate useEffect延迟渲染 Portal 内容approval-dialog.tsx 的标准做法十、小结In-App HITL 的本质是把「需要人工裁决的暂停点」从模型内部转移到前端应用层Agno 侧用external_execution工具声明这段代码由前端跑前端用useFrontendTool 挂起 Promise 制造暂停再用createPortal把裁决 UI 提升到页面层级。三者结合既保证了操作员拥有全页面上下文工单、金额、政策都在视野内也让 Agent 的每次高风险操作都有据可查、可控可拒。配合仓库内的 QA 清单 与 E2E 测试你可以在自己的业务中快速复刻这一模式。【免费下载链接】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),仅供参考
延伸阅读

更多相关文章

2026/11/10 2:01:05

macOS 应用精选集 awesome-macOS:告别盲目找软件的烦恼

macOS 应用精选集 awesome-macOS:告别盲目找软件的烦恼 【免费下载链接】awesome-macOS  A curated list of awesome applications, softwares, tools and shiny things for macOS. 项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-macOS 装个…

2026/11/13 15:50:28

宁波哪里考高压电工证 上岗必备的证书怎么选

宁波哪里考高压电工证 上岗必备的证书怎么选 之前考的证在外省能不能转过来?这几乎是所有想在宁波从事高压电工工作的人都会问的问题。你不是一个人,很多人都在为这个纠结。如果你手里已经有电工证,但不在宁波,或者你在宁波刚开始接触这个行业,那么…

2026/11/13 15:50:28

建筑特种工怎么报名保姆级教程:外省证能转吗?手把手教你全流程

建筑特种工怎么报名保姆级教程:外省证能转吗?手把手教你全流程 之前考的证在外省能不能转过来?这是很多建筑特种工最关心的问题,特别是那些在外地打工、换过工作的朋友。别急,今天我就用保姆级教程,一步一步告诉你【建筑特种工怎么报名】,从材料准备到…

2026/11/13 15:50:28

煤矿电工作业风险辨识怎么考过?电子证书到底靠谱不?

煤矿电工作业风险辨识怎么考过?电子证书到底靠谱不? 怕考不过白交培训费,这事儿真不是开玩笑。煤矿电工作业风险辨识考试,每年都有不少人交了培训费,结果还是没通过,白白浪费了时间和钱。很多人问,电子证书到底能不能替代纸质证书?能不能顺利通过考试…

2026/11/13 15:45:27

电工证丢失网上补办,含金量怎么保?外省证能转吗

电工证丢失网上补办,含金量怎么保?外省证能转吗 之前考的证在外省能不能转过来?这可能是很多电工朋友的真实困扰,尤其是在像驻马店这样的地方,很多电工从外地过来打工、学习,考了证后却发现证在老家,不能直接用,也不太清楚怎么补办。现在好了,…

2026/11/12 20:35:13

电工证丢失还能补吗?千万别踩坑,丢了证等于失业

电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。…

2026/11/13 0:01:06

办上海高压电工证良心建议:别被中介坑了,自己看这篇就够了

办上海高压电工证良心建议:别被中介坑了,自己看这篇就够了 你是不是也像我一样,第一次听说“高压电工证”这个名词,就感觉一头雾水?其实很多人都跟我一样,不知道去哪报名、怕被中介坑,结果花了冤枉钱还考不上。今天我就从一个在商丘培训过的学员角度,…

2026/11/13 0:01:06

黎里考电工证在哪里值不值得考

黎里考电工证在哪里值不值得考 之前考的证在外省能不能转过来?这是很多在外地工作、准备回老家发展的朋友最关心的问题。特别是在黎里地区,很多从事电力相关工作的朋友,都面临这样一个难题: 外省的电工证能否在本地使用?值不值得考?…

2026/11/13 0:01:06

毕节哪里考电工证 怎么报名学历不高也能上手

毕节哪里考电工证 怎么报名学历不高也能上手 你是不是也和我一样,学历不高,想着考个电工证,但又担心自己条件不够,怕报不上名?其实不然,只要符合条件,学历不高也能考电工证。今天就来聊聊【毕节哪里考电工证】这个问题,还有怎么报名,让你一步到位不…

2026/11/12 22:10:13

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/13 12:14:39

SSM电子书店源码全解析:结构、原理、部署与排错实战

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

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑