上周我让 Claude Code 帮我写一个新模块的 ViewModel。它写得很认真,代码也没有语法错误——但用了 LiveData 而不是 StateFlow,继承了 ViewModel() 而不是用 Hilt 注入,网络请求直接在 viewModelScope 里 try-catch 而不是走我们封装好的 Result 链式调用。
代码能跑吗?能。代码能合进主分支吗?必须全改。
这就是 AI 编码助手在真实项目里最大的尴尬:它写的代码和你项目的代码,不像一个人写的。
上一篇聊了怎么用 AI 从 PRD 生成高质量 Spec。有了好 Spec,AI 生成的代码"方向"不会错——但"风格"和"规范"会错。这一篇就解决这个问题:怎么让 AI 真正理解你的项目,写出和你团队风格一致的代码。
核心是三件事:Rule 定规范,Skill 封任务,上下文工程控输入。
一、Rule:给 AI 一本你团队的编码手册
先说最直接的方案。2026 年主流 AI 编码工具都支持某种形式的"规则文件"——你在项目根目录放一个特定文件,AI 每次生成代码时都会先读它。
三家的方案长这样:
| 工具 | 规则文件 | 加载方式 |
|---|---|---|
| Cursor | .cursor/rules/*.mdc | 按 glob 模式匹配文件路径自动加载 |
| Claude Code | CLAUDE.md | 项目根目录 + 子目录,自动合并 |
| GitHub Copilot | .github/copilot-instructions.md | 单文件全局生效 |
看起来只是"放个文件"这么简单的事,但写好这个文件的差距是巨大的。
1.1 Rule 的两个层次:团队规范 vs 模块约定
我把 Rule 分成两层。第一层是团队规范——放在项目根目录,对所有代码生效。第二层是模块约定——放在特定目录下,只对该模块的代码生效。
为什么要分层?因为你的 app 模块和 network 模块写法不一样。app 层写 ViewModel 用 Hilt 注入,network 层写拦截器就是纯 OkHttp。如果把所有规则揉在一个文件里,AI 在写网络层代码时也会莫名其妙给你加 @HiltViewModel。
来看一个我在实际项目里用的根目录 CLAUDE.md:
# CLAUDE.md
## 项目概览
Android 电商 App,minSdk 26,
targetSdk 35,纯 Kotlin。
架构:MVI + Clean Architecture
(Presentation → Domain → Data)
## 编码规范
- 状态管理:StateFlow,禁止 LiveData
- DI:Hilt,所有 ViewModel 用
@HiltViewModel + @Inject constructor
- 协程:统一走 domain 层
UseCase.invoke(),
ViewModel 不直接调 repository
- 错误处理:sealed class AppResult
不允许裸 try-catch
- 命名:feature 包按功能命名
(cart/、profile/、search/),
不按层命名(viewmodel/、repo/)
## 禁止项
- 禁止 GlobalScope
- 禁止 Dispatchers.IO 硬编码
(用注入的 CoroutineDispatcher)
- 禁止在 Composable 里直接
调 suspend 函数
- 禁止 !! 操作符
然后在 feature/cart/ 目录下放一个模块级的 CLAUDE.md:
# feature/cart/CLAUDE.md
## 购物车模块上下文
- 状态类:CartUiState(在 state/ 下)
- 事件类:CartEvent(sealed interface)
- 本模块使用乐观更新模式:
UI 先更新,失败后回滚
- 价格计算统一走 PriceCalculator
工具类,不要在 ViewModel 里
手写算术逻辑
- 库存校验是异步的,
加入购物车时不阻塞 UI
这样 Claude Code 在帮你写购物车模块代码时,既知道全局规范(用 StateFlow、用 Hilt),也知道这个模块的特殊约定(乐观更新、PriceCalculator)。两层叠加,生成的代码一致性直接上了一个台阶。
1.2 Cursor Rules 的 glob 匹配:更精准但更碎
Cursor 的方案更灵活一些。它用 .cursor/rules/ 目录下的 .mdc 文件,每个文件头部声明一个 glob 模式,匹配到的文件编辑时自动加载对应规则。
# .cursor/rules/viewmodel.mdc
# globs: **/viewmodel/**/*.kt,
# **/*ViewModel.kt
所有 ViewModel 必须:
1. 使用 @HiltViewModel 注解
2. 通过 constructor inject 获取
UseCase,不直接依赖 Repository
3. 暴露 StateFlow<UiState> 给 UI
4. 使用 sealed interface 定义
UiEvent
ViewModel 模板:
@HiltViewModel
class {Feature}ViewModel
@Inject constructor(
private val {xxx}UseCase:
{Xxx}UseCase,
) : ViewModel() {
private val _uiState =
MutableStateFlow(
{Feature}UiState()
)
val uiState =
_uiState.asStateFlow()
}
好处是精准——只有编辑 ViewModel 文件时才加载 ViewModel 规则。坏处也很明显:规则碎片化。一个中型项目可能需要十几个 .mdc 文件(ViewModel、Repository、Composable、UseCase、Test……),维护成本不低。
我个人的判断:如果团队超过5人,用 Cursor Rules 的 glob 模式更好,因为不同人负责不同模块,规则可以各自维护。如果是个人项目或小团队,Claude Code 的分层 CLAUDE.md 够用且更简洁。
1.3 Rule 写作的五个血泪教训
写了大半年 Rule,踩了不少坑,总结几条最痛的:
教训一:Rule 里要写"不要做什么",不只是"要做什么"。我最开始只写了"用 StateFlow",但 AI 还是会在某些场景里偷偷用 LiveData——因为它的训练数据里 LiveData 的示例太多了。加上"禁止 LiveData"之后,违规率从30%降到接近0。禁止项是 Rule 里效果最立竿见影的部分。
教训二:给模板,别给原则。说"代码要简洁"是废话,AI 不知道你的"简洁"标准是什么。直接给一个你觉得好的 ViewModel 模板代码,比写十条原则有效十倍。AI 是模式匹配的,给它模式它就能复现。
教训三:Rule 不是越长越好。我一度把 CLAUDE.md 写到了2000多字。后来发现 AI 生成速度变慢了不说,遵守度也没提高——信息过载。现在控制在 800 字以内,核心就那么几条,精简且明确。
教训四:写 Rule 时预期"AI 会犯什么错"。不是凭空想规则,而是先让 AI 不带 Rule 写几次代码,看它犯什么错,然后针对性地写 Rule。这样写出来的 Rule 每一条都有实际意义。
教训五:Rule 要跟代码一起 Code Review。有人改了架构但没更新 Rule,后果是 AI 按旧架构生成代码,然后代码跑不起来。Rule 应该和代码一样进 PR Review 流程。我们现在的 .cursor/rules/ 改动必须有人 Approve 才能合入。
二、Skill:把重复任务封装成 AI 可执行的技能
Rule 解决的是"AI 写出的代码像你团队的风格"。但还有另一个问题:有些任务你每天都在做,每次都得给 AI 重新描述一遍。
比如"给这个功能加个新的 API 接口"——你每次都得说:先在 ApiService 加接口定义,然后写 Repository 实现,然后写 UseCase 封装,然后在 di 模块绑定,最后写个单元测试。六个步骤,每次都一样,只是接口名和参数不同。
这就是 Skill 要解决的:把一个多步骤的开发任务,封装成一句话就能触发的"技能"。
2.1 Skill 的本质:任务模板 + 上下文注入
不同工具对 Skill 的叫法不同——Cursor 叫 Agent Rule + Notepads,Claude Code 叫自定义命令(/commands),Google 的 Agent Skills 是更标准化的定义。但核心思想都是一样的:
Skill = 任务描述 + 步骤序列 + 上下文文件
触发词:"加个新接口"
↓
Step 1: 读取 ApiService.kt,在末尾添加接口定义
↓
Step 2: 在 data/repository/ 下创建 {Feature}RepositoryImpl
↓
Step 3: 在 domain/usecase/ 下创建 {Feature}UseCase
↓
Step 4: 在 di/NetworkModule 中绑定 Repository
↓
Step 5: 生成 UseCase 单元测试
我来给个实际例子。这是我为 Claude Code 写的一个自定义命令,用来在项目里创建一个完整的新功能模块:
# .claude/commands/new-feature.md
创建新功能模块,参数:
- $FEATURE_NAME:功能名(PascalCase)
- $PACKAGE:包路径
步骤:
1. 创建目录结构:
feature/$FEATURE_NAME/
├── ui/
│ ├── ${FEATURE_NAME}Screen.kt
│ └── ${FEATURE_NAME}ViewModel.kt
├── domain/
│ └── ${FEATURE_NAME}UseCase.kt
├── data/
│ ├── ${FEATURE_NAME}Repository.kt
│ └── ${FEATURE_NAME}RepositoryImpl.kt
└── di/
└── ${FEATURE_NAME}Module.kt
2. ViewModel 遵循根目录 CLAUDE.md
中的 MVI 模式
3. Screen 使用 Compose,接收
UiState 参数,不持有 ViewModel
引用
4. Repository 接口在 domain/,
实现在 data/
5. Module 用 @InstallIn(ViewModelComponent)
6. 为 UseCase 生成单元测试
参考文件(AI 会自动读取):
- feature/cart/(作为已有模块参考)
- core/network/ApiService.kt
用的时候一句话:/new-feature Search com.app.search。十秒钟,六个文件全部生成,而且代码风格和项目里其他模块完全一致——因为它参考了你指定的已有模块。
2.2 高频 Skill 推荐:Android 项目的五个必备技能
经过几个月实战,这五个 Skill 的 ROI 最高:
① new-feature:上面讲过了,创建完整功能模块。每次省30-45分钟脚手架时间。
② add-api:新增 API 接口全链路(ApiService → Repository → UseCase → DI 绑定 → 单测)。每个新接口从手写15分钟压缩到2分钟。
③ compose-screen:根据设计稿描述生成 Compose 页面。关键是在 Skill 里指定好你团队的 Design System 组件库路径——让 AI 用你自己的 AppButton、AppTopBar 而不是 Material3 原生组件。
④ migrate-to-compose:把一个 XML layout + Fragment 页面迁移到纯 Compose。这个 Skill 要指定"保留原有业务逻辑,只改 UI 层"——不然 AI 会趁机重构你的 ViewModel,改着改着就改出 Bug 了。
⑤ write-test:给指定类生成单元测试。Skill 里要指定你团队的测试风格(用 Turbine 测 Flow、用 MockK 不用 Mockito、Given-When-Then 命名等)。
三、上下文工程:让 AI 理解 10 万行代码的秘密
好了,现在 AI 有了你的规范(Rule),有了任务模板(Skill)。但还差一个关键要素:它看不到你的整个项目。
一个中大型 Android 项目轻松 10-30 万行代码。AI 的上下文窗口再大——Claude 200K tokens、Gemini 1M tokens——也不可能把全部代码塞进去。何况塞进去也没用,信息太多等于噪音。
上下文工程要解决的问题是:在 AI 完成当前任务所需的最小信息集合和上下文窗口容量之间,找到最优平衡点。
3.1 上下文的四个层次
我把 AI 编码时的上下文分成四层,从成本最低到最高:
上下文金字塔
L4:全项目索引
成本最高·效果不确定 ↑
L3:相关文件引用
中等成本·效果好 ↑
L2:模块 Rule
低成本·效果显著 ↑
L1:项目 Rule
最低成本·基础保障
L1 项目 Rule(~500 tokens):根目录的 CLAUDE.md / .cursor/rules,每次自动加载。成本几乎为零。
L2 模块 Rule(~300 tokens):子目录的规则文件,按需加载。
L3 相关文件引用(~2000-5000 tokens):这是差距最大的一层。你要告诉 AI "写这个新 ViewModel 之前,先看一下 CartViewModel.kt 的写法"。手动 @ 引用文件,或者在 Skill 里预设参考文件列表。
L4 全项目索引(10000+ tokens):用 Cursor 的 codebase indexing 或 Claude Code 的 codebase_search 工具。让 AI 自己搜索相关代码。效果好坏取决于项目结构和命名规范。
我的经验结论:L3 的投入产出比最高。精心选择3-5个参考文件,比让 AI 漫无目的搜索全项目有效得多。
3.2 实战:一个被上下文决定的代码质量案例
来看一个真实场景。我要给 "我的订单" 页面加一个下拉刷新功能。
不给上下文的结果:
// AI 生成——能跑,但不对
class OrderViewModel :
ViewModel() {
private val _isRefreshing =
MutableStateFlow(false)
fun refresh() {
viewModelScope.launch {
_isRefreshing.value = true
try {
val orders =
repository.getOrders()
_orders.value = orders
} catch (e: Exception) {
_error.value =
e.message
} finally {
_isRefreshing.value =
false
}
}
}
}
问题一堆:没用 Hilt、裸 try-catch、直接调 repository、没走 UseCase、_isRefreshing 应该合并到 UiState 里。
给了 Rule + 参考文件后的结果:
// AI 生成——符合项目规范
@HiltViewModel
class OrderViewModel
@Inject constructor(
private val
getOrdersUseCase:
GetOrdersUseCase,
) : ViewModel() {
private val _uiState =
MutableStateFlow(
OrderUiState()
)
val uiState =
_uiState.asStateFlow()
fun onEvent(
event: OrderEvent
) {
when (event) {
is OrderEvent.Refresh ->
refresh()
}
}
private fun refresh() {
viewModelScope.launch {
_uiState.update {
it.copy(
isRefreshing = true
)
}
getOrdersUseCase()
.onSuccess { orders ->
_uiState.update {
it.copy(
orders = orders,
isRefreshing =
false
)
}
}
.onFailure { error ->
_uiState.update {
it.copy(
error =
error.toUiError(),
isRefreshing =
false
)
}
}
}
}
}
同一个 AI,同一个需求——差别完全来自上下文。后者用了 Hilt 注入、MVI 事件模式、UseCase 链式调用、UiState 统一管理。这才是能直接合入主分支的代码。
3.3 上下文预算管理:别浪费你的 token 额度
上下文不是免费的。每一个塞进去的文件都消耗 token,而 token 既有速度成本(AI 思考变慢),也有金钱成本(API 按 token 计费),还有质量成本(信息过多时 AI 容易"走神")。
我的上下文预算管理策略:
策略一:只引用接口,不引用实现。让 AI 写新的 Repository 实现?给它 Repository 接口文件和一个已有的 Impl 参考就够了。不需要把五个 Impl 都塞进去——它们的模式是一样的,看一个就会了。
策略二:用 .cursorignore / .claudeignore 屏蔽噪音。把 build/、.gradle/、generated/ 等目录排除掉。我还把 proguard-rules.pro 也排除了——AI 看到 ProGuard 规则会"灵感涌现"开始帮你优化混淆配置,完全偏离正题。
# .claudeignore
build/
.gradle/
*.generated.*
**/generated/**
proguard-rules.pro
# 第三方 SDK 的wrapper,
# AI看了反而会照抄它的烂写法
libs/legacy-sdk-wrapper/
策略三:写 "项目地图" 文件。这是我发现的一个非常好用的技巧。在根目录写一个 PROJECT_MAP.md,用极短的篇幅描述项目结构和每个模块的职责:
# PROJECT_MAP.md
## 模块结构
app/ → 壳工程,只有 Application
和 MainActivity
feature/ → 业务功能模块
cart/ → 购物车(乐观更新模式)
profile/ → 个人中心
search/ → 搜索(ElasticSearch)
order/ → 订单(带分页)
core/
network/ → Retrofit + OkHttp 配置
database/ → Room 数据库
designsystem/ → Compose 组件库
common/ → 工具类
## 关键约定
- 每个 feature 独立成模块
- feature 之间通过 Navigation
解耦,不直接依赖
- core/ 是共享基础设施
- 数据流:Screen → ViewModel
→ UseCase → Repository → API
这个文件只有200 tokens,但它让 AI 对项目有了"鸟瞰图"。AI 不会再把 Repository 放错目录,也不会在 core/ 里创建业务逻辑。
四、三套工具的实战对比
聊了理论,来点硬核对比。我用同一个 Android 项目,分别在 Cursor、Claude Code、GitHub Copilot 三个工具上配置了等价的 Rule 和 Skill,跑了一个月,记录了一些数据。
声明:这是我个人在特定项目上的体验,不是严格基准测试。你的结论可能不同。
| 维度 | Cursor | Claude Code | Copilot |
|---|---|---|---|
| Rule 精细度 | ⭐⭐⭐ glob 精准匹配 | ⭐⭐ 目录层级 | ⭐ 仅全局单文件 |
| Rule 遵守率 | ~85% | ~90% | ~70% |
| Skill 系统 | Agent Rule + Notepad | /commands 自定义命令 | 暂无等价方案 |
| 上下文管理 | @ 引用 + codebase 索引 | codebase_search + 手动引用 | 自动推断为主 |
| 我的选择 | 日常开发主力 | 复杂重构和架构任务 | 快速补全和小改 |
一个可能有争议的判断:Claude Code 在遵守 Rule 方面做得最好,但它的 IDE 集成不如 Cursor 丝滑。我的解决方案是 Cursor 配 Claude 后端——结合了 Cursor 的交互体验和 Claude 的指令遵循能力。
Copilot 的 Rule 遵守率最低,它的 copilot-instructions.md 经常被"忽略"——特别是当你的指令和它训练数据里的常见模式冲突时。比如你写了"禁止 LiveData",它有时还是会用,因为 LiveData 在训练数据里出现频率太高了。
五、实战演练:从零配置一个 Android 项目的 AI 编码环境
理论讲够了,给一个可以直接抄的实战配置。假设你有一个标准的 Clean Architecture Android 项目。
Step 1:写项目根 Rule(15分钟)
把你项目的核心约定写下来。不用很长——先从那些 AI 最容易犯错的地方开始:
# 最小可用 CLAUDE.md
## 技术栈
Kotlin, Compose, Hilt, Retrofit,
Room, StateFlow, Coroutines
## 架构
Clean Architecture:
UI(Compose) → ViewModel → UseCase
→ Repository → DataSource
## 必须
- ViewModel 用 @HiltViewModel
- 状态用 StateFlow
- 错误处理用 Result<T> 链式调用
## 禁止
- LiveData
- GlobalScope
- Dispatchers.IO 硬编码
- !! 操作符
- 裸 try-catch
就这么多。15 行,不到 200 tokens。但这 15 行能让 AI 生成代码的可用率从 50% 提升到 80% 以上。
Step 2:写项目地图(10分钟)
PROJECT_MAP.md,前面给过模板。关键是写清楚每个目录放什么、模块之间的依赖关系。这个文件你写一次就行,项目结构不变就不用更新。
Step 3:配置 ignore 文件(5分钟)
# .claudeignore / .cursorignore
build/
.gradle/
.idea/
*.generated.*
**/generated/**
local.properties
# 三方 SDK,别让 AI 学坏
libs/
Step 4:创建第一个 Skill(20分钟)
选你最高频的任务。不确定选什么?看看你过去一周给 AI 说得最多的指令是什么——那个就是你的第一个 Skill 候选。
Step 5:迭代(持续)
这是最重要的一步。Rule 和 Skill 不是写一次就完事了。每次 AI 生成的代码不符合你的预期,先想想:是 Rule 没覆盖到,还是上下文没给对?然后更新相应的配置文件。
我团队现在有个习惯:每个 PR 如果涉及到"手动修正了 AI 生成的代码",就在 PR description 里标注一下修正了什么。每周五花30分钟回顾这些标注,更新 Rule。三个月下来,我们的 AI 代码直接可用率从 55% 提升到了 87%。
六、踩坑记录:被 AI 坑过的三次
讲了这么多正面案例,也说几次被坑的经历,让你别走我的弯路。
坑1:AI 参考了错误的文件。有一次我让 Claude Code 写一个新的 Repository,它自动搜索了项目里的相关文件做参考——结果找到的是一个两年前的遗留模块,那个模块还在用 RxJava。AI 按 RxJava 风格写了一版,然后我的所有 Rule 里完全没提 "不要用 RxJava"(因为我以为项目里已经没有了)。教训:遗留代码要么删掉,要么在 ignore 文件里屏蔽。坑2:Skill 步骤太多导致中间出错。我写了一个7步的 new-feature Skill,AI 在执行到第5步时把第2步创建的文件包名写错了——因为上下文太长,它"忘记"了前面的步骤。教训:单个 Skill 最多5步,复杂任务拆成多个 Skill 链式执行。坑3:Rule 互相矛盾。根目录 Rule 写了"用 Coroutines",但数据库模块的 Rule 写了一个 Room 示例代码用了 callback 模式。AI 在写数据库相关代码时就精神分裂了,有时用协程有时用回调。教训:模块 Rule 不能和根 Rule 矛盾,只能在根 Rule 基础上增加细节。
本篇核心要点
• Rule 分两层:根目录定团队规范,子目录定模块约定——"禁止项"比"推荐项"效果更好
• Skill 封装高频任务:把多步骤开发流程变成一句触发词,关键是指定参考文件
• 上下文工程四层金字塔:项目Rule → 模块Rule → 精选参考文件 → 全局索引,L3 的 ROI 最高
• 持续迭代是灵魂:每次手动修正 AI 代码都是 Rule 优化的信号
• 三工具选择:Cursor 交互好 + Claude 遵守强 → Cursor 配 Claude 后端是当前最优解
下一篇我们聊一个更有争议的话题:AI Code Review。AI 能不能当你的 Code Reviewer?它的审查和人类 Reviewer 的审查有什么区别?我在团队里试了一个月 AI 自动 Review——效果有惊喜,但也有意想不到的翻车。敬请期待第4篇《AI Code Review:让每一行代码都有AI审查员》。
—— 全文完 ——