# 37 · Org Graph 该不该是部门级,而不是公司级 > **日期**:2026-08-24 > **触发**:用户测试 e2e-demo(一家"开发型公司")时发现,公司除了写代码交付这条主线,还需要人事(招·留·管·退)、财务、市场、运营这类"公司自己运转"所需的职能——"麻雀虽小,五脏俱全"。但现状是**一家公司只有一张 org graph**(`27-P2` 文档标题原文就是"Org Graph — **公司级** Workflow 的生成与落地"),这张图被设计成"一条交付流水线"(规划→执行→验收),塞不下 HR/财务这类跟"交付什么产物"无关、却是任何公司都需要的常设职能。 > **结论先说**:用户提出的重新定义是对的——**org graph 应该是部门级(department-scoped),一家公司可以装配多张**,而不是公司级单例。这不是发明新概念,是把代码库里已经写好但从没拧紧的几颗螺丝对齐:`system/agents/hr/` 这个本体已经存在且职责原文就是"招·留·管·退",只是从没被装进任何图;`org_graph_nodes.reports_to_node_id` 这一列在 schema 里存在但零引用;`org-graph-templates/`(`dev.json`/`content.json`/`ops.json`)本身的形状早就是"一个部门一份",只是今天的用法是"公司建立时选一个"而不是"可以装配多个"。 > **状态**:设计讨论,尚未立项实现。本文档记录缘起、现状证据、提议方案与影响面,供下一步决定要不要排期。 --- ## 一、缘起(Why) 这次讨论的起点是三层递进的对话(同一会话内,2026-08-24): 1. **先确认"org graph vs work graph 的边界在哪"**——排查发现 lemonboss 的这套两层划分(角色图/工作图)跟姊妹项目 `graphlemon`(`docs/graphlemon_architecture.md`)是同一条设计脉络:Org Graph 人工固定、慢变、可审计;Work Graph 每次任务动态规划、可以快变。这条边界本身是对的,也有先例支撑。 2. **再讨论"能不能按 goal/task 现场 plan 出不同的 org graph"**——排查发现这个想法如果直接落地,会把"角色图"的慢变属性拉进"工作图"的快变节奏,破坏治理红线;而且引擎的 `activateSuccessTargets`(`server/services/graph-engine.js`)对 `condition='success'` 的出边是**无条件全部点亮**,没有 `fail_reason` 那种按路由键精确匹配的机制——就算图里画出多条分支,今天也做不到"按任务类型只走一条"。 3. **用户指出真实场景**:一家开发公司也需要人事、财务、市场、运营——这些不是"交付流水线上的一步",是任何公司都需要的**常设职能**。这才是问题的正确提法:不是"一张图要不要按任务变形",而是**"一家公司本来就不该只有一张图"**。 **Why 这件事现在值得写下来**:不是臆测的未来需求——`system/agents/hr/agent.md` 已经真实存在、职责描述("招·留·管·退")跟用户这次原话逐字对应,说明这个需求早就被感知到、甚至已经动手做了一半(写了 HR 本体),只是没人把它跟 org graph 打通,它今天完全是编外人员。 ## 二、现状实锤(What is) 按 `company_id` 单例设计的证据,散在四处,此前从没被合起来看: 1. **Schema 本身是单例**:`org_graph_nodes`/`org_graph_edges`(`server/services/graph-db.js:318` 起)只有 `company_id` 一个作用域键,没有部门/命名空间维度。`org-graph-service.getGraph(companyId)` 因此只能返回"这家公司唯一那张图"。 2. **规划 prompt 把这个假设写死了**:`server/services/task-planning.js:199` 给拆分 agent 的说明原文是"公司 org graph 摘要(**每条 task 都会各自走一遍这条流水线**)"——不是"选一条适合的流水线",是无条件全走一遍。 3. **就算画出多分支,引擎也不支持按类型选路**:`activateSuccessTargets`(`graph-engine.js` §四.D.1)遍历某节点**所有** `condition='success'` 的出边逐一点亮下游,唯一门槛是 fan-in 汇合;`fail_reason` 精确匹配选边的机制只长在 fail 边上,success 边没有对应物。 4. **新雇的角色不在图上就被排除在图外**:`company-service.js:114` 附近的排除规则③明文写着"公司已经有一张成型的组织工作流图,而这次雇的角色不在图上 → 不入图"——`hr` 正是被这条规则挡在图外的典型:它是真实存在、定义完整的本体,却从未出现在任何公司的 `org_graph_nodes` 里,今天的"招聘/训练"全靠 `POST /:companyId/hire` 触发的 fire-and-forget chat 流水线(`server/routes/company.js` 的 hire 路由 + `org-innovator` chat 调用),没有断点续跑、没有图执行那一套审计轨迹,跟 `org-coder` 走图执行的路是两套完全不同的机制。 5. **半埋下的层级维度**:`org_graph_nodes.reports_to_node_id`(`graph-db.js:324`)这一列存在,但 `org-graph-service.js` 里零引用——是个没人接上的部件,形态正好能承载"这个节点属于哪个部门"这类信息。 6. **模板本身已经是"一个部门一份"的形状**:`dev.json`/`content.json`/`ops.json`(`system/org-graph-templates/`)都是三节点小图(规划→执行→验收),`README.md` 原文说"每个 `*.json` 是一种公司类型的执行流图起点"——设计时想的就是"选一个",不是"可以都要"。 7. **连标题都写死了**:已实现并归档的 `docs/done/system-design/27-p2-org-graph.md` 标题就是"Org Graph — **公司级** Workflow 的生成与落地",这次要挑战的正是标题里这个前提。 ## 三、跟已有参考设计的关系 `graphlemon`(`docs/graphlemon_architecture.md`)和这次翻出来的三篇行业参考文章(`docs/reference/`)都没有讨论过"一家公司多个部门、按项目动态选用"这个问题——graphlemon 的 MVP 明确把范围锁定成单一"Dev Organizer"工厂类型(Planner→Coder→Validator 三节点,永远固定),它文档里"多 Goal 并发(多个 Org Graph 实例)"指的是**同一张图并发跑多个 goal 的多个 run**,不是"不同 goal 用不同形状的图"。所以本文档提议的"部门级 org graph"不是抄现成先例,是在已验证过的"角色图慢变可审计 / 工作图快变"这条边界之上,往前多走一步——这一步两边参考资料都没走过,需要自己想清楚。 ## 四、提议方案(How / What) **核心改动**:org graph 的作用域从 `company_id` 单例升级为 `(company_id, department_id)` 多例。每个部门的图依然保持"人工固定、慢变、可审计"的治理属性——只是一家公司现在可以有好几张这样的图,而不是被迫塞进一张。 1. **Schema**:`org_graph_nodes`/`org_graph_edges` 加一个部门作用域键(建议默认值兜住存量单图公司,不破坏兼容——存量公司的这张图归到一个隐式的默认部门,不需要数据迁移)。 2. **服务层**:`orgGraphService.getGraph/writeGraph/patchGraph/validateGraph` 从 `(companyId)` 升级为 `(companyId, departmentId)`。 3. **规划**:`orggraph-planning` 可以对同一家公司规划**多张**图——建公司时按 `whenToUse` 选出的不再是"唯一一张图的模板",而是"这家公司初始要装配哪几个部门",`dev.json`/`content.json`/`ops.json`/未来的 `hr.json` 都可以同时被选中、各自独立生成一张图。 4. **goal 路由**:`task-planning.js` 现在读"公司唯一那张图"喂给拆分 agent,需要改成先做一步"这个 goal 归哪个部门"的分类,再读那张部门图的摘要——这一步本质是复用 `orggraph-planning` 建公司时已经在做的 `whenToUse` 选型逻辑,只是从"建公司时跑一次"变成"每个 goal 都跑一次"。 5. **顺带收编招聘/训练**:HR 部门自己有一张 org graph 后,"雇一个新 agent""对某个 agent 做 FDE 训练"就能变成走图执行、可断点恢复、可审计的正经 goal,而不是今天这种 fire-and-forget 的 chat 调用(详见 [36-prd-agent-methodology-training-feasibility.md](./36-prd-agent-methodology-training-feasibility.md) 记录的独立试验训练全靠人工一条条 chat 催的现状)。 **治理属性没有丢**:这不是把角色图变成动态生成——每张部门图依然要走人工评审、依然慢变。变的只是"一家公司允许持有的图的数量"从 1 变成 N,这恰好更贴近人类公司的真实形态:组织架构(谁向谁汇报、部门怎么设置)是慢变的,但公司可以同时有好几个部门,每个项目只会用到其中相关的几个。 ## 五、影响面(调用点扫描) **2026-08-24 追加:全仓库扫描(fork)已完成。结论——改造面比最初估的两个调用点大约 4 倍,且真正的单点真相不在最初以为的那个文件里。** ### 5.1 服务层(`org-graph-service.js`)——机械可扩,风险低 导出 `validateGraph/writeGraph/patchGraph/getGraph/terminalAgentId/removeNode`,全部只按 `companyId` 作用域,调用方都是浅层引用,加一个 `departmentId` 参数即可: - `server/routes/company.js:564,586,599,611,1866,2074,2276,2515` —— CRUD 路由 + 4 处"图存不存在"的入队前置闸门 - `server/services/graph-engine.js:821` —— 淘汰过期 `workgraph-planning` 雇佣时的 `removeNode` - `server/services/task-settle.js:447` —— `terminalAgentId(companyId)` 找 goal 汇报人 - `server/services/task-planning.js:242-243` —— 读 `getGraph(companyId)` 摘要喂进拆分 prompt(已知) ### 5.2 绕过 service 层的直接 DB 访问——比假设的大得多,这才是真正的单点真相 - **`work-graph-db.js:1074` 的 `readDefGraph(kind, graphId)`**:`kind==='org'` 时直接对 `org_graph_nodes`/`org_graph_edges` 跑 `WHERE company_id = ?` 的裸 SQL,代码注释原文是**"kind === 'org':graphId 就是 company_id(§四.A.2)"**——这是一条**写进设计文档的架构不变式**,不是顺手写的代码。`graph-engine.js` 真正执行时走的 `defIndex(run)` 就是调它——**引擎的执行路径吃的是这份裸 SQL,不是 `org-graph-service.js` 那层**。只改服务层 API、不改这里,运行时引擎依然是公司级单例。 - **`graph-service.js`**(跟 `org-graph-service.js` 名字接近、是两个不同文件,容易搞混)自己也有一份裸 SQL,还持有一个**局部唯一索引** `idx_org_graph_nodes_company_role_active ON org_graph_nodes(company_id, role_name) WHERE status='active'`——这份索引在 `graph-db.js` 的 `migrate()` 里还有一份**必须字节级保持一致**的重复定义(代码注释明确警告这两处会打架)。这个文件还持有 `orgGraphMembership(companyId, roleName)`(就是撑住"图已成型、新雇角色被排除"那条规则③的真身)和 `upsertOrgGraphNode`(其幂等靠的 `ON CONFLICT(company_id, role_name)` 跟这份索引形状死死绑定)。 - **`graph-seed-service.js`**:一整个独立的"`company.json` ↔ DB 对账"子系统,8 处裸 SQL(公司删除级联、孤儿节点清理、一致性校验,分布在 246-259/311-322/493-533/609-698/756 行),全部按 `company_id` 作用域——**最初的两点估算完全没算到这一块**。 ### 5.3 路由层 `GET/POST/PATCH /:companyId/org-graph`、`POST /:companyId/org-graph/replan` 四个端点没有部门维度,需要 `?department=` 或嵌套路径 `/:companyId/departments/:deptId/org-graph`——无论哪种,§5.1 列的 8 个调用点形状都要跟着变。 ### 5.4 goal/task 派发链路 新发现:`enqueueTaskRun`(`company.js:1046-1062`)建 org 类型 run 时写的是 `wdb.createRun({graphKind:'org', graphId: companyId, ...})`——**"org run 的 `graph_id` 字面上就是 `companyId`" 这条约定的唯一生产写入点就在这里**,是 §5.2 那条不变式的源头。`work_graph_nodes.owner_org_node_id → org_graph_nodes(id)` 这条外键不受影响——只要 `org_graph_nodes.id` 继续是自增主键,部门只是这一行上多一列,外键完整性不受牵连。 ### 5.5 执行引擎 `graph-engine.js` 不按公司缓存图定义(每次 `advance` 都现查 `defIndex(run)`,没有单例缓存要操心)——但它拿"用哪张图"的唯一路径就是 `run.graph_id`,今天恒等于 `companyId`(§5.2/5.4)。6 处分支判断 `run.graph_kind === 'org'`(1017/4059/4114/4203/4252 行),全部是传递性受影响——只要 `readDefGraph` 的 SQL 改一处(比如 `graph_id` 改成 `${companyId}:${departmentId}` 这种复合键),这 6 处不需要各自单独动。 ### 5.6 招聘流程 排除规则③(`company-service.js` ~114-140 行)和 `_recordHireInGraph`/`decideOrgNodeRecording` 实际调的是 `graph-service.orgGraphMembership`(§5.2)——真正要改的落点比原以为的更深一层。雇佣本身的 `promoteDraftIfComplete` 不碰 org graph,不受影响。 ### 5.7 心跳/调度 `heartbeat-service.js`、`company-heartbeat-service.js` 对 `org_graph`/`orgGraph` **零引用**——心跳按 `company_agents` 枚举雇员,不查 org graph 表,无影响。 ### 5.8 前端消费方 `GraphConsole.vue:417`、`CompanyDashboard.vue:226-268`(渲染单条线性 `orgGraphChain` 数组)、`ReplanOrgGraphButton.vue:79,99` —— 三处都是"渲染唯一一张图",需要加部门选择器,没有更深的架构耦合。 ### 5.9 测试守卫 - **`company-hire-org-layer.test.js`**(146-238 行起,10+ 条专门用例)直接测"图已成型 → 新雇角色被排除"这条不变式本身——这是不变式的**正式测试合同**,改造时要重写语义(部门感知),不是简单传参。 - `org-graph-service.test.js` 等约 25 个文件只是顺手调用 `getGraph`/`writeGraph` 做测试前置,机械跟改即可。 ### 5.10 文档与 agent 人设 - **`system/agents/orggraph-planning/agent.md:5-6`**:自我认知原文是"这家公司**唯一**一次性的基础设施角色……你的唯一职责:为这家公司规划出**一张** org graph"——部门化直接跟这个 agent 的人设文本相矛盾,需要从"一次性、产出一张图"改写成"可按部门重复调用"。 - **`system/agents/task-planning/agent.md`**(24/29/36/52/63/74 行)通篇是单数框架("每条 task 都会各自走一遍**完整的** org graph"、"org graph 已经定了")——不只是 `task-planning.js:199` 那处代码里的 prompt 片段,人设文档本身也要重写。 - `system/org-graph-templates/README.md`:已知,"选一个"式表述。 - `docs/done/system-design/27-p2-org-graph.md`(补充:fork 用原路径没找到,实际文件已在 P2 完成后归档进 `done/`,本文档第一节已用正确路径引用过)。 ### 5.11 最大的几个意外 1. **"org graph = company_id" 真正的单点真相在 `work-graph-db.js` 的 `readDefGraph` 裸 SQL 里,不在 `org-graph-service.js`**——而且是写进设计文档的不变式(§四.A.2),坐在执行引擎的热路径上。只改服务层 API 不改这里,运行时引擎依然是公司级单例,等于白改。 2. **有两个名字相近、容易搞混的服务文件**(`org-graph-service.js` 与 `graph-service.js`)都各自持有对 `org_graph_nodes`/`org_graph_edges` 的裸 SQL,还共同维护一份**必须字节级一致、写了警告注释的重复唯一索引**——部门化要同时改两份索引定义 + `upsertOrgGraphNode` 的 `ON CONFLICT` 目标。 3. **`graph-seed-service.js`** 是完全独立的第三个子系统(`company.json` ↔ DB 对账/修复),又是 8 处裸 SQL——最初的两点估算里完全没算到这一块。 4. **排除规则③有一整套专属测试**(`company-hire-org-layer.test.js`),外加**两份 agent 人设文档把"只有一张图"写进了自我认知**(`orggraph-planning`/`task-planning`)——这次改造跟代码改动一样重的是 prompt/文档改写,不是"改完代码顺手补几个测试"能打发的。 **净结论**:改造面大约是最初两个调用点估算的 **4 倍**,横跨 3 个后端服务(不是 1 个)、执行引擎的裸 SQL 热路径、一份重复维护的 DB 索引、3 个前端组件、一整套专属测试、2 份 agent 人设文档。 ## 六、开放问题 - 一家公司同时有多张图之后,"公司整体健康度/进度"这类跨部门汇总视图(`CompanyDashboard.vue`)要不要重新设计,还是简单地按部门分区展示即可? - goal 的部门分类步骤本身会不会分错——分类错误的后果(一个明明该走 HR 图的 goal 被分进了 dev 图)需要有和 `body_not_ready`/`blocked_pending_human` 同级别的兜底与人工可见性,不能默默按错误部门执行到底。 - 部门之间会不会需要互相产出/消费 artifact(比如 HR 招到的新 agent 要交给 dev 部门"验收"),跨部门的边算不算这次设计范围内,还是留到部门内部图先跑顺再看要不要扩展成跨部门边。 ## 七、建议推进顺序 **2026-08-24 追加**:第五节扫描结果显示改造面是当初估算的 4 倍、横跨 3 个后端服务 + 执行引擎热路径 + 2 份 agent 人设文档,**不是一个中等改动**,不适合直接对存量公司/存量图动手。调整后的顺序: 1. **先钉死唯一入口**:部门化的落点不是 `org-graph-service.js`,是 `work-graph-db.js:readDefGraph` 那条 `graph_id === company_id` 的不变式(§5.2/5.4)——`enqueueTaskRun` 是这条约定的唯一生产写入点。任何验证都必须先改这一处(比如把 `graph_id` 换成 `${companyId}:${departmentId}` 复合键),否则表面上服务层/路由层都改完了,运行时引擎依然在按公司单例执行。 2. **最小验证仍然成立,但要绕开排除规则③和它的专属测试**:只给一家全新的测试公司加一张 `hr` 部门图(复用现成的 `system/agents/hr/`),跑通"HR 部门收到一个招聘类 goal → 走图执行 → 完成雇佣"这一条链路——**不要**碰 `company-hire-org-layer.test.js` 守着的存量单图公司行为,两边先并存,不急着让旧公司迁移。 3. **`org-graph-service.js` 与 `graph-service.js` 的裸 SQL + 那份必须字节级一致的重复索引**(§5.2)建议借这次机会合并成一处持有,不要在部门化时把"两份定义要保持一致"这条隐患原样搬进新维度——不合并的话,以后改索引形状要记得改两个文件,是新的技术债。 4. **`orggraph-planning`/`task-planning` 两份 agent 人设文档的重写,跟代码改动同批验收**,不要留到"代码先上、文档以后补"——这两份文档目前把"只有一张图"写进了自我认知,代码改完而文档没跟上,agent 会带着错误的自我认知继续按旧假设规划。 5. 验证通过后再决定要不要把"部门分类"这一步做成通用机制、推广到存量公司;`graph-seed-service.js` 那 8 处对账 SQL 放在最后处理——它是修复性质的子系统,不在 goal 执行的关键路径上,可以等主链路验证稳定后再补。