Claude Code 工程化最佳实践:从 CLAUDE.md 到 Hooks
我把 Claude Code 引入团队日常开发已经大半年了。最初以为写一份详细的 CLAUDE.md 就够了,结果发现 Agent 在长会话里开始忽略关键规则、忘记测试命令、甚至在上下文膨胀后产出与项目风格不一致的代码。这些问题不是模型能力不够,而是工程化配套没有跟上。
这篇文章整理了我在这半年里踩过的坑和逐渐成型的实践,围绕四个核心问题展开:哪些知识需要长期保留,哪些行为必须被约束,哪些结果需要验证,以及上下文如何管理。
上下文窗口是稀缺资源
Claude Code 的上下文窗口上限为 100 万 tokens,但实际可用的「高质量区间」远没有看起来那么大。社区测量数据显示,当上下文使用率达到 40% 左右时,模型输出质量就开始出现可感知的下降;到 60% 时应该主动结束当前会话1。一个 500 行的 TypeScript 文件大约消耗 4000 tokens,一次详细回复占用 1500–3000 tokens——这意味着一个认真工作的会话大约 50 轮交互就会触及退化边界。
Anthropic 在 Claude Code 官方文档中明确了记忆系统的分层加载机制:全局 ~/.claude/CLAUDE.md、项目根目录 CLAUDE.md、子目录 CLAUDE.md、以及 .claude/rules/*.md 条件加载规则文件2。每一层都会在会话启动时注入上下文。如果所有层级加起来超过 2000 tokens,Agent 对规则的遵从率会整体下降——不是后面的规则被忽略,而是所有规则的有效率同时降低3。
这让我意识到一个根本性的设计原则:输出质量是上下文质量的直接函数。每一次往上下文里塞信息,都要问自己这条信息是否值得占用有限的注意力预算。
CLAUDE.md 不是许愿池
最早的 CLAUDE.md 我写了 150 行,把编码风格偏好、架构愿景、临时需求和硬性约束混在一起。结果 Agent 在关键约束上的遵从率大约只有 35%,和我写「写出整洁代码」这种模糊指令时的遵从率差不多3。
问题出在信息密度和分层策略上。经过几轮调整,我总结出一套有效的分层结构:
CLAUDE.md # 项目级硬约束,控制在 60 行以内
CLAUDE.local.md # 个人偏好,加入 .gitignore
.claude/rules/*.md # 条件加载规则,按目录激活
反面案例:把一切塞进 CLAUDE.md
# 反面示例:CLAUDE.md 里的信息噪音
## 编码风格
- 使用单引号,不要双引号
- 不要尾分号
- 函数不超过 30 行
- 变量命名要有意义
- 注释要写清楚 # ← 模糊指令,Agent 无法执行
- 写出整洁的代码 # ← 更是如此
## 架构想法
- 未来可能迁移到微服务 # ← 临时愿景,不是当前约束
- 考虑用 event sourcing # ← 应该放在 RFC 文档里
## 临时需求
- 这周先把搜索功能做完 # ← 任务级别,不属于项目记忆
- 记得改一下首页 banner # ← 应该放在 Issue 里这种写法有三个问题:模糊指令不产生可验证的行为变化;临时需求污染长期记忆;架构愿景让 Agent 在应该遵循现有模式时犹豫不决。
正面案例:分层后的 CLAUDE.md
# 项目协作约束
## 项目事实
- Monorepo: pnpm + Turborepo, Node.js 22+
- 主应用: apps/web (Next.js 16 App Router)
- 共享 UI: packages/ui
- Prompt 模板: packages/prompts
## 代码边界 [CRITICAL]
- 页面文件只做路由装配,复杂 UI 放 _components/ 或 features/
- API 请求只能通过 apps/src/api/ 模块进入
- 环境变量只能通过 apps/src/config/env/ 读取
- 单文件不超过 500 行,超出需先讨论拆分方案
## 禁止操作
- 不执行 git reset --hard / git push --force
- 不还原用户已有改动
- 不使用 class-based views
- 不在 tests/ 目录外写测试文件
## 验证命令
- 类型检查: pnpm typecheck
- 全量验证: pnpm verify
- 提交前必须运行 pnpm verify这份 30 行的 CLAUDE.md 比之前 150 行的版本有效得多。关键改动是:每条规则都能回答「它阻止了哪类误判」;硬性约束和代码边界用标签区分优先级;临时需求全部移出,放入 Issue 跟踪。
CLAUDE.md 中的否定表达陷阱
一个容易忽略的问题是:否定句会把被否定的概念重新拉进模型的注意力范围。当 CLAUDE.md 里写「不要使用分号」时,模型反而更容易产出带分号的代码,因为「分号」这个概念被激活了。实测数据表明,将否定规则改写为正向等价表达后,违规率大约降低一半3。
| 否定写法(容易触发违规) | 正向等价写法(推荐) |
|---|---|
| 不要使用分号 | 使用 ESLint no-semicolon 规则 |
| 不要用 any 类型 | 使用 unknown + 类型收窄 |
| 不要写默认导出 | 使用命名导出 |
| 不要直接读 process.env | 环境变量统一从 config/env 模块读取 |
| 不要写类组件 | 使用函数组件 + Hooks |
这个改写的核心逻辑是:把「不要做 X」转换成「做 Y 来替代 X」。模型更容易遵从正向指令,因为它不需要在生成过程中持续抑制一个已被激活的概念。
Hooks:把规则从建议变成执行
CLAUDE.md 里的规则本质上是「建议」——模型大概率会遵守,但不保证。当团队需要确定性保障时,Hooks 是更可靠的选择。Hooks 是在 Agent 生命周期特定节点执行的确定性 Shell 命令,它们运行在 Agent 循环之外,不受模型推理影响4。
我按风险等级把 Hooks 分成三层:
安全层 Hook 示例
这个 Hook 在每次命令执行前拦截危险操作,是投入产出比最高的一层:
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
// 拦截不可逆 git 操作和危险删除命令
// exit code 2 表示阻断,Agent 会收到拒绝通知
"command": "jq -r '.tool_input.command' | grep -qE 'git reset --hard|git push --force|rm -rf /' && exit 2 || exit 0"
}
]
}
]
}
}质量层 Hook 示例
文件写入后自动运行格式化,比在 CLAUDE.md 里写「记得运行 Prettier」可靠得多:
// 反面:只在 CLAUDE.md 里写规则(不可靠)
// "记得每次编辑后运行 prettier"
// 问题:模型在长会话中经常忘记,且无法审计
// 正面:用 Hook 强制执行(确定性)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
// 自动格式化被修改的 TypeScript 文件
// 只在文件匹配 .ts/.tsx 时执行,避免无关文件触发
"command": "jq -r '.tool_input.file_path' | grep -qE '\\.(ts|tsx)$' && npx prettier --write \"$(jq -r '.tool_input.file_path')\" || true"
}
]
}
]
}
}| 约束方式 | 可靠性 | 适用场景 | 失败代价 |
|---|---|---|---|
| CLAUDE.md 文字规则 | 中(~89% 遵从率) | 编码风格、架构约定 | Agent 可能产出风格不一致的代码 |
| Hooks 自动执行 | 高(确定性执行) | 格式化、类型检查、危险命令拦截 | 无——Hook 始终执行 |
| settings.json 权限 | 高(工具级阻断) | 工具白名单、命令审批 | 配置错误可能阻断正常工作流 |
| 子代理隔离 | 高(独立上下文) | 代码审查、大规模文件扫描 | 子代理看不到主会话上下文 |
子代理:上下文隔离的核心武器
当任务可能产生大量文件读取和搜索操作时,把这部分工作交给子代理是保持主会话上下文干净的最有效手段。子代理拥有独立的上下文窗口、独立的工具白名单和独立的权限集,完成后只把最终结果返回给主会话5。
我犯过的一个典型错误是让主代理直接做代码审查——它会读取几十个文件,搜索大量引用关系,等审查完成时主会话的上下文已经膨胀到退化区间,接下来再做的任何任务质量都会下降。
# 反面做法:在主会话中做代码审查
# 问题:几十个文件的读取和搜索全部留在主上下文
我: 帮我审查这个 PR 的变更,检查有没有安全和性能问题
Claude: [读取 30 个文件,搜索引用关系,分析 diff...]
我: 审查完了,现在帮我重构搜索模块
Claude: [上下文已被审查任务污染,重构质量下降]正确的做法是定义一个专门的审查子代理,让它处理所有的文件读取和分析工作:
# .claude/agents/code-reviewer.md
---
name: code-reviewer
description: 专注代码审查的子代理,只返回审查结论
tools: Read, Grep, Glob, Bash
model: haiku
---
你是一名资深代码审查员。关注以下维度:
1. 正确性:逻辑是否符合需求
2. 安全性:是否有注入、越权、敏感信息泄露
3. 性能:是否有 N+1 查询、不必要的重渲染
4. 项目约定:是否遵循 CLAUDE.md 中的代码边界
只返回审查结论和具体问题列表,不需要返回文件内容。主会话调用方式:
我: 用 code-reviewer 代理审查当前分支相对 main 的变更
审查过程中的所有文件读取、搜索操作都发生在子代理的上下文窗口里,主会话只收到一份精炼的审查报告。
子代理适用场景对比
| 场景 | 适合子代理? | 原因 |
|---|---|---|
| 代码审查(读大量文件) | 是 | 读取和搜索操作不污染主上下文 |
| 大规模文件重构 | 是 | 可并行处理,各自隔离 |
| 核心业务逻辑调试 | 否 | 需要主会话的完整对话上下文 |
| 数据库 Schema 迁移 | 是 | 任务独立,有明确的验证标准 |
| 多模块 API 集成测试 | 是 | 每个模块可以独立验证 |
会话生命周期管理
一个会话应该有一个明确的作用域——「实现分页功能」或「排查鉴权 Token 刷新问题」。作用域完成后,会话就应该结束。在同一个会话里开始第二个任务,仅仅因为上下文预算还有剩余,是产出质量下降最常见的原因6。
上下文退化有几个可观察的信号:
- 重复建议:Agent 开始建议已经尝试过的方案
- 风格漂移:产出的代码风格开始偏离项目约定
- 幻觉增加:出现不存在的函数名或虚构的 API
当这些信号出现时,正确的做法是 /compact 或 /clear,而不是在退化的上下文里继续推进。
# 反面做法:在退化上下文中硬撑
# 问题:Agent 已经出现重复建议,继续对话只会累积噪音
我: 帮我修复搜索分页的 bug
Claude: [尝试方案 A,失败]
Claude: [尝试方案 B,失败]
Claude: [再次建议方案 A——重复信号出现]
我: 再试试别的方案 # ← 上下文已经退化
Claude: [建议一个不存在的 API——幻觉信号出现]# 正面做法:识别信号后主动重置
我: 帮我修复搜索分页的 bug
Claude: [尝试方案 A,失败]
Claude: [尝试方案 B,失败]
我: /clear
我: 搜索分页 bug 仍未解决。之前尝试了 A 和 B 都失败了。
请从不同角度分析根因,重点关注 SQL OFFSET 计算逻辑。
Claude: [在干净的上下文中从新角度分析,找到真正根因]两种做法的关键差异在于:重置后用 /clear + 简要摘要,我控制了哪些信息进入上下文,而不是让失败的尝试和修正过程继续占据注意力预算。
验证证据:让交付可审查
AI 生成代码的审查焦点应该从「这是不是 AI 写的」转向「这次变更有没有证据支撑」。我要求所有 Claude Code 的产出都必须附带验证证据,格式固定为三部分:变更范围、验证结果、剩余风险。
## 变更范围
- 修改了搜索请求入口 `apps/src/features/search/api.ts`
- 新增空结果兜底组件 `apps/src/features/search/EmptyState.tsx`
## 验证结果
- pnpm typecheck: 通过
- pnpm test --filter search: 12 个测试全部通过
- 本地浏览器访问 /search?q=test: 搜索框正常返回结果
- 本地浏览器访问 /search?q=xyznotfound: 空结果兜底正常展示
## 剩余风险
- 未验证生产 API 的限流场景(需要部署后验证)
- 后端 500 错误只做了前端 Toast 提示,没有重试机制这份证据把 Reviewer 的注意力聚焦在需要人工判断的地方——剩余风险,而不是逐行检查 AI 写的代码是否合理。
落地检查清单
以下清单按实施阶段分组,建议从第一阶段的 4 项开始,逐步推进。
第一阶段:基础配置(第 1 天)
- CLAUDE.md 控制在 60 行以内,只包含硬性约束和代码边界
- 所有否定规则改写为正向等价表达
- 临时需求和架构愿景移出 CLAUDE.md,放入 Issue 或 RFC
- 确认验证命令(typecheck / test / lint)写入 CLAUDE.md
第二阶段:Hooks 防护(第 2-3 天)
- 配置安全层 Hook,拦截
git reset --hard、rm -rf等不可逆操作 - 配置质量层 Hook,文件写入后自动运行格式化
- 确认 Hook 的 exit code 语义:0 通过,2 阻断
- 在
.claude/settings.json中配置合理的工具权限白名单
第三阶段:子代理与上下文管理(第 1 周)
- 为代码审查创建专用子代理,避免主上下文膨胀
- 为大规模文件扫描创建专用子代理
- 团队约定:单个会话只处理一个明确作用域的任务
- 团队约定:出现重复建议或幻觉信号时主动
/clear
第四阶段:交付规范(第 2 周)
- 定义固定的验证证据模板(变更范围 + 验证结果 + 剩余风险)
- 所有 Claude Code 产出必须附带验证证据才能提交 Review
- Review 关注点从「是否 AI 生成」转向「证据是否充分」
- 每周回顾 CLAUDE.md,移除已不再适用的规则,补充新发现的误判模式
小结
Claude Code 的工程化不是一个一次性的配置任务,而是一个持续校准的过程。CLAUDE.md 的每一条规则都应该能回答「它减少了哪类误判」;每一个 Hook 都应该有明确的风险等级;每一次交付都应该附带可验证的证据。上下文窗口是稀缺资源,像管理内存一样管理它。
参考资料
Footnotes
-
Context Management Strategies for Claude Code — Iceberg Lakehouse, 2026. 详细分析了上下文退化阈值和管理策略。 ↩
-
Memory - Claude Code Docs — Anthropic 官方文档。说明了 CLAUDE.md 的分层加载机制。 ↩
-
Claude Code Best Practices: A Developer's Guide (2026) — MCP Directory. 包含 CLAUDE.md 长度建议和否定表达陷阱的实测数据。 ↩ ↩2 ↩3
-
Automate actions with hooks - Claude Code Docs — Anthropic 官方文档。Hooks 的完整生命周期事件和使用指南。 ↩
-
Create custom subagents - Claude Code Docs — Anthropic 官方文档。子代理的定义方式和上下文隔离机制。 ↩
-
Claude Code Guide 2026: Context Engineering & Planning — Generative Inc. 会话生命周期管理和四阶段工作流设计。 ↩