news 2026/9/10 11:16:14

CopilotKit × AG2 双向共享状态实战:UI 与 Agent 如何读写同一个 State(Shared State Read + Write)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CopilotKit × AG2 双向共享状态实战:UI 与 Agent 如何读写同一个 State(Shared State Read + Write)

CopilotKit × AG2 双向共享状态实战:UI 与 Agent 如何读写同一个 State(Shared State 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 仓库中 AG2 集成 demo 的Shared State(Read + Write)功能展开,系统讲解"前端写、Agent 读;Agent 写、前端读"的双向共享状态机制。你将掌握useAgent()/agent.setState()的完整用法、后端基于 AG2ContextVariables+ReplyResult的状态回写原理,以及一份可复用的端到端验收清单(含错误处理与预期结果),可直接用于验证同类功能的正确性。

一、功能全景:什么是"双向共享状态"

Shared State 是 CopilotKit 中让UI 与 Agent 共享同一份状态对象的能力。在showcase/integrations/ag2中,这个 demo 的共享状态由两个字段组成(见 前端类型定义):

  • preferences:由 UI 通过agent.setState()写入(用户档案:名字、语气、语言、兴趣),Agent 在每轮回复前读取并据此调整行为;
  • notes:由 Agent 通过set_notes工具写入(Agent 的"备忘清单"),UI 通过useAgent()订阅并实时渲染。

两个方向同时成立,就形成了闭环:

UI 写入 preferences ──▶ agent.setState() ──▶ 后端注入系统提示词 ──▶ Agent 回复适配 UI 读取 notes ◀── Agent 调用 set_notes ◀── ContextVariables 状态回传 ◀── useAgent 订阅

官方说明(见 demo README)将其概括为三点:

  • UI → agent:侧边栏表单(name、tone、language、interests)通过agent.setState(...)写入state.preferences;后端中间件每轮读取并注入系统提示词。
  • agent → UI:Agent 的set_notes工具写入state.notes;侧边栏 notes 卡片在 Agent 更新时实时重渲染。
  • Round-trip:在侧边栏编辑偏好,能立刻体现在 Agent 的下一轮回复中(语气、语言、直呼其名)。

二、前置条件(Prerequisites)

运行本功能前需满足以下条件(对应 QA 文档):

  1. Demo 已部署并可通过/demos/shared-state-read-write访问;
  2. Agent 后端健康,GET /api/copilotkit返回agent_status: reachable
  3. 后端已配置OPENAI_API_KEY
  4. shared-state-read-writeAgent 已挂载在 FastAPI 服务器的/shared-state-read-write路径下(见 agent_server.py)。

后端 Agent 的挂载逻辑位于 shared_state_read_write.py 末尾:stream = AGUIStream(agent)构建 AG-UI 流式服务,随后shared_state_read_write_app.mount("", stream.build_asgi())将其挂载为独立 FastAPI 应用,再在agent_server.py中统一注册到主服务器。

三、页面结构与初始状态(Step 1:页面渲染)

3.1 页面骨架

页面由SharedStateReadWriteDemo组件构成(见 page.tsx):

<CopilotKit runtimeUrl="/api/copilotkit" agent="shared-state-read-write"> <DemoContent /> </CopilotKit>

DemoContent内部通过DemoLayout(见 demo-layout.tsx)渲染两个卡片和一个聊天侧边栏:

区域标识说明
Preferences 卡片data-testid="preferences-card"用户编辑偏好(Name / Tone / Language / Interests)
Agent notes 卡片data-testid="notes-card"展示 Agent 写入的备忘笔记
聊天侧边栏CopilotSidebaragentId="shared-state-read-write"defaultOpen={true},占位文案 "Chat with the agent..."

3.2 初始共享状态

INITIAL_PREFERENCES定义在 page.tsx:

const INITIAL_PREFERENCES: Preferences = { name: "", tone: "casual", language: "English", interests: [], };

页面加载后,Preferences 卡片底部的"Shared state"JSON 面板(data-testid="pref-state-json")会展示与之一致的初始值;notes 卡片则显示空态文案 "No notes yet. Ask the agent to remember something."(前端组件实际文案见 notes-card.tsx,QA 文档记录的是空态语义,两者表达一致)。

值得注意的一个细节:DemoContent在挂载时通过useEffect调用一次agent.setState()播种初始状态(见 page.tsx),确保 Agent 在第一轮对话前就有preferencesnotes可读——即使此时默认值全部为空。

四、UI → Agent:前端写入(Step 2)

4.1 写入入口:agent.setState()

前端把每次表单变更直接同步进共享状态(见 page.tsx):

const handlePreferencesChange = (next: Preferences) => { agent.setState({ preferences: next, notes, // 保留 Agent 已写入的内容 } as RWAgentState); };

关键点:写入时必须携带完整的状态对象preferences+notes),否则会覆盖掉 Agent 端写入的notes。这是双向共享状态最容易踩的坑。

PreferencesCard本身是纯受控组件(见 preferences-card.tsx),对 Agent 一无所知:每个输入变化只是调用onChange({ ...value, [key]: v }),真正与 Agent 状态对接的逻辑上移到了父级页面。这种分层让卡片可独立复用。

4.2 可编辑的偏好维度

  • Name:文本输入(data-testid="pref-name"),例如填入Atai
  • Tone:下拉选择,取值为formal/casual/playfuldata-testid="pref-tone");
  • Language:下拉选择,支持English/Spanish/French/German/Japanese
  • Interests:Badge 按钮组,预置选项见 INTEREST_OPTIONS:CookingTravelTechMusicSportsBooksMovies,可多选/取消。

每次编辑后,"Shared state" JSON 面板立即更新——这验证了写入已即时进入共享状态。

4.3 后端如何消费 UI 的写入

UI 的写入并不会凭空生效。后端在 shared_state_read_write.py 中做了两件事:

① 定义与前端一致的数据契约(Pydantic 模型):

class Preferences(BaseModel): name: str = Field(default="", description="The user's preferred name") tone: str = Field(default="casual", ...) language: str = Field(default="English", ...) interests: List[str] = Field(default_factory=list, ...) class SharedSnapshot(BaseModel): preferences: Preferences = Field(default_factory=Preferences) notes: List[str] = Field(default_factory=list)

② 提供get_current_preferences工具:工具注释明确要求 Agent "Always call this BEFORE answering"。其实现通过_load_snapshot()ContextVariables中读取快照并返回 JSON(见 shared_state_read_write.py)。

③ 在系统提示词中强制读取system_message写明 "Callget_current_preferencesBEFORE answering, every turn, and tailor your reply to those preferences",并要求 "Never repeat preferences back at the user verbatim — just adapt"(见 shared_state_read_write.py)。

正是这套"契约 + 工具 + 提示词约束"的组合,保证了 Agent 会用用户的名字、按指定语气、用指定语言回复,而不是简单复读 JSON。

4.4 验证方式

按 QA 文档输入示例:

  1. Name 填Atai,Tone 选playful,Language 选Spanish,再勾选 2~3 个兴趣(如CookingTech);
  2. 发送"Greet me in one sentence."
  3. 期望:Agent 用名字称呼你、语气俏皮、用西班牙语回复,且不是复读 JSON。

五、Agent → UI:后端写入,前端实时渲染(Step 3)

5.1 写入工具:set_notes

set_notes是 Agent 侧唯一的状态写入通道(见 shared_state_read_write.py):

@tool() async def set_notes(context_variables: ContextVariables, notes: List[str]) -> ReplyResult: snapshot = _load_snapshot(context_variables) cleaned = [str(n).strip() for n in notes if str(n).strip()] snapshot.notes = cleaned context_variables.update(snapshot.model_dump()) return ReplyResult( message=f"Notes updated. Total notes: {len(cleaned)}.", context_variables=context_variables, )

三个实现要点:

  • 整表替换而非 diff:工具注释强调 "Always pass the FULL notes list (existing + new) — not a diff"(每轮必须携带完整列表,防止遗漏历史条目);
  • 状态回传靠ReplyResult:工具把更新后的context_variables挂在ReplyResult上返回,AGUIStream 将其经 AG-UI 协议回传给前端,触发OnStateChanged更新;
  • 入参清洗:去空白、过滤空串,保证笔记列表的整洁。

5.2 前端订阅:useAgent({ updates: [OnStateChanged] })

前端通过订阅机制感知 Agent 的每次状态变更(见 page.tsx):

const { agent } = useAgent({ agentId: "shared-state-read-write", updates: [UseAgentUpdate.OnStateChanged], });

只要 Agent 通过set_notes变更状态,该 hook 就会触发、组件重渲染,notes 卡片随之更新。NotesCard是纯展示组件(见 notes-card.tsx):空态显示占位文案,有数据时渲染带编号的data-testid="note-item"列表。

5.3 验证方式

  1. 发送"Remember that I prefer morning meetings and that I don't eat dairy."
  2. 期望:notes 卡片从空态过渡为至少 2 条note-item(对应上述两个事实),且在 Agent 轮次进行中/结束后实时更新;
  3. 再发送"Also remember I work in Pacific time."
  4. 期望:列表至少 3 条——证明 Agent 传回了完整列表而非仅新增项。

六、Round-trip 与清除(Step 4)

这一环节同时验证"读"与"写"两个方向在同一字段上的闭环(见 page.tsx):

const handleClearNotes = () => { agent.setState({ preferences, notes: [] } as RWAgentState); };

操作步骤与预期:

  1. 点击 notes 卡片的Clear按钮(data-testid="notes-clear-button",仅在有笔记时显示)→ notes 卡片立即回到空态;
  2. 发送"What do you remember about me?"→ Agent 回答"没有任何记忆的笔记";
  3. 含义:UI 通过agent.setState的清除操作,在 Agent 的下一轮被其看到——证明 UI 的写回对 Agent 可见。

这就是最完整的"Round-trip":UI 能写(偏好)、Agent 能写(笔记)、UI 能覆盖 Agent 的写入(Clear)、Agent 能在下一轮感知 UI 的覆盖。

七、错误处理与鲁棒性(Step 5)

QA 文档对异常路径的验收要求:

  • 发送空消息 → 被优雅处理(不崩溃、不破坏 UI);
  • 正常使用过程中无控制台报错。

与此对应,后端在状态读取层面也做了容错设计:_load_snapshot()采用"尽力加载"策略(见 shared_state_read_write.py):

  • ContextVariables中状态缺失或损坏,先尝试从preferencesnotes两个独立槽位做部分恢复
  • 单个槽位仍失败则回退到默认值,保证 Agent 在 UI 尚未调用agent.setState第一轮也能正常工作;
  • 同时记录 WARNING 日志,让静默损坏在服务端可见,而非悄悄降级。

八、预期结果(验收基线)

QA 文档最终定义了五条可量化验收标准:

验收项期望
页面加载< 3 秒
偏好编辑即时传播到 Agent 状态
Agent 回复明显适配偏好(名字、语气、语言)
Notes 卡片反映每一次set_notes调用
UI 清除笔记在 Agent 下一轮被感知

九、端到端测试佐证

仓库为该功能提供了 Playwright 端到端测试(见 shared-state-read-write.spec.ts),覆盖:

  • 挂载断言:"Your preferences" 与 "Agent Scratch pad" 两个面板均可见;
  • 起始建议渲染:"Greet me"、"Remember something"、"Plan a weekend" 三个建议按钮可见;
  • 回归用例(防夹具串扰):测试注释记录了历史上"Greet me"建议曾错误匹配feature-parity.json中裸"hi"夹具、"Plan a weekend"曾匹配裸"plan"夹具的 bug。修复方式是添加子串更长的d5-all.json专用夹具使其优先命中——测试通过正反双断言(toContainText(/shared-state co-pilot/i)not.toContainText(/showcase assistant.../))锁定行为。

这套测试可以作为你验证自己 Shared State 实现的参考模板:正向断言 + 负向断言组合,防止"碰巧命中通用夹具"造成的假阳性。

结语

Shared State(Read + Write)模式的核心价值在于把"用户画像"和"Agent 工作记忆"统一进一份状态:前端负责交互式编辑,后端负责每轮读取与回写,useAgent/agent.setState/ContextVariables/ReplyResult四者协作实现闭环。实践时牢记两个原则——写入携带完整状态、Agent 回传完整列表,即可稳定复现本文描述的全部行为。

【免费下载链接】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/10 11:14:45

Python课程设计:田径运动会管理系统源码深度解析

简介&#xff1a;这是一份Python课程设计田径运动会管理系统源码与数据库打包&#xff0c;面向需要完成数据库或Python课程设计的高校学生&#xff0c;提供了一套可直接运行的运动会管理项目。压缩包共35个文件、约11.24MB&#xff0c;包含4个.py源码、1个.sql数据库脚本、3个.…

作者头像 李华
网站建设 2026/9/10 11:13:23

用R做流行病学数据分析:从回归建模到生存分析实战指南

这本书我一定要推荐给所有想用R做流行病学数据分析的朋友。书名是Epidemiology with R&#xff0c;作者是Bendix Carstensen&#xff0c;牛津大学出版社出的&#xff0c;全英文。市面上讲R语言入门的书多如牛毛&#xff0c;但能真正把“流行病学问题”和“R代码实现”拧成一股绳…

作者头像 李华
网站建设 2026/9/10 11:13:08

广东10米土地覆盖数据解析:分类栅格的元数据与Python处理

简介&#xff1a;本资源为面向GIS研究者、遥感分析人员及城乡规划从业者的高精度广东土地覆盖数据集&#xff0c;解决区域生态评估、城市扩张监测、农业用地识别等空间分析需求。数据包含10米分辨率栅格与省级行政边界矢量两类核心地理信息&#xff1a;2个.tif主文件&#xff0…

作者头像 李华
网站建设 2026/9/10 11:12:25

家政返物业费小程序开发,服务缴费联动开发

家政返物业费小程序开发&#xff0c;服务缴费联动开发家政返物业费小程序的核心价值&#xff0c;在于实现家政服务消费与物业缴费场景的深度联动&#xff0c;打破家政交易、额度返还、物业费抵扣、账单核销的业务壁垒。传统社区家政小程序只独立支撑上门服务下单与支付功能&…

作者头像 李华