以下情境为教学假设,人物与数字不代表经核实的真实案例。
风险登记册最常见的写法,是把所有担心的事列成一张清单:客户会流失、云会宕机、数据会泄露、平台会改规则。清单越长,越像在安慰自己——至少我写下来了。但它不能回答一个更实际的问题:这些事里,哪一件值得先花钱?
要让登记册变成决策工具,每一行必须能算出一个数。这个数就是期望损失:概率乘以影响。它不预测哪一天出事,也不保证一年只出一次,但它把「可能很糟」变成「一年平均要为此准备多少钱」。有了这个数,你才能把有限的现金、时间和保险预算按顺序分配。
五个字段:风险、概率、影响、责任人、缓解
登记册的一行至少要有五个字段。少一个,这一行就会退化成一句口号。
| 字段 | 写什么 | 不合格的写法 | 合格的样子 |
|---|---|---|---|
| 风险 | 一个具体事件,不是一类情绪 | 客户关系不稳定 | 最大客户在续约日不续 |
| 概率 | 档位加教学 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 | 创始人 |
| Tidecast | Steam 依赖 | 第 9、10 章渠道与依赖 | 低 5% | $110,537 | $5,527 | 创始人 |
| Tidecast | IP 侵权 | 第 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,240 | 16.4% |
| Tidecast | $26,527 | $121,171 | 21.9% |
| Nord | $17,008 | $34,320 | 49.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 业务连续性计划 。
常见错误与边界
- 把登记册写成担忧清单。 没有概率、影响金额和责任人的行,不能参与排序;它只是情绪记录。
- 概率用精确百分比。 一人公司没有足够样本;用 7.3% 这样的数字会制造虚假精度,档位加教学 bps 更诚实。
- 影响金额拍脑袋。 影响要能从一个案例数字复算;写「损失很大」等于没写。
- 只按概率排序。 低概率、高影响的事件可能排第一;LedgerLite 的责任索赔就是例子。
- 只看绝对金额。 Nord 的期望损失合计最低,但占 SDE 49.6%;不归一化会低估它的优先级。
- 责任人写「团队」。 一人公司里「团队」就是「没人」;写名字才能让缓解措施有人执行。
- 缓解措施不可验证。 「加强管理」不改变公式里的任何变量;要写能降低概率、影响或时长的具体动作。
- 把期望损失当成预算。 它是长期平均成本,不是今年要留出的现金;现金准备要看最坏情况和 runway。
- 忽略风险相关性。 云宕机与数据泄露可能同时发生;单行相加会低估组合损失。
- 用代码替代判断。 代码保证算术一致;档位是否合理、影响是否完整,仍然要人来判断。
本节的边界是「单行风险的期望损失与排序」:只覆盖概率、影响、责任人、缓解四个变量,不展开风险相关性、情景分析和保险定价。所有概率 bps 和 15% 的泄露成本比例都是教学假设;具体案件的损失因行业、辖区和合同而异,本节不构成法律或保险意见。风险如何进入业务连续性计划,见 12.2 业务连续性计划 ;保险、备份与预案如何互补,见 12.3 保险、备份与应急预案的联动 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。