现在很多人用 AI 写代码,一上来就问:
帮我实现一个用户管理页面
AI 很快就能生成一堆代码。
问题是,它不知道你的项目结构,不知道你用的是 React 还是 Vue,不知道接口目录在哪,不知道组件风格是什么,不知道路由怎么组织,也不知道项目里哪些坑不能碰。
于是它开始猜。
猜目录。
猜技术栈。
猜组件写法。
猜状态管理。
猜接口封装。
猜样式方案。
最后生成的代码看起来很勤奋,合进去一跑,项目像被陌生人装修过一样别扭。
AI 编程最容易被忽略的一点是:模型不是只靠一句需求工作,它更依赖上下文。
所以这篇文章做一个小工具:
用 Node.js 自动扫描项目,生成一份 project-context.md,让 Cursor、Kiro、Claude、ChatGPT 在写代码前先理解项目。
它不调用任何 AI API,也不上传代码,只在本地生成 Markdown 文件。你可以先人工检查,再决定复制哪些内容给 AI。
这一步看起来不酷,但很有用。软件工程里很多有用的东西都不酷,比如日志、测试、文档和不把 .env 提交上去。可惜人类经常只在事故之后才想起它们。
一、为什么 AI 写代码前需要项目上下文?
AI 编程工具确实越来越强,但它们不是你项目里的老员工。
它不知道:
- 项目用什么技术栈;
- 路由文件放在哪里;
- API 请求如何封装;
- 组件命名规则是什么;
- 状态管理用什么;
- 样式是 CSS Modules、Tailwind 还是 Sass;
- 哪些目录是生成文件;
- 哪些代码是历史包袱;
- 哪些接口返回结构不能改;
- 哪些文件不能随便动。
如果你不给上下文,它只能从你当前贴的代码里猜。
这会带来几个问题:
1. 生成代码不符合项目风格
比如项目里组件都用函数式写法,它突然给你生成一套奇怪的类组件。
项目里接口都走统一 request 封装,它自己写一个 fetch。
项目里状态管理已经统一了,它又 invent 一个小型状态宇宙。
这类代码不一定不能跑,但合进去很难看。
2. 改动范围容易失控
你只是想加一个按钮,它顺手帮你重构半个页面。
你只是想补一个 loading,它把接口层、组件层、类型定义全改了。
AI 很热心。热心到有时候像拿着电锯帮你修指甲。
3. Review 成本变高
如果 AI 生成的代码不贴合项目结构,开发者还要花大量时间改回项目原来的风格。
这时候所谓"提效",就变成了"先快后慢"。
二、我们要做什么?
目标很简单:生成一份 AI 可读的项目上下文文档。
最终输出:
project-context.md
里面包含:
项目基本信息
技术栈和依赖
运行脚本
目录结构
关键文件内容
给 AI 的使用建议
安全提醒
然后你可以这样用:
这是当前项目上下文,请先阅读,不要直接写代码。
后续我会给你具体需求,请严格按照项目结构、技术栈和已有风格生成代码。
这比直接甩一句"帮我写功能"靠谱很多。
三、项目目录示例
假设你的项目是一个前端项目:
css
my-web-app/
├── src/
│ ├── api/
│ ├── components/
│ ├── pages/
│ ├── router/
│ └── main.tsx
├── package.json
├── vite.config.ts
├── tsconfig.json
└── tools/
我们在 tools 下新建脚本:
bash
mkdir -p tools
touch tools/gen-ai-context.mjs
四、完整 Node.js 脚本
把下面代码写入 tools/gen-ai-context.mjs:
ini
import {
existsSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from 'node:fs';
import { extname, join, relative } from 'node:path';
const rootDir = process.cwd();
const args = new Map(
process.argv.slice(2).map((item) => {
const [key, value = ''] = item.split('=');
return [key, value];
})
);
const outputFile = args.get('--out') || 'project-context.md';
const maxFiles = Number(args.get('--maxFiles') || 80);
const maxFileChars = Number(args.get('--maxFileChars') || 5000);
const maxTotalChars = Number(args.get('--maxTotalChars') || 50000);
const includeExtensions = new Set([
'.js',
'.jsx',
'.ts',
'.tsx',
'.vue',
'.svelte',
'.json',
'.md',
'.css',
'.scss',
'.less',
]);
const importantFiles = new Set([
'package.json',
'vite.config.js',
'vite.config.ts',
'webpack.config.js',
'next.config.js',
'next.config.mjs',
'nuxt.config.ts',
'tsconfig.json',
'README.md',
]);
const excludeDirs = new Set([
'node_modules',
'.git',
'dist',
'build',
'coverage',
'.next',
'.nuxt',
'.output',
'.turbo',
'.cache',
]);
const excludeFiles = new Set([
'package-lock.json',
'pnpm-lock.yaml',
'yarn.lock',
'.env',
'.env.local',
'.env.production',
'.env.development',
]);
const sensitivePatterns = [
/AKIA[0-9A-Z]{16}/,
/AIza[0-9A-Za-z_-]{35}/,
/sk-[A-Za-z0-9_-]{20,}/,
/(password|passwd|pwd|secret|token|api[_-]?key)\s*[:=]\s*['"][^'"]{8,}['"]/i,
];
function toPosixPath(filePath) {
return filePath.replace(/\/g, '/');
}
function isExcludedDir(name) {
return excludeDirs.has(name);
}
function isExcludedFile(name) {
return excludeFiles.has(name);
}
function shouldIncludeFile(filePath) {
const relativePath = toPosixPath(relative(rootDir, filePath));
const fileName = relativePath.split('/').at(-1);
if (isExcludedFile(fileName)) {
return false;
}
if (importantFiles.has(fileName)) {
return true;
}
return includeExtensions.has(extname(fileName));
}
function walk(dir, result = []) {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
if (!isExcludedDir(entry.name)) {
walk(fullPath, result);
}
continue;
}
if (entry.isFile() && shouldIncludeFile(fullPath)) {
result.push(fullPath);
}
}
return result;
}
function readJson(filePath) {
if (!existsSync(filePath)) {
return null;
}
try {
return JSON.parse(readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
function readTextFile(filePath) {
try {
return readFileSync(filePath, 'utf8');
} catch {
return '';
}
}
function truncate(text, maxChars) {
if (text.length <= maxChars) {
return text;
}
return `${text.slice(0, maxChars)}\n\n[内容过长,已截断]`;
}
function detectSensitiveText(text) {
return sensitivePatterns.some((pattern) => pattern.test(text));
}
function buildTree(files) {
return files
.map((file) => `- ${toPosixPath(relative(rootDir, file))}`)
.join('\n');
}
function collectPackageInfo() {
const packagePath = join(rootDir, 'package.json');
const packageJson = readJson(packagePath);
if (!packageJson) {
return '未检测到 package.json 或 JSON 格式异常。';
}
const scripts = packageJson.scripts || {};
const dependencies = packageJson.dependencies || {};
const devDependencies = packageJson.devDependencies || {};
const scriptText = Object.keys(scripts).length
? Object.entries(scripts).map(([key, value]) => `- ${key}: ${value}`).join('\n')
: '- 未配置 scripts';
const deps = [
...Object.keys(dependencies),
...Object.keys(devDependencies),
].sort();
const depsText = deps.length
? deps.slice(0, 80).map((name) => `- ${name}`).join('\n')
: '- 未检测到依赖';
return `## package.json 摘要
项目名称:${packageJson.name || '未命名项目'}
### scripts
${scriptText}
### dependencies / devDependencies
${depsText}`;
}
function collectFileSections(files) {
const sections = [];
let totalChars = 0;
for (const file of files.slice(0, maxFiles)) {
const relativePath = toPosixPath(relative(rootDir, file));
const content = readTextFile(file);
if (!content.trim()) {
continue;
}
if (detectSensitiveText(content)) {
sections.push(`## ${relativePath}
[跳过] 该文件疑似包含 token、secret、password、api key 等敏感信息,请人工确认后再决定是否提供给 AI。`);
continue;
}
const safeContent = truncate(content, maxFileChars);
const ext = extname(file).replace('.', '') || 'text';
const section = `## ${relativePath}
```${ext}
${safeContent}
````;
totalChars += section.length;
if (totalChars > maxTotalChars) {
sections.push(`
[提示] 上下文内容超过 ${maxTotalChars} 字符,后续文件已省略。建议缩小扫描范围或分模块生成上下文。`);
break;
}
sections.push(section);
}
return sections.join('\n\n');
}
function getProjectType(packageInfo) {
const lower = packageInfo.toLowerCase();
if (lower.includes('vite')) return '可能是 Vite 项目';
if (lower.includes('next')) return '可能是 Next.js 项目';
if (lower.includes('nuxt')) return '可能是 Nuxt 项目';
if (lower.includes('vue')) return '可能是 Vue 项目';
if (lower.includes('react')) return '可能是 React 项目';
return '未自动识别,请人工补充项目类型';
}
function main() {
const allFiles = walk(rootDir);
const selectedFiles = allFiles.filter((file) => {
const relativePath = toPosixPath(relative(rootDir, file));
if (relativePath.startsWith('tools/gen-ai-context')) {
return false;
}
return true;
});
const packageInfo = collectPackageInfo();
const projectType = getProjectType(packageInfo);
const tree = buildTree(selectedFiles.slice(0, maxFiles));
const fileSections = collectFileSections(selectedFiles);
const markdown = `# AI 项目上下文
> 这份文档由 tools/gen-ai-context.mjs 自动生成。
> 使用前请人工检查是否包含敏感信息,不要把密码、Token、密钥、公司机密、用户隐私直接提供给外部 AI 工具。
## 项目识别
${projectType}
${packageInfo}
## 目录结构摘要
${tree || '未检测到可用文件。'}
## 给 AI 的使用要求
请先阅读项目上下文,再根据我的具体需求输出代码。
要求:
1. 不要擅自改变项目技术栈;
2. 不要新增不必要的依赖;
3. 优先复用已有目录结构和代码风格;
4. 修改前先说明会影响哪些文件;
5. 涉及接口、鉴权、环境变量时必须提醒我人工确认;
6. 输出代码时说明放在哪个文件;
7. 如果上下文不足,请先提问,不要直接猜。
## 关键文件内容
${fileSections || '未收集到关键文件内容。'}
`;
writeFileSync(outputFile, markdown, 'utf8');
console.log(`AI 项目上下文已生成:${outputFile}`);
console.log(`收集文件数:${selectedFiles.length}`);
}
main();
五、配置 package.json 脚本
在 package.json 里加一条命令:
json
{
"scripts": {
"ai:context": "node tools/gen-ai-context.mjs"
}
}
然后执行:
arduino
npm run ai:context
或者 pnpm 项目执行:
pnpm ai:context
生成:
project-context.md
如果想控制输出文件名:
ini
node tools/gen-ai-context.mjs --out=docs-ai-context.md
如果项目比较大,可以限制文件数量:
css
node tools/gen-ai-context.mjs --maxFiles=40 --maxTotalChars=30000
六、生成的上下文长什么样?
生成后的 project-context.md 大概是这样:
markdown
# AI 项目上下文
## 项目识别
可能是 Vite 项目
## package.json 摘要
项目名称:my-web-app
### scripts
- dev: vite
- build: vite build
- preview: vite preview
- lint: eslint .
### dependencies / devDependencies
- @vitejs/plugin-react
- typescript
- vite
- react
- react-dom
## 目录结构摘要
- src/api/user.ts
- src/components/UserCard.tsx
- src/pages/UserPage.tsx
- src/router/index.ts
- vite.config.ts
- tsconfig.json
## 给 AI 的使用要求
1. 不要擅自改变项目技术栈;
2. 不要新增不必要的依赖;
3. 优先复用已有目录结构和代码风格;
...
这个文档的价值在于:
它不是让 AI "看完整个项目",而是先给 AI 一个项目地图。
AI 有了地图,才不至于每次都像新员工第一天入职,连厕所在哪都要靠猜。
七、怎么配合 Cursor、Kiro、Claude、ChatGPT 使用?
生成文档后,不建议直接全量丢给 AI 然后让它随便发挥。
更好的用法是:
这是项目上下文,请先阅读,不要写代码。
阅读后请用 5 点总结你理解的项目结构、技术栈和开发约束。
如果你发现上下文不足,请先问我。
等 AI 总结完,再给具体需求:
markdown
基于上面的项目上下文,请帮我新增一个用户详情页。
要求:
1. 复用现有 api/request 封装;
2. 页面放在 src/pages/UserDetail.tsx;
3. 组件风格参考 src/components/UserCard.tsx;
4. 补充 loading、empty、error 状态;
5. 不要新增依赖;
6. 输出需要修改的文件列表。
这样 AI 的回答会稳定很多。
尤其是 Cursor 和 Kiro 这类 AI 编程工具,本身就适合结合项目上下文做开发辅助。你给它一份清晰的 project-context.md,比直接让它在项目里自己乱翻要稳。
Claude 和 ChatGPT 也一样。
它们擅长理解长文本,但前提是你给的上下文是整理过的,而不是一堆文件碎片。
八、为什么要过滤 node_modules、dist、lock 文件?
脚本里过滤了这些目录和文件:
ini
const excludeDirs = new Set([
'node_modules',
'.git',
'dist',
'build',
'coverage',
'.next',
'.nuxt',
'.output',
'.turbo',
'.cache',
]);
const excludeFiles = new Set([
'package-lock.json',
'pnpm-lock.yaml',
'yarn.lock',
'.env',
'.env.local',
'.env.production',
'.env.development',
]);
原因很简单:
node_modules没必要给 AI;dist、build是构建产物;- lock 文件太长,价值低;
.env可能包含敏感信息;.git不是本次任务重点;- coverage、cache 都是噪音。
AI 上下文不是越多越好。
给太多无关内容,模型反而更容易分心。
这就像开会时把全公司群聊记录都打印出来,指望大家更快理解需求。不能说完全没用,只能说很像折磨。
九、为什么要扫描敏感信息?
脚本里做了一层很粗的敏感信息检测:
css
const sensitivePatterns = [ /AKIA[0-9A-Z]{16}/,
/AIza[0-9A-Za-z_-]{35}/,
/sk-[A-Za-z0-9_-]{20,}/,
/(password|passwd|pwd|secret|token|api[_-]?key)\s*[:=]\s*['"][^'"]{8,}['"]/i,
];
如果文件里出现类似 token、secret、password、apiKey 的内容,它会跳过并提醒人工确认。
这不是安全审计。
它只是一个低成本兜底。
真正使用 AI 编程工具时,仍然要注意:
- 不要上传
.env; - 不要上传真实密钥;
- 不要上传用户隐私;
- 不要上传公司内部敏感业务数据;
- 不要把生产接口、鉴权信息直接暴露给外部工具;
- 提供上下文前先人工检查。
安全这事不能靠"我觉得应该没事"。
"应该没事"在工程里通常是事故预告片。
十、适合生成哪些文件?
不是所有源码都应该放进上下文。
比较适合放的文件:
bash
package.json
vite.config.ts
tsconfig.json
README.md
src/main.tsx
src/router/index.ts
src/api/request.ts
src/api/*.ts
src/components/*.tsx
src/pages/*.tsx
src/stores/*.ts
不建议放:
bash
.env
.env.local
大体积 mock 数据
构建产物
日志文件
图片资源
第三方库源码
包含密钥的配置
包含用户隐私的数据
上下文的目标不是"把项目搬给 AI"。
目标是让 AI 理解:
- 这个项目是什么;
- 用什么技术栈;
- 代码怎么组织;
- 常见写法是什么;
- 哪些规则不能破坏;
- 本次需求应该改哪些地方。
这才是有效上下文。
十一、可以再加一个 focus 模式
如果项目很大,你可能不想扫描整个项目。
可以扩展一个 --focus 参数,只扫描某个目录。
比如:
bash
node tools/gen-ai-context.mjs --focus=src/pages
下面是一个简单改法。
先读取参数:
ini
const focusDir = args.get('--focus') || '';
然后把扫描入口改成:
scss
const scanRoot = focusDir ? join(rootDir, focusDir) : rootDir;
if (!existsSync(scanRoot) || !statSync(scanRoot).isDirectory()) {
console.error(`扫描目录不存在:${scanRoot}`);
process.exit(1);
}
const allFiles = walk(scanRoot);
这样你就可以针对某个模块生成上下文。
比如只让 AI 看用户模块:
css
node tools/gen-ai-context.mjs --focus=src/pages/user --out=user-context.md
这在大型项目里很实用。
AI 不需要一上来理解整个系统。
先让它理解当前模块,效果反而更好。
给上下文也要讲边界,不然 AI 和人一样,会在信息过载里开始胡说八道。
十二、加一个 .aiignore 会更好
如果你想更灵活,可以模仿 .gitignore 做一个 .aiignore。
比如:
bash
src/mock/
src/legacy/
docs/private/
*.log
*.local.ts
然后脚本读取 .aiignore,跳过这些路径。
最小实现可以先做简单字符串匹配:
scss
function loadAiIgnore() {
const ignorePath = join(rootDir, '.aiignore');
if (!existsSync(ignorePath)) {
return [];
}
return readFileSync(ignorePath, 'utf8')
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#'));
}
const aiIgnoreRules = loadAiIgnore();
function matchAiIgnore(relativePath) {
return aiIgnoreRules.some((rule) => relativePath.includes(rule));
}
然后在 shouldIncludeFile 里加:
kotlin
if (matchAiIgnore(relativePath)) {
return false;
}
这样团队可以自己控制哪些内容不提供给 AI。
这一步很适合团队项目。
毕竟每个项目都有一些"不要让外人看见"的历史遗迹。
有些是敏感信息,有些是代码质量,有些两者都是。惨得很完整。
十三、团队里怎么落地?
个人项目可以直接用:
pnpm ai:context
团队项目建议这样落地:
1. 放到 tools 目录
bash
tools/gen-ai-context.mjs
2. package.json 加脚本
json
{
"scripts": {
"ai:context": "node tools/gen-ai-context.mjs"
}
}
3. README 加说明
perl
## AI 编程上下文生成
执行:
```bash
pnpm ai:context
使用前请人工检查敏感信息,不要上传 .env、Token、用户隐私和公司机密。
ini
### 4. PR 模板里加检查项
```markdown
## AI 辅助开发确认
- [ ] 已确认 AI 生成代码符合项目结构
- [ ] 已检查没有上传敏感信息
- [ ] 已人工 Review AI 修改
- [ ] 已补充必要测试
这样团队不会只停留在"大家用 AI 注意点"这种口头文明阶段。
口头提醒在工程里很脆弱。
真正能留下来的,是脚本、文档、检查项和流程。
十四、这套方案的局限
这个脚本不是万能的。
它解决的是"给 AI 一个项目上下文入口",不是让 AI 完全理解你的系统。
局限包括:
- 它不能理解业务规则;
- 它不能判断所有安全风险;
- 它不能替代代码 Review;
- 它不能替代测试;
- 它不适合直接输出公司敏感代码;
- 项目太大时仍然需要分模块生成;
- AI 读完上下文后仍然可能理解错误。
所以你需要把它当成辅助工具,而不是自动驾驶。
更好的用法是:
让 AI 先总结它理解的项目结构
↓
你确认它有没有理解错
↓
再给具体需求
↓
让它输出改动方案
↓
你人工 Review
↓
再决定是否改代码
AI 编程不是"我提一句需求,它替我完成一切"。
那叫许愿。
工程不是许愿池,虽然很多需求文档写得确实像。
十五、总结
这篇文章用 Node.js 做了一个本地脚本:
go
扫描项目文件
↓
过滤无关目录
↓
跳过敏感配置
↓
提取 package.json
↓
生成目录结构
↓
汇总关键文件
↓
输出 project-context.md
它的核心价值不是复杂,而是稳定。
当你给 Cursor、Kiro、Claude、ChatGPT 提供一个清晰的项目上下文,AI 才更容易生成贴合项目风格的代码。
AI 编程工具真正需要的,不只是更强模型。
还需要你给它更好的上下文、更清楚的边界、更明确的规则。
否则它写得越快,你返工也越快。
如果你长期使用 ChatGPT Plus、Claude Pro、Cursor、Kiro、Gemini Advanced、Grok 这类 AI 工具,也可以把 gpt68.com 作为第三方 AI 会员充值平台入口之一去了解。它解决的是订阅充值流程问题,不是替代工具本身。使用前建议看清楚套餐说明、账号要求、到账说明和售后规则。
工具负责辅助,开发者负责判断。
这个分工虽然冷酷,但至少不会骗你。