ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CopilotKit 无头中断实战:基于 MS Agent Framework 的聊天外阻塞式排期交互

CopilotKit 无头中断实战:基于 MS Agent Framework 的聊天外阻塞式排期交互 CopilotKit 无头中断实战基于 MS Agent Framework 的聊天外阻塞式排期交互【免费下载链接】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 仓库中showcase/integrations/ms-agent-dotnet集成下的interrupt-headless演示为核心讲解一个典型的“无头中断”headless interrupt交互聊天框只负责触发 Agent当 Agent 需要用户选择时间时选择器弹窗渲染在**聊天区域之外的应用表面app surface**中用户点击某个时段后才解析挂起的工具调用、Agent 再回到聊天中确认。读完本文你可以掌握在没有原生interrupt()原语的 Microsoft Agent Framework.NET后端上如何用useFrontendTool的异步 Promise handler 等价实现 LangGraph 版的中断/恢复流程并看懂前后端完整的路由与挂载关系。这个演示展示什么演示页位于 page.tsx页面布局是“左侧应用表面 右侧聊天栏”的双栏结构聊天侧只负责触发 Agent通过useConfigureSuggestions提供两条示例问法如 “Book a call with sales”当 Agent 调用schedule_meeting工具时时间选择器弹窗不出现在聊天消息流里而是出现在左侧应用表面用户选择一个时段或点 Cancel挂起的工具调用被解析弹窗消失Agent 回到聊天中用一句话确认“已排期”或“已取消”。源码中的注释把交互流程写得很直白见 page.tsx// Layout: chat on the right, empty app surface on the left. The user triggers // the agent from a chat suggestion. When the agent calls schedule_meeting, // we render a time-picker popup IN THE APP SURFACE (left pane) — outside of // the chat. Picking a slot resolves the tool call, the popup vanishes, and // the agent confirms back in chat.这种“聊天外 UI 承载中断”的模式与在聊天内联渲染选择器的做法相对后者由兄弟演示gen-ui-interrupt承担两者共用同一个 .NET 后端差异完全在前端。与 LangGraph 版的机制差异为什么需要“适配”原演示的 LangGraph 版本依赖一个自研的useHeadlessInterrupthook它监听 AG-UI 流上 LangGraph 原生的interrupt()事件并通过copilotkit.runAgent({ forwardedProps: { command: { resume } } })把用户选择回传给后端、恢复挂起的执行。而 Microsoft Agent Framework.NET没有对应的 interrupt 原语——无法在工具执行中途暂停并携带调用方提供的值恢复。因此这个 .NET 移植版采用的是一种等价机制shim维度LangGraph 版MS Agent Framework 适配版暂停点后端工具内的原生interrupt()前端工具 handler 内的awaitPromise恢复方式runAgent携带command: { resume }Promise 在用户点击弹窗时被 resolve中断 UI 位置聊天外 app surface相同聊天外 app surface用户可见体验等价等价关键思想是把“中断”从后端状态机问题转化为前端的一个不 resolve 的 Promise。后端 Agent 只是被提示“凡是排期请求必须调用schedule_meeting工具”工具定义由前端通过useFrontendTool注册AG-UI 协议会把前端工具定义转发给模型工具调用则回落到客户端 handler 执行handler 阻塞多久这次工具调用就“挂起”多久。前端实现schedule_meeting与 Promise 门控工具注册与异步 handler页面通过copilotkit/react-core/v2注册前端工具参数用 zod 描述handler 返回Promisestring。核心代码如下摘自 page.tsx 的region[headless-promise-primitives]区块useFrontendTool({ name: schedule_meeting, description: Ask the user to pick a time slot for a meeting via a picker popup that appears outside the chat. Blocks until the user chooses a slot or cancels., parameters: z.object({ topic: z .string() .describe(Short human-readable description of the meeting.), attendee: z .string() .optional() .describe(Who the meeting is with (optional).), }), // Async handler: sets the pending payload so the popup renders, then // returns a Promise that only resolves once the user interacts with the // popup. This is the MS Agent shim for the LangGraph headless interrupt // resume flow. handler: async ({ topic, attendee }: { topic: string; attendee?: string }): Promisestring { setPending({ topic, attendee }); const result await new PromisePickerResult((resolve) { resolverRef.current resolve; }); setPending(null); if (cancelled in result result.cancelled) { return User cancelled. Meeting NOT scheduled.; } if (chosen_label in result) { return Meeting scheduled for ${result.chosen_label}.; } return User did not pick a time. Meeting NOT scheduled.; }, // Render nothing inside the chat — the UI lives in the app surface. render: () null, });这段代码有三个值得注意的设计点setPending驱动弹窗渲染handler 一进入就调用setPending({ topic, attendee })把“待决工具调用的载荷”提升为组件 state左侧 app surface 据此渲染TimeSlotPopup。也就是说弹窗的显隐完全由“是否有一个在途的schedule_meeting调用”决定。resolverRef保存 resolve 函数handler 内部await一个new PromisePickerResult并把 resolve 存进useRef。外部任何组件想“结束这次中断”只需调用resolve(result)。这是一个典型的“Promise 作为跨组件异步握手”的写法。render: () nulluseFrontendTool允许工具在聊天内联渲染自己的结果 UI这里显式返回null保证聊天消息流里不出现任何工具卡片——这正是 “headless” 的含义工具调用有执行副作用阻塞 返回字符串但在聊天中无视觉存在。配套的 resolve 封装page.tsxconst resolve (result: PickerResult) { const fn resolverRef.current; resolverRef.current null; fn?.(result); };先把 ref 清空再调用可避免同一 handler 被重复 resolve。类型定义与时段数据交互涉及三种类型page.tsxtype InterruptPayload { topic?: string; attendee?: string; }; type TimeSlot { label: string; iso: string }; type PickerResult | { chosen_time: string; chosen_label: string } | { cancelled: true }; const DEFAULT_SLOTS: TimeSlot[] [ { label: Tomorrow 10:00 AM, iso: 2026-04-25T10:00:00-07:00 }, { label: Tomorrow 2:00 PM, iso: 2026-04-25T14:00:00-07:00 }, { label: Monday 9:00 AM, iso: 2026-04-28T09:00:00-07:00 }, { label: Monday 3:30 PM, iso: 2026-04-28T15:30:00-07:00 }, ];PickerResult是一个判别联合要么携带用户选中的 ISO 时间与展示文案要么是{ cancelled: true }。handler 里用cancelled in result/chosen_label in result做窄化最后统一返回纯文本结果字符串回给模型——模型拿到这句文本后组织确认话语这一返回值刻意与 LangGraph 版后端工具的返回文案保持一致从而让两个版本的对话行为几乎不可区分。弹窗组件与布局应用表面AppSurface根据pending是否非空在“空状态 / 弹窗”之间切换page.tsxdiv classNamerelative flex flex-1 items-center justify-center p-8 {pending ? ( TimeSlotPopup payload{pending} onPick{(slot) resolve({ chosen_time: slot.iso, chosen_label: slot.label }) } onCancel{() resolve({ cancelled: true })} / ) : ( EmptyState / )} /divTimeSlotPopup把DEFAULT_SLOTS渲染成两列按钮网格附带 Cancel 按钮整个弹窗带roledialog与data-testidinterrupt-headless-popup每个时段按钮都有data-testid{interrupt-headless-slot-${slot.iso}}——这些测试钩子表明该演示被仓库的 e2e/回归测试所覆盖testid 命名与演示目录一一对应。页面最外层还固定了 CopilotKit 的运行时与 Agent 名page.tsxexport default function InterruptHeadlessDemo() { return ( CopilotKit runtimeUrl/api/copilotkit agentinterrupt-headless Layout / /CopilotKit ); }runtimeUrl指向 Next.js 侧的/api/copilotkit路由agentinterrupt-headless决定请求被路由到哪个后端 Agent——这条链路的下一环见下文“Agent 路由”。后端实现一个“只有提示词”的排期 Agent.NET 后端位于 InterruptAgent.cs。它的工厂方法构造了一个ChatClientAgentInterruptAgent.cspublic AIAgent CreateInterruptAgent() { var chatClient _openAiClient.GetChatClient(gpt-4o-mini).AsIChatClient(); // No backend fallback tool is registered. If the frontend tool is // missing, the demo should fail visibly instead of bypassing the // picker with a server-side response. var chatClientAgent new ChatClientAgent( chatClient, name: InterruptAgent, instructions: You are a scheduling assistant. Whenever the user asks you to book a call or schedule a meeting, you MUST call the schedule_meeting tool. Pass a short topic describing the purpose and attendee describing who the meeting is with. After the tool returns, confirm briefly whether the meeting was scheduled and at what time, or that the user cancelled., tools: []); return new SharedStateAgent(chatClientAgent, _jsonSerializerOptions, _loggerFactory.CreateLoggerSharedStateAgent()); }从源码结构可以看到三个刻意的设计取舍tools: []——后端不注册任何工具也没有兜底的schedule_meeting服务端实现。注释明确说明这是有意为之如果前端工具缺失演示应当“显式失败”而不是被服务端悄悄绕过选择器、直接给出一个答案。这把“排期必须由用户在前端弹窗里决策”变成了强约束。系统提示词承担“中断语义”MUST call the schedule_meeting tool的措辞让模型在收到排期类请求时稳定地发起前端工具调用工具说明前端 zod schema description会经 AG-UI 转发给模型模型据此生成topic/attendee参数。复用SharedStateAgent包装与 showcase 中其他 Agent 保持一致的封装模式虽然排期中断演示本身并不依赖状态同步。挂载与路由/interrupt-adapted如何被两个演示共用Agent 通过 AG-UI 挂载点发布。在 .NET 宿主程序的 Program.cs 中// Interrupt-adapted agent: mounted on its own path so the Next.js runtime // can proxy the gen-ui-interrupt and interrupt-headless demo names to // it. The two demos share this single backend — the differentiation happens // on the frontend (in-chat picker vs. headless/app-surface picker). var interruptAgentFactory new InterruptAgentFactory(builder.Configuration, loggerFactory, jsonOptions.Value.SerializerOptions); app.MapAGUI(/interrupt-adapted, interruptAgentFactory.CreateInterruptAgent());注意这里的路径名是/interrupt-adapted它表达的是“这是中断演示的适配版后端”而不是某个具体演示。两个前端演示聊天内选择器 vs 聊天外弹窗共用这一个端点。Next.js 侧的运行时路由 route.ts 完成 Agent 名到后端路径的映射// Agent names routed to the interrupt-adapted scheduling backend. Both // gen-ui-interrupt and interrupt-headless share the same MS Agent Framework // scheduling agent; only the frontend UX differs (inline in chat vs. external // popup driven from a button grid). const interruptAgentNames [gen-ui-interrupt, interrupt-headless]; // Interrupt-adapted demos — frontend-tool shim for LangGraph interrupt(). // Both gen-ui-interrupt and interrupt-headless share the same scheduling agent; // only the frontend UX differs (inline time-picker vs. external popup). for (const name of interruptAgentNames) { agents[name] createReplaySafeAgent(/interrupt-adapted, [ schedule_meeting, ]); }这里有两个细节createReplaySafeAgent(/interrupt-adapted, [schedule_meeting])表明该 Agent 是**回放安全replay-safe**的封装且第二个参数声明了需要透传的前端工具名schedule_meeting——运行时借此知道该工具由客户端执行调用不应落到后端。从源码结构看同一 route 文件里hitl-in-app、hitl-in-chat等 HITL 演示也采用“后端tools[] 前端工具注入”的同款模式说明“前端工具作为人机协作挂起机制”是该 showcase 中一套被反复复用的适配套路而 interrupt-headless 是其中“UI 完全离开聊天”的特化形态。一个需要留意的仓库事实演示 READMEREADME.md“Related”一节沿用了 LangGraph 原版的表述指向 Python 侧的src/agents/interrupt_agent.py与src/agent_server.py而在这个 .NET 集成中实际对应的后端文件是 agent/InterruptAgent.cs 与 agent/Program.cs 中的MapAGUI(/interrupt-adapted, ...)Python 参考实现存在于仓库其他 Python 集成如showcase/integrations/langgraph-python/src/agents/interrupt_agent.py中。与兄弟演示 gen-ui-interrupt 的对照同一后端的另一个消费方是 gen-ui-interrupt 演示其 README 对适配方式的描述与本文一致LangGraph 参考实现用interrupt()在中途暂停后端工具并经useInterrupt暴露载荷.NET 版则用useFrontendTool的 async handler在聊天内渲染TimePickerCard、await 用户选择并返回一句与 LangGraph 后端工具返回值一致的纯文本。由此可以得到一个清晰的对照表对比项gen-ui-interruptinterrupt-headless本文主题后端端点/interrupt-adapted共用/interrupt-adapted共用前端工具schedule_meetingasync handlerschedule_meetingasync handler选择器位置聊天内联TimePickerCard聊天外 app surface 弹窗工具render渲染聊天内卡片() null聊天内零渲染阻塞机制handler 内 await Promisehandler 内 await Promise两个演示证明了同一件事在 MS Agent Framework 适配下“中断体验”与“工具渲染位置”是正交的——挂起语义由 Promise handler 提供UI 落在哪里则由render与组件布局决定。小结与延伸阅读interrupt-headless演示的价值在于给出了一套在无中断原语的 Agent 框架上实现“人在环中阻塞式交互”的可复用配方后端只负责稳定的工具调用意图提示词 空工具表前端用useFrontendTool的 async handler 把用户决策变成一个不提前 resolve 的 Promiserender: () null则保证聊天流不被侵入。这套模式对任何“Agent 需要用户在聊天外做出选择/确认”的场景审批、排期、表单补全都适用。建议按以下路径继续深入演示说明interrupt-headless README前端完整实现page.tsx后端 AgentInterruptAgent.cs、Program.cs 挂载点路由映射route.ts对照演示gen-ui-interrupt README前端工具的通用用法可参考仓库核心包 packages/react-core 的useFrontendTool实现与测试【免费下载链接】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),仅供参考
返回列表