CopilotKit 集成 CrewAI Flows:用 Hashbrown 流式渲染声明式生成式 UI 的完整实践指南
【免费下载链接】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 仓库中
showcase/integrations/crewai-crews的 BYOC(Bring Your Own Components)Hashbrown 集成示例为蓝本,系统讲解如何让一个极简的 CrewAI 单 Agent Crew 输出符合 Hashbrown schema 的 JSON 信封(envelope),并借助@hashbrownai/react的前端解析器把流式 JSON 逐步组装成 MetricCard、饼图、柱状图、交易卡片与 Markdown 混排的销售仪表盘。读完本文,你将掌握该集成的端到端调用链(CrewAI Crew → FastAPI → CopilotRuntime → React 渲染器)、纯 JSON 输出的系统提示词约束技巧,以及完整的前端渲染与 E2E 验证方法。
背景:为什么需要 BYOC 与 Hashbrown
在大模型驱动的对话应用中,常规做法是让 LLM 输出自然语言文本,再由前端把文本展示给用户。但在销售分析、数据看板这类场景中,用户期望的不是一段话,而是结构化的可视化 UI——KPI 卡片、饼图、柱状图等。Hashbrown 正是为此设计的"组件目录 + 结构化输出"方案:LLM 不再输出自由文本,而是输出一份受 schema 约束的 JSON 信封,前端拿到后按组件目录逐块渲染。
本示例名为 BYOC(Bring Your Own Components),核心思想是:前端自己定义组件目录(catalog)并暴露给 LLM,LLM 只负责输出符合目录的 JSON 结构,渲染完全由前端组件完成。与之相对的是qa/declarative-json-render.md这类"通用 JSON 渲染"路径——两者区别在于:Hashbrown 方案有严格的组件 props schema 约束与流式逐块组装能力,而通用 JSON 渲染则更自由但缺少目录校验。
在 CopilotKit 生态中,这一集成落在 CrewAI 集成 showcase 内,对应的 QA 文档为 qa/declarative-hashbrown.md,本文即围绕该文档及其源码实现展开。
架构总览:四层调用链
从整体上看,这个演示由四层组成,它们协同完成"用户点击 → 流式 JSON → 逐步渲染 UI"的完整链路:
- CrewAI 后端:
src/agents/byoc_hashbrown_agent.py定义一个专门输出 Hashbrown JSON 信封的 Crew,通过 agent_server.py 挂载到 FastAPI 端点/byoc-hashbrown。 - CopilotRuntime 代理层:
src/app/api/copilotkit-byoc-hashbrown/route.ts在 Next.js 侧创建一个CopilotRuntime,把请求代理到后端的AGENT_URL(默认http://localhost:8000)。 - React 前端:
src/app/demos/declarative-hashbrown/page.tsx渲染CopilotKit包装的聊天界面;hashbrown-renderer.tsx注册组件目录并用useJsonParser解析流式 JSON。 - E2E 测试:
tests/e2e/declarative-hashbrown.spec.ts用 Playwright 验证三类建议词触发的渲染结果。
调用链对应的关键文件与行号如下(后文逐一展开):
| 层 | 文件 | 职责 |
|---|---|---|
| Crew 定义 | byoc_hashbrown_agent.py | 纯 JSON 输出的系统提示词 + 单 Agent Crew |
| 后端挂载 | agent_server.py | add_crewai_crew_fastapi_endpoint(app, ByocHashbrown(), "/byoc-hashbrown") |
| Runtime 代理 | route.ts | HttpAgent指向${AGENT_URL}/byoc-hashbrown |
| 前端页面 | page.tsx | CopilotKit+HashBrownDashboard布局 |
| 渲染器 | hashbrown-renderer.tsx | useUiKit定义组件目录、useJsonParser流式解析 |
| 聊天组件 | chat.tsx | CopilotChat接入自定义 assistant 消息槽 |
一、CrewAI 后端:让 Crew 只输出 JSON
1.1 系统提示词:Hashbrown JSON 信封规范
后端核心是 byoc_hashbrown_agent.py 中的BYOC_HASHBROWN_SYSTEM_PROMPT。它告诉 LLM:每次回复必须是一个单一的 JSON 对象,且形如{"ui": [...]}信封,其中ui数组中的每个元素是组件名到{ props: {...} }的映射:
{ "ui": [ { "metric": { "props": { "label": "Total Revenue", "value": "$1.2M" } } }, { "pieChart": { "props": { "title": "...", "data": "[{...}]" } } }, { "barChart": { "props": { ... } } }, { "dealCard": { "props": { ... } } }, { "Markdown": { "props": { "children": "..." } } } ] }这里有一个关键设计(源码注释中特别强调):CrewAI 驱动的 LLM 必须直接输出原始 schema 形状,而不是 Hashbrown 官方的 XML<ui>...</ui>DSL。那个 XML DSL 是当 Hashbrown 自己驱动 LLM 时,由它把 DSL 编译成 schema 文档使用的;本示例通过 CrewAI 驱动,所以必须直接输出 JSON schema 本身。
各组件 props 规范如下(均出自系统提示词原文):
| 组件 | props | 说明 |
|---|---|---|
metric | label: string,value: string | KPI 卡片,value为预格式化字符串,如"$1.2M"或"248" |
pieChart | title: string,data: string | 环形图,data是JSON 编码的字符串(内嵌 JSON),为至少 3 段的{label, value}数组 |
barChart | title: string,data: string | 垂直柱状图,data同上,至少 3 根柱,通常按时间排序 |
dealCard | title: string,stage: string,value: number | 单笔销售交易;stage必须属于六个枚举值之一;value为裸数字(不含货币符号与逗号) |
Markdown | children: string | 简短说明文字,用于小标题与过渡句,支持标准 Markdown |
提示词还规定了硬性约束:不包代码围栏、不输出 JSON 对象之外的任何前言或解释、不调用任何工具、不询问澄清性输入;图表数据优先给出 3~6 行合理样本数据,标签保持简短;data必须是 JSON 字符串,内层引号需要转义。提示词末尾还附了一个完整的销售仪表盘示例响应,供 LLM few-shot 参考。
1.2 为何要"绕过"CrewAI 的默认系统提示词
CrewAI 有一个让开发者头疼的默认行为:ChatWithCrewFlow.build_system_message会用固定的 "CrewAI platform" 样板包装 Crew 描述,这段样板会主动怂恿 LLM 自我介绍、询问澄清输入——这恰好与"只输出一个 JSON 对象"的需求直接冲突。
源码 byoc_hashbrown_agent.py 的模块 docstring 明确指出了这一点,并给出了两条解决路径:
- 预播种系统提示词(
preseed_system_prompt):把我们的提示词注册为crew_description,同时跳过启动时的二次 AI 调用(CrewAI 默认会用 LLM 探测来生成 crew 描述),让 Crew 构建保持同步、廉价、快速。 - 安装硬覆盖(
install_custom_system_message):monkey-patchChatWithCrewFlow.__init__,在实例构造完成后立刻把自定义系统消息写回self.system_message,覆盖掉上游代码组装的那一份。
这两者的实现都在 _chat_flow_helpers.py 中,且都通过crew_name作为 key 注册,未注册的 Crew 会回落到默认行为,互不干扰。
1.3 最小 Crew 的搭建与端点挂载
由于聊天行为完全由自定义系统消息驱动,Crew 本身只需要一个"躯壳"即可——源码注释把它描述为"The agent body is only here becauseChatWithCrewFlowrequires a crew with at least one agent + task"。实际创建如下(见 byoc_hashbrown_agent.py):
agent = Agent( llm="gpt-5.4", role="Hashbrown JSON Emitter", goal="Emit hashbrown-shaped JSON responses.", backstory=BYOC_HASHBROWN_SYSTEM_PROMPT, verbose=False, tools=[], ) task = Task( description="Respond with a single hashbrown-shaped JSON object.", expected_output="A JSON object matching the hashbrown schema.", agent=agent, ) return Crew( name=CREW_NAME, agents=[agent], tasks=[task], process=Process.sequential, verbose=False, chat_llm="gpt-5.4", )随后在 agent_server.py 中将其挂载为 FastAPI 端点:
add_crewai_crew_fastapi_endpoint(app, ByocHashbrown(), "/byoc-hashbrown")其中ByocHashbrown是实现了name属性与crew()方法的适配器类,crew()使用模块级缓存_cached_crew保证只构建一次 Crew(见 byoc_hashbrown_agent.py)。
二、Runtime 代理层:把前端请求转发给 CrewAI
在 Next.js 一侧,route.ts 创建了一个专用的 CopilotRuntime:
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000"; function createAgent() { return new HttpAgent({ url: `${AGENT_URL}/byoc-hashbrown` }); } const agents: Record<string, AbstractAgent> = { "byoc-hashbrown-demo": createAgent(), default: createAgent(), }; const runtime = new CopilotRuntime({ agents });关键点:
AGENT_URL环境变量:默认指向http://localhost:8000(即本地 agent_server 的监听地址),可通过环境变量覆盖指向远程部署的后端。- 两个 agent 别名:
byoc-hashbrown-demo与default指向同一个HttpAgent,前端页面通过agent="byoc-hashbrown-demo"显式选中该 agent。 - 导出 POST 处理器:使用
createCopilotRuntimeHandler并以mode: "single-route"、basePath: "/api/copilotkit-byoc-hashbrown"挂载,捕获异常后返回结构化{ error, stack }JSON(500 状态码)。
三、React 前端:组件目录注册与流式 JSON 渲染
3.1 页面组装
page.tsx 使用CopilotKit组件包裹整个页面,指定runtimeUrl="/api/copilotkit-byoc-hashbrown"与agent="byoc-hashbrown-demo",内部再套一层自定义的HashBrownDashboardprovider:
<CopilotKit runtimeUrl="/api/copilotkit-byoc-hashbrown" agent="byoc-hashbrown-demo" > <HashBrownDashboard> {/* 页面布局与 <Chat /> */} </HashBrownDashboard> </CopilotKit>页面头部标题为 "Declarative UI: Hashbrown",副标题说明这是通过@hashbrownai/react实现的流式结构化输出。聊天区由 chat.tsx 渲染CopilotChat,并把默认的CopilotChatAssistantMessage槽替换为自定义的HashBrownRenderMessage(需要类型断言以满足 slot 签名)。
3.2 组件目录:useUiKit + exposeComponent
hashbrown-renderer.tsx 是本方案的核心。它调用@hashbrownai/react的useUiKit声明组件目录:
function useSalesDashboardKit() { return useUiKit({ examples: prompt`...`, components: [ exposeMarkdown(), exposeComponent(MetricCard, { name: "metric", description: "A KPI metric card with label, value, and optional trend", props: { label: s.string("The metric label/name"), value: s.string("The metric value (formatted)"), }, }), exposeComponent(PieChartWithStringData, { name: "pieChart", description: "A donut/pie chart. `data` is a JSON-encoded string ...", props: { title: s.string("Chart title"), data: s.string("JSON array of {label, value} segments"), }, }), // barChart、dealCard 同理 ... ], }); }值得注意的工程细节:
examples中使用 Hashbrown 的prompt模板字面量写一段<ui>...</ui>的示例,混合展示Markdown标题、metric、pieChart、barChart、dealCard,并附 hint 提示图表必须包含title与data,data是 JSON 编码的{label, value}数组字符串——这与后端系统提示词的约定完全一致。dealCard的stage用s.enumeration声明,枚举值正好是后端提示词中规定的六个管道阶段(prospect/qualified/proposal/negotiation/closed-won/closed-lost),前后端 schema 严格对齐。
3.3 图表 data 字符串化:流式解析下的稳定性设计
前端组件PieChartWithStringData与BarChartWithStringData都接收data: string,内部用parseChartData做JSON.parse,解析失败(流式中途的截断 JSON)则渲染null,成功才把真实数组交给图表组件。源码注释解释了这样做的原因:"The LLM streams JSON as text anyway, so we modeldataas a string and parse inside the wrapper"。这保证了 schema 在部分流式状态下保持稳定,避免中途的非法 props 触发渲染崩溃——这是 Hashbrown 流式方案的基石。
HashBrownDashboard通过useUiKit拿到kit(内含schema与render)后放入 Context,供消息渲染器使用。若在 provider 之外使用会抛出HashBrownRenderMessage must be used within HashBrownDashboard错误。
3.4 消息槽渲染:useJsonParser 渐进式组装
AssistantMessage组件对每条 assistant 消息调用useJsonParser(content, kit.schema):
const { value } = useJsonParser(content, kit.schema); if (!value) return null; return <div contenteditable="false">【免费下载链接】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),仅供参考