第 4 章 / 共 11 章
系统提示的海拔
4.1 一份 400 行的 CLAUDE.md,和它带来的两种失败
shopfront 的 CLAUDE.md 现在有 400 行。它是三年、七八个人一条条加出来的:谁被 agent 坑过一次,就往里补一条。
翻开来看,里面混着两种东西。
一种是真规矩,比如:
结算金额一律用整数分,禁止在任何层出现浮点金额。
这条值得存在。它是团队用一次线上事故换来的,跟场景无关,agent 不知道就一定会犯错。
另一种是把 if-else 写成散文,比如:
如果文件名以
legacy开头且不在tests目录下,则先问我;但如果是.spec.ts结尾的可以直接改;除非它在src/checkout/下面,那种情况还是要问。
这条也是有人被坑过一次留下的。它描述的是那一次的具体情形,不是一个判断准则。
结果就是你现在看到的样子:该问的时候它不问,不该问的时候它问。 agent 要改 src/checkout/legacy/coupon.ts——一个真该确认的改动——它没问,因为文件名不以 legacy 开头,目录才是。而它改一个跟结算八竿子打不着的 legacy-icons.tsx,倒是老老实实停下来问你了。
更糟的是第三种东西,那些没人反对所以一直留着的句子:
编写高质量、可维护、符合最佳实践的代码。
这一行占了 token,提供了零信号。删掉它,agent 的行为不会有任何变化。
这三种句子的问题不一样,但可以用同一个尺子量:海拔。
4.2 为什么「再加一条」永远修不好它
面对上面的失败,团队的默认反应是往 CLAUDE.md 里再加一条:「注意:路径中包含 legacy 的也算。」
这个动作看起来在修 bug,实际上在加剧病因。
第一,你在补的是上一次踩到的那个分支,不是判断准则。下一次会有 src/checkout/v1-legacy/、会有 LegacyCart.tsx、会有软链接。你永远补不完,因为你在用穷举去覆盖一个本该用原则覆盖的空间。
第二,CLAUDE.md 是每一轮都要重放的固定成本。第 2 章盘点的时候你已经看到它在预算里的位置了。400 行里有 300 行是历史分支穷举,这 300 行每一轮都在跟真正重要的信息抢注意力。
第三,条款越多,内部冲突的概率越高。第 80 行说「改动前先跑测试」,第 310 行说「不要主动跑测试,测试很慢」。两条都在窗口里,模型只能挑一条,而你无法预测它挑哪条。这就是第 3 章讲的上下文冲突,只不过发生在你自己写的文件里。
正确的动作不是加,是把每一条抬到合适的高度重写。
4.3 海拔:太低会崩,太高没信号
海拔(altitude) 说的是一条指令的抽象层级。
太低,就是硬编码脆弱逻辑。你把一段本该由模型判断的东西,写成了穷举的分支。它在你写的那几个情况里能用,一遇到没覆盖的情况就崩——而且是静默地崩:模型会严格执行你写的条件,条件不匹配就什么都不做,你看不到任何报错。上面那条 legacy 规则就是典型。
太高,就是没有信号的废话。「写高质量代码」「遵循最佳实践」「注意性能」——这些句子的问题不是错,是它们不排除任何行为。一条指令如果无法让模型在两个具体做法之间做出选择,它就没有携带信息。
合适的海拔在中间:足够具体到能指导行为,又足够灵活到给模型强启发式。
用上面那条改写试试:
涉及结算路径(
src/checkout/)的改动,只要它会影响金额计算或订单状态流转,先停下来跟我确认。其余目录直接改。
注意这句话做了什么。它没有枚举文件名,它说的是你真正在意的那件事——钱和订单状态。coupon.ts 在这个描述下会被正确拦下,legacy-icons.tsx 会被正确放行,而且下次出现一个你从没见过的 discount-engine.ts,它也能判断。
这就是把散文里的 if-else 抬高一层的收益:一条顶掉原来的三条,还覆盖了原来覆盖不到的情况。
4.4 海拔自检三问
每次你想往 CLAUDE.md 里加一条,或者回头审视一条老的,问这三个问题。
第一问:这条规矩换个场景还成立吗?
把条件里的具体名字换掉——换个目录、换个文件名、换个模块。如果换完这条规矩就不成立了,说明你写的是一个案例,不是一条规矩。
「legacy 开头的文件要问我」换成 src/checkout/legacy/coupon.ts 就不成立了。「影响金额计算的改动要问我」换到任何目录都成立。
第二问:它是在描述结果,还是在枚举分支?
好的规矩描述你想要的状态:「结算金额在任何层都是整数分」。差的规矩枚举路径:「在 A 情况下做 X,在 B 情况下做 Y,除非 C」。
枚举分支的句子有个明显特征——它有大量的「如果」「除非」「但是」。数一数你那条规矩里这三个词出现了几次。超过一次,基本可以确定海拔太低了。
第三问:如果模型没读到它,会犯什么具体的错?
这一问专治海拔太高。你要能说出一个具体的、能被观察到的错误行为。
- 「写高质量代码」——模型没读到会犯什么错?说不出来。删。
- 「结算金额用整数分」——模型没读到会写
price * 0.9产生浮点误差,测试里表现为expect(total).toBe(1799)收到1798.9999999999998。说得出来。留。
说不出具体错误的条款,一律删除。它们不是「聊胜于无」,它们在消耗每一轮的注意力预算。

图 4.1:一条竖直的海拔轴。底部是枚举分支的脆弱条款,顶部是无信号的口号,中间那段窄带是「足够具体到能指导行为、又足够灵活到给强启发式」。看每组改写在轴上的位移。
4.5 五组改写:从 shopfront 的真实条款出发
下面每一组都取自 shopfront 的 CLAUDE.md,左边是它现在的样子或者常见的过度矫正,右边是抬到合适海拔之后。
| 主题 | 太低(枚举分支,一遇例外就崩) | 太高(没有信号) | 刚好(可指导行为,且能外推) |
|---|---|---|---|
| API 迁移 | 「把 src/api/client.ts 里的 postOrder、getOrder、cancelOrder 换成 postOrderV2、getOrderV2、cancelOrderV2」 | 「迁移到新 API」 | 「结算相关调用一律走 v2 客户端。v1 与 v2 的字段映射以 docs/api-migration.md 为准;遇到该文档没覆盖的接口,停下来问,不要自己猜映射」 |
| 错误处理 | 「postOrder 返回 4xx 时抛 OrderError,返回 5xx 时重试三次,超时时返回 null,除非是退款流程」 | 「做好错误处理」 | 「订单类请求的失败必须冒泡到调用方并带上原始 error,不允许吞掉或降级成 null。是否重试由 src/api/client.ts 的统一策略决定,不要在业务层各写一套」 |
| 测试位置 | 「结算的测试放 tests/checkout/,工具函数的测试放 tests/utils/,组件测试放组件旁边,hooks 的测试放 tests/hooks/」 | 「记得写测试」 | 「测试镜像源码目录结构,放在 tests/ 下的对应路径;只有 React 组件的测试与组件同目录。新增一类文件时按这条推断,不用问」 |
| 提交信息 | 「提交信息格式为 [模块] 动作:描述,模块只能是 checkout、api、ui 三者之一,动作只能是 add、fix、refactor」 | 「写清晰的提交信息」 | 「提交信息一行说清改了什么行为,而不是改了哪些文件;涉及 API 迁移的提交在正文里注明对应的 v1 接口名,方便回溯」 |
| 什么时候停下来问人 | 「文件名以 legacy 开头且不在 tests 目录下则先问我;.spec.ts 结尾可以直接改;但在 src/checkout/ 下的还是要问」 | 「不确定时请询问」 | 「会影响金额计算、订单状态流转、或对外接口签名的改动,先停下来确认。其余直接做,做完在总结里列出改动清单」 |
对着这张表,注意「刚好」那一列的共同点:
- 它们说的是为什么(钱、状态、对外契约),不是在哪(文件名、目录名)。
- 它们把易变的细节外置到一个文件(
docs/api-migration.md、src/api/client.ts的统一策略),指令里只留指针。这是第 3 章的「写」在系统提示层的应用。 - 它们都给了边界之外怎么办:「遇到没覆盖的,停下来问」「按这条推断,不用问」。这句话才是真正防崩的部分——它告诉模型在你没预料到的情况下该怎么行动。
4.6 把项目指令分成事实、规矩、偏好
400 行难改,一个原因是它把三类完全不同的东西混在了一起。分开之后你会发现,其中只有一类真的适合写死。
事实:目录约定、常用命令、端口、依赖版本、环境变量名。
shopfront的例子:结算代码在src/checkout/;本地开发跑npm run dev,端口 5173;MCP 订单服务的地址在.env的ORDER_MCP_URL。
这类适合写死,因为它们是客观的、可验证的、模型不可能推断出来的。它们也最便宜——一行一个事实,没有歧义。写事实的唯一风险是过期,所以每条事实都该有一个可以核对的来源(命令跑得通、路径存在)。
规矩:必须做什么、禁止做什么。
shopfront的例子:金额一律整数分;结算路径的改动先确认;订单请求的错误不许吞。
这类要写,但要按三问抬高海拔。规矩是最容易堕落成 if-else 散文的地方,因为每一条规矩都诞生于一次具体的事故。写的时候强迫自己回到事故背后的原则,而不是事故本身。
偏好:风格倾向。
shopfront的例子:倾向早返回而不是深嵌套;倾向具名导出;注释写为什么不写做了什么。
这类最不适合写死,也最该压缩。偏好的特点是「违反了也不致命」,而每一条偏好都在占用注意力预算,跟那些致命的规矩抢位置。三条偏好写成一句总纲,比十条分列强。能交给 linter 和 formatter 的,全部交出去——那是零 token 的执行方式,而 CLAUDE.md 里的每一条都是要花钱的。
给你一个粗糙的配比感觉:如果你的项目指令里偏好比规矩还多,说明它已经跑偏了。
4.7 示例要少、要杂、不要太整齐
第一个坑是数量。很多人的做法是把踩过的边界情况一条条贴进去,攒成一长串「反例集」。这跟穷举 if-else 是同一个错误,只是换了个形式。正确的做法是少量、多样、有代表性——三个覆盖不同形态的例子,比十五个覆盖不同文件名的例子有用得多。示例的作用是划定空间的形状,不是填满空间。
**第二个坑更隐蔽:太整齐的示例会让模型陷入重复模式。**这是 Manus 团队在生产环境里得到的经验。当上下文里堆着一串格式高度统一的范例时,模型会开始模仿那个格式本身,而不是理解它背后的意图。放到 shopfront 里就是:你给了五个格式一模一样的 v1→v2 迁移示例,agent 迁到第六个接口时,会硬套那个格式,哪怕这个接口的参数结构根本不同。
Manus 的应对办法是在序列化方式和措辞上引入轻微变化——同样的意思,换个句式、换个字段顺序。这听起来像玄学,但它的道理跟第 3 章的上下文分心是同一个:**上下文里的模式一旦足够强,就会压过模型的训练知识。**你的示例越整齐,这个模式就越强。
所以写示例时的三个动作:控制在三五个以内;让它们形态各异(一个正常的、一个有例外的、一个该停下来问的);不要把它们排成一个模板。