首页 / 文章

如何规划 Claude Code 项目,让智能体在大型代码库中保持专注

大型代码库的六种架构模式:分层式的CLAUDE.md文件、技能模块、子智能体、拒绝规则、稀疏工作树,以及能够保持智能体上下文整洁的语言服务器插件。

发布者:REACTAPP.TOP阅读约 10 分钟2372 词

来源: How I Structure Claude Code Projects So Agents Don’t Get Lost in Large Codebases

想象一下向 Claude Code 提出的一个简单请求:修复认证中间件中的会话超时错误。真正的解决方案只有三行代码。然而在找到这些代码之前,该智能体先在日志系统、迁移历史记录、一半的 API 接口以及出于无人能解释的原因而涉及的 Terraform 配置中四处搜寻。读到第 147 个文件时,开发人员已经不得不按下退出键。

在这样的场景中,问题并不出在模型上,而在于项目结构。所有的规范、构建命令和架构决策都被汇总到一份长达 340 行的根文件 CLAUDE.md 中,智能体在启动时会忠实读取这份文件,之后便把剩余的计算资源用于探索与认证无关的代码。

当把智能体当作新员工对待,要求其在纠正拼写错误前先通读整个公司手册时,就会出现这种情况。它需要在恰当的时机获得合适的上下文,而非一次性获取所有信息。下文将介绍六种解决此问题的方法,以及判断何时该使用每种方法的依据。

为何上下文会迅速耗尽

此处使用的模型,其Claude Code上下文窗口约为200,000个标记,听起来似乎很大,但一旦列出会占用空间的内容就会发现其实不然:你发送的每条消息、读取的每个文件、所有的命令输出、每条CLAUDE.md指令以及任何自动保存的记忆笔记都会占用空间。一个标记大约相当于四个字符。

在庞大的代码库中,还没来得及编写一行代码,50,000个令牌就可能已被消耗掉。一个包含300行的根级CLAUDE.md文件大约需要1,200个令牌,500行的文件则需要2,000个令牌,而使用grep或find进行搜索还会进一步消耗令牌。等到智能体理解任务时,一半的令牌可能已经用完。随着令牌逐渐耗尽,性能也会下降:早期的指令会被遗忘,文件需要重新读取,从而出现在上下文完整时不会出现的错误。增加令牌上限并非解决办法,关键在于将无关内容排除在上下文之外。

模式1:分层式的CLAUDE.md文件

第一个错误就是把所有内容都塞进根级文件中:将API的构建命令、前端组件模板、迁移规则、部署检查清单以及测试规范混在一起。

Claude Code 在会话启动时会读取从工作目录到仓库根目录的 CLAUDE.md 文件。如果在子目录中启动它,它会读取该文件夹中的文件以及其所有上级目录直至根目录的文件。因此需按作用范围划分内容:

monorepo/
  CLAUDE.md                     # repository-wide rules only
  packages/
    api/
      CLAUDE.md                 # API-specific conventions
      .claude/skills/
      src/
    web/
      CLAUDE.md                 # frontend-specific conventions
      .claude/skills/
      src/
    shared/
      CLAUDE.md                 # shared library conventions
      src/

根文件仅包含仓库结构与提交规范,约40行代码,每个包则包含其自身针对特定技术栈的详细信息。在packages/api/中启动代理后,它会加载根文件以及API相关的文件,而不会涉及前端规范。若切换到packages/web/,同样的逻辑会提供不同的上下文信息。

根文件的内容可能如下:

# Repository Structure

This is a monorepo with three packages under packages/:

- packages/api: Node.js REST API with Express, TypeScript, PostgreSQL
- packages/web: React frontend with Vite, TypeScript, TailwindCSS
- packages/shared: shared TypeScript utilities

Run commands from the package directory, not the monorepo root.
Each package has its own tsconfig.json, package.json, and test suite.

# Commit Conventions

- Prefix commits with the package name: "api: fix session timeout"
- One commit per logical change
- Run tests before committing

API 包仅在其中添加了真正重要的内容:

# API Package

This is the REST API server.

## Commands

- Run tests: `npm test` (uses Vitest)
- Run dev server: `npm run dev` (port 3001)
- Database migrations: `npm run migrate`
- Environment variables: copy `.env.example` to `.env`

## Code Patterns

API routes are in src/routes/. Each route file exports an Express router.
Database queries use Knex in src/db/. Never write raw SQL in route handlers.

## Testing

Tests are in src/__tests__/ mirroring the src/ directory.
Use supertest for HTTP assertions, not raw fetch.
Always wrap database tests in a transaction that rolls back.

在这个示例中,加载的上下文行数从340减少到80行,而身份验证相关的处理文件也从147个变为12个。

模式2:按需获取知识的技能

即便采用分层架构也可能会造成臃肿。测试模板、部署检查清单以及调试指南都属于流程性内容而非固定事实,代理在启动时并不需要这些内容,只有在任务需要时才会使用。这就是技能的作用。

技能存储在.claude/skills/目录下,当Claude判断其相关时或通过斜杠命令明确调用时才会被加载。技能的描述会说明使用时机,其内容则包含实际功能。在此示例中,测试指南从API文件中移出并放入了某个技能中:

# .claude/skills/api-testing/SKILL.md

---
name: api-testing
description: Testing patterns for the API package. Use when writing or modifying tests in packages/api/.
---

## Test Structure

Tests are in `src/__tests__/` mirroring the `src/` directory structure.
Each route file has a corresponding `.test.ts` file.

## Running Tests

- All tests: `npm test`
- Single file: `npm test -- src/__tests__/routes/users.test.ts`
- Watch mode: `npm test -- --watch`

## Test Utilities

- `src/__tests__/helpers/db.ts`: provides `setupTestDb()` and `teardownTestDb()`
- `src/__tests__/helpers/auth.ts`: provides `createTestUser()` and `getAuthToken()`

## Patterns

- Use `supertest` for HTTP assertions, not raw fetch
- Always wrap database tests in a transaction that rolls back
- Mock external services in `src/__tests__/mocks/`

要求对认证中间件进行测试,代理会加载测试技能并遵循其规则。让它修复超时错误时,该技能却保持未加载状态,因为测试与此无关。

技能也适用于可重复的工作流程。通过 /fix-pr-comments 调用的技能可以读取 GitHub pull request 上的评审意见,确定所需的修改内容,编辑代码,运行测试并推送更改。整个流程都存储在技能中,而非每次会话都需要重新读取上下文的 CLAUDE.md 文件中。

一般原则是:每次会话都需要的信息放在 CLAUDE.md 中,而流程和参考资料则转化为技能。

模式3:用于独立探索的子代理

即便使用了分层结构和各种技巧,探索过程仍显得过于繁琐。“认证机制是如何工作的?”这个问题在处理登录、会话存储、令牌刷新及登出等流程时可能需要读取40个文件,而所有相关内容在问题得到解答后仍会留在主对话中,从而污染整个会话的整洁性。

子代理在独立的上下文窗口中执行探索操作,仅返回总结信息。文件读取及grep搜索的结果则保留在其自身上下文中。主线程仅接收总结内容,从而保持主对话的整洁。一个研究请求可能如下所示:

Use a subagent to investigate how our authentication system handles
session timeout and token refresh. Report back what files are involved
and how the flow works.

子代理会读取所需内容,追踪流程并反馈结果,这样主对话就能收到约200字的简洁说明,而非40个文件的全部内容。在实施完成后,子代理还能协助进行审查:

Use a subagent to review the session timeout fix for edge cases
and consistency with our existing auth patterns.

审查子代理看到的差异是最新状态,但不包含导致该变化的推理过程。

需要注意一点:摘要会丢失细节。如果需要精确的行号或代码片段,主代理可能不得不重新阅读子代理已经查看过的文件。在其他地方进行探索的同时,子代理会负责处理实现相关的主要任务。

模式4:保持生成的文件和第三方提供的文件未被查看

默认情况下,.gitignore 中列出的文件会被忽略,因此 node_modules/、dist/ 和 build/ 目录不会出现在搜索结果中。不过仍有一些生成的代码或第三方提供的代码会被提交:编译后的 protobuf 定义、第三方提供的 SDK、生成的 GraphQL 类型。如果搜索结果中出现了这些文件,代理也会将其打开,但实际上不应该出现这种情况。

权限拒绝规则会阻止读取选定的文件模式。其大致结构如下,具体语法需参照您所使用的 Claude Code 版本的文档进行确认:

# packages/api/.claude/settings.json

{
  "permissions": {
    "deny": [
      "Read(./**/dist/**)",
      "Read(./**/build/**)",
      "Read(./**/*.generated.*)",
      "Read(./vendor/**)"
    ]
  }
}

当搜索“GraphQL resolvers”时若找到生成的架构文件,虽然会显示结果,但读取工具会返回权限被拒的响应,同时智能体通常会询问真实源文件的位置。需要注意的一个副作用是:同样的规则也会阻止你在聊天窗口中手动发起的读取操作。要求它打开src/schema.generated.ts时也会被拒绝,此时可暂时移除该规则。这样做的好处是能让生成的冗余内容不会出现在上下文中,从而引导智能体关注真正的源文件。

模式5:稀疏工作树

Claude Code能够创建git工作树以实现并行或隔离的工作,而默认情况下每个工作树都会检出整个仓库。在单体仓库中这种做法很浪费资源,因为修复API并不需要前端代码。

Git的稀疏检出功能仅将选定的目录写入磁盘,其余目录则保留在索引中,worktree.sparsePaths设置便是基于此机制实现的。请根据你所使用的版本文档核对该配置键的用法:

# packages/api/.claude/settings.json

{
  "worktree": {
    "sparsePaths": [
      ".claude",
      "packages/api",
      "packages/shared"
    ]
  }
}

工作树仅包含 .claude/、packages/api/ 和 packages/shared/,以及 package.json 和 tsconfig.base.json 等根目录文件。这类工作树创建更快,占用的磁盘空间也更小。如果智能体试图读取未被检出的文件,git 会抛出错误,此时智能体通常会询问有哪些目录存在。稀疏检出功能需要 git 2.25 或更高版本,可通过 git --version 查看版本信息。

模式 6:代码智能分析而非文件扫描

在庞大的代码库中查找某个函数的定义位置或使用场景需要多次执行 grep 操作并读取文件,因为智能体需要逐一扫描结果、打开候选文件并持续查找。

代码智能插件可将代理与语言服务器相连。它不会进行扫描,而是直接询问refreshAuthToken的定义位置,并立即获取对应的文件及行号。对于TypeScript项目,官方的LSP插件安装方式如下,不过不同版本的安装语法可能有所差异,建议查阅相关文档:

/plugin install typescript-lsp@claude-plugins-official

询问 refreshAuthToken 的使用位置后,代理会查询语言服务器并得到回复:

src/middleware/auth.ts:47
src/routes/users.ts:103
src/__tests__/auth.test.ts:22

由于无需读取任何文件,因此响应速度更快,且上下文保持整洁。该插件在未运行 tsc 的情况下也能检测出编辑后的类型错误,从而立即发现问题。它与拒绝规则配合得很好:拒绝规则可过滤掉无关内容,而代码智能功能则减少了对剩余内容的扫描需求。

六者结合使用

应用每种模式后,最初的缺陷修复大致如下:

  1. 从 packages/api/ 启动 Claude。
  2. 它会加载根目录下的 CLAUDE.md(40 行)以及 API 相关的文件(50 行)。
  3. 你要求它修复认证中间件中的会话超时问题。
  4. 它会派出一个子代理来分析认证流程,该子代理会在自己的上下文中读取大约 20 个文件。
  5. 子代理反馈称,超时逻辑位于 src/middleware/auth.ts 的第 47 行,且使用了错误的比较运算符。
  • 主开发人员读取该文件,修复错误,编写测试并运行它,总共涉及主项目中的大约四个文件。
  • 在这个示例中,处理前的情况是147次文件读取、3次失败尝试以及35分钟的时间,而处理后则是大约4次读取、1次成功尝试以及6分钟的时间。这只是一个开发人员的个人经历,并非基准数据,但关键在于其趋势:这种结构消除了问题与答案之间的障碍。

    选择所需功能

    并非每个项目都需要全部六种功能。一个仅有50个文件的仓库就不需要分层文件或稀疏工作树。一旦代码量超出一个人能够记忆的范围,这些功能就开始发挥价值:

    • 分层式的 CLAUDE.md 文件适用于在根文件中滚动跳过无关部分时,或当团队成员询问为何 API 文件中包含前端规则时。
    • 技能模块适用于 CLAUDE.md 中的某部分已变成流程或检查清单时,或当你不断将相同指令粘贴到聊天中时。
    • 子代理适用于调查任务要求“阅读这30个文件”时,或当你希望获得无偏见的审查结果时。
    • 拒绝规则适用于代理无视 .gitignore 文件仍不断打开生成的或第三方提供的代码时。
    • 稀疏工作树适用于单仓库检出操作需要超过10秒,或当你同时创建多个工作树时。
  • 代码智能插件适用于使用编程语言工作的场景,此时智能体花费在查找定义上的时间多于编写代码的时间。
  • 目标并非盲目应用所有方法,而是要察觉到上下文何时变得混乱,然后选择合适的模式来清理它。其他布局也适用于大型代码库,但这六种模式能让原本会阅读所有内容的智能体转变为只阅读相关文件、编写修复代码后就停止工作的智能体。