《互联网产品的 AI 架构·平台化与治理》10.1 平台的自助服务边界

自助服务不是把所有按钮都放开。本节把回声平台的七类请求拆成四类自助与三类人工,用准入目录、档位、额度与语料隐私四个条件判定路由,给出 24 小时、5 个工作日与 10 个工作日三档审批时限,并用可运行代码跑出 10 个示例请求的路由结果与 40% 自助覆盖率。

一个平台,两个团队,三种工单

周三上午,平台团队收到三张工单。客服团队想把 chat.reply 的档位从 standard 调到 frontier,因为退款话术不能出错;内容团队想把团队份额从 4,000 万微元调到 1,200 万微元,理由是双十一要提前备量;合规团队问,能不能让一个境外租户的评测语料在境内团队可见。

三张工单看起来都是「配置一下」,实际分属三种性质。第一张改变了能力的成本与质量边界,第二张突破了团队份额的自助上限,第三张涉及数据跨境。如果平台把这些都做成按钮,业务方会按错;如果都做成工单,平台团队会被日常请求淹没。

自助服务的边界,就是回答「哪些按钮可以给人按,哪些必须有人签字」。它不是一个 UI 问题,而是一组可判定的路由规则,加上每一类人工请求的响应时限。

四类自助与三类人工

先把请求分类。分类的依据不是「操作是否复杂」,而是「越界后谁来承担后果」。

请求默认路由判定依据越界后果
申请能力(开通已准入能力)自助能力在准入目录、档位开放成本进入团队份额
配团队配额自助不超过自助上限、不超过租户余额挤占其他团队
看账单与用量自助只读无
查评测与跑公开评测自助语料非受限、单次预算内消耗评测预算
新能力上线人工不在准入目录质量、安全、合规未知
跨租户借用额度人工涉及两个租户的商务承诺一方挤占另一方
合规例外人工涉及保留、跨境或敏感数据法律与声誉风险

这张表的关键在最后一行:合规例外永远不自助。不是因为它操作难,而是因为它的后果不可逆。平台可以把申请表单做得很顺滑,但不能把签字权也做进表单。

反过来,看账单永远自助。它只读、不改变任何状态,没有任何理由让人工审批插在中间。一个平台如果连账单都要工单,说明它的权限模型还没拆开「读」与「写」。

四个判定条件

七类请求可以收敛成四个条件。每个条件回答一个「是否越界」的问题,全部为否才允许自助。

第一个条件是准入目录。能力必须已经通过平台的质量、成本、安全与合规评审,才允许业务方自助开通。目录里没有的能力,哪怕调用方式与已有能力一模一样,也走人工。这条规则防止「换个名字绕过评审」。

第二个条件是档位开放。同一个能力,economy 与 standard 可以自助,frontier 默认人工。档位不是性能参数,它是成本与质量的承诺:frontier 的单次成本可能是 standard 的数倍,而且更容易触发供应商限流。让业务方自助打开 frontier,等于让他在没有容量评估的情况下改变整条链路的成本结构。

第三个条件是额度上限。团队可以在自助界面调整自己的份额,但有两个天花板:平台设定的单团队自助上限,以及租户当前剩余额度。超过任何一个,转人工。前者防止单一团队把租户总额吃光,后者防止配置出一个账面上不存在的额度。

第四个条件是语料隐私。公开语料与租户自有语料可以自助跑评测;标记为 restricted 的语料,哪怕调用方就是语料所有者,也要人工确认可见范围。评测平台最容易泄漏隐私的地方不是结果页,而是「谁能把哪些样本放进同一个评测集」。

四个条件里,前三个是数值与目录判断,可以完全自动化;第四个涉及语义与授权,必须有人工环节。这也是为什么自助边界不能只写一句「按风险分级」,而要落到具体字段。

审批时限:24 小时、5 个工作日、10 个工作日

人工请求如果没有时限,就会退化成邮件列表。平台要对每类人工请求承诺首次响应时间,并明确「超时只升级,不自动批准」。

人工请求首次响应时限折算超时动作
额度超过自助上限24 小时1 个工作日升级至团队负责人
租户余额不足24 小时1 个工作日升级至商务负责人
受限语料评测24 小时1 个工作日升级至数据负责人
跨租户借用24 小时1 个工作日升级至两个租户负责人
合规例外120 小时5 个工作日升级至合规负责人
新能力上线240 小时10 个工作日升级至平台评审委员会

时限的设计有一个取舍:越接近「配置调整」的请求,时限越短;越接近「新资产准入」的请求,时限越长。额度与借用是已有资产的重新分配,一天内必须给答复;新能力上线要做评测、容量与合规三份材料,十个工作日是合理下限。

合规例外放在 5 个工作日,是因为它需要法务与数据保护官共同判断,不能为了追求「平台响应速度」而压缩审查。时限短不等于治理好,时限与风险匹配才是。

路由与时限的实现

下面的模块只做三件事:按四个条件给出路由、计算首次响应截止时间、统计自助覆盖率。它不读写数据库,也不调用审批系统。

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

/** 平台请求分两类:能自助完成的,和必须有人拍板的。 */
export type RequestKind =
  | "enable-capability"
  | "set-team-quota"
  | "view-billing"
  | "run-evaluation"
  | "launch-capability"
  | "cross-tenant-borrow"
  | "compliance-exception";

export type ServiceRoute = "self-service" | "manual";
export type CorpusPrivacy = "public" | "tenant" | "restricted";

export interface PlatformPolicy {
  /** 已通过准入、允许业务方自助开通的能力。 */
  approvedCapabilities: readonly string[];
  /** 自助开通允许选择的档位;frontier 默认不进自助。 */
  selfServiceTiers: readonly string[];
  /** 团队可自助调整的份额上限;超过即转人工。 */
  maxSelfServiceQuotaMicros: number;
  /** 单次评测的预算上限,防止评测把生产预算跑穿。 */
  evalBudgetMicros: number;
}

export interface ServiceRequest {
  kind: RequestKind;
  capability?: string;
  tier?: string;
  requestedQuotaMicros?: number;
  tenantRemainingMicros?: number;
  corpusPrivacy?: CorpusPrivacy;
  evalCostMicros?: number;
}

export interface RouteDecision {
  kind: RequestKind;
  route: ServiceRoute;
  /** 自助为 0;人工为首次响应的时限,单位小时。 */
  slaHours: number;
  dueAtMs: number;
  reason: string;
  escalation: string;
}

/** 人工请求的首次响应时限;时限是承诺,不是审批必然通过的保证。 */
export const MANUAL_SLA_HOURS: Record<RequestKind, number> = {
  "enable-capability": 240,
  "set-team-quota": 24,
  "view-billing": 0,
  "run-evaluation": 24,
  "launch-capability": 240,
  "cross-tenant-borrow": 24,
  "compliance-exception": 120,
};

const HOUR_MS = 3_600_000;

function isNonNegativeInteger(value: number | undefined): value is number {
  return value !== undefined && Number.isInteger(value) && value >= 0;
}

function decide(
  request: ServiceRequest,
  route: ServiceRoute,
  reason: string,
  nowMs: number,
): RouteDecision {
  const slaHours = route === "self-service" ? 0 : MANUAL_SLA_HOURS[request.kind];
  return {
    kind: request.kind,
    route,
    slaHours,
    dueAtMs: nowMs + slaHours * HOUR_MS,
    reason,
    escalation: route === "self-service" ? "无需审批" : "超时升级至平台值班与对应负责人",
  };
}

/**
 * 路由只看四件事:动作是否在准入目录内、档位是否开放、额度是否在自助上限内、
 * 语料是否允许跨团队可见。任何一项越界就转人工,不在自助界面里做例外。
 */
export function routeRequest(
  policy: PlatformPolicy,
  request: ServiceRequest,
  nowMs: number,
): RouteDecision {
  switch (request.kind) {
    case "view-billing":
      return decide(request, "self-service", "read-only", nowMs);

    case "enable-capability": {
      const capability = request.capability ?? "";
      if (!policy.approvedCapabilities.includes(capability)) {
        return decide(request, "manual", "capability-not-approved", nowMs);
      }
      if (!policy.selfServiceTiers.includes(request.tier ?? "")) {
        return decide(request, "manual", "tier-requires-review", nowMs);
      }
      return decide(request, "self-service", "approved-catalog", nowMs);
    }

    case "set-team-quota": {
      const requested = request.requestedQuotaMicros;
      const remaining = request.tenantRemainingMicros;
      if (!isNonNegativeInteger(requested) || !isNonNegativeInteger(remaining)) {
        return decide(request, "manual", "invalid-quota", nowMs);
      }
      if (requested > policy.maxSelfServiceQuotaMicros) {
        return decide(request, "manual", "above-self-service-ceiling", nowMs);
      }
      if (requested > remaining) {
        return decide(request, "manual", "tenant-capacity", nowMs);
      }
      return decide(request, "self-service", "within-ceiling", nowMs);
    }

    case "run-evaluation": {
      if (request.corpusPrivacy === "restricted") {
        return decide(request, "manual", "restricted-corpus", nowMs);
      }
      if (!isNonNegativeInteger(request.evalCostMicros)) {
        return decide(request, "manual", "invalid-eval-cost", nowMs);
      }
      if (request.evalCostMicros > policy.evalBudgetMicros) {
        return decide(request, "manual", "above-eval-budget", nowMs);
      }
      return decide(request, "self-service", "public-or-tenant-corpus", nowMs);
    }

    case "launch-capability":
      return decide(request, "manual", "new-capability-onboarding", nowMs);

    case "cross-tenant-borrow":
      return decide(request, "manual", "cross-tenant-transfer", nowMs);

    case "compliance-exception":
      return decide(request, "manual", "compliance-exception", nowMs);
  }
}

/** 超时只改变升级对象,不自动批准;自助请求的时限恒为 0。 */
export function overdueRequests(
  decisions: readonly RouteDecision[],
  nowMs: number,
): RouteDecision[] {
  return decisions.filter(
    (decision) => decision.route === "manual" && decision.dueAtMs <= nowMs,
  );
}

/** 自助覆盖率的分母是全部请求,不是人工请求;它衡量的是边界是否划得够宽。 */
export function selfServiceRatio(decisions: readonly RouteDecision[]): number {
  if (decisions.length === 0) return 0;
  return decisions.filter((decision) => decision.route === "self-service").length / decisions.length;
}

routeRequest 的每个分支都返回同一个结构:路由、时限、原因与升级对象。原因码是给看板聚合用的,不是给业务方看的文案;业务方看到的是「你的请求超过自助上限,已转人工,24 小时内响应」。把原因码与文案分开,才能既做统计又不泄漏内部规则。

overdueRequests 只把人工请求计入超时。自助请求的时限是 0,但它没有「超时」的概念,因为它在请求返回时就已经完成。把自助请求算进超时率,会让平台团队误以为自己在积压。

selfServiceRatio 的分母是全部请求,不是人工请求。这个口径很重要:如果分母只算人工请求,自助率永远是 100%;只有把自助与人工放在一起,才能看出边界划得够不够宽。

示例样本上的运行结果

用 10 个请求跑一遍:4 个自助、6 个人工,自助覆盖率 40%。示例时间从 0 开始,所有数字都是示例样本上的运行结果。

请求路由原因截止时间
查看账单自助read-only0
开通摘要 standard自助approved-catalog0
份额调到 800 万自助within-ceiling0
公开语料评测 5,000 微元自助public-or-tenant-corpus0
开通 chat.reply frontier人工tier-requires-review240 小时
份额调到 1,200 万人工above-self-service-ceiling24 小时
开通未准入能力人工capability-not-approved240 小时
受限语料评测人工restricted-corpus24 小时
跨租户借用人工cross-tenant-transfer24 小时
合规例外人工compliance-exception120 小时

这张表里有两个容易误读的地方。第一,份额 1,200 万被拒不是因为租户没钱,租户还剩 6,000 万,而是因为它超过了单团队 1,000 万的自助上限;平台要的是「大额调整必须有人看一眼」,不是「没钱了才找人」。第二,受限语料评测被拒不是权限不足,而是可见范围需要人工确认;同一个评测集,换成公开语料就能自助。

如果再加入两个请求——新能力上线与份额超过租户余额——人工数会变成 8,自助覆盖率降到 33.33%。这说明自助覆盖率不是一个越高越好的指标:它只描述当前请求结构,不能单独作为平台团队的 KPI。真正要看的,是「人工请求里有多少本来可以自助」以及「自助请求里有多少事后被回滚」。

自助界面的三个护栏

边界划好之后,界面还要防止「合法但错误」的操作。平台至少要给三个护栏。

第一是预演。调整配额前,界面要显示调整后的三层余额、受影响的链路与预计可支撑的调用次数。回声的例子是:内容团队把份额从 4,000 万调到 1,000 万,摘要单次 6,200 微元,预计可支撑 16,129 次;如果团队日均调用 20,000 次,界面应该直接提示「按当前用量只够 0.8 天」。没有预演,用户只能通过被拒绝来学习。

第二是二次确认。不是所有自助操作都要弹窗,只有会改变成本或质量承诺的操作需要:切换档位、调整份额、开启新能力。确认框里要写清楚「这次改动从什么时候生效、影响哪些链路、如何回滚」,而不是一句「确定吗」。

第三是变更审计。每一次自助变更都要记录操作者、时间、旧值、新值与生效范围。审计日志不是给合规看的摆设,它是事后回答「为什么这条链路突然变贵」的唯一证据。没有审计,自助就会变成不可解释的变更。

三个护栏合起来,才让「自助」不等于「失控」。自助服务的目标不是减少人工,而是把人工集中到真正需要判断的地方;护栏的作用是让不需要判断的操作也不容易出错。

与配额、能力目录的衔接

自助边界不是孤立的。它依赖两个已有资产:能力目录与三层配额。

能力目录提供「准入」的事实:哪些能力可以开通、每个能力的默认档位、可覆盖参数与计费单位。没有目录,平台无法判断「申请能力」是否越界,只能靠人工记忆。

三层配额提供「额度」的事实:租户总额、团队份额、单次上限。没有配额,平台无法判断「配份额」是否超过租户余额,也无法在预演里算出「够跑多少次」。

反过来说,自助边界也给这两个资产提出要求。目录必须把「自助可开通的档位」写成字段,而不是散落在文档里;配额必须暴露「租户当前剩余」的只读接口,否则界面无法做预演。治理资产之间不是并列关系,它们互相提供判定所需的事实。

什么时候应该收紧边界

边界不是一次划完就固定。出现下面三种信号时,平台应该收紧自助范围。

第一种,同一类自助操作在短期内被回滚多次。说明用户并不理解它的后果,或者默认值不合理。先修默认值与预演,再考虑收紧;如果修完仍然回滚,就转人工。

第二种,自助变更导致了跨租户影响。比如一个团队把份额调高,挤占了另一个团队的借用额度。这说明自助上限没有考虑租户层的联动,应该把「租户剩余」纳入判定,而不是简单调低上限。

第三种,合规或安全团队对某类语料提出新的可见性要求。这类变化通常来自外部,不是平台能预测的;正确动作是把对应评测请求直接转为人工,并在目录里标记语料级别,而不是加一个「承诺不泄漏」的复选框。

收紧边界时要注意可解释性。用户必须知道「为什么昨天可以、今天不行」,否则他会认为平台在随意变更规则。每一次收紧都要同时更新原因码、界面文案与时限表,让规则可查、可复现。

小结

自助服务的边界由四个条件决定:能力在准入目录、档位开放、额度在上限内、语料不受限。四类请求可以自助,三类请求必须人工;人工请求按风险给出 24 小时、5 个工作日与 10 个工作日三档时限,超时只升级、不自动批准。

示例样本的 10 个请求里,4 个自助、6 个人工,自助覆盖率 40%。这个数字不是越高越好,它只描述请求结构;真正要盯的是人工请求里有多少本可自助,以及自助变更的回滚率。

阅读导航:上一节:9.3 事故响应与披露 · 下一节:10.2 SDK、CLI 与配置分发 。

常见错误与边界

  • 把自助理解成「所有按钮都放开」。没有准入、档位、额度与语料四个条件,自助会变成风险放大器。
  • 把人工理解成「都走工单」。只读的账单、目录查询也走工单,平台团队会被无意义的请求淹没。
  • 只按操作复杂度分类。合规例外操作很简单,但后果不可逆,必须人工。
  • 审批没有时限。工单会退化成邮件列表,业务方无法预期。
  • 超时自动批准。时限是响应承诺,不是风险豁免;超时只能升级。
  • 自助上限只看团队份额,不看租户剩余。会配置出账面上不存在的额度。
  • 把 frontier 当成普通档位开放。成本与限流风险会在没有容量评估时被放大。
  • 自助界面没有预演。用户只能通过被拒绝来学习规则。
  • 变更没有审计。事后无法回答「这条链路为什么突然变贵」。
  • 用自助覆盖率做唯一 KPI。它会奖励「把风险操作伪装成自助」,而不是奖励「把边界划对」。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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