《互联网产品的 AI 架构·平台化与治理》11.1 谁拥有 AI 能力:平台团队与业务团队

能力归属不能按组织架构图分配。本节给出平台全权、业务全权、平台管契约业务管内容三种模型,用七个现场事实排出判定顺序,并把结论落到能力目录的 owner 字段:accountable、contractOwner、contentOwner、oncall、escalation 与复查周期。配套代码校验模型、团队类型与复查节奏,示例样本上五条能力得到三种归属,三条问题条目一次返回八个错误。

一次「复用」引发的归属争议

「回声」的客服团队要在私信里加一条会话总结,直接复用了内容团队的 content.summarize。两周后摘要质量下降,内容团队说自己只负责内容侧提示,客服团队说能力不是自己建的。事故复盘时,能力目录的 owner 一栏写着 team-content,但契约版本谁冻结、SLO 谁承诺、半夜谁值班,三个问题各有不同的答案。

问题不在于谁写代码,而在于所有权模型从未被定义。平台团队以为「能力在目录里登记过」就等于交接完成,业务团队以为「调用方就是使用方」不等于负责方。缺少一份明确的模型与判据,owner 字段就只是一行字符串。

三种所有权模型

所有权模型回答三件事:契约谁冻结、内容谁维护、发布谁能否决。三者可以落在同一个团队,也可以拆开,但不能没有答案。

模型平台负责业务负责发布决定权典型失效
平台全权 platform-owns契约、实现、默认提示、评测基线、值班提需求、报障、在自己的调用方内做参数覆盖平台;主版本仍需评审委员会业务私自改提示绕过评测,质量口径分裂
业务全权 business-owns网关、观测、配额、契约格式校验契约内容、提示、样本、阈值、值班业务跨租户复用后平台无法统一升级与审计
平台管契约业务管内容 platform-contract-business-content契约版本、SLO、审计口径、一线值班提示、样本、阈值、领域知识、二线支持契约变更平台评审,内容变更业务自主双方都以为对方负责,事故时互相等待

模型三不是折中口号,它有明确的切分线:凡是调用方可见的输入输出、失败语义、版本号与 SLO,归平台;凡是影响单次输出质量但不改变契约的提示、样本与阈值,归业务。切分线一旦写进 owner 字段,事故复盘时就不需要重新谈判。

选型判据:按顺序判定,不按偏好投票

模型选择容易变成组织政治,所以判据必须是有限的现场事实,并且有固定顺序。判定只看七个输入:调用团队数、租户数、是否跨租户、内容变更频率、是否需要领域知识、输出是否受监管、是否平台原语。

判定顺序如下:

  1. regulatedOutput = true:直接进入模型三。受监管输出必须由平台持有契约、版本与审计口径,业务可以维护内容,但不能单独决定行为边界。
  2. platformPrimitive = true 且内容变更频率为低、且不需要领域知识:进入平台全权。平台原语加稳定内容,集中维护的成本低于多方协调。
  3. 调用团队数大于等于 2,或租户数大于等于 2,或跨租户:进入模型三。复用范围一旦超出单团队,契约就必须被冻结。
  4. 其余情况:业务全权。单团队、单租户、非平台原语,平台只在网关层提供能力。

顺序本身就是优先级。一个受监管的平台原语先命中第一条,不会被第二条抢走;一个多团队使用但内容高度变化的摘要能力命中第三条,不会被「平台原语」误判为平台全权。

示例样本的五个能力得到三种归属:

能力调用团队租户关键事实判定
chat.reply32跨租户、内容高频变化模型三
search.rerank11平台原语、内容稳定平台全权
content.summarize21多团队复用、需要领域知识模型三
moderation.classify32受监管输出模型三
support.assist11单团队、内容高频变化业务全权

owner 字段不是自由文本

模型确定后,要落成能力目录里可校验的 owner 字段。最小结构包含六项:accountable、contractOwner、contentOwner、oncall、escalation、reviewCadenceDays。每一项都有对应的团队类型约束,不能只写一个团队名。

字段含义校验规则
accountable最终负责团队,事故与季度复盘都找它必须是非空、已登记的团队
contractOwner契约、版本与 SLO 的所有者平台全权与模型三必填;模型三必须等于 accountable
contentOwner提示、样本与阈值的所有者业务全权与模型三必填;模型三必须不同于 contractOwner
oncall一线值班标识非空;平台全权与模型三应是平台值班
escalation二线升级路径非空;业务全权可指向业务负责人
reviewCadenceDays所有权复查周期1 到 365 的整数;受监管能力不得超过 90 天

平台全权要求 contractOwner 等于 accountable,因为平台同时持有契约与实现;业务全权要求 contractOwner 与 contentOwner 都等于 accountable,即使显式记录也不能指向别的团队;模型三要求契约方是平台、内容方是业务,且两者不能合并。之所以把「等于」也写进校验,是因为所有权拆分一旦退化成同一个团队,模型三就名存实亡。

复查周期不是形式字段。受监管能力的内容与提示会直接影响对外输出,90 天是上限;平台原语通常 90 天;业务全权可以放宽到 180 天。周期超过上限时,校验直接报 cadence-too-slow,而不是等到季度审计才发现。

把模型与字段写成确定性校验

org.ts 提供三组能力:模型判定、owner 字段校验、整目录一致性校验。判定返回模型与判据;单条校验检查团队类型、字段完整性与周期;整目录校验再补三类跨条目规则:能力重复、owner 缺失、owner 指向不存在的能力。模型判定结果还会与 owner 声明的 model 比对,防止目录写一套、现场跑另一套。

// file: src/gov/org.ts
// tier: L2
// verify: node --test test/gov/org.test.ts

/** 三种所有权模型:平台全权、业务全权、平台管契约业务管内容。 */
export type OwnershipModel =
  | "platform-owns"
  | "business-owns"
  | "platform-contract-business-content";

export type ContentVolatility = "low" | "medium" | "high";

/** 判定只看这七个事实,不看组织架构图上的汇报关系。 */
export interface CapabilityProfile {
  capability: string;
  /** 调用方按团队计数,同一团队的多个入口只算一个。 */
  callingTeams: number;
  tenants: number;
  crossesTenant: boolean;
  contentVolatility: ContentVolatility;
  requiresDomainExpertise: boolean;
  regulatedOutput: boolean;
  platformPrimitive: boolean;
}

export type OwnershipBasis =
  | "regulated-output"
  | "platform-primitive"
  | "multi-team-reuse"
  | "cross-tenant"
  | "single-team-local";

export interface OwnershipDecision {
  capability: string;
  model: OwnershipModel;
  basis: readonly OwnershipBasis[];
  rationale: string;
}

/** 判定顺序就是选型判据:先合规,再原语,再复用范围,最后才允许业务自治。 */
export function decideOwnership(profile: CapabilityProfile): OwnershipDecision {
  const base = { capability: profile.capability };
  if (profile.regulatedOutput) {
    return { ...base, model: "platform-contract-business-content", basis: ["regulated-output"],
      rationale: "受监管输出由平台持有契约与审计口径,业务只维护内容" };
  }
  if (profile.platformPrimitive && profile.contentVolatility === "low"
    && !profile.requiresDomainExpertise) {
    return { ...base, model: "platform-owns", basis: ["platform-primitive"],
      rationale: "平台原语且内容稳定,平台同时持有契约、实现与默认内容" };
  }
  if (profile.callingTeams >= 2 || profile.tenants >= 2 || profile.crossesTenant) {
    const basis: OwnershipBasis[] = [];
    if (profile.callingTeams >= 2) basis.push("multi-team-reuse");
    if (profile.tenants >= 2 || profile.crossesTenant) basis.push("cross-tenant");
    return { ...base, model: "platform-contract-business-content", basis,
      rationale: "多团队或多租户复用,契约由平台冻结,业务保留内容调整权" };
  }
  return { ...base, model: "business-owns", basis: ["single-team-local"],
    rationale: "单团队单租户且非平台原语,业务可以端到端持有" };
}

/** 批量判定;重复能力标识直接拒绝,避免后者覆盖前者。 */
export function decideAll(profiles: readonly CapabilityProfile[]): OwnershipDecision[] {
  const seen = new Set<string>();
  for (const profile of profiles) {
    if (seen.has(profile.capability)) throw new Error(`duplicate capability: ${profile.capability}`);
    seen.add(profile.capability);
  }
  return profiles.map(decideOwnership);
}

/** 写进能力目录 owner 字段的最小结构。 */
export interface CapabilityOwner {
  capability: string;
  model: OwnershipModel;
  /** 最终负责团队,事故与季度复盘都找它。 */
  accountable: string;
  /** 契约所有者;模型一与模型三必填。 */
  contractOwner?: string;
  /** 内容所有者;模型二与模型三必填。 */
  contentOwner?: string;
  oncall: string;
  escalation: string;
  /** 所有权复查周期,天;受监管能力不得超过 90 天。 */
  reviewCadenceDays: number;
}

export interface Team { id: string; kind: "platform" | "business" }

export type OwnerIssueCode =
  | "duplicate-capability" | "duplicate-owner" | "unknown-capability" | "missing-owner"
  | "empty-field" | "unknown-team" | "team-kind-mismatch" | "missing-contract-owner"
  | "missing-content-owner" | "owner-same-party" | "owner-not-accountable"
  | "model-mismatch" | "invalid-cadence" | "cadence-too-slow";

export interface OwnerIssue {
  capability: string;
  code: OwnerIssueCode;
  field: string;
  detail: string;
}

const MODELS: readonly OwnershipModel[] = [
  "platform-owns", "business-owns", "platform-contract-business-content",
];

function bad(capability: string, code: OwnerIssueCode, field: string, detail: string): OwnerIssue {
  return { capability, code, field, detail };
}

function teamIssue(
  teams: ReadonlyMap<string, Team>, capability: string, field: string,
  teamId: string | undefined, kind: "platform" | "business", issues: OwnerIssue[],
): void {
  if (teamId === undefined || teamId.trim() === "") return;
  const team = teams.get(teamId);
  if (team === undefined) {
    issues.push(bad(capability, "unknown-team", field, `${teamId} is not registered`));
  } else if (team.kind !== kind) {
    issues.push(bad(capability, "team-kind-mismatch", field, `${teamId} is ${team.kind}`));
  }
}

/** 单条 owner 字段校验;模型与团队类型必须互证。 */
export function validateOwnerEntry(owner: CapabilityOwner, teams: readonly Team[]): OwnerIssue[] {
  const issues: OwnerIssue[] = [];
  const byId = new Map(teams.map((team) => [team.id, team]));
  for (const field of ["capability", "accountable", "oncall", "escalation"] as const) {
    if (owner[field].trim() === "") {
      issues.push(bad(owner.capability, "empty-field", field, `${field} is empty`));
    }
  }
  if (!MODELS.includes(owner.model)) {
    issues.push(bad(owner.capability, "model-mismatch", "model", `unknown ${owner.model}`));
  }
  if (!Number.isInteger(owner.reviewCadenceDays)
    || owner.reviewCadenceDays < 1 || owner.reviewCadenceDays > 365) {
    issues.push(bad(owner.capability, "invalid-cadence", "reviewCadenceDays", "not in 1..365"));
  }
  const add = (code: OwnerIssueCode, field: string, detail: string): void => {
    issues.push(bad(owner.capability, code, field, detail));
  };
  if (owner.model === "platform-owns") {
    if (owner.contractOwner === undefined) add("missing-contract-owner", "contractOwner", "required");
    teamIssue(byId, owner.capability, "accountable", owner.accountable, "platform", issues);
    teamIssue(byId, owner.capability, "contractOwner", owner.contractOwner, "platform", issues);
    if (owner.contractOwner !== undefined && owner.contractOwner !== owner.accountable) {
      add("owner-not-accountable", "contractOwner", "must equal accountable");
    }
    if (owner.contentOwner !== undefined && owner.contentOwner !== owner.accountable) {
      add("owner-not-accountable", "contentOwner", "platform-owns keeps content in-house");
    }
  } else if (owner.model === "business-owns") {
    if (owner.contentOwner === undefined) add("missing-content-owner", "contentOwner", "required");
    teamIssue(byId, owner.capability, "accountable", owner.accountable, "business", issues);
    teamIssue(byId, owner.capability, "contentOwner", owner.contentOwner, "business", issues);
    teamIssue(byId, owner.capability, "contractOwner", owner.contractOwner, "business", issues);
    for (const field of ["contentOwner", "contractOwner"] as const) {
      const value = owner[field];
      if (value !== undefined && value !== owner.accountable) {
        add("owner-not-accountable", field, "business-owns keeps both with accountable");
      }
    }
  } else if (owner.model === "platform-contract-business-content") {
    if (owner.contractOwner === undefined) add("missing-contract-owner", "contractOwner", "required");
    if (owner.contentOwner === undefined) add("missing-content-owner", "contentOwner", "required");
    teamIssue(byId, owner.capability, "accountable", owner.accountable, "platform", issues);
    teamIssue(byId, owner.capability, "contractOwner", owner.contractOwner, "platform", issues);
    teamIssue(byId, owner.capability, "contentOwner", owner.contentOwner, "business", issues);
    if (owner.contractOwner !== undefined && owner.contractOwner !== owner.accountable) {
      add("owner-not-accountable", "contractOwner", "must equal accountable");
    }
    if (owner.contentOwner !== undefined && owner.contentOwner === owner.contractOwner) {
      add("owner-same-party", "contentOwner", "contract and content must not collapse");
    }
  }
  return issues;
}

/** 整目录校验:能力、owner、团队三方一一对上,模型还要与现场事实一致。 */
export function validateOwners(
  profiles: readonly CapabilityProfile[],
  owners: readonly CapabilityOwner[],
  teams: readonly Team[],
): OwnerIssue[] {
  const issues: OwnerIssue[] = [];
  const profileIds = new Set<string>();
  for (const profile of profiles) {
    if (profileIds.has(profile.capability)) {
      issues.push(bad(profile.capability, "duplicate-capability", "capability", "appears twice"));
    }
    profileIds.add(profile.capability);
  }
  const ownerIds = new Set<string>();
  for (const owner of owners) {
    if (ownerIds.has(owner.capability)) {
      issues.push(bad(owner.capability, "duplicate-owner", "capability", "appears twice"));
    }
    ownerIds.add(owner.capability);
    const profile = profiles.find((item) => item.capability === owner.capability);
    if (profile === undefined) {
      issues.push(bad(owner.capability, "unknown-capability", "capability", "no profile"));
    } else {
      const expected = decideOwnership(profile).model;
      if (owner.model !== expected) {
        issues.push(bad(owner.capability, "model-mismatch", "model", `expected ${expected}`));
      }
      if (profile.regulatedOutput && owner.reviewCadenceDays > 90) {
        issues.push(bad(owner.capability, "cadence-too-slow", "reviewCadenceDays",
          `${owner.reviewCadenceDays} days for a regulated capability`));
      }
    }
    issues.push(...validateOwnerEntry(owner, teams));
  }
  for (const profile of profiles) {
    if (!ownerIds.has(profile.capability)) {
      issues.push(bad(profile.capability, "missing-owner", "capability", "no owner entry"));
    }
  }
  return issues;
}

代码里有两个刻意的取舍。第一,判定函数不接收组织架构,只接收七个现场事实;组织调整不应该让所有权判定自动漂移。第二,business-owns 也要求显式写 contractOwner 与 contentOwner,而不是留空表示「都在业务」;留空会在所有权转移时丢失责任锚点。

整目录校验的顺序也重要:先查能力重复,再查 owner 重复与未知能力,最后逐条做字段校验。这样同一份提交里的错误会一次列全,团队不需要在「发布—报错—再发布」之间来回。

示例样本上的运行结果

示例样本包含五个能力画像、五个 owner 条目与六个团队。以下是示例样本上的运行结果。

操作结果
五个画像判定平台全权 1、业务全权 1、模型三 3
五条完整 owner0 个问题
三条问题 owner8 个问题:团队类型不匹配 4、契约与内容同队 1、复查超期 1、模型不符 1、owner 缺失 2
删除 support.assist 的 owner1 个 missing-owner
重复提交同一能力画像抛出 duplicate capability

三条问题条目分别是:把 chat.reply 的 contentOwner 写成平台团队、把 moderation.classify 的复查周期改成 120 天、把 support.assist 的模型改成模型三。它们分别触发 team-kind-mismatch 与 owner-same-party、cadence-too-slow、model-mismatch 与两处团队类型错误。另有两条能力因为被从目录里删掉而报 missing-owner,合计八个问题。

这组数字说明校验的价值不是「找到一个错」,而是让一份错误的目录提交无法被合并。八个问题里任何一个单独存在,都足以在事故当晚让值班人找不到能改配置的团队。

所有权转移的三条规则

第一条,所有权绑定版本,不绑定能力名。把 content.summarize 从业务全权改为平台管契约,必须发一次主版本,旧版本保留原 owner 直到迁移窗口结束。原地改 owner 会让已经锁定旧版本的调用方失去责任主体。

第二条,模型三不能省掉任何一方。契约方是平台、内容方是业务,accountable 必须落在平台;如果内容方空缺,平台就会在事故时同时承担内容责任,模型三退化为平台全权。

第三条,复用范围变化要重新判定。一个业务全权能力从单团队扩展到两个租户,下一次发布前必须重新跑判定;判定结果变化时,先完成所有权迁移,再开放新租户调用。

三条规则合起来,把「谁拥有 AI 能力」从一次会议结论变成目录里可以查询、可以校验、可以随版本迁移的状态。

小结

能力所有权有三种模型:平台全权、业务全权、平台管契约业务管内容。选择不靠偏好,而靠七个现场事实与固定判定顺序:先合规,再平台原语,再复用范围,最后才允许业务自治。示例样本的五个能力得到 1 个平台全权、1 个业务全权、3 个模型三。

模型要落到 owner 字段才算完成。accountable、contractOwner、contentOwner、oncall、escalation、reviewCadenceDays 六项分别回答最终责任、契约责任、内容责任、值班、升级与复查节奏;团队类型、字段相等关系与周期上限都由代码校验。五条完整条目返回 0 个问题,三条问题条目一次返回 8 个问题。

阅读导航:上一节:10.3 平台自身的 SLO 与容量 · 下一节:11.2 评审委员会与决策记录 。

常见错误与边界

  • 把 owner 写成一个人名。人会离职,团队才是可交接的责任主体。
  • 用「能力在谁的系统里」推断所有权。系统归属与契约、内容、值班责任可以分离。
  • 模型三只写一个 owner。契约与内容没有分开记录,事故时必然互相等待。
  • 业务全权却不记录契约所有者。所有权转移时找不到旧契约的签字方。
  • 受监管能力把复查周期设成一年。内容漂移在两次审计之间无人发现。
  • 判定顺序随意调整。受监管输出可能被「平台原语」规则误判为平台全权。
  • 所有权变更原地改字段。已锁定旧版本的调用方失去责任主体,回滚也无法复现。
  • 复用范围扩大后不重判。单团队能力直接开放给第二个租户,契约却仍由业务单方修改。
  • 把校验当成发布前的形式检查。owner 缺失应该在目录提交时失败,而不是在事故值班表上发现。
  • 认为模型一旦确定就永久有效。组织、租户与监管范围变化都会触发重新判定。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查