.cursorrules / CLAUDE.md 为什么必然漏规矩

wescode · 2026-09-25 · CSE / 规则文件 / 隐式约束

利益声明:本文作者参与了 wescode 的开发。文中涉及 wescode 的技术描述基于内部测试数据;涉及其他工具的描述基于各家公开文档和社区反馈。

每个项目都有一堆不成文的规矩。错误处理必须用 new AppError('message', { cause: err }) 包装、数据库写入必须走 Repository 层、这个接口的字段顺序不能改因为有序列化依赖……

这些规矩没写在任何文档里,但老成员心照不宣。新人踩一次坑学一条,攒半年才踩完。AI 不会踩坑——它直接无视这些规矩,写出编译通过、测试通过、上线之后才爆的代码。

现在的对策是写规则文件:Cursor 用 .cursorrules,Claude Code 用 CLAUDE.md,Copilot 用 .github/copilot-instructions.md。想法很好,但这条路有一个结构性问题:你写不全。


先看一个真实的 .cursorrules 文件

下面是一个 TypeScript 后端项目的 .cursorrules,20 条规则,已经算写得很认真了:

1. 错误处理用 new AppError('msg', { cause: err }) 包装
2. 数据库操作必须走 Repository 层,Controller 不直接调 Prisma
3. import 语句先外部库后内部模块,之间空一行
4. 函数命名 camelCase,类名 PascalCase,文件命名 kebab-case
5. 所有 API 返回统一的 { code, data, message } 格式
6. 请求参数用 Zod schema 校验
7. 禁止循环 import,用 @/ 路径别名
8. 日志用 logger,不用 console.log
9. 数据库事务用 prisma.$transaction() 包裹
10. 环境变量从 src/config/env.ts 统一导出
11. 测试文件和源码同目录,命名 *.test.ts
12. 异步函数必须有 try/catch 兜底
13. 接口字段顺序不能改(外部 Excel 导出依赖列序)
14. 支付 API 必须写审计日志
15. DTO 定义在 src/types/ 下
16. HTTP 状态码只用 200/400/401/403/404/500
17. Redis 缓存 key 必须带业务前缀
18. 所有 cron 任务注册在 src/cron/registry.ts

我们逐条标注——哪些 CSE 能从代码里自动推导,哪些必须人写:

编号规则CSE 能推导?推导方式
1AppError + { cause } 包装✅统计推断:95% 的 catch 块都这么写
2Controller 不直接调 Prisma✅模块边界 Checker:检测层级越界
3import 排序✅导入规则 Checker:扫描已有排序模式
4命名风格✅命名 Checker:统计命名模式
5统一返回格式✅API 契约 Checker:比对返回类型
6Zod 校验✅统计推断:90% 的路由都用了 Zod
7禁止循环、@/ 别名✅导入规则 Checker:依赖图 + 别名一致性
8–12logger / 事务 / 环境变量 / 测试 / try-catch✅统计推断 + 模块边界 Checker
13接口字段顺序(Excel 依赖)否业务约束,代码看不出原因
14支付 API 审计日志否合规要求
15–16DTO 归属 / 状态码集合✅模块边界 + API 契约 Checker
17Redis key 前缀否外部命名约定
18cron 注册到 registry✅模块边界 Checker:模式一致性

结果:18 条中 15 条 CSE 能自动推导(83%),3 条必须人写。 你花时间手写的 15 条,其实等于在做扫描器就能做的事。


为什么你写不全

1. 你不知道自己知道什么

大多数隐式规矩的特点是:遵守的人不觉得它是规矩。你每次写错误处理都带 { cause: err },不是因为记得有这条规矩,而是肌肉记忆。让你列出"项目有哪些编码规矩",你可能想到 20 条——但实际上有 200 条。

试一个实验:打开最近 20 条 PR review 评论,看看有多少条在指出"我们不这么写"。每一条都是一个隐式规矩。

2. 规矩在变,文件不跟着变

.cursorrules 写好的那天是最准确的。之后每新增一个模块、每改一次架构,这个文件就过期一点:

时间仍然有效已过期但看不出明显过期
写入当天30(100%)00
1 个月后25(83%)3(10%)2(7%)
3 个月后18(60%)8(27%)4(13%)
6 个月后12(40%)10(33%)8(27%)

"已过期但看不出"最危险。 规矩变了但文件还是旧的,AI 按旧规矩写代码,review 也觉得"反正 .cursorrules 里是这么写的"。谁来更新?改架构的人不会想到还有个规则文件要同步——因为他自己写代码不需要看这个文件。

3. 规矩有上下文

"错误处理用 { cause: err } 包装"是简化版。完整版是:在 src/services/ 里用 AppError 包装,在 CLI 入口里直接 console.error + process.exit(1)。写进规则文件的通常是简化版,然后 AI 在 src/cli/index.ts 里也层层包装了。

CSE 理解代码的方式:从语法树到语义模式再到项目惯例


CSE:从代码里自动学规矩

CSE 约束满足引擎的工作流程:从代码扫描到惯例推导再到实时约束

如果规矩不靠人写,而是从代码本身推导出来呢?扫描项目里的所有代码,统计实际的编码模式——如果 95% 的错误处理都用了 { cause } 包装,那这就是这个项目的规矩,即使没人把它写进任何文件。

wescode 的 CSE(约束满足引擎)走的就是这条路。

13 个 Checker

类别Checker 示例检查什么
命名规范变量 / 函数 / 文件camelCase vs snake_case、前缀、复数
错误处理包装 / 传播 / 类型{ cause } 包装、try/catch vs if null
导入规则排序 / 别名 / 循环外部先内部后、@/ 一致、禁止循环
模块边界层级 / 依赖方向 / 归属Controller 不调 Repository、单向依赖
API 契约返回类型 / 参数 / 状态码统一响应格式、Zod 校验、状态码集合

每个 Checker 不是固定规则,而是推导器——它看你的代码怎么写,推导出你的规矩。同一个"命名 Checker"在 Python 项目里推导出 snake_case,在 Java 项目里推导出 camelCase。

tree-sitter AST:CSE 怎么读懂代码

CSE 不是做正则匹配——它通过 tree-sitter 把源码解析为 AST,在结构化的语法节点上做模式匹配。比如对 try/catch,tree-sitter 产出:

try_statement
├── statement_block (try body)
└── catch_clause
    └── throw_statement
        └── new_expression: AppError(..., { cause: err })

错误处理 Checker 遍历所有 catch_clause 节点,提取 throw → new_expression 的模式。当 95% 的 catch 块都是同一种模式,就推导出惯例。基于 AST 比正则精确得多——不会把注释里的代码算进去,也不被换行缩进干扰。tree-sitter 的增量解析还意味着文件改动时只重新解析变化的部分。

统计推断的阈值机制

CSE 根据模式频率决定约束强度:

频率阈值约束等级行为典型场景
90%+强惯例偏离时高亮提示,注入 AI 上下文错误处理包装、命名风格
80–90%一般惯例低调提示,不阻断导入排序、测试文件位置
< 80%无明确惯例不产生约束团队在两种写法间分裂

分级的关键不是数字,而是语义:区分"团队已达成共识"和"团队还在探索"。 85% 用 async/await、15% 用 .then() 的项目,CSE 标记为一般惯例——提醒你多数人的选择,但不强制改那 15%(可能有具体原因)。

4 条推断路径

  1. 种子约束——项目首次打开时全量扫描,统计每种模式的频率。打开即生效,无需配置。
  2. 统计推断——持续监控代码变更。团队有意改了惯例(多人都不再用 { cause }),CSE 自动放松约束。规矩跟着代码走,不跟着文件走。
  3. L2.5 回归学习——wescode 的 L2.5 行为基线检测到"行为变化"且用户确认是错误的,CSE 学到新约束。下次类似改动时提前拦截。
  4. 用户教学——用户 review 拒绝了 AI 的改动,CSE 记录并推导约束,下次在生成阶段就拦截。

L2.5 行为基线如何与 CSE 配合

CSE 管"代码怎么写",L2.5 管"代码怎么跑"。两者形成闭环:

同一个错误最多犯两次:第一次 L2.5 发现,第二次 CSE 预防。


写脚本能替代 CSE 吗?三个例子

模块边界检测(TypeScript)

// 检测 Controller 是否违反"只能通过 Service 访问数据"的约束
const LAYER_RULES: Record<string, string[]> = {
  'src/controllers': ['src/repositories', 'prisma'],
  'src/services':    ['src/controllers', 'src/routes'],
};

function checkFile(filePath: string): Violation[] {
  const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
  const fileLayer = Object.keys(LAYER_RULES).find(l => filePath.includes(l));
  if (!fileLayer) return [];

  const violations: Violation[] = [];
  ts.forEachChild(sourceFile, (node) => {
    if (ts.isImportDeclaration(node)) {
      const importPath = (node.moduleSpecifier as ts.StringLiteral).text;
      for (const banned of LAYER_RULES[fileLayer]) {
        if (importPath.includes(banned))
          violations.push({ file: filePath, importPath, rule: `${fileLayer} 禁止导入 ${banned}` });
      }
    }
  });
  return violations;
}

问题: LAYER_RULES 是硬编码的。加了 src/middleware/ 或 src/infra/ 层,脚本就漏检。CSE 从实际 import 模式推导层级关系,不需要维护这张表。

代码惯例统计(Python)

# 统计命名风格和错误处理模式的比例
import ast
from collections import Counter

def classify_naming(name: str) -> str:
    if '_' in name and name == name.lower(): return 'snake_case'
    if name[0].islower() and '_' not in name: return 'camelCase'
    if name[0].isupper() and '_' not in name: return 'PascalCase'
    return 'other'

def analyze(root: str):
    naming, errors = Counter(), Counter()
    for py_file in Path(root).rglob('*.py'):
        tree = ast.parse(py_file.read_text())
        for node in ast.walk(tree):
            if isinstance(node, ast.FunctionDef):
                naming[classify_naming(node.name)] += 1
            if isinstance(node, ast.ExceptHandler):
                has_raise = any(isinstance(c, ast.Raise) for c in ast.walk(node))
                errors['re-raise' if has_raise else 'swallow'] += 1
    # 输出频率 → 判断惯例强度
    for style, count in naming.most_common():
        pct = count / sum(naming.values()) * 100
        strength = "强惯例" if pct > 90 else "一般" if pct > 80 else "无共识"
        print(f"  {style}: {pct:.1f}% → {strength}")

问题: 只覆盖命名和错误处理两个维度。13 个 Checker × 4 条推断路径 × 持续更新,你实际上需要从零构建一个约束推导引擎。

约束规则自动生成(Java)

// 从代码库提取实际模式,写入 YAML 约束文件
public class ConstraintExtractor {
    public static void main(String[] args) throws Exception {
        int totalCatches = 0, wrappedThrows = 0;
        var files = Files.walk(Path.of("."))
            .filter(p -> p.toString().endsWith(".java"))
            .filter(p -> !p.toString().contains("/test/")).toList();

        for (Path file : files) {
            CompilationUnit cu = StaticJavaParser.parse(file);
            for (CatchClause cc : cu.findAll(CatchClause.class)) {
                totalCatches++;
                if (cc.findAll(ThrowStmt.class).stream()
                    .anyMatch(t -> t.toString().contains("cause")))
                    wrappedThrows++;
            }
        }
        double ratio = (double) wrappedThrows / totalCatches;
        String strength = ratio >= 0.9 ? "strong" : ratio >= 0.8 ? "moderate" : "none";
        // 写入 project-constraints.yaml ...
    }
}

问题: 只能跑一次生成快照。代码变了要记得重跑——和 .cursorrules 面临完全一样的过期问题。

三个脚本加起来 = 一个初步的、手动维护的、只覆盖两三个维度的检查系统。CSE 是全自动、13 维度、持续更新的内置能力。


.cursorrules 手动维护 vs CSE 自动推导

治理管线:从规则声明到自动执行的全链路

维度.cursorrules 手动维护CSE 自动推导
初始成本高——核心成员花 2-4 小时列规则零——打开项目即扫描
持续维护高——每次架构变更需手动更新零——自动跟随代码变化
覆盖率低——通常只覆盖 10-20% 的隐式规矩高——自动发现 80%+ 的编码惯例(内部测试数据)
准确率(第 1 天)高——人写的当天是对的中高——统计推断有少量误报
准确率(第 90 天)中低——30%+ 已过期高——持续更新,紧跟代码现状
跨语言每种语言各维护一份同一套 Checker 自动适配

真实场景对比:

Before(手动维护): Tech Lead 花 2 小时写了 30 条 → 第 2 个月重构了错误处理,没人更新 → 第 3 个月新人 AI 按旧规矩写了 5 个 PR → review 才发现 → Tech Lead 花 30 分钟只更新了这一条 → 循环继续……

After(CSE 自动推导): 项目打开,CSE 扫描出 180 条惯例 → 重构错误处理,CSE 检测到模式变化自动更新 → 新人 AI 在生成阶段被拦截 → Tech Lead 只维护 3-4 条 CSE 推导不出的业务约束。


CSE 和 ESLint / Prettier 有什么区别

维度ESLint / PrettierCSE
规则来源人工配置 .eslintrc从代码自动推导
检查范围语法和格式语义和架构模式
能检查缩进 / 分号 / 命名格式错误处理模式 / 模块边界 / API 契约
维护成本需要持续更新配置零配置,自动跟随代码

AI 生成了 const user = await prisma.user.findUnique(...) 直接放在 Controller 里——ESLint 通过(语法正确),CSE 标出"你项目里其他 9 个 Controller 都通过 Repository 访问数据库"。ESLint 管格式层一致性,CSE 管语义层一致性,两者互补不冲突。

技能系统:CSE 如何通过 Skill 框架向 AI 注入项目特定的编码知识


CSE 能力边界

四类约束 CSE 推导不出来:

  1. 业务意图——"接口字段不能改因为有外部 Excel 依赖列序"
  2. 合规要求——"支付 API 必须写审计日志"
  3. 外部命名约定——"Redis key 带 user:session: 前缀"
  4. 历史原因——"参数顺序不合理但有 3 个下游服务按位置传参"

好消息:这只占总约束的 ~20%(内部测试数据)。最好的组合是 CSE 自动覆盖 80%,规则文件维护那 20%——而且你只维护 20 条而不是 100 条时,每条获得的注意力是 5 倍。


各工具方案对比

工具方案规矩来源自动发现自动更新代价/局限
Cursor.cursorrules人写不支持不支持—
Claude CodeCLAUDE.md人写不支持不支持—
Copilot.github/copilot-instructions.md人写不支持不支持—
wescodeCSE + 可选规则文件代码统计 + 人写补充支持支持首次索引需 5-15 秒;动态派发等场景无法推导

常见问题

Q:我们 .cursorrules 维护得挺好,有必要换吗?

A:如果有专人维护且定期更新,.cursorrules 确实覆盖很多场景。CSE 的优势在于:(1) 发现你没想到的规矩——你能想到的 20 条之外还有 180 条;(2) 代码变了自动跟着变,不依赖"有人记得去更新"。但如果规则文件已运转良好,这不是必须换的理由。

Q:CSE 会误报吗?

A:会。基于统计——95% 用了 { cause } 包装,第一个没包装的会被标为异常。但那个"异常"可能是有意的例外。所以 CSE 的结果是建议而非强制:告诉你"这里和项目惯例不一致",由你决定是遵从还是有意打破。

Q:CSE 支持什么语言?

A:通过 tree-sitter 解析 AST,原生支持 TypeScript / Python / Java / Go 等主流语言。命名和导入规则的 Checker 在各语言间自动适配——不需要你告诉它"Python 用 snake_case",它从代码里自己看出来。

Q:CSE 和 .cursorrules 能同时用吗?

A:可以。wescode 支持读取 .cursorrules / CLAUDE.md 作为显式约束,CSE 推导的隐式约束作为补充。两者冲突时显式约束优先。

Q:CSE 的推导结果怎么查看?

A:在 wescode 的项目概览面板中查看。每条约束有来源(种子扫描 / 统计推断 / L2.5 学习 / 用户教学)、置信度(频率百分比)和影响范围。这不是静态文件——是实时更新的约束视图,反映代码库当前状态。

Q:团队有人故意用不同风格写某些模块(比如数据层用 snake_case),CSE 怎么处理?

A:CSE 的约束是路径感知的。如果 src/services/ 下 95% 用 camelCase 但 src/data/ 下 90% 用 snake_case,CSE 会推导出两条独立的约束,分别绑定到不同的目录。AI 在 src/data/ 下生成代码时看到的惯例是 snake_case,不会被全局统计误导。这比 .cursorrules 写"变量命名用 camelCase"精确得多——因为规则文件很难表达"在 A 目录用风格 X,在 B 目录用风格 Y"这种上下文依赖。

Q:项目刚开始、代码量很少的时候 CSE 有用吗?

A:代码量少意味着模式还没稳定——CSE 推导出的约束会比较少,且置信度偏低(低于 80% 阈值的不会生成约束)。这时候 .cursorrules 更有价值:你在定义规矩而不是发现规矩。CSE 的收益随代码量增长而增长——当项目超过 5000 行、有 3+ 个模块时,隐式规矩开始远超你能手写的范围,CSE 开始显著减少 review 中的"我们不这么写"评论。两者不是替代关系,而是项目不同阶段的重心不同:早期靠规则文件定义方向,中后期靠 CSE 发现和维护团队已经形成但未显式记录的共识。

Q:CSE 支持 monorepo 吗?多个子项目的惯例会互相干扰吗?

A:支持。CSE 的分析以 workspace 内的目录结构为边界。monorepo 中 packages/frontend/ 和 packages/backend/ 会被当作独立的分析区域——前者的 React 组件命名惯例和后者的 Express 路由命名惯例各自推导、互不污染。子项目间如果存在共享的惯例(比如都用同一种错误处理模式),CSE 也能识别并生成跨目录的全局约束。这是路径感知推导的自然延伸——monorepo 只是"不同目录不同风格"的大规模版本。

本文对 Cursor、Claude Code、Copilot 的描述基于其 2026-09-20 的公开文档。