最近把一个 Galgame 资源站从 Hugo 整个重写成了 Next.js,仓库是 vns-next。从 create-next-app 那个初始 commit 到能挂上线,一共 13 个 commit,时间跨度不到 20 小时。

事后翻仓库的时候发现一件挺好笑的事——我给 Claude 写的那份约定文件,全文只有一行。而且那一行还不是我写的喽…

这篇不聊渲染模式也不聊部署,就只说「项目起步时该给 AI 留什么约定」这一件事。记录下我在几个项目里来回试出来的结论…

我的 CLAUDE.md 全文就一行

先上原文,vns-next 根目录的 CLAUDE.md 全文:

@AGENTS.md

对,就这一行,用 import 语法指向隔壁的 AGENTS.md。那再看 AGENTS.md,全文 5 行:

<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

注意那对 BEGIN / END 标记——这段不是我写的,是 create-next-app 自己塞进来的。核心就一句话:别信你训练数据里的 Next.js,写代码前先去读 node_modules/next/dist/docs/

之前开 thdl(东方同人资源站)的时候就是这套,CLAUDE.md 一模一样,也是一行 import 指向 AGENTS.md,内容一字不差。好处很明显:单一事实源,两份规则不会打架~

就这么点,够不够用?我一开始也觉得肯定不够…

另一个极端:一个仓库三套约定文件

之前那个项目 mikiacg 走的是完全相反的路——同时维护三套:

  • 根目录 CLAUDE.md,111 行
  • .cursor/rules/ 下 9 个 .mdc,其中 project.mdclibrary-versions.mdcalwaysApply: true
  • .github/copilot-instructions.md,开篇就把自己降级成索引:「完整约定见项目根目录 CLAUDE.md

CLAUDE.md 第 3 行是这么开场的:

时效性提示:本项目使用大量前沿版本的库,AI 训练数据很可能覆盖不到。遇到不确定的 API 用法时,务必查询最新文档(Cursor 中可用 Context7 MCP)。

然后专门开了一节「关键版本差异(AI 常见错误)」,6 条:

- `params` / `searchParams` 在 Next.js 16 中是 **Promise**,服务端用 `await`,客户端用 `use()`
- Tailwind v4 用 **CSS 配置**`@import "tailwindcss"` + `@theme {}`),无 `tailwind.config.ts`
- Prisma 7 generator 为 `"prisma-client"`(不是 `"prisma-client-js"`),导入路径 `@/generated/prisma/client`
- Zod 4 导入 `import { z } from "zod"`(不是 `zod/v4`- 日期用 `dayjs`,不用 `date-fns`
- 认证用 Better Auth(`@/lib/auth-client`),不是 next-auth

.cursor/rules/library-versions.mdc 结尾还有一张 ❌→✅ 对照表,7 条:

1.`params.id` → ✅ `(await params).id``use(params).id`
2.`tailwind.config.ts``theme.extend` → ✅ `globals.css``@theme { }`
3.`import { PrismaClient } from "@prisma/client"` → ✅ `import { PrismaClient } from "@/generated/prisma/client"`
4.`provider = "prisma-client-js"` → ✅ `provider = "prisma-client"`
5.`import { z } from "zod/v4"` → ✅ `import { z } from "zod"`
6. ❌ 用 `date-fns` 处理日期 → ✅ 用 `dayjs`
7. ❌ 用 `next-auth` API → ✅ 用 Better Auth(`@/lib/auth-client`

这份是真有用的。因为它写的全是 AI 不可能知道的东西——版本差异。Prisma 7 的 generator 名字变了、Zod 4 的导入路径变了,这种事没写进去,模型就会按记忆里的老写法糊一版给你,然后你花二十分钟 debug 一个根本不存在的 import。这钱我付过好几次了…

三套文件的代价是漂移

commit 519e5a7 那次我更新文档,把 CLAUDE.md 里一条写错的说明拆开了——原文写「pnpm lint — ESLint + TypeScript 检查」,但 package.jsonlint 只跑 eslint .,根本不含类型检查。改完是三条:

- `pnpm lint` — 仅 ESLint
- `pnpm typecheck` — 仅 TypeScript 类型检查(`tsc --noEmit`- `pnpm check` — ESLint + TypeScript 类型检查(提交前完整校验)

同一个 commit 还把版本号补齐了:tRPC 11.15 → 11.17、Zod 4.3 → 4.4、Prisma 7.6 → 7.8、Better Auth 1.5 → 1.6。

然后呢?.cursor/rules/project.mdc 至今仍写着 tRPC 11.15、Zod 4.3、Prisma 7.6、Better Auth 1.5,连那条错的 lint 说明都原样躺在那儿。改了一份,忘了另一份,这不就漂移了嘛…

在多人协作的仓库里这事会更糟:两个 AI 工具各读各的目录,拿到的上下文根本不是同一份。那个场景我在另一篇里单独写过,这里就不展开了。

约定文件也是代码,会过期。而且没有 CI 会告诉你它过期了…

那知识实际落在哪

回头看 vns-next:没有 .claude/、没有 .cursor/、没有 .github/、没有 docs/、没有任务清单也没有规格文档。但它一点都不缺上下文。

因为知识全写在代码注释和 commit body 里了呢~

next.config.tstrailingSlash 那一段,把「为什么要」和「为什么还要关掉内置的」一起写死了:

// 与旧站(Hugo)URL 完全一致:尾斜杠必须保留,
// Artalk/Valine 历史评论按 /p/{id}/ 路径存储,去掉斜杠会丢评论
trailingSlash: true,
// 但内置的尾斜杠 308 会把 POST /api/auth/* 重定向成 /api/auth/*/ 导致 404(Better Auth 全挂),
// 故关掉内置重定向,由 src/proxy.ts 只对页面 GET 请求补尾斜杠
skipTrailingSlashRedirect: true,

src/proxy.ts:21-23 是一条纯警告,写给下一个想「优化」这段代码的人(大概率是 AI):

// 注意:不能用 nextUrl.clone() 再改 pathname——NextURL 的 setter 会按
// trailingSlash 配置把尾斜杠再规范化掉,Location 指回自身造成死循环。
// 官方 skipTrailingSlashRedirect 示例即用裸 new URL() 构造。

src/lib/cache.ts:1-5 写明了这个文件将来会怎么被替换:

/**
 * 内存 TTL 缓存 helper
 *
 * 接口对齐 acgn-flow 的 redis helper(getCache/setCache/deleteCache/deleteCachePattern),
 * 将来若要换 ioredis 可以无痛替换本文件实现,调用方不需要改动。
 */

commit b07e20d 的 message 则连验证手段都写了——这个坑本身(构建期没有数据库)我在另一篇里细说过,这里只看它往 commit body 里塞了什么:

fix(deploy): 全站 force-dynamic——Docker 构建环境无数据库,静态预渲染必然失败

根 layout 声明 dynamic='force-dynamic'(查库页面请求时渲染,内存缓存兜底),
sitemap/feed.xml/llms.txt/index.xml 同步声明。已用死库 DATABASE_URL 本地复现
构建通过。

这些位置的共同点是:下次一定会被读到。改 proxy.ts 的人必然会看到那条死循环警告,看 next.config.ts 的人必然会看到尾斜杠的来龙去脉。而 docs/architecture.md 会不会被打开,全看运气啦…

我现在的分法

内容类型放哪使用频率
框架版本差异与 API 陷阱CLAUDE.md★★★★★
某一行代码为什么这么写代码注释★★★★★
某次改动的动机与验证手段commit body★★★★
部署与运维步骤单独一份 README★★★
项目介绍、目录结构别写,会过期

只有第一类是模型不可能从代码里推出来的,不写就是不知道。至于「本项目采用 App Router,目录结构如下……」这种,模型 ls 一下就有了呢~

顺便自嘲一下 README

说到会过期的东西。vns-next 的 README 至今是 create-next-app 的默认模板,36 行,讲 npm/yarn/pnpm/bun run dev 和 Deploy on Vercel,从初始 commit 85aa812 起没改过一个字。

而这个项目实际是 bun + Docker Compose 自托管的,部署压根没走 Vercel(next.config.ts 里那个 VERCEL 分支只是留个后路)。README 和现实完全脱节…

不过 .dockerignore:13-19 是这么写的:

# 内容导出快照 / 文档 / 部署自身
content-export
deploy
README.md
AGENTS.md
CLAUDE.md
.claude

README、AGENTS.mdCLAUDE.md.claude 一起被排除出了镜像上下文(.claude 是顺手防着的,这项目实际压根没建过那个目录哦)。所以我干脆懒得改了,反正它也进不了镜像…

顺带一提,那 13 个 commit 里有 12 个带 Co-Authored-By: Claude Opus 4.8 的 trailer,唯一没有的是 create-next-app 生成的初始 commit。全部 commit 的 author 都是我自己——署名归署名,锅还是我的啦~

反正我下个项目大概还是只写一行,等踩到坑再往里加就行~