Skip to content
Published at:

第 29 章:让 AI 理解你的项目

在上一章中,我们学习了如何通过 Prompt 工程向 Claude Code 精确表达需求。但 Prompt 只是一时的——每次新会话,你都需要重新组织语言。有没有一种方式,能让 Claude Code 进入项目就自带理解力

答案是:设计 AI 友好的项目结构。这不是什么玄学,而是一套务实的工程实践——当你按照 AI 的"阅读习惯"来组织代码时,不需要每次解释,Claude Code 就能自动理解你的意图。

本章将介绍一套系统的方法论:如何从目录结构、文件命名、注释策略、类型系统等维度,让你的项目对 AI 一目了然

本章目标:掌握设计 AI 友好项目的核心原则和实践技巧,让 Claude Code 在进入项目的瞬间就建立准确的认知模型。

29.1 项目结构设计原则(AI 友好型架构)

一个 AI 友好的项目结构,和一个人类友好的项目结构,在设计原则上高度重叠——因为它们都服务于同一个目标:降低理解成本。区别在于,AI 的"理解"方式与人类不同:人类可以依靠直觉和联想,而 AI 依赖模式识别和路径导航。

原则一:单一职责目录

每个目录应该有一个明确的、单一的目的。当你打开一个目录时,不需要读文件内容就应该知道里面有什么。

Claude Code 在探索项目时,首先会用 Glob 和目录列表来建立对项目结构的认知。如果 src/utils/ 里同时放了日期格式化、数据库连接、React Hook 和 CSS 样式,Claude 就无法建立可靠的路径预期——它每次都需要深入文件内容才能判断一个模块的归属。

✅ 好的目录结构:
src/
  components/     ← 只有 UI 组件
  hooks/          ← 只有自定义 Hook
  services/       ← 只有 API 调用和数据获取
  utils/          ← 只有纯工具函数
  types/          ← 只有 TypeScript 类型定义

❌ 混乱的目录结构:
src/
  common/         ← 什么都有:组件、工具、类型...
  helpers/        ← 和 common 有什么区别?不知道
  shared/         ← 又是各种东西混在一起

关键原则:给目录起一个"自解释"的名字。一个新加入项目的开发者(无论是人还是 AI)看到 components/UserProfile/,应该立刻知道这里面是 UserProfile 组件相关的所有文件。

原则二:扁平优于深层

深层嵌套让 Claude 难以通过路径导航找到目标文件。每一次目录深入,都是一次额外的 Glob 或 Read 调用,消耗宝贵的上下文窗口。

✅ 推荐(最多 3 层):
src/
  components/
    UserProfile.tsx
    UserProfile.test.tsx
  hooks/
    useAuth.ts

❌ 避免(5+ 层):
src/
  modules/
    user/
      components/
        profile/
          sections/
            PersonalInfo.tsx  ← 第 6 层!

推荐的最大深度为 3 层(如 src/components/Button/Button.tsx)。如果超过 3 层,说明目录层级承载了过多的分类信息——考虑用命名约定来替代层级深度。例如,与其用 src/modules/user/components/profile/sections/,不如用 src/components/UserProfile/ 将相关文件扁平地放在一起。

原则三:命名即文档

文件名应该描述内容,而非描述类型。一个好的文件名是一个微型的文档——它告诉你这个文件里有什么,而不需要你打开它。

✅ 描述性的命名:
formatDate.ts         ← 一眼就知道是日期格式化
useAuth.ts            ← 一眼就知道是认证 Hook
UserAvatar.tsx        ← 一眼就知道是用户头像组件
validateEmail.test.ts ← 一眼就知道是邮箱验证的测试

❌ 含糊的命名:
utils.ts              ← 什么工具?
helpers.ts            ← 帮什么?
common.ts             ← 跟什么共用的?
misc.ts               ← 杂项 = 垃圾场
index.ts              ← 每个目录都叫 index.ts,搜索时全是同名文件

一个实用的测试:在项目里全局搜索一个文件名,能不能立刻回忆起它是什么?如果能,这个命名就是合格的。如果搜 utils.ts 出来 20 个结果,这个命名就失败了。

原则四:入口文件清晰

对于模块或目录,使用 index.ts(或 index.tsx)作为桶导出(barrel export)文件,让 Claude 能快速理解模块的公共 API。

typescript
// src/components/UserProfile/index.ts
// 桶导出:一眼看清这个模块暴露了什么
export { UserProfile } from './UserProfile';
export { UserProfileSkeleton } from './UserProfileSkeleton';
export type { UserProfileProps } from './types';

Claude Code 读到一个 index.ts 时,不需要深入每个子文件就能理解模块的对外契约。这在大型项目中尤其重要——Claude 可以通过读取每个模块的 index 文件,快速建立对整个项目 API 表面的认知。

注意:不要把所有东西都塞进 index.ts。如果模块内部有大量不对外暴露的实现细节,它们的文件名应该保持描述性,只在 index 中导出真正公开的部分。

原则五:配置集中

把配置文件放在标准位置——Claude Code 的训练数据中包含大量开源项目,它对标准位置有先验认知。

✅ 配置集中 + 标准位置:
.env                    ← 环境变量
.env.example            ← 环境变量模板
tsconfig.json           ← TypeScript 配置
vitest.config.ts        ← 测试配置
.github/workflows/      ← CI/CD 配置
.claude/settings.json   ← Claude Code 配置

❌ 配置散落:
src/config/database.ts     ← 数据库配置
src/config/api.ts           ← API 配置
scripts/env-setup.sh        ← 环境变量脚本
internal/ci/deploy.yaml     ← CI 配置

配置集中的好处不止于 AI 友好。当新人(或未来的你)需要修改某个配置时,他们知道去哪个目录找——而不是在整个代码库里大海捞针。

好结构 vs 坏结构对比

让我们看一个完整的对比。以下两个项目功能完全相同,但结构设计天差地别:

❌ 坏结构:
my-app/
├── src/
│   ├── stuff/
│   │   ├── thing1.js         ← 这是什么?
│   │   ├── thing2.js
│   │   └── other/
│   │       └── deep/stuff.js ← 5 层深
│   ├── utils/
│   │   ├── helpers.js        ← 2000 行,什么都有
│   │   └── moreUtils.js
│   ├── components/
│   │   └── old/
│   │       └── new/
│   │           └── Button.js
│   └── index.js
├── config.js                 ← 配置散落在根目录
├── db.conf                   ← 非标准格式
└── test/                     ← 测试和源码分离
    └── some_test.js

✅ 好结构:
my-app/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.test.tsx
│   │   │   └── Button.module.css
│   │   └── UserProfile/
│   │       ├── UserProfile.tsx
│   │       └── UserProfile.test.tsx
│   ├── hooks/
│   │   ├── useAuth.ts
│   │   └── useAuth.test.ts
│   ├── utils/
│   │   ├── formatDate.ts
│   │   └── formatCurrency.ts
│   └── types/
│       └── user.ts
├── .env.example
├── tsconfig.json
└── vitest.config.ts

当你让 Claude Code "找到日期格式化函数并修复时区 bug"时,在坏结构中它需要盲目搜索,在好结构中它可以直接定位到 src/utils/formatDate.ts。这种效率差异在大型项目中是指数级的。

29.2 文件命名与组织规范

文件命名是最被低估的 AI 友好实践。一个好的文件名是一份微型文档——你在文件名中投入的每一点思考,都会在 Claude Code 的上下文效率上得到回报。

命名规范(附正反例)

✅ 好的命名:
src/
  components/
    UserProfile/
      UserProfile.tsx              ← 组件主文件
      UserProfile.test.tsx         ← 对应测试
      UserProfile.module.css       ← 样式文件
      UserProfileSkeleton.tsx      ← 变体
      index.ts                     ← 桶导出
  hooks/
    useDebounce.ts                 ← Hook,use 前缀
    useLocalStorage.ts
  utils/
    formatDate.ts                  ← 函数名就是文件名
    formatCurrency.ts
    parseCSV.ts
  services/
    userService.ts                 ← 数据层
    orderService.ts
  types/
    user.ts                        ← 类型定义
    api.ts

❌ 坏的命名:
src/
  stuff/
    thing1.js                      ← "thing" 是什么?
    misc_functions.js              ← "杂项函数" = 所有东西
    temp_backup_2023.js            ← 临时文件进了代码库
    final_v2_revised.js            ← 哪个版本是最终版?
  components/
    comp1.jsx                      ← 缩写到无法理解
    Box.jsx                        ← 太通用
    ThatOneComponent.jsx           ← 什么功能?

核心命名规则

规则一:同名共置测试文件。

✅ 测试紧邻源文件:
components/
  Button/
    Button.tsx
    Button.test.tsx      ← 同名 + .test 后缀

❌ 测试放在独立目录:
components/
  Button/
    Button.tsx
tests/
  components/
    button.test.js       ← 文件和测试天各一方

共置测试有双重好处:对 AI 来说,读到源文件时能立刻发现并读取对应的测试文件(测试往往是最好的使用文档);对人来说,修改源码后不需要跨半个项目去找测试文件。

规则二:使用一致且完整的扩展名。

✅ 一致:
.ts   .tsx   .css   .test.ts
.js   .jsx   .mjs   .cjs

❌ 混乱:
.ts   .tsx   .js   .jsx   混用不加区分
有的 .test.ts,有的 .spec.ts  不统一

如果你的团队决定测试文件用 .test.ts,就全部用 .test.ts,不要同时出现 .spec.ts_test.tsTest.ts

规则三:禁用缩写。

文件名和变量名不同——文件名是全项目搜索的入口。缩写会让搜索变得不可靠。

✅ 完整单词:
validateEmail.ts
calculateShippingFee.ts
UserAuthenticationProvider.tsx

❌ 缩写:
valEm.ts               ← 什么?验证元素?评估紧急?
calcShip.ts            ← 船运计算?
UsrAuthProv.tsx        ← 痛苦的阅读体验

唯一的例外是业界通用的缩写(如 api.tsurl.tsdom.tsdb.ts),这些缩写已经成为了领域词汇。

文件组织的"Claude 友好"模式

除了命名,文件组织也有特定的模式让 AI 更容易理解:

✅ 推荐:功能共置(Co-location)
features/
  auth/
    LoginForm.tsx         ← 与认证相关的所有文件在一起
    LoginForm.test.tsx
    SignupForm.tsx
    authService.ts
    authTypes.ts
    useAuth.ts

❌ 避免:按文件类型分离
components/
  LoginForm.tsx
  SignupForm.tsx
services/
  authService.ts
hooks/
  useAuth.ts
types/
  authTypes.ts
tests/
  LoginForm.test.tsx

按类型分离的目录结构要求 Claude 在五个不同目录之间来回跳跃才能理解"认证"这个功能。功能共置则把所有相关信息放在一个目录下,Claude 可以在一次读取中建立对功能全貌的理解。

29.3 注释与文档策略

注释是写给人的,也是写给 AI 的。但 AI 阅读注释的方式与人不同——它会逐字读取上下文中的所有注释,而不像人类那样"扫一眼"。因此,注释的质量直接关系到 AI 的推理质量。

Claude 能从中受益的注释

第一:JSDoc / TSDoc 在公共 API 上。

Claude Code(以及几乎所有 AI 编程工具)的训练数据中包含大量带 JSDoc 的代码。这意味着它对 JSDoc 格式有先验理解——它知道 @param 是参数、@returns 是返回值、@throws 是异常。

typescript
/**
 * 将日期格式化为指定格式的字符串。
 * 内部使用 Intl.DateTimeFormat,自动处理时区。
 *
 * @param date - 要格式化的日期对象或时间戳
 * @param format - 目标格式,支持 'YYYY-MM-DD' | 'MM/DD/YYYY' | 'relative'
 * @param locale - 本地化语言,默认 'zh-CN'
 * @returns 格式化后的日期字符串
 * @throws {RangeError} 当 date 参数无法解析为有效日期时
 *
 * @example
 * formatDate(new Date('2025-06-15'), 'YYYY-MM-DD') // '2025-06-15'
 * formatDate(Date.now(), 'relative') // '3 天前'
 */
export function formatDate(
  date: Date | number,
  format: DateFormat = 'YYYY-MM-DD',
  locale: string = 'zh-CN'
): string {
  // ...
}

当你给 Claude Code 一个任务"修改日期格式支持当地时间"时,它会首先读这份 JSDoc,理解当前函数支持的格式、参数含义和边界条件,然后做出精准的修改。

第二:文件顶部放模块级注释。

每个文件顶部用一句话说明模块的职责和它在系统中的位置。这对 AI 非常有用——它读文件的第一秒就知道"我在读什么"。

typescript
/**
 * 用户认证服务模块
 *
 * 职责:处理用户注册、登录、Token 刷新、会话管理。
 * 依赖:userRepository(数据层)、tokenService(JWT 生成/验证)
 * 被调用:AuthController → authService → userRepository
 *
 * 注意:密码哈希使用 bcrypt(同步版本会阻塞事件循环,统一使用异步版本)。
 */

这段注释为 Claude 提供了三个维度的信息:职责(这个模块做什么)、依赖关系(它在系统中的位置)、注意事项(已知的陷阱)。这三者正是 AI 理解一个模块时最需要的信息。

第三:解释"为什么",而非"是什么"。

❌ 无用的注释(只描述了代码本身):
// 遍历订单列表
for (const order of orders) { ... }

// 将 status 设置为 'cancelled'
order.status = 'cancelled';

✅ 有价值的注释(解释了原因):
// 使用倒序遍历以避免 splice 导致的索引偏移
for (let i = orders.length - 1; i >= 0; i--) { ... }

// 状态设为 cancelled 而非 deleted,因为需要保留审计记录
// 参见 ADR-007:订单状态管理策略
order.status = 'cancelled';

Claude Code 在读取代码时,自己就能看出"这段代码在做什么"——代码本身就是"做什么"的最佳描述。它需要注释来理解的是为什么这样做——设计决策、边界情况、历史包袱。这些信息无法从代码中推断,但却是正确修改代码的前提。

第四:复杂模块的 README。

对于包含多个文件、有复杂交互逻辑的模块,在模块目录下放一个简短的 README.md。

markdown
# 支付模块 (payment/)

## 模块职责
处理支付创建、支付回调、退款、对账。

## 核心流程
1. 用户发起支付 → `createPayment()` 调用第三方支付 API
2. 支付平台异步回调 → `handleCallback()` 验签并更新订单状态
3. 退款 → `refund()` 调用退款 API,记录退款流水

## 关键约定
- 所有金额单位为分(避免浮点精度问题)
- 异步回调需要做幂等处理(回调可能重复到达)
- 支付状态变更通过 EventEmitter 通知订单模块

## 测试
- 单元测试:mock 第三方 API 响应
- 集成测试:使用沙箱环境

这份 README 的价值在于:当 Claude Code 第一次进入支付模块时,它用 50 行文字就建立了一个完整的认知模型。没有这份 README,它需要读 5 个文件、理解函数间的调用链、推断数据流——这个过程消耗的上下文可能超过 2000 tokens。

不该做的事:过度注释

过度注释的危害不亚于没有注释——它占用上下文窗口,稀释关键信息。

❌ 过度注释:
// 声明一个变量叫 name,类型是 string,初始值为空字符串
const name: string = '';

// 调用 API 获取用户数据
const user = await api.getUser(id);

// 如果 user 存在
if (user) {
  // 更新用户名
  user.name = name;
}

这些注释没有提供任何代码之外的信息,只是在翻译代码。对于 Claude 来说,读这种注释是纯粹的上下文浪费。

注释的核心原则:如果你的注释能被任何一个懂这门语言的开发者从代码本身直接读出来,那这个注释就是多余的。只有当注释传达了代码中不可见的信息(意图、原因、陷阱、历史)时,它才有价值。

CLAUDE.md 作为注释的"总目录"

所有模块级注释和 README 最终可以被 CLAUDE.md 索引。CLAUDE.md 不重复这些注释的内容,而是指向它们的位置:

markdown
## Architecture
- `src/payment/` — 支付模块。详见 `src/payment/README.md`
- `src/auth/` — 认证模块。核心逻辑在 `authService.ts` 的模块注释中

这种"总索引 + 按需深入"的模式是最节省 AI 上下文的方式——CLAUDE.md 提供地图,Claude 按需读取具体的文档。

29.4 CLAUDE.md 作为"项目的说明书"

如果说好的目录结构和注释是"给 AI 铺路",那 CLAUDE.md 就是"给 AI 一张完整的地图"。它是整个 AI 友好策略中投入产出比最高的一项投入。

CLAUDE.md —— 最重要的单一文件

CLAUDE.md 是 Claude Code 进入项目时读取的第一个文件。它被注入到系统提示词中,在整个会话期间持续生效。一份好的 CLAUDE.md 意味着 Claude 从对话的第一秒就理解了项目——不需要你解释"这个项目是做什么的"、"目录怎么组织的"、"命令怎么运行"。

我们在第 12 章中已经深入讲解了 CLAUDE.md 的编写方法论。本章不再重复基础内容,而是从"AI 友好项目"这个更高维度,重新审视 CLAUDE.md 应该包含什么。

每一份 CLAUDE.md 应该包含的内容

1. 技术栈概览       ← Claude 知道该用什么工具和库
2. 目录结构地图     ← Claude 知道去哪里找什么
3. 构建/测试/开发命令 ← Claude 知道怎么运行项目
4. 编码规范         ← Claude 知道怎么写代码
5. 要避免的反模式   ← Claude 知道什么不能做
6. 陷阱和变通方案   ← Claude 知道哪里有坑、怎么绕过去

第 6 项(陷阱和变通方案)是最容易被忽略但最关键的。 项目的隐性知识——"这个库的 3.x 版本有内存泄漏"、"配置文件必须手动更新"、"dist 目录有自己的 git 仓库不能删除"——这些信息 Claude 无法通过读代码发现,但却是决定代码正确性的关键。

何时更新 CLAUDE.md

每当以下事件发生时:

事件CLAUDE.md 更新内容
新增或删除目录Architecture 部分的目录结构
技术栈升级(如 React 17 → 18)Tech Stack + Commands
新增构建/测试命令Commands
发现新的坑陷阱和变通方案
编码规范变更Code Style
新增模块Architecture + 关键文件说明

CLAUDE.md 不是一成不变的文档——它是项目的"活文档",随项目一起演进。把"检查 CLAUDE.md 是否需要更新"加入你的 PR checklist。

真实案例:本书项目的 CLAUDE.md

本书项目(一个基于 VitePress 的个人 Wiki/Blog)就使用了一份约 60 行的 CLAUDE.md。它的精妙之处在于——每一行都有明确的目的,没有任何冗余信息

让我们逐段分析:

markdown
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working
with code in this repository.

第一段:声明了这是一个给 Claude Code 的指导文件。直接、简洁。

markdown
## Commands

```bash
pnpm dev          # Start dev server (hot-reloads .md; restart for config.ts changes)
pnpm build        # Build static site to .vitepress/dist/
pnpm preview      # Preview built site

make post 'title' # Scaffold new post at src/posts/YYYY/MM_DD_title.md
make image        # Resize images to 1400px, convert png/jpg → webp
make deploy       # Clean, build, force-push dist/ to GitHub Pages repo
make clean        # Remove .vitepress/cache and .vitepress/dist/assets
```

No test suite exists.

Commands 部分:每条命令一行加注释。特别注明 pnpm dev.md 文件热更新但对 config.ts 修改需重启——这是一个很容易踩的坑。最后明确声明"没有测试套件",避免 Claude 反复尝试运行测试。

markdown
## Architecture

Personal wiki/blog built with VitePress. Content in Markdown under `src/`,
deployed as a static site to GitHub Pages (`https://wshibin.github.io`).

- **`src/`** — Content root (`srcDir: 'src'`). All `.md` files become pages.
- **`src/posts/YYYY/MM_DD_title.md`** — Blog posts by year.
  `posts.data.ts` reads frontmatter and exposes a sorted `Post[]`
  for the Archives page.
- **`src/drafts/`** — Draft posts, excluded from build via `srcExclude`.
- **`.vitepress/config.ts`** — Single source of truth for nav, sidebar,
  and all site settings. Must be updated manually when adding new sections.
- **`.vitepress/theme/`** — Custom theme.
- **`.vitepress/dist/`** — Build output with its own `.git` repo.
  **Do not delete this directory.**

Content areas: `csprimer/`, `linux/`, `windows/`, `lang/`, `misc/`,
`backend/`, `multimedia/`.

Architecture 部分:每个目录说明了为什么存在怎么使用。两个地方被加粗强调:"必须手动更新 config.ts"和"不要删除 dist 目录"——这是最致命的两个陷阱。

markdown
## Content Conventions

### Two page types

**Post** (blog entries):
```yaml
---
title: 'Post Title'
sidebar: false
prev: false
next: false
comments: true
date: 2025-01-15 10:00:00
---
```

**Doc** (reference pages):
```yaml
---
title: 'Doc Title'
sidebar: true
prev: true
next: true
date: 2025-01-15 10:00:00
---
```

Key differences: posts have sidebar: false, prev: false, next: false,
comments: true, and categories/tags. All pages use
`# {{ $frontmatter.title }}` as the first heading.

Content Conventions 部分:通过嵌入完整的 frontmatter 模板和差异说明,Claude 在创建新页面时能直接复制正确的格式。第 29 章:让 AI 理解你的项目 这个规则如果不写,Claude 大概率会写死一个字符串。

markdown
## Other

- git commit 信息使用中文

Other 部分:最重要的特殊约定,一句话说完。Claude 看到这条后,所有 commit message 都会自动用中文。

这份 CLAUDE.md 总共约 60 行(不含代码块),但信息密度极高。 它做到了精准(每条信息都有明确的目的)、可执行(Claude 可以据此直接行动)、简洁(不浪费一行文字)。

CLAUDE.md 扩展策略

当项目增长到 CLAUDE.md 装不下时,不要把它变成 500 行的巨无霸。使用引用策略

markdown
# CLAUDE.md(根目录,保持 80 行以内)

## Project Overview
Monorepo with 5 packages, managed by pnpm workspaces.

## Commands  (全局命令)
...

## Packages
- `packages/web/` — React frontend. 详见 packages/web/CLAUDE.md
- `packages/api/` — Express backend. 详见 packages/api/CLAUDE.md
- `packages/shared/` — Types + utils. 影响所有 package

## Global Rules
...

根 CLAUDE.md 作为总目录,具体信息放在子项目的 CLAUDE.md 或 README.md 中,Claude 按需读取。这样既保持了根文件的精简,又确保了信息的可达性。

29.5 模块化与单一职责

"模块化"这个概念我们听得太多了——高内聚低耦合、SOLID 原则……但在 AI 时代,模块化多了一个新的、非常务实的理由:让 AI 在单次读取中理解一个文件

AI 窗口经济

Claude Code 每次读取文件的默认行数约为 200-300 行。这意味着一个 200 行的文件可以被完整读入上下文——Claude 能看到它的全貌、理解它的完整逻辑。而一个 2000 行的文件,Claude 需要分段读取,在多次读取之间拼凑理解,容易遗漏跨段的逻辑关联。

文件大小与 AI 理解能力的关系:

 50-200 行: ✅ Claude 一次读完,理解完整,分析精确
200-500 行: ✅ 仍然可以一次读完,理解良好
500-1000 行:⚠️ 需要分段,Claude 需在片段间建立联系
1000-2000 行:❌ 必须分段,Claude 的上下文窗口被大量消耗
2000+ 行:    ❌ Claude 永远只看到文件的一部分,理解不完整

这提供了一个全新的模块化驱动力:不是因为"大文件不好维护",而是因为"大文件让 AI 无法完整理解"。

"Claude 测试"

一个实用的自检方法:"Claude 测试"——如果让 Claude Code 只读一次这个文件,它能准确理解里面有什么吗?

"Claude 测试":

问:这个文件做什么?
→ Claude 能回答 ✅
→ Claude 说不清楚 ❌ → 需要拆分

问:这个文件的公开 API 有哪些?
→ Claude 能列举 ✅
→ Claude 漏掉或弄混 ❌ → 需要拆分

问:如果要修改 X 功能,改哪个文件?
→ Claude 能准确指出 ✅
→ Claude 要翻好几个文件 ❌ → 需要更好的模块化

如果一个文件通过不了"Claude 测试",它很可能也通过不了人类同事的代码审查——这两者的标准高度重合。

单一职责:一个文件 = 一个目的

✅ 单一职责:
formatDate.ts         ← 只做日期格式化
useDebounce.ts        ← 只做防抖
UserAvatar.tsx        ← 只渲染用户头像

❌ 职责混乱:
userUtils.ts          ← 包含认证、格式化、验证、权限检查……什么都有
helpers.ts            ← 2000 行的"万能助手"
index.ts              ← 既导出又包含业务逻辑

当一个文件只有一个职责时:

  • 文件名可以准确描述内容(回到 29.2 节的命名规范)
  • Claude 可以一次读完并完全理解(回到窗口经济)
  • 修改时的影响范围明确(改 formatDate 不会意外影响 UserAuth)

拆分策略:根据什么拆分

不是所有大文件都需要拆分。拆分的标准是职责边界,而不是行数。

拆分的信号:
1. 文件中有多个不相关的 export 函数 → 按功能域拆分
2. 同样的概念被多个 export 共享,但未形成模块 → 提取为独立文件
3. import 列表超过 15 行 → 这个文件做了太多事
4. 文件描述需要一个"和"字 → formatDate AND validateEmail → 拆
5. 测试文件难以命名 → 测试覆盖多个不相关的功能 → 拆

不拆的信号:
1. 多个函数紧密协作完成一个目标 → 保留在一起更好
2. 拆分后需要在文件间共享大量私有状态 → 拆分增加了复杂度
3. 文件虽长但逻辑线性清晰(如算法实现)→ 一起读更有上下文

权衡原则:拆分的目的是让 AI 和人更容易理解,而不是机械地追求 200 行上限。如果一个 400 行的算法文件拆开后反而更难理解,那就不拆。如果一个 150 行的"万能工具"文件包含了三个完全不相关的功能域,那就拆。

29.6 类型系统的作用

对于使用 TypeScript、Rust、Kotlin 等静态类型语言的项目,类型系统是 AI 理解代码的第一辅助。类型是机器可读的文档——它们精确地描述了数据形状、函数契约和模块边界,而不需要任何人类语言。

类型作为文档

对于 AI 来说,读取类型定义是最高效的理解方式。不需要读注释,不需要读实现,只需要看接口定义就能理解数据如何流动。

typescript
// 看这个接口定义,Claude 立刻知道:
// 1. User 对象有哪些字段
// 2. 每个字段的类型和含义
// 3. 哪些字段是可选的
// 4. preferences 的嵌套结构
interface User {
  id: number;
  username: string;
  email: string;
  avatar?: string;                  // 可选
  role: 'admin' | 'editor' | 'viewer';  // 联合字面量
  createdAt: Date;
  preferences: {
    theme: 'light' | 'dark';
    language: string;
    notifications: boolean;
  };
}

一份精确的类型定义胜过十行注释。 注释可能过时(代码改了注释没改),但类型定义和代码是耦合的——类型改了,编译器会报错。这意味着类型定义始终是最新的"文档"。

"接口优先"设计

在 AI 辅助编程中,推荐采用"接口优先"的开发模式:先定义类型/接口,再写实现。这样做有两个好处:

  1. 你可以在不写一行实现代码的情况下,让 Claude 帮你验证设计——给它接口定义,问它"这个 API 设计合理吗?有没有遗漏的边界情况?"
  2. Claude 基于接口生成的实现,质量远高于无类型的纯描述——接口消除了歧义,Claude 不需要猜测参数类型和返回值结构
实际工作流示例:

第一步:定义接口(你写或让 Claude 写)
```typescript
interface PaymentService {
  createPayment(order: Order): Promise<Payment>;
  handleCallback(data: CallbackData): Promise<void>;
  refund(paymentId: string, amount: number): Promise<Refund>;
}

第二步:让 Claude 实现 "请基于 PaymentService 接口实现具体的支付服务, 使用 Stripe SDK,处理错误和重试。"

第三步:Claude 基于接口生成实现,类型约束保证了实现的正确性


在这个工作流中,接口扮演了**契约**的角色——它明确划定了实现的边界,Claude 在这个边界内自由发挥,不会偏离轨道。

### 类型系统如何帮助 AI

类型系统对 AI 的帮助体现在三个层面:

**层面一:消除歧义。**

```typescript
// 没有类型的 JavaScript
function process(data, options) {
  // data 是什么?字符串?对象?数组?
  // options 有哪些字段?Claude 全靠猜
}

// 有类型的 TypeScript
function process(
  data: UserInput,
  options: ProcessOptions
): ProcessResult {
  // Claude 准确地知道所有参数和返回值
}

层面二:提供自动补全般的上下文。

当 Claude 修改一个函数时,它能通过类型系统看到所有可用的属性和方法。这相当于 IDE 的自动补全,但在 AI 的"大脑"里运行——它不需要实际执行代码就能知道 user.email 是 string、user.role 是联合字面量。

层面三:错误预防。

Claude 生成的代码如果类型不匹配,编译器和 LSP 会立刻报错。这形成了一个安全的反馈循环:AI 生成代码 → 类型检查 → 如果不通过,AI 根据错误修正。类型系统充当了 AI 代码的"自动验收测试"。

没有类型系统怎么办

如果你的项目使用 JavaScript、Python、Ruby 等动态语言,没有静态类型系统,如何实现类似的效果?

替代方案:契约注释(Contract Comments)。

javascript
/**
 * @contract
 * 计算订单总价(含运费和可能的折扣)。
 *
 * @param {Object} order
 * @param {Array<{id: string, price: number, quantity: number}>} order.items
 * @param {string} order.shippingAddress
 * @param {string} [order.couponCode] - 可选的优惠券代码
 * @returns {{ total: number, discount: number, shipping: number }}
 */
function calculateOrderTotal(order) {
  // ...
}

这种 @contract 注释用 JSDoc 的 @param@returns 详细描述了数据结构。虽然不如 TypeScript 的类型系统那样有编译器保证,但至少给了 Claude 明确的预期。

更好的替代方案:如果项目允许,逐步引入 TypeScript 或使用 JSDoc 类型注解(TypeScript 支持从 JSDoc 推断类型)。

29.7 AI 友好项目检查清单

前面六节覆盖了从宏观到微观的 AI 友好设计原则。本节将它们浓缩为一张可操作的检查清单——你可以在新项目开始前对照它做规划,也可以在旧项目中逐个勾选,渐进式地提升 AI 友好度。

检查清单

AI 友好项目检查清单

📋 基础层(最低要求,每个项目都应满足)

□ CLAUDE.md 存在且内容有效
  - 包含技术栈、命令、架构、规范四个核心模块
  - 最近三个月内有更新记录
  - 项目特有的陷阱和变通方案已被记录
  - 总长度控制在 300 行以内

□ 目录结构逻辑清晰
  - 每个目录有单一的、明确的职责
  - 目录嵌套不超过 3 层
  - content 的根目录(如 src/)直下没有"孤儿文件"

📋 命名层

□ 文件名描述性
  - 用"这个文件做什么"来命名,而非"这个文件是什么类型"
  - 没有 utils.ts、helpers.ts、common.ts 之类的"万能文件"
  - 文件扩展名在整个项目中一致(全部 .ts 或全部 .js,不混用)

□ 公共 API 有注释
  - 所有 export 函数有 JSDoc/TSDoc 注释
  - 注释包含 @param、@returns、@throws(如适用)
  - 复杂函数有 @example

📋 组织层

□ 测试与源文件共置
  - 测试文件紧邻被测试的源文件
  - 测试文件命名约定统一(如 .test.ts 或 .spec.ts,二选一)

□ 没有任何文件超过 500 行
  - 超过的已被拆分,拆分标准是职责边界而非行数
  - 每个文件通过"Claude 测试"

□ 构建/测试/运行命令已文档化
  - 在 CLAUDE.md 或 package.json 的 scripts 中清晰记录
  - 新成员能在 5 分钟内跑通项目

📋 高级层(加分项)

□ 类型系统被充分利用
  - TypeScript 严格模式
  - 公共 API 有明确的接口定义
  - 不使用 any 类型(除非有充分理由并注释说明)

□ 模块级文档存在
  - 复杂模块有 README.md
  - 每个源文件顶部有简短的模块级注释

□ 代码规范被记录
  - ESLint/Prettier/Biome 配置在代码库中
  - 团队的特殊约定写入 CLAUDE.md

□ CI/CD 中有 AI 友好的验证
  - 禁止 utils.ts 等万能文件名(可配置 lint rule)
  - 检查 CLAUDE.md 是否存在且非空(CI 中自动验证)

如何渐进式达标

如果你的项目现在离检查清单差得很远,不要试图一天全部改完。推荐的渐进路径:

第一周:CLAUDE.md + 目录整理
  □ 写一份 CLAUDE.md(用 /init 起手,手动精炼)
  □ 整理顶层目录结构,删除空目录,合并重复目录

第二周:文件命名 + 注释补充
  □ 重命名所有 utils/helpers/common 文件为描述性名称
  □ 为所有 export 函数补充 JSDoc

第三周:文件拆分 + 测试共置
  □ 拆分所有超过 500 行的文件
  □ 将独立目录的测试文件移到源文件旁边

第四周:类型系统 + 持续维护
  □ 升级 TypeScript 严格模式
  □ 建立"CLAUDE.md 随架构变化而更新"的习惯

每一步完成后,你都会感受到 Claude Code 理解力的提升——它不再需要你反复解释项目结构,不再需要盲目搜索文件,不再生成风格不一致的代码。

29.8 本章小结

让 AI 理解你的项目,不是靠"魔法提示词",而是靠工程设计。本章从七个维度构建了一套完整的 AI 友好项目方法论:

维度核心原则一句话总结
目录结构单一职责、扁平、命名即文档让 Claude 通过路径就知道文件在哪
文件命名描述性、一致性、禁用缩写文件名是微型文档,每个字都在提供信息
注释策略JSDoc 在公共 API、解释"为什么"、模块级注释注释传达代码中看不见的信息
CLAUDE.md技术栈 + 命令 + 架构 + 规范 + 反模式 + 陷阱投入产出比最高的单一投入
模块化200-500 行、单一职责、功能共置让 Claude 在单次读取中理解文件全貌
类型系统接口优先、类型即文档、消除歧义机器可读的文档,永不过时的注释
检查清单基础层 → 命名层 → 组织层 → 高级层可操作、可度量、可渐进实施

最重要的一个转变:把"AI 能不能理解我的项目"从一个被动问题("AI 行不行?")变成一个主动问题("我的项目对 AI 够不够友好?")。当你开始主动设计 AI 友好的项目结构时,受益的不仅是 Claude Code——清晰的结构、描述性的命名、到位的注释、严格的类型,这些同时也在提升人类同事的开发体验。

AI 友好的项目 = 人类友好的项目。为 AI 优化就是为人优化。

在下一章,我们将探讨 AI 时代的核心工作模式——人机协作。如何在人与 AI 之间建立高效的分工?什么该交给 AI,什么必须由人来决策?如何避免"AI 写了我看不懂的代码"的窘境?这些问题将帮助你把 AI 编程从"会用"提升到"用得好"。