以下情境为教学假设,人物与数字不代表经核实的真实案例。
复盘最容易写成两种东西:一种是给外人看的月报,数字很漂亮,但看完不知道要做什么;另一种是情绪日记,写了很多「感觉」「可能」「下次注意」,三个月后连当时为什么改价都想不起来。这一节只要一页纸:本期数字、与预算差异、三个原因、一个决策、下期验证点。它的目的不是汇报,是让未来的自己能够复算今天的选择。
复盘不是汇报,是留下可复算的决策
汇报回答「这个月发生了什么」,复盘回答「我们决定做什么,以及下个月怎么知道这个决定对不对」。两个问题的读者不同:汇报的读者是投资人、合伙人、客户;复盘的读者是三个月后的自己。三个月后的自己不会记得当时的焦虑,只会看到一行数字和一个决策;如果那一行没有证据、没有验证点,复盘就退化成故事。
一份有用的复盘记录要满足三个条件:
| 条件 | 含义 | 反例 |
|---|---|---|
| 数字可复算 | 每个数都能从结账包或银行流水追到 | 「收入大概涨了」 |
| 原因可证伪 | 每个原因都写了下个月什么情况会推翻它 | 「市场变好了」 |
| 决策可验证 | 有一个负责人、一个日期、一个指标 | 「继续优化产品」 |
三个条件对应一页纸的五栏:本期数字、与预算差异、三个原因、一个决策、下期验证点。五栏装不下,说明这个月的问题还没有收敛;应该先删到能装下,而不是把一页纸扩成十页。
复盘会的频率可以月度,也可以季度,但记录必须按月留。月度记录是季度复盘的原始材料;没有月度记录,季度复盘只能凭记忆,而记忆会偏向最近一次成功或失败。
一页纸模板:五栏
| 栏目 | 写什么 | 不写什么 | LedgerLite 十月示例 |
|---|---|---|---|
| 本期数字 | 五个指标及与上期/预算的对比 | 全部明细、所有客户名单 | MRR $20,020;净增 +$740(+3.8%);毛利率 84.2%;SDE 率 54.2%;经营现金流 $5,358(月均);零收入 runway 8.6 个月 |
| 与预算差异 | 金额、比率、方向、年初至今 | 「差不多」「略低」 | 本月 $0;年初至今 −$468(−0.78%),来自 Q1 新签 8 家而非 10 家 |
| 三个原因 | 事实、证据、可证伪 | 形容词、外部归因、超过三条 | 新签回到 10 家;扩张 +$310;流失 −$290 与 1.5% 一致 |
| 一个决策 | 做什么、谁、何时 | 愿望清单、三个以上并行 | 下月第一周排查前 5 家客户续约;负责人 founder;截止 2026-11-10 |
| 下期验证点 | 指标、目标、日期 | 「继续观察」 | NRR ≥ 100%;逾期 >30 天占应收 ≤ 6%;新签 ≥ 10 家 |
这张表里有两个数字需要说明。第一,「年初至今 −$468」沿用第 5 章的教学假设:Q1 每月新签 8 家而不是预算的 10 家,季度缺口 468 美元;十月本身回到预算,所以本月差异是 0,但全年仍有累计缺口。第二,前 5 家客户占收入 31%、最大一家 9%,这是 LedgerLite 的集中度事实;它不在五个指标里,却是十月决策的直接依据。
一页纸的写法有顺序:先填数字,再填差异,再写原因,最后写决策。顺序反了,就会先有结论再找数字。原因必须恰好三条,不是「至少三条」;三条装不下,说明还没有归因,应该继续合并,而不是继续列。
原因、决策与验证
三个原因不是三个借口,而是三个可被下个月推翻的假设。每个原因都要带三类信息:事实是什么、证据在哪里、什么情况会推翻它。
| 原因类型 | 写法 | 证据 | 可证伪条件 |
|---|---|---|---|
| 数量(volume) | 新签回到 10 家 | MRR 桥新签 +$780 | 下月新签低于 8 家 |
| 价格/结构(price/mix) | 扩张 +$310,来自存量升级 | MRR 桥扩张项 | 下月扩张低于 $200,或升级客户退回原档 |
| 时点(timing) | 流失 −$290,约 1.5%,集中在月末 | 流失清单与发生日期 | 下月流失率高于 2.5%,或流失提前到月初 |
LedgerLite 的十月复盘可以写成:
- 数量:新签 10 家、新增 MRR $780,回到预算节奏。证据是 MRR 桥的新签项;如果下月新签低于 8 家,这个原因不成立。
- 结构:扩张 +$310,来自存量客户升级。证据是扩张项;如果下月扩张低于 $200,说明这次扩张是一次性的,不是结构改善。
- 时点:流失 −$290、约 1.5%,与全年假设一致。证据是流失清单;如果下月流失率高于 2.5%,说明流失在加速,不是正常波动。
三条原因分别对应三种动作:数量问题改获客,结构问题改定价或套餐,时点问题改续约节奏。如果三条原因都写成「市场不好」,动作就只能是「等市场变好」,这不是复盘。
原因还要区分暂时与结构,这一点第 5 章的季度校准已经讲过。月度复盘的三个原因只需要回答「这个月为什么和预算不一样」,不需要立刻判断全年;暂时与结构的判断留到季度,但每条原因都要写下个月能验证的条件,否则季度复盘时没有材料。
一页纸只有一个决策。不是每个月只能做一件事,而是复盘记录只对一件事负责;其他事情照常做,但不进入决策留痕。决策要写三件事:做什么、谁负责、什么时候完成。缺一件,下个月就无法复算。
LedgerLite 十月的决策是「下月第一周排查前 5 家客户的续约」,负责人 founder,截止 2026-11-10。它对准的是集中度:前 5 家占收入 31%,最大一家 9%;这不是五个指标里的红灯,但它是 runway 和 NRR 的上游风险。决策不需要宏大,但必须能在两周内开始、在一个月内看到第一个结果。
下期验证点要写成「指标 + 目标 + 日期」,不能写成「继续观察」。LedgerLite 的三个验证点是:
| 验证点 | 目标 | 日期 | 判定 |
|---|---|---|---|
| NRR | ≥ 100% | 2026-11-30 | 低于 100% 说明存量在漏 |
| 逾期 >30 天占应收 | ≤ 6% | 2026-11-30 | 高于 6% 说明回款在恶化 |
| 新签 | ≥ 10 家 | 2026-11-30 | 低于 10 家说明数量原因不成立 |
下个月复盘的第一行,应该先读上个月的决策和验证点,再读本月的数字。顺序不能反:先看数字,会被本月的新鲜事吸引,忘记上个月承诺过什么。代码里的 nextReviewPrompt 就是干这件事的:它把上个月的决策和验证点拼成一句话,放在本期复盘的最前面。
决策的结果只有三种:命中、未命中、无法判定。
| 结果 | 含义 | 下月动作 |
|---|---|---|
| 命中 | 决策有效,验证点达标 | 保持,或扩大范围;不要因为一次命中就加码三倍 |
| 未命中 | 假设错,或执行不到位 | 改假设,不重复同一个动作;先分清是判断错还是没做 |
| 无法判定 | 验证点没测,或数据缺失 | 先补测量,再谈决策;无法判定本身就是流程问题 |
留痕的价值在第三种结果上最明显。没有记录,三个月后只会记得「当时好像做过一次续约排查」,无法判断它有没有用;有记录,就能看到当时的目标、日期和实际结果,决定要不要再做一次。
三案例的复盘差异
同一张一页纸,三个案例的复盘频率和重点不同:
| 项 | LedgerLite | Tidecast | Nord Studio |
|---|---|---|---|
| 复盘频率 | 月度 | 月度看现金流,季度看销量 | 月度,两位合伙人共同参加 |
| 主指标 | MRR 净变化、NRR、集中度 | 滚动三个月净得、退款率 | 咨询工时、产品 MRR、SDE 率 |
| 决策类型 | 续约、定价、获客 | 开发排期、折扣、续作预算 | 合伙人时间分配、招聘、服务产品配比 |
| 留痕重点 | 每月 MRR 桥与集中度 | 季节性假设与退款准备 | 两人的分工与退出条款 |
Tidecast 的月度数字波动大:12 月 2,400 份、1 月 480 份,Q4 占全年销量 38%。它的复盘不能因为 1 月销量低就改价,也不能因为 12 月销量高就扩产;正确做法是把季节性假设写进记录,用滚动三个月净得判断趋势,把退款率 4% 作为固定验证点。它的决策周期应该跟着开发里程碑走,而不是跟着自然月走。
Nord Studio 的复盘要同时看两条线:咨询 110 小时/月 × $85 = $9,350,产品 90 家 × $39 = $3,510;服务收入占 72.7%,咨询续约率 70%,产品月流失 3.0%。两条线的驱动不同,不能合成一个「总收入」决策。两位合伙人各 50%、没有书面退出条款,复盘记录还要留下两人对分工的共识;否则一次招聘或一次退出就会把过去一年的决策推翻。
三个案例的共同点是:复盘记录不是给外人看的,而是给下一次决策用的。LedgerLite 用它跟踪集中度,Tidecast 用它区分季节与趋势,Nord 用它固定合伙人的分工假设。记录的形式可以不同,但五栏不能少。
把复盘写成代码
下面的 review.ts 把一页纸结构化:buildReview 接收本期数字、预算、三个原因、一个决策和验证点,先算预算差异,再检查结构完整性;nextReviewPrompt 把上个月的决策和验证点拼成下一期复盘的第一行。
// file: src/fin/review.ts
// tier: L1
// verify: node --test test/fin/review.test.ts
import { type Cents } from "../core/money.ts";
import { type Result, fail, ok } from "../core/result.ts";
import {
type Variance,
type VarianceDirection,
measureVariance,
} from "./variance.ts";
export type ReasonKind =
| "volume"
| "price"
| "mix"
| "cost"
| "timing"
| "one_off";
export const REASON_KINDS: ReasonKind[] = [
"volume",
"price",
"mix",
"cost",
"timing",
"one_off",
];
export type Reason = {
kind: ReasonKind;
statement: string;
evidence: string;
falsifier: string;
};
export type Decision = {
statement: string;
owner: string;
dueDate: string;
};
export type Verification = {
metric: string;
target: string;
dueDate: string;
};
export type ReviewInput = {
period: string;
actual: Cents;
budget: Cents;
direction: VarianceDirection;
reasons: Reason[];
decision: Decision | null;
verification: Verification[];
};
export type ReviewRecord = {
period: string;
variance: Variance;
reasons: Reason[];
decision: Decision | null;
verification: Verification[];
missing: string[];
complete: boolean;
};
function filled(value: string): boolean {
return typeof value === "string" && value.trim().length > 0;
}
function checkDecision(decision: Decision): string | null {
if (!filled(decision.statement)) {
return "decision statement is required";
}
if (!filled(decision.owner)) {
return "decision owner is required";
}
if (!filled(decision.dueDate)) {
return "decision due date is required";
}
return null;
}
function checkVerification(item: Verification): string | null {
if (!filled(item.metric)) {
return "verification metric is required";
}
if (!filled(item.target)) {
return "verification target is required";
}
if (!filled(item.dueDate)) {
return "verification due date is required";
}
return null;
}
/**
* A review record is allowed to be incomplete. Structural errors fail, but a
* missing reason, decision or verification point is recorded in `missing`
* instead of being rejected. The draft is still worth keeping: next month it
* shows exactly what the review could not answer at the time.
*/
export function buildReview(input: ReviewInput): Result<ReviewRecord> {
if (!filled(input.period)) {
return fail("period is required");
}
if (!Array.isArray(input.reasons)) {
return fail("reasons must be an array");
}
if (!Array.isArray(input.verification)) {
return fail("verification must be an array");
}
for (const reason of input.reasons) {
if (!reason || !REASON_KINDS.includes(reason.kind)) {
return fail("unknown reason kind");
}
if (
!filled(reason.statement) ||
!filled(reason.evidence) ||
!filled(reason.falsifier)
) {
return fail("each reason needs a statement, evidence and falsifier");
}
}
const decision = input.decision ?? null;
if (decision !== null) {
const decisionError = checkDecision(decision);
if (decisionError) {
return fail(decisionError);
}
}
for (const item of input.verification) {
const verificationError = checkVerification(item);
if (verificationError) {
return fail(verificationError);
}
}
const variance = measureVariance(input.actual, input.budget, input.direction);
if (!variance.ok) {
return variance;
}
const missing: string[] = [];
if (input.reasons.length !== 3) {
missing.push("exactly three reasons");
}
if (decision === null) {
missing.push("one decision");
}
if (input.verification.length === 0) {
missing.push("at least one verification");
}
const record: ReviewRecord = {
period: input.period,
variance: variance.value,
reasons: input.reasons,
decision,
verification: input.verification,
missing,
complete: missing.length === 0,
};
return ok(record);
}
/**
* The first line of the next review. Its job is to force a re-computation of
* last month's decision, not to summarize the record.
*/
export function nextReviewPrompt(record: ReviewRecord): string {
if (record.decision === null) {
return "上一期没有决策,本期只做复算,不追责。";
}
const metrics = record.verification.map((item) => item.metric).join("、");
return `复算:${record.decision.statement};验证点:${metrics}。`;
}
代码有四个细节。第一,buildReview 允许保存草稿:原因不是三条、没有决策、没有验证点,都不会让函数失败,而是写进 missing;结构错误——未知原因类型、空证据、空负责人——才会失败。第二,预算差异复用第 5 章的 measureVariance,收入正向为有利、成本正向为不利,符号不会在本节重新定义。第三,验证点必须同时有指标、目标和日期,任何一项为空都会被判为结构错误;但「继续观察」这种空泛目标代码识别不了,只能靠模板和复盘习惯约束。第四,nextReviewPrompt 在没有决策时明确说「上一期没有决策」,而不是编一个;这句话本身就是留痕的一部分。
小结
- 复盘不是汇报,是给三个月后的自己留一份可复算的决策记录;数字可复算、原因可证伪、决策可验证,三条缺一不可。
- 一页纸五栏:本期数字、与预算差异、三个原因、一个决策、下期验证点;装不下说明问题还没收敛。
- 三个原因要带事实、证据、可证伪条件;LedgerLite 十月写成数量(新签 10 家)、结构(扩张 +$310)、时点(流失 −$290,约 1.5%)。
- 一个决策要有做什么、谁负责、何时完成;LedgerLite 的决策是下月第一周排查前 5 家客户续约,对准 31% 的集中度。
- 验证点写成指标 + 目标 + 日期;下月复盘先读上个月的决策,再读本月的数字。
- 决策结果分命中、未命中、无法判定;无法判定是流程问题,先补测量,再谈下一步。
阅读导航:上一节:10.2 月度关账流程 · 下一节:11.1 三种估值口径:ARR 倍数、SDE 倍数、DCF 。
常见错误与边界
- 把复盘写成月报。 月报给外人看,复盘给未来的自己看;没有决策和验证点,数字再漂亮也无法复算。
- 原因写成形容词。 「市场不好」「客户很喜欢」不是原因;原因要有证据和可证伪条件,否则下个月无法判断对错。
- 原因超过三条。 三条装不下,说明还没有归因;继续合并,而不是把一页纸扩成三页。
- 决策写成愿望。 「优化产品」「提升体验」没有负责人和日期;决策要能在两周内开始、一个月内看到第一个结果。
- 一个月做五个决策。 记录只对一件事负责;五个并行决策,下个月无法判断哪个起了作用。
- 验证点写成「继续观察」。 没有指标、目标和日期,就无法判定命中还是未命中;这等于没有验证。
- 先看本月数字,后看上个月决策。 新鲜事会吸引注意力,承诺会被忘记;顺序要反过来。
- 把命中当加码信号。 一次命中可能来自季节或运气;先确认验证点,再决定是否扩大范围。
- Tidecast 用单月销量做决策。 12 月 2,400 份、1 月 480 份,单月波动是季节,不是趋势;买断制用滚动三个月。
- Nord 把两条线合成一个决策。 咨询 72.7%、产品 27.3%,驱动不同;合成一个「总收入」会掩盖工时和流失的单独变化。
本章的边界是「一页纸的月度复盘」:它假设结账已经完成、五个指标口径稳定、决策可以在一个月内看到第一个结果。它不处理战略级决策、融资谈判和合伙人退出;那些需要更长的验证周期和更正式的文件。复盘的下一步是估值:当历史数字能复算、决策能验证,估值才有可比的基线,这正是第 11 章要用的材料。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。