news 2026/9/12 23:42:18

基于 CopilotKit 的 Agentic Generative UI:用 `useAgent` 将代理状态实时渲染进聊天对话

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于 CopilotKit 的 Agentic Generative UI:用 `useAgent` 将代理状态实时渲染进聊天对话

基于 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,将状态更新与中间结果流式地推送到聊天中。

具体机制包含两条核心链路:

  1. 状态即协议:后端 Agent 定义自己的状态 schema(steps: list[Step]),并提供自定义工具set_steps供模型调用以变更状态;每次set_steps调用都会把更新后的steps流式推送到客户端。
  2. 前端即渲染器:前端订阅实时状态(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 订阅实时状态:useAgentUseAgentUpdate.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

CopilotChatmessageView.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:稳定不透明的句柄,作为 Reactkeykey={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.pysubagents.py共用的后端策略:专用 Flow 通过add_crewai_flow_fastapi_endpoint挂载到独立路径。

3.1 状态模型:StepAgentState

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):

  1. 规划恰好 3 个具体步骤,先set_steps一次全量发布三个pending步骤;
  2. 对第 1/2/3 步依次执行in_progress → completed两次调用;
  3. 全部完成后发送一条总结性的最终助手消息并终止。

同时提示词明确:

  • 禁止并行调用set_steps,必须等待上一次调用返回(代码中同时设置parallel_tool_calls=False双保险,因为部分 Provider 会忽略该参数);
  • 每个步骤的id在计划生命周期内必须保持稳定
  • 步骤标题要保留用户场景关键词(如产品发布须含launchmarketing;团队 offsite 须含venueagenda),保证演示视觉上贴合用户输入。

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 数据流全景

一次典型交互的完整链路为:

  1. 用户在聊天框发起任务(或点击useConfigureSuggestions注入的建议);
  2. CopilotKitRuntime 经/api/copilotkit将请求以 AG-UI 协议转发到/conversational_flows/gen-ui-agent
  3. Flow 内 LLM 首轮枚举 3 个pending步骤并调用set_steps→ 工具执行 →copilotkit_emit_state发射 STATE_SNAPSHOT;
  4. 前端useAgentOnStateChanged订阅收到快照,agent.state.steps更新;
  5. CopilotChat.messageView.children中挂载的MessageListWithState检测到steps.length > 0,在聊天记录内渲染/原地刷新InlineAgentStateCard
  6. LLM 依次推进in_progress/completed,每步都重复步骤 3-5,卡片逐步打勾;
  7. 第 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"]
  • 前端组件上的data-testid/data-status属性(见 InlineAgentStateCard.tsx)正是为了让探针与端到端测试能够稳定定位 DOM 节点。
  • _coerce_steps的容错逻辑(丢弃坏条目而非抛异常)与_active_turn_messages的回合裁剪,都是基于 AIMock 确定性 fixture 与真实 Provider 行为总结出的健壮性增强。

六、迁移参照:从useCoAgentStateRenderuseAgent

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 集成中复刻该模式,需要四件套:

  1. 后端:实现一个Flow,持有结构化状态(如steps),定义全量语义的状态工具(set_steps),在每次工具执行后调用copilotkit_emit_state发射快照,并通过add_crewai_flow_fastapi_endpoint挂载到专用路径;
  2. Runtime 路由:在 Next.jsroute.ts中显式注册 Agent 别名并指向该路径(参照 api/copilotkit/route.ts);
  3. 前端订阅useAgent({ agentId, updates: [UseAgentUpdate.OnStateChanged] })读取agent.state
  4. 就地渲染CopilotChatmessageView.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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 23:39:15

Postman之外:15款接口测试工具实测与选型指南

Postman 这个名字&#xff0c;基本已经成了接口测试的代名词&#xff0c;很多团队招人时甚至会写“熟悉 Postman”当作一项加分技能。但坦白讲&#xff0c;从 2020 年之后&#xff0c;我越来越少把它当主力工具使用了——不是觉得 Postman 不好用&#xff0c;而是接口测试这件事…

作者头像 李华
网站建设 2026/9/12 23:39:02

aarch64下Qt 5.14.2静态交叉编译完整实战手册

1. 为什么你大概率需要这份手册 做嵌入式Linux开发的同行应该都明白&#xff0c;目标板子跑的是aarch64架构&#xff0c;开发机却是x86_64的PC&#xff0c;这种组合下给板子准备Qt运行环境&#xff0c;就绕不开交叉编译。如果你只是做动态库版本&#xff0c;把编译好的so扔到板…

作者头像 李华
网站建设 2026/9/12 23:38:00

Kubernetes 1.31 一键部署:Containerd + kubeadm 脚本化实战

入行这些年&#xff0c;我最大的感悟是&#xff1a;装 Kubernetes 这事儿&#xff0c;本身一点不玄乎&#xff0c;真正费时间的往往是那些反反复复的“手动重复劳动”——关交换分区、装运行时、对版本、改配置、等镜像&#xff0c;然后踩一遍别人早就踩过的坑。尤其是跑到 1.3…

作者头像 李华
网站建设 2026/9/12 23:37:22

普通人怎么用国产AI?真实场景下的工具匹配指南

1. 这不是“AI工具测评”&#xff0c;是普通人在真实生活里怎么用国产AI的实操笔记最近三个月&#xff0c;我帮身边27个朋友——包括刚退休的阿姨、初中语文老师、开小餐馆的老板、做电商客服的00后姑娘、还有两个正在准备考研的大学生——一起梳理他们每天实际要解决的问题&am…

作者头像 李华
网站建设 2026/9/12 23:37:16

PSO-LSTM粒子群算法优化LSTM超参数,提升时间序列预测精度

简介&#xff1a;基于粒子群算法优化长短期记忆神经网络的时间序列预测完整项目&#xff0c;包含可直接运行的源程序与配套数据集&#xff0c;面向计算机、电子信息、数学等专业学生&#xff0c;适用于课程设计、期末大作业、毕业设计等场景&#xff0c;也适合刚接触深度学习的…

作者头像 李华