ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

在聊天流内实现人工审批:CopilotKit 的 In-Chat HITL(Booking)前端定义工具实战

2026/9/12 17:49:13 拓冰建站 浏览量
在聊天流内实现人工审批:CopilotKit 的 In-Chat HITL(Booking)前端定义工具实战 在聊天流内实现人工审批CopilotKit 的 In-Chat HITLBooking前端定义工具实战【免费下载链接】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 应用常常需要在对话中途插入人的决策点确认预约时间、审批操作、选择分支等。CopilotKit 的 In-Chat HITLHuman-in-the-Loop提供了一种极简做法——把决策 UI 直接渲染进聊天流由前端用useHumanInTheLoop定义工具后端 agent 完全不需要实现中断逻辑。本文以showcase/integrations/claude-sdk-python集成中的 Booking 示例book_call预约工具为主线逐步拆解前端工具定义、交互卡片渲染、Claude Agent SDK 后端转发以及底层useHumanInTheLoop的原理实现帮助你在自己的 Agent 聊天应用中快速落地同类交互。一、In-Chat HITL 的核心思路前端定义工具后端零中断传统 HITL如 LangGraph 的interrupt()依赖后端把 agent 执行挂起、由外部恢复。而 In-Chat HITL 把等待人工决策这件事完全放在前端book_call工具由前端通过useHumanInTheLoop定义后端没有对应的工具实现后端只是一个基本 Claude Agent SDK 聊天循环负责把 AG-UIAgent UI 协议携带的前端工具定义原样转发给 Claude并让标准 tool-call 生命周期解析用户的最终选择没有任何后端中断工具执行中时前端渲染TimePickerCard时间段选择卡片用户点选后respond(...)把结果像普通工具返回值一样送回 agent。这段描述直接来自示例文档README.md而示例的后端代码注释也印证了这一点The book_call tool is defined on the FRONTEND via useHumanInTheLoop, so there is no backend tool here. The agent simply responds in chat and relies on the standard frontend-tool / tool-call lifecycle to invoke book_call when the user asks to book.引用位置hitl_in_chat_agent.py该 demo 同时提供了 LangGraph 参考实现hitl_in_chat_agent.py后端同样使用tools[]仅挂载CopilotKitMiddleware即可接收前端建议与渲染 hook——两个后端框架殊途同归都证明前端工具是跨框架的统一机制。二、前端第一步用useHumanInTheLoop声明一个聊天内工具示例前端代码位于 page.tsx核心注册代码如下useHumanInTheLoop({ agentId: hitl-in-chat, name: book_call, description: Ask the user to pick a time slot for a call. The picker UI presents fixed candidate slots; the users choice is returned to the agent., parameters: z.object({ topic: z .string() .describe(What the call is about (e.g. Intro with sales)), attendee: z .string() .describe(Who the call is with (e.g. Alice from Sales)), }), render: ({ args, status, respond }: any) ( TimePickerCard topic{args?.topic ?? a call} attendee{args?.attendee} slots{slots} status{status} onSubmit{(result) respond?.(result)} / ), });各字段的作用与约束如下字段含义示例值 / 说明agentId注册到哪个 agent 名下必须与CopilotKit agenthitl-in-chat、后端路由 agent 名一致name工具名是 LLM 调用的标识book_call前后端不实现同名工具description发给 LLM 的工具说明决定模型何时调用要写明UI 会展示候选时间段用户选择会返回给 agentparameterszod schema定义工具入参topic通话主题、attendee通话对象render渲染函数接收args / status / respond渲染TimePickerCard并回传结果2.1 组件挂载与 agent 绑定整个 demo 由CopilotKitProvider 包住runtimeUrl指向/api/copilotkitagent指定后端路由名聊天组件用CopilotChatCopilotKit runtimeUrl/api/copilotkit agenthitl-in-chat ... CopilotChat agentIdhitl-in-chat classNameh-full rounded-2xl / /CopilotKit从源码结构看useHumanInTheLoop依赖useCopilotKit()拿到全局copilotkit实例并通过useFrontendTool在布局阶段完成注册因此它必须在 Provider 内部使用。2.2 预置建议suggestions降低用户输入门槛Chat组件内还调用useConfigureSuggestions提供两条可点击的示例提示方便测试与演示useConfigureSuggestions({ suggestions: [ { title: Book a call with sales, message: Please book an intro call with the sales team to discuss pricing., }, { title: Schedule a 1:1 with Alice, message: Schedule a 1:1 with Alice next week to review Q2 goals., }, ], available: always, });候选时间段由buildDefaultSlots()动态生成默认给出明天 10:00、明天 14:00、下周一 9:00、下周一 15:30四个槽位每个槽位包含label人类可读文案与isoISO 时间戳后续作为工具返回值回传 agent。三、交互卡片TimePickerCard 的状态机决策 UI 是 HITL 的人机接口实现在 time-picker-card.tsx。3.1 状态定义export type TimePickerStatus inProgress | executing | complete;inProgress模型正在生成 tool call卡片可先展示占位信息executing工具正在执行此时respond可用用户可点选complete已提交结果卡片只读。这与 react-core 中ToolCallStatus.InProgress / Executing / Complete三种状态一一对应见 use-human-in-the-loop.tsx组件内部用disabled判断做防重复提交const disabled status ! executing || picked ! null || cancelled;3.2 三种渲染分支组件根据内部状态渲染三种形态并带有data-testid供端到端测试断言已取消显示 Cancelled — no time picked.data-testidtime-picker-cancelled已选择显示 Booked for {label} 绿色确认条data-testidtime-picker-picked待选择展示主题、对象与 2×2 时间段按钮网格以及None of these work取消按钮data-testidtime-picker-card。onClick{() { setPicked(s); onSubmit({ chosen_time: s.iso, chosen_label: s.label }); }}onClick{() { setCancelled(true); onSubmit({ cancelled: true }); }}可以看到用户无论选择还是取消都通过同一个onSubmit回调把结果返回给 agent。返回值有两种合法形状——{ chosen_time, chosen_label }或{ cancelled: true }由TimePickerCardProps.onSubmit类型定义约束这为后端/LLM 判断后续流程提供了依据。四、后端零中断Claude Agent SDK 如何转发前端工具后端实现位于 hitl_in_chat_agent.py是典型的 AG-UI 服务端流式接口接收RunAgentInput产出编码后的 AG-UI 事件流。4.1 系统提示与模型SYSTEM_PROMPT dedent( You help users book an onboarding call with the sales team. When they ask to book a call, call the frontend-provided book_call tool with a short topic and the users name (use a sensible placeholder like Alice from Sales if no attendee was specified). Keep any chat reply to one short sentence. ).strip()调用时模型从环境变量读取normalize_claude_model(os.getenv(ANTHROPIC_MODEL, claude-opus-4-8))max_tokens1024且仅在tools非空时才把工具定义传给 Claude——正常运行时 AG-UI 会带来前端定义的book_call因此实际都会带上。4.2 关键点一把 AG-UI 消息转换为 Anthropic 格式AG-UI 提供三类消息角色后端必须做映射user→ Anthropic 用户消息assistant→ Anthropic assistant 消息若带tool_callsAG-UI 的AssistantMessage把工具调用存在tool_calls字段而非content需转成content里的tool_use块tool→ 已解析的前端工具结果必须转成 Anthropic 的role: usercontent[].type tool_result结构并用tool_use_id与之前的tool_use配对。这正是用户点选时间段后runtime 重新调用 agentagent 必须看到完整 tool 调用历史的底层机制。源码注释明确说明CopilotKit runtime 在用户解析前端工具后例如在book_callHITL UI 里选了一个时间段会重新调用本 agent此时消息数组包含 ①带tool_use的 assistant 消息 ②携带解析结果的 tool 消息。4.3 关键点二把 AG-UI 的前端工具定义转发给 Claudetools: list[dict[str, Any]] [] for t in input_data.tools or []: name getattr(t, name, None) or (t.get(name) if isinstance(t, dict) else None) description getattr(t, description, None) or ( t.get(description, ) if isinstance(t, dict) else ) parameters getattr(t, parameters, None) or ( t.get(parameters, {}) if isinstance(t, dict) else {} ) if not name: continue tools.append({ name: name, description: description or , input_schema: parameters or {type: object, properties: {}}, })AG-UI 的 Tool schema 是{ name, description, parameters }JSON-SchemaClaude API 期望input_schema两者形状一致直接映射即可。这个循环是前端定义工具、后端自动感知的关键——agent 代码里没有任何book_call的硬编码全靠input_data.tools透传。4.4 关键点三流式事件转发run_hitl_in_chat_agent是异步生成器用EventEncoder编码 AG-UI 事件先发RunStartedEvent再发TextMessageStartEvent用client.messages.stream(...)消费 Anthropic 流式响应文本增量 →TextMessageContentEvent工具块开始 →ToolCallStartEvent参数 JSON 增量 →ToolCallArgsEvent工具块结束 →ToolCallEndEvent收尾发TextMessageEndEvent与RunFinishedEvent。工具调用事件的父消息 ID 固定为msg-{run_id}-0从而把工具调用挂到同一条 assistant 消息上前端据此把卡片渲染在正确的气泡上下文里。4.5 为什么不会死循环文档注释说明了这里的回合语义前端useHumanInTheLoop解析book_call后runtime 会把解析结果注入下一轮对话而每一轮对话都是独立的 POST 请求因此本 agent 发完RunFinishedEvent即交还控制权不会在单个请求内自循环。五、底层原理useHumanInTheLoop 在 react-core 中如何工作要理解 In-Chat HITL值得看一眼它的实现use-human-in-the-loop.tsx。5.1 respond 本质是一个可被 resolve 的 PromiseuseHumanInTheLoop内部把respond与一个 promise 的 resolve 函数绑定const respond useCallback(async (result: unknown) { if (resolvePromiseRef.current) { cleanupAbortRef.current?.(); cleanupAbortRef.current null; resolvePromiseRef.current(result); resolvePromiseRef.current null; } }, []);同时它构造了一个handler返回一个在人工响应前一直挂起的 Promise。这个handler就是前端工具的执行体——工具被调用时挂起等待respond(result)被调用时 Promise 被 resolve结果继续走标准 tool-call 生命周期回到 agent。这就是前端在工具执行时渲染卡片、点选后返回结果的底层等价物。5.2 abort 支持取消/中断时不静默丢结果handler接收context.signalAbortSignal若信号已中止则立即 reject否则注册一次性abort监听中止时清理引用并 rejectHuman-in-the-loop interaction aborted。cleanupAbortRef确保 promise 一旦 settlerespond 或 abort监听器立刻移除、不会二次触发或泄漏。useLayoutEffect清理函数还会在卸载时调用copilotkit.removeHookRenderToolCall(tool.name, tool.agentId)避免组件卸载后仍残留渲染器。5.3 渲染器按状态注入 respondRenderComponent根据props.status分支InProgress/Complete注入respond: undefined此时不可交互Executing注入respond此时可交互。也就是说respond只在工具执行中这一生命周期窗口内是活函数与前端组件用status ! executing禁用按钮的逻辑完全一致。5.4 注册机制useFrontendTooluseHumanInTheLoop最终把{ ...tool, handler, render }交给useFrontendTooluse-frontend-tool.tsx后者在useLayoutEffect中同名同 agent 已注册时先removeTool再addTool覆盖式注册并给出 warning通过addHookRenderToolCall注册渲染器注意注释即使parameters未定义也要注册 renderHITL 确认对话框正是这种无参数但有 UI的工具卸载时removeTool但故意不移除 render让历史消息中的卡片仍可渲染。这解释了为什么聊天历史里已经完成的 HITL 卡片依然可见——它们是渲染 hook 而非活动工具。六、运行方式与扩展建议6.1 运行该 demo示例属于 Claude SDK Python 集成示例应用showcase/integrations/claude-sdk-python配置环境变量ANTHROPIC_API_KEY必填、可选ANTHROPIC_MODEL默认claude-opus-4-8启动集成示例的前端src/appNext.js与后端 agent 服务将前端runtimeUrl指向/api/copilotkit路由在聊天中输入Book a call with sales或直接点击预置 suggestion模型将调用book_call聊天流内出现时间选择卡片点选一个时间段或取消结果回传 agentagent 输出简短确认语。注意agent 的系统提示要求任何聊天回复保持一句话目的是让演示聚焦工具交互本身。6.2 扩展思路更多决策形态替换render里的卡片组件即可实现确认弹窗、下拉选择、多选审批等返回值形状由onSubmit类型决定保持与 LLM 可读性即可。多工具 HITL每个决策点声明一个useHumanInTheLoop工具即可若希望一个渲染器覆盖多个工具react-core 支持通配工具名*WILDCARD_TOOL_NAME见 use-human-in-the-loop.tsx此时渲染器收到的props.name是真正被调用的工具名。对比后端中断本方案适合决策点简单、UI 内嵌聊天的场景若需要服务端状态持久化、审批流恢复等能力则考虑基于中断的 HITL 方案。七、小结In-Chat HITL 的精髓在于把人工决策抽象成前端定义的工具前端负责注册工具、渲染交互卡片、回传结果后端只需遵循 AG-UI 协议把input_data.tools里的前端工具定义透传给 LLM再按标准 tool-call 生命周期处理。CopilotKit 的useHumanInTheLoop用 Promise 挂起 respondresolve 渲染器状态注入这套机制在 react-core 中完整实现了这一模式且对 Claude Agent SDK 与 LangGraph 两类后端同样适用。掌握它你就能在任何 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),仅供参考