news 2026/9/16 2:24:47

Electric Agents 实体协作模式:用 blackboard 共享状态构建多 Agent 协作系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Electric Agents 实体协作模式:用 blackboard 共享状态构建多 Agent 协作系统

Electric Agents 实体协作模式:用 blackboard 共享状态构建多 Agent 协作系统

【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric

多个 Agent(实体)如何围绕同一份数据集协同工作,而不是各自闭门造车?在 Electric Agents 的实体设计体系中,blackboard(黑板 / 共享状态)模式给出了标准答案:把读写操作追加到一条**所有参与者都连接的持久流(durable stream)**上,由父实体创建共享状态,子实体连接进来,借助自动生成的 CRUD 工具完成协作。本指南以 blackboard 模式文档 为骨架,结合 agents-runtime 运行时源码 与 测试用例 的验证,完整讲解共享状态的 schema 设计、自定义 worker 注册、父/子实体 handler 写法,以及模式专属的审查清单与反模式。读完你将能够独立实现辩论、Wiki 协作、同行评审、交易大厅等"多实体共写一份数据"的实体系统。

blackboard 模式是什么:多个实体通过共享数据结构协调

blackboard(shared state)模式的核心定义是:

多个实体通过一个共享的数据结构进行协调——所有的读取和写入都被追加到一条持久的流(durable stream)上,每个参与者都连接到这条流。父实体负责创建共享状态;子实体连接进来,并使用自动生成的 CRUD 工具进行协作。

这与我们熟悉的"黑板架构"一致:黑板本身是一块共享的、可被所有人读写的存储区域,专家(这里的子实体)在上面不断添加自己的贡献,父实体(或某个阅读者实体)负责汇总、审查与综合。在 Electric Agents 的实现里,这块"黑板"不是内存对象,而是持久化的事件流——写入的每一行都是流上的一条事件,任何参与者都可以回放、连接并响应。

在 designing-entities 技能 归纳的七种协调模式(single-agent、manager-worker、pipeline、map-reduce、dispatcher、blackboard、reactive-observers)中,blackboard 是一种可以叠加在其他模式之上的协作模式:manager-workermap-reduce负责定义"如何派生子实体",而 blackboard 定义的是"子实体之间通过哪条数据通道共享成果"。因此它特别适合贡献是"加法式、累积式"的工作负载。

为什么共享状态工作流必须使用自定义 worker 类型

Electric Agents 服务器内置的worker(即 SKILL.md 中描述的 built-in agent 类型)是一个最小权限沙箱:它只接受{ systemPrompt, tools }两个 spawn 参数,不会收到ctx.electricTools,也不认识sharedState。也就是说,直接 spawn 内置"worker"并把sharedState塞进参数里是无效的——该参数会被静默忽略。

因此,blackboard 工作流有两条实现路径,二选一:

  1. 在应用中注册自定义 worker 实体类型:该类型在ctx.args中接收sharedState,调用ctx.observe(db(args.sharedState.id, args.sharedState.schema)),并向自己的 Agent 暴露共享集合的 CRUD 工具。playground 的 worker 是一个现成模板——把它的写法复制到你的应用中,用唯一的类型名(例如"blackboard-worker")注册即可。文档中标注的权威示例位于examples/durable-agents-playground/src/workers/worker.ts(配套示例debate.tswiki.tspeer-review.tstrading-floor.ts位于examples/durable-agents-playground/src/blackboard/,该 playground 目录属于文档体系中的 canonical material,当前仓库树中未包含),它接受sharedState/sharedStateToolMode/builtinTools三个 spawn 参数。
  2. 改用ctx.send+ctx.observe协调:父实体写入自己的状态,子实体观察父实体,子实体再把结果通过ctx.send发回。这种方案的带宽更低,但放弃了共享集合 CRUD 工具的便利性。

下文所有示例均假设你选择了方案 1——即已在应用中注册了自定义的blackboard-worker类型。

适用场景:何时该选择 blackboard 模式

当开发者的描述符合以下特征时,blackboard 模式就是正确的选择(触发词表完整定义见 pattern-triggers.md):

  • 多个 worker 需要读取并向同一份数据集贡献内容(研究结论、辩论论点、订单、Wiki 章节);
  • 贡献是加法式或累积式的——每个 worker 往里加东西,而不覆盖别人的成果;
  • 父实体(或某个阅读者实体)负责从共享状态中聚合 / 审查 / 综合最终结果;
  • 可以叠加在manager-workermap-reduce之上——前者定义派生关系,后者定义共享数据通道。

在 pattern-triggers.md 的触发词表 中,命中 "shared knowledge base"、"collaborative writing"、"debate"、"wiki"、"shared state"、"collective intelligence"、"workers updating the same board" 等短语的描述,都指向 blackboard。当"manager-worker + blackboard"叠加出现时(例如"多个 worker 研究主题并把结论写到共享板上,manager 读取后产出最终报告"),需要进一步澄清一个关键问题:父实体是等所有 worker 完成后才综合,还是在结论落到黑板上时实时响应?前者用wake: { on: "runFinished" }等待完成,后者则需要在观察共享状态时配置wake: { on: "change" }

模式所需状态:父状态与共享 schema 的定义

blackboard 模式涉及两类状态定义:父实体自己的状态,以及由父实体创建、父子共享的 schema。

父实体状态(用于记录当前阶段的机器状态):

state: { status: { schema: z.object({ key: z.literal("current"), value: z.enum(["idle", "active", "done"]), }), primaryKey: "key", }, // 如果要动态派生 worker,还需要 children 追踪 }

共享 schema(必须从一个共享模块导出,父实体和子实体都从这个模块导入同一个 schema 对象):

// shared-schema.ts import { z } from 'zod/v4' export const debateSchema = { arguments: { schema: z.object({ key: z.string(), side: z.enum(['pro', 'con']), round: z.number(), text: z.string(), }), type: 'shared:argument', primaryKey: 'key', }, } as const

这里有几个值得注意的约定:

  • type: 'shared:argument':共享集合的type遵循"shared:<name>"命名约定。这与 源码中的SharedStateCollectionSchema类型 一致——每个共享集合有一个schema(Zod 或其他 Standard Schema 校验器)、一个type(持久流中使用的事件类型字符串)和一个primaryKey(行的字符串主键字段)。shared:前缀用于在持久流中区分共享状态事件与实体本地状态事件。
  • zod/v4:Electric Agents 运行时使用 Zod v4 的校验对象(StandardSchemaV1兼容)。后文的反模式中会强调:不要把 schema 序列化成 JSON 字面量传过去——必须传递导入的对象本身。
  • schema 必须唯一SharedStateSchemaMap本质上是"集合名 → 集合 schema 定义"的映射(见 types.ts 的注释示例)。父子两边若各自声明一份看起来一样的 schema 对象,运行时并不会把它们当成同一个 schema,会引发静默写入失败或 CRUD 工具形状漂移。

父实体 handler 骨架:创建共享状态并派生 worker

父实体的 handler 完整骨架如下(节选自 blackboard 模式文档):

import { debateSchema } from "./shared-schema" import { db } from '@electric-ax/agents-runtime' async handler(ctx, wake) { const sharedId = `debate-${ctx.entityUrl}` if (ctx.firstWake) { ctx.mkdb(sharedId, debateSchema) ctx.db.actions.status_insert({ row: { key: "current", value: "idle" } }) } const shared = await ctx.observe(db(sharedId, debateSchema), { wake: { on: "change", debounceMs: 500 }, // optional — 如果父实体需要响应更新 }) const startDebateTool: AgentTool = { execute: async (_id, { topic }) => { const pro = await ctx.spawn( "blackboard-worker", // 应用中注册的自定义类型 — 不是内置的 "worker" "pro", { systemPrompt: "Argue FOR the proposition...", sharedState: { id: sharedId, schema: debateSchema }, }, { initialMessage: `Topic: ${topic}`, wake: { on: "runFinished", includeResponse: true } } ) // ... 类似地派生 con ... return { content: [{ type: "text", text: "Debate started." }], details: {} } }, // ... } // 后续需要读取共享状态: // const args = shared.arguments.toArray ctx.useAgent({ /* ... */ }) await ctx.agent.run() }

逐段解读这段骨架:

  • sharedId = \debate-${ctx.entityUrl}``:共享状态的 ID 通常由实体 URL 派生,保证每个父实体实例拥有独立、可预测的共享流。
  • if (ctx.firstWake) { ctx.mkdb(...) }mkdb只能在首次唤醒时调用。它负责初始化共享状态的底层流;再次调用会报错。这在源码中有直接佐证——setup-context.ts 的buildSharedStateHandle中,当以create模式调用而该 ID 已存在句柄时,会抛出[agent-runtime] shared DB "<id>" already exists — use observe(db("<id>", schema)) to get a handle的错误。
  • ctx.observe(db(sharedId, debateSchema), { wake: { on: "change", debounceMs: 500 } }):每次唤醒都要调用(句柄不会跨唤醒持久)。配置wake: { on: "change" }后,共享状态发生变化会再次唤醒父实体;debounceMs用于合并高频更新。
  • spawn 自定义类型:子实体必须是应用注册的自定义 worker 类型(如"blackboard-worker"),spawn 参数中携带sharedState: { id, schema }。内置的worker不支持这些参数。

子 worker handler 骨架:连接共享状态并贡献数据

子 worker 的 handler 完整骨架如下:

import { debateSchema } from "./shared-schema" // 同一个 schema 对象 import { db } from '@electric-ax/agents-runtime' async handler(ctx) { const args = ctx.args as { sharedState: { id: string; schema: typeof debateSchema } } const shared = await ctx.observe(db(args.sharedState.id, args.sharedState.schema)) // Worker 通过 shared.<collection>.insert / .update 写入 // 当 sharedStateToolMode: "full" 时,自动生成的 CRUD 工具 // (write_arguments、read_arguments 等)也会暴露给它的 LLM ctx.useAgent({ systemPrompt: args.systemPrompt, model: "claude-sonnet-4-5-20250929", tools: [...ctx.electricTools], }) await ctx.agent.run() }

关键点:

  • ctx.args{ id, schema }:按惯例放在args.sharedState.idargs.sharedState.schema。这是父子之间唯一的"握手协议"——父实体 spawn 时传入,子实体在 handler 中解包。
  • ctx.observe(db(...))每次唤醒都调用:子 worker 不调用mkdb(共享状态已由父实体创建),只以connect模式观察。
  • 写入方式:代码里可以直接shared.arguments.insert(...);当 spawn 参数设置sharedStateToolMode: "full"时,运行时还会把每个共享集合的 CRUD 工具(write_argumentsread_arguments等)注入到tools,交给 LLM 自主调用。这正是"自动生成的 CRUD 工具"一词的含义。
  • ctx.electricTools展开到 tools:这与 设计实体的通用规范 一致——注意内置worker拿不到electricTools,而自定义 worker 可以。

运行时原理:mkdb 与 observe 的底层实现

blackboard 模式的两个核心 API——ctx.mkdbctx.observe(db(...))——在 setup-context.ts 中有完整实现,理解它们有助于把握模式的不变量。

ctx.mkdb(id, schema)是"创建模式"(create)下的buildSharedStateHandle。它把共享状态注册进唤醒会话的 manifest(kind: "shared-state",key 形如shared-state:<id>),并从 schema 提取每个集合的typeprimaryKey(setup-context.ts#L395-L409)。如果同一 ID 已存在句柄而以create模式再次调用,会直接抛错——这正是"mkdb只能firstWake调用一次"的运行时保证。

ctx.observe(db(...))则是"连接模式"(connect)。观察到的实体/共享状态句柄会被缓存(observeHandleCache),并对已观察的实体注册唤醒(setup-context.ts#L1043-L1056 附近的 spawn 逻辑中,observe: false才能关闭对派生实体的自动观察)。此外,延迟 wiring 的观察句柄会在preloadDeferredObservedHandles中统一preload()(setup-context.ts#L1313-L1319),保证读取前数据已就绪。

写操作的语义:共享集合的代理类型StateCollectionProxy(types.ts#L481-L491)定义了insert/update/delete(返回EntityTransaction,可 fire-and-forget 或await tx.isPersisted.promise等待持久化)以及get/toArray(读取委托给底层 TanStack DB 集合)。

测试用例的完整验证:agents-runtime 的 runtime-dsl.test.ts 用 D1–D7 一组用例系统验证了共享状态语义,对应快照在 runtime-dsl.test.ts.snap 中:

用例验证点源码位置
D1mkdb产生带 manifest 条目(shared-state:<id>)的实体历史runtime-dsl.test.ts#L1942-L1949
D2即使没有任何写入,共享状态流也已经存在runtime-dsl.test.ts#L1951-L1959
D3对共享状态的写入同时反映在实体历史与共享状态历史中runtime-dsl.test.ts#L1960-L1977
D4第二个实体可以连接既有共享状态并读取之前写入的行runtime-dsl.test.ts#L1979-L1993
D5共享状态的 update / delete 事件跨唤醒保持持久runtime-dsl.test.ts#L1995-L1999
D6多集合共享状态在 writer 与 reader 实体之间保持一致runtime-dsl.test.ts#L2011 附近
D7多个实体可以向同一共享集合贡献持久行runtime-dsl.test.ts#L2053 附近

这些测试从行为层面证实了 blackboard 模式的三个关键承诺:共享流先于写入存在(D2)、写入跨实体可见且持久(D3–D5)、天然支持多写入者(D7)。

不变量:blackboard 模式的正确姿势

把上面的分析收敛为五条必须遵守的不变量:

  • mkdb只在ctx.firstWake中调用:它初始化底层流;再次调用是 no-op 或直接报错(如setup-context.ts中"already exists"错误所示)。
  • observe(db(...))每次唤醒都调用(父实体和子实体都是如此):观察句柄不跨唤醒持久,必须在每次 handler 运行中重新获取。
  • 父实体和子实体使用同一个 schema 对象:从共享模块导入,禁止重复声明。schema 不一致会导致静默写入失败或 CRUD 工具形状漂移。
  • 子实体通过ctx.args接收{ id, schema }:按惯例放在args.sharedState.idargs.sharedState.schema
  • 写入是最终一致的:不要假设一次写入能立即被其他实体看到;需要响应更新时,使用wake: { on: "change" }

模式专属审查清单

designing-entities 技能 的第 4 阶段要求机械式套用通用审查清单 + 模式专属清单。blackboard 模式文档 给出了 BB1–BB7 七条模式专属规则(也可对照 review-checklist.md 中的 W2 条——spawn 参数中不得出现sharedState/sharedStateToolMode/builtinTools,需要时只能 spawn 自定义 worker 类型):

#规则原因
BB1共享 schema 在共享模块中声明,父实体和子实体都导入它防止写入者与读取者之间的 schema 漂移
BB2mkdb位于if (ctx.firstWake) { ... }每次唤醒都调用会报错
BB3observe(db(...))firstWake守卫之外、每次唤醒都调用句柄必须每次唤醒重新获取
BB4Worker 通过ctx.args接收{ id, schema },并调用ctx.observe(db(args.sharedState.id, args.sharedState.schema))标准的 worker 契约
BB5若父实体要响应更新(而非等待子实体完成),配置observe(db(...), { wake: { on: "change", debounceMs } })否则父实体永远不会因共享状态变化而唤醒
BB6共享集合type遵循"shared:<name>"约定在持久流中区分共享状态事件与实体本地状态事件
BB7被派生的 worker 是应用注册的自定义类型(非内置worker),或者改用ctx.send+ctx.observe协调内置 worker 是最小权限沙箱,不支持sharedState参数

反模式:需要避免的常见错误

  • 在 worker 文件里重复声明 schema:两个看起来一模一样、但相互独立的对象字面量并不是同一个 schema。必须从共享模块导入。
  • 每次唤醒都调用mkdb:根据运行时不同会报错或覆盖数据;只能在firstWake调用。
  • 假设写入立即可见:共享写入是异步的;需要响应时使用 wake-on-change。
  • 把 schema 作为 JSON 字面量传递:运行时需要的是 Zod(或等效 Standard Schema)对象——传递导入的对象,而不是JSON.stringify(schema)的产物。

与其他协调模式的组合使用

blackboard 是"可叠加"的协作模式,典型组合包括:

  • manager-worker + blackboard:父实体按固定角色派生多个专家 worker,worker 的结论统一写入共享集合,父实体读取聚合。若父实体需要等全部完成再综合,用wake: { on: "runFinished" };若需要实时响应新结论,则对共享状态配置wake: { on: "change" }
  • map-reduce + blackboard:动态分块派生 worker,每个块把结果写到共享集合,reduce 阶段从集合中读取全部结果进行合并。

判别方法见 pattern-triggers.md 的消歧流程:先问"是否派生/协调其他实体",再问"并行还是串行"、"固定角色还是动态类型"、"多个实体是否读写同一份数据集"——最后一条命中即指向 blackboard,且它通常作为次级模式叠加在某种派生模式之上。在设计时,把"定义 handler 形状"的模式作为主模式,把 blackboard 作为额外的状态与唤醒需求来声明。

从模式文档到运行时实现,blackboard 的设计环环相扣:mkdb保证共享流只创建一次,observe(db(...))保证每个参与者都能在每次唤醒时重新连接,"shared:<name>"的 type 约定让共享事件在持久流中可辨识,而 D1–D7 测试则从行为上锁定了这套语义。按照本文的 schema 组织、父/子 handler 骨架与审查清单,你就可以在自己的 Electric Agents 应用中搭建出稳定、可扩展的多实体共享状态协作系统。

【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 2:21:55

Reactor模式实现HTTP服务器:从模型选型到线上排查

把一行标题做成能跑、能压、能上生产的服务器&#xff0c;中间要踩的坑&#xff0c;比想象中多得多。“基于 Reactor 模式的 HTTP 服务器”这个标题写起来很轻巧&#xff0c;真正动手你会发现&#xff0c;Reactor 只是骨架&#xff0c;HTTP 解析、连接复用、超时管理、缓冲区策…

作者头像 李华
网站建设 2026/9/16 2:21:19

Linux 下 MySQL 安装配置要点:从初始化到性能调优与备份恢复

做后端业务和技术运维的人&#xff0c;跟 Linux 和 MySQL 打交道基本是躲不开的。先说结论&#xff1a;Linux 环境下的 MySQL 搭建&#xff0c;真没有网上那些教程写得那么玄乎&#xff0c;核心就三件事——选对版本和安装方式、把初始化和安全配置做干净、再把数据目录和关键参…

作者头像 李华
网站建设 2026/9/16 2:20:33

NFS与iSCSI如何选?一文讲透文件级与块级存储的核心差异

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 2:20:01

从像素坐标到世界坐标:相机标定与坐标系转换实战指南

去年做巡检机器人项目时&#xff0c;我需要在机器人识别到目标物之后&#xff0c;把图像上的目标点换算成真实世界坐标。当时翻遍了各种资料&#xff0c;满屏都是“世界坐标系”“相机坐标系”“图像坐标系转换”这些关键词&#xff0c;公式堆了一堆&#xff0c;却几乎没有一篇…

作者头像 李华
网站建设 2026/9/16 2:19:20

uniapp-admin实战:从多端适配到Vue3迁移的完整指南

简介&#xff1a;面向uni-app开发者的多平台后台管理系统模板&#xff0c;采用Vue.js语法编写&#xff0c;一套代码可编译发布到iOS、Android、H5及各类小程序&#xff0c;适合需要快速搭建管理后台的团队或个人&#xff0c;也适合学习跨平台开发流程的初中级开发者。压缩包共4…

作者头像 李华
网站建设 2026/9/16 2:19:18

Java同城生活服务平台:从订单状态机到高并发抢单实战

"JAVA赋能同城生活&#xff1a;家政按摩私教茶艺随心享"&#xff0c;这句话拆开看&#xff0c;前半句是技术&#xff0c;后半句是市场。我在琢磨同城生活服务平台类项目时发现&#xff0c;家政、按摩、私教、茶艺这批服务有一个共性——全是"低频高客单"的…

作者头像 李华