基于 CopilotKit 的 Agentic Generative UI:用useAgent将代理状态实时渲染进聊天对话
【免费下载链接】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 仓库中crewai-conversational-flows集成的gen-ui-agent演示展开,讲解"代理式生成式 UI"(Agentic Generative UI)的完整落地模式:后端 Agent 在执行长任务时不断发布结构化状态,前端通过useAgent(v2 API)订阅该状态,并将其渲染为内嵌于聊天记录中的状态卡片。读完本文,你将掌握从后端状态模型、set_steps工具定义、STATE_SNAPSHOT 事件发射,到前端messageView.children自定义渲染的端到端实现方法,可直接复用到 Mastra、Strands、AG2、Agno、LangGraph、Pydantic AI 等其他集成(仓库内各集成的gen-ui-agent演示均遵循同一模式)。
一、核心思想:让 Agent 拥有"渲染什么"的最终决定权
传统 AI 聊天中,Agent 只能输出文本,界面组件与对话内容彼此割裂。Agentic Generative UI 反转了这一关系:Agent 在推进长任务时,自行决定在对话流中呈现何种 UI。
关联文档 README 对演示的定位是:
Agent 在处理长任务时渲染自定义 UI,将状态更新与中间结果流式地推送到聊天中。
具体机制包含两条核心链路:
- 状态即协议:后端 Agent 定义自己的状态 schema(
steps: list[Step]),并提供自定义工具set_steps供模型调用以变更状态;每次set_steps调用都会把更新后的steps流式推送到客户端。 - 前端即渲染器:前端订阅实时状态(
useAgent),并借助messageView.children在聊天记录内部渲染一张InlineAgentStateCard,状态到达时卡片原地刷新——不产生逐消息的重复声明,也不会出现重复卡片。
值得注意的是,README 中提到"Frontend usesuseAgentRender",而该演示当前的实际源码(page.tsx)使用的是 v2 的useAgent+messageView.children组合;同时,page.tsx 的注释明确指出,这一方案取代了早期会产生"每张状态变更消息一张卡片"的useCoAgentStateRender方式。下文以当前源码实现为准展开。
二、前端实现:从useAgent订阅到卡片渲染
2.1 顶层挂载:CopilotKitProvider 与 Agent 绑定
page.tsx 中,页面通过CopilotKit组件声明运行时地址与默认 Agent:
export default function GenUiAgentDemo() { return ( <CopilotKit runtimeUrl="/api/copilotkit" agent="gen-ui-agent"> <div className="flex justify-center items-center h-screen w-full"> <div className="h-full w-full max-w-4xl"> <Chat /> </div> </div> </CopilotKit> ); }关键点:
runtimeUrl="/api/copilotkit"指向 Next.js 侧的 CopilotKit Runtime 路由(详见第四节);agent="gen-ui-agent"声明会话默认绑定的 Agent,与后端 Flow 一一对应。
2.2 订阅实时状态:useAgent与UseAgentUpdate.OnStateChanged
Chat组件通过useAgent订阅 Agent 的实时状态:
type AgentState = { steps?: Step[]; }; function Chat() { const { agent } = useAgent({ agentId: "gen-ui-agent", updates: [UseAgentUpdate.OnStateChanged], }); useSuggestions(); const steps = (agent.state as AgentState | undefined)?.steps ?? []; const status = agent.isRunning ? "inProgress" : "complete"; // ... }值得注意的细节:
updates: [UseAgentUpdate.OnStateChanged]声明订阅"状态变更"事件。这与后端每次set_steps后发射的STATE_SNAPSHOT事件精准对应(见第三节)。agent.isRunning被映射为卡片的inProgress/complete状态,驱动卡片头部在"进行中/完成"两种视觉之间切换。- 状态读取使用可选链兜底(
?.steps ?? []),在首帧无状态时也能安全渲染空卡片占位。
2.3 自定义消息列表:messageView.children
CopilotChat的messageView.children允许完全接管消息列表的组装逻辑,把 Agent 状态卡片"嵌入"聊天记录内部:
<CopilotChat agentId="gen-ui-agent" className="h-full rounded-2xl" messageView={{ children: ({ messageElements, interruptElement }) => ( <MessageListWithState messageElements={messageElements} interruptElement={interruptElement} steps={steps} status={status} /> ), }} />对应的 message-list-with-state.tsx 把"消息元素 + 状态卡片 + 中断元素"按顺序组装为纵向 flex 布局:
export function MessageListWithState({ messageElements, interruptElement, steps, status, }) { return ( <div>export type Step = { id: string; title: string; status: "pending" | "in_progress" | "completed"; };id:稳定不透明的句柄,作为 Reactkey(key={step.id ?? idx}),保证跨状态迁移时 React 不会重建节点;status:三态枚举pending → in_progress → completed,与后端工具 schema 中的enum完全一致(见第三节)。
卡片根据状态计算头部文案:
const total = steps.length; const done = steps.filter((s) => s.status === "completed").length; const headline = status === "complete" || (total > 0 && done === total) ? `All ${total} steps complete` : total > 0 ? `Step ${Math.min(done + 1, total)} of ${total}` : "Planning…";即:
- 全部完成时显示
All N steps complete; - 进行中显示
Step N of M; - 尚无步骤时显示
Planning…。
每行步骤条目(StepMarker)按状态呈现三种视觉:
- completed:绿色圆底对勾,标题文字灰色并加删除线(
line-through); - in_progress:紫色圆底旋转 spinner,标题加粗深色;
- pending:白色圆底序号数字,标题为次级灰。
这一组件树(含data-testid="agent-state-card"、data-testid="agent-step"、data-status等属性)同时服务于 UI 展示与端到端测试断言,是"测试可驱动"的典型写法。
2.5 入口建议:useConfigureSuggestions
suggestions.ts 通过useConfigureSuggestions注入三个演示建议(产品发布规划、团队 offsite 组织、竞品调研),available: "always"表示建议常驻。这为无头演示提供了可点击的冷启动入口。
三、后端实现:CrewAI Flow 中的set_steps工具与状态发射
后端核心位于 gen_ui_agent.py。它没有使用 CrewAI 的 Crew 端点,而是实现为一个crewai.flow.Flow,直接掌控 LLM 调用、工具 schema 与状态变更。文件头的注释解释了原因:
一个 Crew 端点无法承载该演示:
ChatWithCrewFlow不会把逐工具的(per-tool)状态变更透传给 AG-UI 桥——它唯一的状态变更是当模型调用特殊的<crew_name>工具时把result.raw追加到state["outputs"]。
这也是shared_state_read_write.py、subagents.py共用的后端策略:专用 Flow 通过add_crewai_flow_fastapi_endpoint挂载到独立路径。
3.1 状态模型:Step与AgentState
class Step(BaseModel): id: str = "" title: str = "" status: Literal["pending", "in_progress", "completed"] = "pending" class AgentState(CopilotKitState): steps: List[Step] = Field(default_factory=list)Step直接镜像 LangGraph 参照实现中的GenUiAgentState.steps[i]与微软 Agent Framework 的STATE_SCHEMA.steps.items,三套集成的状态形状保持一致;AgentState继承自CopilotKitState(来自ag_ui_crewai桥接包),并追加steps字段,作为前端agent.state.steps读取的数据源。
3.2 工具 schema:SET_STEPS_TOOL
这里刻意选用OpenAI 兼容的 JSON Schema 工具定义(而非 CrewAIBaseTool),因为监督 LLM 调用直接走litellm.acompletion,JSON schema 是最合适的原语(与shared_state_read_write.SET_NOTES_TOOL一致):
SET_STEPS_TOOL = { "type": "function", "function": { "name": "set_steps", "description": ( "Publish the current plan and step statuses. Call this every " "time a step transitions (including the first enumeration of " "steps). Always include the FULL list of steps on each call " "(this is the complete source of truth — not a diff)." ), "parameters": { "type": "object", "properties": { "steps": { "type": "array", "items": { "type": "object", "properties": { "id": {"type": "string"}, "title": {"type": "string"}, "status": { "type": "string", "enum": ["pending", "in_progress", "completed"], }, }, "required": ["id", "title", "status"], }, } }, "required": ["steps"], }, }, }两个设计要点值得借鉴:
- 全量语义(非 diff):description 中反复强调"每次都传完整步骤列表",即
set_steps是唯一事实源(source of truth),客户端按全量替换而不是增量合并,天然避免同步错位; - 状态枚举约束:
enum限定了三态,配合后端的_coerce_steps防御性解析,单条坏数据不会炸掉整个 Flow。
3.3 系统提示词:约束模型的行为序列
SYSTEM_PROMPT要求模型执行严格序列(参见 gen_ui_agent.py):
- 规划恰好 3 个具体步骤,先
set_steps一次全量发布三个pending步骤; - 对第 1/2/3 步依次执行
in_progress → completed两次调用; - 全部完成后发送一条总结性的最终助手消息并终止。
同时提示词明确:
- 禁止并行调用
set_steps,必须等待上一次调用返回(代码中同时设置parallel_tool_calls=False双保险,因为部分 Provider 会忽略该参数); - 每个步骤的
id在计划生命周期内必须保持稳定; - 步骤标题要保留用户场景关键词(如产品发布须含
launch、marketing;团队 offsite 须含venue、agenda),保证演示视觉上贴合用户输入。
3.4 Flow 主循环:工具执行、状态替换与 STATE_SNAPSHOT
GenUiAgentFlow(gen_ui_agent.py)实现了一个与 LangGraph 参照 ReAct 循环等价的结构:
@start() async def chat(self) -> None: tools = [*self.state.copilotkit.actions, SET_STEPS_TOOL] for _iteration in range(self._MAX_ITERATIONS): # 20 次上限 messages = [system_message, *_active_turn_messages(self.state.messages)] response = await copilotkit_stream( await acompletion( model="openai/gpt-5.4", messages=messages, tools=tools, parallel_tool_calls=False, stream=True, ) ) message = response.choices[0].message self.state.messages.append(message) tool_calls = message.get("tool_calls") or [] if not tool_calls: return # 无工具调用 → 最终文本响应,本回合结束 for tool_call in tool_calls: # 逐个处理工具调用,防止部分 Provider 多返回时静默丢调用 if tool_name != "set_steps": # 前端注册的 action:由 AG-UI 客户端完成往返,这里仅补占位 tool result ... continue new_steps = _coerce_steps(args.get("steps")) self.state.steps = new_steps # 全量替换,last-write-wins steps_changed = True await copilotkit_emit_tool_result(tool_call_id, result_content) if steps_changed: await copilotkit_emit_state(self.state) # 发射 STATE_SNAPSHOT各环节的工程细节:
- 迭代上限:
_MAX_ITERATIONS = 20。名义脚本为 1 次枚举 + 3×2 次状态迁移 + 1 次最终文本 = 8 次往返,20 提供约 2.5 倍余量应对模型重试工具调用格式,对齐 LangGraph 参照实现的recursion_limit=50启发式(约 3 倍名义值)。 - 防御性迭代:尽管设置
parallel_tool_calls=False,代码仍遍历全部tool_calls而非取[0]——否则会静默丢弃多余调用,留下无匹配role: "tool"回复的 assistanttool_calls消息,大多数聊天 API 会在下一轮拒绝(与shared_state_read_write.py相同的防御模式)。 - 状态归约:
self.state.steps = new_steps是全量替换(last-write-wins),对应 LangGraph 的_last_stepsreducer 与 MAF 的state_update形状;测试探针明确断言 swap-not-accumulate 语义。 - 快照发射时机:仅在
steps_changed为真时调用copilotkit_emit_state(self.state),让 UI 的useAgent({updates: [OnStateChanged]})订阅立即触发、卡片即时重绘,而纯前端工具轮次不污染共享状态。 - 防御解析:
_coerce_steps丢弃非 dict、缺 key、非法 status 的条目而不是抛异常——一行坏数据不应毁掉整个 Flow。 - 会话裁剪:
_active_turn_messages只保留最近一个用户回合及其后的消息。这是因为 AIMock 的确定性多步 fixture 按最新工具结果作为切换键,若把旧的完成步骤带入新一轮 Flow 运行,旧工具 id 会与新的链路竞争并重绘过期步骤。
文件末尾的模块级单例gen_ui_agent_flow = GenUiAgentFlow()配合add_crewai_flow_fastapi_endpoint按请求深拷贝,初始化成本只在 import 时支付一次。
四、端到端链路:路由注册、代理与后端挂载
4.1 Next.js 侧:Agent 别名注册
api/copilotkit/route.ts 将gen-ui-agent映射到后端专用 Flow 路径:
// gen-ui-agent routes to a dedicated CrewAI Flow backend that owns the // `set_steps` tool + per-call STATE_SNAPSHOT emit (see // src/agents/gen_ui_agent.py). agents["gen-ui-agent"] = createAgent("/gen-ui-agent");createAgent(path)构造HttpAgent,将请求代理到AGENT_URL(默认http://localhost:8000)下的/conversational_flows/${feature},通过 AG-UI 协议与 Python 后端通信。注释同时提醒:若某个别名静默回退到根 chat 端点,UI 看似连接成功,实际会丢掉该演示所依赖的专用 AG-UI 事件——因此每个别名都必须显式注册。
4.2 Python 侧:Flow 挂载
agent_server.py 遍历CONVERSATIONAL_FLOW_TYPES,为每个 Flow 调用:
add_crewai_flow_fastapi_endpoint( app, flow_type(), f"/conversational_flows/{feature}", conversational=True, emit_interrupt_outcome=interrupt_feature, enable_legacy_on_interrupt_event=not interrupt_feature, )于是gen-ui-agentFlow 被挂载到/conversational_flows/gen-ui-agent,与 Next.js 侧createAgent("/gen-ui-agent")的 URL 拼装严格对应,完成前后端闭环。
4.3 数据流全景
一次典型交互的完整链路为:
- 用户在聊天框发起任务(或点击
useConfigureSuggestions注入的建议); CopilotKitRuntime 经/api/copilotkit将请求以 AG-UI 协议转发到/conversational_flows/gen-ui-agent;- Flow 内 LLM 首轮枚举 3 个
pending步骤并调用set_steps→ 工具执行 →copilotkit_emit_state发射 STATE_SNAPSHOT; - 前端
useAgent的OnStateChanged订阅收到快照,agent.state.steps更新; CopilotChat.messageView.children中挂载的MessageListWithState检测到steps.length > 0,在聊天记录内渲染/原地刷新InlineAgentStateCard;- LLM 依次推进
in_progress/completed,每步都重复步骤 3-5,卡片逐步打勾; - 第 3 步完成后 LLM 输出最终总结文本并终止,卡片头部显示
All 3 steps complete。
五、契约与验证:探针如何约束该模式
该演示不是"写完就算"的 UI 玩具,而是有明确自动化契约的。文档与源码中暴露的验证点包括:
- gen_ui_agent.py 头部注释声明的契约(探针位于
probe harness/src/probes/scripts/d5-gen-ui-agent.ts):- Agent 规划恰好 3 个步骤,且逐一遍历
pending → in_progress → completed; - 每次迁移都通过
set_steps(steps=[...])发布并作为 STATE_SNAPSHOT 发射; - 前端渲染
[data-testid="agent-state-card"]与每个state.steps[i]对应的[data-testid="agent-step"]。
- Agent 规划恰好 3 个步骤,且逐一遍历
- 前端组件上的
data-testid/data-status属性(见 InlineAgentStateCard.tsx)正是为了让探针与端到端测试能够稳定定位 DOM 节点。 _coerce_steps的容错逻辑(丢弃坏条目而非抛异常)与_active_turn_messages的回合裁剪,都是基于 AIMock 确定性 fixture 与真实 Provider 行为总结出的健壮性增强。
六、迁移参照:从useCoAgentStateRender到useAgent
page.tsx 明确记录了本模式与旧方案的差异:
这镜像了其他所有集成的
gen-ui-agent演示(mastra、strands、ag2、agno、crewai-conversational-flows、langgraph-typescript、pydantic-ai……)所用的模式,并取代了早期会产生"每张状态变更消息一张卡片"的useCoAgentStateRender方案。
两者的本质区别:
- 旧方案
useCoAgentStateRender:为每条状态变更消息各渲染一张卡片,状态推进时聊天记录里堆积多张卡片; - 新方案
useAgent+messageView.children:单张卡片原地更新,状态只是"流过"卡片,消息记录保持干净。
如果你的项目正在维护基于useCoAgentStateRender的旧代码,迁移方向就是:用useAgent({updates: [UseAgentUpdate.OnStateChanged]})订阅共享状态,再通过messageView.children在消息列表内挂载一个"就地渲染"的状态组件。
七、复用到你的项目:最小实现清单
要在自己的 CopilotKit + CrewAI 集成中复刻该模式,需要四件套:
- 后端:实现一个
Flow,持有结构化状态(如steps),定义全量语义的状态工具(set_steps),在每次工具执行后调用copilotkit_emit_state发射快照,并通过add_crewai_flow_fastapi_endpoint挂载到专用路径; - Runtime 路由:在 Next.js
route.ts中显式注册 Agent 别名并指向该路径(参照 api/copilotkit/route.ts); - 前端订阅:
useAgent({ agentId, updates: [UseAgentUpdate.OnStateChanged] })读取agent.state; - 就地渲染:
CopilotChat的messageView.children内按steps.length > 0条件渲染状态卡片,卡片 key 绑定稳定的步骤id。
按照此清单,你可以把"规划中 / 执行中 / 已完成"的多步任务可视化直接嵌入任意聊天界面,获得与本文演示一致的流式 Agentic UI 体验。
关联源码索引
- 关联文档:README
- 前端页面与状态订阅:page.tsx
- 状态卡片组件:InlineAgentStateCard.tsx
- 消息列表组装:message-list-with-state.tsx
- 入口建议配置:suggestions.ts
- 后端 Flow 实现:gen_ui_agent.py
- Runtime 路由与别名注册:route.ts
- Flow 挂载入口:agent_server.py
【免费下载链接】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),仅供参考