Claude-Code-操作模型-二-

Claude Code 操作模型(二)

原文:The Claude Code Operating Model

译者:飞龙

协议:CC BY-NC-SA 4.0

Use the skill-creator skill to help me build a skill for [your use case]






这个工具的存在本身就揭示了一个重要事实:Skills 体系已经足够成熟,可以实现“自举”,也就是用 Skills 体系本身来构建新的 Skills

软件工程视角下的 Skills

与子代理一样,Skills 也是一个全新的概念。其核心本质,是软件工程中久经考验的经典智慧在 AI Agent 知识架构中的迁移与适配。

关注点分离

核心理念:“授人以鱼,不如授人以渔。”

Skills 将解决问题的方法、步骤和经验,沉淀为可复用、结构化的资产,而非仅仅提供一次性的答案。这使得 Agent 能够从依赖“临场灵感”转向稳定复现高质量的工作流。

三层架构的职责划分如下:

  • CLAUDE.md:全局规则(项目背景、通用规范)。

  • Skills:专业工作流(特定领域复杂逻辑的封装)。

  • Sub-agents:任务执行(动态规划与实时操作)。

工程师警示:切勿将所有逻辑都塞进 CLAUDE.md。这就像软件开发时把所有代码都写进主函数,会导致上下文冗余、维护困难、扩展性差。

依赖倒置

  • 核心机制:面向接口编程,而非面向实现编程。

  • Claude 不直接依赖某个 Skill 的具体内部实现(如特定脚本或提示词细节),而是依赖其抽象接口(即 description 与输出契约)。只要接口契约不变,开发者可以随时替换、重构或升级 Skill 的内部逻辑。对于 Claude 和用户而言,这些变化完全透明,从而大幅降低了系统耦合度。

缓存优化与懒加载

核心策略:渐进式披露 = 按需加载。

“渐进式披露”是一种典型的懒加载策略。系统不会在启动时预加载所有知识库,而是仅在首次需要某个特定技能时,才加载相关资源。

这类似于 Web 开发中的代码分割和数据库中的延迟加载:在正确的时间加载正确的资源,以最小化初始开销并提升响应速度。

最小权限原则

安全基石:allowed‑tools 是经典安全原则在 AI 领域的直接映射。

只授予一个 Skill 足以履行其职责的最小权限——不多不少。就像在 Linux 中使用 chmod 精确控制文件读、写、执行权限,或在 AWS 中使用 IAM Policy 精细限制服务访问。

明确界定“不能做什么”,对系统安全而言比界定“能做什么”更有效,因为它能防止恶意代码或幻觉导致的越权操作。

开放标准

生态愿景:声明式、自包含、以知识为中心。

该规范于 2025 年 12 月 18 日作为开放标准发布,此后已被数十个 Agent 平台采用(包括 OpenAI Codex CLI、Google Gemini CLI、Cursor 和 GitHub Copilot)——截至 2026 年中期,大约有 40 个兼容产品。请将该数字视为截至某一时间点的数据,而非固定不变的数字。

Skills 成功的三个基本属性如下:

  • 声明式:纯 Markdown 格式,任何大模型都能读取和理解,没有黑盒二进制文件

  • 自包含:所有必要组件都包含在一个文件夹内;安装就像复制一样简单,无需复杂的依赖管理

  • 以知识为中心:核心价值在于内容本身而非任何特定格式,且不绑定于单一平台

在 Claude Code 中精心设计的一个 Skill,理论上可以无缝迁移到任何支持该标准的 AI 平台,真正实现“一次编写,到处运行”。

这不仅仅是一个关于产品功能的故事——这是一个关于行业标准的故事。你正在学习的,不仅仅是“如何在 Claude Code 中配置一个功能”,而是“如何为 AI Agent 生态构建标准化的知识包”。

小结

Skills 直接解决了开发者的一个日常痛点:如何确保 Claude 永久保留专业的工作范式,从而避免每次对话都要“重新教学”。在 Claude Code 的五层架构中,Skills 位于知识层——建立在工具层(定义“能做什么”)之上,并使能代理层(决定“谁来做”)。向下,它们通过 allowed‑tools 约束工具行为;向上,它们通过 skills 字段向子代理注入专业知识,与 CLAUDE.md 形成平行互补的关系(前者始终活跃,后者按需调用)。

理解这一机制的关键在于把握四个核心设计原则。

  • 渐进式披露:实现知识的按需加载——从上下文中始终驻留的 description,到触发时加载的 SKILL.md 正文,再到执行期间动态读取的引用文件。在大多数场景下,这一机制可节省 50%–98% 的 token 消耗。

  • 语义触发:确保专业能力在正确的时机自动激活。这里,description 是这一机制的灵魂:它不是面向用户的手册,而是辅助 Claude 决策的“语义指纹”。

  • 安全约束:通过 allowed‑tools,将知识限制在安全的操作边界内,实现知识与行动统一的安全设计。

  • 动态注入:通过参数传递($ARGUMENTS)和命令执行(!`command`),Skill 获得了根据运行时上下文灵活适应的能力。

从更广阔的视角来看,Skills 本质上是人类数十年知识管理经验的数字化映射。在这个体系中,SKILL.md 相当于部门 SOP 的主页,reference/ 对应企业知识库,templates/ 是标准化输出模板,scripts/ 是自动化工具包——这些角色在任何成熟企业中都有精确的对应物。

它更像是一个由众多厂商同意实现的共享接口;价值来自大家都采用相同的形态,而非来自某个标准机构的认证。

在 第 4 章 中,我们将深入探讨 Claude Code 扩展系统的一个里程碑式功能——子代理。如果说 Skills 赋予了 Claude 专业的“知识”,那么子代理则提供了更根本的“能力”:将复杂任务分解并分配给多个具有独立上下文的专家,让它们在隔离环境中深度协作,最终将提炼后的结论汇入主对话。

思考题

  1. 在你的日常工作中,有哪些领域知识或工作规范,你发现自己需要反复向 Claude 解释?尝试将它们提炼并封装为一个 Skill 的 description。在仅限 1,024 个字符的限制下,你会优先保留哪些核心信息?

  2. 请设计一个名为 /deploy 的任务型 Skill。在设计时,请特别关注以下要素:disable‑model‑invocation 配置的必要性、allowed‑tools 的精确权限范围、执行 !`command` 前需要预加载的上下文(如当前分支、最近构建状态等),以及完善的错误处理路径。最后,输出完整的 Frontmatter 配置和 SKILL.md 文件骨架。

  3. 假设你的项目安装了 25 个 Skills,description 字符总预算为 16,000。你如何在严格遵守这一预算的同时,最大化每个 Skill 的触发准确率?哪些类型的 Skills 适合配置 disable‑model‑invocation: true,从而释放宝贵的 description 空间?

4

第四章:分而治之:子代理与任务委派的艺术

“**把每一难题分成若干部分,逐一解决,分得越细越好,需要分多细就分多细。”——勒内·笛卡尔

周四下午三点,小冰的表情像是一个即将溢出的栈。她紧盯着屏幕上与 Claude 的对话记录:一场持续了一个多小时的对话,上下文里塞满了日志片段、测试输出和代码审查细节。

为了排查支付模块的一个线上 bug,她先让 Claude 分析了 500 多行错误日志;然后跑了一轮回归测试,200 多行测试输出涌入对话;接着,她又让 Claude 读取了七八个相关源文件。此刻,当她让 Claude 基于之前的分析给出修复方案时,Claude 的回答开始变得古怪:它不仅“忘记”了日志分析中发现的关键错误码,还把测试输出中一个无关的失败误判为核心问题,甚至在修复建议中引用了一个根本不存在的函数名。

“它疯了,”小冰嘟囔道。她尝试换一种方式重新表述问题,但 Claude 的回答依然在日志片段、测试噪音和代码细节之间漂移,就像一个被嘈杂声包围的人,听不清任何一个声音。

小雪瞥了一眼屏幕,瞬间看出了问题的根源,提醒她:“你把所有信息都塞进了一个聊天窗口。日志、测试结果、代码审查——加起来大概有几万 token。Claude 不是‘忘记’了——它是被信息洪流淹没了。”

上下文窗口困境

小冰和小雪带着这个问题去找 Kage。听完她们的描述,Kage 没有急于回答,而是转身在白板上画了一张桌子。

“想象你面前有一张桌子,”Kage 解释道。“桌面大小固定,只能放下大约 20 份文件。现在,你需要同时处理三件事:审阅一份 20 页的财务报告、分析一堆系统日志,以及撰写一份技术方案。如果你把这三件事涉及的所有文件都堆到这张桌子上,会发生什么?”

“桌子会一团糟,你找不到需要的东西,”小冰回答。

“不止如此。”Kage 在白板上画了一张图,展示了一个信噪比严重失衡的上下文窗口(见 图 4.1)。“当你开始起草技术方案时,你的眼睛扫过桌面,看到的全是财务数字和日志堆。这些无关信息不是静止的——它会不断干扰你的思维,让你在技术方案中不自觉地采用财务报告的行文风格。这正是你刚刚遇到的困境——上下文污染。”

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/the-claude-code-operating-model/img/B39394_4_1.png

图 4.1:上下文窗口中严重的信噪比失衡

Claude Code 的默认上下文窗口很大——200,000 个 token——通过 Amazon Bedrock 等云服务提供商还可以使用更大的窗口(最高可达一百万个 token)。即便如此,它也是有限的。

当你把 500 行日志塞进上下文时,也许只有 3 行错误堆栈真正有价值;当你倒入 200 行测试输出时,你真正需要的可能只是“3 个测试失败,47 个通过”这一句总结。剩下的几百行都是噪音——它们不仅占用了宝贵的上下文空间,还稀释了真正重要的信号,让 Claude 难以聚焦于核心问题。

这个问题在软件工程中其实并不陌生。

  • 为什么操作系统需要进程隔离?因为如果所有程序共享同一内存空间,一个程序中的野指针就能让整个系统崩溃。

  • 为什么微服务架构取代了单体应用?因为当所有业务逻辑都挤在一个进程里时,任何一个模块的内存泄漏都可能拖垮整个系统。

关注点分离、进程隔离、故障隔离舱——这些经典软件工程原则,其核心都在回答同一个问题:如何防止不同任务相互干扰。

而 Claude 遇到的“上下文污染”,正是大模型时代的“内存泄漏”和“进程干扰”。

“那该怎么办?每来一个新任务就开一个新对话?”小冰有些着急地问。

“那太粗暴了,”Kage 摇了摇头。“直接开新对话意味着你会丢失所有上下文,包括真正有价值的背景信息和项目规范。这就像为了清理桌面,把整张桌子连同所有文件一起丢进碎纸机。”

他顿了顿,目光扫过两人,然后给出了关键答案:“正确的做法,是把任务委派给子代理。”

子代理的本质

Kage 转身在白板上画了一张新图。这一次,不是那张拥挤的桌子,而是一个布局清晰的现代办公室。

“我们换个角度来看,”Kage 指着新图说。“现在,你不再是一个埋首于任务的员工,而是一家公司的 CEO。今天,你桌上有三件急事:审计财务报表、排查系统故障、起草季度战略规划。你会把自己锁在房间里,试图一个人全包吗?”

“当然不会,”小雪毫不犹豫地回答。“我会让 CFO 去审计报表,让 SRE(站点可靠性工程师)去排查故障。我只需要坐在办公室里,等他们送来简洁的报告,然后基于这些报告做最终决策。”

“完全正确!”Kage 眼中闪过一丝赞许。“这就是子代理背后的核心思想。你不需要让 Claude 吞下所有原始数据——相反,让它扮演 CEO,把具体任务分解并委派给专业的‘员工’。”

接着,他在白板中央写下三个关键词,并画出它们的层级关系(见 图 4.2)。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/the-claude-code-operating-model/img/B39394_4_2.png

图 4.2:CEO 委派模型——子代理的核心思想

子代理的定义清晰而简洁:它是一个具有独立上下文窗口、受限工具权限和明确任务范围的 Claude 实例。主代理按需启动子代理,并向其传递任务描述;子代理在自己的隔离上下文中执行任务,只向主代理返回结论——而非所有中间细节。

这一机制体现了三个核心价值,每一个都对应着软件工程中一条久经考验的原则。

  • 隔离:子代理拥有自己独立的上下文窗口。它读取的文件、执行命令的输出、以及中间的推理过程,都被严格限制在自己的上下文空间中,绝不会回流到主对话。这类似于操作系统中的进程隔离——每个进程有自己的地址空间,一个进程的内存泄漏不会影响其他进程。在这个场景中,500 行日志的“噪音”完全被 containment 在子代理内部;主对话只收到提炼后的故障诊断,从根本上解决了信噪比问题。

  • 约束:用户可以为每个子代理配置严格的工具权限白名单。例如,一个代码审查子代理可能只被授予 Read、Grep 和 Glob 三项只读工具权限。即使 Claude 误解了意图,它也无法修改任何文件。这种约束不依赖于“请不要修改文件”这类容易被忽视的提示,而是建立了物理边界。就像给用户分配只读文件系统权限一样,无论意图如何,系统直接禁止写操作。这就是软件安全中的最小权限原则。

  • 复用:子代理以 Markdown 文件形式定义,支持团队共享和项目迁移。一位开发者精心设计的代码审查子代理,新团队成员可以立即使用,无需反复调试或重新训练。这一特性体现了软件工程的组件化原则——将核心能力封装为可复用、标准化的模块。

当主代理决定委派任务时,Claude Code 通过其内置的 Agent 工具生成一个新的 Claude 实例。这不是操作系统层面的进程 fork;它是一个独立的代理实例,拥有自己的上下文窗口、自己的系统提示词和自己的工具权限。父级的任务描述作为该实例的第一条消息到达,只有它的最终文本返回给父级。进程隔离的类比仍然有用——只是要把它当作类比来看待。

在这个干净的上下文中,子进程独立执行文件读取、命令执行和结果分析。任务完成后,只有结果的摘要返回给主进程。主对话最终收到一个简洁的结论,而子代理内部的所有中间交互在子进程终止时被完全释放。

这意味着什么?让我们回到小冰的场景:如果她把日志分析任务委派给一个名为 log-analyzer 的子代理,那 500 行冗余日志会在子代理内部被完全消化。主对话界面只会显示一条简洁的结论:“根因已定位:src/payment/charge.ts:89 缺少空值检查,导致 NullPointerException。”

随后,当她让 Claude 生成修复方案时,上下文中没有日志噪音——只剩下精确的故障诊断作为决策依据。

定义与配置子代理

“子代理听起来不错,但到底怎么创建呢?”小冰问道。

“事实上,定义一个子代理就像在项目的 .claude/agents/ 目录下创建一个 Markdown 文件一样简单,”Kage 回答。

一个典型的子代理目录结构如下:






your-project/

└── .claude/

└── agents/

├── code-reviewer.md ← 代码审查 subagent

├── test-runner.md ← 测试运行 subagent

├── log-analyzer.md ← 日志分析 subagent

└── bug-locator.md ← Bug 定位 subagent


每个 subagent 定义文件包含两部分:YAML front matter 元数据和 Markdown 正文说明。

front matter 元数据定义了 subagent 是什么:指定其身份、能力边界(如工具权限白名单)以及运行时配置参数。

正文说明规定了 subagent 如何工作:详细描述执行流程、标准化输出格式以及所需的领域专业知识。

下面是一个完整的代码审查 subagent 定义示例。在展示实际代码之前,先说明其设计意图:该 subagent 定位为严格的只读审查者,只被允许观察代码,严禁修改任何文件;其审查结果将以固定的结构化格式输出,使主 agent 能够高效提取关键信息。


name: code-reviewer

description: |

An expert in reviewing code quality, security vulnerabilities, and performance issues

Use when the user requests code review, security audit, or quality assessment

tools:

  • Read

  • Grep

  • Glob

model: sonnet

permissionMode: plan


You are a senior code review expert with over 10 years of engineering experience.

Review Dimensions

Security

  • Check for hardcoded credentials (such as API Keys, passwords, Tokens)

  • Check for injection vulnerabilities such as SQL injection, XSS, CSRF

  • Check the completeness of input validation

  • Check how sensitive data is handled (e.g., whether it is encrypted, whether it is logged)

Code Quality

  • Whether functions follow the single responsibility principle

  • Whether naming is clear and consistent

  • Whether there is duplicate code (e.g., DRY principle)

  • Whether error handling is appropriate (e.g., not swallowing exceptions, not using empty catch blocks)

Performance

  • Whether there are unnecessary nested loops (complexity above O(n²))

  • Whether data structure selection is reasonable

  • Whether there are N+1 problems in database queries

  • Whether there are any unclosed resources (such as file handles, database connections)

Output Format

Please strictly follow the format below to output the review results

Review Summary

[A paragraph summarizing the overall code quality, including highlights and major issues]

Issues Found

  • [Critical/Major/Minor] Issue description at file_path:line_number

Improvement Suggestions

[Specific improvement suggestions, prioritized]


在这个定义文件中,YAML front matter 的每个字段都承载着关键的设计意图,值得深入探讨。

- `name`(唯一标识符):这是 subagent 的“身份证”,作为系统日志、调试追踪和内部调用链中的唯一键。
- `description`(触发描述/路由依据):这是 subagent 的“广告牌”,功能类似于传统编程中的技能描述。它是主 agent 路由任务的核心依据。
  - **语义匹配**:当用户发出命令(例如“请帮我 review 这个 PR”)时,主 agent 会扫描所有已注册 subagent 的 `description` 字段
  - **意图识别**:通过比较用户意图与描述文本的语义相似度,主 agent 判断当前任务是否落在该 subagent 的能力范围内
  - **自动委派**:一旦主 agent 发现 code-reviewer 的 description 与“code review”或“security audit”等需求高度匹配,就会自动打包任务上下文并委派给 code-reviewer,无需用户明确指定 subagent 的名称
- `tools`(构建安全屏障的“工具白名单”):这是 subagent 机制中最具安全价值的特性,在系统层面确立“最小权限原则”。在上述 `definition file` 中,code-reviewer 的白名单仅包含三个只读操作:`Read`(读取文件内容)、`Grep`(在文件中搜索特定模式)和 `Glob`(匹配文件名路径)。此处未列出的任何工具(如 `Edit`、`Write`、`Bash`)对该 subagent 均不可见、不可访问。
- `model`(指定 subagent 使用的模型):这是一个常被低估的成本优化杠杆。对于简单且模式化的任务(如格式检查、日志过滤),可使用 Haiku 这类轻量、响应快的模型;对于需要深度理解和复杂推理的任务(如架构设计、安全审计),应选择 Sonnet 或 Opus 这类高性能模型。选择合适的模型能够在保证任务质量的同时有效控制成本。
- 当设置为 `plan` 时,subagent 以只读规划模式运行。请将其视为对 tools 白名单的强化层,而非取代层:如果父会话以 auto 模式运行,subagent 将继承父会话的模式,其自身 frontmatter 中的任何 `permissionMode` 都会被忽略。你可以依赖的物理边界是 tools 白名单——省略 `Edit`、`Write` 和 `Bash`,subagent 无论认为自己处于什么模式都无法写入。

现在,来看一个执行型 subagent 的示例——test runner。与上一个示例的关键区别在于,该 subagent 需要调用 `Bash` 工具执行命令,因此其工具权限更宽,但输出格式要求更严格。


name: test-runner

description: |

Run the project test suite and analyze the test results

Use when the user requests to run tests, check test coverage, or analyze the reasons for test failures

tools:

  • Read

  • Grep

  • Glob

  • Bash

model: haiku


You are a test execution expert. Your core value lies in distilling key information from massive amounts of test output, providing the main conversation with precise test summaries.

Execution Process

  1. Confirm the project's test command (refer to package.json or CLAUDE.md)

  2. Run the test suite

  3. Analyze the output results, distinguishing between passed and failed test cases

  4. For failed tests, pinpoint the specific cause of failure

Output Format (Strictly Follow)

Test Summary

  • Total: X tests

  • Passed: X

  • Failed: X

  • Skipped: X

Failure Details (List only failed tests)

  • test_name: Reason for failure (described in one sentence) at file_path:line_number

Suggestions

[If there is an obvious failure pattern, provide directions for fixing it]

Note: The output must not contain the complete test log; only the summary information in the above format is required.


请注意,该 subagent 选择了 Haiku 模型。运行测试、解析输出、提取失败信息这类任务并不需要顶级的推理能力。Haiku 不仅完全胜任,还在速度和成本上具有优势。这就像你不会雇佣首席架构师去跑单元测试一样。

一旦 subagent 定义完成,主要有两种使用方式。

- **自动委派**:用户只需向 Claude 发出常规请求,Claude 会根据任务性质自动判断是否将任务委派给特定 subagent
- **显式请求**:用户可以直接指示 Claude 调用特定 subagent 来完成任务

下面是一个自动委派的示例。

User: Help me review the code changes in the src/payment/ directory.

Claude: Okay, I will delegate the review task to the "code review expert."

[Activate sub-agent: code-reviewer]

...

Review complete. Two issues found:





1. [严重] `src/payment/processor.ts:45` 处存在 SQL 注入风险。

2. [次要] `src/payment/validator.ts:23` 处缺少边界值检查。





在这个过程中,`code-reviewer` 子代理可能已经读取了数十个文件,搜索了多种代码模式,但所有这些细节都保留在子代理隔离的上下文中。主对话只呈现两条简洁的审查结论。

**Kage 的笔记**

子代理的 YAML front-matter 元数据还支持两个可按需启用的高级字段。一个是 `skills`,它允许为子代理预加载特定的 Skill 知识包。例如,为 `impact-analyzer` 子代理预加载 `chain-knowledge` Skill,使其在启动时即具备系统依赖关系的相关知识。另一个是 `hooks`,它允许配置事件钩子,在特定时刻自动执行检查或操作。这两个字段的详细应用及其与 Skills 和 Hooks 的结合将在后续章节中探讨。

# 子代理模式

了解了子代理的基本概念和配置方法后,我们现在进入本章的核心:五种子代理模式。这绝不是一个简单的分类列表,而是五种根本不同的架构范式;每一种都精确对应特定的任务特征,并深深植根于软件工程原理之中。

Kage 在白板上画了四个方框,开始逐一分析。

## 只读模式:安全的观察者

只读模式是最基本、最安全的子代理模式,其核心特征可以概括为**只读,不写**。

该模式的典型应用场景包括代码审查、安全审计、架构分析和依赖检查。这些任务的共同特点是它们本质上是“观察”活动,需要读取大量信息并进行深入分析,但绝不能对系统引入任何副作用。正如代码审查者应该指出问题而不是直接修改代码,审计师应该揭示财务漏洞而不是篡改账本。

只读子代理的工具权限严格限制在三个核心工具:`Read`、`Grep` 和 `Glob`。这三者涵盖了代码分析所需的所有能力:`Read` 用于读取文件内容,`Grep` 用于搜索特定模式,`Glob` 用于查找符合规则的文件路径。它们的共同特点是无论怎样调用,都绝不会对文件系统做出任何修改。

从软件安全的角度来看,这是**最小权限原则**的完美体现,即授予实体恰好足以履行其职责的权限,不多不少。在传统软件系统中,我们为数据库副本(`ReadReplica`)配置只读权限,或在 Linux 中使用 `chmod` 精确控制文件的读、写、执行权限;子代理的工具白名单机制是这一经典安全概念在 AI Agent 领域的自然延伸。

该设计在架构层面降低了写入风险,工具白名单才是你真正依赖的边界。

## 执行模式:高噪声任务处理器

执行子代理是日常开发中使用最广泛的模式,其核心价值可以概括为“信噪比优化”:输入海量数据(如 500 行),只输出简洁结论(如 5 行)。

无论是运行生成数千行日志的测试套件,分析涉及数万条数据条目的系统记录,还是触发数百个文件变更的代码格式化,开发者通常只需要关注关键结果:哪些测试失败了,根本原因是什么,以及具体修改了哪些文件。此类任务的共同特点是**过程伴随大量冗余噪声,而最终价值高度集中**。如果任由这些噪声涌入主对话,信息信噪比将急剧下降;但通过在执行子代理的隔离空间中处理,噪声可以被有效阻挡在“隔离舱”之外,确保主对话聚焦于核心结论。

之前定义的 `test-runner` 就是一个典型的执行子代理。由于它需要调用 `Bash` 工具来运行测试命令,其权限范围自然比只读型更广。然而,这种“广”是受到严格约束的——通过工具白名单机制,可以精确界定 `Bash` 的使用边界。

例如,配置 `Bash(npm test:*)` 意味着子代理只允许执行以 `npm test:` 开头的命令。这一限制从根本上阻止了子代理执行任何其他任意 shell 操作,确保即使具备执行能力,系统的安全基线仍然牢牢在握。

执行子代理设计的核心是结构性隔离,而非输出格式化。主代理只会收到子代理的最终消息,因此冗长的日志和中间工具输出会自动留在子代理自己的上下文中——无需任何输出格式指令即可实现这一点。约束输出格式是第二道保障:它使返回的摘要对下游的任何消费方都具有可预测性。

从软件工程的角度来看,执行子代理是**外观模式(Facade Pattern)**的经典应用。

- **复杂子系统**:测试框架生成的详细日志、底层系统交互细节
- **简化接口**:子代理对外提供的结构化摘要
- **客户端**:主对话不需要了解子系统的复杂内部运作,只需通过这个“外观”接口获得简洁可用的结果

## 并行模式:多专家工作流

当你需要从多个独立维度对同一问题进行深入分析时,并行子代理模式展现出其独特的威力(参见*图 4.3*)。这就像派遣三位侦探协同工作,每人聚焦于不同的证据线索。

- **侦探 A**:专注于物证
- **侦探 B**:负责询问证人
- **侦探 C**:调查分析监控录像

<https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/the-claude-code-operating-model/img/B39394_4_3.png>

图 4.3:并行子代理——多位专家同时工作

三个子代理在各自隔离的空间中同步工作,互不干扰,大大缩短了整体分析时间。最终,系统汇总各自的发现,形成一份全面的、多视角的报告。

并行模式有一个严格的前提条件:**子任务必须完全独立**,没有共享状态或依赖关系。

在此模式下,安全审计不需要等待代码质量评估的结果;性能分析不依赖安全审计的结论。

这三个子代理可以真正在各自隔离的空间中同步运行,互不干扰,从而最大限度地利用计算资源,缩短所需的整体时间。

然而,一旦任务链中存在逻辑依赖关系(例如,“必须先定位 bug 才能修复它”),并行模式就不再适用。如果在这种情况下强行并行,后续任务将因缺乏前置输入而失败或产生错误结果。对于这类具有明确先后顺序的任务流,应使用下一节介绍的流水线模式,以确保执行逻辑的严谨性。

这种模式在软件工程领域有一个众所周知的对应物——**MapReduce**。MapReduce 是一种用于在分布式系统中处理大数据的编程模型:Map 阶段将任务拆分为并行子任务,Reduce 阶段合并其结果。在子代理应用的语境中,这一经典架构得到了完美的映射。在 Map 阶段,主代理充当“编排者”,将一个大而复杂的问题分解为多个独立的子任务,并将它们分发给不同的子代理(工作节点)。每个子代理在隔离环境中处理自己那部分数据或逻辑,并行运行。在 Reduce 阶段,一旦所有子代理完成计算,主代理再次介入,收集、整合并提炼各子代理的输出,最终生成一个统一完整的答案。

在这种架构中,子代理只专注于自己独立的计算逻辑,无需关心全局状态或其他节点的结果;而主代理则专注于编排——负责分解任务和汇总结果。

并行子代理的一大优势是专业化。接下来的角色草图是说明性的而非经过实测的:每个子代理可以被赋予一个狭窄的职责说明——`security-auditor`。

有必要区分两个容易被混淆的概念。真正的并发来自主代理在单次回合中生成多个子代理,然后由运行框架同时执行它们。后台化——将长时间运行的子代理发送到后台,使主线程不被阻塞——是另一种独立的便利机制,其按键绑定因平台和 Claude Code 版本而异,因此请检查你自己的按键绑定,而不要轻信印刷的快捷键。另请注意,子代理(通过 `Agent` 工具在会话中生成)与代理视图中显示的后台会话是不同的机制。

## 流水线模式:串行处理链

并行模式适用于独立任务,而流水线模式则是为**具有明确阶段依赖关系的任务**而设计的(参见*图 4.4*)。经典示例是 bug 修复流水线:首先,确定问题的根本原因;接着,基于诊断实施修复;然后,验证修复的有效性;最后,分析修复的影响。在此过程中,每个阶段的输出作为下一阶段的输入,执行顺序不可颠倒。

<https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/the-claude-code-operating-model/img/B39394_4_4.png>

图 4.4:流水线子代理——bug 修复的四阶段串行处理链

这条流水线有一个微妙而关键的设计特征:**每个阶段都有明显不同的工具权限**。`bug-locator` 具有只读访问权限,确保诊断过程中不会修改任何内容;`bug-fixer` 具有读写和 `Bash` 执行权限,允许其编辑代码并运行构建命令;`bug-verify` 回到“只读加 `Bash`”模式,只需运行测试来验证修复的有效性,严禁任何代码更改;`bug-report` 回到纯只读状态,因为影响分析只需要查看代码和变更历史。这种权限的动态调整是流水线模式独有的安全机制,确保每个阶段只持有履行其职责所需的最小权限。

流水线模式的关键技术细节在于**交接契约**:每个阶段的输出格式必须严格匹配下一阶段的输入要求。

以下是一个完整的四阶段 bug 修复流水线示例定义。


name: bug-locator

description: 定位缺陷的根本原因

tools:

  • Read

  • Grep

  • Glob

permissionMode: plan


你是一名缺陷定位专家。你的任务是识别缺陷的根本原因。

定位流程

  1. 理解症状:分析错误信息和复现步骤。

  2. 搜索相关代码:使用关键词和文件模式定位可疑区域。

  3. 追踪调用链:从错误点向上追溯至根本原因。

  4. 确认根本原因:明确识别导致问题的具体代码行。

输出格式(下游阶段依赖此格式,请严格遵守)

根本原因文件:[file_path:line_number]

问题描述:[用一句话总结根本原因]

调用链:[从入口点到错误位置的完整路径]

修复方向:[修复的简要思路]


name: bug-fixer

description: 根据定位结果修复缺陷

tools:

  • Read

  • Grep

  • Glob

  • Edit

  • Write

  • Bash


你是一名缺陷修复专家。你将收到 bug-locator 输出的定位结论,并据此实施修复。

修复原则

  1. 最小改动:仅修改必要的代码,避免无关重构。

  2. 不引入新问题:确保修复不会破坏现有功能。

  3. 风格一致:遵循项目现有的代码风格。

  4. 防御性代码:添加必要的保护措施,防止类似问题再次发生。

输出格式

修改的文件:[file_path_1, file_path_2, ...]

每处修改的原因:[逐一说明]

潜在副作用:[如有,请详细说明]

建议的测试命令:[用于验证修复的具体命令]

值得注意的是,bug-locator 的输出格式明确包含了"根本原因文件"和"修复方向",这正是 bug-fixer 开始工作所需的关键输入;同时,bug-fixer 的输出涵盖"修改的文件"和"建议的测试命令",这反过来又为 bug-verify 阶段的验证提供了核心依据。这种**上下游之间严格的格式契约**构成了可靠流水线运行的基石。

Unix 管道体现了同样的思想,例如命令 `cat log.txt | grep ERROR | sort | uniq ‑c | sort ‑rn`,

管道的实际执行过程如下:

用户指令:修复 issue #123 中报告的支付失败问题

主智能体协调过程:

  1. 委派给 bug-locator(独立上下文)

任务:分析根本原因

结论:src/payment/charge.ts:89 处缺少空值检查

  1. 传递给 bug-fixer(独立上下文)

输入:上述定位结论

任务:实施代码修复

结论:在 charge.ts:89 处添加了空值检查逻辑

  1. 传递给 bug-verify(独立上下文)

输入:修复后的文件和建议的测试命令

任务:运行测试以验证修复的有效性

结论:支付模块的全部 12 个测试用例均通过

  1. 传递给 bug-report(独立上下文)

输入:修复和验证的完整记录

任务:分析影响范围并生成报告

结论:修复范围可控,无副作用,建议今后补充空值边界测试

最终结果:四个子智能体各自在隔离的上下文中完成了深入工作,主对话仅接收并展示了四条简洁、结构化的关键结论。

基于团队:一种自组织协作机制。请注意,Agent Teams 是实验性功能,默认处于禁用状态——你需要通过 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS 环境变量来启用它——因此,如果读者在没有启用该功能的情况下尝试此模式,将会发现它不可用。

基于团队的模型是子代理模式中最复杂、最强大的架构。它与前面介绍的四种模式有着本质区别:在前四种模式中,子代理仅在任务执行期间活跃,任务完成后即终止,表现出“一次性”的特征;而在基于团队的模型中,子代理具有长期持久性。就像一个真实的团队一样,它们可以持续协作、实时沟通,并自主分工。

一个 Agent Team 由三个核心要素组成(参见图 4.5)。

  • 团队负责人(Team Lead):负责任务分解、资源调度和进度跟踪,类似于项目经理。

  • 队友(Teammates):由具有不同专业领域的子代理组成,各自负责特定的功能模块。

  • 共享基础设施:包含两个关键机制。任务列表(Task List)确保所有成员能够实时掌握整体进度和自己的待办事项;信箱(Mailbox)支持成员之间的异步通信(例如,前端开发者可以通过该机制与后端开发者确认 API 规范)。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/the-claude-code-operating-model/img/B39394_4_5.png

图 4.5:基于团队的子代理——多会话自组织协作

基于团队的子代理模型适用于四种典型的协作模式。

  • 竞争假设模式:当面对原因不确定的复杂问题时(例如,性能瓶颈究竟是源于数据库查询还是网络 IO),可以派遣多个子代理基于不同假设并行调查。最终,由团队负责人比较各调查报告,选择证据最充分的结论。

  • 并行审查模式:对于同一代码变更,可以同时进行安全审查和功能审查。两位审查者独立工作、互不干扰,最终将意见合并。这种模式不仅比串行审查更高效,多视角的独立性也显著提升了审查质量。

  • 模块归属模式:在大规模重构任务中,按模块划分代码库并分配给不同的团队成员。例如,前端子代理负责 UI 组件重构,后端子代理负责 API 层重构,数据层子代理负责数据库迁移。每个子代理深入专注于其分配的模块,并通过信箱机制协调跨模块的接口变更。

  • 提案审批模式:一个子代理提出重构方案,另一个子代理充当“魔鬼代言人”,专门提出质疑和反对意见。团队负责人根据双方的论据做出最终决策。这种对抗式设计有效避免了方案中的盲点。

什么时候应该使用基于团队的模型,而不是简单的子代理模型?一个实用的判断标准是协作的复杂度:如果子代理需要多轮沟通和深度协调,就应选择基于团队的模型;如果每个子代理只需执行单一任务直至完成,简单的并行或流水线模式就足够了。

需要注意的是,基于团队的模型会带来更高的运行开销——维护长期上下文窗口意味着持续的 token 消耗。因此,该模式仅在真正需要复杂协作的场景中才具有成本效益。

Kage 的笔记

超过两层的嵌套会使系统的推理和调试变得明显更加困难。请将此视为一条经验法则,而非强制上限:Claude Code 允许子代理生成自己的子代理,实际限制取决于 harness 版本,而非一个值得记住的数字。如果你发现自己触及了第三层,请将其视为分解方式需要重新调整的信号。

对于大多数日常开发场景,仅使用只读模式和执行模式通常就足够了。注意不要过度设计。

子代理与技能之间的协作

在前面介绍的五种模式中,子代理展现了出色的任务分解和隔离能力,但它们回避了一个核心问题:子代理的“专家知识”究竟从何而来?

以流水线模式中的 bug-fixer 为例。它的任务是修复一个空值检查 bug,并配备了 Read、Edit、Write、Bash 等一整套工具,但它对项目的编码规范一无所知——错误处理应该使用 Result 模式还是 try-catch?日志记录应该调用 console.log 还是项目专用 logger?变量命名应该遵循 camelCase 还是 snake_case?

如果这些规范被写入项目的 CLAUDE.md 文件中,子代理确实可以自动继承它们。然而,CLAUDE.md 通常只涵盖通用规范,不太可能包含诸如“bug 修复的安全编码检查清单”这样的深层领域知识——后者显然属于技能的范畴。

技能与子代理的组合只有两种原子模式,其核心区别在于包含关系(参见图 4.6)。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/the-claude-code-operating-model/img/B39394_4_6.png

图 4.6:技能与子代理之间的两种协作方向

方向 A:子代理预加载技能(子代理包含技能)

在此模式下,子代理的定义文件在启动时通过 skills 字段预加载一个或多个技能作为领域知识库。

  • 角色定位:子代理是执行者,而技能是它的手册。

  • 适用场景:流水线中需要特定领域知识的角色。例如,bug-fixer 需要加载安全编码规范,doc-generator 需要加载文档生成流程。

方向 B:技能生成子代理(技能包含子代理)

在此模式下,技能在触发时通过 context: fork 配置自动创建一个隔离的子代理来执行任务,确保中间过程不会污染主对话上下文。

  • 角色定位:技能是调度者,子代理是执行实例

  • 适用场景:需要严格隔离和一次性完成的重型任务,如深度代码分析和批量文档生成

这两种模式的核心区别在于 System Prompt 控制权的归属。

  • 方向 A:子代理的定义文件(.md 正文)构成 System Prompt,而技能的完整内容作为领域知识上下文注入

  • 方向 B:由 agent 字段指定的 agent 类型提供 System Prompt,而 SKILL.md 的内容作为具体任务指令传入

尽管两者的底层机制完全相同,但它们的调用关系是根本对立的。

接下来,我们将通过两个示例深入探讨方向 A。第一个是 bug 修复器的改进版本。

<!-- .claude/agents/bug-fixer.md -->
---
name: bug-fixer
description: Fixes bugs based on localization results, adhering to the team's secure coding standards.
tools:
  - Read
  - Grep
  - Glob
  - Edit
  - Write
  - Bash
skills:
  - secure-coding       # Preload secure coding Skill
---

You are a bug-fixing expert. You will receive localization conclusions from the bug-locator,
and perform fixes accordingly.

在修复过程中,你必须严格遵守 secure-coding Skill 中定义的安全编码检查清单。

请特别注意以下关键点:空值检查、输入验证和错误处理模式。

修复原则

  1. 最小变更:只修改必要的代码,避免无关的重构。

  2. 零副作用:确保修复不会破坏现有功能。

  3. 标准合规:严格遵循 Skill 中定义的安全编码标准。

  4. 防御性编程:添加防御性代码,防止类似问题再次发生。

输出格式

修改的文件:[file_path_1, file_path_2, ...]

修改原因:[逐一解释每处变更的原因]

安全检查清单项:[列出 Skill 中已确认通过的检查清单项]

建议的测试命令:[用于验证修复的具体命令]

对应的 Skill 定义如下:
<!-- .claude/skills/secure-coding/SKILL.md -->

name: secure-coding

description: 用于缺陷修复和新功能开发的安全编码检查清单。

Use when fixing bugs, writing new code, or reviewing security aspects.

user-invocable: false


安全编码标准

空值防御

  • 输入验证:所有外部输入必须经过非空检查。

  • 安全访问:优先使用可选链操作符(?.)访问嵌套属性;严禁使用非空断言(!)。

  • 数据空值检查:数据库查询结果在用于业务逻辑之前,必须显式检查是否为空。

错误处理

  • 统一异常:业务逻辑异常必须使用项目统一的 AppError 类。禁止直接抛出原生异常或字符串。

  • 禁止静默失败:严禁使用空的 catch 块。捕获异常后,至少必须记录错误日志或实现重试机制。

  • 超时控制:所有异步操作必须有明确的超时时间。

日志规范

  • 专用接口:生产环境中严禁使用 console.log()。必须使用项目封装的日志对象(如 logger.error())。

  • 完整信息:错误日志必须包含唯一的错误码和完整的堆栈跟踪。

  • 数据脱敏:严禁在日志中打印任何敏感信息(包括但不限于密码和 API 令牌)。

需要注意的是,`user-invocable: false` 意味着这个 Skill 不需要出现在用户的“`/`”菜单中。它纯粹是“参考手册”类型的内容,会在子代理启动时自动预加载。

让我们看另一个跨领域的例子——一个 API 文档生成器子代理,它通过预加载“文档生成 Skill”来增强其专业能力。

<!-- .claude/agents/api-doc-generator.md -->

name: api-doc-generator

description: 通过扫描路由文件生成 API 文档。

tools:

  • Read

  • Grep

  • Glob

  • Write

  • Bash

skills:

  • api-generating ← 预加载 Skill 以注入领域知识

其中,`skills: [api‑generating]` 本质上是将一份“操作手册”交给子代理。有了明确的角色定位(API 文档专家)和嵌入 Skill 中的专业工作流(包括具体步骤、模板和脚本),子代理就从一位样样通、样样松的通用代理转变为具备专业技能的领域专家。

这两个例子——bug‑fixer 搭配 secure‑coding,以及 api‑doc‑generator 搭配 api‑generating——揭示了一个清晰的职责划分原则:Subagent 定义“身份与目标”,而 Skill 定义“方法与标准”。

具体分工如下:

Subagent(在 .md 文件中配置)负责战略层面。

Who(身份):确立角色,例如“你是一名漏洞修复专家”或“你是一名 API 文档专家”。

What(任务):明确核心目标,例如“修复漏洞”或“生成 API 文档”。

  • Where(范围):界定工作边界,例如“写入 docs/api/ 目录”

  • Output(交付物):指定输出格式,例如“返回包含修复详情的摘要”或“统计路由数量”

  • Skill(SKILL.md 及相关文件)负责战术执行

  • How(流程):详述操作步骤,例如“步骤 1:运行 detect‑routes.py → 步骤 2:分析结果”

With What(工具):指定所需资源,例如脚本 scripts/detect‑routes.py 或模板 templates/api‑doc.md。

  • By What Standard(规范):设定执行标准,例如“检查认证中间件并标记为锁定”

  • Quality(质量):定义验收标准,例如“所有路由均已归档,且 Schema 与代码严格匹配”

这种分离机制赋予了系统强大的可复用性。同一个 secure‑coding Skill 可以被 bug-fixer 预加载以指导漏洞修复,也可以被 code‑reviewer 调用来审计代码合规性;同样,api‑generating Skill 可以协助文档生成子代理创建新文档,也可以赋能 API 审查子代理验证现有文档的合规性。简而言之,Skill 是可复用的知识模块,而子代理是这些知识的灵活消费者。

<!-- .claude/skills/codebase-health-check/SKILL.md -->

name: codebase-health-check

description: 执行全面的代码健康分析

context: fork

agent: general-purpose

allowed-tools:

  • Read

  • Grep

  • Glob


当用户输入 `/codebase-health-check src/` 时,系统会自动在一个隔离的上下文中生成一个子代理。这个子代理可以独立扫描所有文件、分析问题并生成报告,而主对话只接收一份简洁的最终结果。过程中产生的大量文件内容和推理过程完全不会污染主上下文。

方案 A 和方案 B 各有其适用场景,判断标准非常直接:谁是主角?

方案 A:角色驱动(子代理是主角)。

  • 核心逻辑:任务的关键在于特定的角色身份和协作过程。

  • 场景示例:在流水线中,一个 bug 修复者需要持续运用专业知识来修复漏洞。在这里,子代理是一个长期的“专家”,而 Skill 只是它工具箱中的一把“扳手”。

  • 结论:当你需要维护角色状态、进行多轮交互或复杂协作时,选择方案 A。

方案 B:知识/流程驱动(Skill 是主角)。

  • 核心逻辑:任务的关键在于执行标准化的知识流程,执行者本身并不重要,甚至需要被隔离。

  • 场景示例:codebase-health-check 是一个标准化的检查流程。在这里,Skill 定义了“怎么做”,子代理仅仅是为了隔离上下文而创建的临时“执行容器”。

  • 结论:当任务是一次性的、需要严格上下文隔离、或者重点是复用标准化流程时,选择方案 B。

Token 经济学

“Kage,子代理会增加 API 调用成本吗?”小冰问了一个非常实际的问题。

这个问题的答案可能令人意外:在大多数场景下,合理使用子代理不仅不会增加成本,反而能显著降低总 token 消耗。

底层的“token 经济学”原理值得深入探讨。

首先,让我们分析没有子代理的情况。假设主 Claude 直接执行一个测试任务,产生 10,000 个 token 的测试输出。由于 Claude 的 API 通常是无状态的,每次调用都需要传输完整的上下文历史。因此,一旦这 10,000 个 token 的测试内容进入主对话上下文,它们将在后续每一轮交互中被重复发送。如果还有五轮对话,这些来自测试的“噪音”数据将被额外冗余传输五次。

没有子代理时的成本计算如下:

假设:

  • 系统提示词:5,000 tokens

  • 已有对话历史:8,000 tokens

  • 测试输出(噪音):10,000 tokens

后续 5 轮每轮增加约 1,000 tokens。

每轮 token 消耗明细:

  • 第 5 轮:5,000(系统)+ 8,000(历史)+ 10,000(测试)= 23,000

  • 第 6 轮:5,000(系统)+ 9,000(历史+新增)+ 10,000(测试)= 24,000

  • 第 7 轮:5,000(系统)+ 10,000(历史+新增)+ 10,000(测试)= 25,000

  • 第 8 轮:5,000(系统)+ 11,000(历史+新增)+ 10,000(测试)= 26,000

  • 第 9 轮:5,000(系统)+ 12,000(历史+新增)+ 10,000(测试)= 27,000

─────────────────────────────────────────────────────

第 5–9 轮总消耗:125,000 tokens

现在让我们看看有子代理的场景。在这种模式下,测试输出被严格隔离在子代理的独立上下文中,主对话只接收一份约 100 tokens 的测试摘要。

有子代理时的成本计算如下:

子代理内部(独立上下文):

  • 第 1 轮:5,000(系统)+ 500(任务描述)= 5,500

  • 第 2 轮:5,000(系统)+ 500(任务)+ 3,000(部分输出)= 8,500

  • 第 3 轮:5,000(系统)+ 500(任务)+ 6,000(累计输出)= 11,500

  • 第 4 轮:5,000(系统)+ 500(任务)+ 10,000(完整输出)= 15,500

子代理总消耗:41,000 tokens

主对话(仅接收 100-token 摘要):

  • 第 5 轮:5,000(系统)+ 8,000(历史)+ 100(摘要)= 13,100

  • 第 6 轮:5,000(系统)+ 9,000(历史+新增)+ 100(摘要)= 14,100

  • 第 7 轮:5,000(系统)+ 10,000(历史+新增)+ 100(摘要)= 15,100

  • 第 8 轮:5,000(系统)+ 11,000(历史+新增)+ 100(摘要)= 16,100

  • 第 9 轮:5,000(系统)+ 12,000(历史+新增)+ 100(摘要)= 17,100

主对话总消耗:75,500 tokens

总体成本统计:

总消耗:41,000(子代理)+ 75,500(主对话)= 116,500

─────────────────────────────────────────────────────

节省的 tokens:125,000 - 116,500 = 8,500

节省比例:约 6.8%。

趋势分析:随着后续对话轮数的增加,由于避免了大量噪音数据的重复传输,token 节省比例将显著上升。

在这个例子中,仅五轮后续对话就带来了 6.8%的成本节省。如果对话延长到 10 轮或 20 轮,节省比例将进一步增加。在某些极端场景下(例如在海量日志分析后继续进行多轮深入讨论),token 节省率甚至可能超过 50%。

当然,必须指出的是,Anthropic 的 Prompt Caching 机制会在一定程度上削弱上述成本优势。该机制对命中缓存的 tokens 仅收取标准价格的 10%,这意味着主对话中反复传输的测试输出数据在缓存时实际成本要低得多。将缓存因素纳入重新计算后,本示例中的成本节省从 6.8%降至 1%–2%。

然而,成本优化只是子代理价值的一个维度,甚至不是核心价值。还有另外两个关键优势是 prompt caching 机制无法替代的。

上下文窗口保护。大模型的上下文窗口容量有限。当大量噪音数据占用存储空间时,Claude 可能会提前触发上下文压缩。在此过程中,关键信息很容易因被视为“低优先级”而丢失。通过将噪音严格隔离在独立上下文中,子代理有效地为主对话保留了宝贵的上下文空间,确保核心信息的完整性。

  • 提升响应质量。干净的上下文环境能显著增强 Claude 的专注力。当对话历史充斥着无关的日志记录和测试输出时,Claude 的“注意力机制”会被分散,导致生成质量下降——这正是小冰最初面临的典型困境。子代理的隔离架构从根本上解决了这种“注意力稀释”问题,确保主模型始终基于高质量、高相关的信息进行推理和响应。

  • 那么,在什么场景下引入子代理会适得其反?这里有一个简洁的决策规则:考察输入与输出的体量比。

高价值场景(输入 >> 输出):当输入数据量远超最终输出时,子代理的价值尤为显著。例如,分析数百行系统日志只为提取五条关键结论。

低价值场景(输入 ≈ 输出):当输入和输出体量大致相当时,子代理的隔离开销就不再值得。例如,修改单个函数或编写一条代码注释,产生的中间数据量有限。在这种情况下,直接在主线对话中完成任务往往更简单高效,且不会引入额外的架构复杂性和通信成本。

Kage 的笔记

启用子代理的决策可以概括为以下四个核心维度。

  • 大规模文件读取(输入噪音隔离):当任务需要读取大量文件(通常超过五个)或处理海量数据源时,使用子代理可以将文件读取过程中产生的“输入噪音”完全隔离在独立环境中,防止主对话上下文被冗余文件内容迅速填满。

  • 高频输出生成(输出噪音过滤):当任务预计会产生大量中间输出时,例如全面的测试报告、详细的系统日志或完整的构建过程记录,子代理充当“过滤器”,只向主对话返回简洁的摘要或关键错误信息。这可以防止冗长的输出数据稀释主模型的注意力并浪费 tokens。

  • 上下文完整性保护(长期任务规划):当当前任务只是复杂工作流的一部分时,必须确保主对话保留足够的上下文空间来支持后续轮次的深度交互或推理。通过卸载高消耗的子任务,子代理机制有效地保护了主对话的“上下文窗口”不被一次性耗尽,确保长期任务信息的连贯性和完整性。

  • 操作权限与安全边界(风险管理):当任务涉及敏感操作(如文件系统写入、网络请求或执行外部命令)或需要严格的权限控制时,子代理可以配置为具有受限权限的独立沙箱环境。即使子任务失败或表现出恶意行为,其影响也被限制在局部范围内,为主系统提供了天然的安全防火墙。

软件工程视角下的子代理

至此,敏锐的读者可能已经意识到:子代理架构并没有凭空发明任何颠覆性的全新概念。其核心本质在于将软件工程领域数十年来积累的经典智慧,创造性地迁移和映射到 AI Agent 的架构设计中。

这种迁移绝非偶然,而是逻辑的演进。因为 AI Agent 本质上就是一个复杂的软件系统,它必然遵循软件系统工程的一般规律和核心原则。

单一职责原则

单一职责原则位居面向对象五大设计原则之首。其核心定义振聋发聩:一个类(或模块)应该只有一个引起变化的原因——换言之,它应该专注于做一件事,并把它做好。子代理架构正是这一经典原则在 AI Agent 领域的完美体现和实现。

  • 职责专门化:code-reviewer 只专注于代码质量审查;test-runner 只负责执行测试用例并收集结果;bug-locator 致力于定位错误的根本原因。每个子代理就像一个高度内聚的独立类,各司其职,不越雷池。

  • 声明式职责规范:每个子代理的 prompt file(如 code‑reviewer.md)本质上就是它的“职责说明书”。

  • 变更隔离与可维护性:例如,如果你需要调整代码审查的严格程度或引入新的安全标准,开发者只需修改 code‑reviewer.md 文件

舱壁模式

舱壁模式是一种借鉴自造船业的韧性设计模式:货船的船体被坚固的隔板分成多个独立的水密舱室。即使一个舱室破损,海水也被限制在局部,不会使整艘船沉没。

在微服务架构中,这种模式被广泛采用,以防止单点故障引发级联崩溃。在 AI Agent 架构中,子代理的上下文隔离机制正是舱壁模式的完美映射。

  • 故障隔离与边界锁定:如果 log-analyzer 子代理在处理海量日志时遇到异常数据(如无限循环、幻觉或格式崩溃),由此产生的“故障”被严格限制在子代理的独立沙箱内

  • 主系统稳定性保障:得益于舱壁,子任务的混乱不会污染主对话的上下文环境

  • 优雅降级:这种设计确保了系统的韧性

MapReduce 模式

MapReduce 模式源于分布式计算领域,最早由 Google 在 2004 年的一篇论文中提出。该模式将大规模数据处理分解为两个阶段:Map(映射)和 Reduce(归约)。Map 阶段将任务分发到多个工作节点并行处理,Reduce 阶段则聚合所有节点的结果。

并行子代理架构是 MapReduce 模式在 AI Agent 领域的创新应用。在该架构中,主代理充当协调者,负责任务分发(Map)和结果聚合(Reduce),而每个子代理作为工作节点,专注于执行并行的 Map 计算任务。

责任链模式

责任链模式是四人帮(GoF)设计模式之一,它定义了一条处理链,请求沿链传递,直到被某个处理器接收并处理。流水线式子代理架构是该模式的一种变体:与传统的由“一个”处理器终止请求的链不同,流水线要求“每一个”处理器都处理请求的一部分,并将中间结果传递给下一阶段。Unix 管道、Java Servlet 过滤器链和 Node.js Express 中间件都是这一思想在不同技术栈中的具体体现,而子代理流水线则是这个家族的最新成员。

这些对应关系并非牵强附会,而是揭示了一条更深层的规律:优秀的架构原则具有跨领域的普适性。无论是设计微服务系统、分布式计算框架,还是构建 AI Agent 架构,开发者始终面临相同的基本挑战——如何分解复杂性、如何隔离故障、如何实现高效协作。精通经典设计模式的工程师在涉足 AI Agent 设计时具有天然直觉优势,因为他们已在其他领域反复验证了这些思想的有效性。

听完后,小雪若有所思地点点头说:“所以学习设计模式不仅仅是为了通过面试——这些思想在 AI 时代更加有用。”

Kage 点头说道:“软件工程的核心智慧从未过时;它只是在每一波新技术浪潮中找到新的表达形式。”

实践注意事项

在实践中使用子代理时,有几个容易被忽视的细节值得提前了解,因为它们往往决定了子代理能否在生产环境中稳定运行。

首先是 CLAUDE.md 的继承关系。项目根目录中的 CLAUDE.md 文件适用于子代理——子代理在启动时会加载这个“项目记忆”,以及项目的 MCP 服务器和 Skills。这意味着你在那里写的任何编码规范都会被每个生成的子代理继承,无需在每个定义中重复。

其次是“消息传递”的上下文传递模式。子代理之间不能直接通信:每个子代理只能访问主代理显式传递的内容,无权访问主对话的历史记录,也无法感知其他子代理的存在。这种信息交换机制是“消息传递”而非“共享内存”——主代理必须从子代理 A 中提取结论,并将其嵌入到子代理 B 的任务描述中,B 才能获取相关信息。深入理解这一机制对于设计高效流水线至关重要:务必确保每个阶段的输出格式高度结构化,以便主代理能够准确提取并顺畅转发信息。

然后是中断恢复机制。如果子代理在执行过程中因网络波动或手动终止而中断,存储在内存中的任何中间工作都将丢失。对于长时间运行或关键任务,一个有效策略是让子代理将中间产物持久化到 file。例如,日志分析子代理可以实时将已分析范围和发现的异常记录到 .claude‑work/log‑analysis.md 文件中。当任务需要重新启动时,新的子代理可以先 read 该文件,精确定位断点并恢复工作,从而避免从头开始的冗余工作。

将嵌套保持在两层左右是出于可维护性的考虑,而不是因为系统禁止更多层

总结

子代理是 Claude Code 扩展系统中架构意义最重大的功能。如果说 CLAUDE.md 赋予 Claude 记忆能力,Skills 赋予它领域专业知识,那么子代理则提供了更根本的能力:分解复杂性的能力。当面对跨越多个维度、伴随大量噪音、需要多阶段处理的复杂任务时,子代理提供了一种结构化的应对策略:将整体任务分解为若干聚焦明确的子任务,在隔离的上下文中独立执行,最后只将提炼后的核心结论聚合回主对话流。

这一策略的价值不仅体现在技术层面(如上下文隔离、权限约束和 token 利用效率优化),更深刻地体现在它所培养的思维方式上。学会使用子代理,本质上就是掌握一种“委托式思维”:不再将所有细节塞进单一对话,而是像一位经验丰富的管理者,清晰地定义每个任务的职责边界、能力要求和交付标准,并将其委托给合适的执行者。这种思维与软件工程中的模块化设计、微服务架构和关注点分离原则高度一致——它们都在应对一个永恒的挑战:如何有效管理复杂性。

五种设计模式(只读、执行、并行、流水线和团队)并非孤立的技术,而是一个递进的能力谱系。从最基础的只读观察到最先进的多对话自组织协作,它们全面覆盖了从日常开发任务到大型工程项目的各种场景。一旦掌握了这五种设计模式和底层软件工程原则,你将拥有为任何新场景构建自适应子代理架构的直觉和能力。

在第 5 章中,我们将深入探讨另一个核心扩展机制——基于 Hooks 的事件驱动自动化。如果说 Skills 和子代理主要作用于 Claude 的“认知层面”,那么 Hooks 则将其控制延伸到“系统执行层面”:它们不再仅仅引导 Claude“如何思考”,而是在物理执行层面直接拦截和约束其行为。

讨论问题

  1. 假设你的团队需要构建一个自动化 PR 审查流程,要求同时执行代码质量检查、安全漏洞扫描和性能分析,最终将结果汇总为一份综合审查报告。基于此,设计一个子代理架构:需要部署多少个子代理?应采用哪种设计模式(并行处理还是流水线运行)?每个子代理的工具权限应如何配置?

  2. 在Token 经济学部分,我们是在“后续五轮对话”的假设下讨论的。现在进一步思考:如果任务执行后没有后续交互(即一次性执行),子代理架构是否仍有成本优势?在哪些具体条件下,引入子代理反而可能导致 token 消耗激增,从而增加总体成本?

  3. 回顾本章,我们深入探讨了五种子代理设计模式,并分析了其背后的核心软件工程原则。现在,将注意力转向你的实际项目:是否存在一个特别适合通过引入子代理架构来解决的痛点场景?如果有,该场景与哪种设计模式相契合?请结合你的具体业务逻辑,解释选择该模式的深层原因。

5

第五章:从指南到护栏:用 Hooks 实现自动化控制

“凡事预则立,不预则废。” —— 本杰明·富兰克林

公司出事了。

那是一个深夜。小张正在处理一个紧急需求。他用 Claude Code 编写代码,本地测试通过后,执行了 git push 将代码上传到远程仓库,然后关机回家休息。

第二天早上九点,团队聊天炸了:“谁推送了 .env 文件?API key 已经暴露了。 所有密钥必须立即撤销并重新签发!”

小张很清楚 .env 文件绝不能提交,但由于熬夜工作疲惫,他在提交前疏忽了检查。然而,误提交的 .env 文件包含三个环境的数据库连接字符串、一个内部 API key,以及支付网关的测试账户密码。这个错误让整个团队花了半天时间来轮换密钥、审计访问日志并修复所有引用。

作为他的直属主管,小冰的第一反应是:“这种错误本可以被自动拦截; 为什么 没有 机制来防止它?”

“因为之前所有的机制都只是 ‘建议’ Claude 该做什么,”Kage 解释道。“CLAUDE.md 是建议,Skills 是引导,但从来没有人告诉 Claude,‘你绝对不能做这个。’ Hooks 则不同: 它们不是建议,而是强制执行。”

小雪立刻接话道:“就像 Web 开发中的中间件?请求通过之前先做检查?”

“正是。”Kage 点点头。“无论是 Express 中间件链、Django 的请求处理层,还是 Spring 的拦截器,它们的核心逻辑都是一样的:在操作执行前后插入额外的处理逻辑。Hooks 正是这一概念在 AI Agent 语境下的实现。”

Hooks 在 Claude 扩展系统中的角色

在深入技术细节之前,让我们先在目前已学习的扩展体系中定位 Hooks。

前四章介绍的三种机制构成了一个“影响力递增”的谱系:CLAUDE.md 建立项目规范,Claude 在每次对话开始时读取;Skills 封装领域工作流,可在 Claude 需要时自动或手动触发;子代理通过上下文隔离实现任务委托。

这三种机制有一个共同特点:它们都在 Claude 的认知层面运作。CLAUDE.md 告诉 Claude“遵循什么规范”,Skills 引导它“如何处理某些问题”,子代理指示它“将子任务委托给谁”。然而,这些本质上仍然只是建议性指导。作为一个语言模型,Claude 理论上能够忽略 prompt 中的任何约束。

Hooks 则在一个完全不同的层面运作。它们不作用于 Claude 的认知,而是在系统执行层直接拦截其行为。

当 Claude 尝试调用 Bash 工具执行 rm ‑rf / 时,PreToolUse Hook 不是“劝说”Claude 放弃该操作,而是在系统层面直接阻止该工具调用的执行,这意味着 Claude 的决定被强制覆盖,无法实施。

用软件工程的话来说,这种区别就是策略与机制的分离。CLAUDE.md 和 Skills 定义策略——应该做什么。Hooks 提供机制:当策略被违反时,命令本身在执行前就被阻止。注意这里被阻止的是什么。它不是 prompt,也不是用户试图达成的任务;而是单个工具调用。正如操作系统中的文件权限管理不依赖于应用程序自我约束,Hook 对 Claude 的约束也不依赖于 prompt 中的引导。

表 5.1 从四个维度比较了 CLAUDE.md、Skills 和 Hooks 的差异。

| 认知层 | | CLAUDE.md | | Skills | | Hooks |

| --- | --- | --- | --- | --- | --- | --- |

| 认知层 | | 执行层 | | 触发方式 | | 始终加载 |

| --- | --- | --- | --- | --- | --- | --- |

| 语义匹配/显式触发 | | 事件自动触发 | | 约束性质 | | 建议 |

| 引导 | | 强制 | | 对 Claude 的控制 | | 引导行为 |

| 治理工作流 | | 拦截/阻止 | | 类比 | | 交通标志 |

| 驾驶手册 | | 路障/限速器 | | 变量 | | 范围 |

表 5.1:CLAUDE.md、Skills 和 Hooks 的差异

就像这样:交通标志(CLAUDE.md)告诉你“此处限速 60 公里/小时”,驾驶手册(Skills)教你“弯道减速”,而路障/限速器(Hooks)则从物理上阻止你的车达到 120 公里/小时。最好的效果来自三者结合使用:交通标志让你意识到规则,手册教你如何操作,路障/限速器确保即使你忘记规则或判断失误,系统仍能守住安全底线。

事件生命周期:二十多个事件

Claude Code 事件系统从早期版本的 7 个事件发展到如今的二十多个,并且几乎每个版本都在持续增长。与其给出一个在你阅读时已经过时的数字,不如查阅官方 hooks 参考文档获取实时列表。重要的是分类:会话级事件、工具调用事件、子代理事件、完成事件,以及一长串较新的事件(Setup、PermissionDenied、PostToolBatch、PreCompact、TaskCreated、FileChanged、Elicitation 等)。你不需要记住它们;你需要的是分组逻辑和每组的特点。

会话级事件

会话级事件管理整个对话的生命周期,主要包括以下三个关键节点。

SessionStart

在会话启动或恢复时触发。其核心能力是通过 CLAUDE_ENV_FILE 注入环境变量。Hook 脚本可以将 export 语句写入该文件,使变量对所有后续 Bash 命令生效。这意味着你可以在会话开始时自动配置开发环境。参见以下代码示例。






```bash

#!/bin/bash

# session-init.sh — SessionStart Hook

if [ -n "$CLAUDE_ENV_FILE" ]; then

    echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"

    echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"

fi

exit 0

SessionEnd

在会话结束时触发。其匹配器可以区分不同的终止原因:clear(用户清除)、logout(登出)或 prompt_input_exit(用户退出输入)。此事件通常用于清理临时资源或记录会话统计信息。

PreCompact

在上下文压缩之前触发。其匹配器可以区分手动(用户发起的 /compact)和自动(上下文窗口已满时的自动压缩)。此事件适用于在压缩前备份完整的对话记录。

工具调用事件

这是最关键的一类事件,涵盖了 Claude 每次工具调用的整个生命周期。工具调用事件主要包含以下五个关键节点。

PreToolUse(整个 Hooks 系统中最强大的事件)

在 Claude 决定调用工具之后、工具实际运行之前触发。其 permissionDecision 可以为 allow(跳过权限检查并运行)、deny(阻止调用并给出原因)、ask(将决定权交给用户)或 defer(交由正常权限流程处理)。参数重写是一个独立机制,而非第四种决策:在 hookSpecificOutput 下放置一个 updatedInput 对象,工具便会使用你提供的参数运行。

“调整输入参数”的能力尤其强大:它允许你在不中断操作的情况下,静默地为命令添加安全参数。请参见以下代码示例。


{

  "hookSpecificOutput": {

    "hookEventName": "PreToolUse",

    "permissionDecision": "allow",

    "updatedInput": {

      "command": "rm -rf /tmp/test --dry-run"

    }

  }

}

在上面的示例中,原本危险的命令 rm ‑rf /tmp/test 被静默修改为 rm ‑rf /tmp/test ‑‑dry‑run,让 Claude 能够继续完成任务,同时避免实际删除文件的风险。

PostToolUse

在工具运行之后触发。它无法撤销已经发生的事情,但有两个核心用途:通过 additionalContext 向 Claude 反馈额外信息(例如 lint 结果),以及对结果进行后处理(自动格式化刚刚写入的文件)。它还可以通过返回 updatedToolOutput 字段直接替换工具的输出。这适用于任何工具,而不仅仅是 MCP 工具。

PostToolUseFailure

在工具执行失败后触发,主要用于错误告警和提供纠错反馈。

PermissionRequest

在权限对话框即将出现时触发。与 PreToolUse 的关键区别在于时机:PreToolUse 在每次工具调用前无条件触发,而 PermissionRequest 仅在 Claude 需要用户手动确认权限时才会激活。通过此事件,你可以以编程方式批准或拒绝权限请求。请参见以下代码示例。


{

  "hookSpecificOutput": {

    "hookEventName": "PermissionRequest",

    "decision": {

      "behavior": "allow",

      "updatedPermissions": {}

    }

  }

}

UserPromptSubmit

在用户提交输入后、Claude 开始处理前触发。此事件通常用于输入预处理或上下文注入场景。例如,在用户每次发送消息时自动追加当前 Git 分支信息。

Subagent 事件

Subagent 事件可与第 4 章介绍的 subagent 机制配合使用。

SubagentStart

在 subagent 启动时触发。其 matcher 可根据 subagent 的类型名称进行过滤,既支持内置类型(如 Bash、Explore、Plan),也支持定义在 .claude/agents/ 目录下的自定义 subagent(如 code-reviewer)。虽然 SubagentStart 无法阻止 subagent 启动,但它通过 additionalContext 在运行时注入关键上下文信息。例如,每次 code-reviewer 启动时自动加载团队的编码规范。

SubagentStop

在 subagent 完成任务后触发。其行为逻辑与全局 Stop 事件完全一致:你既可以放行停止操作,也可以拦截请求,强制 subagent 继续工作,直到满足特定质量标准。此外,SubagentStop 的输入数据包含两个关键路径:transcript_path(主会话记录)和 agent_transcript_path(subagent 自身的对话记录)。借助这些信息,Hook 脚本可以审查 subagent 的完整工作流程,从而精确评估其输出质量。

完成事件

Stop

在 Claude 完成一个完整响应周期时触发。这是实现“质量门禁”的核心机制:如果输出不符合预设标准(如编码规范、安全策略等),你可以通过设置 decision: "block" 阻止会话结束,强制 Claude 继续修改或完善工作,直至满足要求。

Notification

在 Claude 发送系统通知时触发。其 matcher 可以精确区分不同类型的通知,如 permission_prompt、idle_prompt 或 auth_success。此事件常用于集成自定义通知渠道——例如,将关键告警转发到 Slack 或 Discord,或触发本地桌面弹窗提醒。

较新的事件类型

2025 年至 2026 年间,Claude Code 扩展了其事件系统,新增了若干关键事件类型,以支持更复杂的协作与运维场景。

TeammateIdle 与 TaskCompleted

专为多 agent 团队协作(Agent Teams)而设计。前者在队友 agent 即将进入空闲状态时触发,后者在任务被标记为完成时触发。

ConfigChange

在配置文件发生变化时触发。此事件主要用于审计与合规,帮助开发者追踪设置变更历史,防止未经授权的配置修改。

WorktreeCreate 与 WorktreeRemove

分别对应 GitWorktree 的创建与删除。通过拦截这些事件,用户可以为版本控制工作流自定义初始化设置(如自动安装依赖)或清理逻辑(如删除临时构建产物)。

“能否被阻止”:最关键的维度

在所有 17 个事件中,“能否被阻止”是最根本的分类维度。它决定了一个事件是用于“控制流程”还是仅用于“观察与记录”。

可被阻止的事件包括 PreToolUse、PermissionRequest、UserPromptSubmit、UserPromptExpansion、Stop、SubagentStop、TeammateIdle、TaskCreated(退出码 2 会回滚任务创建)、TaskCompleted、ConfigChange、PostToolBatch、PreCompact(退出码 2 会停止压缩),以及两个 Elicitation 事件。

其余事件(如 PostToolUse、Notification、SubagentStart 等)均为只读。它们主要用于读取上下文、注入附加信息或触发副作用(如发送通知),但无法直接阻止或修改 Claude 的核心执行逻辑。

在日常开发与运维中,最常用的三个事件是:PreToolUse(工具执行前的守门员)、PostToolUse(工具执行后的质量守护者)和 Stop(任务完成时的质量门禁)。如果时间有限,优先掌握 PreToolUse、PostToolUse 和 Stop,就能构建起稳健的自动化闭环。

配置系统:6 个位置,6 种用途

Claude Code 的配置系统采用分层叠加机制,优先级自上而下递减。Hook 配置使用标准 JSON 格式,并严格遵循六层优先级架构(见图 5.1)。这种设计允许开发者根据作用范围灵活部署自动化逻辑。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/the-claude-code-operating-model/img/B39394_5_1.png

图 5.1:Hook 配置的六个层级与作用范围

项目配置(.claude/settings.json)是团队协作的核心载体。一旦提交到 Git 仓库,所有成员在克隆项目时都能自动同步团队约定的安全检查与自动化规则。本地项目配置(.claude/settings.local.json)被 .gitignore 忽略,适用于需要覆盖团队默认配置的个人场景。用户全局配置(~/.claude/settings.json)用于管理跨项目的个人偏好,如自定义日志格式或桌面通知方式。

配置结构采用三级嵌套设计:事件类型 → matcher 分组 → Hook 处理器列表。参见以下代码示例。


{

  "hooks": {

    "PreToolUse": [

      {

        "matcher": "Bash",

        "hooks": [

          {

            "type": "command",

            "command": "./.claude/hooks/block-dangerous.sh",

            "timeout": 30

          }

        ]

      }

    ],

    "PostToolUse": [

      {

        "matcher": "Write|Edit",

        "hooks": [

          {

            "type": "command",

            "command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r

              '.tool_input.file_path'); prettier --write \"$FILE\""

          }

        ]

      }

    ]

  }

}

matcher 字段用于指定该组 Hook 所适用的工具范围。

  • "Bash":匹配所有 Bash 调用
  • "Write|Edit":匹配 Write 或 Edit 工具(管道符 | 表示逻辑“或”)
  • "*":匹配所有工具

对于 Stop、Notification、UserPromptSubmit 等生命周期事件,matcher 字段会被忽略,因为这些事件并不针对特定工具。而在 SubagentStart 或 SubagentStop 事件中,matcher 匹配的是 subagent 类型名称,而非工具名称。

三类处理器:确定性的阶梯

Hook 处理器共有五种类型。其中 command、prompt 和 agent 三种构成了一架确定性递减、灵活性递增的阶梯,选择哪一种取决于检查需要多严格。另外两种是集成接口,而非阶梯上的层级:http 将事件 JSON POST 到你控制的 URL,mcp_tool 则调用已连接的 MCP 服务器上的工具。/hooks 菜单列出了全部五种。

Command 类型:确定性规则

此类型用于执行 shell 命令或脚本。作为最常用、最可靠的类型,确定性规则总是比大模型做出的判断更值得信赖。参见以下代码示例。


{

  "type": "command",

  "command": "./.claude/hooks/check-security.sh",

  "timeout": 30

}

命令类型的 Hook 通过 stdin 接收 JSON 格式的上下文数据(包括 session_id、tool_name、tool_input 等),通过 stdout 输出 JSON 格式的决策,并通过退出码表达最终意图。

  • Exit code 0:成功。系统将解析 stdout 中的 JSON 作为决策依据。
  • Exit code 2:有意阻止。系统会将 stderr 的内容作为错误原因返回给 Claude。
  • 其他退出码:脚本错误。运行不会被中断,但失败不再是静默的——对话记录中会显示一个“<hook name> hook error”提示,附上 stderr 的第一行,完整的 stderr 则会写入调试日志。

Exit code 2 的设计至关重要,因为它严格区分了“有意阻止某个操作”与“脚本故障”。脚本故障不应妨碍正常工作流程——就像故障的烟雾报警器不应阻止人们进出建筑物一样。

Prompt 类型:单次大模型评估

当验证逻辑需要一定程度的判断,但不需要执行多步操作时,建议使用 prompt 类型。该类型会调用一个轻量级大模型(通常是 Haiku)来评估当前情况。参见以下代码示例。


{

  "type": "prompt",

  "prompt": "Evaluate whether this code modification introduces

    security vulnerabilities. $ARGUMENTS",

  "model": "claude-haiku-4-5",

  "timeout": 30

}

其中,$ARGUMENTS 是一个占位符,在运行时会替换为 Hook 接收到的完整输入 JSON。大模型的响应必须遵循以下 JSON 格式。

允许操作的 JSON 格式:


{"ok": true, "reason": "The code modification is safe and

  does not introduce any known vulnerability patterns."}

拒绝操作的 JSON 格式:


{"ok": false, "reason": "Potential SQL injection risk detected: user input

  is concatenated directly into the query string without escaping."}

Agent 类型:多轮智能验证

当验证逻辑需要实际检查代码文件、执行搜索或多步操作才能得出结论时,应使用 agent 类型。该类型会启动一个子代理,使其能够利用 Read、Grep、Glob 等工具进行多轮深入验证。参见以下代码示例。


{

  "type": "agent",

  "prompt": "Check whether all modified files have passed unit tests.

    Run the test suite and verify the results. $ARGUMENTS",

  "timeout": 120

}

agent 类型的子代理最多经过 50 轮对话/操作后必须返回决策。其响应格式与 prompt 类型相同,返回一个包含 ok(布尔值)和 reason(字符串)的 JSON 对象。

在选择 Hook 处理器类型时,应遵循降级原则:“尽可能使用 command 类型而非 prompt 类型,尽可能使用 prompt 类型而非 agent 类型”(参见图 5.2)。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/the-claude-code-operating-model/img/B39394_5_2.png

图 5.2:三种 Hook 处理器类型的确定性与灵活性比较

确定性规则(如模式匹配、文件名检查和正则表达式)在速度和可靠性上始终优于大模型判断。只有当验证逻辑真正需要“理解能力”(语义分析)或“代码检查能力”(多文件上下文检索)时,才应考虑升级到 prompt 类型或 agent 类型。

hookSpecificOutput:与 Claude 通信的协议

PreToolUse Hook 的输出格式在 2025 年经历了重大升级。早期版本使用顶层的 decision 和 reason 字段,而新版本建议使用嵌套在 hookSpecificOutput 对象中的 permissionDecision 格式。虽然目前两种格式都受支持,但新代码应遵循新规范。参见以下代码示例。


{

  "hookSpecificOutput": {

    "hookEventName": "PreToolUse",

    "permissionDecision": "deny",

    "permissionDecisionReason": "This command attempts to delete a

      protected system directory",

    "additionalContext": "Protected path patterns: /etc, /usr, /var"

  }

}

permissionDecision 支持四种可能的值。

  • allow:绕过权限系统,直接执行。
  • deny:阻止执行。
  • ask:将决定权交给用户。
  • defer:落入正常权限流程。

其中,ask 是一个微妙但实用的选项——它不会自动拒绝操作,而是传达“我不确定,让人类来决定”的态度。

additionalContext 字段适用于所有事件类型,其内容将被注入 Claude 的上下文。这一机制形成了高效的反馈回路。例如,PostToolUse Hook 可以使用 additionalContext 将代码静态分析(lint)结果反馈给 Claude。收到这些信息后,Claude 会自动修复问题,整个过程无需人工干预。

此外,所有事件都支持以下通用顶层字段。


{

  "continue": false,

  "stopReason": "Security violation detected, session terminated",

  "suppressOutput": false,

  "systemMessage": "Warning: This operation has been blocked by

    the security policy"

}

  • continue: false:这相当于一个“紧急刹车”。无论当前处于哪个事件阶段,该设置都会立即终止 Claude 的处理。
  • systemMessage:该字段的内容将直接显示给用户,不会传递给 Claude。

工程实践 1:安全防护体系

让我们回到本章开头提出的挑战性案例。现在,我们将使用 Hooks 构建一个完整的安全防护体系,通过以下三道防线来守护项目:危险命令拦截、敏感文件保护和全面操作审计。

PreToolUse:危险命令拦截

第一道防线旨在拦截可能造成灾难的 Bash 命令。该脚本在设计上融入了几个关键细节。

  • 调试信息输出到标准错误(stderr,即 >&2),而非标准输出(stdout)。这是因为 stdout 必须严格保留用于输出 JSON 格式的决策结果。
  • 使用 jq 工具解析输入的 JSON 数据,避免脆弱的字符串手动匹配。
  • 每次拦截操作都附有清晰具体的操作原因。

参见以下代码示例。


#!/bin/bash

# .claude/hooks/block-dangerous.sh

set -e

INPUT=$(cat)

# Extract the command (debug output goes to stderr)

COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

echo "DEBUG: Checking command: $COMMAND" >&2

# List of dangerous command patterns

DANGEROUS_PATTERNS=(

    "rm -rf /"

    "rm -rf ~"

    "rm -rf \$HOME"

    "> /dev/sd"

    "mkfs."

    ":(){:|:&};:"                         # Fork bomb

    "chmod -R 777 /"

    "git push --force origin main"

    "git push --force origin master"

    "git reset --hard origin"

    "DROP DATABASE"

    "DROP TABLE"

    "TRUNCATE"

    "curl.*| sh"                          # Dangerous pipe execution

    "curl.*| bash"

)

for pattern in "${DANGEROUS_PATTERNS[@]}"; do

    if [[ "$COMMAND" == *"$pattern"* ]]; then

        echo "BLOCKED: $pattern" >&2

        cat <<EOF

{

    "hookSpecificOutput": {

        "hookEventName": "PreToolUse",

        "permissionDecision": "deny",

        "permissionDecisionReason": "Intercepted dangerous command pattern: $pattern"

    }

}

EOF

```bash
exit 2
fi
done
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
exit 0

PreToolUse:敏感文件保护

第二道防线聚焦于保护敏感文件免遭意外修改。此 Hook 的 matcher 配置为 "Write|Edit",确保仅在文件写入或编辑操作时触发,从源头上阻断风险。请看以下代码示例。

#!/bin/bash
# .claude/hooks/protect-files.sh
set -e
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
if [ -z "$FILE_PATH" ]; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
exit 0
fi
FILENAME=$(basename "$FILE_PATH")
# Protected Filename Patterns
PROTECTED_FILES=(".env" ".env.local" ".env.production" "credentials.json"
"secrets.yaml" "secrets.json" "id_rsa" "id_ed25519")
# Protected Extensions
PROTECTED_EXTENSIONS=("pem" "key" "p12" "pfx")
# Protected Directories
PROTECTED_DIRS=(".git/" ".ssh/" "node_modules/")
# Check Directories
for dir in "${PROTECTED_DIRS[@]}"; do
if [[ "$FILE_PATH" == *"$dir"* ]]; then
        cat <<EOF
    {
      "hookSpecificOutput": {
      "hookEventName": "PreToolUse",
      "permissionDecision": "deny",
      "permissionDecisionReason": "Modifying files in protected directories is not allowed: $dir"
    }
}
EOF
exit 2
fi
done
# Check file names
for name in "${PROTECTED_FILES[@]}"; do
if [[ "$FILENAME" == "$name" ]]; then
        cat <<EOF
    {
      "hookSpecificOutput": {
      "hookEventName": "PreToolUse",
      "permissionDecision": "deny",
      "permissionDecisionReason": "Modifying sensitive files is not allowed: $name"
    }
}
# Check file extensions
exit 2
fi
done
EXT="${FILENAME##*.}"
for ext in "${PROTECTED_EXTENSIONS[@]}"; do
if [[ "$EXT" == "$ext" ]]; then
        cat <<EOF
    {
      "hookSpecificOutput": {
      "hookEventName": "PreToolUse",
      "permissionDecision": "deny",
      "permissionDecisionReason": "Modification of key files is not allowed: *.$ext"
    }
}
fi
done
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
exit 0

PostToolUse:全面操作审计

第三道防线是全面操作审计。通过配置 matcher: "*",该工具能够捕获并记录所有工具调用。在合规要求严格的企业环境中,这是不可或缺的机制,因为它清晰地回答了审计的核心问题:“Claude 何时对哪个目标执行了什么操作?”

#!/bin/bash
# .claude/hooks/audit-log.sh
INPUT=$(cat)
LOG_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/logs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/audit-$(date +%Y-%m-%d).log"
TIMESTAMP=$(date -Iseconds)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
echo "[$TIMESTAMP] $TOOL_NAME: $TOOL_INPUT" >> "$LOG_FILE"
echo '{}'
exit 0

完整配置

通过将上述三个独立脚本整合到 .claude/settings.json,便构建起一套强健的纵深防御体系。具体配置如下。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/block-dangerous.sh"
          }
        ]
      },
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/protect-files.sh" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/audit-log.sh" }
        ]
      }
    ]
  }
}

这套配置建立起覆盖“事前拦截、事中保护、事后审计”的完整安全闭环:危险命令拦截 → 敏感文件保护 → 全量操作审计。通过这种组合机制,我们得以将过去依赖“人工谨慎”的脆弱流程,升级为“代码即法律”的自动化安全体系。正如本章开头案例所演示的,若第二层保护得以应用,小张的“悲剧”完全可以避免。

工程实践二:代码质量自动化

安全防护旨在“防患于未然”,而代码质量自动化则致力于“确保交付卓越”。本节的实践案例将深入探讨 PostToolUse 与 Stop Hook 的协同工作机制。

PostToolUse:自动格式化

每当 Claude 写入文件时,系统将自动触发格式化工具。此 Hook 的精妙之处在于其实现了“关注点分离”:Claude 无需感知项目所采纳的具体格式规范,只需专注于编写代码逻辑,而格式化过程则在后台无缝完成。请看以下代码示例。

#!/bin/bash
# .claude/hooks/auto-format.sh
set -e
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
    echo '{}'
    exit 0
fi
EXTENSION="${FILE_PATH##*.}"
case "$EXTENSION" in
    js|jsx|ts|tsx|json|md|css|scss|html)
        if command -v npx &> /dev/null; then
            npx prettier --write "$FILE_PATH" 2>/dev/null
            echo '{"hookSpecificOutput":{"additionalContext":"Formatted with Prettier"}}'
        fi
        ;;
    py)
        if command -v black &> /dev/null; then
            black "$FILE_PATH" 2>/dev/null
            echo '{"hookSpecificOutput":{"additionalContext":"Formatted with Black"}}'
        fi
        ;;
    go)
        if command -v gofmt &> /dev/null; then
            gofmt -w "$FILE_PATH" 2>/dev/null
            echo '{"hookSpecificOutput":{"additionalContext":"Formatted with gofmt"}}'
        fi
        ;;
    *)
        echo '{}'
        ;;
esac
exit 0

该脚本引入了使用 -v 命令进行环境检查。若未检测到已安装格式化工具,Hook 将静默跳过该步骤而非抛出错误。这体现了“优雅降级”的设计原则:Hook 自身的异常不应阻塞核心工作流的正常运行。

PostToolUse:Lint 反馈循环

自动格式化保证了代码的“美观性”,而 Lint 检查则确保了代码的“正确性”。此方案的核心在于利用 additionalContext 将 Lint 检查结果反馈给 Claude,从而建立起“修改 → 检查 → 反馈 → 修正”的自动化闭环。请看以下代码示例。

#!/bin/bash
# .claude/hooks/lint-check.sh
set -e
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
if [[ "$FILE_PATH" == *.js || "$FILE_PATH" == *.ts || "$FILE_PATH" == *.jsx || "$FILE_PATH" == *.tsx ]]; then
    LINT_RESULT=$(npx eslint "$FILE_PATH" 2>&1) || true
    if [ $? -ne 0 ]; then
        ESCAPED=$(echo "$LINT_RESULT" | head -30 | jq -Rs '.')
        echo "{\"hookSpecificOutput\":{\"additionalContext\":\"ESLint found problems:\n${ESCAPED}\"}}"
    else
        echo '{"hookSpecificOutput":{"additionalContext":"ESLint check passed"}}'
    fi
else
    echo '{}'
fi
exit 0
该脚本引入了 `-v` 命令用于环境检查。如果检测到格式化工具未安装,Hook 会静默跳过而不是抛出错误。这体现了"优雅降级"的设计原则:Hook 本身的异常不应阻塞核心工作流的正常运行。

Stop Hook:测试质量门禁

Stop Hook 是质量保障的一道防线:当 Claude 声称任务完成时,自动触发测试套件。如果测试失败,系统将阻止会话结束,强制继续修复。参见以下代码示例。

#!/bin/bash
# .claude/hooks/run-tests.sh
INPUT=$(cat)

# [关键机制] 防止无限循环:检查 stop_hook_active 标志
# 如果此标志为 true,说明已经发生过一次重试,此时必须放行以避免死锁
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_ACTIVE" = "true" ]; then
    exit 0  # 终止拦截,允许 Claude 停止
fi

# 切换到项目目录
if [ -n "$CLAUDE_PROJECT_DIR" ]; then
    cd "$CLAUDE_PROJECT_DIR"
fi

# 检测项目类型并运行测试
TEST_PASSED=true
TEST_RESULT=""

if [ -f "package.json" ] && grep -q '"test"' package.json; then
    TEST_RESULT=$(npm test 2>&1) || TEST_PASSED=false
elif [ -f "pyproject.toml" ] || [ -f "pytest.ini" ]; then
    TEST_RESULT=$(pytest 2>&1) || TEST_PASSED=false
elif [ -f "go.mod" ]; then
    TEST_RESULT=$(go test ./... 2>&1) || TEST_PASSED=false
else
    echo '{"hookSpecificOutput":{"additionalContext":"No test framework detected"}}'
    exit 0
fi

if [ "$TEST_PASSED" = true ]; then
    echo '{"hookSpecificOutput":{"additionalContext":"All tests passed"}}'
else
    # 截取错误日志的前 50 行并转义
    TEST_ESCAPED=$(echo "$TEST_RESULT" | head -50 | jq -Rs '.')
    # 返回 block 决策,强制 Claude 继续工作
    cat <<EOF
{
  "decision": "block",
  "reason": "Test failed, please fix before stopping",
  "hookSpecificOutput": {
    "additionalContext": $TEST_ESCAPED
  }
}
EOF
fi

脚本中的 stop_hook_active 字段是防止系统陷入"无限循环"的关键。其逻辑类似于递归函数的终止条件。当 Stop Hook 执行失败时,如果系统尝试修复并再次触发 Hook,stop_hook_active 会被设置为 true。脚本检测到该标志后选择放行,从而打破循环——正如递归函数必须有终止条件,Stop Hook 也必须有明确的退出机制。如果没有这一检查,Claude 将陷入"修复 → 测试失败 → 再修复"的无限循环,无法自行脱身。

Subagent Hooks:精准的上下文管理

在 第 3 章 中,我们了解到 Subagent 通过隔离上下文来实现任务委托。Hooks 系统为此提供了两个专用事件——SubagentStart 和 SubagentStop。但更关键的是第三个机制:在 Subagent 的 Frontmatter 中直接定义 Hooks。

全局 vs Frontmatter:精准性问题

假设你有一个名为 db-reader 的 Subagent,专门用于执行 SQL 查询。如果你需要审计它运行的每一条 Bash 命令,以防范 SQL 注入风险,那么在全局 settings.json 中配置 Hook 并非最佳方案。全局配置会不加区分地拦截所有 Bash 命令,包括编译代码、运行测试或安装依赖等与数据库无关的操作。这不仅浪费系统资源,还会大幅增加误报的风险。

更好的方案是在 Subagent 的 Frontmatter 中直接定义 Hook。参见以下代码示例。

HAS_ISSUES=$(grep -c "issue\|problem\|bug" "$TRANSCRIPT" || true)
HAS_SUGGESTIONS=$(grep -c "suggest\|suggestion\|recommend" "$TRANSCRIPT" || true)

---
name: db-reader
description: Read-only database analysis tool
tools: Read, Grep, Glob, Bash
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "./.claude/hooks/check-sql-injection.sh"
  Stop:
    - hooks:
        - type: prompt
          prompt: "Check whether the query results contain PII (such as names, email addresses, or phone numbers). If any are found, reply with ok: false and request that the data be desensitized."
---

You are a database analysis expert. Only execute SELECT queries; executing any SQL statements that modify data is strictly prohibited.

使用 Frontmatter 定义 Hook 的核心优势在于其生命周期紧密绑定:Hook 随 Subagent 的启动而激活,任务完成后自动清理。此外,配置与 Subagent 定义集成在同一文件中,可随 .md 文件一起分发。用户无需修改全局 settings.json,极大降低了配置复杂度。

SubagentStart:自动注入上下文

SubagentStart Hook 的一个典型用例是在 Subagent 启动时动态注入团队规范。例如,每当 code-reviewer Subagent 启动时,系统可以自动注入团队的编码规范。参见以下代码示例。

{
  "hooks": {
    "SubagentStart": [
      {
        "matcher": "code-reviewer",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":\"Team coding standards: use camelCase naming, maximum line length 100 characters, all public APIs must include JSDoc comments\"}}'"
          }
        ]
      }
    ]
  }
}

SubagentStop:验证输出质量

SubagentStop Hook 可用于验证 Subagent 的工作是否达到要求标准。通过 agent_transcript_path 读取 Subagent 的完整交互记录,系统可以执行细粒度的质量验收。参见以下代码示例。

#!/bin/bash
# verify-review-quality.sh
INPUT=$(cat)
AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type')
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')

# 仅验证 code-reviewer Subagent
if [ "$AGENT_TYPE" != "code-reviewer" ]; then exit 0; fi

# 防止无限循环(如果当前处于 stop hook 触发阶段,则跳过)
if [ "$STOP_ACTIVE" = "true" ]; then exit 0; fi

TRANSCRIPT=$(echo "$INPUT" | jq -r '.agent_transcript_path')
if [ -f "$TRANSCRIPT" ]; then
    HAS_ISSUES=$(grep -c "issue\|problem\|bug" "$TRANSCRIPT" || true)
    HAS_SUGGESTIONS=$(grep -c "suggest\|suggestion\|recommend" "$TRANSCRIPT" || true)
    if [ "$HAS_ISSUES" -gt 0 ] && [ "$HAS_SUGGESTIONS" -eq 0 ]; then
        echo '{"decision":"block","reason":"Issues were found but no suggestions for fixes were provided. Please supplement each issue with an improvement plan."}'
        exit 0
    fi
fi
exit 0

这三层保护各有各的用途,共同构成一个完整的质量保障反馈闭环。

  • Frontmatter Hook:负责内部自检,确保子代理问自己"我的输出完整吗?"

  • SubagentStart Hook:负责外部注入,在启动时提供必要的上下文("给它必要的背景信息")

  • SubagentStop Hook:负责外部验收,在结束时严格检查"它的工作是否达到要求标准?"

异步 Hook:后台执行,不阻塞

默认情况下,Hook 脚本是同步执行且阻塞的——Claude 会暂停当前工作流,直到脚本执行完毕并返回结果。对于快速的正则匹配检查,这几毫秒的延迟通常可以忽略不计;但对于运行测试套件、调用外部 API 或发送通知等耗时操作,同步阻塞会显著拖慢 Claude 的响应速度,影响用户体验。

为此,Claude Code 在 2026 年初引入了异步 Hook 支持。

{
  "type": "command",
  "command": "./.claude/hooks/run-tests-background.sh",
  "async": true,
  "timeout": 300
}

配置 async: true 后,Hook 将以非阻塞方式在后台运行。Claude 无需等待其完成,可以立即继续后续任务。异步 Hook 执行完毕后,其输出会在下一轮对话中自动传递给 Claude,供其参考或处理。

异步 Hook 有两个关键限制,决定了它的适用范围。

  • 类型限制:只有 command 类型的 Hook 支持异步执行。Prompt 类型和 agent 类型的 Hook 必须同步运行。

  • 拦截能力限制:异步 Hook 无法阻止当前操作。由于 Hook 一触发主流程就继续执行,异步 Hook 失去了在操作发生前进行干预的机会。

因此,异步 Hook 适用于"后处理"类任务,例如日志记录、异步通知、后台数据校验和非关键质量审计。它不适用于实时拦截的安全检查(如 SQL 注入防御或敏感信息过滤)。

环境变量与调试

Hook 可用的环境变量

表 5.2 列出了 Hook 可用的环境变量,涵盖其作用域和核心用途。

| 核心用途 | | 所有 Hook | | 获取当前项目根目录的绝对路径 |

| --- | --- | --- | --- | --- |

| CLAUDE_PROJECT_DIR | | 所有 Hook | | 当前会话的唯一标识 |

| CLAUDE_ENV_FILE | | 仅通知 | | 包含具体的通知消息内容 |

| CLAUDE_CODE_REMOTE | | 仅插件 Hook | | 插件安装的根目录路径 |

| CLAUDE_PLUGIN_ROOT | | 文件路径 | | 作用域 |

表 5.2:Hook 可用的环境变量

三种调试技巧

调试 Claude Hook 脚本有三种核心方法:

  1. 将调试信息输出到 stderr:由于 stdout 保留用于输出 JSON 决策结果,所有调试信息必须重定向到 stderr。

    echo "DEBUG: Checking file $FILE_PATH" >&2  # Debug information
    echo '{"decision": "allow"}'                # JSON decision
    
  2. 手动测试 Hook 脚本:通过构造模拟输入直接验证脚本逻辑。

    echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | ./.claude/hooks/block-dangerous.sh
    
    echo "Exit code: $?"
    
3. 使用 `claude --debug` 查看详细的 **Hook** 执行信息。

    调试模式会显示匹配到的 Hook 列表、每个脚本的执行时间以及返回的结果。

## 常见陷阱

使用 Hook 时有两个常被忽视的问题。

*   如果你的 shell 配置文件(如 `~/.zshrc` 或 `~/.bashrc`)中包含无条件的 `echo` 语句(例如用于显示欢迎信息),这些输出会污染 `standard output (stdout)`,导致 JSON 解析失败。解决办法是用 `[[ $- == *i* ]]` 条件判断包裹这些 echo 语句,确保它们只在交互式 shell 中执行。
*   直接编辑 `settings.json` 后,Hook 往往不会立即生效。这是因为 Claude Code 只在启动时捕获一次配置快照;运行时对文件的更改不会自动同步。要应用更改,用户需要在 **/hooks** 菜单中确认,或重启当前会话。

# 工程设计方法论

面对具体的自动化需求,设计 Hook 方案需要厘清以下三个核心维度:

*   **拦截时机(事件选择)**:操作前拦截用 `PreToolUse` 或 `UserPromptSubmit`;操作后反馈用 `PostToolUse`;完成检查用 `Stop` 或 `SubagentStop`;生命周期管理用 `SessionStart` 或 `SessionEnd`
*   **判断方式(类型选择)**:规则明确(如模式匹配或文件检查)用 command 类型;需要语义判断但输入足够用 prompt 类型;需要深度代码分析用 agent 类型
*   **配置范围(位置选择)**:团队统一规范配置在 `.claude/settings.json`;个人偏好配置在 `~/.claude/settings.json`;子代理专用检查配置在 Frontmatter

设计流程通常遵循“三步走”策略。

1.  先基于 `PostToolUse` 事件配置一个 `matcher:"*"` 的审计日志 Hook,观察 Claude 的实际工具调用模式,积累数天的真实操作数据
2.  基于审计数据识别高风险操作模式,设计针对性的 `PreToolUse` 拦截规则
3.  逐步收紧拦截规则,同时始终保留日志功能,确保任何误报都能快速追溯到根因

**Kage 的笔记**

Hook 是团队级基础设施,不是个人实验玩具。配置在 `.claude/settings.json` 中的 Hook 会应用到所有克隆该仓库的成员。如果成员因未知原因被意外拦截,会严重扰乱工作流并造成挫败感。因此,务必遵循以下准则:

*   提交 Hook 配置前,与团队充分讨论并达成共识。
*   每条拦截规则都必须包含明确的原因说明,告知用户被拦截的具体原因。
*   使用审计日志实时监控 Hook 触发频率,以便及时发现并纠正误报。

# 总结

Hook 填补了 `CLAUDE.md` 和 Skills 留下的空白:它们在工具即将运行的那一刻强制执行执行规则。其中一些规则是团队约定;另一些——拦截破坏性命令、拒绝触碰凭据文件——则纯粹是安全考量,即使在个人项目中也同样适用。作为 Claude Code 扩展机制中唯一位于执行层而非认知层的组件,Hook 不依赖 Claude 理解任何东西:脚本拦截调用。

二十多种事件类型覆盖了从会话开始到会话结束、从主对话到子代理的完整弧线;请查阅官方参考以获取当前列表。

三种处理器类型(command、prompt、agent)构成了“确定性递减、灵活性递增”的能力阶梯。选择原则是:先用 command,再用 prompt,最后用 agent。

支持在子代理的 Frontmatter 中配置内联 Hook,以实现比全局配置更精细的控制。

异步 Hook 的引入有效解决了长时间运行操作导致的阻塞问题。

从工程方法论角度看,Hook 的设计严格遵循“先观察,后控制”的原则:先用 `PostToolUse` 事件记录审计日志,深入分析真实工具调用模式和行为特征;再基于积累的数据洞察,设计针对性的 `PreToolUse` 拦截规则,确保策略的准确性和必要性。内置的 `stop_hook_active` 标志有效防止了 Hook 逻辑本身触发的无限递归调用。`Exit code 2` 被定义为表示“有意拦截”,将其与系统错误清晰区分,体现了异常处理和状态定义上的周全考虑。

Hook 的核心价值不在于“扩展能力边界”(让 Claude 做更多事),而在于“强化执行基础”(让 Claude 所做的一切更可靠)。它将那些“本应发生却常被遗忘”的关键检查(如敏感文件保护、代码格式化规范、测试验收流程)从基于自律的“软约束”升级为不可回避的系统级“硬约束”。开篇案例中提到的惨痛教训,本可以通过一个简单的 `PreToolUse` Hook 从根本上避免。

至此,我们已经深入剖析了 Claude Code 的内部扩展机制。在 *第 6 章* 中,我们将把目光转向外部,探索它如何连接到更广阔的外部世界:MCP。

# 讨论问题

1.  在日常开发工作流中,有哪些“本不该发生却偶尔因疏忽而发生”的错误?选择其中一个场景,设计一个完整的 Hook 方案:指定触发事件、Hook 类型、Matcher 配置规则,以及脚本需要检查的具体条件。
2.  Stop Hook 的“质量门”模式与 CI/CD 流水线中的自动化测试有何异同?在哪些场景下应优先使用 Stop Hook,在哪些场景下应优先使用 CI/CD?
3.  `PreToolUse` 具有拦截能力,而 `PostToolUse` 仅用于反馈。这种非对称设计的工程意义是什么?如果给 `PostToolUse` 赋予拦截能力(即撤销已执行操作的能力),可能会出现什么问题?
4.  请比较两种 Hook 配置方式:`global settings.json` 与子代理的 Frontmatter。在你的项目中,哪些 Hook 应放在全局配置中,哪些更适合定义在特定子代理的 Frontmatter 中?

# 6

# 连接一切:使用 MCP 集成外部工具

“没有人是一座孤岛,在大海里独踞;每个人都像一块小小的泥土,连接成整个陆地。”——约翰·多恩

技术评审会议接近尾声时,产品经理提出了一个看似简单的需求:“*让 Claude 分析一下上周 Jira 中所有 P1 级 bug 的分布规律吧。*”

小冰在心里快速盘算了一下:Jira 部署在公司内网,而 Claude 运行在 Anthropic 的云端服务器上。这两个世界之间天然存在一道网络隔离墙。要让 Claude 访问 Jira 数据,要么手动导出粘贴,要么写脚本抓取后喂进去,要么……

“*要么用 MCP,*”Kage 在她开口前就接了话,“*全称是 Model Context Protocol,模型上下文协议。*”

“*这个名字我在好几个地方见过,但一直没真正理解它解决的核心问题是什么,*”小雪插话道。

“*它解决的是 AI 与数据之间最古老的问题——如何实现安全、标准化的连接。*”Kage 在白板上画了两个圆圈,左边标注“Claude”,右边标注“Jira/数据库/API”,中间画了一条连接线。“*在 MCP 之前,这条线的两端每次都要重新搭建:GitHub 要对接 Claude,得写一个适配器;Jira 要对接 Claude,又得写一个。而如果你换了 AI 平台,所有适配器还得推倒重来。这本质上是一个 N×M 的复杂度问题。*”

小冰立刻领悟了:“*就像 USB-C 普及之前的充电线?每个设备都要配一根专用线,出门得带六七根?*”

“*没错。MCP 就是 AI 工具集成领域的 USB-C,*”Kage 点头。

# 从 N×M 到 N+M:标准化的力量

USB-C 的类比不只是修辞;在结构上,它精确映射了 MCP 所解决的核心问题。

想想 USB-C。在单一连接器胜出之前的那几年,笔记本电脑、手机和相机各自带着一堆互不兼容的接口和充电器,每一对搭配都需要专属的线缆。这个类比是对一段漫长历史的简化——USB 本身已经取代了更早一代的串口和并口——但问题的形态是准确的:

MCP 实现的正是完全相同的逻辑(参见*图 6.1*)。在 MCP 之前,每一个 AI 工具厂商和具体数据源的集成都需要开发并维护一个专用适配器。例如,要让 GitHub 接入 Claude 的代码,你需要开发一个 GitHub-Claude 适配器;要把同一个 GitHub 接入 ChatGPT,你又得开发一个 GitHub-ChatGPT 适配器。假设有 5 个 AI 客户端和 100 个数据源,理论上你需要维护 500 个独立适配器——这意味着每新增一个 AI 客户端或数据源,都会带来大量冗余的集成工作。

<https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/the-claude-code-operating-model/img/B39394_6_1.png>

图 6.1:MCP 将 N×M 问题简化为 N+M 问题

MCP 的核心思想恰恰如此:定义一套通用标准协议。在这个架构中,每个 AI 客户端(如 Claude、ChatGPT 等)只需实现一次 MCP 客户端协议,每个数据源(如 Jira、GitHub、数据库等)只需实现一次 MCP 服务端协议。只要双方遵守这一标准,就能即插即用、互相连接,而无需为每一种组合编写针对性的适配器代码。

回到前面的例子,有 5 个 AI 客户端和 100 个数据源,采用 MCP 后,开发工作量从惊人的 500 次(5×100)降低到仅 105 次(5+100)。

AAIF 于 2025 年 3 月宣布将采用 MCP:支持首先落地在 OpenAI 的 `Agents SDK` 中,Responses API 和 ChatGPT 桌面应用将随后跟进

AAIF 由 Anthropic、Block 和 OpenAI 共同创立,各自贡献一个创始项目——分别是 MCP、goose 和 AGENTS.md——Google、Microsoft、Amazon Web Services、Cloudflare 和 Bloomberg 作为白金会员和支持者加入

官方 MCP SDK(Python 和 TypeScript)的月下载量超过 9700 万次,消除了 N×M 集成的痛点,这是一个迫切的、全行业的需求

# 架构:客户端-服务端与 JSON-RPC

MCP 采用经典的客户端-服务端架构(参见*图 6.2*),通过 JSON-RPC 2.0 协议进行通信。

<https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/the-claude-code-operating-model/img/B39394_6_2.png>

图 6.2:MCP 客户端-服务端架构

MCP 客户端内置于 Claude Code 中,负责发现 MCP 服务端提供的能力、构造工具调用请求并处理响应。MCP 服务端是一个独立程序,连接到数据库、API 或文件系统等外部服务,并通过 MCP 将这些服务的能力暴露给客户端。

MCP 的共同创造者 David Soria Parra 和 Justin Spahr-Summers 以 Language Server Protocol 为蓝本设计了该协议;Soria Parra 在接受采访时描述了 MCP 如何借鉴 LSP 的消息流设计理念。

一个 MCP 服务端可以向客户端暴露以下三种核心能力:

*   **工具(Tools)**:工具是最基础、最常用的能力。工具是 Claude 可以主动调用的函数,例如“在 Jira 中创建新工单”“从数据库查询过去 7 天的订单”或“向 Slack 发送消息”。每个工具都定义了清晰的名称、描述、输入参数模式和返回值格式。当 Claude 判断需要调用某个工具时,它会自动构造符合模式的请求;服务端执行操作后,返回结构化结果。
*   **资源(Resources)**:资源在概念上类似于“文件”,指 Claude 可以按需读取的数据。与工具的关键区别在于:工具用于执行操作(通常有副作用),而资源用于获取数据(只读)。例如,“获取 `README.md` 的内容”是资源访问,而“向代码库提交”是工具调用。
*   **提示(Prompts)**:提示是可复用的提示模板,旨在标准化特定任务的发起。例如,一个代码审查专用的 MCP 服务端可能提供代码审查提示模板,调用时会自动加载标准审查格式和关注点。尽管此功能与 Skills 有一定重叠,且在实际中使用不如前两者广泛,但它为任务初始化提供了一条便捷路径。

在这三种能力中,工具占据了实际使用场景的绝大多数。例如,当用户问“帮我在 Jira 中查找 P1 级 bug”时,Claude 调用的是 Jira MCP 服务端暴露的 `list_issues` 工具。

# 传输方式:三种连接形式

MCP 传输层定义了客户端与服务端之间的物理通信机制。目前支持三种主要传输方式。

*   **stdio(标准输入/输出)**:此方式专为本地进程通信设计。在此模式下,客户端(如 **Claude Code**)将 MCP 服务端作为子进程启动,并通过标准输入(`stdin`)和标准输出(`stdout`)交换 JSON-RPC 消息。此方式的优点是完全在本地运行、无需网络连接、安全性最高且延迟最低。它适用于文件系统访问、本地数据库连接以及与本地开发工具的集成。绝大多数通过 `npx` 启动的官方 MCP 服务端都使用此方式。
*   对于远程 HTTP 服务端,MCP 授权规范强制要求使用带 PKCE 的 `OAuth 2.1`,并且它可以与企业单点登录系统轻松集成。
*   **SSE**(**Server-Sent Events**):这是远程传输的初始方案,但独立 SSE 传输已被 MCP 规范中的 Streamable HTTP 取代;如果你在旧项目中看到 SSE 配置,请迁移到 HTTP。

遵循“本地用 stdio,远程用 HTTP”这一简单规则,就能满足绝大多数需求。

# 配置详解:从 CLI 到配置文件

## 快速 CLI 配置

**Claude Code** 提供了一组 `claude mcp` 命令,方便管理 MCP 服务端配置。请参见以下代码示例。

添加本地 stdio 服务器:以 /workspace 作为工作目录启动本地文件系统服务

claude mcp add filesystem npx -y @modelcontextprotocol/server-filesystem /workspace

添加远程 HTTP 服务器:连接公司内部 Jira 服务

claude mcp add --transport http company-jira https://jira.company.com/mcp

添加用户级服务器(全局可用):将 GitHub 服务添加到用户级配置中,使其对所有项目生效

claude mcp add --scope user github -- npx -y @modelcontextprotocol/server-github

添加带认证的服务器:通过自定义 HTTP 头传递认证令牌

claude mcp add --transport http --header "Authorization: Bearer ${TOKEN}" api https://api.example.com/mcp

管理命令

claude mcp list        # 列出配置:查看当前配置的所有 MCP 服务器

claude mcp test github # 测试连接:验证指定服务器(如 GitHub)的连接状态与可用性

claude mcp remove github  # 移除服务器:删除指定的 MCP 服务器配置

.mcp.json 配置文件

claude mcp CLI 命令本质上是在操作 .mcp.json 配置文件。理解该文件的结构是实现细粒度配置的关键。参见下方代码示例。

{

  "mcpServers": {

    "filesystem": {

      "command": "npx",

      "args": ["-y", "@modelcontextprotocol/server-filesystem",

        "/workspace"],

      "env": {}

    },

    "company-jira": {

      "type": "http",

      "url": "https://jira.company.com/mcp",

      "headers": {

        "Authorization": "Bearer ${JIRA_TOKEN}"

      }

    },

    "postgres": {

      "command": "npx",

      "args": ["-y", "@modelcontextprotocol/server-postgres"],

      "env": {

        "DATABASE_URL": "${DB_URL:-postgresql://localhost:5432/mydb}"

      }

    }

  }

}

环境变量替换是上述配置文件中最重要的功能。基本语法${VAR_NAME}会在运行时直接将占位符替换为对应的环境变量值;默认值语法${VAR_NAME:-default_value}会在环境变量VAR_NAME未设置时使用指定的default_value。通过利用环境变量替换,你可以将敏感数据(如 API 密钥、数据库密码等)存储在操作系统的环境变量或 .env 文件中,而不是硬编码在mcp.json配置文件里。由于配置文件中只包含变量引用(如${JIRA_TOKEN})而非实际密钥,你可以安全地将mcp.json提交到 Git 仓库以供团队共享,而无需担心凭据泄露。

配置文件位置与作用域

mcp.json配置文件的作用域因其位置而异,如表 6.1所示。

| 位置 | 作用域 | 适用场景 |

| --- | --- | --- |

| .mcp.json(项目根目录) | 项目级 | 仅适用于当前项目。适用于绑定到特定代码库的团队共享服务(项目的数据库连接、特定的内部工具)。通常会提交到 Git。 |

| ~/.claude/目录 | 所有项目,用户级 | 适用于所有项目。适用于个人通用工具(个人文件系统访问、全局代码搜索工具)。位于用户主目录中,不随项目变化。 |

表 6.1:MCP 配置文件的位置与作用域

如果某个 MCP 服务器的凭据不应提交到 Git(例如个人 GitHub Token),你可以将凭据部分存储在.claude/settings.local.json中(该文件已被.gitignore忽略),同时将服务器的基本配置保留在mcp.json中以供团队共享。

实践示例 1:连接数据库

让我们从最常见需求开始:让 Claude 能够直接查询数据库。

首先,配置一个 PostgreSQL MCP 服务器。参见下方代码示例。

{

  "mcpServers": {

    "postgres": {

      "command": "npx",

      "args": ["-y", "@modelcontextprotocol/server-postgres"],

      "env": {

        "DATABASE_URL":

          "${DATABASE_URL:-postgresql://localhost:5432/mydb}"

      }

    }

  }

}

配置完成后,你只需向 Claude 发出自然语言指令,例如以下内容:

You: 帮我查看数据库中上个月的订单数量和总金额。

Claude: 正在查询数据库...

[调用 postgres MCP Server: sql_query 工具]

上个月(2025 年 12 月)的订单统计:

- 订单数量:1,234

- 总金额:$456,789.00

- 平均订单价值:$370.21

- 与上月相比增长 12.3%

在此过程中,Claude 会自动识别查询需求、定位 Postgres MCP 服务器、生成并执行 SQL 语句,最后整理并返回结果。你不需要了解 SQL 语法,也不需要打开数据库客户端或手动导出数据。

然而,这里有一个关键的安全原则:数据库连接应只使用只读账户。

只读允许列表是正确的默认设置。像DROP TABLE这样的语句会销毁一张表及其中的每一行,而且与查询不同,再次运行它无法撤销损害。这种不对称性正是将写操作视为破坏性操作并将其排除在允许列表之外的原因。

实践示例 2:构建自定义 MCP 服务器

当现有 MCP 服务器无法满足你的需求时(例如,当你需要连接公司内部 API 或以特定格式自定义数据处理工具时),你可以使用 TypeScript 或 Python SDK 构建自己的专用 MCP 服务器。

下面是一个基于 TypeScript 的待办事项管理服务器示例,展示了 MCP 服务器的基本架构。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { StdioServerTransport } from

    "@modelcontextprotocol/sdk/server/stdio.js";

import { z } from "zod";

// In-memory storage

const todos: { id: string; text: string; done: boolean }[] = [];

// Create MCP server instance

const server = new McpServer({

    name: "todo-server",

    version: "1.0.0",

});

// Define tool: Add a to-do item

server.tool(

    "todo_add",                                     // Tool name

    "Add a new todo item",                          // Tool description

        // (for Claude to determine when to invoke)

    { text: z.string().describe("The todo text") }, // Input parameter

        // schema

    async ({ text }) => {                           // Execution logic

        const todo = {

            id: Math.random().toString(36).substring(2, 9),

                text,

            done: false,

        };

        todos.push(todo);

        return {

            content: [{ type: "text",

                text: `Added: ${todo.id} - ${todo.text}`

            }],

        };

    }

);

//

// Define tool: List all todo items

server.tool("todo_list", "List all todo items", {}, async () => {

    const text = todos.length === 0

    ? "No todos found."

    : todos.map((t) => `[${t.done ? "x" : " "}] ${t.id}:

        ${t.text}`).join("\n");

    return { content: [{ type: "text", text }] };

});

// Define resource: Statistics

server.resource("stats", "Server statistics", async () => {

    return {

        // Start the server

        async function main() {

      mimeType: "application/json",

      text: JSON.stringify({

        total: todos.length,

            const transport = new StdioServerTransport();

            await server.connect(transport);

            console.error("MCP Server started");  // Note: Log output is

                // sent to stderr

            completed: todos.filter((t) => t.done).length,
pending: todos.filter((t) => !t.done).length,
}, null, 2),
}],
};
});
// Start the server

重点关注检查安全漏洞、逻辑错误和性能问题。

posted @ 2026-09-23 21:08  绝不原创的飞龙  阅读(16)  评论(0)    收藏  举报