ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CopilotKit × LangGraph TypeScript:用共享状态实现 UI 与 Agent 双向读写的实战解析

CopilotKit × LangGraph TypeScript:用共享状态实现 UI 与 Agent 双向读写的实战解析 CopilotKit × LangGraph TypeScript用共享状态实现 UI 与 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本文以 CopilotKit 仓库中showcase/integrations/langgraph-typescript下的 Shared State (Read Write) 示例为核心完整拆解 UI 与 Agent 之间的双向共享状态模式前端如何把用户偏好写进 Agent 状态并影响模型回复Agent 又如何通过set_notes工具把“记忆”写回 UI 并实时触发重渲染。读完本文你将掌握useAgentagent.setState LangGraphCommand这套端到端双向状态同步的完整链路并能独立复刻类似的“表单驱动 Agent 行为”应用。示例定位同一个状态对象两边都能读和写该示例README演示的是UI 与 Agent 之间的双向共享状态——两侧都读、都写同一个状态对象具体包含三条主线UI → Agent写页面侧栏表单name / tone / language / interests通过agent.setState(...)把数据写入state.preferences后端每一轮对话都会读取它并注入系统提示词system prompt。Agent → UI写Agent 的set_notes工具把内容写入state.notes侧栏的笔记卡片在 Agent 每次更新后自动重新渲染。往返闭环Round-trip在侧栏修改偏好后Agent 的下一条回复会肉眼可见地随之改变——语气tone、语言language、用名字称呼用户。如何与示例交互先编辑侧栏偏好然后依次尝试以下提示词它们也对应页面里由 suggestions.ts 注入的三枚建议按钮Say hi and introduce yourself.Remember that I prefer morning meetings and that I dont eat dairy.Suggest a weekend plan based on my interests.观察要点Agent 的回复会随偏好变化而改变当你让它“记住”某些事时侧栏笔记卡会实时出现新条目。前端状态形状、订阅与写入状态形状preferences由 UI 写notes由 Agent 写入口页面 page.tsx 定义了双向状态的整体形状// 双向共享状态的形状 // - preferences 由 UI 通过 agent.setState() 写入 // - notes 由 Agent 通过其 set_notes 工具写入、 // 由 UI 通过 useAgent() 读取 interface RWAgentState { preferences: Preferences; notes: string[]; }其中Preferences在 preferences-card.tsx 中定义字段为export interface Preferences { name: string; tone: formal | casual | playful; language: string; interests: string[]; }页面通过CopilotKit runtimeUrl/api/copilotkit agentshared-state-read-write把运行时地址和 Agent ID 绑定到整个页面。读侧useAgent订阅状态变更const { agent } useAgent({ agentId: shared-state-read-write, updates: [UseAgentUpdate.OnStateChanged], }); const agentState agent.state as RWAgentState | undefined; const preferences agentState?.preferences ?? INITIAL_PREFERENCES; const notes agentState?.notes ?? [];updates: [UseAgentUpdate.OnStateChanged]让组件订阅 Agent 的每一次状态变更只要 Agent 侧例如set_notes工具修改了state.notes该 hook 就会触发重渲染侧栏笔记卡随之刷新——这就是 Agent → UI 方向的“读”的实现。另外页面用useEffect在首次挂载时做了一次状态播种seed若agentState.preferences尚不存在则调用agent.setState({ preferences: INITIAL_PREFERENCES, notes: [] })保证 Agent 在第一轮就有可读取的偏好tone: casual、language: English、空的 name 与 interests。写侧所有编辑都流经agent.setState侧栏表单的每一次变更都由 demo-layout.tsx 中的PreferencesCard受控表单上抛onChange最终在页面层收敛为同一个调用const handlePreferencesChange (next: Preferences) { agent.setState({ preferences: next, notes, // 保留 Agent 已写入的笔记 } as RWAgentState); }; // UI 反向清空 Agent 写的笔记 const handleClearNotes () { agent.setState({ preferences, notes: [] } as RWAgentState); };两个关键细节值得注意setState是全量替换语义写入preferences时必须把当前的notes一并带上否则会覆盖 Agent 之前写入的笔记——这也是handlePreferencesChange里显式传notes的原因。同一字段双向可写notes既由 Agent 的set_notes工具写入也能被 UI 的 “Clear” 按钮通过agent.setState({ notes: [] })清空。QA 清单qa/shared-state-read-write.md中专门验证了这一点清空后再问 What do you remember about me?Agent 不应再引用被清掉的笔记因为状态已被 UI 回写。组件分层表单组件不感知 Agent从源码结构看PreferencesCard和NotesCardnotes-card.tsx都是“纯组件”前者只接收value/onChange后者只接收notes/onClear自身从不触碰 Agent 状态所有与 Agent 的接线都上移一层到页面组件中。这种分层让卡片可以被独立测试和复用。此外PreferencesCard底部用pre实时打印当前 preferences 的 JSONdata-testidpref-state-json让“UI 到底写了什么进状态”变得可见、可断言——QA 脚本正是依赖这一预览做校验的。后端LangGraph 图中的注入、工具与路由状态注解CopilotKitStateAnnotation之上扩展业务槽位Agent 实现位于 shared-state-read-write.ts。共享状态通过 LangGraph 的Annotation.Root声明并在 CopilotKit 的基础槽位之上叠加两个业务字段const AgentStateAnnotation Annotation.Root({ ...CopilotKitStateAnnotation.spec, // messages / copilotkit 等基础槽位 preferences: AnnotationPreferences | undefined, notes: Annotationstring[], });CopilotKitStateAnnotation来自copilotkit/sdk-js/langgraph它带来了消息通道与copilotkit动作槽位preferences与notes则是本示例自定义的双向共享通道。UI 写入如何被模型“看见”偏好注入README 中提到后端有PreferencesInjectorMiddleware.wrap_model_call该写法对应 Python 版 shared_state_read_write 的中间件实现。在 TypeScript 版中等价逻辑落在 chat node 内每一轮都从状态里读出最新preferences构造一条SystemMessage前置到消息列表function buildPreferencesMessage(prefs: Preferences | undefined): SystemMessage | null { if (!prefs) return null; const lines: string[] []; if (prefs.name) lines.push(- Name: ${prefs.name}); if (prefs.tone) lines.push(- Preferred tone: ${prefs.tone}); if (prefs.language) lines.push(- Preferred language: ${prefs.language}); if (prefs.interests prefs.interests.length 0) { lines.push(- Interests: ${prefs.interests.join(, )}); } if (lines.length 0) return null; // 空偏好时跳过注入 return new SystemMessage({ content: [ The user has shared these preferences with you:, ...lines, Tailor every response to these preferences. Address the user by name when appropriate., ].join(\n), }); }chatNode中BASE_SYSTEM_PROMPT要求模型尊重偏好、在用户要求“记住”时调用set_notes与偏好消息一起前置const systemMessages prefsMessage ? [baseSystem, prefsMessage] : [baseSystem]; const response await modelWithTools.invoke([...systemMessages, ...state.messages], config);模型侧参数为temperature: 0、gpt-4o-mini并关闭parallel_tool_calls见makeChatOpenAI调用处。由于注入发生在每一轮UI 的写入无需重发即可持续影响后续回复——QA 清单中“把 tone 改为 playful 后连续两轮追问”的测试正是验证这种跨轮持久性。Agent 写入set_notes工具用Command双路回写Agent → 共享状态的写路径由set_notes工具承担核心是返回一个Command同时完成两件事向notes通道写入新值UI 会因此重渲染以及产出一条ToolMessage让 LLM 在下一轮看到格式合法的工具结果const setNotes tool( async ({ notes }, config: ToolRunnableConfig) { const toolCallId config.toolCall?.id; if (typeof toolCallId ! string || toolCallId.length 0) { throw new Error(set_notes: missing tool_call_id — ...); } return new Command({ update: { notes, messages: [ new ToolMessage({ status: success, name: set_notes, tool_call_id: toolCallId, content: Notes updated., }), ], }, }); }, { name: set_notes, description: Replace the notes array in shared state with the full updated list. ... Always pass the FULL notes list (existing notes any new ones), not a diff., schema: z.object({ notes: z.array(z.string()).describe(The full updated notes list (replaces previous value).), }), }, );这里有三个工程要点全量替换契约工具描述明确要求传“完整的新列表现有 新增”而不是增量且每条笔记建议小于 120 字符。QA 流程验证了这一点先记住两条再补一条 Also remember I live in Berlin.笔记列表应只增不丢。tool_call_id校验若工具脱离ToolNode上下文被调用拿不到 tool call id直接抛错拒绝发射空tool_call_id的ToolMessage因为 OpenAI 会拒绝这类消息——这是对 LLM API 格式约束的显式防御。Command.update一次写两个通道notes共享状态与messages对话历史保证 UI 视图与模型上下文一致。路由与图编译区分前端动作与后端工具shouldContinue负责条件路由若最后一条 AIMessage 携带的tool_calls中有不属于CopilotKit 前端动作state.copilotkit.actions的调用则路由到tool_node否则结束本轮const hasBackendToolCall lastMessage.tool_calls.some((toolCall) !actions || actions.every((action) action.name ! toolCall.name) );即前端动作交由 CopilotKit 协议处理后端工具set_notes留在图内执行。图结构为经典的 chat ↔ tool 循环const workflow new StateGraph(AgentStateAnnotation) .addNode(chat_node, chatNode) .addNode(tool_node, new ToolNode(tools)) .addEdge(START, chat_node) .addEdge(tool_node, chat_node) .addConditionalEdges(chat_node, shouldContinue as any); const graph workflow.compile({ checkpointer: new MemorySaver() });MemorySavercheckpointer 提供会话内状态持久化这正是notes跨轮保留的前提。端到端闭环与注册链路把两侧串起来一条 “Remember that I prefer morning meetings... 消息的完整链路是UI 侧栏此前已通过agent.setState({ preferences, notes })把偏好写入共享状态消息进入shared-state-read-write图chatNode从state.preferences构建偏好SystemMessage前置后调用模型模型决定调用set_notes并给出完整笔记列表路由到tool_node执行Command.update同时写入notes通道与messages通道状态变更经 CopilotKit 运行时推回前端useAgent({ updates: [OnStateChanged] })触发重渲染笔记卡出现新条目。该图在 langgraph.json 中注册为shared_state_read_write: ./shared-state-read-write.ts:graph前端 Agent 名到图名的映射shared-state-read-write - shared_state_read_write则在 route.ts 中完成两者命名不同前端用短横线、后端用下划线但必须一一对应否则 Agent 无法命中图。运行前提与验证方式运行时依赖LangGraph 部署需暴露shared_state_read_write图部署配置见 langgraph.jsonNode 20环境变量取自项目.env需配置OPENAI_API_KEY页面入口为/demos/shared-state-read-write依赖/api/copilotkit代理与健康的 Agent 后端/api/health完整的行为验证清单见 qa/shared-state-read-write.md它按“UI 写 → Agent 读”“Agent 写 → UI 读”“UI 回写 Agent 槽位”“多轮持久化”“错误处理”五个维度覆盖了本示例的全部关键路径含 testid 断言与响应时限可作为复现该模式时的验收模板。小结本示例展示了双向共享状态的最小完备形态Annotation声明共享槽位、agent.setState承担 UI 写入注意全量替换语义、Command(update)承担 Agent 侧“写状态 回消息”的双路更新、useAgent({ updates: [OnStateChanged] })承担读侧订阅、每轮注入系统消息让 UI 的写入持续影响模型。理解这条链路后可以将其推广到任何需要“表单/画布驱动 Agent且 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),仅供参考
返回列表