摘要
接手老项目时,AI 最容易给出“看起来很合理、实际上没法落地”的答案,根源通常不是模型不够强,而是项目上下文不完整。本文给出一套可直接复制的流程:先扫描目录树、过滤敏感内容、生成PROJECT_CONTEXT.md,再让 AI 分模块分析依赖、入口和排错路径。
正文
接手一个老项目,最让人头疼的往往不是某一行代码看不懂。
真正难的是:你不知道这个项目从哪里启动、核心逻辑藏在哪、某个接口经过了几层调用、数据库字段是谁写进去的、一个报错到底是配置问题还是业务问题。
很多人这时候会直接把几段代码、一个报错截图,或者整个目录结构扔给 AI,然后问:
这个项目怎么跑?
这个接口为什么 500?
这段代码能不能重构?
你帮我看一下核心业务逻辑。
AI 往往也会给出一大段看似完整的回答。
但你真正去验证时,常常会发现:入口文件判断错了、模块关系猜错了、依赖方向搞反了,甚至建议修改了根本不该动的文件。
这不是 AI “不会写代码”,而是它拿到的上下文不够完整。
Anthropic 当前的提示词最佳实践文档已将 Claude Sonnet 5 纳入覆盖模型范围,但模型能力再强,也不能替代开发者把项目边界、目录结构、运行方式和业务约束交代清楚。对老项目来说,先建立一份结构化上下文,比一上来让 AI 改代码重要得多。
这篇文章分享一套我更建议在真实项目里使用的办法:
先让 AI 看懂“项目地图”,再让它分析局部问题。
不要先让它猜。
一、老项目最容易犯的错:把“碎片代码”当成“项目上下文”
假设你接手了一个典型的旧项目:
legacy-api/ ├── src/ │ ├── controllers/ │ ├── services/ │ ├── repositories/ │ ├── middlewares/ │ ├── jobs/ │ └── utils/ ├── config/ ├── scripts/ ├── migrations/ ├── package.json ├── docker-compose.yml └── README.md你随手复制一个UserController、一个报错日志,再加一句:
帮我分析用户注册为什么失败。
问题在于,AI 不知道:
- 用户注册入口是 HTTP 接口、消息队列消费者,还是定时任务;
UserController有没有经过鉴权、中间件、参数校验;- 数据库操作是在
Repository、ORM 模型,还是远程 RPC 服务; - 项目是本地启动、Docker 启动,还是依赖某个内部配置;
- 这次报错是测试环境、开发环境,还是生产环境;
- 你是否允许它修改数据库结构、接口返回值或历史兼容逻辑。
没有这些信息,AI 只能根据常见项目结构猜。
猜得顺的时候,会让人觉得它很聪明;猜错的时候,返工比自己读代码还慢。
所以第一步不是问 AI:
“这个项目是干什么的?”
而是先给它一份可验证的事实清单。
二、我会先准备两份东西:目录树和项目说明书
第一份叫做:
PROJECT_CONTEXT.md它不是完整设计文档,也不是为了写得漂亮。
它的目标只有一个:
让 AI 第一次接触项目时,先知道“这个项目有哪些模块、哪些地方不能碰、当前问题发生在哪”。
这份文件至少要包含六类信息:
| 信息类别 | 作用 | 示例 |
|---|---|---|
| 项目结构 | 告诉 AI 文件和模块在哪里 | src/controllers、src/services |
| 技术栈 | 避免 AI 给错框架方案 | Node.js + TypeScript + MySQL |
| 启动方式 | 帮助定位入口与运行链路 | pnpm dev、docker compose up |
| 核心模块 | 明确各目录责任 | services负责业务编排 |
| 当前问题 | 让 AI 聚焦真实任务 | 注册接口偶发 500 |
| 约束边界 | 防止它随意改动 | 不改表结构、不改鉴权逻辑 |
第二份是自动生成的目录树。
为什么不建议手写?
因为老项目文件多、目录深,手写一遍容易漏,也容易过期。更现实的方式是用一个脚本生成基础结构,再由人补业务说明。
下面这段 Python 脚本只使用标准库,可以扫描项目目录、过滤常见依赖目录和敏感文件,并输出一份可作为 AI 上下文的 Markdown 文档。
—
三、可直接运行:生成PROJECT_CONTEXT.md的 Python 脚本
新建文件:
generate_project_context.py粘贴下面代码:
#!/usr/bin/env python3from__future__importannotationsimportargparsefromdatetimeimportdatetimefrompathlibimportPathfromtypingimportIterable IGNORED_DIRS={".git",".hg",".svn",".idea",".vscode",".vs",".venv","venv","env","node_modules","dist","build","coverage","__pycache__",".next",".nuxt","target",".turbo",".cache",}SENSITIVE_PREFIXES=(".env",)SENSITIVE_TOKENS=("secret","credential","private_key","id_rsa","keystore","apikey","api_key",)IMPORTANT_FILENAMES={"package.json","pnpm-lock.yaml","yarn.lock","package-lock.json","pyproject.toml","requirements.txt","poetry.lock","pipfile","pom.xml","build.gradle","settings.gradle","go.mod","cargo.toml","dockerfile","docker-compose.yml","docker-compose.yaml","readme.md","makefile","vite.config.ts","next.config.js",}defis_sensitive(name:str)->bool:lowered=name.lower()return(lowered.startswith(SENSITIVE_PREFIXES)orany(tokeninloweredfortokeninSENSITIVE_TOKENS))defdisplay_name(path:Path)->str:returnpath.name+("/"ifpath.is_dir()else"")defwalk_tree(root:Path,max_depth:int,max_files:int,)->tuple[list[str],list[Path],int]:lines=[f"{root.name}/"]files:list[Path]=[]skipped_sensitive=0stopped=Falsedefvisit(current:Path,prefix:str,depth:int)->None:nonlocalskipped_sensitive,stoppedifstoppedordepth>max_depth:returntry:children=sorted(current.iterdir(),key=lambdaitem:(notitem.is_dir(),item.name.lower()),)exceptPermissionError:lines.append(prefix+"└── [permission denied]")returnvisible_children:list[Path]=[]forchildinchildren:ifchild.is_dir()andchild.nameinIGNORED_DIRS:continueifis_sensitive(child.name):skipped_sensitive+=1continuevisible_children.append(child)forindex,childinenumerate(visible_children):iflen(files)>=max_files:lines.append(prefix+"└── ... [file limit reached]")stopped=Truereturnis_last=index==len(visible_children)-1branch="└── "ifis_lastelse"├── "lines.append(prefix+branch+display_name(child))ifchild.is_dir():next_prefix=prefix+(" "ifis_lastelse"│ ")ifdepth<max_depth:visit(child,next_prefix,depth+1)else:lines.append(next_prefix+"└── ...")else:files.append(child)visit(root,"",1)returnlines,files,skipped_sensitivedefimportant_files(files:Iterable[Path],root:Path)->list[str]:result:list[str]=[]forfile_pathinfiles:name=file_path.name.lower()ifnameinIMPORTANT_FILENAMESorname.endswith((".csproj",".sln")):result.append(file_path.relative_to(root).as_posix())returnresult[:30]defmain()->None:parser=argparse.ArgumentParser(description=("Generate a safe project-structure context file ""for AI-assisted code reading."))parser.add_argument("project_root",type=Path)parser.add_argument("--max-depth",type=int,default=4)parser.add_argument("--max-files",type=int,default=400)args=parser.parse_args()root=args.project_root.resolve()ifnotroot.is_dir():raiseSystemExit(f"Not a directory:{root}")tree_lines,files,skipped_sensitive=walk_tree(root=root,max_depth=args.max_depth,max_files=args.max_files,)key_files=important_files(files,root)print("# PROJECT_CONTEXT")print()print("## 1. Scan metadata")print(f"- Project root: `{root.name}`")print(f"- Generated at: "f"`{datetime.now().isoformat(timespec='seconds')}`")print(f"- Max depth: `{args.max_depth}`")print(f"- Visible files: `{len(files)}`")print(f"- Sensitive paths skipped: `{skipped_sensitive}`")print("- Ignored generated/dependency directories: "+", ".join(sorted(IGNORED_DIRS)))print()print("## 2. Directory tree")print("```text")print("\n".join(tree_lines))print("```")print()print("## 3. Important build and configuration files")ifkey_files:foriteminkey_files:print(f"- `{item}`")else:print("- No common build or configuration files were detected automatically.")print()print("## 4. Fill in before asking AI to analyze the project")print("- Business purpose:")print("- Runtime and dependency commands:")print("- Entry point / startup path:")print("- Core modules and their responsibilities:")print("- External dependencies (database, message queue, third-party APIs):")print("- Current problem and reproduction steps:")print("- Constraints: files AI must not modify, ""compatibility requirements, test expectations:")if__name__=="__main__":main()我已经按一个包含src/controllers、src/services、.env、node_modules的示例项目验证过这段脚本:它会跳过.env和node_modules,并正常输出目录树、关键配置文件与待补充说明。
运行方式:
python generate_project_context.py /你的项目路径 --max-depth4>PROJECT_CONTEXT.md例如:
python generate_project_context.py ./legacy-api --max-depth4>PROJECT_CONTEXT.md如果项目特别大,可以再限制文件数量:
python generate_project_context.py ./legacy-api\--max-depth4\--max-files300\>PROJECT_CONTEXT.md四、脚本输出大概长什么样
假设你的项目是:
legacy-api/ ├── src/ │ ├── controllers/ │ │ └── user.ts │ └── services/ │ └── user_service.ts ├── package.json ├── README.md ├── .env └── node_modules/生成结果会类似这样:
# PROJECT_CONTEXT ## 1. Scan metadata - Project root: `legacy-api` - Max depth: `4` - Visible files: `4` - Sensitive paths skipped: `1` ## 2. Directory tree ```text legacy-api/ ├── src/ │ ├── controllers/ │ │ └── user.ts │ └── services/ │ └── user_service.ts ├── package.json └── README.md3. Important build and configuration files
package.jsonREADME.md
4. Fill in before asking AI to analyze the project
- Business purpose:
- Runtime and dependency commands:
- Entry point / startup path:
- Core modules and their responsibilities:
- External dependencies:
- Current problem and reproduction steps:
- Constraints:
注意,这只是“机器生成的结构层”。 它告诉 AI:项目有什么。 但还没有告诉 AI:项目为什么这样设计。 所以脚本跑完后,我建议你至少补充下面这段内容。 --- ## 五、真正决定 AI 输出质量的,是这段人工补充说明 在 `PROJECT_CONTEXT.md` 最后加上: ```markdown ## 5. Manual project brief ### Business purpose 这是一个面向后台运营人员的用户管理系统。 主要能力包括:用户注册、用户登录、角色分配、订单查询。 ### Runtime - Node.js 20 - TypeScript - PostgreSQL - Redis - Docker Compose ### Startup - 本地开发:`pnpm dev` - 数据库启动:`docker compose up -d postgres redis` - 测试:`pnpm test` ### Core modules - `src/controllers`:HTTP 请求入口,负责参数接收与响应返回。 - `src/services`:业务编排层,处理注册、登录、权限判断。 - `src/repositories`:数据库访问层。 - `src/middlewares`:鉴权、请求日志、统一异常处理。 - `src/jobs`:定时任务和异步任务。 ### Current issue 用户注册接口偶发返回 500。 已知现象: 1. 只有部分邮箱地址会触发。 2. 数据库中没有生成用户记录。 3. 日志中出现 `duplicate key value`。 4. 不能修改现有数据库表结构。 ### Constraints - 不要读取 `.env`、生产配置、密钥文件。 - 不要修改登录和鉴权中间件。 - 优先给出排查路径,再给出修改建议。 - 所有修改建议必须附带测试点。这段内容比“把更多代码贴进去”更重要。
因为它给的是判断边界。
AI 最怕的不是代码多,而是信息没有优先级。
六、把项目上下文交给 AI 后,不要直接让它改代码
很多人生成完PROJECT_CONTEXT.md,下一步就会说:
现在帮我修复所有问题。
不建议。
更稳的步骤是分四轮。
第 1 轮:让 AI 复述项目结构
可以直接这样问:
下面是一个项目上下文文件。 请先不要修改代码,也不要给重构方案。 请只完成四件事: 1. 用不超过 300 字说明项目职责; 2. 列出主要启动入口和请求链路; 3. 按“控制层、业务层、数据层、基础设施层”归类目录; 4. 标出你无法根据现有信息确定的部分。 如果存在猜测,请明确标记为“待确认”。这一步的目的不是拿答案。
而是先检查 AI 有没有理解错方向。
如果它连项目启动方式、核心模块、依赖层次都判断错了,后面的改代码建议就没有必要看。
第 2 轮:让 AI 画出问题相关链路
接着再问:
当前问题是: 用户注册接口偶发返回 500。 日志中出现 duplicate key value。 数据库中没有生成用户记录。 不能修改现有数据库表结构。 请基于项目上下文,输出: 1. 从 HTTP 请求进入到数据库写入的可能调用链; 2. 最可能相关的文件路径; 3. 需要补充的日志位置; 4. 需要验证的数据库约束; 5. 不建议直接修改的文件或模块; 6. 一个按优先级排序的排查计划。 不要直接生成修复代码。这里最关键的一句是:
不要直接生成修复代码。
因为老项目排错最怕“先改再说”。
AI 给出的价值,应该先体现在缩小排查范围,而不是先替你做风险操作。
第 3 轮:让 AI 给“最小改动方案”
当你已经确认调用链、日志和数据约束后,再进入代码建议阶段。
目前已确认: 1. 注册请求会进入 `UserController.register`; 2. `UserService.createUser` 会先查询邮箱,再执行插入; 3. 数据库 email 字段存在唯一索引; 4. 并发请求可能同时通过“邮箱不存在”的检查; 5. 不允许修改表结构。 请给出最小改动方案。 要求: - 不改接口返回结构; - 不删除现有校验逻辑; - 优先通过事务、唯一约束异常捕获或幂等处理解决; - 给出修改涉及的文件; - 给出每个修改点的风险; - 给出对应的测试用例。这样提问后,AI 输出会更像技术方案,而不是一段“看上去能跑”的补丁。
第 4 轮:让 AI 写测试点,而不是只写代码
最后一步很容易被跳过。
但老项目最需要的,恰恰是测试点。
可以继续要求:
请基于上面的最小改动方案,补充测试清单: 1. 单用户正常注册; 2. 同邮箱连续注册; 3. 两个并发请求同时注册同邮箱; 4. 数据库插入异常; 5. Redis 不可用; 6. 邮件服务超时; 7. 旧接口调用方兼容性。 每个测试点请包含: - 前置条件; - 输入; - 预期结果; - 需要观察的日志; - 是否适合自动化测试。AI 很适合补全边界条件、帮助你想测试场景。
但上线前的人工 Code Review、自动化测试、日志验证和灰度策略,还是不能省。
七、一个可复用的老项目分析提示词模板
下面这段可以直接保存成:
legacy-project-analysis-prompt.md以后接手任何陌生项目都能复用。
你是一名协助理解老项目的开发助手。 下面我会提供项目上下文文件、问题描述和部分代码。 你的目标不是直接修改代码,而是先帮助我建立可验证的理解。 请严格遵守: 1. 不根据缺失信息补充业务事实; 2. 不读取或要求提供 .env、密钥、生产凭证、用户隐私数据; 3. 不建议直接修改数据库结构,除非我明确允许; 4. 所有推断都要标记“已确认”或“待确认”; 5. 先输出排查路径,再输出最小改动建议; 6. 修改建议必须包含风险点和测试点。 请按以下格式输出: ## 项目理解 - 项目职责: - 启动入口: - 核心模块: - 关键依赖: ## 当前问题链路 - 请求入口: - 涉及模块: - 可能的数据流: - 待确认信息: ## 排查计划 1. 2. 3. ## 最小改动建议 - 修改位置: - 修改原因: - 风险: - 回滚方案: ## 测试清单 - 正常路径: - 异常路径: - 并发场景: - 兼容性:这类模板看起来普通,但它能明显减少 AI “自由发挥”的空间。
八、不要把这些文件直接交给 AI
项目上下文越完整越好,不等于项目文件越多越好。
下面这些内容,默认不建议直接放进 AI 对话或外部工具:
| 文件或信息 | 原因 |
|---|---|
.env | 可能包含密钥、数据库账号、第三方 Token |
| 云服务配置 | 可能暴露访问地址和权限信息 |
| 生产数据库导出 | 可能涉及用户隐私和业务数据 |
| 支付、身份认证配置 | 可能涉及敏感凭证 |
| 内部接口密钥 | 可能导致权限泄露 |
| 完整生产日志 | 可能包含手机号、邮箱、订单号、用户标识 |
Claude Code 的官方安全建议也明确提到:处理敏感代码时,应审查所有建议修改、为敏感仓库设置项目级权限,并考虑用开发容器做额外隔离。其设置文档还给出了通过permissions.deny阻止读取.env、secrets、凭证文件和构建目录的方式。
如果你使用 Claude Code,也可以在项目里加一份最基础的限制配置:
{"permissions":{"deny":["Read(./.env)","Read(./.env.*)","Read(./secrets/**)","Read(./config/credentials.json)","Read(./build)"]}}这不是万能防护,但至少能让“默认不该读的文件”先被挡在外面。
九、这套流程适合什么项目,不适合什么项目
适合:
- 新人接手老系统;
- 接手外包项目;
- 重构前做代码摸底;
- 排查跨模块 Bug;
- 给 AI 准备稳定上下文;
- 写项目 README、接口文档、模块说明;
- 做代码 Review 前的第一轮分析。
不适合直接拿来做:
- 直接把 AI 输出当作生产修改;
- 未脱敏的生产日志分析;
- 涉及支付、身份认证、权限系统的无审查改动;
- 大规模数据库迁移;
- 核心安全逻辑重构;
- 高并发、分布式一致性问题的最终决策。
AI 可以帮你缩小范围、梳理链路、补测试点。
但它不能替你承担线上风险。
十、最后总结
老项目不是“代码多”才难读。
而是它的上下文散在目录、配置、历史约定、业务流程和人的记忆里。
所以我更推荐这条顺序:
先扫描目录 → 再过滤敏感内容 → 再生成项目上下文 → 再补人工业务说明 → 再让 AI 复述项目理解 → 再分析调用链 → 最后才讨论修改方案当你把 AI 当成“先帮我建立项目地图的搭档”,而不是“直接帮我改代码的人”,它给出的建议通常会更稳定,也更容易验证。