我做过一次统计,在我过去一年的 Claude Code 使用记录里,凡是出了问题的对话,80% 发生在跨文件操作上。

单文件让它写个函数,几乎不会出错。一旦涉及多个文件同时改动——Schema 改了、API 要跟着改、前端类型要同步、测试要更新——Claude 开始顾此失彼,改了 A 忘了 B,或者 A 和 B 改完发现逻辑对不上。

这不是 Claude 变笨了,是我们用错了姿势。

跨文件重构为什么这么容易翻车

我当时傻乎乎地以为,把所有相关文件都 Add to Context,然后一次性告诉 Claude "把这些文件都改一下",它就会统筹协调,改得整整齐齐。

结果是:它确实都改了,但改完之后,几个文件之间的接口对不上——API 返回的字段名和前端用的字段名不一致,Prisma Schema 加了新字段但迁移脚本没生成,类型定义文件没有同步更新。

每次都要花大量时间手动排查哪里对不上。

问题出在哪里?Claude 的多文件修改是并行的,不是串行的。 它会同时"考虑"所有文件,但不会真正追踪修改在文件间的传递链。

换句话说,它知道每个文件应该改成什么样,但不会在改完 A 之后,用 A 的结果去推导 B 应该怎么改。这个"传递性"需要你来控制。

核心原则:把并行改动变成串行流水线

跨文件重构的正确思路,是把改动顺序设计成一条单向流水线——每个步骤的输出,是下一个步骤的输入。

出海 Web 项目里,这条流水线通常长这样:

数据库 Schema
    ↓
Prisma 迁移脚本
    ↓
后端 API 逻辑
    ↓
API 类型定义(TypeScript)
    ↓
前端调用层
    ↓
前端组件
    ↓
i18n 文案(多语言)
    ↓
测试文件

改动从哪里开始,就从哪里进流水线,往后推,不往前改。

这个顺序不是我拍脑袋定的,是依赖关系决定的——后面的文件依赖前面的文件,反过来不行。你先改前端再改 Schema,必然要返工。

实战:一次完整的跨文件重构演示

场景:出海 SaaS 产品,要给用户表加一个 timezone 字段,存储用户的时区偏好,影响到 API、类型、前端设置页、多语言文案。

涉及文件:schema.prismamigrations/app/api/users/route.tstypes/user.tsapp/settings/page.tsxmessages/en.jsonmessages/zh.json__tests__/api/users.test.ts

一共 8 个文件。错误做法是把它们全塞进上下文一次改完。正确做法是走流水线。

第一步:改 Schema,让 Claude 生成迁移脚本

只打开 prisma/schema.prismaAdd to Context,然后:

在 User 模型里加一个 timezone 字段,类型 String,默认值 "UTC",可为空(用户可以不设置)。加完之后帮我生成 Prisma 迁移命令。

Claude 会改 Schema 并给你迁移命令:

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  timezone  String?  @default("UTC")
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
npx prisma migrate dev --name add_user_timezone

说人话就是: 第一步只做一件事——确定数据结构。跑完迁移,数据库层尘埃落定,后面的所有改动都以这个 Schema 为基准。

这里有个细节要注意: timezone 设置为可空(String?)而不是必填,是因为已有用户没有这个字段,迁移时不会报错。如果设成必填又没有默认值,迁移会失败。

第二步:改 API,把新字段加入接口

新 Session。打开 app/api/users/route.ts,同时把刚才的 schema.prismaAdd to Context(让 Claude 知道 Schema 的最新状态):

参考已加载的 Prisma Schema,在用户 API 里做两件事:

  1. GET 接口的返回值加上 timezone 字段

  2. PATCH 接口支持更新 timezone,更新前用 Zod 验证格式(必须是有效的 IANA 时区字符串,比如 "America/New_York")

Claude 会生成带验证的 API 代码:

import { z } from "zod"
import { prisma } from "@/lib/prisma"
import { NextRequest, NextResponse } from "next/server"

// 验证时区格式的 schema
const updateUserSchema = z.object({
  timezone: z.string().refine(
    (tz) => {
      try {
        Intl.DateTimeFormat(undefined, { timeZone: tz })
        return true
      } catch {
        return false
      }
    },
    { message: "Invalid IANA timezone string" }
  ).optional(),
})

export async function PATCH(req: NextRequest) {
  try {
    const body = await req.json()
    const validated = updateUserSchema.parse(body)
    
    const user = await prisma.user.update({
      where: { id: req.headers.get("x-user-id")! },
      data: validated,
      select: { id: true, email: true, timezone: true, updatedAt: true },
    })
    
    return NextResponse.json({ success: true, data: user })
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { success: false, error: error.errors[0].message },
        { status: 400 }
      )
    }
    return NextResponse.json(
      { success: false, error: "Internal server error" },
      { status: 500 }
    )
  }
}

这里有个细节要注意: 时区验证用 Intl.DateTimeFormat 而不是正则,因为有效的 IANA 时区字符串列表很长,正则写不完,直接用浏览器/Node 原生 API 验证才是正道。

第三步:同步类型定义

新 Session。打开 types/user.ts,把 API 路由文件也 Add to Context

参考已加载的 API 路由文件,更新 User 相关的 TypeScript 类型定义,加上 timezone 字段,类型为 string | null

这一步通常很快,Claude 只需要加一行类型,但必须做——否则前端用这个类型的地方会报 TypeScript 错误。

第四步:改前端设置页

新 Session。打开设置页组件,把 types/user.ts 和 API 路由一起 Add to Context

参考已加载的类型定义和 API 路由,在用户设置页面加一个时区选择器:

  • 用原生 Intl.supportedValuesOf("timeZone") 获取时区列表

  • 当前值从用户数据里读取,默认显示 UTC

  • 选择后调用 PATCH 接口更新,成功后显示 Toast 提示

  • 不要引入额外的时区库,用浏览器原生 API

说人话就是: 把前两步的产物(类型 + API)喂给 Claude,让它知道数据从哪来、往哪发,不会自己发明字段名或接口路径。

第五步:多语言文案

新 Session。打开 messages/en.jsonmessages/zh.json,都 Add to Context

为时区设置功能,在两个语言文件里同时添加以下 key:

  • settings.timezone.label:时区

  • settings.timezone.placeholder:选择时区

  • settings.timezone.saved:时区已保存

  • settings.timezone.error:时区格式无效 英文要自然,中文要简洁

大多数教程不告诉你的细节: 把两个语言文件同时加进上下文,Claude 会保持 key 一致、结构一致,不会出现中英文 key 不对齐的情况。如果分开操作,很容易漏掉某个 key 或者拼写不一致。

第六步:更新测试

新 Session。打开测试文件,把 API 路由和类型定义 Add to Context

参考已加载的 API 路由,为 PATCH /api/users 接口补充以下测试用例:

  1. 更新有效时区("America/New_York")→ 应返回 200 和更新后的用户数据

  2. 更新无效时区("Invalid/Zone")→ 应返回 400 和错误信息

  3. 不传 timezone 字段 → 不报错,其他字段正常更新

跨文件重构 SOP(可直接复用)

把上面的流程抽象成可复用的标准作业流程:

跨文件重构标准流程

准备阶段
  1. 画出改动影响的文件列表
  2. 按依赖关系排序(被依赖的在前)
  3. 确认每个文件需要改什么

执行阶段(每个文件一个 Session)
  ┌─────────────────────────────────────┐
  │ 新 Session                          │
  │  → Add to Context:当前文件         │
  │  → Add to Context:上一步的输出文件 │
  │  → 写 Prompt:明确说改什么          │
  │  → Show Diff:确认改动范围          │
  │  → 接受 → 运行验证(编译/测试)     │
  └─────────────────────────────────────┘
  重复以上步骤,直到最后一个文件

收尾阶段
  1. 全量跑一次测试:npm test
  2. 类型检查:npx tsc --noEmit
  3. 生成改动摘要,存入 .claude/decisions/

踩坑环节

坑一:让 Claude 一次改太多文件,结果各处不一致

那次我要把项目里所有 API 接口的错误返回格式统一(从 { error: string } 改成 { success: false, error: string, code: string }),涉及大概 12 个路由文件。

我偷懒,把 12 个文件全加进上下文,让 Claude "把所有接口的错误格式统一改了"。

它改是改了,但我后来发现:有 3 个文件里的 code 字段用的是不同的命名规范——有的是 "USER_NOT_FOUND",有的是 "user_not_found",有的是 404。因为上下文里文件太多,Claude 在不同文件里"参考"了不同的风格,自己没有统一。

我发现的方式: 前端联调的时候,错误处理逻辑走了好几个不同分支,追源头才发现 code 格式不一致。

怎么解决的: 重新来过。先写一个标准错误定义文件 lib/errors.ts,定义所有 error code 的枚举,然后让每个路由文件 Add to Context 这个定义文件,逐个改。有了"锚点"文件,Claude 每次都参考同一个标准,不会自己发挥。

坑二:忘记在每个 Session 里带入上一步的结果

改完 Schema 之后开新 Session 改 API,忘了把 Schema 文件 Add to Context。Claude 凭"记忆"写的 API 里,用的字段名是 userTimezone,但 Schema 里定义的是 timezone

两边字段名不一样,Prisma 查询直接报错。

我发现的方式: 本地跑开发服务器,PATCH 接口 500,查日志才发现 Prisma 说 userTimezone 字段不存在。

怎么解决的: 从那之后,每个新 Session 开始前,我会养成习惯:上一步改了什么文件,这一步就把那个文件 Add to Context。不管觉得 Claude 应该记得还是不记得,一律手动加,加了没坏处,不加可能翻车。

跨文件重构不是"一次性"的任务,而是"接力赛"。 每一棒交接的时候,你要把棒子(上一步的结果)亲手递给下一棒(Claude 的新 Session),不能靠它自己去找。

你来负责信息的传递,它来负责单步的执行。分工清楚,就不会乱。


你做过最大规模的跨文件重构是什么?涉及几个文件,最后有没有翻车,怎么收场的?

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐