HDD: Hypothesis-Driven Development
七步流程:Step 0 → HT → PT → AT Design → DEV → AT Run → E2E
每个测试阶段的核心动作相同:写测试用例 → 执行 → 判定通过/不通过。 信息收集(查数据、搜网、读文档、跑命令)是写测试用例的输入,不是判定依据。
流程
0. Resource Mapping(资源映射)— 动手前必做
最底层的错误假设,是假设了自己的能力边界而没有去核实。
在提任何技术假设之前,先回答:"我有哪些资源可以帮我解决这个问题?"
- 自身能力:
- 平台工具、权限、配置: [我能直接做什么?有没有已有能力可以用?]
- .env / credentials: [有没有未使用的认证或服务?]
- 本地知识:
- 项目文档 / HANDOFF: [有没有已规划但未实现的方案?]
- 已有代码/配置/脚本: [有没有相关但未启用的能力?]
- memory / 文件系统: [之前讨论过这个问题吗?已有什么相关资料?]
- 外部资源:
- 官方文档: [涉及的 API/工具有没有文档?先读完再动手]
- 社区/搜索: [有没有已知的最佳实践?可以存为 reference 的现成方案?]
为什么是 Step 0: 如果跳过资源映射直接提假设,你会在错误的解空间里浪费时间——用笨办法绕过一个根本不存在的限制。
1. HT: Hypothesis Test(假设验证)
假设在被验证之前,无论多笃定,都只是假设,应默认大概率不为真。 验证必须基于可观测事实,不基于推理或反思。
1.1 提出假设 & 赋先验
- 假设: [对问题根因/方案可行性的判断]
- 类型: 事后诊断 / 事前规划
- 先验: P(H) = [0.0-1.0],依据: [参考类别]
先验赋值参考(Reference Class Forecasting):
| 假设基础 | 先验范围 | 理由 |
|---|---|---|
| 全新假设,无先例 | 0.05-0.15 | 没有任何支撑 |
| 基于已知模式/文档推断 | 0.2-0.4 | 有模式但未在此验证 |
| 有直接类比案例 | 0.4-0.6 | 类似条件下成立过 |
| 有机制解释+类比证据 | 0.6-0.75 | 机制已知且有先例 |
铁律:主观信心 ≠ 先验概率。 "我觉得肯定是"不构成贝叶斯更新。
1.1b 中立性检查
- 暂停修复思维:HT 阶段禁止同时构思修复方案——这会污染验证的中立性
- 预写证伪条件:写验证测试前,先写"看到什么则假设为假",与证实条件同等具体
- 证伪 = 排除一条路:正确反应是"好,少了一个可能",然后换下一个假设
1.2 自我攻击
- 攻击 Ground Truth:我的判断依据本身有没有破绽?有没有反例?
- 检查其他可解释性:同一现象是否有完全不同的解释?(问题可能不是"怎么做 X"而是"需不需要 X")
- 找反例:已有事实中,有没有直接反驳假设的证据?
三项检查不通过 → 假设不成立,重新提假设。
1.3 证据等级
| 等级 | 名称 | 定义 | 贝叶斯更新力 |
|---|---|---|---|
| V1 | 现场复现 | 在受控条件下让事件再次发生,直接观测结果 | 极高(P(E|H)≈1, P(E|¬H)≈0) |
| V2 | 证据链闭合 | 找到实际发生过的直接痕迹,推导链连续、完备、排他 | 高 |
| V3 | 证据链部分闭合 | 有痕迹和推导,但存在缺口或未排除替代解释 | 中 |
| V4 | 类比证据 | 类似条件下的类似事件有记录,但非当前环境直接痕迹 | 低 |
| V5 | 纯推理/反思 | 想了想觉得说得通,逻辑自洽 | 零(不构成证据) |
V5 铁律:任何仅在思维层面的推理、反思、挑战,都不构成假设验证为真。假设依然为假设。
V2 证据链闭合三条件:
- 来源可追溯(不是"我记得大概是这样")
- 环环相连(A→B→C,不能跳 A→C)
- 排他性(排除了能解释同一现象的其他假设)
任一条不满足 → 降级为 V3。存在另一条同样闭合的证据链指向不同结论 → 两条都不算 V2。
1.4 验证路径(按假设类型分流)
事后诊断(问题已发生,确认根因):
- 刚性标准:必须达到 V2 或 V1
- 推荐路径:先 V2(找痕迹闭合证据链),再 V1(复现确认)
- V3 及以下:假设仍为假设,不能用于指导 DEV
- 信息收集: [查日志/文件/状态 — 寻找直接痕迹]
- 证据链:
- 痕迹1: [在哪发现的,说明了什么]
- 痕迹2: [...]
- 推导: 痕迹1 → 痕迹2 → 结论
- 排他性: [是否排除了替代解释?如何排除?]
- 复现测试(如条件允许):
- 操作: [如何触发]
- 预期: [如果假设为真应观测到什么]
- 实际: [观测结果]
- 达到证据等级: V1 / V2 / V3
事前规划(事件尚未发生,确认机制可行性):
整体事件尚未发生,不可能直接 V1/V2。 核心方法:把机制拆成因果链,对每个环节单独验证。 事前的 V1 不是对最终结果的复现,而是对每个因果箭头的微型复现。
刚性标准(必须满足,否则假设不可用于决策):
- 可证伪性:假设必须有明确的"如果看到X,则假设为假"。不可证伪的假设不是假设,是信仰
- 预注册:成功/失败标准必须在执行前锁定,不允许事后调整标准适配结果(这是 AT Design 在 DEV 之前的底层理由)
- 机制分解与逐环验证:拆成因果链 A→B→C→D,每个箭头必须满足以下之一:
- 有直接类比证据(过去相同环节在相似条件下成立过)
- 可通过 spike/原型直接测试(对该环节做微型 V1)
- 有公认理论/文档支撑(物理定律级确定性)
- 任何一环不满足 → 标记为"未验证环节" → 整体降级为"有条件假设"
- 最高风险环节优先验证:先验最接近 0.5 的环节,信息增益最大,优先做 spike
建议标准(不强制,但显著提高假设质量):
- 类比审判:搜索历史上类似方案的成败记录,提供 base rate 锚定
- 事前尸检(Pre-mortem):假设方案已失败,反推最可能的失败原因,每个原因作为子假设独立 HT
- 爆炸半径评估:假设为假时的损失决定验证力度
| 爆炸半径 | 验证投入 | 例子 |
|---|---|---|
| 可逆,成本低 | 建议标准即可 | 换个 CSS 框架 |
| 不可逆,成本中 | 刚性标准全满足 | 选定数据库方案 |
| 不可逆,成本高 | 刚性标准 + 独立对照实验 | 架构重写 |
- 可证伪条件: [看到什么则假设为假]
- 预注册标准: [成功 = ?, 失败 = ?]
- 因果链分解:
- 环节A → B: 验证方式=[类比/spike/理论], 结果=[✅/❌/未验证]
- 环节B → C: 验证方式=[...], 结果=[...]
- 环节C → D: 验证方式=[...], 结果=[...]
- 未验证环节: [列出]
- 最高风险环节: [哪个], spike 结果: [...]
- 爆炸半径: 可逆低成本 / 不可逆中成本 / 不可逆高成本
- 达到证据等级: 每环 V? → 整体 V?
1.5 判定
- 直接观测到了什么: [列出具体的观测事实,不含推理。写不出来 → V5]
- 证据等级: V[1-5]
- 后验估计: P(H|E) = [更新后的概率]
- 判定:
- ✅ 验证通过(V1 或 V2)→ 进入 PT
- ⚠️ 有条件通过(V3,存在未闭合环节)→ 标注风险,谨慎进入 PT
- ❌ 验证未通过(V4/V5 或反证据)→ 替换假设,重跑 HT
关键规则:
- 不能提了假设就一直绕,必须先验证假设是否成立
- 假设不通过 → 必须替换假设,不能在错误假设上调方案
- V5(纯推理)永远不能作为通过依据
2. PT: Plan Test(方案验证)
提出方案,用测试用例验证可行性。
- 方案: [具体方案描述]
- 信息收集: [查文档/搜网/确认可行性 — 为写测试服务]
- 测试用例:
- PT1: [方案前提是否成立] → 预期: [xxx] → 实际: [xxx] → ✅/❌
- PT2: [关键参数是否可用] → 预期: [xxx] → 实际: [xxx] → ✅/❌
- 判定: ✅ 进入 AT Design / ❌ 调整方案 / ❌❌ 回退 HT
回退规则:
- PT 失败但假设没问题 → 调整方案,重跑 PT
- PT 失败且发现假设有问题 → 回退 HT,重新提假设
3. AT Design: Acceptance Test Design(验收用例编写)
在写代码之前,先定义"做完长什么样"。
- 验收标准:
- AT1: [输入/操作] → 预期结果: [xxx]
- AT2: [边界条件] → 预期结果: [xxx]
- AT3: ...
关键规则:
- 必须在 DEV 之前完成
- 用例要具体可执行,不能是模糊描述
4. DEV: Development(开发)
基于 PT 方案 + AT 用例执行开发。
- 改动清单:
- [文件1]: [改了什么]
- [文件2]: [改了什么]
5. AT Run: Acceptance Test Run(验收测试执行)
执行 AT Design 中定义的用例。
- AT1: → 实际: [xxx] → ✅/❌
- AT2: → 实际: [xxx] → ✅/❌
- 判定: ✅ 进入 E2E / ❌ 回 DEV 修复
6. E2E: End-to-End Test(端到端验收)
由用户亲自验收。
- E1: [验收项] → ✅/❌
- E2: ...
- 判定: ✅ 完成 / ❌ 回退 DEV 或 PT
回退路径
HT ←────────────────────┐
↓ pass │
PT ←─────┐ │
↓ pass │ PT fail │ 根因在假设
AT Design│ │
↓ └→ 调整方案 ────┘
DEV
↓
AT Run ──→ fail → 回 DEV
↓ pass
E2E ────→ fail → 回 DEV 或 PT
↓ pass
Done ✅
收尾检查(E2E 通过后必做)
E2E 通过后,执行以下 checklist:
- [ ] 是否需要新建/更新项目文档?
- [ ] 是否有新的经验教训需要记录?
- [ ] 相关的 memory 文件是否已更新?
不满足任何一项可以跳过,但必须逐项过一遍再标记 Done。
何时使用
需要 HDD(满足任一):
- 涉及"为什么"(诊断/debug)
- 涉及"哪种方案更好"(优化/选型)
- 改动影响上下游(架构/配置变更)
- 结果不确定,需要验证
不需要 HDD:
- 指令明确、结果确定("把标题改成xxx")
- 纯信息查询("这个 API 怎么用")
- 简单追加/删除操作
信息收集指引
HT 和 PT 阶段都可能需要信息收集,作为写测试用例的输入:
- 查本地文件: 读配置、日志、源码
- 搜向量库: 查历史决策和上下文
- 搜网: 查文档、方案、已知问题
- 跑命令: 验证环境、版本、状态
信息收集不是目的,写出可执行的测试用例才是。
异常模式识别(Anomaly Detection)
HT 阶段之前,还有一个更根本的能力:从数据中发现异常。很多 bug 不是用户报告的,而是在做别的事时偶然注意到的。
看整体模式,不只看单条数据
单条结果对不对很难判断,但整体分布的异常一眼就能看出:
- 不自然的一致性:一个分类器把所有输入都归为同一类 → 输入可能是错的(分类逻辑错会随机错,但全部一样说明看到的是同一种东西)
- 不该出现的空值/默认值:某个字段全是 null 或 default → 上游数据没传进来
- 计数不对:预期 8 种分类结果但只看到 1 种 → 系统性问题
实战案例:
在一个消息分类系统中,观察 debug log 时发现所有消息(包括 "good"、"可以推进"、语音消息)全部被分为同一类别。分类器不可能把所有消息都归同一类——除非它根本没看到实际内容。这个观察直接改变了问题方向:从"加什么分类"变成"为什么分类器输入是错的"。
保持对"正常"的校准
要发现异常,首先要知道什么是正常。在系统运行初期:
- 主动检查几条典型输入的处理结果,建立基线认知
- 对关键环节保留日志/统计,方便后续发现偏移
- 定期抽查,不只是出了问题才看日志
调试技术(Debug Techniques)
HT/PT 阶段经常需要调试来验证假设。以下方法经过实战验证。
逐步打桩法(Step-by-Step Instrumentation)
当多步处理链输出不对时:
- 在每步之间插入 debug log(记录该步的输入和输出)
- 对比每步结果,定位第一个"输入对但输出错"的步骤
- 问题就在那一步的逻辑里
// 示例:在每步处理后加 debug
dbg(`after step1: "${result.slice(0, 120)}"`);
dbg(`after step2: "${result.slice(0, 120)}"`);
// → 发现 step3 输入正确但输出没变 → 问题在 step3 的逻辑
关键: 不要猜"可能是这步"然后直接改代码。打桩定位比猜测修复快得多,而且不会引入新问题。
模拟 vs 真实验证
- 本地模拟(
node -e/ 单元测试)可以快速验证逻辑 - 但模拟的输入格式可能与真实环境不同
- 模拟通过 ≠ 真实通过,必须在真实环境中再验证一次
- 真实与模拟结果不一致时 → 问题在输入格式假设上
实战教训: 本地模拟文本提取函数时用了多行文本(手动加了换行),regex 匹配成功;但真实输入是单行格式(没有换行),regex 中的 .*?\n 匹配失败。模拟给了"代码没问题"的假象。
格式假设检查
正则表达式和解析逻辑依赖对输入格式的假设(换行符、分隔符、字段顺序等)。当"逻辑对但不工作"时:
- 用
JSON.stringify()打出原始文本 → 暴露不可见字符、换行、编码 - 用
.length确认文本完整性 → 排除截断 - 用
.startsWith()/.includes()做简单断言 → 缩小范围 - 特别注意:单行 vs 多行、
\nvs\r\n、空格 vs tab
减少调试来回
真实环境测试往往需要重启服务 + 用户配合,周期长。为减少来回:
- 每次重启前多加几个 debug 点,一次收集尽可能多的信息
- 同时打印输入和输出,不要只打输出(看不出输入是否符合预期)
- 打印长度和关键特征(startsWith、includes),不只是截断内容
- 保留 debug log 到问题完全解决,过早清理可能需要重新加回来