← 返回文章列表

从设计者角度理解源码·方法篇:机制、原型与提问协议

从设计者角度理解源码·方法篇:机制、原型与提问协议 这是《从设计者角度理解源码》系列的一篇"元文章":它不讲某个模块怎么实现,只讲读源码这件事本身该怎么做。 案例全部来自同系列的 Hook 设计的两种答案——那篇给的是"填好的表",本篇给的是"怎么用这张表"。 引言 先讲一个很常见的困境。 你遇到了

从设计者角度理解源码·方法篇:机制、原型与提问协议

这是《从设计者角度理解源码》系列的一篇"元文章":它不讲某个模块怎么实现,只讲读源码这件事本身该怎么做。

案例全部来自同系列的 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 CodeDeepSeek 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 判别 + expectedHookEventinterface Events 声明合并 + engines.dsh 兼容检查

怎么用这张表:

  1. 读第一个框架:把四列填满。填不满说明没读全;同一行填出好几个答案,说明还停在"决定"层。
  2. 读第二个框架:只填后两列。"和上一个差在哪"那一格,会自动变成你的判据。
  3. 读第三个框架:你会发现很多格子是重复的——重复的那几条,就是真道。

这也是为什么我建议"读两个系统比读一个系统读两遍更值":单一系统会产生虚假的一般性。

举例: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,能和参考实现对比

"不做清单"比"要做清单"更重要。

再加三条纪律:

  1. 时间盒:每晚 90 分钟,到点就停,留一句"明天从哪继续"
  2. 每步一次提交:commit message 写"这一步我学到了什么"
  3. 每步追加一行到 NOTES.md:写你"原本以为会怎样、实际怎样"

九、"用不上"怎么办:自己造反馈

暂时用不上最大的损失不是"学不到",是没有反馈。

你写了个 hook,它能跑,你就知道自己对了;你读了个机制,没有任何东西能告诉你"你到底懂没懂"。

解药只有一个:自己造反馈。 最便宜的形式是"预测—验证":

  1. 挑 3 个边界场景(比如:hook 超时了会怎样?两个 hook 冲突谁赢?不信任工作区时 hook 会跑吗?)
  2. 先写下你的预测(不许查)
  3. 再去看代码 / 跑一下
  4. 记下你猜错的地方

你猜错的那几条,就是你真正的知识边界。 这比读十遍源码都准,而且只要一小时。

而且这个动作有个额外好处:它把"读"变成了"赌"。赌输了会疼,疼了才记得住。


十、结语

回到开头那个困境:想学一个暂时用不上的框架,时间只有周末。

诚实的答案是:你不可能"精通"它。 一个成熟框架有几十个机制,一个周末连目录都看不完。这是客观的,不用纠结。

但你完全可以做到:带走 1~2 个机制,外加一套判据。

所以别问"我该怎么学这个框架",问:

"这个周末,我要带走哪一个机制?"

一个周末一个机制,一年五十个。这已经是普通人能做到的上限——而且足够让你在两三年后,面对一个新框架时,看一眼就知道它的骨架和取舍。

最后把整篇压缩成三句:

道不是细节的替代品,是细节的沉淀物。

机制不是"一段代码",是"提对了的问题 + 被约束逼出来的答案"。

原型只有八个,实例无穷多;你该记的是那八个问题,不是四十个答案。

评论