《独立开发者的商业指南·合同、应收与风险》12.1 风险登记册:概率、影响、责任人

风险登记册不是一张担忧清单,而是一张把概率、影响金额、责任人和缓解措施写在一起的定价表。本节用低、中、高三档概率和期望损失公式,把 LedgerLite、Tidecast、Nord 前 11 章的风险收进同一张表,算出三家的期望损失合计分别占 SDE 的 16.4%、21.9% 和 49.6%,并给出排序。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

风险登记册最常见的写法,是把所有担心的事列成一张清单:客户会流失、云会宕机、数据会泄露、平台会改规则。清单越长,越像在安慰自己——至少我写下来了。但它不能回答一个更实际的问题:这些事里,哪一件值得先花钱?

要让登记册变成决策工具,每一行必须能算出一个数。这个数就是期望损失:概率乘以影响。它不预测哪一天出事,也不保证一年只出一次,但它把「可能很糟」变成「一年平均要为此准备多少钱」。有了这个数,你才能把有限的现金、时间和保险预算按顺序分配。

五个字段:风险、概率、影响、责任人、缓解

登记册的一行至少要有五个字段。少一个,这一行就会退化成一句口号。

字段写什么不合格的写法合格的样子
风险一个具体事件,不是一类情绪客户关系不稳定最大客户在续约日不续
概率档位加教学 bps有可能低,5%,500 bps
影响一次事件的现金损失损失很大12 个月费用上限 240,240
责任人一个能采取行动的人团队创始人(基础设施)
缓解降低概率或影响的具体动作加强管理热备、DPA、保险背书

概率用档位而不是精确百分比,是因为一人公司没有足够的事件样本。你不可能用过去三年的数据算出「数据泄露概率是 7.3%」,但你可以判断它属于低、中、高哪一档。为了让档位能参与计算,本书给三档各配一个教学 bps:

档位教学概率bps直观含义
低5%500大约二十年一遇,或从未发生过但行业里有先例
中15%1,500大约六七年一遇,或过去五年见过一次
高30%3,000大约三年一遇,或过去两年见过一次

这三个 bps 是教学假设,不是统计结果,也不适用于所有行业。它们的用处是让「低/中/高」能进入同一个公式,而不是让你把 5% 当成精确预报。

公式只有一行:

期望损失 = 影响金额 × 概率

以 LedgerLite 的责任索赔为例:公司层面 12 个月费用上限是 $240,240,概率档位取低(5%),期望损失就是 $240,240 × 5% = $12,012。这个数不是说你今年要准备 $12,012 现金,而是说在很长一段时间里,这类事件的年平均成本大致在这个量级。它要和其他风险排在一起看,才有意义。

责任人这一栏最容易被一人公司跳过:反正都是我。但写名字仍然有用。写「创始人」意味着这件事没有第二个人能接手;写「合伙人 A」意味着另一个人知道触发条件。Nord 有两位合伙人,同一个风险如果只写「团队」,出事时两个人都会以为对方在管。责任人的作用不是追责,而是让缓解措施有一个明确的执行者。

缓解措施要能改变公式里的某一个变量,而不是一句态度。买保险改变的是影响金额(把赔付转移给保险人);热备改变的是停机时长,从而改变影响;代码审计和权限收窄改变的是概率。写「加强安全意识」不算缓解,写「季度权限复核 + 离职当天回收账号」才算。

把前 11 章的风险收进一张表

卷二前 11 章分别从合同、应收、保险、IP、渠道、依赖、数据条款的角度拆过风险。把它们收进一张表时,只保留能在案例事实里找到金额依据的条目。下面十条覆盖三家最主要的敞口,影响金额的推导写在表后。

案例风险来源概率影响金额期望损失责任人
LedgerLite责任索赔第 2 章责任上限低 5%$240,240$12,012创始人
LedgerLite数据泄露第 11 章 DPA 与安全中 15%$36,036$5,405创始人
LedgerLite客户集中第 7 章集中度中 15%$21,622$3,243创始人
LedgerLite云依赖第 10 章关键依赖中 15%$4,671$701创始人
Tidecast爆款失败第 8 章授权与续作中 15%$120,000$18,000创始人
TidecastSteam 依赖第 9、10 章渠道与依赖低 5%$110,537$5,527创始人
TidecastIP 侵权第 7 章 IP 组合低 5%$60,000$3,000创始人
Nord客户集中 48%第 9 章集中度中 15%$74,074$11,111合伙人 A
Nord关键人依赖第 5 章服务合同中 15%$28,050$4,208合伙人 A
Nord单一大额应收第 3、4 章应收与坏账中 15%$11,264$1,690合伙人 B

每一行的金额都能从案例事实复算:

  • LedgerLite 的责任索赔取公司层面 12 个月费用上限 $240,240。这是最坏情况的上限,不是单客户上限;单客户年费只有 $924。
  • LedgerLite 的数据泄露取年收入的 15%,即 $240,240 × 15% = $36,036。15% 是教学假设,用来代表通知、取证、信用监测和客户补偿的组合成本;它不是行业统计值。
  • LedgerLite 的客户集中取最大一家客户的年收入:9% × $240,240 = $21,622。
  • LedgerLite 的云依赖取 7 天停机(教学假设)对应的 MRR:$20,020 × 7 ÷ 30 = $4,671。
  • Tidecast 的爆款失败取续作预算 $120,000;这笔钱在失败时已经花掉,收不回来。
  • Tidecast 的 Steam 依赖取 Steam 渠道净得的全年金额:$138,171 × 80% = $110,537。
  • Tidecast 的 IP 侵权取一年授权保底 $60,000;如果侵权导致授权终止,至少这一年的保底收入消失。
  • Nord 的客户集中取前 3 家客户的年收入:48% × $154,320 = $74,074。
  • Nord 的关键人依赖取一位合伙人缺席 6 个月、咨询收入减半的损失(教学假设):$112,200 × 50% × 50% = $28,050。
  • Nord 的单一大额应收取年末应收中的单笔大额:7.3% × $154,320 = $11,264。

把每家的期望损失相加,再除以各自的 SDE,得到的是「风险成本占 owner earnings 的比例」:

案例期望损失合计SDE占 SDE
LedgerLite$21,361$130,24016.4%
Tidecast$26,527$121,17121.9%
Nord$17,008$34,32049.6%

这张表比绝对金额更有信息量。Nord 的期望损失合计只有 $17,008,是三家最低;但它占 SDE 的 49.6%,是三家最高。原因是 Nord 的 SDE 只有 $34,320,任何一笔上万美元的损失都会吃掉半年利润。LedgerLite 的期望损失合计 $21,361 比 Nord 高,但只占 SDE 的 16.4%,因为它有 $130,240 的 SDE 作为缓冲。

排序不是概率排序

登记册按期望损失从高到低排序,直觉上会让人以为「概率高的排前面」。实际结果恰好相反:三家里排第一的风险,有两家是低概率事件。

LedgerLite 排第一的是责任索赔,概率只有 5%,但影响金额是 $240,240,期望损失 $12,012。排第二的数据泄露概率 15%,影响 $36,036,期望损失 $5,405。责任索赔的概率只有数据泄露的三分之一,期望损失却是它的 2.2 倍。这就是「低概率、高影响」在公式里的样子:概率低不等于可以往后放,只要影响足够大,它仍然排第一。

Tidecast 排第一的是爆款失败,概率 15%,影响 $120,000,期望损失 $18,000,占 SDE 的 14.9%。排第二的 Steam 依赖概率只有 5%,期望损失 $5,527。爆款失败之所以排第一,不是因为它更可能发生,而是因为它一旦发生,续作预算全部沉没。Steam 依赖的影响金额更大($110,537),但概率更低,期望损失反而只有爆款失败的三分之一。

Nord 排第一的是客户集中,期望损失 $11,111,占 SDE 的 32.4%。这一条单独就吃掉了近三分之一的 owner earnings。排第二的是关键人依赖 $4,208,排第三的是单一大额应收 $1,690。Nord 的风险结构是「一个客户 + 一个人」:前 3 家客户占 48%,两位合伙人各占 50%,两者叠加时,一次客户流失或一次合伙人长期缺席就足以让当年 SDE 归零。

排序之后还要做一次归一化。绝对金额告诉你「这件事值多少钱」,占 SDE 的比例告诉你「这件事值不值得用一年的自由现金流去防」。同样的 $12,012,对 LedgerLite 是 9.2% 的 SDE,对 Nord 是 35.0% 的 SDE;同一个数字,在不同体量的生意里是完全不同的优先级。

写成代码:risk-register.ts

下面的 risk-register.ts 把三档概率写成 BAND_BPS,expectedLoss 只做一次乘法,buildRegister 负责校验、排序、求和,并用 divide 把总额换算成 SDE 的 bps。排序时用输入下标做第二排序键,保证期望损失相同的两行不会在每次运行时换位置。

// file: src/contract/risk-register.ts
// tier: L1
// verify: node --test test/contract/risk-register.test.ts

import { type Cents, add } from "../core/money.ts";
import { type Bps, applyRate, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";

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

export type RiskEntry = {
  name: string;
  band: ProbabilityBand;
  impactCents: Cents;
  owner: string;
};

export type RegisterRow = {
  name: string;
  band: ProbabilityBand;
  probabilityBps: Bps;
  impactCents: Cents;
  expectedLossCents: Cents;
  owner: string;
};

export type RegisterSummary = {
  rows: RegisterRow[];
  totalExpectedLossCents: Cents;
  sdeCents: Cents;
  ratioBps: Bps;
};

/** Probability bands are teaching assumptions, not measured frequencies. */
export const BAND_BPS: Record<ProbabilityBand, Bps> = {
  low: 500,
  medium: 1500,
  high: 3000,
};

function isBand(value: string): value is ProbabilityBand {
  return value === "low" || value === "medium" || value === "high";
}

function validAmount(value: Cents): boolean {
  return Number.isInteger(value) && value >= 0;
}

/**
 * Expected loss is probability times impact. The band is converted to basis
 * points first, so the result is an integer number of cents.
 */
export function expectedLoss(
  impactCents: Cents,
  band: ProbabilityBand,
): Result<Cents> {
  if (!validAmount(impactCents)) {
    return fail("impact must be a non-negative integer amount in cents");
  }
  if (!isBand(band)) {
    return fail("band must be low, medium or high");
  }
  return ok(applyRate(impactCents, BAND_BPS[band]));
}

/**
 * Build a register ordered by expected loss, highest first. Ties keep the
 * input order so the output is stable across runs. The summary divides the
 * total by SDE, which turns a list of risks into a share of owner earnings.
 */
export function buildRegister(
  sdeCents: Cents,
  entries: RiskEntry[],
): Result<RegisterSummary> {
  if (!validAmount(sdeCents) || sdeCents === 0) {
    return fail("SDE must be a positive integer amount in cents");
  }
  const rows: RegisterRow[] = [];
  for (const entry of entries) {
    if (entry.name.trim() === "") {
      return fail("risk name cannot be empty");
    }
    if (entry.owner.trim() === "") {
      return fail(`${entry.name} needs an owner`);
    }
    const loss = expectedLoss(entry.impactCents, entry.band);
    if (!loss.ok) {
      return loss;
    }
    rows.push({
      name: entry.name,
      band: entry.band,
      probabilityBps: BAND_BPS[entry.band],
      impactCents: entry.impactCents,
      expectedLossCents: loss.value,
      owner: entry.owner,
    });
  }

  const ordered = rows
    .map((row, index) => ({ row, index }))
    .sort(
      (a, b) =>
        b.row.expectedLossCents - a.row.expectedLossCents || a.index - b.index,
    )
    .map((entry) => entry.row);

  const totalExpectedLossCents = ordered.reduce(
    (acc, row) => add(acc, row.expectedLossCents),
    0,
  );

  return ok({
    rows: ordered,
    totalExpectedLossCents,
    sdeCents,
    ratioBps: divide(totalExpectedLossCents, sdeCents),
  });
}

把三家的数字换成美分代进去:

  • LedgerLite:SDE 13,024,000 美分,四条风险的期望损失合计 2,136,135 美分,即 $21,361.35,占 SDE 1,640 bps,也就是 16.4%。排序结果是责任索赔、数据泄露、客户集中、云依赖。
  • Tidecast:SDE 12,117,100 美分,合计 2,652,685 美分,即 $26,526.85,占 2,189 bps,也就是 21.9%。排序结果是爆款失败、Steam 依赖、IP 侵权。
  • Nord:SDE 3,432,000 美分,合计 1,700,820 美分,即 $17,008.20,占 4,956 bps,也就是 49.6%。排序结果是客户集中、关键人依赖、单一大额应收。

代码只做三件事:把档位换成 bps、把概率乘上影响、把结果排序并汇总。它不判断概率档位是否合理,也不处理风险之间的相关性。云宕机可能同时触发数据泄露,客户集中可能同时放大应收坏账;两条风险同时发生时,损失不是两个期望损失相加,而是两个影响金额相加。登记册是单行工具,相关性要在情景分析里单独做。

小结

  • 风险登记册的一行有五个字段:风险、概率档位、影响金额、责任人、缓解措施;少一个就会退化成口号。
  • 概率用低、中、高三档,教学 bps 分别是 500、1,500、3,000;它们是让档位能参与计算的假设,不是统计预报。
  • 期望损失 = 影响金额 × 概率;LedgerLite 的责任索赔是 $240,240 × 5% = $12,012。
  • 前 11 章的风险可以收进一张十行的表;三家的期望损失合计分别是 $21,361、$26,527、$17,008。
  • 占 SDE 的比例比绝对金额更有信息量:LedgerLite 16.4%、Tidecast 21.9%、Nord 49.6%。
  • 排序按期望损失,不按概率;低概率、高影响的责任索赔在 LedgerLite 排第一。
  • 归一化后再决定优先级:同样的 $12,012,对 LedgerLite 是 9.2% SDE,对 Nord 是 35.0% SDE。
  • 代码只负责乘法、排序和汇总,不处理风险相关性;相关性要放进情景分析。

阅读导航:上一节:11.3 客户审查与安全问卷 · 下一节:12.2 业务连续性计划 。

常见错误与边界

  1. 把登记册写成担忧清单。 没有概率、影响金额和责任人的行,不能参与排序;它只是情绪记录。
  2. 概率用精确百分比。 一人公司没有足够样本;用 7.3% 这样的数字会制造虚假精度,档位加教学 bps 更诚实。
  3. 影响金额拍脑袋。 影响要能从一个案例数字复算;写「损失很大」等于没写。
  4. 只按概率排序。 低概率、高影响的事件可能排第一;LedgerLite 的责任索赔就是例子。
  5. 只看绝对金额。 Nord 的期望损失合计最低,但占 SDE 49.6%;不归一化会低估它的优先级。
  6. 责任人写「团队」。 一人公司里「团队」就是「没人」;写名字才能让缓解措施有人执行。
  7. 缓解措施不可验证。 「加强管理」不改变公式里的任何变量;要写能降低概率、影响或时长的具体动作。
  8. 把期望损失当成预算。 它是长期平均成本,不是今年要留出的现金;现金准备要看最坏情况和 runway。
  9. 忽略风险相关性。 云宕机与数据泄露可能同时发生;单行相加会低估组合损失。
  10. 用代码替代判断。 代码保证算术一致;档位是否合理、影响是否完整,仍然要人来判断。

本节的边界是「单行风险的期望损失与排序」:只覆盖概率、影响、责任人、缓解四个变量,不展开风险相关性、情景分析和保险定价。所有概率 bps 和 15% 的泄露成本比例都是教学假设;具体案件的损失因行业、辖区和合同而异,本节不构成法律或保险意见。风险如何进入业务连续性计划,见 12.2 业务连续性计划 ;保险、备份与预案如何互补,见 12.3 保险、备份与应急预案的联动 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查