Claude Code 误解你的项目架构:用「我们用 / 我们不用」清单 + canonical 文件 + PreToolUse hook 锁死

Claude Code 建议「拆成微服务」但你刻意单体——在 CLAUDE.md 写「we use / we don't use」+ 真文件 canonical、用 .claude/rules/ 按路径限定,并用 PreToolUse hook 硬拦架构漂移。

你让 Claude Code 加一个 feature。代码能跑,但架构歪了:3 个内部 helper 被抽成「shared service」、引入了你没要的 dependency injection container、明明是普通函数它给你写成 class,或者在单体 Astro app 上提议把新 feature 拆成独立微服务。

最快修法:在项目 CLAUDE.md 里写一段明确的「we use / we don’t use」架构清单,每条规则配一个真实文件让它照抄,并在 task prompt 里点名那个文件。如果同一个模式反复漂移,就别再靠文字了——加一个 PreToolUse hook 把它硬拦掉。CLAUDE.md 是 Claude 会去尽量遵守的上下文,不是强制规则;任何你真的不能允许的东西,应该写进 hook。

架构不合,是因为 Claude 把训练数据里的通用「best practice」套到你团队刻意做出的决定上。要赢,就让你的约定不可避免:具体、锚到文件的例子能打败泛型先验,而 hook 在必要时能打败两者。

你属于哪一类

症状最可能的原因跳到
哪里都没有架构规则CLAUDE.md 缺失 / 太虚原因 1、Step 1
输出像 NestJS/Spring/Rails 默认风训练先验压过你原因 2、Step 1
在迁移中却挑了更老那个模式模式混杂、没 canonical原因 3、Step 4
有规则但 Claude 不理抽象规则、没例子文件原因 4、Step 2
只在某个目录漂移(如 src/api/没有按路径限定的规则原因 4、Step 2b
你 prompt 里写了「service」/「controller」/「repo」prompt 措辞带偏了原因 5、Step 6
Claude 照 doc 干、但 doc 是错的CLAUDE.md 过期原因 6、Step 5
每次都犯同一个错、文字拦不住需要强制、不是上下文Step 7(hook)

常见原因

按命中率从高到低。

1. CLAUDE.md 缺架构或太虚

没有明确规则(「我们是刻意单体;不要提拆分」),Claude 就默认走训练数据里「看着专业」的写法——往往就是微服务、DI、repository pattern。注意:CLAUDE.md 是作为 system prompt 之后的一条 user message 注入的,所以 Claude 会读、会尽量遵守,但没有严格合规保证,措辞含糊或互相矛盾时尤其如此。

如何判断grep -i "architecture\|monolith\|microservice\|pattern" CLAUDE.md 空——架构是隐式的,没写下来。在会话里跑 /memory 确认到底加载了哪些指令文件;如果列表里没有你的 CLAUDE.md,那 Claude 根本没看到它。

2. 训练先验比你的约定强

Claude 训练时见过成千上万个「抽成 class + DI」的例子,而你的「只用函数」在它见过的例子里是 0 个。没有显式 override,先验就赢了。

如何判断:建议镜像了某个流行框架的模式(NestJS DI、Spring bean、Rails service object)——你拿到的是框架默认,不是你代码库的默认。

3. 项目内模式混杂、agent 挑它最熟的

代码库一半 Redux 一半 Zustand(迁移进行中),Claude 两种都看到、分不清哪个 canonical,于是默认 Redux——因为训练里更常见。

如何判断:建议用的是更老 / 更主流的模式,忽略了更新 / 项目 canonical 那个——迁移歧义。

4. CLAUDE.md 抽象规则没例子

「优先组合不要继承」是个好原则,但没有一个具体文件给 Claude 抄就没用。抽象规则不迁移,具体例子才迁移。

如何判断CLAUDE.md 里都是声明、没有文件指针——给每条配一个 canonical example 路径。

5. task 措辞暗示了新模式

「Build a billing service」——「service」一词就暗示了 service-oriented 设计。「在 src/features/ 加 billing」则暗示沿用你现有的 feature 结构。

如何判断:你的 prompt 里出现了项目其实不用的框架词(「service」「controller」「repository」)。

6. CLAUDE.md 过期了

架构以前是那样的,CLAUDE.md 还在描述旧方案。Claude 老老实实照着 doc 干,产出和(真)架构矛盾的代码,然后你怪 Claude。

如何判断CLAUDE.md 几个月没动了,而架构早就往前走了。过期 doc 正在主动骗 Claude。

最短修复路径

按收益从高到低。Step 1–3 在耐久层锁住正确模式;Step 7 是文字拦不住时的升级手段。

Step 1:写显式「we use / we don’t use」规则

写进项目 CLAUDE.md(放 repo 根目录,或 ./.claude/CLAUDE.md——两者都会作为 project memory 加载):

## Architecture

We use:
- Single Astro app (monolith); deploy as one unit
- Functions over classes (no class-based services)
- Direct DB queries via Drizzle (no repository pattern, no ORM abstractions)
- Local component state + URL state (no Redux/Zustand global store)
- Server actions for mutations (no API routes for first-party calls)
- Vitest for tests (no Jest)

We don't use:
- Microservices — single-process Astro is intentional
- Dependency injection containers
- Repository / unit-of-work patterns
- GraphQL — REST + server actions only
- Class-based components — functional only

If you find yourself proposing any of the "don't use" patterns, stop and ask first.

显式的 don’t 和 do 一样重要——真正 override 训练先验的就是它们。整个 CLAUDE.md 控制在 200 行以内;更长的文件会吃掉更多上下文、反而降低遵守度(这是 Anthropic 官方的建议,截至 2026 年 6 月)。如果你还没有 CLAUDE.md,跑 /init 让 Claude 从代码库生成初稿,再手动收紧架构那段。/init 还会读取已有的 AGENTS.md.cursorrules.windsurfrules 并把相关部分折进来。

Step 2:每条规则配一个 canonical example

抽象规则会滑掉,具体的文件指针会粘住:

## Canonical examples

- Feature module: src/features/auth/
  - index.ts (public API)
  - actions.ts (server actions)
  - components/ (UI)
- Database access: src/db/queries/users.ts (direct Drizzle, no repo)
- Component state: src/features/billing/components/CheckoutForm.tsx (useState + URL params)

Claude 读这些文件、抄它们的形状,这就打败了抽象先验。你也可以不内联粘贴、而是用 @path 语法在 CLAUDE.md 任意位置 import 一份更长的约定文档(相对路径相对该文件解析,import 可嵌套,最多四跳,截至 2026 年 6 月):

See @docs/architecture.md for the full pattern catalog.

被 import 的文件会在启动时加载进上下文,所以它帮助的是组织结构,并不会省上下文预算。

Step 2b:用 .claude/rules/ 按路径限定规则

如果漂移只发生在某一块(比如 Claude 老在 src/api/ 下加 controller),别把全局 CLAUDE.md 塞胖。把一条按路径限定的规则放进 .claude/rules/,它只在 Claude 碰到匹配文件时才加载:

---
paths:
  - "src/api/**/*.ts"
---

# API conventions

- API endpoints are Astro server actions in `actions.ts`, not REST controllers.
- No class-based handlers. No DI. Copy `src/features/auth/actions.ts`.

.claude/rules/ 里每个文件管一个主题;paths glob 意味着这条规则只在 Claude 读到匹配文件时才进上下文,让你常驻的 CLAUDE.md 保持精简。没有 paths 字段的规则会每个会话都加载,优先级和 ./.claude/CLAUDE.md 相同。

Step 3:在 task 里点名 canonical example

Add the billing feature.
Architecture: follow `src/features/auth/` exactly — same file structure, same patterns.
DO NOT propose:
- Extracting anything into a separate service
- Adding a dependency-injection layer
- Converting functions to classes
- A global state store

If the task seems to need any of these, stop and ask before changing direction.

「stop and ask」这句是安全阀:Claude 能 flag 出真正的架构需要,而不是私自引入。

Step 4:review 时拒掉架构漂移 PR,并为混杂模式钦定 canonical

Claude 提了发散设计,就推回去而不是接受:

Your PR introduces a `BillingService` class with DI.
This contradicts `src/features/auth/` and CLAUDE.md.

Refactor to:
- Functions in `src/features/billing/actions.ts`
- No service class
- No DI

Then resubmit.

每一次拒收都会引导这个 session 的剩余部分。但对混杂模式(原因 3),单次拒收不够——把赢家写下来,Claude 下次才不会猜错:

## State management
Canonical: Zustand (`src/stores/`). Redux is legacy and being removed; never add to it.
Migrate any Redux you touch; do not extend it.

Step 5:定期审 CLAUDE.md 对不对得上现状

过期 doc 在骗 Claude。把 doc 的「年龄」对照最近的架构改动看一下:

# When did CLAUDE.md last update vs major architecture changes?
git log -1 --format="%ai" CLAUDE.md
git log --since="6 months ago" --oneline -- src/features/ | head

如果 CLAUDE.md 上次编辑之后又落地了大量 feature 改动,那 doc 很可能不反映现状了。同时检查内部矛盾:两条规则打架时,Claude 会随机挑一条。跑 /memory 会列出所有加载的 CLAUDE.mdCLAUDE.local.md.claude/rules/ 文件——用来揪出 monorepo 里某个被遗忘的祖先文件,它可能正把 Claude 往错的模式上拽。

Step 6:prompt 里别用框架词汇

词汇塑造设计。对比:

Bad:  "Build a billing service with a controller and repository."
      -> Claude builds NestJS-style code.

Good: "Add billing to src/features/billing/.
       Follow src/features/auth/ patterns."
      -> Claude follows your conventions.

必须用框架词(「API endpoint」)时,立刻把它绑到你 repo 的形状上:「API endpoint = Astro server action in actions.ts」。

Step 7:用 PreToolUse hook 硬拦该模式(文字拦不住时)

CLAUDE.md.claude/rules/ 都是上下文——Claude 会读、通常会照办,但没有保证。如果某个违规反复出现(比如总有人放过一个 *Service.ts 的 class),就在系统层用 PreToolUse hook 强制掉。Hook 在工具执行之前运行,exit code 2 会拦掉这次调用,无论模型决定了什么。

写进 .claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/no-service-class.sh" }
        ]
      }
    ]
  }
}

一个拒绝新建 class ...Service 的最小守卫:

#!/usr/bin/env bash
# .claude/hooks/no-service-class.sh
input=$(cat)
content=$(echo "$input" | jq -r '.tool_input.content // .tool_input.new_string // ""')
if echo "$content" | grep -Eq 'class [A-Za-z]+Service'; then
  echo "Blocked: this repo uses functions in actions.ts, not service classes. See CLAUDE.md." >&2
  exit 2
fi
exit 0

这就是「指导」和「强制」的区别:hook 不管模型怎么「决定」都生效。省着用,只给那一两个你真的不能允许的模式上。

如何确认修好了

  1. /memory,确认项目 CLAUDE.md(以及相关的 .claude/rules/ 文件)出现在已加载列表里。
  2. 把之前漂移的同一个 task 再发一遍,并点名 canonical example 文件。
  3. 对照你的禁用清单查 diff:没有新 service class、没有 DI container、没拆微服务、是函数不是 class。
  4. 如果你加了 Step 7 的 hook,故意引入一次坏模式,确认这次工具调用被拦下并打出你的提示信息。

预防建议

  • CLAUDE.md 带显式「we use」+「we don’t use」架构清单,控制在 200 行内,并定期对照代码审一遍。
  • 每条架构规则都指向一个 canonical example 文件,让 Claude 能读 + 抄。
  • 目录专属约定放进按路径限定的 .claude/rules/,不要塞进常驻的 CLAUDE.md
  • task prompt 点名 canonical,并显式禁掉常见的发散模式。
  • repo 内模式不一致先钦定赢家(并迁移输家),Claude 才不会挑错。
  • 那一两个你绝对不能允许的模式,用 PreToolUse hook 强制,而不是光写在文字里。

常见问题

为什么 Claude 无视一条明明写在 CLAUDE.md 里的规则? 因为 CLAUDE.md 是作为 system prompt 之后的一条 user message 加载的,不是强制配置——Claude 把它当上下文、会尽量遵守。措辞含糊、某个祖先 CLAUDE.md 里有冲突规则、或文件根本没加载,都会导致漏掉。跑 /memory 确认它加载了,把规则写具体(「no class-based services;照抄 actions.ts」),不可妥协的东西用 PreToolUse hook。

Claude Code 读我的 AGENTS.md 吗? 不读——Claude Code 读 CLAUDE.md,不读 AGENTS.md。如果你的 repo 已经用 AGENTS.md,就建一个 CLAUDE.md、在第一行用 @AGENTS.md 把它 import 进来,再在下面加 Claude 专属说明;或者 ln -s AGENTS.md CLAUDE.md 做软链。在已有 AGENTS.md 的 repo 上跑 /init 也会把它折进来。

架构规则该放哪——CLAUDE.md、.claude/rules/ 还是 hook? 全项目、每个会话都适用的约定放 CLAUDE.md。只在某个目录有意义的规则,用带 paths: glob 的 .claude/rules/,按需加载、不撑爆上下文。必须强制而非建议的规则,用 PreToolUse hook。

我的 CLAUDE.md 变长之后 Claude 遵守得没那么稳了,怎么办? 超过约 200 行的文件会吃更多上下文、降低遵守度(Anthropic 建议,2026 年 6 月)。把按目录的说明拆进按路径限定的 .claude/rules/,把长篇参考资料挪进 @path import,并删掉 linter 已经强制的东西。更短更具体,胜过又长又全。

/compact 之后指令就没了,为什么? 项目根目录的 CLAUDE.md 能挺过 compaction——Claude 之后会从磁盘重新读它。子目录里嵌套的 CLAUDE.md 不会自动重新注入,要等下次 Claude 读到该子目录的文件时才重载。只在聊天里说过的话,compaction 时会丢,所以把反复出现的纠正挪进 CLAUDE.md

相关阅读

外部参考:

标签: #排查 #Claude Code #排查 #架构