Claude Code过于顺从,容易走捷径妥协;通过明确的工程规范契约(AGENTS.md)定义“完成标准”,让它不再自作主张降低代码质量。
Claude Code 太听话了,而这恰恰是问题所在。当没有东西告诉它工程纪律长什么样时,它就会发明一个看似合理的版本:类型随意放宽到 any、测试只断言实现细节所以怎么都能过、静默的 catch (e) {} 吞掉你需要知道的失败。我花了几个月时间逐个修复这些问题。解决方案不是更好的提示词,而是一份合同。
Claude Code 是我用过的最好的指令执行者,而这恰恰是问题所在。给它一个模糊的需求,它不会卡住,而是会猜。它选择最快、最看似合理的路径,而这条路通常是一个小退步:这里把类型放宽到 any,那里写一个只断言代码已有行为的测试,一个空 catch 块悄悄吞掉本该大声报告的错误。
这一切都不是恶意。这是一个非常积极、非常快速的助手在没人告诉它"完成"长什么样时会做的事。你不能站在它旁边纠正每一个回答,那样的话雇一个需要像实习生一样监督的 agent 就毫无意义。真正的问题是:如何在"看似合理"和"正确"之间画一条线,并让它在每个任务、每个会话、每个工具中都保持一致。
我一开始和大家一样用的是 CLAUDE.md,一份描述项目的散文文件。它有用,但读起来像文档。Claude Code 扫过它,吸收了氛围,当遇到没有覆盖的情况时就又回去发明看似合理的工程方案。
转变发生在我把所有东西都移到 AGENTS.md,并停止用给同事留便签的方式写作。Claude Code 第一个读取 AGENTS.md,其他 agent 也会尊重它,所以这份纪律跟着仓库走,而不是活在某个工具的提示词里。更重要的是,我开始用写合同的方式写它。愿望清单说"写好测试"。合同说清楚什么是好、什么是坏、什么时候测试是坏的、当前置工作不断失败时该怎么做。具体、可编号、可测试。那点差异就是大部分的收益。
我的合同有八条基线规则、一个六级验证阶梯和一个失败协议。每个改动都必须爬完这个阶梯才能算完成,前三级是硬性要求。以下是它的形状,精简到真正干活的部分:
# AGENTS.md
## Baseline Rules
1. Never widen a type to `any` to make a check pass. Fix the check.
2. A test that cannot fail is not a test. Delete it and write one that can.
3. No silent `catch (e) {}`. Handle it, rethrow it, or log it with context.
4. Prefer the codebase's existing patterns over new abstractions.
5. A change that compiles but is not verified does not exist.
6. Public APIs get tests before they get callers.
7. If a rule conflicts with a deadline, the deadline loses.
8. When in doubt, ask. Guessing is the expensive path.
## Verification Ladder
Rungs 1-3 are mandatory for every change:
1. Does it compile?
2. Does the changed behavior actually work?
3. Does it break anything adjacent?
4. Does it follow this codebase's conventions?
5. Does it hold at the boundaries: empty, null, huge, concurrent?
6. Is the result observable in production?
## Failure Protocol
- First failure: fix it and re-verify. Normal day.
- Second failure: stop and re-derive. Your mental model of the system is wrong.
- Third failure: stop, revert to last known good, and document what happened.
这是人们问得最多的部分,也是节省最多时间的部分。Claude Code 迭代很快,乐于燃烧 tokens 在一个破碎的假设上 grinding。它会重读同一个文件,用不同的方式尝试相同的修复,并完全自信地告诉你为什么这次会成功。没有刹车踏板,那就是金钱和耐心往坑里填。
三次尝试,这就是整个协议。第一次失败,修复并重新验证,正常的一天。第二次失败,停下来重新推导,因为到那时模型的系统心智模型已经错了,而不是它的打字。第三次失败,停下来,恢复到上一个已知良好状态,并记录发生了什么。恢复很重要:它让系统回到已知地面而不是更差的地面,记录意味着下一个会话从教训开始而不是重复它。
就这一条规则,第三次后停下来,比文件里其他任何东西都为我节省了更多浪费的工作。在破碎的假设上 grinding 是昂贵的失败模式,而协议在结构上使它变得不可能。
基线规则是地板,但项目中大多数任务都遵循一种形状:特性、bugfix、审查。这些形状不是合同,它们是流程,是有_gate between them_的顺序步骤。所以它们活在独立的工作流文件里,当任务开始时 agent 被指向正确的那个。
把它们分开很重要,因为工作流是顺序的而合同不是。埋在系统提示词里,工作流会被扫过。在自己的文件里,它会被遵循。当 Claude Code 开始一个特性时,它打开特性工作流,而那个文件要求的第一件事是这个改动的两句话合同。就这一步就杀掉了大量漂移。
# Feature Workflow
1. State the contract for this feature in two sentences: what it does, what it deliberately doesn't.
2. Find the closest existing pattern in this codebase and follow it.
3. Write the behavior-proving test first. Watch it fail.
4. Implement the smallest change that makes it pass.
5. Climb the verification ladder. Rungs 1-3 are mandatory.
6. If the design fights you at rungs 4-5, stop and re-derive before writing more code.
7. Report back: what you changed, what you tested, what you left untested.
阶梯的第二级是纪律真正落地的位置,因为那是 Claude Code 默认本能去死的地方。放任它,它写的是断言实现细节的测试、无法失败的测试——因为它们只检查代码是否运行,而不是是否正确。
// Asserts the mechanism, not the behavior. Passes even when the feature is broken.
test("addItem stores the item", () => {
const cart = new Cart();
cart.addItem({ name: "sword", price: 45 });
expect(cart.items.length).toBe(1); // still passes if the total is never charged
});
// Asserts the behavior. Fails when the feature is broken.
test("addItem charges the full price of what was added", () => {
const cart = new Cart();
cart.addItem({ name: "sword", price: 45 });
cart.addItem({ name: "shield", price: 30 });
expect(cart.total()).toBe(75);
});
这是我用来判断任何测试的规则:删除实现,测试应该失败。如果不失败,它从来就没测试过任何东西。行为证明测试是那种当有人破坏特性时会 break 的测试,而这些是唯一值得让 Claude Code 写的测试。
这一切都不会让 Claude Code 变成魔法。它还是同一个模型在做同样的推理。改变的是它读取的第一个文件现在把纪律描述为流程而不是偏好,当流程说停,它就停。你还是要 review 它的工作,只是少了很多。