一场持续近一年的拉锯,昨天结束了
先说昨天的新闻:Claude Code 2.1.277 开始支持 AGENTS.md(9 月 18 日)。
The Register 的标题起得很直接——“Anthropic 决定支持 OpenAI 的 markdown 指令规范”。
如果你不是天天泡在 AI 编程工具里,可能会觉得:一个配置文件名,至于吗?
至于。因为这场拉锯的起点,是 Shopify CEO 公开放的一句狠话。
时间倒回 8 月 26 日。Shopify CEO Tobi Lütke 在 X 上表态:
我正考虑在 Shopify 禁用 Claude Code,直到他们愿意读取 AGENTS.md、.agents/skills 等文件。
一家电商巨头,威胁要全公司禁用一款主流 AI 编程工具——原因只是它不肯读一个 markdown 文件。
而他给出的理由,我觉得比新闻本身更有价值:
AGENTS.md 和 CLAUDE.md 都会沿目录树递归生效。数千名开发者维护一个 monorepo,难免有目录漏掉一份文件,结果就是一部分开发者用上"被切掉一块脑子"的智能体。
我们用自动化解决了问题,但这是笔愚蠢的复杂度税,根本不该付。
“被切掉一块脑子的智能体”——这个说法,凡是维护过大型仓库的人应该都会心一沉。
先把 AGENTS.md 说清楚
AGENTS.md 的官方定义只有一句话:
Think of AGENTS.md as a README for agents.
给 agent 看的 README。
README 给人看——怎么快速上手、项目是做什么的。而 AGENTS.md 装的是 agent 需要、但不适合塞进 README 的东西:构建步骤、测试怎么跑、代码约定、几条铁律。
它现在的地位比很多人想的高:
- 已被 6 万+ 开源项目采用
- 起源于多家协作:OpenAI Codex、Amp、Google 的 Jules、Cursor、Factory
- 现在由 Linux 基金会旗下的 Agentic AI Foundation 托管
官方给的最小示例长这样:
# AGENTS.md
## Setup commands
- Install deps: `pnpm install`
- Start dev server: `pnpm dev`
- Run tests: `pnpm test`
## Code style
- TypeScript strict mode
- Single quotes, no semicolons
就这么简单。 没有 schema,没有特殊语法——就是一份 markdown。
而它真正好用的地方在于:一份文件跨多个 agent 通用。Codex、Cursor、Jules、Copilot、Claude Code 都读它,你不用为每个工具维护一份配置。
顺带一提,Claude Code 之前坚持用自己的
CLAUDE.md。开发者从 2025 年 8 月就开始提需求,相关 issue 攒了近 5000 个 GitHub 点赞,最后被关了,回复是"不予修复"。直到 Shopify 威胁禁用,才等来了 9 月 18 日的支持。
为什么"一份文件"会升级成平台工程问题
很多人会想:不就是让 Claude Code 多读一个文件吗,为什么 Shopify 要上升到"全公司禁用"?
因为在一个几千人的 monorepo 里,一个文件格式差异会变成失控。
Lütke 说的那个机制值得抄下来:
monorepo/
├── AGENTS.md ← 全局规则(技术栈、提交规范)
├── backend/
│ └── AGENTS.md ← 后端特有(不要动 db/migrations)
├── mobile/
│ └── AGENTS.md ← 移动端特有(打包、签名)
└── infra/
└── AGENTS.md ← 基础设施特有
AGENTS.md 是"就近生效"的——agent 读到当前目录最近的那份,继承上层的规则。
问题就出在这儿:
- 假设团队同时用 Cursor(读 AGENTS.md)和 Claude Code(只读 CLAUDE.md)
mobile/目录下写了AGENTS.md,但没人记得再写一份CLAUDE.md- 用 Cursor 的开发者:agent 知道"打包前要跑签名脚本"
- 用 Claude Code 的开发者:agent 不知道
同一份代码,同一个任务,两个 agent 给出的结果不一样。 而这区别不会被任何报警发现——你只是偶尔发现某个同事的 AI"特别笨"。
这就是 Lütke 说的"复杂度税":不是不能解决,而是这个问题根本不该存在。
落到实操:一个 KMP + AI Chat 客户端怎么写
好了,新闻讲完,接下来是正题。
假设我们在做的是一个 Kotlin Multiplatform + Compose Multiplatform 的 AI Chat 客户端——一套 Kotlin 代码,跑 Android 和 iOS。
这种项目写 AGENTS.md 有它的特殊性:同一个仓库里,三种代码的规则完全不同。
先看目录树:
ai-chat/
├── AGENTS.md # 根:全局铁律 + 通用命令
├── settings.gradle.kts
├── gradle/
│ └── libs.versions.toml # 版本目录(依赖版本唯一来源)
├── shared/ # 共享业务逻辑(无 UI)
│ ├── AGENTS.md # 这一层的规矩
│ ├── build.gradle.kts
│ └── src/
│ ├── commonMain/kotlin/ # ← 三端共享
│ ├── androidMain/kotlin/ # ← Android 实现
│ ├── iosMain/kotlin/ # ← iOS 实现
│ └── commonTest/kotlin/
├── composeApp/ # Compose Multiplatform UI
│ ├── AGENTS.md
│ └── src/
│ ├── commonMain/
│ ├── androidMain/
│ └── iosMain/
├── androidApp/ # Android 壳工程
│ ├── AGENTS.md
│ └── src/main/AndroidManifest.xml
└── iosApp/ # iOS 壳工程(Xcode)
├── AGENTS.md
└── iosApp.xcodeproj
注意:这个结构本身就是给 agent 的第一层信息。 如果 agent 分不清 shared/ 和 composeApp/ 的区别,它就会把 UI 代码写到逻辑层里。
根 AGENTS.md:只写"全局铁律"和"通用命令"
# AGENTS.md
KMP + Compose Multiplatform 的 AI Chat 客户端。
一套 Kotlin 代码,编译到 Android / iOS。
## 项目结构
- `shared/` — 共享业务逻辑,**不含任何 UI**
- `composeApp/` — Compose Multiplatform UI(三端共用同一套界面)
- `androidApp/` — Android 壳(Activity / Application / manifest)
- `iosApp/` — iOS 壳(Xcode 工程 / SwiftUI 入口)
## 构建与测试
- 全量编译: `./gradlew build`
- 共享层编译: `./gradlew :shared:compileKotlinMetadata`
- Android 装到机:`./gradlew :androidApp:installDebug`
- 跑全部测试: `./gradlew allTests`
- 只跑一个类: `./gradlew :shared:allTests --tests "*ChatRepository*"`
- iOS 框架: `./gradlew :shared:linkDebugFrameworkIosSimulatorArm64`
## 铁律(违反会导致三端编译失败)
1. `commonMain` 里**禁止**出现 `java.*`、`android.*`、`platform.*`
2. 平台差异一律走 `expect` / `actual`,不要写 `if (isAndroid)`
3. **不要为 iOS 复制一份逻辑** —— 那就失去了用 KMP 的意义
4. 新依赖先加到 `gradle/libs.versions.toml`,模块里只引别名,**不要写死版本号**
5. 改 `commonMain` 之后,必须能同时编译 Android 和 iOS
这里有几个写法上的讲究,我认为很关键:
① 命令要"可复制执行",不要描述。
写 ./gradlew :shared:compileKotlinMetadata,不要写"编译一下共享模块"。agent 拿到能直接跑的命令,才不用猜。
② 铁律要写"违反的后果"。 “禁止在 commonMain 用 java.*” 是个规定;但加上"会导致三端编译失败",agent 才知道这是硬约束,不是风格偏好。
③ 用"唯一来源"消灭歧义。
“版本号只写在 libs.versions.toml"——这一句能让 agent 不再到处硬编码版本。
shared/AGENTS.md:只写"这一层特有的”
子目录的 AGENTS.md 不该重复根目录的内容,只写"进到这个目录才需要知道的事":
# shared/ — 共享业务逻辑
## 三端目录的职责
- `commonMain/` — 平台无关逻辑。要用平台能力,在这里先 `expect` 声明
- `androidMain/` — `expect` 的 Android 实现(用 Android SDK / JVM API)
- `iosMain/` — `expect` 的 iOS 实现(用 Kotlin/Native + Foundation)
- **每加一个 `expect`,必须同时补齐 androidMain 和 iosMain 两份 `actual`**
## 目录约定
- `data/` — Repository、数据源、DTO
- `domain/` — 领域模型、UseCase
- `platform/` — expect/actual 的平台抽象
(KeyValueStore、HttpEngine、AppInfo…)
## 网络(AI Chat 的核心)
- 统一用 **Ktor**,禁止在共享层直接用 OkHttp 或 URLSession
- 流式响应统一返回 `Flow<ChatChunk>`,不要在 shared 里做 UI 状态管理
- 超时、重试、错误映射统一在 `data/remote/` 里处理
## 命令
- 测试: `./gradlew :shared:allTests`
- iOS 框架: `./gradlew :shared:linkDebugFrameworkIosSimulatorArm64`
看见没,这一层装的全是"KMP 特有"的知识:
expect/actual的配对规则(这是 KMP 最容易出错的地方)- 网络层为什么必须用 Ktor(因为 OkHttp 在 iOS 上不存在)
- 流式响应为什么用
Flow(AI Chat 的核心特征)
这些是通用 README 里不会写、但 agent 每次都会踩的东西。
三端壳工程:各自写"别乱碰什么"
androidApp/AGENTS.md:
# androidApp/ — Android 壳
## 这里只放什么
- Activity、Application、AndroidManifest.xml、res/ 资源
- **业务逻辑一律放 shared/,UI 一律放 composeApp/**
## 常用命令
- 装机: `./gradlew :androidApp:installDebug`
- 看日志: `adb logcat -s AiChat:V`
## 注意
- 新增权限必须改 AndroidManifest.xml,并**同步更新本文件**
- 改 minSdk 前先确认 shared/ 里用到的 API 兼容性
iosApp/AGENTS.md:
# iosApp/ — iOS 壳(Xcode 工程)
## 重要:这里不用 Gradle 构建
- 这是 Xcode 工程,**不要尝试用 gradlew 构建它**
- 改了 shared/ 的代码后,先跑:
`./gradlew :shared:linkDebugFrameworkIosSimulatorArm64`
让 Xcode 拿到新框架,再去 Xcode 里 Run
## Swift 侧只做桥接
- 生命周期、权限申请、导航容器
- 业务逻辑**全部回 shared**,不要在 Swift 里重写一遍
## 常见坑
- Info.plist 缺权限描述:模拟器运行才报错,Xcode 不提示
- 改了 framework 的 export 后要 Clean Build Folder
- Kotlin 的 sealed class 在 Swift 侧要遍历所有分支(写全,别用 default)
为什么这三份要分开写?
因为**“该做什么"和"不该做什么"在不同目录里完全相反**:
- 在
shared/,你要写"不要碰平台 API” - 在
androidApp/,你要写"不要写业务逻辑" - 在
iosApp/,你要写"不要用 Gradle 构建这个目录"
同一个仓库,规则互相冲突。 一份根 AGENTS.md 写不下这些——这就是"就近生效"存在的意义。
顺带一提:如果团队里工具是混用的
AGENTS.md 只是"约定",不同工具读它的方式仍有差异。官方给了一些配置参考:
- Aider:在
.aider.conf.yml里加一行read: AGENTS.md - Gemini CLI:在
.gemini/settings.json里配置{ "context": { "fileName": "AGENTS.md" } } - 老文件迁移:
mv AGENT.md AGENTS.md && ln -s AGENTS.md AGENT.md(软链保留兼容)
放到 KMP 项目里,还有一种情况是工具版本不齐——比如有人的 Claude Code 还没升到 2.1.277。这时候可以在关键目录放一个 CLAUDE.md -> AGENTS.md 的软链顶上。
但请记住 Lütke 那句话:这就是在付"复杂度税"。
能统一升级工具,就别维护两份文件——软链是过渡方案,不是终局。
几个我踩过的坑
① 别把 AGENTS.md 写成 README 的翻译版。 README 是给人看的"项目介绍",AGENTS.md 是给 agent 的"操作手册"。区别在于:README 讲 what,AGENTS.md 讲 how,而且要能直接执行。
② 别写"应该是"“建议"“尽量”。 agent 对模糊表述的处理方式是不可预测的。要么是硬约束(写清后果),要么就不要写。
③ 别让它过期。 过期的命令比没有命令更糟——agent 会照着跑,然后卡在一个已经不存在的任务上。改构建脚本时顺手改 AGENTS.md,应该成为习惯。
④ 别贪多。 官方那个最小示例就十几行,够用了。上下文是有预算的,你写给 agent 的每一行,都在挤占它理解代码的空间。
⑤ 目录树画出来。
这个投入产出比最高。一张准确的目录树,能省掉 agent 十次 ls。
我的看法
第一,AGENTS.md 的胜负,本质是"平台中立"对"工具绑定"的胜利。
Anthropic 拖了近一年才支持,最后还是支持了。原因很简单:当 6 万个项目用同一个格式,你不支持,就是在给用户加税。
Shopify 那句"复杂度税”,戳中的是所有大仓库的真实痛点。
第二,对 KMP 项目来说,AGENTS.md 的价值可能比对普通项目更高。
因为 KMP 的坑特别"目录相关":
- 这段代码在
commonMain还是iosMain?规则完全不同 - 这个 API 三端都有吗?
- 改完要不要重新 link iOS 框架?
这些判断,如果每次都靠人在对话里说一遍,成本很高;写进目录级的 AGENTS.md,就变成了一次性投入。
第三,这件事的长期意义可能比看起来大。
当 AI 编程工具从"补全代码"走向"独立完成任务",“仓库里有没有一份说清楚规矩的文件”,会越来越像"这个项目有没有自动化测试"——是工程成熟度的标志之一。
而且写它还有个附带好处:写 AGENTS.md 的过程,本身就是一次团队规范的梳理。 你会被迫把那些"只有老员工才知道"的隐含规则写下来——这件事的价值,可能比让 AI 聪明一点更大。
想问问你:
你现在的项目里有 AGENTS.md 吗? 如果还没有,你觉得最难写的是哪部分——是构建命令,还是那些"只可意会"的约定?
或者你在 KMP 项目里踩过什么坑,后来靠文档解决了? 评论区聊聊,我可以整理成下一篇。
参考资料:AGENTS.md 官方规范(agents.md)、The Register(9/18)、36Kr / CSDN 关于 Shopify 与 Anthropic 争议的报道(8/26)、Claude Code 2.1.277 更新说明。
