CopilotKit 工具渲染(Tool Rendering)实战指南:在聊天流中为 Agent 工具调用渲染 React 卡片
【免费下载链接】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
导读
当后端 Agent 在对话中调用工具(查询天气、搜索航班、获取股价、掷骰子)时,CopilotKit 允许开发者在前端把这些工具调用渲染成内嵌在聊天流中的 React 组件,并区分“工具执行中”与“执行完成”两种视觉状态。本文以仓库built-in-agent(TanStack AI 内建 Agent)示例中的 Tool Rendering 演示为对象,逐条解析其 QA 验收清单背后的实现原理与验证方法。读完本文,你将掌握useRenderTool按工具注册专属渲染器、useDefaultRenderTool注册兜底渲染器的完整用法,以及如何用 Playwright 对工具卡片做确定性回归测试。
Demo 概览与运行环境
Tool Rendering 演示位于showcase/integrations/built-in-agent/,页面路径为/demos/tool-rendering。根据 QA 文档 tool-rendering.md,运行该演示需要满足以下前置条件:
| 前置项 | 说明 |
|---|---|
| API Key | 在.env.local(或环境变量)中设置OPENAI_API_KEY |
| 依赖安装 | 在built-in-agent/包目录下执行npm install --legacy-peer-deps |
| 启动 | 在built-in-agent/包目录下执行npm run dev |
| 访问地址 | http://localhost:3000/demos/tool-rendering |
其中--legacy-peer-deps表明该演示依赖树中存在 peer dependency 版本约束,需跳过严格 peer 校验;这也是仓库内多个示例包的通用安装约定。
从页面源码 page.tsx 可以看到,Demo 通过CopilotKit组件挂载运行时,并通过agent="tool-rendering"绑定到后端注册的命名 Agent:
<CopilotKit runtimeUrl="/api/copilotkit" agent="tool-rendering"> <CopilotChat agentId="tool-rendering" className="h-full rounded-2xl" /> </CopilotKit>对应的后端路由在 route.ts 中注册为"tool-rendering": createBuiltInAgent(),由InMemoryAgentRunner在进程内运行。也就是说,这个 Demo 的 Agent 与前端同属一个 Next.js 应用,工具调用通过/api/copilotkit单路由(mode: "single-route")完成。
三层工具渲染架构:专属渲染器 + 兜底渲染器
演示 README(README.md)概括了核心机制:后端 Agent 的工具调用被渲染为聊天流中的 React 组件;前端用useRenderTool按工具名注册渲染器,接收args、result、status三个输入,从而让 UI 同时反映“进行中”与“已完成”两种状态。
在 page.tsx 的注释中可以看到该 Demo 的完整注册清单,这正是“三层”结构:
| 后端工具 | 前端渲染器 | 类型 |
|---|---|---|
get_weather | <WeatherCard /> | 专属渲染器 |
search_flights | <FlightListCard /> | 专属渲染器 |
get_stock_price | <StockCard /> | 专属渲染器 |
roll_d20 | <D20Card /> | 专属渲染器 |
| 其他任意工具 | <CustomCatchallRenderer /> | 兜底渲染器 |
专属渲染器:useRenderTool
useRenderTool是@copilotkit/react-core/v2提供的 Hook,用于为特定工具名注册渲染函数。以天气工具为例(page.tsx):
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} /> ); }, }, [], );几个关键点:
parameters用 zod 描述入参契约(与后端inputSchema对应),既是运行时校验,也是类型推导依据;render回调每次工具状态变化时都会重新执行,因此loading = status !== "complete"可以在执行期间显示加载态;result在完成前通常是原始字符串,Demo 通过共享工具函数parseJsonResult(位于 parse-json-result.ts)将其安全解析为结构化对象;- 注册后传入的依赖数组
[]与 React Hook 约定一致,需保持引用稳定。
航班搜索(page.tsx)、股价(L126-L146)、d20 掷骰(L150-L169)三个工具遵循完全相同的模式,分别渲染FlightListCard、StockCard、D20Card。
兜底渲染器:useDefaultRenderTool
并非每个后端工具都值得定制 UI。对于未被专属渲染器认领的工具调用,useDefaultRenderTool提供了通配兜底(page.tsx):
useDefaultRenderTool( { render: ({ name, parameters, status, result }) => ( <CustomCatchallRenderer name={name} parameters={parameters} status={status as CatchallToolStatus} result={result} /> ), }, [], );兜底渲染器 custom-catchall-renderer.tsx 是一个自包含的“通用工具卡”,展示工具名、状态徽章(streaming/running/done三态)、格式化后的参数 JSON 与结果 JSON。CatchallToolStatus类型定义在 custom-catchall-renderer.tsx,取值为"inProgress" | "executing" | "complete",其状态描述逻辑在describeStatus函数中实现。
这套“专属优先、兜底托底”的设计意味着:新增后端工具时,前端无需改动即可自动获得可读的工具卡片;需要品牌化展示时再补一个专属渲染器即可。
QA 一:页面加载检查
QA 清单的第一组断言聚焦页面初始状态:
- 页面标题 "Tool Rendering" 可见;
- 提示文本
Try: "What's the weather in Tokyo?"可见; - 聊天输入框可见。
从体验设计看,这组断言对应 suggestions.ts 中通过useConfigureSuggestions配置的 5 个建议药丸(suggestion pill):
| 药丸标题 | 对应消息 |
|---|---|
| Weather in SF | What's the weather in San Francisco? |
| Find flights | Find flights from SFO to JFK. |
| Stock price | What's the current price of AAPL? |
| Roll a d20 | Roll a 20-sided die. |
| Chain tools | 单轮内链式调用:东京天气 + SFO→东京航班 + 掷 d20 |
这些药丸是 QA 交互的快捷入口,同时也是 e2e 测试的确定性触发源。对应验证逻辑可参见 e2e 用例 tool-rendering.spec.ts,其中逐一对 5 个药丸标题做了可见性断言。
QA 二:快乐路径交互(Happy Path)
快乐路径是工具渲染的核心体验验证,QA 文档要求逐步检查三个场景。
场景 1:执行中状态可见
发送 "What's the weather in Tokyo?" 后,工具执行期间应出现一张内联的 "Fetching weather…" 进行中卡片。
这一行为的实现就在WeatherCard的加载态分支(weather-card.tsx):loading为真时,副标题位置渲染"Fetching weather..."文本,右侧主视觉替换为省略号占位,同时隐藏温度、湿度、风力等数据区块(weather-card.tsx)。也就是说,“进行中卡片”与“完成卡片”是同一张卡片组件在两个状态下的切换,而不是两套独立 UI。
场景 2:完成态数据正确
Agent 执行完毕后,卡片应显示:城市名、温度(°F)、天气状况文本、湿度百分比。
WeatherCard渲染的字段与后端返回一一对应(server-tools.ts):
export const getWeatherTool = toolDefinition({ name: "get_weather", description: "Get the current weather for a given location. ...", inputSchema: z.object({ location: z.string(), }), }).server(async ({ location }) => ({ city: location, temperature: 68, humidity: 55, wind_speed: 10, conditions: "Sunny", }));后端temperature、humidity、wind_speed、conditions四个字段分别映射到卡片的温度({temperature}°F)、湿度({humidity}%)、风速({windSpeed} mph)与状况文本;状况还会被conditionsEmoji映射为 emoji 图标(weather-card.tsx)。
注意这里getWeatherTool返回的是 mock 数据(固定 68°F / 55% / 10 mph / Sunny),这正是 QA 与 e2e 断言能够确定性的基础。
场景 3:多次工具调用的隔离
继续询问第二座城市(如 "And what about London?"),应出现第二张天气卡片,且不得干扰第一张。
这个“每张卡片独立”的行为源于渲染模型:每个工具调用都会以独立的 React 节点挂载到消息流中。d20 卡片是这一点的极端体现——每次掷骰都会产生一张独立卡片,e2e 用轮询断言“恰好出现 5 张 d20 卡片,且最后一张结果为 20”(tool-rendering.spec.ts)。而 "Chain tools" 药丸则验证单轮内多工具并存的场景:一次提问同时渲染出天气卡、航班卡和 d20 卡(tool-rendering.spec.ts)。
QA 三:边界用例检查
边界 1:非 weather 工具走兜底渲染器
发送触发weather之外工具的消息(如 "Tell me today's date"),应渲染通用工具卡——显示工具名、状态和 JSON 载荷——而不是自定义天气卡。
这正是useDefaultRenderTool兜底渲染器的工作。CustomCatchallRenderer携带稳定的data-testid="custom-catchall-card"与data-tool-name={name}、data-status={status}属性(custom-catchall-renderer.tsx),便于自动化定位。执行期间结果区显示 "waiting for tool to finish…",完成后通过parseResult尝试 JSON 解析并美化为缩进展示。
这个设计同时印证了一个关键约束:当后端新增一个没有专属渲染器的工具时,前端不会白屏或丢失信息,兜底卡会立即接管展示。
边界 2:纯对话消息不渲染工具卡
发送普通对话消息(如 "Hi there"),Agent 正常回复,且不出现任何工具卡片。
这验证的是渲染器的触发条件:只有存在真实的工具调用流时,useRenderTool/useDefaultRenderTool的render才会被调用;纯文本回复不经过工具渲染路径。这一行为保证了工具卡片不会误伤正常对话体验。
补充:状态机视角
从兜底渲染器的状态徽章可以反推工具生命周期:inProgress(streaming,流式输出中)→executing(running,工具执行中)→complete(done,完成)。专属卡片则把状态折叠为二元判断status !== "complete"。理解这一状态序列,有助于设计自己的卡片加载态与错误态。
后端工具契约:与渲染器对齐的输入输出
工具渲染之所以能“前后端对齐”,是因为工具定义集中在 server-tools.ts 中,并通过baseServerTools数组挂载到createBuiltInAgent()。除天气外,Tool Rendering 相关的还有:
searchFlightsTool(server-tools.ts):入参origin/destination,返回 3 条确定性 mock 航班(UA231、DL412、B6722),供FlightListCard渲染“航空公司 + 航班号 + 起降时间 + 价格”列表;getStockPriceTool(L93-L109):入参ticker,返回随机价格与涨跌幅,StockCard据此显示$price与带正负号的change%(涨绿跌红,见 stock-card.tsx);rollD20Tool(L136-L145):入参sides(默认 6),返回{ sides, result },D20Card在结果为 20 时显示 "critical!" 徽章并加高亮描边(d20-card.tsx)。
工具描述文本(description)还承担着提示工程职责:例如get_weather的描述提示模型“提到城市时也考虑查航班”,search_flights描述规定“未匹配到出发地时默认 SFO”。这解释了为什么 "Chain tools" 药丸能可靠地触发多工具链式调用。
e2e 自动化验证:把 QA 清单变成可回归的测试
QA 文档的每一条检查项,在 tool-rendering.spec.ts 中都有对应的 Playwright 用例,形成 6 个测试的完整闭环:
| e2e 用例 | 对应 QA 检查 | 关键断言 |
|---|---|---|
| 页面加载与 5 个建议药丸 | 页面加载 | 各药丸标题可见 |
| Weather in SF 药丸 | 快乐路径 | weather-card可见,城市为 "San Francisco"、湿度 "55%"、风速含 "10" |
| Find flights 药丸 | 快乐路径 | flights-card可见,SFO → JFK,至少 2 行flight-row |
| Stock price 药丸 | 快乐路径 | stock-card可见,ticker 为 "AAPL"、价格 "$338.37"、涨跌 "-2.96%" |
| Roll a d20 药丸 | 隔离性 | 恰好 5 张d20-card,最后一张值为 "20",前 4 张非 20 |
| Chain tools 药丸 | 多工具并存 | 同一轮内天气卡、航班卡、d20 卡全部可见 |
测试注脚(tool-rendering.spec.ts)说明了确定性来源:aimock fixtures 位于showcase/aimock/d5-all.json,把每个药丸提示固定到确定的工具调用序列;卡片则依赖稳定data-testid定位。测试超时分为建议加载(15s)与工具执行(60s)两档。
这说明 QA 文档并非一次性人工清单,而是与自动化回归测试一一对应的“可执行验收规范”。如果你要复现验证,只需要在满足前置条件后访问/demos/tool-rendering,依次点击药丸并按 QA 清单勾选;要自动化,则可直接复用上述 spec 的定位策略(data-testid="weather-card"、weather-city、weather-humidity等)。
变体与延伸:工具渲染的进阶形态
工具渲染在本仓库内还有多个进阶变体,可作为继续深入的方向:
tool-rendering-default-catchall:展示框架内置的默认兜底渲染器(shadcn 风格),位于 tool-rendering-default-catchall;tool-rendering-custom-catchall:仅演示自定义兜底渲染器,见 tool-rendering-custom-catchall;tool-rendering-reasoning-chain:在工具渲染之上叠加可见的推理链(reasoning)展示,见 tool-rendering-reasoning-chain;headless-complete的 tools 目录 提供了weather-card.tsx、generic-tool-card.tsx、chart-card.tsx等更丰富的渲染器集合,可参考其无头模式下的注册方式。
这三个变体各自带有独立的 e2e spec(tool-rendering-default-catchall.spec.ts、tool-rendering-custom-catchall.spec.ts、tool-rendering-reasoning-chain.spec.ts),验证方式与本 Demo 一脉相承。
验收核对表:一次完整的 QA 执行
最后,汇总成可直接执行的核对表(对应 tool-rendering.md 全量条目):
准备:设置OPENAI_API_KEY→npm install --legacy-peer-deps && npm run dev→ 打开http://localhost:3000/demos/tool-rendering
页面加载
- 标题 "Tool Rendering" 可见
- 提示文本 "What's the weather in Tokyo?" 可见
- 聊天输入框可见
快乐路径
- 发送天气问题后,出现 "Fetching weather…" 进行中卡片
- 完成后卡片显示城市、温度(°F)、状况、湿度
- 追问第二城市,第二张卡片渲染且不影响第一张
边界用例
- 触发非
weather工具时,渲染通用工具卡(工具名 + 状态 + JSON 载荷) - 发送纯对话消息,正常回复且无任何工具卡片
至此,你既掌握了 CopilotKit 工具渲染的前端注册机制(useRenderTool+useDefaultRenderTool),也拥有了完整的验收标准与自动化回归测试方法,可以直接迁移到自己的 Agent 前端项目中。
【免费下载链接】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),仅供参考