主题
基于工具的生成式 UI
把 CrewAI 智能体的工具调用映射到 React 组件,并在参数到达时将其流式传入。
将工具调用渲染为组件
当你的 Crew 或 Flow 调用工具时,你通常不希望把原始参数直接倒进聊天里。基于工具的生成式 UI 会把智能体调用的每个工具映射到一个你自己拥有的 React 组件。智能体决定何时调用工具;你决定用户看到什么。
由于 CopilotKit 会在模型生成工具调用的同时把它流式传输到前端,参数会逐步填充。你的组件可以在第一个字段到达时就绘制出来,并在其余内容流入时更新。
本指南构建一个俳句生成器:智能体调用 generate_haiku 工具,前端把每首俳句渲染为一张卡片。它假定你已经有一个与 Next.js 应用通信的 Crew 或 Flow。如果没有,请从 前端概览 开始,了解完整的服务端、运行时与服务商设置。
NOTE
工具渲染对 Crews 和 Flows 都适用。下面的示例使用 Flow,但无论哪种方式,前端接线完全相同。
分步讲解
Define the tool on the backend
用 JSON schema 声明该工具并传给模型。copilotkit_stream 包装器配合 stream=True,才能在工具调用生成时逐个参数分块地将其流式传输到前端。
python
# haiku_flow.py
from crewai.flow.flow import Flow, start
from litellm import acompletion
from ag_ui_crewai.sdk import copilotkit_stream, CopilotKitState
GENERATE_HAIKU_TOOL = {
"type": "function",
"function": {
"name": "generate_haiku",
"description": "Generate a haiku in Japanese and its English translation",
"parameters": {
"type": "object",
"properties": {
"japanese": {
"type": "array",
"items": {"type": "string"},
"description": "Three lines in Japanese",
},
"english": {
"type": "array",
"items": {"type": "string"},
"description": "Three lines in English",
},
},
"required": ["japanese", "english"],
},
},
}
class HaikuFlow(Flow[CopilotKitState]):
@start()
async def chat(self):
system_prompt = "You help the user write haikus. Use the generate_haiku tool."
response = await copilotkit_stream(
await acompletion(
model="openai/gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
*self.state.messages,
],
tools=[GENERATE_HAIKU_TOOL],
parallel_tool_calls=False,
stream=True,
)
)
message = response.choices[0].message
self.state.messages.append(message)
if message.tool_calls:
self.state.messages.append({
"tool_call_id": message.tool_calls[0].id,
"role": "tool",
"content": "Haiku generated.",
})该工具没有 Python 实现。它存在只是为了让模型发出一个前端可渲染的结构化调用。调用之后,追加一条简短的工工具结果,使对话在下一轮保持格式良好。
Serve the Flow over AG-UI
在 FastAPI 应用中用独立路径暴露该 Flow:
python
# server.py
from fastapi import FastAPI
from ag_ui_crewai.endpoint import add_crewai_flow_fastapi_endpoint
from haiku_flow import HaikuFlow
app = FastAPI(title="CrewAI Agent Server")
add_crewai_flow_fastapi_endpoint(
app=app,
flow=HaikuFlow(),
path="/haiku",
)把智能体注册到 CopilotKit 运行时,并完全按 前端概览 所示将 <CopilotKit> 指向它。本指南其余部分假定该智能体以 id haiku 注册。
Register the rendering component
在前端,用与后端声明的相同 name 调用 useRenderTool。useRenderTool 是用于渲染工具调用的 hook:它接收一个 render 函数,没有要执行的东西,因为这个工具纯粹用于展示。
NOTE
当工具只绘制 UI 时使用 useRenderTool。如果该工具还需要在浏览器中运行某些东西,请改用 useFrontendTool,它把 handler 与可选的 render 配对。
tsx
"use client";
import { useRenderTool } from "@copilotkit/react-core/v2";
import { z } from "zod";
useRenderTool({
name: "generate_haiku",
parameters: z.object({
japanese: z.array(z.string()),
english: z.array(z.string()),
}),
render: ({ args, status }) => {
if (!args.japanese) return <></>; // still streaming
return <HaikuCard japanese={args.japanese} english={args.english} />;
},
});该工具由 <CopilotKit agent="haiku"> provider 限定到活动智能体,因此这里不需要 agentId。有几点需要注意:
name必须与后端工具名完全一致(generate_haiku)。正是这种匹配让 CopilotKit 把调用路由到这个组件。render接收{ args, status }。 随着模型流式传输调用,args会逐步填充;早期它可能为空或不完整。如果你想在参数流式传输期间显示加载状态,status会经历"inProgress"/"executing"到"complete"。- 对不完整参数做防护。 在你需要的字段存在之前返回空片段。这里我们先等待
args.japanese,再渲染卡片。
Render the haiku
render 函数委托给一个普通的 React 组件。它没有任何 CopilotKit 特有的东西:接收 props 并返回标记。
tsx
function HaikuCard({
japanese,
english,
}: {
japanese: string[];
english: string[];
}) {
return (
<div className="haiku-card">
{japanese.map((line, i) => (
<div key={i} className="haiku-line">
<span className="jp">{line}</span>
<span className="en">{english?.[i]}</span>
</div>
))}
</div>
);
}由于 english 与 japanese 一同流式传入,请使用可选访问(english?.[i]),这样在译文仍在到达时卡片也能干净地渲染。
Run it
启动两个进程,向助手要一首俳句。随着参数流式传入,卡片会逐行填充渲染。
bash
uvicorn server:app --port 8000 # terminal 1
npm run dev # terminal 2渐进式渲染的工作原理
模型不会一次性发出工具调用。它流式输出 token,CopilotKit 会在每一块新参数到达时重新调用你的 render 函数:
- 调用开始。
args为空,因此你的防护返回空片段。 args.japanese逐行填充。卡片出现并增长。args.english填充。译文各就其位。- 调用完成。
args持有最终且完全校验过的对象。
这就是不完整参数防护很重要的原因:render 按设计就是针对不完整数据运行的。只读取你已有的字段,让其余部分在到达时渲染出来。
后端工具
上面的 generate_haiku 工具没有 Python 实现——它存在只是为了让模型发出一个前端可渲染的结构化调用。但由你的 Crew 或 Flow 在服务端运行的真实工具的渲染方式相同。
当智能体或 Crew 在运行期间执行工具时,桥接层会呈现该工具调用及其结果。为该工具名注册一个 useRenderTool,并在 render 中读取 result:
tsx
useRenderTool({
name: "get_weather",
parameters: z.object({ location: z.string() }),
render: ({ args, result, status }) => {
if (status !== "complete") return <WeatherSkeleton location={args.location} />;
return <WeatherCard data={JSON.parse(result)} />;
},
});NOTE
后端工具必须返回 JSON 字符串,而不是 Python dict。桥接层会把工具输出字符串化,因此原始 dict 会以 Python repr 形式到达,浏览器无法 JSON.parse。请从工具中返回 json.dumps(...)。