CopilotKit 与 CrewAI Conversational Flows:Shared State 只读模式的完整 QA 验证指南
【免费下载链接】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中的Shared State(Reading)演示(shared-state-read),系统讲解如何对「前端发布、Agent 只读」的共享状态功能进行端到端 QA 验证。读完本文,你将掌握该 demo 的功能验收清单、UI 测试断言(data-testid)、关键源码实现(agent.setState与后端 Flow 的状态注入),以及如何用 Playwright 自动化复现测试步骤。
什么是 Shared State(Reading)Demo
在 CopilotKit 中,useAgent暴露的agent.state是前端与 Agent 之间共享的单一事实来源(single source of truth)。shared-state-read演示的核心设计非常明确:
- UI 拥有状态:前端通过
agent.setState将菜谱数据写入 agent 状态; - Agent 只读:后端 Flow 每一轮对话都会读取该菜谱,但不提供任何修改工具,因此 UI 始终是唯一的数据权威;
- 双向感知:用户既可以在表单中编辑状态,也可以直接向 Agent 提问“我在做什么菜谱”,Agent 的回答会实时引用当前状态。
该 demo 的源码位于 showcase/integrations/crewai-conversational-flows/src/app/demos/shared-state-read/,对应清单条目shared-state-read在 manifest.yaml 中被标记为shared-state-read特性,与同目录下的shared-state-read-write(双向读写)形成对照。
验证前置条件
开始 QA 前,需满足以下两个条件(详见 QA 文档):
- Demo 已部署且可访问:本地开发或 Docker Compose 环境均可,生产环境通过 Railway 部署;
- Agent 后端健康:访问
/api/health接口应返回正常状态。
本地 D6 验证可通过crewai-conversational-flows的 compose 服务在端口3120上运行(见 PARITY_NOTES.md)。该集成包基于 CrewAI 官方 Conversational Flows API:每个 Agent 路由以conversational=True注册,Flow 通过stream_turn(message, session_id=...)处理对话,并将 AG-UI 的threadId作为 CrewAI 会话的session_id。
一、基础功能验证
QA 文档的第一组检查点覆盖页面加载与基本对话链路:
- 导航至
shared-state-readdemo 页面; - 验证菜谱卡片表单加载成功(
data-testid="recipe-card"); - 验证CopilotSidebar默认打开,标题为"AI Recipe Assistant";
- 通过侧边栏发送一条消息;
- 验证 Agent 正常响应。
在源码中,这些要求逐一对应。页面主体由 page.tsx 构成:
<CopilotKit runtimeUrl="/api/copilotkit" agent="shared-state-read"> <CopilotSidebar defaultOpen labels={{ modalHeaderTitle: "AI Recipe Assistant" }} /> </CopilotKit>defaultOpen保证了侧边栏默认展开,labels.modalHeaderTitle控制标题文案。而data-testid="recipe-card"由表单组件 recipe-card.tsx 暴露:
<form>await expect(page.locator('[data-testid="recipe-card"]')).toBeVisible({ timeout: 15000, }); await expect(page.getByText("AI Recipe Assistant")).toBeVisible({ timeout: 10000, });二、初始菜谱状态验证
QA 文档要求核对表单的默认值。这些默认值并非硬编码在 JSX 中,而是集中定义在 types.ts 的INITIAL_RECIPE常量里:
export const INITIAL_RECIPE: RecipeData = { title: "Make Your Recipe", skill_level: SkillLevel.INTERMEDIATE, cooking_time: CookingTime.FortyFiveMin, special_preferences: [], ingredients: [ { icon: "🥕", name: "Carrots", amount: "3 large, grated" }, { icon: "🌾", name: "All-Purpose Flour", amount: "2 cups" }, ], instructions: ["Preheat oven to 350°F (175°C)"], };对应 QA 检查点:
| QA 检查点 | 源码位置 |
|---|---|
| 菜谱标题默认为 "Make Your Recipe" | INITIAL_RECIPE.title |
| 烹饪时间下拉默认 "45 min" | CookingTime.FortyFiveMin,枚举值见types.ts |
| 技能等级下拉默认 "Intermediate" | SkillLevel.INTERMEDIATE |
| 默认食材:胡萝卜(3 large, grated,🥕)、通用面粉(2 cups,🌾) | INITIAL_RECIPE.ingredients |
| 默认步骤:"Preheat oven to 350 F" | INITIAL_RECIPE.instructions |
烹饪时间下拉的默认值在recipe-card.tsx中通过索引映射实现:cookingTimeValues.find((t) => t.label === recipe.cooking_time)?.value ?? 3,其中value: 3恰好对应45 min,兜底值保证了异常情况下的默认行为。
初始状态的注入时机
一个容易忽略的实现细节是:初始菜谱并非在组件渲染时就写入状态,而是通过useEffect在首次渲染后一次性播种(见 page.tsx):
useEffect(() => { if (!(agent.state as RecipeAgentState | undefined)?.recipe) { agent.setState({ recipe: INITIAL_RECIPE } satisfies RecipeAgentState); } }, []);这段代码的含义是:只有当agent.state.recipe尚不存在时才写入初始值,确保 Agent 在第一轮对话时就能读到内容;此后所有编辑都走agent.setState通道。QA 中验证“初始菜谱正确显示”,本质上就是在验证这次播种是否成功。
三、建议(Suggestions)验证
QA 要求确认三条起步建议可见:
- Create Italian recipe
- Make it healthier
- Suggest variations
这三条建议由useConfigureSuggestions配置,位于 page.tsx:
useConfigureSuggestions({ suggestions: [ { title: "Create Italian recipe", message: "Create a delicious Italian pasta recipe." }, { title: "Make it healthier", message: "Make the recipe healthier with more vegetables." }, { title: "Suggest variations", message: "Suggest some creative variations of this recipe." }, ], available: "always", });available: "always"表示建议在对话过程中始终可用(而非仅在首屏出现)。Playwright 测试对这三条建议做了逐条可见性断言(page.getByRole("button", { name: title })),可直接用于自动化回归。
四、本地状态编辑验证(Local State)
QA 文档中篇幅最大的一组检查点是不经过 AI、纯前端驱动的表单编辑。核心设计是:表单是一个完全受控组件(fully controlled component),每一次编辑都立即通过agent.setState同步进共享状态(见 page.tsx):
const handleChange = (next: RecipeData) => { agent.setState({ recipe: next } satisfies RecipeAgentState); };受控组件的更新链在 recipe-card.tsx 中实现:update(partial)做浅合并,updateIngredient与updateInstruction分别做不可变数组更新。QA 步骤与实现对应关系如下:
| QA 步骤 | 实现要点 |
|---|---|
| 编辑标题 | update({ title: e.target.value }),aria-label="Recipe title" |
| 切换技能等级下拉 | update({ skill_level: value as SkillLevel }) |
| 切换烹饪时间下拉 | 通过cookingTimeValues[Number(value)].label写入 |
| 切换饮食偏好(如 "Vegetarian") | recipe.special_preferences数组的增删,Badge组件以aria-pressed标记选中态 |
| 点击 "+ Add Ingredient" | data-testid="add-ingredient-button",追加{ icon: "🍴", name: "", amount: "" }空行 |
| 编辑食材名称与数量 | updateIngredient(index, field, value) |
| 点击 "x" 删除食材 | 过滤recipe.ingredients数组 |
| 点击 "+ Add Step" | 追加空字符串到instructions |
| 编辑步骤文案 | updateInstruction(index, value) |
| 点击 "x" 删除步骤 | 过滤instructions数组 |
偏好选项来自 types.ts 的SpecialPreferences枚举:High Protein、Low Carb、Spicy、Budget-Friendly、One-Pot Meal、Vegetarian、Vegan,共 7 个可多选标签。
自动化方面,Playwright 测试验证了 "Add Ingredient" 的行为(shared-state-read.spec.ts):
const ingredientCards = page.locator('[data-testid="ingredient-card"]'); const initialCount = await ingredientCards.count(); await page.locator('[data-testid="add-ingredient-button"]').click(); await expect(ingredientCards).toHaveCount(initialCount + 1, { timeout: 5000 });五、AI 驱动的菜谱更新(useAgent + 共享状态)
这一节验证 Agent 在读取共享状态后给出文本建议,但不会真的改写 UI 数据。QA 步骤为:
- 点击 "Create Italian recipe" 建议;
- 验证 Agent 根据当前菜谱输出关于标题、食材、步骤的建议;
- 验证ping indicator在变更区域出现;
- 验证 "Improve with AI" 按钮(
data-testid="improve-button")在加载中变为 "Please Wait..."; - 点击 "Improve with AI" 并验证菜谱被增强。
其中handleImprove的实现展示了「手动触发生成」的完整调用链(page.tsx):
const handleImprove = () => { if (agent.isRunning) return; agent.addMessage({ id: crypto.randomUUID(), role: "user", content: "Improve the recipe", }); void copilotkit .runAgent({ agent }) .catch((err) => console.error("[shared-state-read] runAgent failed", err)); };按钮的加载态由isLoading(即agent.isRunning)驱动(recipe-card.tsx):
<Button>SYSTEM_PROMPT = ( "You are a concise recipe assistant. The frontend-owned recipe state is " "included below. Read it when answering, but never claim to have edited " "the recipe because this demo intentionally gives the agent read-only " "access.\n\nCurrent recipe state:\n{recipe}" ) class SharedStateReadState(CopilotKitState): recipe: dict[str, Any] | None = None class SharedStateReadFlow(Flow[SharedStateReadState]): @start() async def chat(self) -> None: recipe = json.dumps(self.state.recipe or {}, indent=2, ensure_ascii=False) response = await copilotkit_stream( await acompletion( model="openai/gpt-5.4", messages=[ {"role": "system", "content": SYSTEM_PROMPT.format(recipe=recipe)}, *self.state.messages, ], tools=self.state.copilotkit.actions, stream=True, ) ) self.state.messages.append(response.choices[0].message)三个关键点值得深入理解:
- 状态注入:每个对话轮次都会把
self.state.recipe序列化为 JSON,拼进 system prompt。这意味着 Agent 每次回答都基于最新的前端状态,用户中途的任何编辑都会立即生效; - 只读约束:后端不注册任何写入工具(
tools为空列表),system prompt 还显式要求 Agent “不要声称编辑过菜谱”——这是该 demo 与shared-state-read-write的本质区别; - 无状态上下文传递:前端无需把菜谱内容当作消息上下文发送,Agent 直接从
runtime.state读取,这正是共享状态模式相比“手动拼上下文”的优势。
该 Flow 在 conversational_flows.py 中注册为_conversational_type(SharedStateReadFlow),对应shared-state-readagent 名称,与前端agent="shared-state-read"及useAgent({ agentId: "shared-state-read" })一一对应。
自动化回归中,发送侧边栏消息的断言方式为(shared-state-read.spec.ts):
const input = page.getByPlaceholder("Type a message"); await input.fill("What recipe am I making?"); await input.press("Enter"); await expect( page.locator('[data-testid="copilot-assistant-message"]').first(), ).toBeVisible({ timeout: 30000 });七、错误处理验证
QA 文档的错误处理检查点有三项:
| 检查点 | 实现/验证方式 |
|---|---|
| 发送空消息应被优雅处理 | 侧边栏消息发送前对空内容进行过滤/拦截,不触发 Agent 调用 |
| 正常使用过程无 console 错误 | handleImprove对runAgent的 Promise 做了.catch兜底,异常只会console.error而不会抛出未捕获错误 |
| "Improve with AI" 在加载期间禁用 | disabled={isLoading}+if (agent.isRunning) return;双重防护 |
recipe-card.tsx中的isLoading由agent.isRunning派生,因此 Agent 执行期间的任何并发触发都会被拦截,避免状态竞争。
八、预期结果与验收标准
QA 文档给出的最终验收标准如下:
- 菜谱卡片与侧边栏 3 秒内加载完成;
- Agent 10 秒内响应;
- 菜谱状态在 UI 与 Agent 之间双向同步(UI 编辑 → Agent 读取到新值;Agent 建议 → UI 呈现);
- ping indicator 高亮被修改的区域;
- 无 UI 错误、无布局破坏。
Playwright 配置中对应的超时阈值(15s 定位、30s 响应等待)为线上环境留出了余量,实际验收以文档中的 3s/10s 为目标基准。自动化测试覆盖了“页面加载 + 侧边栏挂载”“建议渲染”“添加食材”“发送消息并收到响应”四条核心链路,其余编辑类操作可参照 QA 清单在手工测试中逐项核对。
结语:从验证清单反推架构
shared-state-read的 QA 文档表面上是一份功能清单,但其背后是 CopilotKit 共享状态模式的一种最小可信实现:前端agent.setState负责写、后端 Flow 通过 system prompt 注入负责读、类型定义RecipeAgentState在前后端之间维持契约。对照同目录的shared-state-read-writedemo(Agent 侧可写回 notes),可以清晰看到 CopilotKit 在「只读上下文」与「双向共享状态」之间的设计取舍。若要在自己的项目中复刻该模式,只需三件事:定义一个共享状态类型、在 UI 中调用agent.setState、在后端 Flow 中把对应字段注入每次模型调用。
【免费下载链接】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),仅供参考