同一个模型、同一个需求,给两份不同的上下文,产出质量差一个档次。差距不在措辞,在模型到底知道多少这个仓库的事实。
它看不到你的 Jira,看不到三年前那次架构评审,看不到 OrderService 里那个看似多余的判空。它默认按"最常见的写法"补全,而最常见的写法常常不是你项目里的写法。
1. 三类盲区
| 盲区 | 典型表现 | 后果 | 补法 |
|---|---|---|---|
| 不知道约定 | 新建 utils/ 而项目统一用 common/;返回 null 而约定用空集合 |
能跑,评审过不去 | 项目级约定文件 |
| 不知道历史决策 | 把当年刻意拆开的两个服务又合并;把废弃的同步调用改回来 | 破坏既有设计意图 | ADR + 注释里的"为什么" |
| 不知道运行环境 | 写死 localhost:8080;用 System.getenv 而部署侧只注入配置文件 |
本地通、线上挂 | 目录级说明 + 部署脚本路径 |
约定类最便宜,写一次管很久;历史类最贵,散落在人的记忆里;环境类最容易补,把 docker-compose.yml 与启动脚本位置告诉它就行。
反直觉的一点:模型对语言本身的规范掌握得比你的项目约定好得多。真正值得写下来的,全是"这个仓库特有的东西"。
2. 上下文的三层供给
2.1 项目级:AGENTS.md / CLAUDE.md
唯一"每次对话自动加载"的一层。Claude Code 读 CLAUDE.md,Codex CLI 读 AGENTS.md,Cursor 走 .cursor/rules。名字不同作用一致:仓库的入场须知。
# AGENTS.md
## 项目
支付网关订单侧服务。Java 17 + Spring Boot,构建用 Maven。
## 构建与验证
- 编译:`./mvnw -q -DskipTests compile`
- 单测:`./mvnw -q test -Dtest=OrderServiceTest`
- 全量:`./mvnw -q verify`(改完只跑相关测试)
## 目录职责
- `api/` 对外 HTTP 契约与 DTO,改动需同步 `docs/openapi.yaml`
- `domain/` 实体与领域服务,**不允许**依赖 `infra/`
- `infra/` 持久化、消息、外部 HTTP 客户端
- `common/` 跨模块工具,新增内容需评审
## 约定与禁忌
- 金额一律 `BigDecimal` + `Money` 包装类,禁止 `double`
- 对外接口不返回 `null` 集合,返回空集合
- 新增依赖前先确认 `pom.xml` 里没有等价能力
- 日志用占位符 `log.info("order_id={}", id)`,禁止字符串拼接
2.2 目录级:局部说明
根级塞不下所有细节,多数工具支持子目录同名文件做局部覆盖(Claude Code 的 CLAUDE.md 逐层向上合并):
repo/
├─ AGENTS.md # 全局,控制在 100 行内
├─ domain/AGENTS.md # 纯 Java,禁止引入 Spring 注解
├─ infra/AGENTS.md # 连接池、重试策略统一在这里配
└─ api/AGENTS.md # 错误码表、版本前缀规则
价值在于只在模型进入该目录时才有意义:那句话放全局文件是噪音,放 domain/AGENTS.md 是提醒。
2.3 任务级:当次对话
| 层级 | 载体 | 生效范围 | 该放什么 | 更新频率 |
|---|---|---|---|---|
| 项目级 | AGENTS.md / CLAUDE.md |
整个仓库 | 构建命令、目录职责、硬约束 | 季度 |
| 目录级 | 子目录同名文件 | 该子树 | 分层规则、局部禁忌 | 半年 |
| 任务级 | 当次对话 | 一次任务 | 目标文件、验收标准、改动边界 | 每次 |
3. 约定文件写什么、不写什么
判断标准只有一条:能从代码里读出来的不要写,读不出来的必须写。
| 该写 | 不该写 | 理由 |
|---|---|---|
金额用 BigDecimal |
Order 有 12 个字段 |
字段模型读代码就有 |
domain/ 不依赖 infra/ |
OrderService 在 domain/service 下 |
路径用 glob 就能找到 |
改接口要同步 openapi.yaml |
接口返回 Result<T> |
读一个 Controller 就明白 |
| 新增依赖需确认无等价能力 | 项目用 Maven | 有 pom.xml 就知道 |
测试命令是 ./mvnw -q test -Dtest=X |
测试类叫 *Test |
目录里一眼可见 |
空话是最常见的失败:「请编写高质量代码、遵循最佳实践」在模型那里有几十种解释,零约束力。换成可判定的写法:
<!-- 正例:每条都能被验证 -->
- 单个方法不超过 40 行;超过就拆,拆不动的在 PR 说明原因
- 公开方法必须说明边界条件(可为空?会抛什么?)
- 捕获异常必须处理或带上下文重抛,禁止 `catch (Exception e) {}`
- 所有外部 HTTP 调用必须显式设置连接与读取超时
每条都能被检查,才叫约定。
4. 让 AI 自己找代码的三种方式
CLI 工具自带读写与检索,关键不是贴代码,是给可执行的定位指令。
# 已知路径
改 domain/src/main/java/.../order/OrderService.java 的 submit()。
不要动同目录其他文件。
# 只知符号名
先 grep 搜 "submitOrder" 与 "OrderSubmitted",列出这条链路
经过的所有类,清单给我确认之后再改。
# 同类文件
用 glob 找 api/**/*Controller.java,按目录约定批量处理。
实际执行:
$ rg -n "submitOrder|OrderSubmitted" --type java -l
| 方式 | 适用时机 | 成本 | 风险 |
|---|---|---|---|
| 精确路径 | 已定位,改动局部 | 最低 | 路径写错直接失败 |
| 关键词检索 | 只知道符号名或日志文案 | 中 | 关键词太通用会捞大量噪音 |
| 结构检索(glob + 目录约定) | 找"所有 Controller"这类同类文件 | 中 | 依赖目录命名一致 |
加一句"不确定路径就用
rg/glob找,不要猜",能明显减少编造文件名。
5. 上下文预算
5.1 不要整贴大文件
2000 行的类贴进去,真正相关的可能只有 60 行,其余全在稀释注意力。只贴片段加位置说明:
文件:infra/.../OrderRepository.java(共 780 行)
相关片段:第 210–260 行 findByStatusAndCreatedAtBetween
调用方:OrderQueryService.listPending(),第 88 行
被调方:JdbcTemplate,SQL 见第 40 行 BASE_SELECT
三行说明比 780 行有用:说明调用方与被调方,等于把推理链路直接给出来。
5.2 预算分配
| 内容 | 建议占比 | 说明 |
|---|---|---|
| 任务描述与验收标准 | 10% | 最不能省 |
| 目标文件的相关片段 | 40% | 只贴会改的部分 |
| 接口/数据模型定义 | 25% | 跨文件任务必需 |
| 约定与约束 | 15% | 已由 AGENTS.md 承担,只补当次相关项 |
数值随窗口变化,顺序稳定:任务描述与目标片段永远优先于背景资料。
6. 跨文件任务的信息组织
顺序错了,模型会先写实现再倒推接口,最后跟调用方对不上。正确顺序是自顶向下:模型 → 接口 → 位置 → 验收。
【模型】order 表:id bigint PK, status varchar(16) not null,
amount decimal(18,2) not null, created_at datetime not null
【接口】POST /api/v1/orders/{id}/cancel
成功 200 {"code":0};已是终态 409 {"code":40901}
【位置】OrderController.cancel() → OrderService.cancel(id) →
新增 OrderStateMachine.canCancel(status)
【验收】./mvnw -q test -Dtest=OrderCancelTest
预期:3 个用例通过,已取消订单返回 409
先给契约,再给落点,它就不需要猜。
7. 历史决策的传递
代码能读出"做了什么",读不出"为什么"。后者只能靠文档。
# ADR-014 订单状态用状态机而非 if-else
状态:已采纳(2024-03)
背景:状态从 4 个增到 9 个后,submit() 里出现 40 行嵌套判断,新增状态要改 3 处。
决策:引入 OrderStateMachine,状态迁移表集中在一处。
代价:多一层间接,调试栈变深。
影响:新增状态只改迁移表;禁止在 Service 里写 status 字面量比较。
影响一节最有价值——它正好是模型最容易踩的那条线。
// 必须保留判空:历史数据里 amount 有 null(2023 年前的老订单),
// 直接相加会 NPE。清理脚本见 scripts/backfill_amount.sql。
if (order.getAmount() == null) {
return Result.fail(ERR_LEGACY_ORDER);
}
有了这段注释,它不会顺手把判空"优化"掉。没有注释的防御性代码,在 AI 眼里就是冗余代码。
| 载体 | 记什么 | 谁读 |
|---|---|---|
| ADR | 一次取舍的背景、代价、影响 | 人 + AI(作为附件) |
| 代码注释 | 局部反直觉写法的原因 | 人 + AI(改到该行时可见) |
| 提交信息 | 某个改动的边界与禁令 | 人 + AI(git log 时可见) |
AGENTS.md |
提炼后的结论性约束 | AI(每次自动加载) |
8. 排错:给了上下文还是不对
| 现象 | 原因 | 处理 |
|---|---|---|
| 风格与仓库不符 | 没给约定文件,或文件名不在识别路径上 | 确认工具读 AGENTS.md 还是 CLAUDE.md,对话里点名引用 |
| 新建了已有实现的重复模块 | 只给需求没给检索范围 | 开头加"先 rg -l '<关键类名>' 再决定新建还是复用" |
| 改完编译不过,符号找不到 | 片段缺 import 上下文,或签名是手写摘要 | 给从源码复制的真实签名 |
| 反复问同一个问题 | 约定文件写的是空话,无法据此决策 | 换成可判定条目 |
| 改 A 顺带改了 B、C | 未声明改动边界 | 明确"只改 X,其他只读" |
| 大文件整贴后质量下降 | 相关片段被无关内容稀释 | 只贴片段 + 行号 + 调用关系 |
最后一条要展开:上下文变长不等于信息变多,塞 3000 行无关代码只会摊薄注意力。
9. 边界:写文档解决不了的部分
| 场景 | 该做什么 | 为什么上下文补不上 |
|---|---|---|
| 需求本身有歧义 | 先跟人确认验收标准 | 模型只能猜,猜错概率与没给上下文一样 |
| 隐式规则只在老员工脑子里 | 访谈 + 写进 ADR | 没写下来的东西,它无从得知 |
| 性能瓶颈需要实测 | profiler + 压测 | 没有运行环境,只能给候选假设 |
| 跨团队接口契约 | 直接给真实 OpenAPI/Proto 文件 | 口述摘要必然失真 |
| 生产数据迁移 | 人工设计 + 双人复核 | 不可逆操作不适合交给推断 |
约定文件能解决"模型不知道",解决不了"没人知道"。
10. 自测
AGENTS.md里最不该出现哪一类内容?判断标准是什么?- 项目同时存在
AGENTS.md和CLAUDE.md时,怎么确认工具读了哪个? - 贴大文件片段时,除了片段本身还必须给哪两项信息?
- 跨文件任务为什么要按"模型 → 接口 → 位置 → 验收"排序?
- 什么样的注释能让 AI 不去"优化"掉一段防御性判空?
- 目录级与项目级约定文件的分工界线在哪里?