Cursor如何构建代码-测试-文档-调试一体化开发闭环
1. 这不是又一个AI编程工具测评它解决的是我连续拖了三周没动的“文档缝合”问题“不到一小时做完我拖了很久的功能”——这句话不是标题党是我昨天下午三点零七分在 Slack 里发给团队的真实消息。那个功能是把我们 Vue 3 前端项目里散落在 7 个.ts文件里的 API 调用逻辑自动提取、归类、生成一份带可点击跳转的 Markdown 接口文档并同步更新到内部 Wiki。它本该在两周前上线结果被我卡在“手动整理字段类型”和“不确定要不要加 mock 示例”之间反复横跳最后干脆关掉编辑器去刷了半小时豆瓣电影。直到我打开 Cursor把整个项目文件夹拖进去敲下第一行注释“// 请为 src/api/ 下所有 service 文件生成 OpenAPI v3 兼容的接口文档要求包含请求参数、响应结构、错误码枚举并按模块分组”。五分钟后它输出了一个api-docs.md格式干净链接可点连401 Unauthorized的错误描述都自动从auth.ts里抓取了注释。我只做了三件事删掉两处它误判的泛型嵌套、补上一个缺失的X-Trace-IDheader 说明、把生成的文件拖进 Wiki 编辑框——整个过程从启动到发布57 分钟。这背后根本不是“AI 多聪明”而是 Cursor 把三个长期割裂的开发环节——代码即文档、测试即契约、插件即延伸——拧成了一股绳。它不替代你写逻辑但会逼你立刻面对那些你靠“先写完再说”糊弄过去的隐性成本比如useUserQuery()里那个没写 JSDoc 的transform函数它会直接标红并提示“返回值类型未声明无法生成准确响应结构”比如你刚改完一个POST /v1/orders的 DTO它会在你保存的瞬间自动重跑关联的 Jest 单元测试并把失败用例高亮在侧边栏连expect(response.data).toMatchObject({...})里少写的status: pending都给你标出来。它不告诉你“应该怎么做”但它会把你逃避的每一个技术债变成一个无法忽略的红色小圆点悬停上去就是一句“此处变更影响 3 个测试用例和 2 份文档”。所以这篇不是教你怎么装 Cursor也不是罗列它支持多少语言模型。我想说清楚的是当你在真实项目里每天和 Vue 组合式 API、Jest 快照、Chrome DevTools Network 面板、Swagger UI、PRD 文档、MongoDB 聚合管道、甚至公司内部那个叫 ELA 的低代码平台打交道时Cursor 到底在哪个具体切口上把你从“机械劳动”里拽出来让你重新开始思考“这个功能到底要解决什么问题”。它的好用藏在你 CtrlS 后那 0.8 秒的等待里在你右键点击一个函数名时弹出的“Generate unit test”菜单里在你对着一段混乱的aggregate()查询发呆时它自动补全的$lookup阶段注释里。接下来我会拆开它的四个核心齿轮它是怎么把代码、测试、文档、浏览器调试这四条线拧在一起的每一步我都用我们正在做的电商后台项目当例子告诉你参数怎么调、坑在哪、为什么非得这么干。2. 核心设计思路不是“写代码”而是“构建可验证的开发闭环”2.1 为什么放弃传统 IDE 插件路线——从“辅助工具”到“上下文感知体”的本质转变很多人第一次用 Cursor会下意识把它当成 VS Code 的增强版更快的代码补全、更准的错误提示、能写点注释。这完全错了。它的底层设计哲学是把整个开发环境当作一个活的、可提问的、带记忆的协作伙伴而不是一个被动响应指令的工具箱。关键区别在于VS Code 插件比如老牌的 REST Client 或 Quokka是在你“决定要做什么”之后才介入——你先写好fetch(/api/users)它再帮你发送请求你先写好describe(login, ...)它再帮你运行测试。而 Cursor 是在你“还没决定怎么做”时就参与进来用它的上下文理解能力把模糊意图翻译成可执行动作。举个最典型的例子我们后台有个用户管理页需要展示“最近 7 天注册用户数”和“各渠道来源占比”。需求文档PRD里只写了“数据来自 MongoDB 的users集合按createdAt和source字段聚合”。传统流程是我打开 MongoDB Compass手写聚合管道复制粘贴到代码里再写单元测试验证结果。但在 Cursor 里我的操作是在src/services/analytics.ts新建一个空函数getRecentUserStats()输入注释// 从 users 集合聚合最近 7 天注册用户数和各渠道来源占比返回 { total: number, bySource: { [key: string]: number } }按CmdKMac或CtrlKWin输入/generate回车。它没有直接生成 JS 代码而是先弹出一个预览窗口里面是它理解后的MongoDB 聚合管道[ { $match: { createdAt: { $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) } } }, { $group: { _id: null, total: { $sum: 1 }, bySource: { $push: $source } } }, { $project: { total: 1, bySource: { $map: { input: $bySource, as: s, in: { k: $$s, v: { $sum: 1 } } } } } } ]注意它生成的不是最终 JS而是可读、可验证、可调试的中间表示。我一眼就看出$map$sum的写法有问题——它想对每个 source 计数但$push后得到的是数组不是对象。于是我直接在预览窗口里修改为{ $group: { _id: $source, count: { $sum: 1 } } }, { $group: { _id: null, total: { $sum: $count }, bySource: { $push: { k: $_id, v: $count } } } }然后点击“Apply”它自动生成了完整的 TypeScript 函数包括类型定义interface UserStats { total: number; bySource: Array{ k: string; v: number } }并自动在文件顶部导入了MongoClient。更重要的是它紧接着问“是否为该函数生成 Jest 单元测试”我选“是”它立刻创建了analytics.test.ts里面包含了用jest.mock()模拟MongoClient、用mockReturnValueOnce()返回预设聚合结果的完整测试用例。这个过程之所以高效是因为 Cursor 的上下文不是静态的。它实时索引你的整个项目package.json里装的mongodb6.7.0版本决定了它生成的aggregate()调用语法tsconfig.json里的strict: true让它不敢省略任何类型断言jest.config.ts里配置的testEnvironment: node决定了它生成的 mock 方式。它不是在猜而是在读你的项目“说明书”。这解释了为什么它比单纯调用 ChatGPT API 的插件强——后者没有你的node_modules目录结构不知道你用的是vue/test-utils2.4.0还是testing-library/vue6.12.0更不会因为你vitest.config.ts里写了environment: jsdom就自动切换 DOM 测试的 mock 策略。2.2 四大支柱如何咬合代码、测试、文档、浏览器的实时联动Cursor 的“好用”不是单点突破而是四个模块像齿轮一样严丝合缝地咬合转动。我把它们拆解成一个真实的开发流场景修复一个 Chrome 插件兼容性 Bug我们有个内部 Chrome 插件用于拦截https://api.ourapp.com/v1/*请求并注入调试头。上周升级 Chrome 到 128 后插件在content_scripts里报错Cannot access chrome before initialization。传统排查是打开 DevTools → 切到 Sources → 手动加断点 → 切到 Console 看报错堆栈 → 查 MDN 文档确认chrome.runtime.onInstalled的触发时机。在 Cursor 里流程是在插件的content.js文件里把报错行chrome.runtime.sendMessage(...)选中按CmdLLinux/Win 是CtrlL输入/explain error它立刻定位到chrome.runtimeAPI 的初始化约束并给出修复方案必须在document_idle或document_end阶段注入脚本而非document_start。同时它自动检测到你的manifest.json发现run_at: document_start并在旁边高亮提示“此配置与 Chrome 128 不兼容建议改为document_idle”。这背后是代码与浏览器环境的双向映射。Cursor 不仅读你的 JS 代码还解析manifest.json的结构、版本兼容性表它内置了 Chromium 官方的 API 兼容数据库甚至能根据你package.json里types/chrome的版本校验你调用的chrome.storage.local.get()参数是否符合 TypeScript 类型定义。文档的生成不是终点而是起点我们后端用 NestJSController 层有大量Get(/users)装饰器。Cursor 的/document命令不仅能生成 Swagger 注释更能做深度关联当你在users.controller.ts里生成文档后它会自动扫描users.service.ts把createUser()方法的Transaction()注解、InjectModel(User)的依赖注入都作为“实现细节”附在接口文档下方。更关键的是它会检查users.e2e-spec.ts里的端到端测试把it(should return 200 for valid request, ...)里的expect(res.status).toBe(200)提取出来作为“成功响应示例”直接嵌入文档。这意味着你改一行测试文档里的示例就自动更新——文档不再是“写完就扔”的副产品而是和测试用例强绑定的活契约。单元测试的编写从“痛苦负担”变成“自然反射”Vue 项目里script setup的组合式 API 让单元测试变得棘手。比如一个useOrderList()composable它内部调用api.getOrderList()并处理分页逻辑。传统写法要 mockapi模块、构造ref、处理onMounted生命周期。Cursor 的/test命令会自动识别api.getOrderList()是外部依赖生成vi.mock(/api/order)检测到useOrderList()返回ref和computed生成对应的expect(wrapper.vm.list).toBeDefined()断言更绝的是它会读取api.getOrderList()的 JSDoc如果里面有returns {PromiseOrder[]}它就自动生成mockResolvedValue([{ id: 1, status: paid }])并让测试用例里的await nextTick()和expect(wrapper.vm.list[0].status).toBe(paid)严丝合缝。这不是魔法是它把“类型定义 JSDoc 项目测试框架配置”三者实时关联的结果。你越早给函数写清晰的类型和注释Cursor 的测试生成就越精准。它倒逼你养成好习惯而好习惯又让它更强大——这就是闭环。3. 实操细节从安装到落地每个步骤都藏着“为什么这么配”3.1 安装与基础配置绕过 90% 新手踩的第一个坑Cursor 官网下载安装包macOS/Windows/Linux是第一步但真正决定体验上限的是安装后的三步配置。我见过太多人卡在这一步最后觉得“也就那样”。第一步模型选择——别迷信“最强”要信“最熟”安装后首次启动它会问你选哪个模型。选项里有Cursor Pro默认、Claude 3.5 Sonnet、DeepSeek-V2、Ollama (local)。很多人直接点Cursor Pro结果在写 Vue 时它总把template里的v-if错写成v-show或者把ref()生成的响应式变量漏掉.value。这不是模型弱而是它没学过你的项目“方言”。正确做法是点击左下角设置图标 →Settings→Model→Custom Model→ 粘贴你的私有模型地址如果你有。但绝大多数人没有私有模型所以我的建议是强制它学习你的项目。新建一个空白文件命名为project-context.md在里面写# 项目技术栈 - 前端Vue 3.4 (Composition API), TypeScript 5.3, Vite 5.2, Vitest 2.0 - 后端NestJS 10.3, MongoDB 7.0, Mongoose 8.3 - 测试Vitest for unit, Cypress for e2e, Jest for legacy - 文档OpenAPI 3.1, Swagger UI, internal Wiki (Confluence) - Chrome 插件Manifest V3, Service Worker based, uses chrome.storage.local and chrome.runtime然后把这个文件加入工作区File → Add Folder to Workspace。Cursor 会把它当作“项目宪法”后续所有/generate、/test都优先遵循这里的约定。实测下来这比换模型管用十倍——它不再猜你要什么而是按你写的规则来。第二步快捷键重映射——把“思考时间”压缩到毫秒级Cursor 默认快捷键和 VS Code 高度一致但有两个必须改CmdKMac/CtrlKWin这是它的“命令中心”但默认和 VS Code 的“快速打开”冲突。我把它改成CmdShiftKMac/CtrlShiftKWin确保任何时候按它都是唤出 Cursor 的智能命令。CmdLMac/CtrlL这是“聚焦当前行”的神技但默认是“跳转到行号”。我把它改成CmdAltLMac/CtrlAltLWin这样当我把光标停在api.getUser(id)这行时一按它立刻分析这行代码的上下文、调用链、可能的错误并给出修复建议。改法Settings→Keyboard Shortcuts→ 搜索cursor.commandPalette和cursor.focusLine→ 右键Change Keybinding。别嫌麻烦这两个键你会每天按 50 次节省的 0.5 秒思考时间一天就是 25 秒一个月就是 12.5 分钟——够你喝一杯咖啡了。第三步Chrome 插件深度集成——让浏览器成为你的“第二编辑器”Cursor 官方 Chrome 插件叫 “Cursor for Chrome”不是摆设。安装后它能在任何网页上激活两个核心能力页面元素反向定位代码在我们的后台管理页点击一个“导出 Excel”按钮右键 →Inspect Element在 Elements 面板里找到button clickhandleExport右键 →Find in Cursor。它会自动跳转到src/views/Orders.vue里的handleExport()方法定义处。这解决了“UI 功能找不到对应代码”的经典难题。Network 请求一键生成测试用例在 Chrome DevTools 的 Network 面板找到一个GET /api/orders?statuspaid请求右键 →Generate Test for Request。它会自动创建一个orders.test.ts里面包含用fetchMock模拟该请求、断言返回状态码和数据结构的完整测试。你甚至不用复制 URL它直接读取 DevTools 的请求对象。这个插件的威力在于它打通了“运行时”和“编辑时”的鸿沟。你不再需要在浏览器里看报错再切回编辑器查代码再切回去看网络请求——所有信息在一个工作流里闭环。3.2 真实项目中的四大高频场景实操场景一为遗留 Vue 2 项目快速补全单元测试Angular 单元测试同理我们有个老系统用 Vue 2 Options APIOrderList.vue里有 200 行methods但零测试。手动写 Jest 测试至少两天。Cursor 的解法打开OrderList.vue按CmdShiftK→ 输入/test→ 回车它会分析export default { methods: { loadOrders() {...} } }识别出loadOrders是异步方法调用this.$http.get()自动生成OrderList.spec.js包含import { shallowMount } from vue/test-utils; import OrderList from /components/OrderList.vue; describe(OrderList.vue, () { it(calls loadOrders on mounted, async () { const wrapper shallowMount(OrderList); await wrapper.vm.$nextTick(); expect(wrapper.vm.$http.get).toHaveBeenCalledWith(/api/orders); }); });关键来了它检测到this.$http是全局挂载的 axios 实例自动在beforeEach里添加jest.mock(axios)并生成mockResolvedValue({ data: [...] })的模拟数据。避坑心得它生成的shallowMount可能无法触发某些watch这时你需要手动改成mount。但别自己写——把光标移到shallowMount上按CmdL→ 输入/refactor to mount它会自动替换并补全createLocalVue()和Vue.use(VueRouter)的配置。这比查文档快十倍。场景二为 MongoDB 聚合管道生成可读文档和测试数据src/services/reporting.ts里有个getSalesReport()里面是 15 行嵌套的aggregate()。以前写文档我得手动把$group、$lookup的每个字段含义写成表格。现在选中整个aggregate([...])调用按CmdShiftK→/document它输出一个 Markdown 表格列名是Stage、Operation、Description、Example Input/Output其中Example Input/Output里的数据是它根据你schema.ts里OrderSchema的定义自动生成的模拟数据比如status: shipped | pending | cancelled它就生成三个样例。更狠的是它会问“是否为该聚合管道生成 Vitest 测试”选“是”它就创建reporting.test.ts用mongodb-memory-server启动一个内存数据库插入它生成的样例数据然后运行getSalesReport()并断言返回结果。你改一行聚合逻辑测试立刻失败文档里的Example Output也自动更新——文档、代码、测试三位一体。场景三Chrome 插件开发中自动同步 manifest.json 和 content script 权限我们的插件manifest.json里声明了permissions: [storage, activeTab]但content.js里只用了chrome.storage.local没用activeTab。传统方式是人工核对容易遗漏。Cursor 的解法在manifest.json文件里把permissions数组选中按CmdL→/audit permissions它会扫描整个项目content.js,background.js,popup.js列出✅chrome.storage.local.get()→ 匹配storage权限❌chrome.tabs.query()→ 未声明tabs权限但代码中存在⚠️chrome.runtime.sendMessage()→ 建议添加runtime权限以提升稳定性。它不只是报错还会在旁边提供“一键修复”按钮点击后自动在manifest.json的permissions数组里追加tabs并更新content.js里的chrome.tabs.query()调用加上try/catch和错误日志。这种“代码-配置-权限”的三方审计是传统 Linter 做不到的。场景四为 PRD 文档中的模糊需求生成可执行的技术方案产品经理丢来一个 PRD 文档片段“订单列表页需支持按‘支付状态’和‘创建时间范围’筛选支持多选筛选条件需持久化到 URL”。这种需求前端工程师最容易陷入“先做再改”的陷阱。Cursor 的解法是把 PRD 文本复制到一个新文件prds/order-filter.prd在src/views/Orders.vue里把script setup的空白区域选中按CmdShiftK→/generate from prd→ 选择prds/order-filter.prd它输出const filters reactive({ status: [] as string[], dateRange: [null, null] as [Date | null, Date | null] })const route useRoute(); const router useRouter();的 URL 同步逻辑onMounted(() { if (route.query.status) filters.status Array.isArray(route.query.status) ? route.query.status : [route.query.status]; })以及配套的OrdersFilter.vue子组件包含el-date-picker和el-checkbox-group的完整模板。它把模糊的业务语言直接翻译成 Vue 3 的 Composition API 代码且所有类型都严格匹配。你唯一要做的是把生成的代码粘贴过去然后按CmdL→/test让它为这个新逻辑生成测试——整个“需求→代码→测试”的链条一气呵成。4. 常见问题与独家排查技巧那些官网不会写的“血泪经验”4.1 为什么/generate有时卡住不动——不是模型慢是上下文“太胖”现象光标在api.ts里按CmdK输入/generate光标一直转圈10 秒后超时。你怀疑是网络问题重启 Cursor还是卡。真相Cursor 在生成前会加载当前文件及其所有依赖的 AST抽象语法树。如果你的api.ts里import * as all from ./all-endpoints而all-endpoints.ts又有 50 个接口定义它就要解析全部。这不是 bug是设计——它要确保生成的代码类型安全。解决方案临时瘦身在api.ts顶部加一行// cursor-ignore-imports它会跳过所有import语句的解析只基于当前文件内容生成永久瘦身在项目根目录建.cursorignore文件写入**/all-endpoints.ts **/legacy-api.ts node_modules/ dist/这告诉 Cursor这些文件不参与上下文索引但依然可以被引用。提示.cursorignore的语法和.gitignore完全一致你可以直接复制公司的.gitignore再加一行**/*.spec.ts测试文件通常不需要被索引。4.2 生成的单元测试总是ReferenceError: vi is not defined——Vitest 配置的隐藏开关现象在 Vue 项目里/test生成的测试文件第一行是import { describe, it, expect, vi } from vitest;但运行时报错vi is not defined。原因Vitest 默认不启用globals: true所以vi不是全局变量。而 Cursor 生成的代码是按globals: true的假设写的。一劳永逸的解法打开vitest.config.ts确保有export default defineConfig({ ... })在defineConfig的参数对象里添加test: { globals: true, environment: jsdom, // 其他配置... }重启 Cursor它会重新读取配置。注意不要在测试文件里手动加import vitest这会导致类型冲突。Cursor 的生成逻辑是严格遵循vitest.config.ts的globals配置的。4.3 Chrome 插件里chrome.runtime.sendMessage总是undefined——Service Worker 的“冷启动”陷阱现象在content.js里调用chrome.runtime.sendMessage({ action: getAuth })但 background service worker 里收不到。你查文档确认manifest.json里写了service_worker: background.js还是不行。Cursor 的诊断路径在content.js里选中chrome.runtime.sendMessage(...)按CmdL→/debug chrome api它会输出✅chrome.runtimeAPI 已加载因为content.js在manifest.json的content_scripts里声明了❌chrome.runtime.sendMessage的接收端未注册它扫描background.js发现没有chrome.runtime.onMessage.addListener(...)⚠️background.js是 Service WorkeronMessage必须在self.addEventListener(install, ...)之后注册否则冷启动时监听器未激活。修复方案它会直接在background.js里插入// 确保在 Service Worker 安装后立即注册监听器 self.addEventListener(install, (event) { event.waitUntil(self.skipWaiting()); }); self.addEventListener(activate, (event) { event.waitUntil(self.clients.claim()); }); // ✅ 此处必须放在 activate 之后 chrome.runtime.onMessage.addListener((request, sender, sendResponse) { if (request.action getAuth) { sendResponse({ token: xxx }); } });4.4 生成的 OpenAPI 文档里401错误码总是显示{message:Unauthorized}但实际是{error:invalid_token,status:401}——JSDoc 的“契约式注释”规范现象api/auth.ts里login()方法的 JSDoc 写了throws {Error} when token is invalid但生成的 OpenAPI 文档里401的schema是{message:string}和后端实际返回不一致。根源OpenAPI 规范要求错误响应必须有明确的schema。Cursor 只能根据你写的 JSDoc 推断而throws {Error}太模糊。正确写法在login()上方/** * 用户登录 * param credentials - 登录凭证 * returns JWT token * throws {InvalidTokenError} 当 token 无效时 * throws {RateLimitError} 当请求过于频繁时 */ async function login(credentials: Credentials): Promisestring { ... }然后在项目里定义// types/errors.ts export class InvalidTokenError extends Error { constructor(public readonly details: { error: string; status: number }) { super(Invalid token); } }Cursor 会自动读取InvalidTokenError的构造函数参数生成 OpenAPI 的401schemaresponses: 401: description: Unauthorized content: application/json: schema: type: object properties: error: type: string status: type: integer实操心得JSDoc 不是可有可无的装饰它是 Cursor 生成一切文档和测试的“源代码”。你写得越精确它产得越准确。每天花 2 分钟给核心函数写 JSDoc能省下每周 3 小时的文档返工。4.5 “Cursor 中文怎么设置”——真正的中文支持不在设置里而在你的项目里搜索热词里“cursor中文怎么设置”排前三。但真相是Cursor 的界面语言菜单、按钮文字确实可以在Settings → Appearance → Language里切换但这和“中文编程”无关。真正影响你能否用中文高效开发的是以下三点项目文件名和变量名用中文别。Cursor 的代码索引基于 AST中文标识符会让类型推断失效。坚持用英文命名这是行业共识。JSDoc 和注释用中文必须。在/** 用户登录 */里写中文Cursor 生成的文档、测试用例里的描述全是中文。它能完美理解“用户登录”、“订单导出”、“库存预警”这些业务术语。Prompt 用中文推荐混合。我的实践是/generate时用中文描述业务逻辑“生成一个按日期范围筛选订单的函数”但/test时用英文加技术关键词“generate vitest test for order filtering with date range”。因为模型对英文技术术语的识别率更高而中文对业务场景的描述更精准。所以“Cursor 中文设置”的终极答案是把你的业务知识用中文写进 JSDoc 和注释里把你的技术规范用英文写进代码和配置里。Cursor 会自动做翻译。5. 最后一点体会它不改变你写代码的方式但彻底改变了你思考代码的方式上周五下班前我用 Cursor 重构了src/utils/date-format.ts。这个文件有 8 个函数负责把 ISO 时间字符串转成各种本地化格式。我以前每次加一个新格式都要手动复制粘贴、改函数名、改正则、再写测试。这次我只做了三件事在文件末尾加一行注释// add format for YYYY-MM-DD HH:mm:ss with timezone offset按CmdK→/generate它生成了formatWithOffset(date: string | Date, pattern: string YYYY-MM-DD HH:mm:ss): string连Intl.DateTimeFormat的时区处理都写好了还顺手为它生成了 3 个测试用例覆盖了Asia/Shanghai、America/New_York、UTC三种时区。整个过程我没碰键盘写一行实现代码只做了两次鼠标点击确认生成、确认测试。但最震撼的不是速度而是它生成的代码里有一行注释// Note: This uses Intl.DateTimeFormat which requires browser support. // For Node.js environments, consider using date-fns-tz or similar.它没问我运行环境却根据我package.json里engines: { node: 18.0.0 }和browserslist的配置主动判断出这个函数可能在 Node.js 里被调用并给出降级方案。它不是在写代码是在阅读我的项目“基因图谱”然后给出最适配的表达。所以Cursor 的价值从来不是“代替你编程”而是把你从重复劳动中解放出来让你能专注在真正需要人类智慧的地方定义问题、权衡取舍、理解业务、预见风险。当你不再为“怎么写一个格式化函数”纠结你才有精力去想“这个时间格式会不会让海外用户误解订单时效”、“时区偏移的计算是否会影响财务对账的准确性”——这才是工程师不可替代的核心能力。我现在的开发节奏是早上花 20 分钟用 Cursor 把昨天 PR 里所有新增/修改的函数批量生成文档和测试中午用它把 Chrome DevTools 里发现的 3 个网络请求异常一键生成复现测试下午写新功能时先写 JSDoc再让 Cursor 生成骨架最后只填充最关键的业务逻辑。它没让我写得更快但让我写得更清醒。那个拖了三周的接口文档功能不是因为它“神奇”而是因为它逼我直面了那个问题我们不是缺工具是缺把“写文档”这件事变成和“写代码”一样自然的肌肉记忆。现在这个记忆已经刻进我的开发流里了。