别让 AI 瞎读项目:用 Node.js 生成项目上下文

现在很多人用 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;
  • distbuild 是构建产物;
  • 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

生成 project-context.md

使用前请人工检查敏感信息,不要上传 .env、Token、用户隐私和公司机密。

ini 复制代码
### 4. PR 模板里加检查项

```markdown
## AI 辅助开发确认

- [ ] 已确认 AI 生成代码符合项目结构
- [ ] 已检查没有上传敏感信息
- [ ] 已人工 Review AI 修改
- [ ] 已补充必要测试

这样团队不会只停留在"大家用 AI 注意点"这种口头文明阶段。

口头提醒在工程里很脆弱。

真正能留下来的,是脚本、文档、检查项和流程。


十四、这套方案的局限

这个脚本不是万能的。

它解决的是"给 AI 一个项目上下文入口",不是让 AI 完全理解你的系统。

局限包括:

  1. 它不能理解业务规则;
  2. 它不能判断所有安全风险;
  3. 它不能替代代码 Review;
  4. 它不能替代测试;
  5. 它不适合直接输出公司敏感代码;
  6. 项目太大时仍然需要分模块生成;
  7. 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 会员充值平台入口之一去了解。它解决的是订阅充值流程问题,不是替代工具本身。使用前建议看清楚套餐说明、账号要求、到账说明和售后规则。

工具负责辅助,开发者负责判断。

这个分工虽然冷酷,但至少不会骗你。

相关推荐
水如烟1 小时前
孤能子视角:摩尔定律、韬定律 vs “摩尔制造“、“韬部署”?
人工智能
jerryinwuhan1 小时前
路径规划相关论文
人工智能
断春风1 小时前
Gemini 2.5 Flash Lite 高效落地实战指南
人工智能·ai编程
hai3152475431 小时前
九章编程法 · HTTP转发代理网关【终极完美版·矩阵步进交换】
人工智能·网络协议·线性代数·http·矩阵·极限编程
bryant_meng1 小时前
【Reading Notes】(10.4)Favorite Articles from 2026 April
人工智能·大模型·行业资讯·vibe coding
ZFSS2 小时前
VS Code + Hailuo MCP 使用指南
人工智能·ai·copilot·ai编程·ai写作
蜀道山老天师2 小时前
OpenClaw Skills 技能开发 + 企业运维全场景实战(进阶篇)
人工智能·windows·microsoft
AndrewHZ2 小时前
【LLM技术全景】开源大模型生态:如何选择适合你的基座模型?
人工智能·深度学习·语言模型·开源·llm·transformer·基座模型
三更两点2 小时前
AI拉呱-2026年06月04日AI技术洞察简报
人工智能