CopilotKit × AG2 集成实战: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 仓库中 AG2 集成 showcase 的 QA 文档 为主线,完整讲解「Shared State(只读)——AI 配方助手」演示应用的功能验证方法。文章先给出可直接照做的分步测试清单与预期指标,再从源码层面剖析「前端通过agent.setState发布状态、后端 Agent 只读不写」这一模式的实现原理,并结合 Playwright E2E 测试说明如何把手工 QA 清单自动化。读完你可以独立完成该演示的验收测试,并理解 CopilotKit v2useAgent共享状态机制的底层工作方式。
一、功能定位:什么是「Shared State 只读」模式
在 AG2 集成中,CopilotKit 支持多种 Agent 状态协作方式。manifest.yaml 的features字段列出了两类状态能力:
readonly-state-agent-context:前端通过useAgentContext向 Agent 提供只读上下文;shared-state-read-write:双向共享状态,UI 写入偏好、Agent 通过 AG2ContextVariables回写笔记。
本 QA 文档覆盖的shared-state-read演示则属于其中"只读"变体的一种:UI 通过agent.setState把配方(recipe)发布到共享状态,Agent 在每一轮对话中读取这份状态来回答用户,但从不改写它。正如 page.tsx 顶部注释所描述:
单一事实来源(single source of truth)是
agent.state.recipe。表单是一个纯受控组件,每一次编辑都直接流入agent.setState({...}),下一次渲染立即反映出来。
因此这份 QA 文档的核心价值在于:验证"前端发布状态 → Agent 读取状态 → 回答反映当前状态"这条链路是否端到端贯通。
二、前置条件(Prerequisites)
在开始验证前,QA 文档要求满足两项前置条件:
- 演示应用已部署且可访问:
shared-state-read演示页位于/demos/shared-state-read路由,由 page.tsx 提供。 - Agent 后端健康:检查
GET /api/copilotkit健康探针。
关于健康检查,route.ts 中的GET处理器会向后端AGENT_URL(默认http://localhost:8000)的/health端点发起带 3 秒超时的探测,返回形如:
{ "status": "ok", "agent_url": "http://localhost:8000", "agent_status": "reachable", "env": { "OPENAI_API_KEY": "set", "NODE_ENV": "development" } }agent_status字段只有reachable时才代表后端就绪。后端健康端点由 agent_server.py 中的HealthMiddleware提供——它特意用中间件而非@app.get("/health")实现,因为根路径会被app.mount("/", ...)的 catch-all 挂载遮蔽,只有跑在路由解析层之上的中间件才能保证/health永远可达。
三、Step 1:基础功能验证
按 QA 清单逐项勾选:
- 导航到
shared-state-read演示页(路由/demos/shared-state-read); - 验证配方卡片表单加载成功(
data-testid="recipe-card"); - 验证
CopilotSidebar默认打开,标题为"AI Recipe Assistant"; - 通过侧边栏发送一条消息;
- 验证 Agent 正常响应。
页面骨架对应 page.tsx:
<CopilotKit runtimeUrl="/api/copilotkit" agent="shared-state-read"> <div className="min-h-screen w-full bg-gray-50"> <Recipe /> <CopilotSidebar defaultOpen labels={{ modalHeaderTitle: "AI Recipe Assistant" }} /> </div> </CopilotKit>CopilotSidebar通过defaultOpen默认展开,labels.modalHeaderTitle即侧边栏标题——这两项直接对应 QA 清单的校验点。消息发送与 Agent 响应则走CopilotKit运行时到后端HttpAgent的 AG-UI 协议链路(详见第六节路由分析)。
四、Step 2:功能特性专项检查
4.1 初始配方状态(Initial Recipe State)
- 配方标题输入框显示"Make Your Recipe";
- 烹饪时间下拉框默认为"45 min";
- 技能水平下拉框默认为"Intermediate";
- 默认配料正确显示:
- Carrots(3 large, grated),带胡萝卜 emoji(🥕);
- All-Purpose Flour(2 cups),带小麦 emoji(🌾);
- 默认烹饪步骤显示:"Preheat oven to 350 F"。
这些默认值并非魔法数字,而是定义在 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)"], };同时 types.ts 定义了完整的状态模型:
SkillLevel枚举:Beginner/Intermediate/Advanced;CookingTime枚举:5 min/15 min/30 min/45 min/60+ min(cookingTimeValues数组将其映射为下拉框选项,默认索引 3 即 45 min);SpecialPreferences枚举:High Protein/Low Carb/Spicy/Budget-Friendly/One-Pot Meal/Vegetarian/Vegan;RecipeData接口与RecipeAgentState(仅含recipe字段)——后者即agent.state的 TypeScript 形态。
4.2 建议(Suggestions)
- 可见"Create Italian recipe"建议;
- 可见"Make it healthier"建议;
- 可见"Suggest variations"建议。
三条建议由 page.tsx 中的useConfigureSuggestions声明,available: "always"表示建议常驻可用:
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", });4.3 配方编辑(本地状态)
- 编辑配方标题并验证实时更新;
- 切换技能水平下拉框并验证更新;
- 切换烹饪时间下拉框并验证更新;
- 切换某个饮食偏好复选框(如 "Vegetarian")并验证被勾选;
- 点击"+ Add Ingredient"(
data-testid="add-ingredient-button")并验证出现新的空行; - 编辑配料的名称与用量;
- 点击 "x" 按钮删除一个配料;
- 点击"+ Add Step"并验证出现新的步骤行;
- 编辑一条步骤并验证保存;
- 点击 "x" 按钮删除一条步骤。
这些编辑操作全部由 recipe-card.tsx 这个纯受控组件完成:RecipeCard接收recipe、isLoading、onChange、onImprove四个 props,内部update(partial)通过onChange({ ...recipe, ...partial })把每一次局部修改合并后上抛。新增配料时默认追加一个空行{ icon: "🍴", name: "", amount: "" };删除则用filter重建数组。因为组件不持有任何本地副本,agent.state.recipe是唯一状态源,所以 QA 中"验证更新/删除"实际上都是在验证setState后的重渲染是否正确。
4.4 AI 驱动的配方更新(useAgent + shared state)
- 点击"Create Italian recipe"建议;
- 验证 Agent 更新了配方标题、配料与步骤;
- 验证变更区域出现ping 指示器(高亮标记);
- 验证"Improve with AI"按钮(
data-testid="improve-button")在加载中变为"Please Wait..."; - 点击"Improve with AI"并验证配方被增强。
useAgent是这一节的核心。看 page.tsx 的用法:
const { agent } = useAgent({ agentId: "shared-state-read", updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged], });UseAgentUpdate枚举定义在 use-agent.tsx:
export enum UseAgentUpdate { OnMessagesChanged = "OnMessagesChanged", OnStateChanged = "OnStateChanged", OnRunStatusChanged = "OnRunStatusChanged", }本演示订阅了OnStateChanged(状态变化触发重渲染)与OnRunStatusChanged(运行状态变化触发重渲染)。ping 指示器的来源正是这种订阅机制:当 Agent 的回复触发OnStateChanged后,React 重渲染由agent.state.recipe派生的 UI 部分,被修改过的字段即可高亮(QA 文档称之为 ping indicator,对应use-agent.tsx中"状态变更通知驱动重渲染"的行为)。
"Improve with AI" 按钮的行为在 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)); };对应在 recipe-card.tsx 中,按钮disabled={isLoading},加载时渲染<Spinner /> + "Please Wait..."——这就是 QA 清单中"加载中显示 Please Wait..."与"加载期间按钮禁用"的实现依据:
<Button>编辑配方(改标题、添加配料);向 Agent 提问"What recipe am I making?"; 验证 Agent 的回答引用当前配方状态。 这是本演示的"灵魂"测试:Agent 必须真正读到agent.state.recipe并据此作答,而不是笼统回复。其底层链路是:前端agent.setState({ recipe: next })→ 通过/api/copilotkit运行时把初始状态随 AG-UI 协议发给后端 → AG2 的AGUIStream把状态映射为 ContextVariables → Agent 在每一轮对话中读取。而"只读"的关键在于后端没有任何会改写 recipe 的工具——可对照 shared_state_read_write.py 的"读+写"版本:那边后端注册了get_current_preferences与set_notes两个工具来读写状态;而本只读演示没有对应后端工具,因此 Agent 只能读、不能写。
五、Step 3:错误处理验证
- 发送空消息(应被优雅处理,不崩溃、不破坏 UI);
- 正常使用过程中无控制台报错;
- 加载过程中 "Improve with AI" 按钮处于禁用状态。
空消息的优雅处理由 CopilotKit 运行时负责兜底;按钮禁用已在 4.4 节说明(disabled={isLoading})。QA 的"无控制台错误"标准同时覆盖前端 React 错误与后端运行时报错——后端一旦抛错,route.ts 会返回带errorId的 500 响应并仅把详细堆栈记录在服务端日志({ error: "internal runtime error", errorId }),前端可通过控制台日志中的errorId与服务端日志关联排查。
六、预期结果(Expected Results)
QA 文档给出的验收指标:
- 配方卡片与侧边栏在3 秒内加载完成;
- Agent 在10 秒内响应;
- 配方状态在 UI 与 Agent 之间双向同步(UI 变更立即反映,Agent 回答引用最新状态);
- ping 指示器高亮显示变更区域;
- 无 UI 错误或布局破损。
这些时间指标也是 Playwright E2E 测试的超时参考(见下节)。
七、源码纵深:运行链路与路由注册
shared-state-read之所以能用"只读"Agent,关键在于 route.ts 中的注册表:shared-state-read被列入sharedAgentNames数组,与agentic_chat、prebuilt-sidebar等一起复用同一个默认 AG2ConversableAgent(由src/agents/agent.py中的AGUIStream包装),统一指向根路径后端:
const sharedAgentNames = [ "agentic_chat", "human_in_the_loop", "tool-rendering", "gen-ui-tool-based", "shared-state-read", "shared-state-write", "shared-state-streaming", ... ]; // ... for (const name of sharedAgentNames) { agents[name] = createAgent(); // 默认路径 "/" }
而真正需要独立状态槽的演示(如shared-state-read-write)则走dedicatedAgents映射,挂到后端独立子应用路径:
const dedicatedAgents: Record<string, string> = { "shared-state-read-write": "/shared-state-read-write/", subagents: "/subagents/", "headless-complete": "/headless-complete/", // ... };
后端侧,agent_server.py 通过app.mount("/shared-state-read-write", shared_state_read_write_app)等把各子应用挂在具名路径下,/根路径留给默认 Agent。文件头部的注释还解释了中间件顺序(CORS → RequestUserMessage → HeaderForwarding → Health → routes)与install_executor_contextvar_propagation()的原因:autogen 的ConversableAgent会通过loop.run_in_executor把 LLM 调用派发到线程池,而 ContextVar 不会自动传播到工作线程,必须显式打补丁才能让请求头(如 aimock 上下文)在出站 httpx 调用中保持可见。这些是理解后端如何为"Agent 读取状态"提供稳定运行环境的关键细节。
八、源码纵深:E2E 测试如何自动化 QA 清单
手工 QA 清单中的大部分检查点已经在 shared-state-read.spec.ts 中被 Playwright 自动化,二者通过data-testid一一对应:
test("recipe card loads with default ingredients and the sidebar mounts", async ({ page }) => { await page.goto("/demos/shared-state-read"); await expect(page.locator('[data-testid="recipe-card"]')).toBeVisible({ timeout: 15000 }); await expect(page.getByText("AI Recipe Assistant")).toBeVisible({ timeout: 10000 }); await expect(page.locator('[data-testid="ingredients-container"]')).toBeVisible(); await expect(page.locator('[data-testid="instructions-container"]')).toBeVisible(); }); test("starter suggestions render", async ({ page }) => { for (const title of ["Create Italian recipe", "Make it healthier", "Suggest variations"]) { await expect(page.getByRole("button", { name: title })).toBeVisible({ timeout: 15000 }); } }); test("clicking 'Add Ingredient' appends a new ingredient-card row", async ({ page }) => { 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 }); }); test("sending a sidebar message returns an assistant response", async ({ page }) => { 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 }); });
可以看到:测试的"3 秒加载 / 10 秒响应"预期在 E2E 中以timeout: 15000/timeout: 30000的形式体现(首屏与首次响应的超时放宽到 2~3 倍以保证 CI 稳定性)。测试配置见 playwright.config.ts:testDir: "./tests/e2e"、本地模式通过webServer自动执行pnpm dev启动 Next.js(端口 3000),且统一注入X-AIMock-Context: ag2请求头用于后端 fixture 匹配。
九、对照阅读:与「读+写」双向状态的区别
若想理解"只读"与"双向"的边界,建议对照 shared-state-read-write.md 与后端实现 shared_state_read_write.py。后者演示了完整的双向链路:
- UI → Agent(写):UI 通过
agent.setState({...})写入preferences,AGUIStream在每次 run 时把初始状态映射进ContextVariables;Agent 通过get_current_preferences工具读取; - Agent → UI(读/写回):Agent 调用
set_notes更新notes槽位,每次调用返回携带更新后ContextVariables的ReplyResult,AGUIStream将其传回 UI,触发useAgent({ updates: [OnStateChanged] })重渲染。
两者的 QA 验证点也因此不同:只读版验证"Agent 的回答引用了当前状态";读写版还要验证"Agent 写的笔记实时出现在 UI 面板"、"UI 清空笔记后 Agent 下一轮能感知"。这组对照恰好勾勒出 CopilotKit 共享状态能力的完整光谱:前端发布、后端读取(只读)→ 前端发布 + 后端读写(双向)。
十、快速上手与排错要点
- 本地运行:前端仓库
pnpm dev启动(见 playwright.config.ts),后端agent_server.py以PORT=8000(默认)运行,前端通过AGENT_URL环境变量指向它(默认http://localhost:8000)。两个进程都就绪后,健康探针GET /api/copilotkit应返回agent_status: "reachable"。 - 依赖密钥:后端依赖
OPENAI_API_KEY(及可选OPENAI_BASE_URL),Agent 的LLMConfig在模块导入时即构造客户端,因此 agent_server.py 特意在导入任何 Agent 模块之前调用load_dotenv(),否则密钥读不到。 - 验证套路总结:凡是涉及状态的问题(标题改了没生效、Agent 回答不引用最新配方),优先检查三点——①
useAgent是否订阅了UseAgentUpdate.OnStateChanged;② 每次编辑是否真正调用了agent.setState(本演示中即handleChange);③ 后端/health是否reachable、OPENAI_API_KEY是否 set。 - QA 与代码的对应索引:手工清单 shared-state-read.md → 前端实现 page.tsx / recipe-card.tsx / types.ts → 自动化测试 shared-state-read.spec.ts,三者共享同一组
data-testid,形成"需求 → 实现 → 测试"的可追踪闭环。
【免费下载链接】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),仅供参考