news 2026/9/14 12:51:58

CopilotKit × Mastra:基于 useInterrupt 的聊天内 HITL 中断实战(含完整 QA 验证清单)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CopilotKit × Mastra:基于 useInterrupt 的聊天内 HITL 中断实战(含完整 QA 验证清单)

CopilotKit × Mastra:基于 useInterrupt 的聊天内 HITL 中断实战(含完整 QA 验证清单)

【免费下载链接】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 仓库中 Mastra 集成示例的gen-ui-interruptdemo 展开,讲解如何在聊天转写区(inline)内实现「人在环」(Human-in-the-Loop)交互:后端 Mastra 工具通过suspend()挂起 agentic 循环,@ag-ui/mastra桥接层将其映射为 AG-UI interrupt 事件,前端用useInterrupt低层原语渲染时间选择卡片,用户选择后resolve(...)恢复 Mastra run 并携带resumeData重新执行工具。读完本文,你将掌握这条原生中断链路的后端/前端完整实现细节、关键实现陷阱,以及一套可直接执行的端到端 QA 验证清单。

1. 前置条件与链路总览

1.1 前置条件(摘自 QA 基线)

  • Demo 已部署并可访问,位于 dashboard host 的/demos/gen-ui-interrupt路径;
  • Agent 后端健康检查通过(/api/health);OPENAI_API_KEY已在 Railway 环境变量中设置;
  • 关键机制说明(QA 文档原注):时间选择卡片通过useInterrupt({ renderInChat: true })内联渲染在聊天转写区内。与 LangGraph 的interrupt()API 不同,Mastra 路径是一个原生 suspend 工具:后端schedule_meeting工具(src/mastra/tools/interrupt.ts)调用suspend({ topic, attendee, slots })@ag-ui/mastra桥接层把该 suspend 映射为 AG-UI interrupt(legacyon_interruptCUSTOM 事件 + 标准的RUN_FINISHEDinterrupt-outcome),随后useInterrupt渲染TimePickerCard;用户点选某个 slot 后resolve(...)会恢复 Mastra run(工具的executeresumeData被重新调用)。

1.2 中断生命周期

整条链路可以概括为六个阶段:

  1. 用户发送 "Book a call with sales"(建议项)或等价措辞;
  2. 模型依据 agent 指令调用schedule_meeting工具(参数topic、可选attendee);
  3. 工具首次执行时返回suspend({ topic, attendee, slots }),agentic 循环在此暂停;
  4. @ag-ui/mastra桥接层把 suspend 暴露为 AG-UI interrupt(mastra_suspend包装 +on_interruptCUSTOM 事件,run 以 interrupt-outcome 结束RUN_FINISHED);
  5. 前端useInterruptrender回调解析 payload 并在聊天流内渲染TimePickerCard
  6. 用户选择 slot 或取消 →resolve(...)→ Mastra run 恢复 →execute第二次进入,executionContext.agent.resumeData携带用户选择 → 工具返回确认文案 → agent 生成最终回复。

这条链路与旧版「Strategy-B」workaround(前端自造schedule_meeting配合useHumanInTheLoop)不同,是真实的后端挂起(见 src/mastra/tools/interrupt.ts 顶部注释)。

2. 后端实现:原生 suspend 工具schedule_meeting

后端工具定义在 showcase/integrations/mastra/src/mastra/tools/interrupt.ts,核心结构如下:

export const scheduleMeetingInterruptTool = createTool({ id: "schedule_meeting", description: "Ask the user to pick a meeting time. Surfaces an interactive time-picker " + "to the user and returns their selection. Call this whenever the user asks " + "to book or schedule a meeting.", inputSchema: z.object({ topic: z.string().describe("What the meeting is about (e.g. 'Intro with sales')."), attendee: z.string().optional().describe("Who the meeting is with (e.g. 'Alice'), if known."), }), suspendSchema: z.object({ topic: z.string(), attendee: z.string().optional(), slots: z.array(z.object({ label: z.string(), iso: z.string() })), }), resumeSchema: z.object({ chosen_time: z.string().optional(), chosen_label: z.string().optional(), cancelled: z.boolean().optional(), }), execute: async (inputData, executionContext) => { const { suspend, resumeData } = executionContext?.agent ?? {}; // 第二次进入:用户已 resolve,带选择恢复 if (resumeData) { if (resumeData.cancelled) { return "The user cancelled — no meeting was scheduled."; } const when = resumeData.chosen_label ?? resumeData.chosen_time ?? "the chosen time"; return `Scheduled "${inputData.topic}" for ${when}.`; } // 第一次进入:挂起并携带 picker payload return suspend?.({ topic: inputData.topic, attendee: inputData.attendee, slots: generateCandidateSlots(), }); }, });

几个源码级要点(都是该文件注释明确标注的「load-bearing」细节):

  • 必须return suspend(...)直接返回,不能写成await suspend(); return x。后者会让工具「完成」,在 fast streaming 下 agentic 循环会越过暂停点继续执行。
  • suspend/resumeData位于executionContext.agent子对象下AgentToolExecutionContext),不在executionContext顶层。直接从顶层解构会得到undefined,导致suspend is not a function工具错误,模型反复重调直至撞上 step 上限,且永远没有tool-call-suspendedchunk。
  • 候选 slot 由后端生成generateCandidateSlots()基于当前时间生成固定四个相对标签 ——Tomorrow 10:00 AMTomorrow 2:00 PMMonday 9:00 AMMonday 3:30 PM,并附带 ISO 时间戳。「下周一」的计算保证至少距今 2 天以上,避免周日/周一时 "Monday" 与 "Tomorrow" 语义冲突。前端只在这些字段缺失时才回退到自己的生成器。

2.1 Agent 与运行时装配

承载该工具的 agent 定义在 showcase/integrations/mastra/src/mastra/agents/index.ts:

  • id: "interrupt-agent",模型gpt-4o-mini,仅注册一个工具schedule_meeting
  • 系统指令要求:只要用户要求预约/安排会议就必须调用schedule_meeting,并传入简短topic(已知时传attendee);工具挂起后由 picker 处理决策,agent 不得自行征求批准;工具返回后简短确认是否已排期及时间,或说明用户已取消。
  • 记忆使用LibSQLStore(working memory 启用,schema 为共享的AgentState)。

resume 能力依赖实例级 storage:Mastra 实例在 showcase/integrations/mastra/src/mastra/index.ts 中配置了storage: new LibSQLStore({ id: "mastra-storage", url: ":memory:" }),挂起的 agentic-loop 快照才能被重新加载完成恢复。前端路由侧则在 showcase/integrations/mastra/src/app/api/copilotkit/route.ts 中将别名gen-ui-interrupt映射到interruptAgent(并校验其存在,缺失时报 "interruptAgent missing from Mastra config")。

3. 前端实现:useInterrupt 低层原语

demo 页面在 showcase/integrations/mastra/src/app/demos/gen-ui-interrupt/page.tsx,完整继承 QA 文档所验证的交互契约:

<CopilotKit runtimeUrl="/api/copilotkit" agent="gen-ui-interrupt"> <div className="flex justify-center items-center h-screen w-full"> <div className="h-full w-full max-w-4xl"> {/* QA: max-w-4xl、full-height 容器 */} <Chat /> </div> </div> </CopilotKit>

Chat组件内的关键 hook:

useInterrupt({ agentId: "gen-ui-interrupt", renderInChat: true, // 内联渲染到转写区(非 portal 到 body) render: ({ event, resolve }) => { // Mastra 将 suspend 值包装为 { type: "mastra_suspend", toolName, suspendPayload, ... } // 且 AG-UI adapter 会将其 JSON 字符串化 —— 先 parse,再读 suspendPayload const raw = event.value ?? {}; const parsed = (typeof raw === "string" ? JSON.parse(raw) : raw) as { suspendPayload?: SuspendPayload } & SuspendPayload; const payload: SuspendPayload = parsed.suspendPayload ?? parsed; const slots = payload.slots && payload.slots.length > 0 ? payload.slots : generateFallbackSlots(); return ( <TimePickerCard topic={payload.topic ?? "a call"} attendee={payload.attendee} slots={slots} onSubmit={(result) => { // 延迟 resolve:等 React 先提交 picked/cancelled 徽标, // 再让 useInterrupt 清除 interrupt 元素(单个 rAF 不可靠) setTimeout(() => resolve(result), 500); }} /> ); }, });

源码注释揭示了三个前端陷阱:

  1. payload 是包装对象event.value里的原始值不是业务数据,而是mastra_suspend包装;业务字段(topic/attendee/slots)在suspendPayload内,且整体可能被 JSON 字符串化,需要「字符串则 parse、再取suspendPayload,取不到就整体当 payload」的容错解包。
  2. resolve 必须延迟:先让本地 state 把time-picker-picked/time-picker-cancelled徽标渲染出来并 commit,再调用resolve(...);否则useInterrupt会立刻清除 interrupt 元素,用户看不到「Booked / Cancelled」的只读结果。单个requestAnimationFrame不够可靠,demo 使用 500mssetTimeout
  3. slot 兜底:若 suspend payload 未携带 slots,回退到 showcase/integrations/mastra/src/app/demos/_shared/interrupt-fallback-slots.ts 的generateFallbackSlots()(与后端生成逻辑镜像,同样相对Date.now()计算,避免硬编码日期一周内过期)。

3.1 TimePickerCard 三态状态机

卡片组件 showcase/integrations/mastra/src/app/demos/gen-ui-interrupt/_components/time-picker-card.tsx 由本地 state(picked/cancelled)驱动三种互斥形态,每种形态对应 QA 文档断言的data-testid

形态data-testid内容
可交互(初始)time-picker-card头部Book a calloutline 徽标 +(若有)With {attendee}行 + 主题标题 + "Pick a time that works for you." 描述;2x2 grid 恰好 4 个 slot 按钮(time-picker-slot);底部 ghost 按钮 "None of these work"(time-picker-cancel
已选择time-picker-picked绿色Bookedsuccess 徽标 + 所选 label 加粗显示;交互卡片整体卸载
已取消time-picker-cancelled红色Cancelled徽标 + "No time picked."

防重复提交由disabled = picked !== null || cancelled保证:首次点击后立即禁用所有按钮,因此快速双击只会提交一次选择。提交回调的 payload 严格对齐后端resumeSchema:选择 slot 时发送{ chosen_time: s.iso, chosen_label: s.label },点击取消时发送{ cancelled: true }

3.2 建议项(Suggestion Pills)

建议项在 showcase/integrations/mastra/src/app/demos/gen-ui-interrupt/suggestions.ts 中通过useConfigureSuggestions注册,available: "always",两条 verbatim 标题:

  • "Book a call with sales"(消息:"Book an intro call with the sales team to discuss pricing.")
  • "Schedule a 1:1 with Alice"(消息:"Schedule a 1:1 with Alice next week to review Q2 goals.")

QA 文档要求校验的是 pill 标题的逐字内容,而非消息体。

4. QA 验证流程(完整检查清单)

以下清单完整继承自 showcase/integrations/mastra/qa/gen-ui-interrupt.md,可直接作为回归验收脚本执行。

4.1 基础功能(Basic Functionality)

  • 访问/demos/gen-ui-interrupt,页面在 3 秒内渲染完成,CopilotChat位于max-w-4xl、全高、rounded-2xl容器中;
  • 首次加载时输入框 placeholder 可见、转写区为空,且页面上不存在data-testid="time-picker-card"
  • 发送 "Hello",agent 仅以纯文本回复(不出现 picker —— 只有明确要求预约/排期时 agent 才会调用schedule_meeting)。

4.2 功能专项检查(Feature-Specific Checks)

建议项(Suggestions)

  • 两个建议 pill 均可见,标题逐字为 "Book a call with sales" 与 "Schedule a 1:1 with Alice"。

中断触发 + 内联渲染(useInterrupt 低层原语)

  • 点击 "Book a call with sales"(或手动输入 "Use schedule_meeting to book an intro call with the sales team about pricing.");
  • 60 秒内 agent 调用schedule_meeting,后端工具suspend(),且 picker内联出现(data-testid="time-picker-card");
  • 确认该卡片是聊天转写区的后代节点(未 portal 到<body>,区别于hitl-in-app的模态形态)——断言不存在body > [data-testid="time-picker-card"]
  • 卡片头部显示 "Book a call" eyebrow 徽标、主题标题(topic)以及 "Pick a time that works for you." 描述;
  • 2x2 网格中恰好 4 个 slot 按钮(data-testid="time-picker-slot"),标签为后端生成的相对时间:Tomorrow 10:00 AMTomorrow 2:00 PMMonday 9:00 AMMonday 3:30 PM
  • 网格下方存在 "None of these work" ghost 按钮(data-testid="time-picker-cancel")。

选择 slot 的恢复路径(Pick-a-Slot Resume Path)

  • 点击四个 slot 之一(例如 "Monday 9:00 AM");
  • 卡片切换为data-testid="time-picker-picked"—— "Booked" 成功徽标 + 所选 label 加粗 —— 且交互卡片卸载(不再有time-picker-card);
  • agent 恢复运行并产出确认消息(后端返回Scheduled "{topic}" for {chosen_label}.,agent 据此回复)。

取消路径(Cancel Path)

  • 发送 "Use schedule_meeting to book a 1:1 with Alice next week to review Q2 goals.";
  • 新的 picker 内联渲染(data-testid="time-picker-card");当 agent 提供了 attendee 时,eyebrow 旁出现 "With Alice" 行;
  • 点击 "None of these work";
  • 卡片切换为data-testid="time-picker-cancelled"—— "Cancelled" 徽标 + "No time picked.";
  • agent 恢复运行并回复会议未被排期。

多轮(Multi-Turn)

  • 在一次选择或取消之后,再发送一条预约指令;确认新的独立 picker 渲染(旧卡片保持已解决态),interrupt 生命周期干净地重放,第二次 resume 端到端可用。

契约检查:interrupt 是低层原语(Contract Check)

  • 仅工具触发路径会渲染 picker:普通对话消息(如 "What's the weather?")应出现 picker;
  • 不出现审批对话框式模态(本 demo 是内联而非 modal);
  • 注意:picker 展示的topic来自模型的工具调用参数,非确定性—— QA 不要对 topic 文案做断言。

4.3 错误处理(Error Handling)

  • 发送空消息:应为 no-op;
  • 快速双击 slot 按钮:仅提交一次选择(首次 pick/cancel 后按钮即禁用);
  • 贯穿 pick / cancel / 多轮全过程,无未捕获的 console 错误。

4.4 预期结果(Expected Results)

  • 聊天页 3 秒内加载完成;纯文本响应 10 秒内返回;
  • 收到预约类提示后 60 秒内内联渲染 picker;
  • picker 通过 slot 按钮({chosen_time, chosen_label})或 "None of these work"({cancelled: true})解决;解决后卡片只读;
  • agent resume 产生的确认消息引用了所选 slot 或取消事实;
  • 无布局破坏、无未捕获 console 错误、单次 interrupt 不出现重复 picker。

5. 关键要点与陷阱小结

  1. Mastra 的中断是工具级原生 suspend,而非 LangGraphinterrupt()那样的图节点机制;桥接层负责把 suspend chunk 翻译为 AG-UI interrupt 事件族(on_interruptCUSTOM +RUN_FINISHEDinterrupt-outcome)。
  2. return suspend(...)与解构位置是后端两个最易踩的坑:前者写错会让循环越过暂停点,后者写错会让suspendundefined并引发工具调用死循环(见 interrupt.ts 中对应注释)。
  3. resume 需要实例 storagenew Mastra({ storage })缺省时挂起的循环快照无法重载,恢复链路断裂。
  4. 前端解包 + 延迟 resolvemastra_suspend包装、JSON 字符串化、以及 500ms 延迟 resolve 三者共同保证「先展示结果徽标、再清除 interrupt 元素」的视觉正确性。
  5. 确定性边界:slot 标签由后端按当前时间生成(四个固定相对标签),但topic由模型产出、不可断言;QA 断言应锚定data-testid、按钮数量、徽标文案等结构化特征。
  6. 内联 vs 模态renderInChat: true使卡片成为转写区后代节点;如需应用表面模态(approval-dialog 风格),参照同仓库的hitl-in-appdemo,二者是同一 interrupt 机制在不同渲染面上的用法。

【免费下载链接】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/14 12:50:19

微信小程序智能机器人:消息链路设计与云开发实战

简介&#xff1a;面向微信小程序开发者和人工智能对话初学者&#xff0c;这份智能机器人小程序源码可帮助快速掌握页面搭建、消息交互与机器人服务对接方法。压缩包共19个文件&#xff0c;整体体积仅15KB&#xff0c;其中包含5个逻辑脚本文件、4个样式表文件、3个页面结构文件、…

作者头像 李华
网站建设 2026/9/14 12:43:11

DB-GPT 启动报端口 5670 “Address already in use“ 怎么排查?

DB-GPT 启动报端口 5670 "Address already in use" 怎么排查&#xff1f; 【免费下载链接】DB-GPT open-source agentic AI data assistant for the next generation of AI Data products. 项目地址: https://gitcode.com/GitHub_Trending/db/DB-GPT 当你用 …

作者头像 李华
网站建设 2026/9/14 12:42:39

NeMo Lightning 模块解析:PTL 与 Megatron Core 之间的训练桥接层

NeMo Lightning 模块解析&#xff1a;PTL 与 Megatron Core 之间的训练桥接层 【免费下载链接】Speech A scalable generative AI framework built for researchers and developers working on Large Language Models, Multimodal, and Speech AI (Automatic Speech Recognitio…

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

基于NSGA-Ⅱ的多能源系统协同优化Matlab实现

1. 项目背景与核心价值区域多能源系统协同优化是当前能源互联网领域的前沿研究方向。我在参与某省级智慧能源项目时&#xff0c;深刻体会到传统单能源系统独立运行的局限性——电、热、气等能源形式各自为政&#xff0c;导致整体能效低下&#xff0c;可再生能源消纳能力不足。这…

作者头像 李华
网站建设 2026/9/14 12:40:54

Nginx Rewrite模块详解:从基础到高级应用

1. Nginx Rewrite基础概念解析 Rewrite是Nginx服务器中一个强大的URL重写模块&#xff0c;它允许我们在请求到达后端应用前对URI进行修改和重定向。这个功能在日常运维和开发中扮演着关键角色&#xff0c;特别是在以下场景&#xff1a; 保持旧URL兼容性同时进行站点结构更新 …

作者头像 李华
网站建设 2026/9/14 12:39:00

基于Fabric超级账本的企业资产管理与防伪溯源系统实践

简介&#xff1a;这是一套基于Hyperledger Fabric超级账本的企业级区块链解决方案&#xff0c;面向需要落地资产管理、交易、防伪、溯源等场景的架构师、开发者和运维人员。整个工程源码以Go语言为主&#xff0c;包含1653个Go文件用于链码与后端服务&#xff0c;另有100个Markd…

作者头像 李华