资讯动态

CopilotKit × LangGraph TypeScript:双向共享状态(Read + Write)的完整实现与 QA 验证指南

发布时间:2026/9/13 16:13:04 来源:尧图企业网站定制
CopilotKit × LangGraph TypeScript双向共享状态Read Write的完整实现与 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 仓库中 LangGraph (TypeScript) 集成演示的 QA 文档 shared-state-read-write.md 展开讲解“UI 与 Agent 双向共享状态”这一核心模式UI 通过agent.setState()写入用户偏好、Agent 每轮从状态中读取并注入 system promptAgent 通过set_notes工具写回状态、UI 通过useAgent()实时读取渲染。读完后你将掌握该模式的前后端完整实现链路、全部可执行测试步骤、预期结果基线以及从源码结构确认的关键设计细节如Command({ update })双通道写回、空偏好跳过注入的容错逻辑。前置条件QA 文档明确列出了运行该演示所需的环境前提演示已部署并可在 dashboard 主机上通过/demos/shared-state-read-write访问Agent 后端健康/api/health可访问Railway 上已设置OPENAI_API_KEYLANGGRAPH_DEPLOYMENT_URL指向一个暴露了shared_state_read_writegraph 的 LangGraph 部署。前端入口在 page.tsx 中可见CopilotKit组件通过runtimeUrl/api/copilotkit与agentshared-state-read-write两个属性完成与运行时和后端的绑定export default function SharedStateReadWriteDemo() { return ( CopilotKit runtimeUrl/api/copilotkit agentshared-state-read-write DemoContent / /CopilotKit ); }状态模型两个切片四个方向的数据流整个演示的共享状态只有一个对象包含两个语义完全不同的切片。前端在 page.tsx 中定义了它的形状// Shape of the bidirectional shared state. // - preferences is WRITTEN by the UI via agent.setState(). // - notes is WRITTEN by the agent via its set_notes tool and READ // by the UI via useAgent(). interface RWAgentState { preferences: Preferences; notes: string[]; }对应的Preferences结构在 preferences-card.tsx 中定义export interface Preferences { name: string; tone: formal | casual | playful; language: string; interests: string[]; }数据流可以概括为四条链路UI → Agent写侧边栏表单每次编辑都经agent.setState({ preferences, notes })写入 Agent 状态Agent ← 读后端 chat node 每轮从state.preferences读出偏好并注入 system promptAgent → UI写Agent 调用set_notes工具更新state.notesUI ← 读 回写UI 用useAgent({ updates: [OnStateChanged] })订阅状态变更实时渲染笔记卡片Clear 按钮则以agent.setState({ notes: [] })反向写回同一个切片。测试步骤 1基础功能验证QA 文档的第一组检查项聚焦页面骨架的完整性全部可通过 DOM 断言data-testid完成访问/demos/shared-state-read-write页面应在 3 秒内渲染出左侧边栏preferences notes 两张卡片与右侧CopilotChat面板验证data-testidpreferences-card可见标题为 Your preferences——与 preferences-card.tsx 中Card>useConfigureSuggestions({ suggestions: [ { title: Greet me, message: Say hi and introduce yourself. }, { title: Remember something, message: Remember that I prefer morning meetings and that I dont eat dairy., }, { title: Plan a weekend, message: Suggest a weekend plan based on my interests., }, ], available: always, });发送 Hello10 秒内应出现助手文本回复确认前端到 LangGraph 后端的完整链路可用。测试步骤 2AUI 写入 → Agent 读取preferences这是“UI 写、Agent 读”方向的完整验证。逐步操作与预期在data-testidpref-name输入 Ataidata-testidpref-state-json应同步更新为包含name: Atai将data-testidpref-tone改为formalJSON 预览应反映tone: formal将data-testidpref-language改为SpanishJSON 预览应反映language: Spanish点击Cooking和Travel兴趣徽章两者应呈现选中样式边框#BEC2FF、背景#BEC2FF1AJSON 预览的interests数组应同时包含两项发送 What do you know about me?10 秒内助手回复应引用 Atai、formal 语气、Spanish 语言以及 Cooking/Travel 兴趣——因为 chat node 每轮都会把偏好注入 system prompt点击 Plan a weekend 建议回复应贴合已选兴趣。源码层面这组断言的“可解释性”来自两个组件的解耦设计。PreferencesCard是一个纯受控表单本身完全不感知 Agent——每个字段的变更都通过set局部函数走onChange冒泡export function PreferencesCard({ value, onChange }: PreferencesCardProps) { const set K extends keyof Preferences(key: K, v: Preferences[K]) onChange({ ...value, [key]: v }); // ... name / tone / language / interests 各控件 }真正的状态写入发生在上一层 page.tsx// WRITE: every edit in the sidebar goes straight into agent state. const handlePreferencesChange (next: Preferences) { agent.setState({ preferences: next, notes, // preserve what the agent has written } as RWAgentState); };注意这里刻意携带了notes字段——UI 写偏好时必须保留Agent 已写入的笔记否则会覆盖掉set_notes的产出。卡片底部那个pref-state-json的pre直接JSON.stringify(value, null, 2)因此 QA 中“每次编辑后 JSON 预览同步更新”的断言是确定性的同步渲染结果。兴趣徽章的选中态与 QA 提到的#BEC2FF/#BEC2FF1A颜色来自 preferences-card.tsx 中按selected ? selected : outline切换的Badge变体。测试步骤 2BAgent 写入 → UI 读取notes这是反向链路的验证点击 Remember something 建议实际发送 Remember that I prefer morning meetings and that I dont eat dairy.15 秒内data-testidnotes-list应出现在 notes 卡片中且至少包含 2 条data-testidnote-item分别提及 morning meetings 和 dairydata-testidnotes-empty空态应不再渲染发送 Also remember I live in Berlin.15 秒内笔记列表应增长旧笔记保留、新笔记追加——这一步验证了 Agent 每次调用set_notes都传入完整更新后的列表而非增量。这条“传全量、不传 diff”的契约由后端工具的描述文字直接固化。在 shared-state-read-write.ts 中const setNotes tool( async ({ notes }, config: ToolRunnableConfig) { // ... 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. Use this tool whenever the user asks you to remember something, or when you have an observation about the user worth surfacing in the UIs notes panel. Always pass the FULL notes list (existing notes any new ones), not a diff. Keep each note short ( 120 chars)., schema: z.object({ notes: z .array(z.string()) .describe(The full updated notes list (replaces previous value).), }), }, );从源码结构看这里有两个关键设计点Command({ update })一石二鸟同一次工具返回里既更新了notes通道UI 侧通过共享状态立即重渲染又追加了一条携带tool_call_id的ToolMessage让 LLM 在下一轮看到格式合法的 tool result。工具内还显式校验config.toolCall?.id非空否则拒绝生成空tool_call_id的ToolMessageOpenAI 会拒绝这类消息说明该实现是面向真实 LLM 提供商约束做了防御性处理渲染侧保持纯读notes-card.tsx 只接收notesprop 并渲染编号列表自身不触碰 Agent 状态。UI 之所以能“实时”看到 Agent 的写入是因为父组件订阅了状态变更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 ?? [];UseAgentUpdate.OnStateChanged使 Agent 侧的任意状态变更包括set_notes触发的notes通道更新都会触发组件重渲染这正是 QA 断言“15 秒内笔记出现”的底层机制。测试步骤 2CUI 回写 Agent 拥有的切片清空笔记同一个notes字段既被 Agent 写、也被 UI 写QA 文档用三步验证这个双向回环有笔记存在时data-testidnotes-clear-button应可见源码中该按钮仅在notes.length 0时渲染与断言一致点击 Clear笔记列表消失data-testidnotes-empty重新渲染再问 What do you remember about me?Agent 不应再引用被清空的笔记——因为 UI 已通过agent.setState({ notes: [] })把状态写回。对应源码// WRITE: let the user clear the agent-authored notes from the UI. const handleClearNotes () { agent.setState({ preferences, notes: [] } as RWAgentState); };这一步同时演示了双向共享状态的一个语义要点UI 的清空是直接改状态而不是发消息让 Agent 去删。下一轮对话时 Agent 从状态里读到的就是空数组自然“忘记”了那些笔记。测试步骤 2D多轮状态持久性将 tone 改为playful并添加Music兴趣发送 Write me a one-line haiku greeting.回复应是俏皮风格并提及音乐追加发送 Do it again in French.回复应保持 playful、切换为法语、且继续体现音乐兴趣——确认偏好在多轮对话中无需重发即持续生效因为偏好存在 Agent 状态里而非聊天记录里刷新页面后偏好应重置为默认值tone: casual、language: English、空 interests、空 name笔记也重置为空。刷新即重置的行为由 page.tsx 中的初始化逻辑解释const INITIAL_PREFERENCES: Preferences { name: , tone: casual, language: English, interests: [], }; // Seed initial preferences empty notes into agent state once, so the // agent has something to read on the very first turn. useEffect(() { if (!agentState?.preferences) { agent.setState({ preferences: INITIAL_PREFERENCES, notes: [], } as RWAgentState); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []);即状态是按会话per-session由页面useEffect播种的QA 清单中“reload 后重置”的预期结果由此得出。后端图结构chat node、tool node 与偏好注入前端之外Agent 侧的完整实现在 shared-state-read-write.ts。状态声明把 CopilotKit 自带的通道与业务通道合并到同一个Annotationexport interface Preferences { name?: string; tone?: formal | casual | playful; language?: string; interests?: string[]; } const AgentStateAnnotation Annotation.Root({ ...CopilotKitStateAnnotation.spec, // messages copilotkitactions 等 preferences: AnnotationPreferences | undefined, notes: Annotationstring[], });“UI 写入如何影响模型”的关键在buildPreferencesMessagechatNode每轮调用前从state.preferences构造一条SystemMessage拼在消息序列最前面function buildPreferencesMessage(prefs: Preferences | undefined) { 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({ /* The user has shared these preferences... */ }); } async function chatNode(state: AgentState, config: RunnableConfig) { const model makeChatOpenAI(config, { temperature: 0, model: gpt-4o-mini, modelKwargs: { parallel_tool_calls: false }, }); const modelWithTools model.bindTools!([ ...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []), ...tools, ]); const prefsMessage buildPreferencesMessage(state.preferences); const systemMessages prefsMessage ? [baseSystem, prefsMessage] : [baseSystem]; const response await modelWithTools.invoke( [...systemMessages, ...state.messages], config, ); return { messages: response }; }这段实现直接解释了 QA 中的两条预期QA 断言“chat node 每轮把偏好注入 system prompt”因此 What do you know about me? 能答出全部偏好同时也解释了 2E 的容错项——当preferences为空对象所有字段 falsy时buildPreferencesMessage返回nullchat node 跳过注入而不报错对应 QA 中“清空偏好后问 Who am I?Agent 应正常作答不崩溃”。图编译部分则是标准的 LangGraph 结构chat_node与ToolNode成环MemorySaver提供 checkpointconst 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); export const graph workflow.compile({ checkpointer: new MemorySaver(), });路由函数shouldContinue有一个值得注意的 CopilotKit 专属细节模型可能同时产生后端工具调用如set_notes和CopilotKit 前端 action 调用。只有当存在“不属于state.copilotkit.actions的任何工具调用”时才进入tool_node否则直接__end__让前端 action 走 CopilotKit 运行时通道function shouldContinue({ messages, copilotkit }: AgentState) { const lastMessage messages[messages.length - 1] as AIMessage; if (lastMessage.tool_calls?.length) { const actions copilotkit?.actions; const hasBackendToolCall lastMessage.tool_calls.some((toolCall) !actions || actions.every((action) action.name ! toolCall.name) ); if (hasBackendToolCall) return tool_node; } return __end__; }测试步骤 3错误处理与边界情况QA 文档列出的三条边界检查及源码依据空消息发送应为 no-op不产生用户气泡、不产生助手回复清空全部偏好与姓名后问 Who am I?Agent 正常作答不崩溃——源码依据即上文buildPreferencesMessage的lines.length 0 → return null分支全程 DevTools Console 无未捕获错误这是跨所有流程的全局不变量。预期结果基线汇总 QA 文档给出的验收基线可用于回归时快速对照页面 3 秒内完成加载助手文本回复在 10 秒内出现偏好写入在每次变更时同步反映到pref-state-json预览Agent 写入的笔记在 remember 类提示后 15 秒内出现在notes-card且后续每次set_notes调用都保留此前的完整列表Clear 按钮完成 UI → Agent 状态的回环下一轮对话中 Agent 不再拥有被清空的笔记全程无 UI 布局破坏、无未捕获的控制台错误。小结与延伸阅读这个演示是 LangGraph TypeScript CopilotKit 双向共享状态的最小完整范式一个共享状态对象、两个方向相反的读写切片、四个数据流且每一侧的代码都保持单一职责卡片组件不碰 Agent状态接线集中在 page 层。相关可追溯的仓库文件QA 清单本文主体shared-state-read-write.md演示说明README.md前端页面与状态接线page.tsx、preferences-card.tsx、notes-card.tsx、suggestions.ts后端 Agent 图shared-state-read-write.ts需要说明的适用前提以上路径均位于showcase/integrations/langgraph-typescript集成演示包内运行依赖已部署的 CopilotKit runtime/api/copilotkit与暴露shared_state_read_write图的 LangGraph 部署QA 清单中个别文案notes 空态占位文本与当前仓库代码存在版本差异执行自动化断言前建议以检出时的源码为准校准。【免费下载链接】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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价