从设计者角度理解源码·方法篇:机制、原型与提问协议
这是《从设计者角度理解源码》系列的一篇"元文章":它不讲某个模块怎么实现,只讲读源码这件事本身该怎么做。
案例全部来自同系列的 Hook 设计的两种答案——那篇给的是"填好的表",本篇给的是"怎么用这张表"。
引言
先讲一个很常见的困境。
你遇到了一个自己喜欢的框架,想认真学它的核心技术。但它暂时用不上——手上没有项目要用它,也不确定半年后会不会用。你只有周末,两天,可能还得留半天陪家人。
于是你打开源码,读了两小时,关掉,第二天想起来昨天读了什么——说不出来。
再过一周,你只记得"它有个 hook 机制,挺精巧的",但精巧在哪、为什么精巧、如果让你用你会不会用——全忘了。
这不是你不努力,是方法把一个"可解决的问题"变成了"不可解决的问题"。
本文想改的不是你的动力,是方法。核心主张只有一句:
道不是细节的替代品,道是细节的沉淀物。
你不能跳过细节去拿道,但你可以在每个细节上只追问三次,让道自己浮出来。
一、先分清你要哪一种"学会"
"学会一个框架"是一个含糊的说法。它至少是三种完全不同的目标,而三种的学法、跳过什么、退出条件都不一样。
| 目标 | 你想拿到什么 | 该学什么 | 该跳过什么 | 退出条件 |
|---|---|---|---|---|
| A. 将来要用 | 用得对 | 骨架 + 契约 + 配置 | 内部实现 | 能在一个真实场景里跑通 |
| B. 借鉴设计 | 判断力 | 约束 → 设计的推导 | 大部分实现 | 能说出 3 个取舍 + 2 个缺陷 |
| C. 满足好奇 | 爽 | 最精妙的那一处 | 其余全部 | 能讲给别人听 10 分钟 |
这三种最容易被混着来,于是产出最差:
- 用 A 的方式学 B:去配环境、跑 demo,跑通了但说不清为什么这样设计
- 用 C 的方式学 A:专挑最炫的部分看,结果该用的地方不会用
- 用 B 的方式学 C:非要归纳出体系,其实只是想看个爽
所以第一步不是打开源码,是写下一句话:"我这次是 A / B / C。"
五分钟,省一个周末。
二、为什么"只讲道"一定会失败
很多人(包括我自己)都有过这个念头:
"别让我钻细节,直接告诉我底层原理和设计思想,那些才是核心价值。"
这个念头有一半是对的——可迁移的价值确实在"为什么",不在"是什么"。27 个事件名一年后会忘,"拒绝必须单调"能记十年。
但它有一个致命的前提错误:"如果不这么做会怎样"这类问题,逻辑上无法脱离机制回答。
一个具体的例子
问:Claude Code 的 hook 为什么要用 exit code 2 表示阻断,而不是在 JSON 里写个 "blocked": true?
只讲道的版本:
因为要兼容简单脚本,降低门槛。
这句话你说它对,它确实对;但它推不出任何东西。你无法据此判断"那我的系统该不该也加一个退出码通道",因为你不知道"不加会怎样"。
讲机制的版本:
因为脚本可能崩在解析 JSON 之前。那一刻它连一行合法输出都没有,但操作系统一定记录了退出码。如果没有带外通道,宿主就分不清"它拒绝了"和"它挂了"——而这两种情况的处理方式完全相反。
这句话推出了一条可迁移的原则:结构化数据可能缺失,进程状态一定存在;所以关键判定要有带外通道。
同一个问题,两种答法,差别不在"深不深",在于"能不能用"。
结论:道不能跳过细节,道只能从细节里提炼。
想要判断力,就必须付出"看机制"的成本。区别只在于——你可以用更省的方式看。
三、三层粒度:决定、机制、原型
"机制"这个词被用烂了,因为它的粒度太模糊。一个系统里到底有几个"机制"?说三个也对,说三十个也对。
我后来用三个层级来卡住它:
| 层 | 是什么 | 数量 | 能不能独立学 |
|---|---|---|---|
| 决定(decision) | 一个具体选择 | 几十到上百 | ❌ 不值得单独记 |
| 机制(mechanism) | 一组决定,被同一条约束绑定,共同回答一个问题 | 每个子系统 3~6 个 | ✅ 能,一个周末 |
| 原型(prototype) | 跨系统反复出现的问题形态 | 8 个左右 | ✅ 这是真正该记的 |
三个校准测试
判断一个东西是"机制"还是"决定",用这三条:
| 测试 | 通过 → 是机制 | 不通过 → 是决定 |
|---|---|---|
| 能不能一句话说清它回答的问题? | "如何让任意语言的扩展者表达意图" | "用 zod 而不是手写校验" |
| 拆掉它,系统还成立吗? | 能成立,但明显变差 | 不成立(那是更底层的约束) |
| 它的几个选择会不会一起变? | 会(约束变了,整组都得改) | 只有一个会变 |
第三条最实用。 举个例子:
- "用 zod 校验" / "把
tool_input定义成z.unknown()" / "校验失败时把 schema 塞进错误信息" - 这三个会一起变吗?会。如果约束从"跨进程弱类型"变成"同进程强类型",它们会一起消失。
- → 所以它们属于同一个机制:契约校验。
再看反例:
- "工具类 hook 默认超时 10 分钟" / "SessionEnd 超时 1.5 秒"
- 这两个会一起变吗?不会。它们只是同一个机制的两个参数。
- → 所以它们是决定,不是机制。
一个真实的数量对比
用这个粒度回看 Claude Code 的 hook 系统。如果按"决定"数,能数出几十个;按"机制"数,只有 5 个:
| 观察点 | 归属的机制 |
|---|---|
| 多个 hook 冲突谁赢? | 裁决 |
| 怎么做到任何语言都能写? | 契约 |
| 崩了继续还是停下? | 失效 |
| 怎么不拖慢主流程? | 编排 |
| 怎么让用户/模型看到? | 观测 + 通道 |
| 怎么防止改写绕过批准? | 失效(TOCTOU)+ 裁决 |
6 个观察点 → 5 个机制。 数量立刻可控了。
这就是为什么"机制太多学不完"通常是个假问题——多数时候是数错了层,把决定当成了机制。
四、真假道:四条判别
机制之上还有一层"道"。但"道"这个词太容易变成正确的废话("要解耦"、"要有扩展性")。所以需要判别式。
| 真道 | 假道 | |
|---|---|---|
| 有约束来源吗 | 有(物理 / 协议 / 成本) | 没有,只有愿望 |
| 能推出反例吗 | 能("什么时候不成立") | 不能,永远成立 |
| 能预测没见过的设计吗 | 能 | 不能 |
| 有失效条件吗 | 有 | 没有 |
一条最狠的判据:如果你说不出"它在什么情况下不成立",那它就不是道,是口号。
一个旁证:成熟机制的名字,长成"问题的形状"
你注意过没有,真正被广泛复用的机制,名字几乎都是问题的形态:
| 名字 | 它回答的问题 |
|---|---|
| 幂等 | 同一个操作做两次怎么办? |
| 背压 | 消费者比生产者慢怎么办? |
| 熔断 | 下游一直失败怎么办? |
| 最终一致 | 分布式下不可能强一致,那退到什么? |
| fail-fast | 错误要不要尽早暴露? |
| 观察与决策分离 | 审计会不会不小心改了行为? |
没有人给机制起名叫 XXManager、XXHandler。 那些是实现的形状,不是问题的形状。
所以检验你有没有抓到机制,有个土办法:你能不能给它起一个"问题式"的名字。
如果你只能说"它里面有个 processHookJSONOutput 函数",那是技术点;如果你能说"它把所有执行器的输出归一成一个意图结构",那才是机制——因为"归一"是一个问题,不是一段代码。
五、八个原型
真正解决"太多"的,是最上面那层。
机制有两类:原型(prototype)和实例(instance)。原型只有 8 个,跨所有系统通用;实例则无穷多,每个框架各做一次。
下面这张表,就是那 8 个问题。每个管理系统扩展点的框架,都必须回答它们;框架之间的差别,只是答案不同。
| # | 原型 | 它回答的问题 | Claude Code | DeepSeek Harness |
|---|---|---|---|---|
| 1 | 契约 | 双方怎么交换信息? | 子进程 stdin/stdout 一行 JSON + 退出码;判别式联合;返回全 optional | 同进程 TS 函数 + next();类型声明合并;参数共享引用 |
| 2 | 路由 | 什么时候触发、触发谁? | matcher + if 预过滤(不匹配连进程都不起) | 插件装不装(部署期选择)+ 事件名 |
| 3 | 编排 | 多个执行者怎么并发/依赖? | all() 并行、无上限、谁先完成谁先到 | waterfall 严格串行、外层包内层 |
| 4 | 失效 | 坏了怎么办? | fail-open:崩溃/超时/取消/没跑都继续 | 抛异常直接关闭当前轮次 |
| 5 | 隔离 | 边界画在哪? | 子进程 + 工作区信任门 | 同进程 + 安装期信任(engines.dsh 版本检查) |
| 6 | 观测 | 怎么知道它在干嘛? | hook_started / hook_progress / hook_response 事件 + debug 日志 | tools/result 仅观测事件 + fiber 诊断 |
| 7 | 裁决 | 冲突时谁赢? | deny > ask > allow;allow 只能免弹窗 | 监听器顺序(prepend 可插队)+ ctx.tools.guard() 单调 |
| 8 | 演化 | 怎么加东西不破坏旧的? | 全 optional 字段 + 按 hookEventName 判别 + expectedHookEvent | interface Events 声明合并 + engines.dsh 兼容检查 |
怎么用这张表:
- 读第一个框架:把四列填满。填不满说明没读全;同一行填出好几个答案,说明还停在"决定"层。
- 读第二个框架:只填后两列。"和上一个差在哪"那一格,会自动变成你的判据。
- 读第三个框架:你会发现很多格子是重复的——重复的那几条,就是真道。
这也是为什么我建议"读两个系统比读一个系统读两遍更值":单一系统会产生虚假的一般性。
举例:CC 的 hook 跨进程,你很容易得出"hook 就该设计成子进程 + JSON"。但看 DSH 就会知道——那是产品定位的产物,不是普遍规律。 只读一个系统,你会把偶然学成必然,而带着错误规律去设计,比不懂更危险。
六、提问协议:三段循环
现在进入最实操的部分。
很多人以为顺序是"先读源码,读一部分之后再开始提问"。这个顺序是反的,而且"读一部分"本身是个坏起点。
6.1 为什么"随便读一部分"最差
你会随机停在某处:一个函数、一个字段、一段注释。此时你既没有全局坐标,也没有完整链条,于是能问出的问题只有一种:
"这段代码是干嘛的?"
这是最低价值的问题——它只能换来一句注释,下一段还要再问一遍。你会陷入"逐段翻译",读五千行,记住零个机制。
你要的不是"读一部分",是"跑通一条链"。
一条链 = 从"谁调用它"到"结果回到哪去"的完整路径。可能只有两百行,但它给你的是形状,不是碎片。
6.2 阶段 0:三十分钟扫出"种子"(不是读,是定位)
只做三件事:
| 动作 | 找什么 | 产出 |
|---|---|---|
| 找入口 | 谁调用了它(搜 executePreToolHooks 这类名字) | 一句话:"它挂在哪几个点上" |
| 找形状 | 输入长什么样,输出长什么样 | 两个 JSON 例子 |
| 找失败 | 它可能怎么坏(catch / timeout / abort) | 三条失败路径 |
只要这三样,不要更多。 三十分钟到了就停。
有了种子,你才有提问的词汇。没有词汇的问题,问出来也是空转。
6.3 阶段 1:先问五个"不需要代码"的问题
这一批必须在读代码前问,因为它建立的是"战略坐标"。答完,你对形态就有了预判——后面读代码就是"验证预判",而不是"被动接受"。
| # | 问题 | 为什么不需要代码 | 答上了的标志 |
|---|---|---|---|
| 1 | 它给谁用?(终端用户 / 开发者 / 模型) | 看文档和配置格式就知道 | 能说出"所以它必须/不必跨进程" |
| 2 | 它的输出,谁消费?(人 / 程序 / 模型) | 从输出字段的形态能看出来 | 能区分"给人看的字段"和"给机器用的字段" |
| 3 | 它和权限系统谁大谁小? | 逻辑推理即可 | 能说出"它能不能越过 deny 规则" |
| 4 | 它全部失效,系统还能用吗? | 推理 | 能推出 fail-open 还是 fail-closed |
| 5 | 不用它,这件事能做吗? | 常识 | 能说出它解决的是哪一类问题 |
这五个问题的价值在于:它们的答案会"预测"后面的设计。
比如第 4 问:如果你判断"它全是用户自己写的脚本,一定会崩",你就能预判出 fail-open——后面读到 non_blocking_error 时,不是"哦原来如此",而是"果然如此"。
预判对了,记忆才牢;预判错了,那个错点就是你真正的知识边界。
6.4 阶段 2:读一条链,然后问六个"需要代码"的问题
现在才打开代码。但只走一条链(比如 PreToolUse 从触发到结果回流),别横着读。
| # | 问题 | 去哪找 | 验收标准 |
|---|---|---|---|
| 1 | 多个扩展者同时给结论,谁赢?哪一行代码决定? | 结果聚合处 | 能指出具体一行,并说出为什么不是"最后一个赢" |
| 2 | 它怎么做到"任何语言都能写"? | 入参 / 返回值的 schema | 能说出"哪个字段故意不定形状"以及为什么 |
| 3 | 它崩了,流程继续还是停下? | 退出码分支 | 能列出所有失效路径(崩溃 / 超时 / 取消 / 根本没跑) |
| 4 | 它怎么不拖慢主流程? | 并发 / 超时 / 异步 | 能说出三层:预过滤 → 并行 → 异步逃逸 |
| 5 | 它怎么让人看到、让机器知道? | 结果 → 消息的转换 | 能区分"给人看"和"给模型看"的通道 |
| 6 | 它怎么防止"改写参数后绕过批准"? | 权限决策处 | 能说出批准绑定的是改写前还是改写后 |
这六个问题的共同点:答案都是"一个机制",不是"一段代码"。
而且它们都能被证伪——你答完之后,可以写一个测试去验证。可验证的问题才是真问题。
6.5 阶段 3:拿第二个系统来问两个"战略问题"
| # | 问题 | 你需要的材料 |
|---|---|---|
| 1 | 这个设计是被约束逼出来的,还是产品选择? | 第二个在同位置做了相反选择的系统 |
| 2 | 如果约束不成立,它会退化成什么? | 推理 + 反例 |
举例:CC 的 hook 跨进程。看 DSH 之后你才能问出:"如果扩展者可信,还需要跨进程吗?"答案:不需要,于是就有了瀑布式中间件。这一问,把"CC 的做法"升级成了"一个带条件的判据"。
6.6 好问题的唯一判据
| 坏问题 | 为什么坏 | 改写成好问题 |
|---|---|---|
| hook 的架构是什么? | 无法判断答没答上 | 一次工具调用会经过几次 hook 执行?顺序有保证吗? |
| hook 怎么设计的? | 同上 | 两个 hook 冲突时,哪一行代码决定谁赢? |
| hook 有什么优点? | 无法证伪 | 它全部失效时,危险命令会被拦住吗? |
| 它为什么这么设计? | 太宽 | 它为了保证 X,放弃了 Y?有没有可能两个都要? |
判据一句话:好问题的答案,应该能落成"一行代码 / 一个字段 / 一张表"。
落不成的,就是还没提对。
七、一个周末的六小时
把上面那套压成时间表。总投入六小时,产出 1~2 个机制。
| 时间 | 做什么 | 产出 | 纪律 |
|---|---|---|---|
| 周五晚 30min | 扫入口、目录、README,列出 10~15 个"机制候选",每个一句话 | candidates.md | 不许读任何实现 |
| 周六上午 2h | 给候选打标:① 没见过 ② 见过但不知细节 ③ 我懂。从 ② 里挑 1~2 个 | 选定 1 个 | 见下面选题判据 |
| 周六下午 3h | 读 100~300 行 + 画一张调用链/数据流图 | 一张图 | 只读这一条链 |
| 周日上午 2h | 写 1 页笔记:3 个取舍 + 2 个缺陷 + 1 个"我能用在哪" | notes.md | 不许摘抄,只写判断 |
| 周日晚上 1h | 预测检验:挑 3 个边界场景,先写预测再验证 | 3 条"我猜错了" | 唯一能把"读过"变成"懂了"的动作 |
为什么挑"② 见过但不知细节"
- ① 太陌生 → 会在基础概念上耗掉半天
- ③ 已经懂了 → 收益低
- ② 是收益最高的区域:你有一点直觉,但经不起追问。这种"半懂"状态一旦钻透,记忆最深、迁移最快。
什么机制值得一个周末
三个判据,满足两个就上:
| 判据 | 问法 | 反例(不值得) |
|---|---|---|
| 重复出现 | 我在别的系统里见过类似的吗? | 某个框架特有的配置格式 |
| 是取舍,不是功能 | 它有没有"这样做 vs 那样做"的对立面? | 纯功能("支持 X 格式") |
| 能推出反例 | 我能不能说出"什么时候不该这样"? | 某个算法常数优化 |
按这个标准:hook 的契约设计 ✅;waterfall 中间件 ✅;"它怎么算 token" ❌;"它的配置文件长什么样" ❌。
八、反干扰:范围契约与"不做清单"
"烂尾"的原因几乎从来不是"做不出来",而是"顺手多做了一个"。
所以开工前先写一张明确不做的清单,贴在文件顶上:
本项目的边界(写死,不加):
- 只用 Node 内置模块,零依赖
- 单进程,不做 server,不做 UI,只 console.log
- 不做持久化、不做配置热加载、不做插件市场
- 只做三个事件(PreToolUse / PostToolUse / Stop)
- 只做 command 执行器,不做 http / prompt / agent
- 不做反向 RPC(那是下一阶段,永远不做)
- 目标:能通过 fixtures,能和参考实现对比
"不做清单"比"要做清单"更重要。
再加三条纪律:
- 时间盒:每晚 90 分钟,到点就停,留一句"明天从哪继续"
- 每步一次提交:commit message 写"这一步我学到了什么"
- 每步追加一行到
NOTES.md:写你"原本以为会怎样、实际怎样"
九、"用不上"怎么办:自己造反馈
暂时用不上最大的损失不是"学不到",是没有反馈。
你写了个 hook,它能跑,你就知道自己对了;你读了个机制,没有任何东西能告诉你"你到底懂没懂"。
解药只有一个:自己造反馈。 最便宜的形式是"预测—验证":
- 挑 3 个边界场景(比如:hook 超时了会怎样?两个 hook 冲突谁赢?不信任工作区时 hook 会跑吗?)
- 先写下你的预测(不许查)
- 再去看代码 / 跑一下
- 记下你猜错的地方
你猜错的那几条,就是你真正的知识边界。 这比读十遍源码都准,而且只要一小时。
而且这个动作有个额外好处:它把"读"变成了"赌"。赌输了会疼,疼了才记得住。
十、结语
回到开头那个困境:想学一个暂时用不上的框架,时间只有周末。
诚实的答案是:你不可能"精通"它。 一个成熟框架有几十个机制,一个周末连目录都看不完。这是客观的,不用纠结。
但你完全可以做到:带走 1~2 个机制,外加一套判据。
所以别问"我该怎么学这个框架",问:
"这个周末,我要带走哪一个机制?"
一个周末一个机制,一年五十个。这已经是普通人能做到的上限——而且足够让你在两三年后,面对一个新框架时,看一眼就知道它的骨架和取舍。
最后把整篇压缩成三句:
道不是细节的替代品,是细节的沉淀物。
机制不是"一段代码",是"提对了的问题 + 被约束逼出来的答案"。
原型只有八个,实例无穷多;你该记的是那八个问题,不是四十个答案。
评论