Claude Code 误解你的项目架构:6 个 generic prior 取胜 + 用「我们用 / 我们不用」清单 + canonical 锁死

Agent 建议「拆成微服务」但你刻意单体——在 CLAUDE.md 写「we use / we don't use」+ 真文件 canonical example,让具体战胜泛型先验。

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

架构不合是 Claude 把通用「best practice」套到你团队刻意决定的架构上。修法是让约定不可避免——用「该做 / 不该做」配对锚到真实文件,让具体例子打败泛型先验。

常见原因

按命中率从高到低:

1. CLAUDE.md 缺架构或太虚

没明确规则(「我们是刻意单体;不要提拆分」),Claude 默认走”训练数据里看着专业的”——往往就是微服务、DI、repository pattern。

如何判断grep -i "architecture\|monolith\|microservice\|pattern" CLAUDE.md 空——架构是隐式的,没写下来。

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

Claude 训练时见过 10 万个「抽成 class + DI」例子,你的”只用函数”在它见过的 0 个例子里。没显式 override,先验取胜。

如何判断:Claude 建议镜像某个流行框架模式(NestJS DI、Spring bean)——你拿到的是框架默认,不是项目默认。

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

代码库一半 Redux 一半 Zustand(在迁移中),Claude 看到两种、不知哪个 canonical,默认 Redux——因为训练里更常见。

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

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

「优先组合不要继承」——原则没错,但 Claude 没具体抄的文件。抽象规则不迁移、具体例子才迁移。

如何判断:CLAUDE.md 都是声明、没文件指针——给每条配一个「canonical: path/to/file.ts」。

5. 新 task 措辞暗示新模式

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

如何判断:你 prompt 里出现了项目不用的框架词(“service”、“controller”、“repository”)——Claude 套了那个框架。

6. CLAUDE.md 过期了

架构以前是那样的,CLAUDE.md 还描述旧方案。Claude 按 CLAUDE.md 干、代码和新(真)架构矛盾、你怪 Claude。

如何判断:CLAUDE.md 6+ 月没更新而架构有演进——过期 doc 在骗 Claude。

最短修复路径

按收益从高到低。前 3 步在耐久层锁住正确模式。

Step 1:写显式「我们用 / 我们不用」规则

CLAUDE.md

## 架构

我们用:
- 单 Astro app(单体),一次部署
- 函数优于 class(不写 class-based service)
- Drizzle 直查 DB(不用 repository pattern,不要 ORM 抽象)
- 局部组件 state + URL state(不用 Redux/Zustand 全局 store)
- Server actions 做 mutation(first-party 调用不走 API route)
- Vitest 测试(不 Jest)

我们不用:
- 微服务——单进程 Astro 是刻意的
- Dependency injection container
- Repository / unit-of-work 模式
- GraphQL——REST + server action only
- Class-based component——只 functional

如果你打算提议任何「不用」清单上的模式,先停下来问。

显式 don’t 和 do 一样重要——它们 override 训练先验。

Step 2:每条规则配 canonical example

抽象规则会滑掉,具体例子会粘住:

## Canonical examples

- Feature 模块:src/features/auth/
  - index.ts(公开 API)
  - actions.ts(server actions)
  - components/(UI)
- DB 访问:src/db/queries/users.ts(直 Drizzle,无 repo)
- 组件 state:src/features/billing/components/CheckoutForm.tsx(useState + URL params)

Claude 读这些文件、抄它们的形状——打败抽象先验。

Step 3:task 里点名 canonical

加 billing feature。
架构:完全照 `src/features/auth/`——同文件结构、同模式。
不要提议:
- 抽什么进独立 service
- 加 DI 层
- 函数转 class
- 全局 state store

如果 task 真需要其中之一,先停下来问再改方向。

「先停下来问」是安全阀——Claude 能 flag 真正的架构需要,而不是私自引入。

Step 4:review 时拒架构漂移

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

你的 PR 引入了 `BillingService` class + DI。
和 `src/features/auth/` + CLAUDE.md 矛盾。

重构成:
- 函数在 `src/features/billing/actions.ts`
- 没有 service class
- 没有 DI

再提交。

每次拒收训练本 session 的剩余部分。

Step 5:季度审 CLAUDE.md vs 现状

过期 doc 在骗 Claude。定季度刷新:

# CLAUDE.md 最后更新 vs 主架构改动时间
git log -1 --format="%ai" CLAUDE.md
git log --since="6 months ago" --oneline -- src/features/ | head

CLAUDE.md 更新后又有大量 feature 改动——doc 可能不反映现状,刷新。

Step 6:prompt 里别用框架词汇

词汇塑造设计:

差:"Build a billing service with a controller and repository."
     → Claude 给你 NestJS 风。

好:"在 src/features/billing/ 加 billing 功能。
     照 src/features/auth/。"
     → Claude 沿用你的约定。

必须用框架词时(“API endpoint”),配上你项目的具体形状(“API endpoint = Astro server action in actions.ts”)。

预防建议

  • CLAUDE.md 写显式「我们用」+「我们不用」清单,季度刷新
  • 每条架构规则配 canonical example 文件让 Claude 读 + 抄
  • task prompt 里点 canonical 并显式禁掉常见发散模式
  • repo 内模式不一致先收敛(钦定一个、迁移另一个)——Claude 才不会挑错
  • 别在 prompt 里用框架词汇,让 example 文件设形状
  • 把架构漂移 PR 当 CLAUDE.md 文档缺口修,不只是单次拒收

相关阅读

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