ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CopilotKit useInterrupt 聊天内 HITL 实战:LangGraph(Python)interrupt 打断与时间选择卡片

CopilotKit useInterrupt 聊天内 HITL 实战:LangGraph(Python)interrupt 打断与时间选择卡片 CopilotKit useInterrupt 聊天内 HITL 实战LangGraphPythoninterrupt 打断与时间选择卡片【免费下载链接】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 仓库中的gen-ui-interrupt演示及其 QA 文档为骨架讲解如何用useInterrupt({ renderInChat: true })与 LangGraph 后端的interrupt()原语在聊天消息流内联inline渲染一张“选时间”卡片实现真正的人在回路HITL人机协作。读完本文你将掌握该演示的前后端完整链路、interrupt/resolve的调用契约、两种恢复路径选时段 / 取消的行为细节以及 QA 文档中全部可验证的测试步骤与预期结果。1. 演示定位与前置条件该演示部署在 LangGraphPython集成宿主中路由为/demos/gen-ui-interrupt。与同目录下的hitl-in-app模态弹窗式 HITL不同本演示的卡片内联渲染在聊天记录transcript中是useInterrupt低层原语的典型用法。QA 文档列出的前置条件如下来自 qa/gen-ui-interrupt.md演示已部署并可在 dashboard 主机的/demos/gen-ui-interrupt访问Agent 后端健康/api/healthOPENAI_API_KEY已在 Railway 上配置LANGGRAPH_DEPLOYMENT_URL指向一个暴露了interrupt_agent图的 LangGraph 部署选择卡片通过useInterrupt({ renderInChat: true })内联渲染在聊天消息流中绑定到后端schedule_meeting工具里 langgraph 的interrupt()原语。2. 后端schedule_meeting工具与interrupt()暂停原语后端实现位于 interrupt_agent.py其注释开宗明义定义一个schedule_meeting(topic, attendee)工具使用 LangGraph 的interrupt()原语暂停本次运行run并把结构化的预约载荷推送到前端前端useInterrupt渲染器以聊天内联时间选择卡片展示最终以{chosen_time, chosen_label}或{cancelled: true}解析resolve工具再据此产出人类可读的结果。关键实现细节from langchain.agents import create_agent from langgraph.types import interrupt from copilotkit import CopilotKitMiddleware tool def schedule_meeting(topic: str, attendee: Optional[str] None) - str: response: Any interrupt( { topic: topic, attendee: attendee, slots: _candidate_slots(), } ) if isinstance(response, dict): if response.get(cancelled): return fUser cancelled. Meeting NOT scheduled: {topic} chosen_label response.get(chosen_label) or response.get(chosen_time) if chosen_label: return fMeeting scheduled for {chosen_label}: {topic} return fUser did not pick a time. Meeting NOT scheduled: {topic}要点interrupt()的语义调用它时 LangGraph 运行暂停把{topic, attendee, slots}结构体转发给客户端前端useInterrupt渲染卡片后调用resolve(...)其返回值就是工具函数里interrupt(...)表达式的结果即response。候选时段动态生成_candidate_slots()基于“当前时间”计算 4 个候选——明天 10:00 / 14:00 与下周一 9:00 / 15:30避免选择器显示过期日期。当周一恰好是明天时days_to_monday 1会把“下周一”再推后一周防止出现两个都叫 “Monday” 的时段源码注释见 interrupt_agent.py 第 40–62 行。时区固定为America/Los_Angeles注释中明确说明这是为了演示截图稳定真实应用应使用用户日历与时区。系统提示词约束工具调用SYSTEM_PROMPT要求“只要用户要求预约/排期必须调用schedule_meeting工具传入简短的topic与attendee”工具返回后简短确认预约结果或取消。这正是 QA 中“普通闲聊不触发选择器”行为背后的提示词依据。Agent 装配create_agent(modelChatOpenAI(...), tools[schedule_meeting], middleware[CopilotKitMiddleware()], system_promptSYSTEM_PROMPT)其中CopilotKitMiddleware负责把 LangGraph 的运行事件含 interrupt桥接到 AG-UI 协议供前端消费。3. 前端useInterrupt内联渲染与恢复路径演示页面位于 page.tsx核心链路CopilotKit runtimeUrl/api/copilotkit agentgen-ui-interrupt div classNameflex justify-center items-center h-screen w-full div classNameh-full w-full max-w-4xl Chat / /div /div /CopilotKitChat组件中的useInterrupt配置同文件第 34–67 行useInterrupt({ agentId: gen-ui-interrupt, renderInChat: true, // 关键卡片作为消息气泡内联进 transcript而不是 portal 到 body render: ({ event, resolve }) { // AG-UI 适配层会把 interrupt 值 JSON 字符串化需要时解析 const raw event.value ?? {}; const payload (typeof raw string ? JSON.parse(raw) : raw) as { topic?: string; attendee?: string; slots?: TimeSlot[]; }; 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 状态再卸载卡片 setTimeout(() resolve(result), 500); }} / ); }, });三个值得注意的实现决策renderInChat: true卡片是 transcript 容器 DOM 树的子节点QA 要求用 DevTools 验证它位于消息气泡之间而非像hitl-in-app那样被 portal 到body。event.value可能是字符串AG-UI 适配层将 interrupt 值做了 JSON 字符串化所以 render 函数中做了typeof raw string ? JSON.parse(raw) : raw的兼容处理。setTimeout(..., 500)延迟 resolve源码注释解释requestAnimationFrame不可靠在某些调度场景下 rAF 先于 React commit 触发用短setTimeout保证用户先看到 “Booked / Cancelled” 徽标卡片再随 interrupt 清除而卸载。3.1 卡片组件的三态time-picker-card.tsx 用一个受控组件表达三个互斥状态状态data-testid呈现待选择time-picker-card头部 eyebrow 徽标 “Book a call”、attendee行“With Alice”、主题标题、描述文案 “Pick a time that works for you.”、2×2 网格的 4 个时段按钮data-testidtime-picker-slot、底部 ghost 按钮 “None of these work”data-testidtime-picker-cancel已选定time-picker-picked绿色描边/底色border-emerald-200 bg-emerald-50/40、“Booked” 徽标 加粗的所选 label所有按钮禁用已取消time-picker-cancelled红色 “Cancelled” 徽标 “No time picked.”点击时段按钮会setPicked(s)并回调onSubmit({ chosen_time: s.iso, chosen_label: s.label })点击 “None of these work” 则回调{ cancelled: true }。disabled picked ! null || cancelled保证一旦进入终态所有按钮立即禁用双击/快速连点只会提交一次选择。3.2 时段回退生成器当 interrupt 载荷缺少slots时前端调用 interrupt-fallback-slots.ts 的generateFallbackSlots()兜底。该文件与interrupt_agent.py逻辑镜像同样按Date.now()生成“明天 10:00/14:00 下周一 9:00/15:30”四个候选注释说明此前硬编码日期“在编写后一周内就腐化了”因此改为相对当前时间生成。3.3 建议话术Suggestionssuggestions.ts 通过useConfigureSuggestions注册两个建议 pillavailable: always“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 中“逐字标题核对”的要求一一对应。4. QA 测试步骤全解以下完整继承 qa/gen-ui-interrupt.md 的检查项便于逐条回归验证。4.1 基础功能访问/demos/gen-ui-interrupt页面应在 3s 内渲染CopilotChat居中于max-w-4xl容器、占满视口高度、rounded-2xl圆角对应 page.tsx 第 16–22 行的布局类名。输入框占位符可见首次加载时 transcript 为空。发送 “Hello”agent 应只以纯文本回复不渲染时间选择器——因为 agent 只在明确要求预约/排期时才调用schedule_meeting。4.2 中断触发与内联渲染点击 “Book a call with sales” 建议或等价提问。20s 内验证agent 调用schedule_meeting后端命中interrupt({topic, attendee})聊天流内联出现data-testidtime-picker-card的选择卡片。用 DevTools 确认卡片是 transcript 容器的后代节点不是 portal 到body的模态区别于hitl-in-app。卡片头部eyebrow “Book a call” 反映 agent 给定 topic 的主题标题例如包含 “sales” / “pricing”。“Pick a time that works for you.” 描述 恰好 4 个按钮的 2×2 网格默认标签为 “Tomorrow 10:00 AM”、“Tomorrow 2:00 PM”、“Monday 9:00 AM”、“Monday 3:30 PM”。网格下方渲染 “None of these work” ghost 按钮。4.3 选时段恢复路径Pick-a-Slot Resume点击任一时段如 “Monday 9:00 AM”。卡片立即切换到已确认态data-testidtime-picker-picked、绿色描边/底色、“Booked for” 且 label 加粗。所有时段按钮禁用透明度降低不可再次点击。agent 在 10s 内恢复运行并回复确认信息后端返回Meeting scheduled for {chosen_label}: {topic}与 interrupt_agent.py 第 94 行的返回串一致。4.4 取消路径Cancel Path发送第二条 prompt“Schedule a 1:1 with Alice next week to review Q2 goals.”验证新的选择卡片内联渲染topic 下方出现 “With Alice” 参加人行。点击 “None of these work”。卡片切换为data-testidtime-picker-cancelled文本 “Cancelled — no time picked.”。agent 恢复并回复会议未排期 / 用户已取消对应后端User cancelled. Meeting NOT scheduled: {topic}。4.5 多轮Multi-Turn完成任一选择/取消路径后再发送 “Book another call tomorrow morning”。验证一张新的、独立的选择卡片内联渲染旧卡片保持其终态interrupt 生命周期干净地重复第二次 resume 端到端可用。4.6 契约检查——interrupt 是低层原语只有工具触发路径渲染选择器发送纯对话消息如 “Whats the weather?”不应渲染选择器。全程不应出现 approval-dialog 风格的模态框本演示是 inline不是 modal。4.7 错误处理发送空消息应为 no-op。快速双击时段按钮只应提交一次选择按钮在首次点击时即禁用见disabled逻辑。以上 pick / cancel / multi-turn 全流程无未捕获的 console 错误。5. 预期结果汇总QA 文档给出的验收基线聊天在 3s 内加载纯文本回复在 10s 内返回排期/预约类 prompt 后 20s 内时间选择卡片内联渲染进聊天选择器通过两种途径之一收敛时段按钮发出{chosen_time, chosen_label}或 “None of these work” 按钮发出{cancelled: true}收敛后卡片只读agent resume 产生引用所选时段 label 或取消事实的确认消息无 UI 布局破损、无未捕获 console 错误、单次 interrupt 不产生重复选择器。6. 延伸阅读useInterrupt的实现与测试useInterrupt的 React 实现位于 packages/react-core/src/v2/hooks/use-interrupt.tsx配套测试在 packages/react-core/src/v2/hooks/tests/use-interrupt.test.tsx其中覆盖了 interrupt 事件注册、resolve回调与渲染位置等契约。若要把本演示的能力迁到自己的项目最小改动面是后端用langgraph.types.interrupt在工具内暂停并发送结构化载荷挂上CopilotKitMiddleware参照interrupt_agent.py前端useInterrupt({ agentId, renderInChat: true, render })在render里解析event.value字符串或对象渲染自己的交互卡片并在用户操作后resolve(...)注意 resume 时机先完成本地状态提交、再resolve演示中用 500mssetTimeout实现避免卡片在未展示终态时卸载。这套“后端interrupt() 前端useInterruptrenderInChat”的组合是 CopilotKit v2 在 LangGraphPython场景下做聊天内 HITL 的标准范式中断载荷即 UI 契约resolve的值即工具返回值。【免费下载链接】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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进