CopilotKit 工具调用渲染(Tool Rendering)实战指南:基于 Langroid 集成的useRenderTool全解析
【免费下载链接】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 仓库 Langroid 集成中的tool-rendering演示单元为核心,完整讲解如何将后端 Agent 的工具调用(Tool Call)渲染为聊天记录(transcript)中的 React 组件。你将掌握useRenderTool按工具名注册专属渲染器、用useDefaultRenderTool注册通配兜底渲染器的完整写法,理解args/result/status三种运行时状态的驱动机制,并看到配套的 e2e 测试如何用稳定 testid 与确定性 fixture 验证渲染结果。
一、背景:从"工具调用的文本回显"到"结构化 UI 渲染"
在 Agent 应用中,后端模型经常会调用工具(查天气、搜航班、查股价、掷骰子)。传统聊天界面只能把这类调用显示成一行文本或原始 JSON,信息密度低、观感生硬。CopilotKit 的 Tool Rendering 机制解决了这个问题:后端 Agent 的工具调用会被前端渲染为聊天记录中的 React 组件,UI 可以同时反映"调用中"(in-flight)与"已完成"(completed)两种状态。
仓库中的规范描述(演示单元 README)明确给出了这套机制的核心 API:
前端使用
useRenderTool按工具名注册渲染器,渲染器接收args、result和status,从而让 UI 既能反映进行中的调用,也能反映已完成的调用。
README 同时说明,该演示的权威描述位于 showcase manifest(manifest.yaml),本 README 是随源码附带给开发者的说明。该演示在 manifest 中被标记为generative-ui分类,描述为"在聊天流中为工具调用提供自定义渲染"(Custom render for tool calls inline in the chat stream),并指向了 agent.py、search_flights.py、get_weather.py 等后端工具与前端 page.tsx 作为高亮文件。
二、整体架构:一个完整的 Tool Rendering 演示长什么样
tool-rendering演示单元位于 showcase/integrations/langroid/src/app/demos/tool-rendering/,目录结构如下:
tool-rendering/ ├── README.md # 开发者说明(本文档主体) ├── page.tsx # 演示入口:注册 4 个 per-tool 渲染器 + 1 个 catch-all ├── weather-card.tsx # get_weather → 品牌化天气卡片 ├── flight-list-card.tsx # search_flights → 航班列表卡片 ├── stock-card.tsx # get_stock_price → 股价卡片 ├── d20-card.tsx # roll_d20 → 掷骰子卡片 ├── custom-catchall-renderer.tsx # 通配兜底渲染器(展示工具名/状态/参数/结果) └── suggestions.ts # 5 个建议提示词(suggestion pills)该演示在源码注释中被定位为"三向进阶中最复杂的一点"("The most sophisticated point in the three-way progression"):每个"有趣"的后端工具都有专属的品牌化 UI,同时一个 catch-all 兜底渲染器负责渲染所有漏网的工具调用。三种进阶形态对应三个独立演示单元:
| 演示单元 | 目录 | 核心差异 |
|---|---|---|
| Tool Rendering(本篇) | tool-rendering/ | 每个工具专属渲染器 + 自定义 catch-all |
| Default Catchall | tool-rendering-default-catchall/ | 只调用useDefaultRenderTool(),使用内置默认卡片 |
| Custom Catchall | tool-rendering-custom-catchall/ | 只使用自定义 catch-all 渲染所有工具 |
从 manifest 看,这三个单元与tool-rendering-reasoning-chain(在 tool-rendering 基础上叠加推理链渲染)共同构成了完整的 Tool Rendering 演示家族(manifest.yaml 第 25、44、60-61、142-156 行)。
三、核心 API:useRenderTool的签名与状态模型
3.1 Hook 签名与配置项
useRenderTool的完整签名定义在 packages/react-core/src/v2/hooks/use-render-tool.tsx:
useRenderTool<S extends StandardSchemaV1>( config: { name: string; // 要渲染的工具名 parameters: S; // 工具参数 schema(Standard Schema V1 兼容:Zod、Valibot、ArkType 等) render: (props: RenderToolProps<S>) => React.ReactElement | null; agentId?: string; // 可选:按 agent 隔离注册,默认作用于当前 agent }, deps?: ReadonlyArray<unknown>, // 可选:依赖变化时刷新注册 ): void;render回调接收的props是一个随状态变化的可辨识联合类型(discriminated union),由三个接口组成(use-render-tool.tsx 第 9-36 行):
| 状态(status) | parameters | result | 语义 |
|---|---|---|---|
inProgress | Partial<...>(参数可能尚未完整) | undefined | 工具调用已发起、参数流式到达中 |
executing | 完整类型输出 | undefined | 工具正在后端执行 |
complete | 完整类型输出 | string(工具返回值) | 工具执行完成,拿到结果 |
3.2 注册行为与去重语义
从实现看(use-render-tool.tsx 第 156-193 行),useRenderTool内部通过defineToolCallRenderer构造渲染器并调用copilotkit.addHookRenderToolCall(renderer)注册:
- 按
agentId:name去重,后注册者生效(latest registration wins); - 组件卸载时不清理注册项("No cleanup removal — keeps renderer for chat history"),这样历史聊天中的工具调用仍然可以正确渲染——这与
useFrontendTool的行为保持一致; - 当
deps变化时重新注册(依赖数组通过JSON.stringify(extraDeps)参与 effect 依赖)。
3.3 默认通配渲染器useDefaultRenderTool
useDefaultRenderTool是对useRenderTool({ name: "*" })的封装(use-default-render-tool.tsx 第 128-156 行),注册通配符"*"渲染器,作为没有任何按名注册匹配时的兜底:
- 无参调用
useDefaultRenderTool()→ 使用框架内置的DefaultToolCallRenderer,即开箱即用的可折叠卡片(显示工具名、Running/Done 状态徽章、Arguments 与 Result 的<pre>块,见 DefaultToolCallRenderer 实现); - 传入
config.render→ 替换为自定义兜底 UI。useDefaultRenderTool内部通过adaptRendererProps把框架内部形状(args+ToolCallStatus枚举)适配为文档化形状(parameters+"inProgress" | "executing" | "complete"字符串联合),未知状态值只会首次出现时打印一次告警并回退到inProgress(mapToolCallStatus 实现)。
四、实战:在 Langroid 集成中注册 4 个专属渲染器
演示前端入口 page.tsx 完整展示了按工具注册专属渲染器的模式。页面以CopilotKit runtimeUrl="/api/copilotkit" agent="tool-rendering"挂载运行时,Chat组件内部依次注册渲染器。
4.1 天气工具:get_weather→ WeatherCard
useRenderTool( { name: "get_weather", parameters: z.object({ location: z.string(), }), render: ({ parameters, result, status }) => { const loading = status !== "complete"; const parsed = parseJsonResult<WeatherResult>(result); return ( <WeatherCard loading={loading} location={parameters?.location ?? parsed.city ?? ""} temperature={parsed.temperature} humidity={parsed.humidity} windSpeed={parsed.wind_speed} conditions={parsed.conditions} /> ); }, }, [], );要点拆解:
- 状态驱动 loading:
status !== "complete"时loading=true,卡片显示 "Fetching weather..." 占位,避免聊天界面看起来"卡死"; - 参数与结果双来源兜底:显示城市名时优先用流式参数
parameters?.location,回退到解析后的parsed.city。parseJsonResult是一个健壮的解析工具(定义在 showcase/integrations/langroid/src/app/demos/_shared/parse-json-result.ts),它同时兼容"工具结果是 JSON 字符串"和"运行时已解析为对象"两种形态,解析失败时安全返回{}; - WeatherCard 组件(weather-card.tsx)展示城市、温度(华氏)、湿度、风速、天气条件文本,并根据条件关键词(sun/clear、rain/storm、cloud、snow)映射对应的天气图标。
4.2 航班工具:search_flights→ FlightListCard
useRenderTool( { name: "search_flights", parameters: z.object({ origin: z.string(), destination: z.string(), }), render: ({ parameters, result, status }) => { const loading = status !== "complete"; const parsed = parseJsonResult<FlightSearchResult>(result); return ( <FlightListCard loading={loading} origin={parameters?.origin ?? parsed.origin ?? ""} destination={parameters?.destination ?? parsed.destination ?? ""} flights={parsed.flights ?? []} /> ); }, }, [], );FlightListCard(flight-list-card.tsx)是一个"富渲染"示例:加载中显示 3 个骨架屏(Skeleton动画块)与 "searching…" 标签,完成后展示每班航班的航空公司、航班号、起降时间和价格。它的注释明确说明:卡片只在后端返回后渲染完整结果,工具运行期间以紧凑的 loading 状态呈现。
4.3 股价工具:get_stock_price→ StockCard
useRenderTool( { name: "get_stock_price", parameters: z.object({ ticker: z.string(), }), render: ({ parameters, result, status }) => { const loading = status !== "complete"; const parsed = parseJsonResult<StockResult>(result); return ( <StockCard loading={loading} ticker={parameters?.ticker ?? parsed.ticker ?? ""} priceUsd={parsed.price_usd} changePct={parsed.change_pct} /> ); }, }, [], );StockCard(stock-card.tsx)会在加载中显示 "fetching…" 标签,完成后以涨绿(#189370)跌红(#D14343)的颜色编码展示价格与涨跌幅,涨跌幅为+2.50%/-2.96%格式。
4.4 掷骰工具:roll_d20→ D20Card
useRenderTool( { name: "roll_d20", parameters: z.object({ value: z.number().optional(), }), render: ({ result, status }) => { const loading = status !== "complete"; const parsed = parseJsonResult<D20Result>(result); const value = typeof parsed.value === "number" ? parsed.value : typeof parsed.result === "number" ? parsed.result : undefined; return <D20Card loading={loading} value={value} />; }, }, [], );这里体现了工具的参数 schema 可以与后端实际返回结构解耦:前端 schema 声明value参数,但后端 Langroid 工具 roll_dice.py 返回的是{"sides": sides, "result": random.randint(1, sides)},因此渲染器同时兼容parsed.value与parsed.result两种取值。D20Card(d20-card.tsx)在掷出 20 时展示 "critical!" 徽章与绿色高亮环,加载中显示省略号。
五、兜底方案:用useDefaultRenderTool接住一切漏网工具
即使做了精细的按名注册,Agent 仍可能调用未预料的工具。演示使用useDefaultRenderTool注册了一个通配兜底渲染器(page.tsx 第 172-188 行):
useDefaultRenderTool( { render: ({ name, parameters, status, result }) => ( <CustomCatchallRenderer name={name} parameters={parameters} status={status as CatchallToolStatus} result={result} /> ), }, [], );CustomCatchallRenderer(custom-catchall-renderer.tsx)是一个完全自包含的通用卡片:
- 头部:显示工具名 + 状态徽章,状态徽章有三种视觉形态(源码中的
describeStatus):inProgress→ "streaming"(琥珀色);executing→ "running"(紫色);complete→ "done"(绿色);
- Arguments 区:
JSON.stringify(parameters, null, 2)格式化展示(经safeStringify防止循环引用崩溃); - Result 区:完成后解析并美化展示结果 JSON,未完成时显示 "waiting for tool to finish…"。
六、后端侧如何产出工具调用:Langroid Agent 视角
Tool Rendering 的数据源头在后端 Langroid Agent。演示后端位于 showcase/integrations/langroid/src/agents/agent.py,它把共享工具实现(get_weather_impl、search_flights_impl等)封装成 LangroidToolMessage子类。以GetWeatherTool为例(agent.py 第 102-116 行):
class GetWeatherTool(ToolMessage): request: str = "get_weather" purpose: str = "Get current weather for a location." location: str def handle(self) -> str: try: result = get_weather_impl(self.location) return _json_dumps(result) except Exception as exc: logger.exception("GetWeatherTool.handle failed") return _tool_error( error=_ToolErrorKind.GET_WEATHER_FAILED, message=f"{exc.__class__.__name__}: {str(exc)[:200]}", )关键设计:
request字段即工具名("get_weather"),与前端useRenderTool({ name: "get_weather" })一一对应;handle()返回 JSON 字符串(经_json_dumps),这正是前端render回调中result为字符串的原因;因此parseJsonResult需要先JSON.parse再使用;- 失败时返回结构化的
{"error": "get_weather_failed", "message": ...}(_ToolErrorKind枚举保证错误码封闭),前端可在complete状态中检测error字段做降级展示。
实际工具实现位于 tools/get_weather.py(以城市名为种子随机数生成稳定的模拟天气)与 tools/search_flights.py(返回航班数据;该实现同时携带 A2UI 操作负载,属于该仓库集成层的高级能力)。
七、如何运行与验证:suggestion pills 与 e2e 测试
7.1 一键试用的建议提示词
为了便于人工验证,演示通过 suggestions.ts 用useConfigureSuggestions注册了 5 个 suggestion pills(available: "always"),每个对应一条工具调用路径:
| 建议标题 | 消息 | 触发的工具 |
|---|---|---|
| Weather in SF | What's the weather in San Francisco? | get_weather |
| Find flights | Find flights from SFO to JFK. | search_flights |
| Stock price | What's the current price of AAPL? | get_stock_price |
| Roll a d20 | Roll a 20-sided die. | roll_d20(连续 5 次) |
| Chain tools | 单轮串联天气 + 航班 + 掷骰 | 多工具组合 |
7.2 e2e 测试如何验证渲染结果
该演示配套了完整的 Playwright e2e 测试(tests/e2e/tool-rendering.spec.ts),通过稳定的data-testid与确定性 fixture 值做断言,是理解渲染行为的"活文档":
- 加载态:页面加载后输入框可见,5 个 suggestion pills 全部渲染;
- 天气卡片:点击 "Weather in SF" 后
data-testid="weather-card"可见,城市显示 "San Francisco",湿度含 "55%",风速含 "10"; - 航班卡片:
data-testid="flights-card"中 origin/destination 为 "SFO"/"JFK",且flight-row数量 ≥ 2(来自确定性 fixture,而非通用 A2UI shell); - 股价卡片:ticker 为 "AAPL",价格 "$338.37",涨跌幅 "-2.96%";
- 掷骰卡片:d20 mock 被脚本化为恰好 5 次连续调用,返回
[7, 14, 3, 19, 20],因此断言恰好渲染 5 张d20-card、最后一张显示 "20"、前四张均非 20; - 多工具串联:"Chain tools" 一次会话内同时渲染 weather、flights、d20 三类卡片。
测试还提供了人工 QA 步骤(qa/tool-rendering.md),覆盖天气卡片各字段(温度、湿度、风速、体感、条件图标与主题色)、多城市多次查询不互相干扰等场景。
八、模式总结:何时用useRenderTool,何时用useDefaultRenderTool
通过本篇演示,可以提炼出一套清晰的选型规则:
| 场景 | 方案 | 参考实现 |
|---|---|---|
| 某个工具需要品牌化/富交互UI(图表、列表、卡片) | useRenderTool({ name, parameters, render }) | 本演示 4 个专属渲染器 |
| 需要兜底渲染所有未逐一注册的工具 | useDefaultRenderTool()(内置卡片)或useDefaultRenderTool({ render })(自定义兜底) | page.tsx |
| 只想展示最朴素的开箱即用 UI | 只调useDefaultRenderTool(),不注册任何按名渲染器 | tool-rendering-default-catchall/page.tsx |
| 需要在 tool-rendering 基础上叠加推理链展示 | 参考 reasoning-chain 变体 | tool-rendering-reasoning-chain/ |
三条工程经验值得在项目中复用:
- 永远注册一个兜底渲染器:
useDefaultRenderTool从源码注释看是"运行时没有任何*渲染器时,工具调用将回退到null、在聊天中不可见"的关键保险(见 tool-rendering-default-catchall/page.tsx 注释); - 用状态机驱动 UI:
status三态(inProgress/executing/complete)与result === undefined的组合,足以表达"流式参数到达 → 后端执行 → 拿到结果"的完整生命周期; - 给渲染卡片打稳定的
data-testid:本演示所有卡片(weather-card、flights-card、stock-card、d20-card、custom-catchall-card)都携带 testid 与数据属性,使 e2e 断言与后续回归测试变得可靠。
从源码结构看,这套 Tool Rendering 机制位于@copilotkit/react-core/v2的 hook 层,Langroid 集成演示是它在真实 Agent 后端(LangroidToolMessage+ AG-UI 适配)之上最完整的落地样例;同样的渲染 API 也出现在仓库内其他集成的 headless-complete 等演示中,可作为跨框架复用的参考。
【免费下载链接】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),仅供参考