)
CopilotKit CrewAI 双向共享状态实战UI 与 Agent 读写同一份状态Read Write【免费下载链接】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 Crews 集成的shared-state-read-write共享状态读写演示讲解如何让前端 UI 与后端 Agent 双向读写同一份共享状态UI 通过agent.setState写入偏好preferencesAgent 通过set_notes工具写入笔记notes两端通过 AG-UI 协议实时同步。读完本文你将掌握这种前端可写、Agent 可读、双向回写状态架构的实现原理、完整调用链与 QA 验证方法。一、功能定位为什么需要双向共享状态在传统聊天架构中状态要么只存在于前端表单、侧栏要么只存在于 Agent对话记忆两端的任何一方都无法感知另一方的数据。shared-state-read-write演示打破了这个壁垒让 UI 与 Agent 共同持有并维护同一个状态对象形成双向数据流UI → Agent写侧栏表单姓名、语气、语言、兴趣通过agent.setState(...)写入state.preferences。Agent 每一轮对话都从自身状态中读取最新偏好注入系统提示词从而让回复即时适配用户设置。Agent → UI读Agent 通过set_notes工具写入state.notes侧栏的笔记卡片通过useAgent订阅状态变更实时渲染 Agent 写入的内容。UI 回写 Agent 产出的切片笔记卡片上的 Clear 按钮将notes清空并写回 Agent 状态Agent 在下一轮对话中便不再引用已清空的笔记。这种模式在 Agentic UI 场景下非常实用偏好设置、表单草稿、任务清单等数据既需要用户在前端直接编辑又需要 Agent 在执行推理时感知并依据它们行动。该演示对应的参考实现位于 showcase/integrations/crewai-crews/src/app/demos/shared-state-read-write/README.md与shared-state-read、shared-state-write、shared-state-streaming等演示共同构成共享状态系列。二、技术前提与部署入口按 showcase/integrations/crewai-crews/qa/shared-state-read-write.md 所述运行与验证该演示需要满足以下前提Demo 已部署且可访问仪表盘宿主上存在/demos/shared-state-read-write路由Agent 后端健康/api/health返回正常OPENAI_API_KEY已配置FastAPI Agent 服务器挂载了 Flow 端点在 src/agent_server.py 中通过add_crewai_flow_fastapi_endpoint将shared_state_read_write_flow挂载到/shared-state-read-write。后端挂载代码的关键调用如下app, shared_state_read_write_flow, /shared-state-read-write值得强调的是该演示不能通过普通 CrewAI Crew 端点承载。原因在 src/agents/shared_state_read_write.py 的模块文档中写得非常清楚ChatWithCrewFlow不会将逐工具的状态变更暴露给 AG-UI 桥接层它唯一的写入操作是在模型调用特殊crew_name工具时把result.raw追加到state[outputs]。因此该模块完全绕过 Crew 流程直接使用add_crewai_flow_fastapi_endpoint挂载一个专用 Agent。三、核心实现后端 Flow 源码剖析双向状态的后端核心位于 src/agents/shared_state_read_write.py它参考了langgraph-python中的同名实现但采用crewai.flow.Flow重新实现从而完全掌控 LLM 调用、工具 Schema 与状态变更。3.1 状态模型定义状态模型包含两个字段分别代表两个方向的写入来源class Preferences(BaseModel): Shape of the user-owned preferences the UI writes via setState. name: str tone: str casual # formal | casual | playful language: str English interests: List[str] Field(default_factorylist) class AgentState(CopilotKitState): Bidirectional shared state. preferences: Optional[Preferences] None # UI 通过 agent.setState 写入 notes: List[str] Field(default_factorylist) # Agent 通过 set_notes 工具写入AgentState继承自CopilotKitState因此天然携带messages、copilotkit等 AG-UI 桥接所需的运行时字段。preferences与notes就是 UI 和 Agent 共享的那一份状态。3.2 set_notes 工具 Schema后端没有使用 CrewAI 的BaseTool而是直接提供 OpenAI 兼容的 JSON Schema 工具定义。因为监督 LLM 调用直接走litellm.acompletionJSON Schema 才是最匹配的原语SET_NOTES_TOOL { type: function, function: { name: set_notes, description: ( Replace the notes array in shared state with the FULL updated list of short note strings. Use whenever the user asks you to remember something, or when you observe something worth surfacing in the UIs notes panel. Always pass the FULL list (existing notes any new ones), not a diff. Keep each note short ( 120 chars). ), parameters: { type: object, properties: { notes: { type: array, items: {type: string}, description: Full list of short notes that should be visible in the UIs notes panel., } }, required: [notes], }, }, }这里有一条非常关键的契约约定模型必须传递完整的新列表已有笔记 新增笔记而不是增量 diff。这与 QA 文档中确认 Agent 按set_notes契约传递完整更新后的列表的验证项一一对应。3.3 偏好注入_build_prefs_block_build_prefs_block将用户偏好拼装为一段系统提示词供每轮对话注入def _build_prefs_block(prefs: Optional[Preferences]) - Optional[str]: if prefs is None: return None has_any bool(prefs.name or prefs.tone or prefs.language or prefs.interests) if not has_any: return None lines [The user has shared these preferences with you:] if prefs.name: lines.append(f- Name: {prefs.name}) if prefs.tone: lines.append(f- Preferred tone: {prefs.tone}) if prefs.language: lines.append(f- Preferred language: {prefs.language}) if prefs.interests: lines.append(f- Interests: {, .join(prefs.interests)}) lines.append( Tailor every response to these preferences. Address the user by name when appropriate. ) return \n.join(lines)注意其防御性当prefs为空或所有字段都为空时返回None上层据此跳过前缀注入——这正是 QA 文档错误处理一节中清空所有兴趣与姓名后发送 Who am I?Agent 仍能正常回答的实现依据。3.4 主流程 chat()工具执行循环chat()是start()标注的 Flow 入口实现了与 LangGraph 参考实现一致的工具自动循环反序列化preferences可能以 dict 形式跨 AG-UI 边界到达因此先判断isinstance(prefs, dict)并重新构造Preferences实例注入偏好构造system_content将偏好块放在基础系统提示词之前合并工具tools [*self.state.copilotkit.actions, SET_NOTES_TOOL]即前端注册的 actions 与后端的set_notes工具并列循环调用 LLM最多_MAX_ITERATIONS 5次往返防止模型持续调用工具导致死循环每次通过copilotkit_streamacompletion(streamTrue)获取流式响应处理工具调用若响应无tool_calls说明模型已产出文本回复直接结束否则遍历所有工具调用代码注释明确指出不能只取[0]否则会丢弃多余的 tool call导致消息线程在下一轮被聊天 API 拒绝非set_notes的工具调用属于前端注册的 actionAG-UI 客户端负责往返后端只需追加占位 tool result 保持消息线程有效set_notes调用则解析参数、清洗列表[str(n) for n in notes if n is not None and str(n)]、写入self.state.notes追加 tool result 并调用copilotkit_emit_tool_result状态快照仅当notes_changed为 True 时才调用copilotkit_emit_state(self.state)广播状态快照触发 UI 的OnStateChanged订阅立即重渲染——纯前端工具轮次不改变共享状态因此无需广播回环携带工具结果再次调用 LLM让模型产出确认文本如 Got it — I noted …否则前端永远看不到 set_notes 之后的确认回复。for _iteration in range(self._MAX_ITERATIONS): messages [system_message, *self.state.messages] response await copilotkit_stream( await acompletion( modelopenai/gpt-5.4, messagesmessages, toolstools, parallel_tool_callsFalse, streamTrue, ) ) message response.choices[0].message self.state.messages.append(message) tool_calls message.get(tool_calls) or [] if not tool_calls: return ... if notes_changed: await copilotkit_emit_state(self.state)模块底部将 Flow 实例化为模块级单例shared_state_read_write_flow SharedStateReadWriteFlow()。add_crewai_flow_fastapi_endpoint会在每个请求时对其进行深拷贝因此初始化成本只在 import 时支付一次。四、核心实现前端双向读写前端演示页位于 src/app/demos/shared-state-read-write/page.tsx。4.1 订阅状态变更读页面通过useAgent订阅 Agent 的每次状态变更const { agent } useAgent({ agentId: shared-state-read-write, updates: [UseAgentUpdate.OnStateChanged], });只要 Agent 通过set_notes工具变更状态并广播快照该 Hook 就会触发、组件重新渲染侧栏面板随即展示最新值。notes与preferences都从agent.state中读取const agentState agent.state as RWAgentState | undefined; const preferences agentState?.preferences ?? INITIAL_PREFERENCES; const notes agentState?.notes ?? [];4.2 写入偏好与初始化写每次侧栏表单编辑都会直接写入 Agent 状态const handlePreferencesChange (next: Preferences) { agent.setState({ preferences: next, notes, // preserve what the agent has written } as RWAgentState); };注意这里显式携带了notes避免 UI 写入偏好时把 Agent 已写入的笔记覆盖掉——这是双向共享状态最容易踩的坑。首次挂载时通过useEffect一次性播种初始状态保证 Agent 在第一轮对话时就有数据可读useEffect(() { if (!agentState?.preferences) { agent.setState({ preferences: INITIAL_PREFERENCES, notes: [], } as RWAgentState); } }, []);初始默认值由INITIAL_PREFERENCES定义name: 、tone: casual、language: English、interests: []。这正是 QA 文档刷新页面后偏好重置为默认值、笔记清空状态按会话隔离由页面 useEffect 播种验证项的实现来源。4.3 UI 回写 Agent 产出的切片Clear 笔记笔记卡片上的 Clear 按钮同样走agent.setStateconst handleClearNotes () { agent.setState({ preferences, notes: [] } as RWAgentState); };至此preferences与notes两个字段分别验证了两种方向的写入UI 写preferences、Agent 写notes而 Clear 则演示了 UI 回写 Agent 产出的切片。4.4 侧栏组件细节偏好卡片preferences-card.tsx是一个纯受控表单完全不知道 Agent 的存在所有编辑通过onChange冒泡给父页面再被父页面送入agent.setState。其数据模型为export interface Preferences { name: string; tone: formal | casual | playful; language: string; interests: string[]; }兴趣标签固定为INTEREST_OPTIONS [Cooking, Travel, Tech, Music, Sports, Books, Movies]支持多选与取消。卡片底部有data-testidpref-state-json的 JSON 预览区实时展示JSON.stringify(value, null, 2)的结果方便在界面上直接核对偏好写入是否生效。笔记卡片notes-card.tsx则相反只负责渲染state.notes无笔记时显示notes-empty空状态有笔记时渲染带编号的note-item列表并仅在notes.length 0时显示notes-clear-button。五、QA 验证手册完整测试步骤以下测试步骤来自 showcase/integrations/crewai-crews/qa/shared-state-read-write.md同时可在 tests/e2e/shared-state-read-write.spec.ts 中找到对应的端到端自动化实现用于回归验证双向共享状态。5.1 基础功能检查导航到/demos/shared-state-read-write页面应在 3 秒内渲染完成左侧为偏好 笔记两张卡片右侧为CopilotChat面板data-testidpreferences-card可见且标题为 Your preferencesdata-testidnotes-card可见且标题为 Agent notes空状态data-testidnotes-empty文案为 No notes yet. Ask the agent to remember something.聊天输入框占位符为 Chat with the agent...3 个建议 pill 均可见标题逐字匹配Greet me、Remember something、Plan a weekend发送 Hello10 秒内出现助手文本回复。说明笔记卡片在演示页中的实际标题为 Agent Scratch pad、空状态文案为 the agent will make observations about you and note them here!见 notes-card.tsx。QA 文档描述的文案可能对应其他部署形态验证时以被测页面实际渲染内容为准。5.2 UI 写入 → Agent 读取偏好在data-testidpref-name输入 Ataipref-state-json随即出现name: Atai将data-testidpref-tone改为formalJSON 预览反映tone: formal将data-testidpref-language改为SpanishJSON 预览反映language: Spanish点击Cooking与Travel兴趣标签两者呈选中态interests数组包含这两项发送 What do you know about me?10 秒内回复应引用 Atai 这个名字、正式语气、西班牙语以及 Cooking/Travel 兴趣——监督 Flow 的chat()步骤每轮都会把偏好块前置到系统消息中即_build_prefs_block的注入逻辑点击 Plan a weekend 建议回复应针对所选兴趣量身定制。5.3 Agent 写入 → UI 读取笔记点击 Remember something 建议发送 Remember that I prefer morning meetings and that I dont eat dairy.15 秒内data-testidnotes-list出现在笔记卡片中且包含至少 2 条data-testidnote-item分别提及 morning meetings 与 dairy——它们经由 Flow 在set_notes工具调用后的copilotkit_emit_state快照送达 UInotes-empty不再渲染发送 Also remember I live in Berlin.15 秒内笔记列表增长旧笔记保留、新笔记追加——确认 Agent 按set_notes契约传递完整更新后的列表。5.4 UI 回写 Agent 产出的切片清空笔记有笔记时data-testidnotes-clear-button可见点击 Clear笔记列表消失notes-empty重新渲染提问 What do you remember about me?Agent 不再引用已清空的笔记——因为状态已由 UI 通过agent.setState({ notes: [] })写回。5.5 多轮状态持久化将语气改为playful并添加Music兴趣发送 Write me a one-line haiku greeting.回复应轻松俏皮且提及音乐追加发送 Do it again in French.回复保持俏皮、切换为法语且仍然承认音乐兴趣——确认偏好跨轮次持续生效无需重复发送刷新页面偏好重置为默认值tone: casual、language: English、空兴趣、空姓名笔记也重置为空——状态按会话隔离由页面useEffect播种。5.6 错误处理发送空消息应为 no-op不产生用户气泡、无助手回复取消全部兴趣并清空姓名后发送 Who am I?Agent 正常回答不崩溃——Flow 的_build_prefs_block对空偏好返回None并跳过前缀注入DevTools → Console 在上述任何流程中均无未捕获异常。5.7 预期结果汇总页面 3 秒内加载完成助手文本回复 10 秒内返回偏好写入在变更时同步反映到pref-state-jsonAgent 产出的笔记在 remember 提示后 15 秒内出现在笔记卡片由copilotkit_emit_state快照驱动后续set_notes调用完整保留既有列表Clear 按钮完成 UI → Agent 状态回写Agent 下一轮即失去对已清空笔记的访问无 UI 布局破坏、无未捕获控制台错误。六、架构总结与工程要点综合前端页面、后端 Flow 与 QA 验证该演示的完整数据链路可以概括为UI 写偏好表单编辑 →agent.setState({ preferences, notes })→ AG-UI 协议同步到后端self.state.preferencesAgent 读偏好每轮chat()反序列化偏好 →_build_prefs_block生成偏好块 → 注入系统提示词 → LLM 依据偏好回复Agent 写笔记模型调用set_notes工具 → Flow 清洗并写回self.state.notes→copilotkit_emit_tool_resultcopilotkit_emit_state广播快照UI 读笔记useAgent({ updates: [OnStateChanged] })收到快照 → 重渲染笔记卡片UI 回写Clear 按钮 →agent.setState({ notes: [] })→ Agent 下一轮不再引用。工程要点归纳如下必须显式保留对方写入的字段UI 每次setState都应携带完整的{ preferences, notes }结构防止单向写入覆盖另一方向的数据Agent 工具契约用全量列表而非 diffset_notes的描述明确要求传递完整列表QA 也专门验证旧笔记保留、新笔记追加仅在有状态变更时广播快照notes_changed为 True 才调用copilotkit_emit_state避免无谓的前端重渲染工具循环上限_MAX_ITERATIONS 5防止模型无限调用工具遍历全部 tool_calls 而非仅取首个保证消息线程对聊天 API 始终有效无法用 Crew 端点承载若需要逐工具的共享状态变更暴露给 AG-UI必须使用add_crewai_flow_fastapi_endpoint挂载专用 Flow。读者可以进一步参考同目录下的 shared-state-read.md、shared-state-write.md 与 shared-state-streaming.md对比单向读、单向写与流式状态三个变体从而完整掌握 CopilotKit 共享状态在 CrewAI 生态中的全貌。【免费下载链接】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),仅供参考