写在前面
最近翻 Now in Android 仓库的时候,注意到根目录多了一个 AGENTS.md。
这个文件很容易被忽略------长得太像 README 了,都是 Markdown。但它的读者不是人,是 Claude Code、Codex、Gemini in Android Studio 这类 AI Coding Agent。换句话说,README 是给我们看的,AGENTS.md 是给 AI 看的。
那为什么需要这么一个文件?
因为 AI 写代码,很多时候不是不会写,而是不知道你项目里的约定。它知道 Compose,知道 Hilt,知道 Gradle,也知道 Navigation。但它不知道 NIA 里面 feature 是怎么拆的,不知道当前导航到底是 Navigation 2 还是 Navigation 3,更不知道截图测试的 baseline 能不能从本机提交。
这些东西如果不写出来,它就只能猜。
这篇文章就不泛泛讲 AGENTS.md 是什么了,直接拿 NIA 这个项目开刀。 
一、先看一下 NIA 的模块结构
Now in Android 是一个很适合讲 AGENTS.md 的项目。它不是 demo,模块拆分很完整。
看 settings.gradle.kts,项目里有这些模块:
text
:app
:app-nia-catalog
:benchmarks
:core:analytics
:core:common
:core:data
:core:database
:core:datastore
:core:designsystem
:core:domain
:core:model
:core:navigation
:core:network
:core:notifications
:core:testing
:core:ui
...
:feature:foryou:api
:feature:foryou:impl
:feature:interests:api
:feature:interests:impl
:feature:bookmarks:api
:feature:bookmarks:impl
:feature:topic:api
:feature:topic:impl
:feature:search:api
:feature:search:impl
:feature:settings:impl
这里面最值得说的是 feature 的拆法。
NIA 不是一 feature 一 module。大部分 feature 都拆成了 api 和 impl 两个子模块。拿 For You 举例:
text
feature/foryou/
api/
impl/
api 放对外契约,比如导航用的 NavKey。impl 放真正的实现------Screen、ViewModel、entryProvider。
这个约定对我们来说看几次目录就明白了,但 Agent 不一定。你让它新增一个页面,如果它不知道这个拆法,大概率把 Screen、ViewModel、NavKey 全写到一个地方。
所以 AGENTS.md 里面最应该先写的,不是"本项目使用 Kotlin + Compose",而是这类项目边界。比如:
markdown
Feature modules are split into `api` and `impl`.
- `feature:*:api` owns public navigation contracts, such as `NavKey`.
- `feature:*:impl` owns Screen, ViewModel and entryProvider.
- Feature `api` modules should not depend on other feature modules.
- Feature `impl` modules may depend on another feature's `api`, but not `impl`.
这种内容对 Agent 才是有用的。它不需要你教它什么是 Compose,它需要知道的是 NIA 里 Compose 代码应该放在哪里。

二、README 都有了,还要 AGENTS.md 干嘛?
NIA 的 README 写得已经够详细了------项目用途、开发环境、build variant、测试、截图测试、Baseline Profile、Compose compiler metrics,全都有。
那 AGENTS.md 是不是重复了?
我觉得不是。README 是给开发者看的学习路线,AGENTS.md 是给 Agent 看的操作手册。
举个例子。README 里会解释:
demoflavor 用本地静态数据prodflavor 用真实后端- 日常开发用
demoDebug - UI 性能测试用
demoRelease
对开发者来说这些描述很友好。但 Agent 执行任务的时候,更想看到的是直接的命令:
markdown
- Build normal development variant: `./gradlew assembleDemoDebug`
- Run local tests: `./gradlew testDemoDebug`
- Run screenshot tests: `./gradlew verifyRoborazziDemoDebug`
- Do not run `./gradlew test`, because it runs all variants.
这个区别很关键。
README 可以解释背景,AGENTS.md 要减少 Agent 的选择空间。尤其 Android 项目,variant 一多,命令写模糊就很容易跑错。NIA 的 README 里专门提醒过,不要跑 ./gradlew test 或 ./gradlew connectedAndroidTest,因为会跑所有 build variants,而当前只有 demoDebug 支持测试。这种话就非常适合进 AGENTS.md。
三、当前 NIA 的 AGENTS.md 写了什么?
NIA 根目录现在同时有 AGENTS.md 和 AGENT.md。AGENT.md 实际上只是指向 AGENTS.md 的兼容入口------这也跟 Android Studio 的历史有关:Narwhal 3 Feature Drop 用 AGENT.md,Narwhal 4 Feature Drop Canary 4+ 开始支持 AGENTS.md。
看当前内容,主要写了这些:
- 项目是 Kotlin Android app
- UI 用 Compose + Material 3
- 状态管理用 UDF + Flow
- DI 用 Hilt
- 数据层用 Repository
- Room、DataStore、Retrofit、OkHttp、WorkManager 各自的职责
- app、feature、core 的目录组织
- 构建、spotless、测试、截图测试的命令
- UI feature 测试用
ComposeTestRule+ComponentActivity - 本地测试可以用 kotlinx.coroutines、Turbine、Truth
- 截图测试由 CI 生成,不要从工作站提交
整体方向是对的,尤其是命令和测试边界,对 Agent 帮助很大。
但是我读到 Navigation 那一段的时候,发现了一个很典型的问题。
四、AGENTS.md 还停在 Navigation 2
当前 NIA 的 AGENTS.md 里写着:
Navigation is handled by Jetpack Navigation 2 for Compose
但看代码,项目已经不是这个结构了。
app/build.gradle.kts 里已经有 Navigation 3 相关依赖:
kotlin
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.lifecycle.viewModel.navigation3)
再看 NiaApp.kt,导航入口已经是 Navigation 3 的写法。核心代码大概是这样:
kotlin
val entryProvider = entryProvider {
forYouEntry(navigator)
bookmarksEntry(navigator)
interestsEntry(navigator)
topicEntry(navigator)
searchEntry(navigator)
}
NavDisplay(
entries = appState.navigationState.toEntries(entryProvider),
sceneStrategy = listDetailStrategy,
onBack = { navigator.goBack() },
)

这跟之前 Navigation 2 的 NavHost + composable(...) 已经不是一个思路了。
现在 NIA 的导航分工大概是这样:
text
feature/*/api
定义 NavKey
feature/*/impl
定义 EntryProviderScope<NavKey>.xxxEntry(...)
core:navigation
定义 Navigator 和 NavigationState
app
把所有 feature entry 组合起来,交给 NavDisplay
拿 Topic 页面看一下。
feature/topic/api/.../TopicNavKey.kt:
kotlin
@Serializable
data class TopicNavKey(val id: String) : NavKey
fun Navigator.navigateToTopic(
topicId: String,
) {
navigate(TopicNavKey(topicId))
}
feature/topic/impl/.../TopicEntryProvider.kt:
kotlin
fun EntryProviderScope<NavKey>.topicEntry(navigator: Navigator) {
entry<TopicNavKey>(
metadata = ListDetailSceneStrategy.detailPane(),
) { key ->
val id = key.id
TopicScreen(
showBackButton = true,
onBackClick = { navigator.goBack() },
onTopicClick = navigator::navigateToTopic,
viewModel = hiltViewModel<TopicViewModel, Factory>(
key = id,
) { factory ->
factory.create(id)
},
)
}
}
然后在 NiaApp.kt 里注册:
kotlin
val entryProvider = entryProvider {
forYouEntry(navigator)
bookmarksEntry(navigator)
interestsEntry(navigator)
topicEntry(navigator)
searchEntry(navigator)
}
看到这里就很清楚了。当前项目需要的不是 route string,不是改一个 NavHost,而是新增 NavKey、entryProvider,并在 app 层注册 entry。
所以这里有个很现实的问题:AGENTS.md 还写着 Navigation 2,Agent 很可能会被带偏。
这也是我觉得 AGENTS.md 最需要注意的地方。它不是写完就没事了,项目架构变了,它也要跟着变。错误的 AGENTS.md 比没有 AGENTS.md 更麻烦,因为它给了 Agent 一个错误前提。

五、这一段应该怎么改?
如果让我来改,导航部分我会写得更具体:
markdown
## Navigation
This project uses Navigation 3.
- Navigation keys are `NavKey` implementations.
- Feature `api` modules own public navigation keys.
- Feature `impl` modules own `EntryProviderScope<NavKey>.xxxEntry(...)` functions.
- `core:navigation` owns `Navigator` and `NavigationState`.
- `app` composes all feature entries in `NiaApp`.
When adding a destination:
1. Add a `NavKey` in the feature `api` module.
2. Add an entry provider in the feature `impl` module.
3. Add navigation helper functions on `Navigator` if another feature needs to navigate to it.
4. Register the entry in `NiaApp`.
Do not add Navigation 2 `NavHost` / route-string based destinations.
最后一句我觉得非常关键。
很多 Agent 会优先生成训练数据里更常见的写法。Navigation 2 的资料明显比 Navigation 3 多,如果不明确禁止,它很自然就会往旧方案上靠。
所以 AGENTS.md 不能只写"当前怎么做",也要写"不要回到哪种旧做法"。
六、回到工程实际:Agent 最容易在哪些地方返工?
单看 AGENTS.md 很容易把它写成一份规则清单。但从 NIA 这种项目来看,我觉得更应该反过来想:哪些地方最容易让 Agent 返工?
6.1 测试命令跑错
Android 项目和后端项目不太一样,很多命令跟 variant 绑定。NIA 就是典型:日常开发是 demoDebug,性能相关又有 demoRelease 和 benchmark。
如果 AGENTS.md 只写一句:
markdown
Run tests with Gradle.
基本等于没写。Agent 很可能直接跑 ./gradlew test,然后开始等一个很大、很慢、本来就不该跑的任务。
NIA 的 README 里已经提醒过,不要跑 ./gradlew test 或 ./gradlew connectedAndroidTest,因为会跑所有 build variants,而当前主要支持的是 demoDebug 这条测试路径。
更工程化的写法是把常用命令写死:
markdown
- Run local tests: `./gradlew testDemoDebug`
- Run instrumented tests: `./gradlew connectedDemoDebugAndroidTest`
- Run screenshot tests: `./gradlew verifyRoborazziDemoDebug`
- Compare screenshot failures: `./gradlew compareRoborazziDemoDebug`
- Do not run `./gradlew test` or `./gradlew connectedAndroidTest`.
这不是洁癖,是省时间。Agent 跑错命令以后,很容易基于错误结果继续修,最后变成一串没必要的改动。
6.2 测试风格写错
NIA 的测试有个很明显的倾向:不用 mocking libraries,用 test doubles。
README 里解释得很清楚,很多生产实现会在测试里替换成 test repository 或 test implementation。这些 test doubles 实现同样的接口,同时提供测试 hook。这样测试不是只验证某个 mock 方法有没有被调用,而是能跑到更多真实逻辑。
这个习惯如果不写,Agent 很容易顺手加 Mockito 或者按常见 mock 写法补测试。代码可能能跑,但风格跟项目就不一致了。
AGENTS.md 里可以写:
markdown
- Do not introduce mocking libraries.
- Prefer test doubles over mocks.
- ViewModel tests should use test repositories when available.
- Coroutine tests may use kotlinx.coroutines test utilities and Turbine.
这类内容不是"测试规范"那么简单。它实际是在告诉 Agent:NIA 更相信接口替换和真实数据流,而不是到处 mock。
6.3 截图测试 baseline 被随手更新
NIA 用 Roborazzi 做截图测试。截图测试最麻烦的地方大家都懂:不同系统、字体、渲染环境可能有细微差异。
README 里专门说了,仓库里的正确截图是在 Linux CI 上生成的。本机------尤其 macOS------可能有差异。所以如果 Agent 看到截图测试失败,然后直接跑 recordRoborazziDemoDebug 更新 baseline,这个操作就很危险。
AGENTS.md 需要写的不是"项目使用 Roborazzi",而是边界:
markdown
- Use `verifyRoborazziDemoDebug` to verify screenshots.
- Use `compareRoborazziDemoDebug` to inspect failures.
- Do not commit screenshot baselines generated from a workstation unless explicitly requested.
这句话其实是在保护 review。截图 baseline 一更新,diff 里看起来就是几张图片变了,但背后代表 UI 行为已经被接受。这个决策不能让 Agent 自己默默做。
6.4 生成文件被手改
Baseline Profile 也类似。
NIA 的 baseline profile 在:
text
app/src/main/baseline-prof.txt
它不是手写配置,是通过 benchmarks 模块里的 benchmark test 生成的。README 里也说了,如果 release 构建触及启动路径相关代码,需要考虑重新生成。
所以 AGENTS.md 里可以写:
markdown
- `app/src/main/baseline-prof.txt` is generated from benchmark tests.
- Do not hand-edit baseline profile rules.
- If startup-critical code changes, mention that baseline profile regeneration may be required.
不是说 Agent 永远不能改它,而是告诉 Agent:这类文件有来源,不能为了让 diff 看起来合理就手写几行。
七、从 NIA 推出来的 AGENTS.md 实践建议
讲到这里再看所谓最佳实践,就不应该是"写清楚项目结构"这种空话了。结合 NIA 这种项目,我觉得 AGENTS.md 至少要覆盖下面几类。
7.1 写项目专属规则,不写通用技术科普
"本项目使用 Jetpack Compose"当然可以写,但意义有限。
更有价值的是:
markdown
- UI only uses Jetpack Compose. Do not add XML layouts.
- Feature `api` modules expose NavKey only.
- Feature `impl` modules own Screen, ViewModel and entryProvider.
- Shared UI should go to `core:ui` or `core:designsystem`, not another feature.
Agent 知道 Compose 是什么,不用再教。真正需要告诉它的是:NIA 里 Compose 代码怎么组织。
7.2 命令写到能直接复制执行
不要写:
markdown
Run unit tests.
写:
markdown
./gradlew testDemoDebug
如果有不能跑的命令,也写出来:
markdown
Do not run `./gradlew test`; use `./gradlew testDemoDebug`.
这在 Android 项目里特别重要。variant 错了,后面的验证结果就没意义。
7.3 写"不要做什么"
很多人写 AGENTS.md 只写正向规则,但我觉得负向规则更关键。
拿 NIA 来说:
markdown
- Do not add Navigation 2 `NavHost` / route-string based destinations.
- Do not introduce mocking libraries.
- Do not commit screenshot baselines generated from a workstation.
- Do not hand-edit `app/src/main/baseline-prof.txt`.
Agent 补代码很在行,但它不知道哪些操作在这个项目里属于越界。把这些边界写出来,比写一堆"follow best practices"有用得多。
7.4 架构迁移时别忘了更新 AGENTS.md
NIA 这个 Navigation 2 / Navigation 3 的例子说明了一个问题:AGENTS.md 不是一次性文档。
我觉得比较合理的做法是,在架构类 PR 里顺手检查一下:
markdown
- [ ] If navigation / modularization / testing strategy changes, update AGENTS.md.
- [ ] If build variants or Gradle tasks change, update AGENTS.md.
- [ ] If generated files change ownership, update AGENTS.md boundaries.
这不是形式主义。AGENTS.md 一旦过期,后面所有 Agent 都会拿着旧上下文干活。
7.5 大项目可以分层,不要把根目录写成百科
NIA 这种项目很适合做分层 AGENTS.md。
根目录写全局规则,feature/*/api 写 api 模块约束,feature/*/impl 写实现模块约束,benchmarks 写性能测试和 Baseline Profile 生成规则。
比如:
markdown
# feature/*/api/AGENTS.md
- Navigation keys live here.
- Do not put UI code here.
- Do not put ViewModels here.
- Do not depend on feature implementation modules.
markdown
# benchmarks/AGENTS.md
- Macrobenchmark tests live here.
- Baseline Profile generation is handled here.
- Prefer stable CUJ flows over broad random UI interactions.
比根目录一个大文件舒服多了。不同模块关心的问题不同,Agent 修改哪个目录,就给它更贴近那个目录的上下文。
八、怎么判断 AGENTS.md 写得好不好?
拿 NIA 举例。假设我要让 Agent 新增一个 Events 页面。
如果 AGENTS.md 写得足够清楚,它至少不应该在这些事情上犯错:
- 要不要新建
feature:events:api和feature:events:impl EventsNavKey应该在apiEventsScreen、EventsViewModel、eventsEntry应该在implNiaApp.kt里需要注册eventsEntry(navigator)- 不要按 Navigation 2 的
NavHost写法新增页面 - 本地测试应该跑
testDemoDebug - UI 可见变化要考虑 Roborazzi
- 不要随便更新截图 baseline
这些信息都没写的话,Agent 也许仍然能写出代码,但很可能写成另一个项目的风格。这个差别挺微妙的:它不是不会 Android,是不懂 NIA。
结语
AGENTS.md 本身没什么神秘的,就是一个 Markdown 文件。
但放到 NIA 这种项目里看,它的用处就很明显了。Android 项目里有太多项目级约定------variant、feature 拆分、导航方案、测试方式、截图基准、Baseline Profile------不写出来,Agent 每次都要猜。
不过 NIA 当前这个例子也提醒我们:AGENTS.md 会过期。项目已经迁到 Navigation 3,但文档里还写着 Navigation 2,这就直接影响 Agent 的判断。
所以我觉得 AGENTS.md 不能当成一次性的配置文件,应该当成项目架构的一部分来维护。架构变了,模块变了,测试命令变了,它也跟着变。
以前这些经验可能散落在 README、Wiki、PR 评论,或者团队成员脑子里。现在 AI 编程工具越来越多,把这些经验整理成一份 Agent 能读懂的说明书,我觉得会慢慢变成 Android 项目里很自然的一件事。