27-graph-agent-architecture.md 21.3 KB

27. Graph Agent 架构 — Org Graph / Work Graph 与 System Agent 统一寻址

版本: v0.2(P0 已实现) 日期: 2026-08-15 状态: P0 ✅ 已实现(server/services/graph-db.js + graph-seed-service.js,见第七节);P1 起 📝 设计中/未实现 前置: 02 四层 Agent 架构、24 公司层架构、22 Agent 记忆系统、server/src/core/devloop/loop-controller.js(现有 retry-with-reflection 实现)


一、核心问题

现有模型(24 号文档)的局限

24 号文档确立的模型是「System Agent(全局单例)+ Agent Template(可复用蓝图)+ Company Agent(雇佣时从模板 文件拷贝 出来的实例)」。这套模型跑通了 Multi-Tenant 公司隔离,但暴露出四个结构性问题:

  1. 状态碎片化:每次 hire() 都是一次 fs.copyFileSync,同一角色在 10 家公司名下就有 10 份互不相干的 agent.md/memory.md。模板更新后,已雇佣的实例不会同步,也无法双向学习——A 公司的 QA 遇到的坑,B 公司的 QA 完全学不到。
  2. 无法自我进化:Agent 的"智能"被锁在一次性拷贝的静态文件里,没有一个机制让它在被反复调用后越来越准。唯一的进化路径是 depositTemplate()——训练出一个新 agent,跑 benchmark,≥95 分才存回模板库,这是"生个孩子"而不是"自己变强"。
  3. 协作关系是扁平列表:company.json.agents[] 只是一个角色名数组,没有边、没有汇报关系,更没有"谁的输出喂给谁"的显式结构。
  4. 失败恢复局限在单一场景:目前唯一的循环恢复机制是 DevLoop 的 runUnit(Coder ↔ Tester 两节点、固定方向、retry_budget 硬顶 5 次),无法泛化到"任意节点失败后,带着上下文跳回图中任意历史节点重跑"这种通用场景。

设计目标

把「company 拥有并复制 agent」改造为「company 引用全局 system agent,并在其上定义静态协作结构(Org Graph)与动态任务执行路径(Work Graph)」:

  1. Agent 是跨公司复用、持续自我进化的一等公民,不是一次性文件拷贝。
  2. 公司的组织结构(Org Graph)与具体任务怎么跑(Work Graph)解耦:前者是company 层面相对稳定的静态定义,后者由 planning agent 针对每个目标动态规划、执行完即归档。
  3. 失败恢复不再局限于"重跑同一个节点":任意节点失败可以携带上下文回退到图中任意历史节点,同时有引擎级别的硬性防死循环机制。

二、术语定义

术语 定义
System Agent 全局唯一的可执行 Agent 实体。自带 prompt 定义、公共记忆(memory.md)、按公司分区的私有历史。是 24 号文档里"System Agent"与"Agent Template"两个概念的合并——不再区分"模板"和"实例",只有"这个 agent,被谁调用过"。
scope System Agent 的可见性:public(跨公司共享,记忆公共累积)或 company(私有,只属于 owner_company_id 指定的一家公司)。
Org Graph 公司级别的静态协作结构。节点 = 对某个 system agent 的引用;边 = 汇报/协作关系。取代现有 company.json.agents[] 扁平列表。
Work Graph 某个 org 节点收到一个 goal/task 后,现场调用 work-graph-planning agent 针对这一具体任务规划出的动态执行图。节点 = 对某个 system agent 的一次调用;边带 success/fail 条件与目标节点。执行完归档,不复用到下一个任务。
Loop-Validator 一个专职的 system agent,内置带硬编码熔断逻辑的 skill。所有"回退到历史节点"的边必须先经过它:备案 → 审计上次修复是否生效 → 判断修复方案是否被正确传递 → 循环数 > 3 强制终止。
Node 内容 / Edge 内容 图结构本身(节点、边、状态、指针)存 SQLite;节点的输入输出内容、日志、上下文,依旧是 Markdown 文件,物理落在对应 system agent 的目录下。

三、架构设计

3.1 三层模型总览

                     ┌─────────────────────────────────────┐
                     │            System Agent 池            │
                     │  (全局唯一,可跨公司复用,自我进化)    │
                     │                                       │
                     │   [ceo]  [coder]  [qa-judge]  [...]   │
                     │     ▲       ▲         ▲                │
                     └─────┼───────┼─────────┼────────────────┘
                           │       │         │  (引用,非复制)
              ┌────────────┴───────┴─────────┴──────────────┐
              │           Company: {companyId}                │
              │                                                │
              │   Org Graph(静态,company 生命周期内相对稳定) │
              │     [CEO 节点] ──reports──> [CTO 节点] ──> ...  │
              │                                                │
              │   Work Graph(动态,per-goal,执行完归档)      │
              │     [node1:coder] --success--> [node2:tester]  │
              │                    --fail-----> [loop-validator]│
              │                                       │         │
              │                                       ▼         │
              │                              [回退到 node1]      │
              └────────────────────────────────────────────────┘

3.2 System Agent:唯一执行体与数据隔离

目录结构(取代现有 system/agents/ 与 system/templates/ 的分离):

system/agents/{agentId}/
  agent.md / soul.md / identity.md / tools.md   # 定义本体,全局唯一
  memory.md                                      # 公共记忆:自我迭代蒸馏出的能力/经验,跨公司共享持续增长
  config.json                                     # { scope: public|company, owner_company_id, status, benchmark_score }
  companies/
    {companyId}/
      memory.md          # 该公司私有工作记忆(目标、上下文、汇报关系)
      history/
        {workGraphId}-{nodeId}-{attempt}.md   # 该公司触发的每次执行原始记录
      logs/
  • company-scoped(私有):companies/ 下永远只有 owner 一个子目录,代码路径与 public 完全一致,不做特殊分支。
  • public(公共):companies/ 下有多个公司各自的隔离子目录。

读取隔离原则:PromptBuilder 组装某次调用的上下文时,只按 (agentId, companyId) 构造路径——公共部分(agent.md/memory.md)+ 这一家公司自己的 companies/{companyId}/。别的公司的目录从物理路径上就不会被触达,跟现有 DevLoop CONVENTIONS.md §12「跨 unit 只能读 ../{otherUnit}/contract.json」是同一种"靠构造方式天然隔离"的思路,不是运行时权限过滤。

写入分级:私有 companies/{id}/memory.md 每次调用后可自由写,风险局限在一家公司内。公共 memory.md 的更新(自我进化)风险更高——写错会污染所有后续调用方——因此需要走一次自省校验(复用 loop-validator 或类似机制),并且像 DevLoop 项目一样纳入 git 版本管理,坏的自我迭代可以回滚。

新建与升级治理:Work-graph 执行中如果需要的 system agent 不存在,直接触发创建,默认 scope=company(私有)。是否升级为 public,复用现有 hireWithTraining() → agent-creator/innovator 训练 → benchmark ≥95 → 升级的门槛链路,只是终点从"写文件到 system/templates/"改为"更新 DB 里的 scope 字段"。不新增治理机制,只改落点。

3.3 Org Graph:公司静态结构

  • 节点:对某个 system agent 的引用(org_graph_nodes),代替 company.json.agents[]。
  • 边:汇报/协作关系(org_graph_edges),公司创建/招聘时由 CEO 或 HR agent 维护。
  • "招聘"语义变化:不再是"拷贝一份文件",而是"在 org graph 里加一个指向某 system agent 的引用节点"。

3.4 Work Graph:任务动态执行图

  1. 某 org 节点收到一个 goal/task。
  2. 该节点调用 work-graph-planning agent,针对这个具体任务规划出一张图:节点指向要执行的 system agent,边带 success/fail 条件。
  3. 目标节点范围不受 org graph 约束,只受 system agent 池约束——planning agent 可以调用任意 public agent、company 自己私有的 agent,或判断需要新建一个(走 3.2 的创建+治理链路)。
  4. 回退边必须显式经过 loop-validator 节点——即"节点A --fail--> loop-validator --> 节点B",而不是"节点A --fail(隐藏回退属性)--> 节点B"。这是画图时的结构要求,不是引擎运行时才附加的隐藏逻辑。
  5. 执行前结构校验:图规划完成后、进入执行前,引擎做一次静态校验——任何边如果目标节点在图中已存在其他指向它的边(即会构成"回到历史节点"),但没有经过 loop-validator 节点,直接拒绝这张图,不进入执行。类比现有 npm run spec:check 的"结构先校验,再允许跑"模式。这是对"planning agent 应该自觉画上 validator"这条规则的引擎级兜底,防止规划疏漏导致死循环绕过审计。

3.5 Loop-Validator:死循环兜底

  • 本身是一个 system agent(可以是 public,因为"判断循环是否收敛"这个能力本质上与具体业务无关,值得跨公司共享进化)。
  • 自带 skill,skill 内部包含硬编码的计数器/熔断逻辑,不是纯靠一次 LLM 推理决定"要不要停":
    1. 备案:任意节点即将进入回退循环前,先向 loop-validator 登记这次尝试。
    2. 审计:目标节点重跑完成后,loop-validator 比对这次结果与上次失败原因,判断是否真的修复。
    3. 修复方案传递校验:如果没修复,检查这次回退时是否把"应该如何修复"的方案正确带给了下一轮,而不是盲目重跑。
    4. 硬顶:循环次数 > 3,无论 LLM 判断如何,skill 内的计数器强制终止,不进入第 4 次回退。
  • 引擎级不变量:任何回退边必须路由经过 loop-validator,这一点不能被 planning agent 在规划阶段跳过——由 3.4 第 5 点的执行前校验保证。

3.6 与执行链路的关系

节点执行本质上仍是现有 AgentManager.callAgent() 的调用,只是 getAgentConfig() 的输入从"companyId:roleName 路径拼接"变成"(system_agent_id, companyId) 的引用查表",PromptBuilder.build() 在装配 prompt 时按 3.2 的隔离规则拼装公共定义 + 私有语境。执行引擎(session-pool、runtime-resolver、stream-executor)不需要改,改的是"config 从哪来"这一层。


四、数据模型(Schema 草稿)

DB 只存关系与状态指针,节点/边的具体内容(任务描述、输出、日志)始终是 Markdown 文件,按 3.2 的目录约定物理落在对应 system agent 目录下(不在 DB 里存文件路径字符串,路径由 (system_agent_id, company_id, work_graph_id, node_id, attempt) 确定性推导,避免路径漂移)。

-- 全局 system agent 注册表
CREATE TABLE system_agents (
  id              TEXT PRIMARY KEY,
  name            TEXT NOT NULL,
  scope           TEXT NOT NULL CHECK (scope IN ('public', 'company')),
  owner_company_id TEXT,              -- scope='company' 时必填
  status          TEXT NOT NULL DEFAULT 'active',  -- active | training | deprecated
  benchmark_score REAL,
  created_at      INTEGER NOT NULL,
  updated_at      INTEGER NOT NULL
);

-- 公司静态协作结构
CREATE TABLE org_graph_nodes (
  id              TEXT PRIMARY KEY,
  company_id      TEXT NOT NULL,
  system_agent_id TEXT NOT NULL REFERENCES system_agents(id),
  role_name       TEXT NOT NULL,
  created_at      INTEGER NOT NULL
);

CREATE TABLE org_graph_edges (
  id              TEXT PRIMARY KEY,
  company_id      TEXT NOT NULL,
  from_node_id    TEXT NOT NULL REFERENCES org_graph_nodes(id),
  to_node_id      TEXT NOT NULL REFERENCES org_graph_nodes(id),
  relation_type   TEXT NOT NULL          -- reports_to | collaborates_with
);

-- 单次任务的动态执行图
CREATE TABLE work_graphs (
  id              TEXT PRIMARY KEY,
  company_id      TEXT NOT NULL,
  triggered_by_org_node_id TEXT REFERENCES org_graph_nodes(id),
  goal_task_id    TEXT,
  status          TEXT NOT NULL DEFAULT 'planning',  -- planning | running | passed | failed | blocked
  created_at      INTEGER NOT NULL,
  updated_at      INTEGER NOT NULL
);

CREATE TABLE work_graph_nodes (
  id              TEXT PRIMARY KEY,
  work_graph_id   TEXT NOT NULL REFERENCES work_graphs(id),
  system_agent_id TEXT NOT NULL REFERENCES system_agents(id),
  node_kind       TEXT NOT NULL DEFAULT 'task',   -- task | loop_validator
  status          TEXT NOT NULL DEFAULT 'pending', -- pending | running | success | fail | blocked
  attempt         INTEGER NOT NULL DEFAULT 1
);

CREATE TABLE work_graph_edges (
  id              TEXT PRIMARY KEY,
  work_graph_id   TEXT NOT NULL REFERENCES work_graphs(id),
  from_node_id    TEXT NOT NULL REFERENCES work_graph_nodes(id),
  to_node_id      TEXT NOT NULL REFERENCES work_graph_nodes(id),
  condition       TEXT NOT NULL CHECK (condition IN ('success', 'fail')),
  edge_kind       TEXT NOT NULL DEFAULT 'forward'  -- forward | backward(backward 必须 to_node 是 loop_validator 或来自 loop_validator)
);

edge_kind='backward' 由 planning agent 在画图时显式标注;执行前校验(3.4 第 5 点)会独立复核——不信任标注本身,而是遍历图结构确认任何指向"已在图中更早出现过"的节点的边,必须满足"经过 node_kind='loop_validator'"这一条件,双重保险。


五、与现有系统的关系 / 迁移策略

现有组件 变化
server/services/company-service.js 的 hire() / _copyTemplate() 废弃文件拷贝,替换为"创建/复用 system agent + 在 org_graph 加引用节点"
system/templates/ 逐步废弃,内容并入 system_agents(scope=public),_registry.json 被 DB 表取代
AgentManager.getAgentConfig() 输入从路径拼接改为 (system_agent_id, company_id) 查表 + 引用 3.2 目录约定
PromptBuilder.build() 增加"公共定义 + 私有语境"两段拼装逻辑
hireWithTraining() / agent-creator / innovator / depositTemplate() 保留,只改落点(写 DB scope 字段而非写模板目录)
DevLoop loop-controller.js 的 runUnit 可视为 Work Graph 的一个特例(固定两节点、单方向重试)。本次不强制收编,留作后续 Phase 6 的可选项

六、开发计划(Phased Roadmap)

Phase 目标 交付物 验收标准
P0 地基 ✅ 建表 + 现有数据迁移,不改变任何现有执行路径 system_agents/org_graph_* 表;把现有 system/agents/ + system/templates/ 目录扫描导入为种子数据(system_agents 行,scope 按当前所属推断) 迁移脚本跑完,system_agents 表行数与现有 agent+模板目录数一致(实际行数与本文档最初估计不同,见第七节);现有 company-service.js 行为不变(并行只读验证,已确认零改动)
P1 虚拟引用 停止文件拷贝,改为引用 改造 AgentManager.getAgentConfig() + PromptBuilder.build() 支持 (agentId, companyId) 寻址;company-service.hire() 改为创建 org_graph 引用节点而非 _copyTemplate()。细化需求与设计见 27-P1 虚拟引用寻址 新雇佣的 agent 不再产生文件拷贝,调用时能正确拼装公共+私有上下文;旧公司(仍是拷贝模式)继续可跑,不强制迁移存量数据
P2 Org Graph company.json.agents[] 迁移为图结构 org_graph_nodes/edges 落地;HR/CEO 招聘走新表 现有 Company Dashboard 前端能正确读出 org graph 并渲染(哪怕暂时按列表展示)
P3 Work Graph 最小闭环 单向执行,先不做回退 work-graph-planning agent(system agent,新建);work_graphs/nodes/edges 表;顺序执行器(success → next,fail → 停止/标记 blocked,暂不回退) 一个 goal 能被规划成 2-3 节点的图并顺序跑完,状态正确落库
P4 回退 + Loop-Validator 引入死循环防护 loop-validator system agent + 内置熔断 skill;edge_kind=backward 支持;3.4 第 5 点的执行前结构校验 人为构造一个"总是失败"的节点,验证第 4 次回退被 loop-validator 强制终止,且校验能拒绝"回退边未经过 validator"的非法图
P5 自我进化 公共记忆蒸馏机制 公共 memory.md 写入走自省校验 + git 版本化;company-scoped → public 的升级门槛复用现有 benchmark ≥95 链路 同一 public agent 被两家公司调用后,memory.md 有可观察的增量更新,且能 git revert 到升级前版本
P6(可选)DevLoop 收编 统一循环逻辑 把 loop-controller.js/unit-registry.js 重构为 Work Graph 引擎上的一个具体图模板 DevLoop 现有测试套件(server/tests/devloop/**)在新引擎上全部通过,无需单独维护两套循环代码

依赖顺序是强顺序的:P1 依赖 P0 的数据先迁移进 DB;P3 的执行器依赖 P1 的引用寻址跑通;P4 的回退依赖 P3 的顺序执行先稳定。P5、P6 相对独立,可以在 P4 稳定后并行推进或往后延。

每个 Phase 建议保留旧路径作为 fallback(不删除 _copyTemplate()/system/templates/ 扫描逻辑),直到下一 Phase 验收通过,避免中途出问题时无法回退。


七、P0 实施发现(2026-08-15)

P0 已实现(server/services/graph-db.js + server/services/graph-seed-service.js + scripts/graph-seed.js,npm run graph:seed 可跑),经 code review 通过。实施过程中对真实仓库数据做了独立核实,几个数字和本文档最初的估计(来自第一轮调研)不一致,记录如下,供 P1/P2 设计时参考:

项 本文档最初估计 真实数据
system/agents/ 里真正的 agent(含 agent.md) 10 8 —— screenwriter/、youtube-crawler/ 两个目录没有 agent.md,不满足"是一个可执行 agent"的最低条件,被迁移脚本正确排除
system/templates/ 目录数 34 39
最终落库的 source_kind='template' 行数 34 33(39 减去下面的命名空间重叠问题里被 global_agent 占用的 6 个)
_registry.json 里 benchmark_score 非空的条目 9 3(ceo=97/qa-judge=95/base=90)——另外 6 个 devloop-* 虽然在 registry 里能匹配上 id,但 registry 原始数据里 benchmarkScore 本来就是 null,不是脚本漏读
idearun 公司 6 个角色里"公司专属、无对应模板"的数量 4 0 —— 6 个角色现在全部能在 system/templates/* 里找到同名目录,全部复用已有行,没有触发新建 scope='company' 记录

关键发现:system/agents/ 与 system/templates/ 命名空间不是互斥的

有 6 个 id(agent-creator/digester/evolver/evolver-youtube-crawler/hunter/innovator)在两个目录下都存在,描述的是概念上"同一个" agent,但物理上是两份独立的文件(内容很可能已经不同步)。这一点在最初两轮只读调研(用于写这份文档正文)里没有被发现,是 P0 实际扫描真实目录时才暴露出来的。

P0 阶段的处理方式是迁移脚本层面的临时保护:system_agents.id 是跨 source_kind 共享的单一 TEXT 主键,seedTemplates() 遇到一个已经被 seedGlobalAgents() 占用的 id 时会跳过写入(不覆盖),保证 global_agent 优先、幂等可重跑。这只是避免了"数据被静默覆盖"的坏结果,没有回答"这 6 个 agent 到底该以哪份文件为准"这个产品问题——system/agents/{id}/agent.md 和 system/templates/{id}/agent.md 目前很可能内容不同,P1 做虚拟引用寻址、P2 做 org graph 招聘的时候都会撞到这批 id,需要在那之前决定:是人工合并成一份、保留 global_agent 版本视为权威并废弃 template 版本,还是干脆认为这 6 个本来就是两个不同的东西(比如一个是"基础设施 agent"、一个是"可招聘的角色模板")、id 冲突只是命名巧合。这个决定应该在 P1 动手改 AgentManager/PromptBuilder 之前做,否则虚拟引用会读到不确定是哪一份的 agent.md。

关联文档

P1(虚拟引用寻址)的详细需求与设计已有独立草稿:27-P1 虚拟引用寻址。