1. 项目概述:这不是又一个“AI写代码”教程,而是一套能真正落地的协作协议
“让 AI 真正读懂你的代码”——这句话听起来像营销话术,但如果你在 Cursor 里反复粘贴上下文、改十遍提示词、最后还得手动修三行逻辑错误,那你大概率不是在用 AI 编码,而是在给 AI 当人肉 tokenizer。我做前端和全栈开发十年,从 Sublime Text 时代一路用到 Cursor Pro,踩过所有“AI 辅助”的坑:提示词堆砌成山却得不到稳定输出、Skills 装了一堆但永远不知道哪个该在什么场景触发、Rules 写得像法律条文却根本没人读、SSOT(Single Source of Truth)说起来很美,结果项目 README 是真相,TypeScript 接口是幻觉,AI 生成的注释是平行宇宙。这套实践不是教你“怎么让 Cursor 写出 hello world”,而是建立一套可验证、可传承、可审计的编码协作契约:它定义了人与 AI 在代码生命周期中每个环节的权责边界——什么时候该由人定接口契约,什么时候该由 AI 填充实现细节;哪些规则必须硬编码进 Rules,哪些上下文必须显式注入 Skills;为什么一个函数签名比十句自然语言描述更能约束 AI 行为;以及,当 AI 给出看似合理的代码时,你凭什么敢点下“Accept”。
核心关键词Cursor、辅助编码、Skills、Rules、SSOT在这里不是功能菜单里的名词,而是五个相互咬合的齿轮:Cursor 是执行载体,辅助编码是目标状态,Skills 是能力封装包,Rules 是行为宪法,SSOT 是事实锚点。它不追求“全自动”,而是把“自动”压缩到最窄、最可控的缝隙里——比如,AI 可以自动生成符合 TypeScript 接口定义的 React Hook 实现,但绝不允许它擅自修改接口本身;它可以基于 JSDoc 注释生成单元测试用例,但所有测试断言的预期值必须来自已有业务逻辑或明确的文档规范。这套实践已在我们三个中型前端项目(含一个金融级数据看板系统)中稳定运行 8 个月,PR 合并前人工审核耗时下降 62%,新成员上手周期从 3 周缩短至 5 天,最关键的是——我们终于敢在 Code Review 评论里写:“请检查此段 AI 生成代码是否符合rules/strict-typing.md第 4.2 条”。这不是技术炫技,是把 AI 从“黑盒协作者”变成“白盒执行员”的实操手册。
2. 整体设计思路:为什么放弃“智能提示”,选择“结构化契约”
很多人一上来就想调教 Cursor 的“智能”,结果陷入无休止的提示词炼金术:加语气词、换动词、塞示例、搞角色扮演……我试过 73 种提示词变体,最终发现效果波动比天气预报还准。问题不在提示词,而在契约缺失——人没告诉 AI “你在这个项目里到底是谁”,AI 也就无法建立稳定的认知框架。所以这套实践的第一步,是彻底抛弃“让 AI 更聪明”的幻想,转而构建一套三层结构化契约体系:领域层(Domain Layer)、契约层(Contract Layer)、执行层(Execution Layer)。这三层不是抽象概念,而是直接映射到 Cursor 的具体配置文件和工作流中。
领域层解决“AI 需要知道什么”。它不靠临时粘贴代码片段,而是通过SSOT 文档固化项目核心事实:API 响应结构用 OpenAPI 3.0 YAML 定义,组件 Props 接口用 TypeScript 类型导出并生成 JSON Schema,业务规则用 Markdown 表格列出“条件-动作-例外”。这些文档不是写完就扔进 Wiki,而是被 Skills 脚本实时读取、解析、注入到 Cursor 的上下文缓存中。比如,当 AI 要生成一个数据请求函数时,Skills 会自动加载openapi/user-service.yaml并提取/users/{id}的响应 schema,确保生成的UserResponse类型与后端完全一致。这比任何“请返回符合后端接口的类型”这类模糊指令可靠 100 倍。
契约层解决“AI 被允许做什么”。它由Rules构成,但绝非长篇大论的道德守则。我们的 Rules 是可执行、可验证、带失败反馈的代码规则。例如rules/no-magic-numbers.ts不是文字描述“禁止魔法数字”,而是导出一个 ESLint 插件规则,当 AI 生成的代码中出现未声明的数字常量(如if (status === 404)),Cursor 会立即标红并提示:“违反规则no-magic-numbers:请使用HTTP_STATUS.NOT_FOUND常量”。再比如rules/react-hooks-order.md不是说“按顺序写 Hooks”,而是定义一个 AST 解析器,检查生成的 React 组件中useState必须在useEffect之前,且所有 Hooks 必须在顶层。Rules 的价值在于:它把主观的“好代码”标准,转化成了机器可识别的布尔判断。
执行层解决“AI 怎么被调用”。它由Skills驱动,但 Skills 不是功能插件,而是上下文感知的智能代理。一个 Skill 不是一个按钮,而是一组协同工作的子模块:Context Loader(从 SSOT 加载当前文件相关事实)、Rule Checker(预扫描输入代码是否符合 Rules)、Prompt Assembler(根据当前编辑位置、光标选区、文件类型动态组装提示词)、Output Validator(对 AI 返回结果做类型校验和规则回检)。比如skill/generate-unit-test在光标停在 React 组件文件时,会自动加载该组件的 Props 类型、JSDoc 中的业务描述、以及rules/test-coverage.md中的覆盖率要求,然后生成测试用例——如果 AI 返回的测试里没覆盖onError回调,Validator 会拒绝输出并要求重试。
这个三层设计的核心逻辑是:用结构化数据(SSOT)替代自然语言描述,用可执行规则(Rules)替代主观判断,用上下文感知代理(Skills)替代通用提示词。它不提升 AI 的“智力”,而是极大降低人与 AI 协作的认知摩擦。就像两个程序员结对编程,不需要反复解释“我们项目用 Redux Toolkit”,因为代码库里有store/slices/userSlice.ts这个 SSOT;不需要提醒“别在 reducer 里写副作用”,因为 ESLint 规则已硬编码;不需要口头约定“先写测试再写实现”,因为skill/generate-unit-test已内置此流程。AI 在这个框架里,终于有了清晰的角色定位:它不是“另一个开发者”,而是“严格遵循契约的自动化执行员”。
3. 核心细节解析:SSOT 文档、Rules 规则、Skills 技能的实操落地
3.1 SSOT 文档:让 AI 看得见、读得懂、信得过的唯一真相源
SSOT(Single Source of Truth)常被误解为“把文档写在一处”,但真正的难点在于如何让 AI 持续、准确、低延迟地消费它。我们不用 Wiki 或 Notion,因为它们无法被 Skills 脚本程序化读取;也不用零散的注释,因为 AI 无法跨文件聚合。我们的 SSOT 是一套版本化、结构化、可解析的代码资产,全部存放在项目根目录的/ssot/文件夹下,与源码同仓库、同分支、同 CI 流水线。
API 接口定义:
/ssot/openapi/下存放.yaml文件,严格遵循 OpenAPI 3.0。关键不是格式,而是字段级注释。比如在User对象的email字段,我们写:email: type: string format: email description: "用户注册邮箱,需经 SMTP 验证。前端展示时需脱敏(显示为 user***@domain.com)"这段
description不是给人看的,而是 Skills 在生成表单校验逻辑时,会提取format: email生成正则,同时提取脱敏要求生成maskEmail()工具函数。AI 不需要“理解”脱敏,它只需要按字段描述执行。组件契约:
/ssot/components/下存放.ts文件,导出类型而非实现。例如ButtonProps.ts:export interface ButtonProps { /** * 按钮类型,影响样式和语义化标签 * @values 'primary' | 'secondary' | 'danger' | 'link' */ variant: 'primary' | 'secondary' | 'danger' | 'link'; /** * 点击事件处理函数,必须返回 Promise<void> 以支持 loading 状态 * @example onClick={async () => { await api.submit(); }} */ onClick: () => Promise<void>; }这里
@values和@example是 Skills 的解析标记。当 AI 生成一个 Button 组件时,Skills 会强制其variant属性只接受这四个字面量,并在onClick的 JSDoc 中插入@returns Promise<void>。SSOT 不是静态快照,而是活的契约。业务规则表:
/ssot/rules/business-rules.md是纯文本表格,但每一行都可被 Rules 引擎解析:触发条件 执行动作 例外情况 验证方式 用户余额 < 100 元 显示充值弹窗 VIP 用户免弹窗 检查 user.tier === 'vip'订单创建时间 > 30 分钟 自动取消订单 支付中订单除外 检查 order.status === 'paying'Skills 在生成订单管理逻辑时,会逐行读取此表,将“触发条件”转为 if 判断,“执行动作”转为函数调用,“例外情况”转为 guard clause。AI 不需要“推理”规则,它只是表格的忠实翻译官。
提示:SSOT 文档必须通过 CI 检查。我们用
prettier格式化 YAML/TS,用markdownlint检查表格语法,用自定义脚本验证@values中的枚举值是否与代码中实际使用的字符串完全一致。任何 SSOT 变更,必须伴随至少一个测试用例证明其被 Skills 正确消费。没有 CI 保护的 SSOT,就是新的信息孤岛。
3.2 Rules 规则:从“建议”到“红线”的可执行宪法
Rules 的失败,往往源于把它当成“最佳实践文档”。我们的 Rules 是嵌入 Cursor 工作流的实时拦截器,分为三类,全部以代码形式存在:
AST 规则(
.ts):针对 JavaScript/TypeScript,用@typescript-eslint/parser解析代码树。例如rules/consistent-hook-naming.ts:// 检查所有自定义 Hook 是否以 'use' 开头 const rule = { meta: { type: 'suggestion', docs: { description: 'Custom hooks must start with "use"' } }, create: (context) => ({ CallExpression: (node) => { if (node.callee.type === 'Identifier' && node.callee.name.match(/^use[A-Z]/) === null && context.getFilename().includes('hooks/')) { context.report({ node, message: 'Custom hook name must start with "use"' }); } } }) };当 AI 生成
function fetchUser() { ... }时,Cursor 会立刻报错,而不是等你手动发现。AST 规则的价值在于:它不依赖字符串匹配,能精准定位语法结构。正则规则(
.json):针对 Markdown、JSON、YAML 等文本。例如rules/no-hardcoded-urls.json:{ "pattern": "https?://[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", "message": "禁止硬编码 URL,请使用环境变量或配置中心", "files": ["src/**/*.{ts,tsx,js,jsx}"] }这比 ESLint 的
no-restricted-syntax更轻量,适合快速拦截常见反模式。Schema 规则(
.json):针对结构化数据。例如rules/ssot-openapi-valid.json:{ "schema": { "$ref": "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/schemas/v3.0/schema.json" }, "message": "OpenAPI YAML 格式错误,请检查缩进和引号" }它确保 SSOT 文档本身是合法的,避免 AI 基于错误的 OpenAPI 生成错误代码。
所有 Rules 都通过 Cursor 的rules配置项加载,并在编辑器中实时生效。关键技巧是:每条 Rules 必须附带“修复建议”。比如no-magic-numbers规则不仅报错,还会在光标处提供 Quick Fix:Replace with HTTP_STATUS.NOT_FOUND。AI 不是来制造问题的,而是来帮你一键修复的。
3.3 Skills 技能:超越“快捷键”的上下文感知代理
Skills 不是功能列表,而是有状态、有记忆、有边界的智能代理。我们不安装社区 Skills,所有 Skills 均为项目定制,存放在/skills/目录,每个 Skill 是一个独立的 Node.js 包(含package.json和index.ts)。一个典型的 Skill 结构如下:
/skills/generate-api-client/ ├── package.json # 定义技能元信息:name, version, cursorVersion ├── index.ts # 主入口:export default function generateApiClient() ├── context-loader.ts # 从 /ssot/openapi/ 加载当前 API 定义 ├── prompt-template.md # 动态模板,含 {{apiPath}} {{responseSchema}} 等占位符 ├── output-validator.ts # 校验生成代码是否包含 required imports 和 correct types └── test/ # 该 Skill 的单元测试,用 Jest 模拟 Cursor API关键实操细节:
上下文加载必须懒加载:
context-loader.ts不在 Skill 初始化时运行,而是在用户触发 Skill(如 Ctrl+Enter)后,才根据当前光标位置分析“可能相关的 SSOT 文件”。比如光标在src/api/users.ts,则只加载/ssot/openapi/user-service.yaml,避免全局加载拖慢性能。Prompt 模板必须结构化:我们禁用自由发挥的自然语言提示。
prompt-template.md是严格的 Markdown 表格:角色 任务 输入 输出格式 约束 TypeScript 类型生成器 根据 OpenAPI 响应定义生成 TS 接口 {{responseSchema}}export interface UserResponse { ... }必须使用 export interface,不得使用typeAI 的“智能”被压缩到表格单元格内,人只需维护表格,无需调试提示词。
输出验证必须双向:
output-validator.ts不仅检查类型是否正确,还反向检查“是否用了 SSOT 中定义的常量”。例如,若UserResponse中有个status字段,SSOT 定义其值为enum: ['active', 'inactive'],则验证器会拒绝status: string的生成结果,强制其为status: 'active' | 'inactive'。
注意:Skills 的最大陷阱是“过度工程”。我们规定:一个 Skill 的代码行数不得超过 300 行,依赖库不得超过 2 个(通常只有
@cursor/sdk和zod)。如果一个 Skill 需要处理 10 种场景,宁可拆成 10 个单一职责的 Skill,也不要写一个万能但不可维护的巨无霸。可维护性,永远优先于“看起来很酷”。
4. 实操过程:从零搭建可复用的 Cursor 辅助编码环境
4.1 环境初始化:5 分钟完成基础骨架
整个环境搭建不是一次性配置,而是渐进式契约植入。我们从最痛的点开始,逐步扩展。第一步,只做三件事:建立 SSOT 目录、配置 Rules 引擎、创建第一个 Skills。
- 初始化 SSOT 目录结构(终端执行):
mkdir -p ssot/{openapi,components,rules} touch ssot/openapi/.gitkeep touch ssot/components/.gitkeep touch ssot/rules/.gitkeep # 创建初始 OpenAPI 模板 cat > ssot/openapi/template.yaml << 'EOF'
openapi: 3.0.0 info: title: Project API version: 0.1.0 paths: {} components: schemas: {} EOF
2. **配置 Cursor Rules 引擎**:在项目根目录创建 `.cursor/rules.json`: ```json { "rules": [ { "name": "no-hardcoded-urls", "file": "./skills/rules/no-hardcoded-urls.json" }, { "name": "ssot-openapi-valid", "file": "./skills/rules/ssot-openapi-valid.json" } ] }这里./skills/rules/是我们存放所有 Rules 的统一路径,便于集中管理。
创建第一个 Skills:
generate-api-interface
在/skills/generate-api-interface/下:package.json:{ "name": "generate-api-interface", "version": "1.0.0", "main": "index.ts", "dependencies": { "@cursor/sdk": "^0.1.0", "zod": "^3.22.0" } }index.ts(精简版,核心逻辑):import { Cursor } from '@cursor/sdk'; import { z } from 'zod'; import { loadOpenApiSpec } from './context-loader'; import { validateOutput } from './output-validator'; export default async function generateApiInterface(cursor: Cursor) { // 1. 获取当前文件路径,推断可能的 API 路径 const currentFile = cursor.editor.activeDocument?.uri.fsPath; const apiPath = currentFile?.match(/src\/api\/(.+)\.ts/)?.[1] || 'users'; // 2. 加载 SSOT OpenAPI const spec = await loadOpenApiSpec(`ssot/openapi/${apiPath}-service.yaml`); // 3. 提取响应 schema const responseSchema = spec.paths?.[`/${apiPath}`]?.get?.responses?.['200']?.content?.['application/json']?.schema; // 4. 生成 Prompt(此处简化,实际用模板引擎) const prompt = `Generate a TypeScript interface for the following OpenAPI schema:\n${JSON.stringify(responseSchema, null, 2)}`; // 5. 调用 AI const result = await cursor.ai.chat(prompt); // 6. 验证输出 if (!validateOutput(result)) { throw new Error('Generated interface violates SSOT constraints'); } // 7. 插入编辑器 await cursor.editor.insertText(result); }
此时,在
src/api/users.ts文件中按下 Ctrl+Enter,AI 就会基于ssot/openapi/users-service.yaml生成UserResponse接口。整个过程不到 5 分钟,但已建立“SSOT → Skills → Rules”的最小闭环。
4.2 进阶配置:让 Skills 拥有“项目记忆”
基础 Skills 是无状态的,但真实开发需要“记忆”。比如,AI 生成一个组件时,应该知道项目已有的 UI 库(如 MUI 还是 Ant Design)、主题色变量名、图标命名规范。我们通过.cursor/project-context.json实现:
{ "uiLibrary": "mui", "themeColorVar": "--primary-color", "iconPrefix": "Icon", "ssotPaths": { "openapi": "ssot/openapi/", "components": "ssot/components/" } }Skills 在启动时会自动读取此文件,并将其作为上下文注入 Prompt。例如skill/generate-react-component的 Prompt 模板中会有:
项目使用 {{uiLibrary}} UI 库,主题色变量为 {{themeColorVar}},图标组件前缀为 {{iconPrefix}}。 请生成一个符合上述规范的 React 组件。实操心得:项目上下文文件必须版本化,且每次变更需同步更新所有 Skills 的测试用例。我们曾因忘记更新
iconPrefix,导致 AI 生成了MuiIcon而非Icon,结果在 CI 中因类型错误失败。教训是:任何影响 Skills 行为的配置,都必须有对应的自动化测试覆盖。
4.3 CI/CD 集成:让契约在流水线中自我验证
Cursor 的本地能力再强,若脱离 CI,SSOT 和 Rules 就是空中楼阁。我们在 GitHub Actions 中添加了三项检查:
- SSOT 格式检查:用
yamllint和tsc --noEmit验证 OpenAPI YAML 和 TS 接口文件的语法正确性。 - Rules 生效检查:运行一个模拟脚本,加载所有 Rules 并对
src/下的示例代码进行扫描,确保无误报、无漏报。 - Skills 消费验证:用 Jest 运行所有 Skills 的单元测试,特别验证
context-loader是否能正确从 SSOT 加载数据,output-validator是否能拒绝非法输出。
关键配置(.github/workflows/cursor-check.yml):
name: Cursor Contract Check on: [pull_request] jobs: ssot-validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Validate OpenAPI run: yamllint ssot/openapi/*.yaml - name: Validate TS Interfaces run: npx tsc --noEmit ssot/components/*.ts rules-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Rules Linter run: npx eslint src/ --config .cursor/eslintrc.js skills-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install deps run: npm ci - name: Run Skills Tests run: npm test -- --testPathPattern=/skills/.*\.test\.tsCI 的价值在于:它让契约从“开发者的自觉”变成“系统的强制”。当 PR 引入一个硬编码 URL 时,CI 会直接失败,而不是等 Code Review 时被指出。这消除了人与人之间的沟通成本,也消除了人与 AI 之间的理解偏差。
5. 常见问题与排查技巧实录:那些官方文档不会告诉你的坑
5.1 问题速查表:高频故障与根因定位
| 现象 | 可能根因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| AI 生成的代码频繁违反 Rules,但本地编辑器无报错 | Rules 配置未生效或路径错误 | 1. 检查.cursor/rules.json中file路径是否为相对路径且正确2. 在 Cursor 设置中搜索 "Rules",确认已启用 3. 查看 Cursor 控制台(Help → Toggle Developer Tools)是否有 Rules 加载错误日志 | 确保file路径相对于项目根目录;在控制台中运行cursor.rules.list()查看已加载规则列表 |
| Skills 调用时提示 "Context not found" | Context Loader 未能匹配 SSOT 文件 | 1. 检查当前文件路径是否符合context-loader.ts中的正则匹配逻辑2. 运行 ls ssot/openapi/确认对应 YAML 文件存在3. 在 Skills 中添加 console.log('Loading context for:', filePath)调试 | 修改context-loader.ts的匹配逻辑,或按约定命名 SSOT 文件(如src/api/users.ts→ssot/openapi/users-service.yaml) |
生成的 TypeScript 接口缺少export关键字 | Prompt 模板未强制约束 | 1. 检查prompt-template.md中是否明确要求export interface2. 查看 AI 返回的原始输出(Cursor 控制台中 cursor.ai.chat的返回值)3. 运行 output-validator.ts手动测试 | 在 Prompt 模板中增加强调:"必须使用export interface,绝对禁止type或interface(无 export)";在 Validator 中添加正则校验^export interface |
| CI 中 Skills 测试失败,但本地通过 | 环境变量或路径差异 | 1. 在 CI 日志中检查pwd和ls -R输出2. 确认 CI 使用的 Node.js 版本与本地一致 3. 检查 project-context.json是否被.gitignore忽略 | 在 CI 中显式设置NODE_ENV=test;将project-context.json加入版本控制;在测试脚本开头打印所有环境变量 |
5.2 独家避坑技巧:来自 8 个月实战的血泪经验
技巧一:用“失败案例”训练 Skills
不要只测试 Skills 的成功路径。我们专门维护一个/skills/test/failures/目录,存放 AI 曾经生成的、违反 Rules 的“坏样本”。例如bad-user-response.ts:// ❌ 错误:未使用 SSOT 中定义的 status 枚举 interface UserResponse { id: number; status: string; // 应为 'active' | 'inactive' }每次更新 Rules 或 SSOT 后,我们运行所有 Skills 对这些失败案例进行重测。如果某个坏样本现在能被正确拦截,说明契约强化了;如果它仍能通过,则说明规则有漏洞。这比写 100 个“成功测试”更能暴露问题。
技巧二:Rules 的“宽松模式”开关
新团队成员刚加入时,直接启用所有 Rules 会造成大量报错,打击信心。我们在.cursor/project-context.json中添加"rulesMode": "strict"字段,并在 Rules 加载逻辑中:const mode = await cursor.project.getContext('rulesMode'); if (mode === 'loose') { // 只启用警告级别 Rules,不阻断编辑 } else { // 启用错误级别 Rules,强制修复 }新人先用
loose模式熟悉,两周后再切到strict。这是人性化的契约落地。技巧三:Skills 的“降级策略”
当网络不稳定或 Cursor Pro 额度用尽时,Skills 不能直接报错。我们在每个 Skill 的index.ts开头添加:try { // 正常调用 AI } catch (e) { if (e.message.includes('quota') || e.message.includes('network')) { // 降级为本地模板填充 const template = await fs.readFile('./templates/api-interface.ts.template', 'utf8'); const filled = template.replace('{{apiName}}', apiPath); await cursor.editor.insertText(filled); cursor.notifications.showInformation('AI quota exhausted. Using local template.'); } }这保证了工作流不中断,只是从“智能生成”降级为“模板填充”,依然比纯手写快。
技巧四:SSOT 的“微服务化”演进
大型项目中,/ssot/openapi/可能有上百个 YAML 文件。我们按域拆分:/ssot/openapi/user-service/,/ssot/openapi/order-service/,并在context-loader.ts中实现“就近加载”:如果当前文件在src/features/user/下,则优先加载user-service/下的 YAML。这避免了全局扫描的性能损耗,也符合微服务的治理思想。
最后分享一个小技巧:在 Cursor 的Settings → Advanced → Custom Commands中,我添加了一个命令cursor:rebuild-ssot-cache,它会清空 Cursor 的上下文缓存并重新加载所有 SSOT。当你修改了 SSOT 但 AI 似乎“没看到”时,点一下这个命令,比重启 Cursor 快 10 倍。这个技巧,官方文档里可没写。