演示与 QA 验证指南 —— 基于 LlamaIndex 集成)
CopilotKit 应用内人工确认HITL In-App演示与 QA 验证指南 —— 基于 LlamaIndex 集成【免费下载链接】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 与 LlamaIndex 集成示例 中体验与验证「应用内人工确认」HITL In-AppHuman-in-the-Loop功能的开发者与 QA 工程师。HITL In-App 让 Agent 在真正执行影响客户的操作退款、改套餐、升级工单之前通过useFrontendTool在前端弹出一个位于聊天界面之外的应用级审批弹窗等待人工操作员点击 Approve / Reject 后再继续执行。阅读本文后你将掌握该演示的部署前置条件、逐步 QA 验证流程、预期结果以及从前端工具注册、Promise 解析到后端 LlamaIndex Agent 与 AG-UI 路由的完整实现原理。前置条件Prerequisites演示应用已成功部署并可通过浏览器访问。该演示属于showcase/integrations/llamaindex展示工程前端为 Next.js 应用后端为独立的 FastAPI Agent Server二者通过 AG-UI 协议经由 CopilotKit Runtime 通信。可参考该工程根目录的 README.md 完成本地启动或部署。功能概览为什么需要「应用内」人工确认在支持工单处理的客服场景中Agent 建议执行的退款、套餐降级、工单升级等操作会直接影响客户绝不能未经确认就执行。CopilotKit 提供两种 HITL 形态In-Chat HITL确认 UI 渲染在聊天流内部对应hitl-in-chat演示使用高层useHumanInTheLoop钩子In-App HITL本文主题确认 UI 是应用级模态弹窗通过createPortal挂载到document.body物理上位于聊天区域之外。操作员可以在左侧工单面板与右侧聊天界面并存的工作台背景下于弹窗内完成审批。从展示工程的 manifest.yaml 可见hitl-in-app被标记为frontend-tools与hitl功能组合交互形态为 sidebar / embedded / chat 均可其interrupt_pattern被标注为promise-based——这正是理解本演示实现原理的关键线索。验证步骤Test Steps1. 进入演示页面在浏览器中导航到/demos/hitl-in-app。该路由由 页面源码 提供页面顶层用CopilotKit runtimeUrl/api/copilotkit agenthitl-in-app包裹将前端 runtime 指向/api/copilotkit路由并将 agent ID 指定为hitl-in-app。2. 验证左右分栏布局左侧support-tickets工单面板应可见。它由 tickets-panel.tsx 渲染内置 3 张硬编码的模拟工单SUPPORT_TICKETS数组#12345Jordan Rivera —— 重复扣费退款请求争议金额 $50#12346Priya Shah —— 降级到 Starter 套餐#12347Morgan Lee —— 升级工单支付卡在 pending 状态。每张工单卡片带有data-testidticket-工单号如ticket-12345QA 脚本正是靠这些 testid 做断言。右侧CopilotChat应可见。本演示使用预置的CopilotPopup组件且defaultOpen{true}输入框占位文案为 Type a message。3. 触发人工审批向聊天发送指令Approve a $50 refund for ticket #12345也可以直接点击建议胶囊suggestion pill。页面通过 suggestions.ts 中的useConfigureSuggestions预置了三个建议分别对应三张工单Approve refund for #12345、Downgrade plan for #12346、Escalate ticket #12347available: always表示始终可用。点击或发送后后端 LlamaIndex Agent 会决定调用前端注册的request_user_approval工具。4. 验证审批弹窗弹出页面中应弹出ApprovalDialogdata-testidapproval-dialog同时其半透明遮罩层带有data-testidapproval-dialog-overlay。弹窗实现位于 approval-dialog.tsx关键特征包括使用createPortal(content, document.body)渲染因此弹窗直接挂在body下是聊天组件树的兄弟节点而不是嵌套在聊天气泡内部遮罩为fixed inset-0 z-50全屏覆盖带backdrop-blur-sm半透明背景弹窗内部展示pending.message如 Please approve a $50 refund to Jordan Rivera on ticket #12345...以及可选的context如工单 ID提供可选备注输入框data-testidapproval-dialog-reason以及两个按钮Rejectdata-testidapproval-dialog-reject和 Approvedata-testidapproval-dialog-approve。5. 点击 Approve 并确认弹窗关闭点击 Approve 后onResolve({ approved: true, reason })被触发弹窗应从页面消失overlay 数量归零。6. 验证 Agent 继续执行并给出确认消息弹窗关闭后Agent 应收到工具结果{ approved: true }并继续执行最终在聊天中输出确认消息例如退款场景下以 I am processing the $50 refund... 开头、升级场景下以 Escalated ticket #12347... 开头的叙述。预期结果Expected Results弹窗出现在聊天之外并 portal 到 body审批弹窗不是聊天界面的一部分而是应用级模态框直接挂载到document.body。这是该演示与 In-Chat HITL 的核心区别。Approve / Reject 会 resolve 前端工具处理器中挂起的 Promise点击任一按钮都会让前端useFrontendTool注册的异步 handler 所返回的 Promise 得到解析并把{ approved, reason? }作为工具结果交还给 Agent驱动其继续执行或中止。这两条预期结果恰恰对应 manifest.yaml 中标注的interrupt_pattern: promise-based——HITL 的底层机制就是工具处理器返回一个挂起的 PromiseUI 事件负责 resolve 它。实现原理Promise 从挂起到解析的完整链路前端注册request_user_approval工具核心代码在 page.tsx 中type ResolveFn (value: { approved: boolean; reason?: string }) void; type DialogState | { open: false } | { open: true; pending: PendingApproval; resolve: ResolveFn }; const [dialog, setDialog] useStateDialogState({ open: false }); 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 }); }, ); }, });关键设计有三点工具描述即 Agent 的调用契约description明确告知 LLM 这是一个在聊天之外弹出应用内模态框的审批工具并声明返回形状为{ approved: boolean, reason?: string }参数使用 Zod schema 描述message必填需包含具体数字与 ID与context可选如工单 ID均带describe提示引导 Agent 生成信息充分的审批请求Handler 返回一个不立即 resolve 的 PromisePromise 的resolve被存入 React state真正的解析动作发生在用户点击 Approve / Reject 的时机。handleResolve负责在弹窗打开时调用dialog.resolve(result)并关闭弹窗从而把{ approved, reason }作为工具结果返回给 Agentconst handleResolve (result: { approved: boolean; reason?: string }) { if (dialog.open) { dialog.resolve(result); setDialog({ open: false }); } };后端LlamaIndex Agent 中的工具桩与系统提示词后端 Agent 实现在 hitl_in_app_agent.py。为了让 AG-UI 协议正确发出TOOL_CALL_CHUNK事件后端以FunctionTool注册了一个同名的工具桩stubdef _request_user_approval_stub(message: str, context: str ) - str: Ask the operator to approve or reject an action before taking it. ... This stub satisfies the AGUIChatWorkflow tool registry so the proper AG-UI TOOL_CALL_CHUNK events are emitted; CopilotKit intercepts the call and opens the modal dialog. return _request_user_approval_tool FunctionTool.from_defaults( fn_request_user_approval_stub, namerequest_user_approval, description( Ask the operator to approve or reject an action before you take it. Returns { approved: boolean, reason?: string }. ), )真正的审批 UI 与解析逻辑完全在前端后端桩函数仅用于满足工具注册表、触发协议事件实际调用会被 CopilotKit 前端截获并弹出模态框。Agent 的系统提示词SYSTEM_PROMPT则约束 LLM 的行为契约凡是影响客户的操作退款、改套餐、取消订阅、升级工单、发送道歉积分等必须先调用request_user_approval征得操作员明确同意message必须是包含具体数字与客户 ID 的简短英文摘要如$50 refund to customer #12345context提供工单 ID、适用策略等可选上下文保持一两句话工具返回{approved: boolean, reason: string | null}为true时用一句话确认正在处理为false时用一句话确认拒绝。该 Agent 使用FixedAGUIChatWorkflow复用自hitl_in_chat_agent.py据模块文档说明这是为了规避上游库的三个 bug重复的工具调用渲染、缺失parent_message_id、工具结果消息角色错误。工作流由get_ag_ui_workflow_router()挂载为 FastAPI 路由并在 agent_server.py 中以prefix/hitl-in-app注册。运行时前端路由将 agent ID 映射到后端子路径API 路由 中hitl-in-app及下划线别名hitl_in_app被显式映射到后端/hitl-in-app子路径const specializedAgents: Recordstring, string { ... hitl_in_app: /hitl-in-app, hitl-in-app: /hitl-in-app, ... };即 CopilotKit Runtime 收到 agent ID 为hitl-in-app的请求后会通过 AG-UI 协议转发到http://localhost:8000/hitl-in-app/runAGENT_URL可通过环境变量覆盖。这样页面上CopilotKit agenthitl-in-app与后端专用路由就形成了完整闭环。可复现的端到端验证E2E 测试视角QA 指南描述的手动步骤在该工程的 Playwright 测试中得到了完全自动化测试位于 hitl-in-app.spec.ts。它可作为手动验证之外的可复现依据几个要点Portal 契约断言测试用body [data-testidapproval-dialog-overlay]选择器断言模态框直接挂在body下从侧面印证弹窗 portal 到 body、位于聊天之外的预期结果审批分支的真实性测试利用 aimock 确定性 fixture 为每个 pill 提供 approve / reject 两条分支续写并采用 serial 串行模式保证 approve 用例先消费sequenceIndex 0、reject 用例后消费sequenceIndex 1从而让 approve/reject 断言各自命中不同响应分支approve 分支断言消息以 I am processing the $50 refund 开头reject 分支断言消息包含 refund request was not approved升级工单场景则分别断言 Escalated ticket #12347 与 Not escalated多 pill 回归场景测试还覆盖了先 approve 退款、再点击升级工单每个 pill 各自弹出独立审批弹窗的回归用例防止第二个 pill 复用第一个工具调用的 fixture 导致弹窗不出现。常见问题排查弹窗没有弹出Agent 直接执行了操作检查后端 Agent 是否注册了request_user_approval工具桩见 hitl_in_app_agent.py并确认系统提示词明确要求影响客户的操作必须调用审批工具同时确认前端useFrontendTool的name与后端FunctionTool的name完全一致都是request_user_approval弹窗出现在聊天内部而不是应用层确认ApprovalDialog使用了createPortal(content, document.body)而不是直接内联渲染在聊天组件树中点击按钮后 Agent 无响应确认handleResolve正确调用了存储在 state 中的resolve函数且 Promise 解析后返回的对象形状与工具描述声明的{ approved, reason? }一致agent ID 404 或路由错误确认前端agenthitl-in-app与 route.ts 中specializedAgents的键一致且后端 agent_server.py 已用prefix/hitl-in-app挂载对应路由。小结HITL In-App 演示展示了 CopilotKit 前端工具Frontend Tools与异步 Promise 解析机制的组合用法后端 LlamaIndex Agent 通过 AG-UI 协议声明式调用request_user_approval前端useFrontendTool将调用转换为一个挂起的 Promise 并驱动应用级审批弹窗最终由操作员的点击完成 Promise 解析、把决策交还给 Agent。整个链路涵盖了工具注册、Zod 参数契约、系统提示词约束、运行时 agent 路由与 E2E 断言是理解 CopilotKit 应用内人工确认模式的完整参考实现。【免费下载链接】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),仅供参考