🐎 第1章 登高望远:Claude Code 技术架构全景
📄 一句话概括

六个组件摆在一起,最要紧的问题只有一个: 谁来决定它什么时候跑。答案决定了两件事——确定性有多高,以及你能不能拦住它。 这一节用一次真实请求的九站走位,把触发权、执行顺序和数据流向一次看清。

📝 本章和前面几章的关系

上一节把四层拆开,这一节把它们装回去。 组件之间的关系不是「谁更强」,而是「谁在什么时刻被谁叫起来」。 这张关系图记住,后面九章讲的每个组件都只是在这里补细节。

一、六个组件,六种触发权

组件之间的第一个差别不是功能,是谁决定它跑。 CLAUDE.md 由 harness 在会话开始时读,每次必读,模型没有拒绝的余地。 Skills 由模型自己决定。它看到一列描述,判断这次任务用得上哪个,就把哪个读进来。 这一步的判断权在模型手上。

子智能体也是模型点名,或者你明确要求。它有独立的上下文,干完只回结论。 Hooks 由 harness 按事件触发。配了就在那个时候跑,不看模型心情。 MCP 的工具由模型在需要时调用,但工具本身做什么、返回什么,由那个服务器决定。

SDK 由你的代码发起,什么时候调、调几次,全在你手里。 把这条线排下来会发现一个规律:越往下,控制力越强,要写的代码也越多。这不是巧合, 控制力从来都是拿工作量换的。

二、确定性是可以排队的

按「配了之后一定执行」的程度排,六个组件大概是这样一列: 最硬的是 Hooks 和 SDK——一个由事件驱动,一个由你的代码驱动,都不经过模型的判断。 中间是 CLAUDE.md——它一定被读进上下文,但模型读完了照不照做是另一回事。 「一定被看到」和「一定被执行」是两件事。

最软的是 Skills 和 MCP 工具调用——什么时候用、用不用,都是模型现场判断。 所以有个常被踩的坑:把必须做的事写进 CLAUDE.md,然后指望它每次都被执行。 它确实每次都被读到,但读到了不等于照做。 必须做的事只有两个去处:写成 Hook,或者写进你的代码。

三、一次请求的九站

演示把一个普通请求拆成九站:会话开始、提交问题、模型规划、加载技能、要动命令、执行工具、工具完成、 模型收尾、准备结束。 九站里四站是钩子在动手:注入上下文、检查命令、写审计日志、跑测试门控。 这四站有一个共同点:它们都是「每次都必须」的事。注入当前分支、拦住危险命令、留下操作记录、 出门前跑一遍测试——哪一件漏了都难受。

所以它们被挂在钩子上,而不是写进提示词。这就是钩子存在的全部理由:模型可能忘,钩子不会。 另外五站是 harness 和主循环。 它们不做事,只搬运:把文件读进来、把工具结果送回去、把模型的意图变成一次调用。

四、上下文只增不减

九站走完,累计 854 token。这个数字本身不重要,重要的是它的形状:单调上升。 第 8 站做决策的时候,前面七站的内容全都还在窗口里。 它不能「翻回去看」,因为整段历史每次都被完整重读——所谓「记得」,就是这个意思。 这也解释了为什么上下文管理是整本书反复出现的主题:窗口是有限的,而历史只增不减, 所以任何能减少「必须带上的东西」的手段都值钱。

技能按需加载、子智能体独立窗口、钩子注入精准片段、MCP 工具输出裁剪——这些看着不相干的做法, 做的是同一件事:让不该进上下文的东西别进来。

五、Plugins:把一套组合打成一个包

上面这些组件配好之后,会形成一个「组合」:几个技能、几个钩子、一两个子智能体、一份约定。 这套组合在 A 项目好用,B 项目也想用。 复制粘贴能撑一阵,但版本一多就乱:谁改过、哪个是最新的、B 项目改的那处要不要回流。 Plugins 就是给这套组合一个封装的壳:一份清单说明包含什么,一个目录装全部内容,一个名字做命名空间。

它不产生新能力,只解决分发。 这一点要分清:打包不是设计,打包是工程。里面装的东西设计得不好,包打得再漂亮也没用。

六、动手 Demo:一个请求的旅程:九站,每一站谁在动手

先列一张触发权对照表,看六个组件分别被谁叫起来、确定性有多高、能不能拦住。 然后把一个真实请求拆成九站,逐步累计上下文,看它怎么涨到 854。最后数一数九站里几站是钩子在动手。

JOURNEY = [
    (u"会话开始", u"harness", u"读 CLAUDE.md", 178),
    (u"提交问题", u"UserPromptSubmit 钩子", u"注入分支与改动", 48),
    (u"模型规划", u"主循环", u"决定调用 deploy-check", 22),
    (u"加载技能", u"harness", u"读入 SKILL.md 正文", 444),
    # …后面还有五站
]
acc = 0
for stage, who, what, tk in JOURNEY:
    acc += tk        # 只增不减
    print(stage, who, u"+%d" % tk, acc)

实跑输出:

--------------------------------------------------------------------
一个请求的旅程:九站,每一站谁在动手
--------------------------------------------------------------------


  ① 先说触发权在谁手上
  --------------------------------------------------------------------
| 组件 | 谁触发 | 确定性 | 能否阻止 |
|---|---|---|---|
| CLAUDE.md | harness,每次会话 | 高,必读 | 否 |
| Skill | 模型读 description 自己定 | 中 | 否 |
| 子智能体 | 模型或人点名 | 中 | 否 |
| Hook | harness 按事件 | 高,配了就执行 | 是,部分事件 |
| MCP | 模型调工具时 | 低,工具自己定 | 是,权限层 |
| SDK | 你的代码 | 高 | 是 |

  这张表是全书的索引:越往下,你对「什么时候跑」的控制力越强,但要写的代码也越多。

  ② 一次真实的顺序:请求从进门到出门
  --------------------------------------------------------------------
| # | 阶段 | 谁做的 | 发生了什么 | 本步 | 累计 |
|---:|---|---|---|---:|---:|
| 1 | 会话开始 | harness | 读 CLAUDE.md | +178 | 178 |
| 2 | 提交问题 | UserPromptSubmit 钩子 | 注入当前分支与改动清单 | +48 | 226 |
| 3 | 模型规划 | 主循环 | 决定调用 deploy-check 技能 | +22 | 248 |
| 4 | 加载技能 | harness | 读入 SKILL.md 正文 | +444 | 692 |
| 5 | 要动命令 | PreToolUse 钩子 | 检查 bash 命令是否在白名单 | +31 | 723 |
| 6 | 执行工具 | harness | alembic heads 返回 2 行 | +19 | 742 |
| 7 | 工具完成 | PostToolUse 钩子 | 把输出写进审计日志 | +12 | 754 |
| 8 | 模型收尾 | 主循环 | 输出「可以发」三段结论 | +74 | 828 |
| 9 | 准备结束 | Stop 钩子 | 跑 pytest 做质量门控 | +26 | 854 |

  九站走完,上下文累计 854 token。注意它只增不减:第 8 站做决策时,前面八站的内容全都还在窗口里。

  ③ 把这张表和上一张表对上
  --------------------------------------------------------------------
  九站里有 4 站是钩子在动手,它们全是「配了就一定执行」,不靠模型想起来。
  第 2 站  注入当前分支与改动清单
  第 5 站  检查 bash 命令是否在白名单
  第 7 站  把输出写进审计日志
  第 9 站  跑 pytest 做质量门控
  ! 这就是钩子存在的理由:模型可能忘,钩子不会。
  凡是你希望「每次都必须」的事,都该挂在钩子上,而不是写进提示词里求它记得。

  ④ 一次请求里,谁改了什么
  --------------------------------------------------------------------
  记忆层被读 1 次、扩展层被读 1 次、集成层被查 1 次(工具名)、编程层没参与——这是最普通的一轮。
  要改的东西越多,循环转的圈数越多,每一步都会重读一遍前面的全部内容。

①第一张表是全书索引。看最后一列:能拦住执行的只有三个——钩子、MCP 工具的权限层、以及你自己的代码。 这一列决定了「安全」该往哪儿做。指望模型不执行危险命令,是把安全交给判断力; 在钩子里拦,是交给配置。 ②第二张表是顺序。注意第 3 站和第 4 站的关系:模型先决定用哪个技能,harness 才去读它的正文。 所以技能省下来的钱,全在「没被选中时不用读」这一点上。这是渐进式披露的机制根源。 第 4 站一次就加了 444,是全表最大的一笔。上下文的大头往往来自知识,不是来自对话。 ③数出来四站是钩子。四站全是「每次都必须」的事。 这里有个判断标准很好用:问自己「这件事漏一次会怎样」。漏一次就出事的,挂钩子; 漏一次只是慢一点的,写进指令里就行。 ④最后一段解释累计 854 的含义。它不是成本,是窗口占用:第 8 站做决策时,前七站的内容都在里面挤着。 窗口够大的时候没感觉,等你把一份两万行的仓库全读进来,就会发现留给「思考」的空间不够了。

30 秒实验

  1. 把第 4 站的技能正文从 444 改成 1200(模拟一个大技能),重跑第 ② 步。累计到第 8 站是多少? 占总窗口的比例呢?
  2. 在九站中间插一站「用户中途改口」,看它对后面的累计有什么影响。什么样的插入会让历史突然变脏?
  3. 把四个钩子站从流程里去掉,重算累计。省下的 token 有多少?这四件事改到提示词里做,代价是什么?

常见误区

  1. 把「一定被读到」当成「一定被执行」。CLAUDE.md 每次都被读,但模型照不照做是概率问题。 要确定执行,只能上钩子。
  2. 用钩子去做需要判断的事。钩子是脚本,它不会读语义。 要判断就用 prompt 类型的钩子,或者干脆交给模型。
  3. 以为组件越多越好。每加一个组件都加一层常驻开销和一处可能出错的地方,先问「不加行不行」。
  4. 把 Plugins 当成能力来源。它只是打包,能力还是在里面的技能和钩子上。 包里的东西设计得差,装到哪都差。
  5. 忽略上下文只增不减这件事。所有「怎么让它记得更久」的讨论,前提都是窗口有限; 窗口无限的话,这些技巧一个都不需要。
自测三问(点开对答案) 1. 六个组件里,哪些的触发权不在模型手上? —— CLAUDE.md(harness 每次读)、Hooks(事件驱动)、SDK(你的代码)。 Skills、子智能体、MCP 工具调用都要模型现场决定。 2. 「每次都必须执行」的事应该放哪?为什么不能放 CLAUDE.md? —— 放 Hooks 或写进自己的代码。 CLAUDE.md 只保证被读到,不保证被照做——它是提高概率,钩子是确定执行。 3. 九站里有四站是钩子,这四站的共同点是什么? —— 都是漏一次就会出事的事:注入上下文、拦危险命令、留审计日志、出门前跑测试。 判断标准是「漏一次会怎样」,而不是「重不重要」。

六个组件、四层架构都摆清楚了,最后一节回答最实际的问题:手上这个需求,到底该用哪个。

相关笔记