Next.js 16 会往你项目里写一个文件,专门写给 AI 看
next dev 检测到 AI 编程工具在运行时,会自动生成并维护 AGENTS.md,指向随包安装的 4.1MB 官方文档。这是框架对抗模型训练数据滞后的一次正面尝试。
用 create-next-app@16 初始化项目,你会在根目录看到一个陌生的文件:AGENTS.md 。打开只有七行:
<!-- 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 -->
翻译过来:这不是你认识的那个 Next.js,写代码前先去读 node_modules 里的文档。
这句话不是写给你的,是写给 AI 的。
它是怎么出现的
把它删掉,跑一次 pnpm dev ,它会回来。
源码在 node_modules/next/dist/server/lib/generate-agent-files.js ,触发点在 start-server.js :
if (isDev) {
// Gated on `agentRules` in next.config (default true).
if (initResult.agentRules !== false) {
const result = await ensureAgentRulesForDev(dir)
// ...
}
}
只有 next dev 会写文件,next build 和 next start 不碰。开关是 next.config.ts 顶层的 agentRules ,默认开启(判断写的是 !== false ,不配置即生效)。
再往里一层,是两道闸门:
async function ensureAgentRulesForDev(dir) {
if (await getAgentName() === null) return null
if (hasCurrentAgentRules(dir)) return null
return writeAgentFiles(dir)
}
第一道:判断是不是 AI 在跑。 实现在 @vercel/detect-agent ,纯粹是读环境变量,按顺序短路返回:
| 环境变量 | 判定 |
|---|---|
CURSOR_TRACE_ID | cursor |
CLAUDECODE / CLAUDE_CODE | claude |
CODEX_SANDBOX / CODEX_THREAD_ID | codex |
GEMINI_CLI | gemini |
COPILOT_MODEL | github-copilot |
AI_AGENT | 通用逃生口,值即 agent 名 |
你自己开终端跑 pnpm dev ,这些变量全是空的,什么都不会发生。只有 AI 工具在跑,它才动手。
第二道:判断块是不是过期了。 比对方式是整块逐字符相等,不是「有没有这个标记」。所以 Next.js 一升级、文案一改,旧块立刻失配,进入更新流程。
写进哪个文件,有一套优先级
if (agentsMdExists && (agentsMdHostsBlock || !claudeMdHostsBlock)) {
return { agentsMd: upsertFile(agentsMdPath, block), claudeMd: 'skipped' }
}
if (claudeMdExists) {
return { agentsMd: 'skipped', claudeMd: upsertFile(claudeMdPath, block) }
}
// Neither file exists — scaffold both, matching create-next-app.
fs.writeFileSync(agentsMdPath, block + '\n', 'utf-8')
fs.writeFileSync(claudeMdPath, CLAUDE_MD_CONTENT, 'utf-8') // 内容只有一行:@AGENTS.md
| 现状 | 结果 |
|---|---|
有 AGENTS.md ,块在它里面 | 更新 AGENTS.md |
有 AGENTS.md ,块在 CLAUDE.md 里 | 尊重现状,更新 CLAUDE.md |
只有 CLAUDE.md | 写进 CLAUDE.md |
| 两个都没有 | 建 AGENTS.md 放块,再建一个只有 @AGENTS.md 一行的 CLAUDE.md |
最后一支有个坑:那个 CLAUDE.md 是 writeFileSync 全量覆盖。如果你自己写了一份 CLAUDE.md 项目规范,又恰好没有 AGENTS.md ,跑一次 next dev ,你的规范会被覆盖成一行 @AGENTS.md 。
我初始化项目时为了保住自己的 CLAUDE.md ,先把它挪走再跑脚手架,事后再覆盖回来 —— 如果当时连 AGENTS.md 一起弄丢,就正好撞上这一支。结论:这两个文件任何时候都别同时缺席。
更新块的方式很克制
const startIdx = existing.indexOf(AGENT_RULES_START_MARKER)
const endIdx = existing.indexOf(AGENT_RULES_END_MARKER)
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
return existing.slice(0, startIdx) + normalizedBlock
+ existing.slice(endIdx + AGENT_RULES_END_MARKER.length)
}
切片替换,只动两个 marker 之间。你在块的上下写自己的内容是安全的。另外:
- 内容完全相同就返回
'unchanged'且不写盘,不会平白刷新 mtime 触发 dev server 的文件监听 - 先检测文件本身的换行风格(有
\r\n就按 CRLF),块跟随文件,避免 Windows 上反复重写 - 有个
while(true)循环清理旧版本 codemod 留下的 marker,防止升级后新旧两块并存
真正的货在 node_modules/next/dist/docs/
AGENTS.md 只是个路标,它指向的才是重点:随 Next.js 一起安装的 456 个官方文档 md 文件,4.1 MB,与安装版本严格对应。
官方愿意让每次安装多付 4MB,动机很直接:模型的训练数据会过期,node_modules 不会。
Next.js 16 相对 14/15 的破坏性变更密度太高了。我按这个约定查了一个问题 —— PPR 在 16 里是什么状态 —— 结果和网上大多数文章都不一样:
grep -rli "partial prerender" node_modules/next/dist/docs/
cacheComponents.md 里写得很清楚:
Additionally,
cacheComponentsimplements Partial Prerendering (PPR) as the default behavior in the App Router. This means theexperimental.pprconfiguration flag and theexperimental_pprroute segment configuration are no longer necessary and have been removed.
也就是说:PPR 本身不再是实验特性,但它被并进了 Cache Components;cacheComponents: true 仍需手动开启,不是默认值;不开则沿用旧缓存模型。
如果我凭记忆写「PPR 还是 experimental,要配 experimental.ppr」,或者反过来说「16 里 PPR 默认开了」——两种说法都错,而且都是很容易脱口而出的错。
值得注意的一个副作用
同一个 agent 检测结果还会进遥测。node_modules/next/dist/telemetry/anonymous-meta.js :
{
// ...
isCI: isCI,
nextVersion: '16.3.5',
agentName: await getAgentName(),
}
Vercel 在统计「有多少 Next.js 项目是 AI 写的」。介意的话 next telemetry disable 关的是这个,和 agentRules 是两回事 —— 后者管写文件,前者管上报。
我的看法
这个设计解决的是一个真问题:框架迭代速度已经超过了模型训练数据的更新速度。 与其指望模型记住每个版本,不如把版本正确的事实源放在它一定会读到的地方。
有意思的是实现方式的克制 —— 有环境检测、有幂等、有 marker 隔离、有换行符处理、有开关,但整个文件不到 200 行。比起在文档站上呼吁「请 AI 先读文档」,往 node_modules 里塞 4MB 然后在项目根放个路标,务实得多。