Claude Code 确定性工程:社区实践三层护栏拆解
说明:本文整理自一篇社区实践分享(来源为掘金社区文章,证据类型为社区信号),并非 Anthropic 官方文档解读。文中涉及的 Hook 退出码语义、Skill 触发方式、规则行数阈值等机制性描述,均为该社区作者的个人实践经验,官方文档待独立核验。读者在落地前应自行对照官方文档确认。
把 Claude Code 当成更聪明的聊天框,是不少生产事故的起点。该社区作者给出的判断很直接:它不是问答系统,而是概率性的执行系统——同一个需求,每次产物都不一样。作者主张要做的事只有一件:把概率压缩成确定性。
作者自述用一年多时间走过一段弯路:一开始以为瓶颈在模型,不断换更强的模型,该出的 bug 还是出;后来注意力从「让模型更聪明」转到「让约束更硬」,提升比换三次模型加起来都明显。这属于作者的个人经验判断,不是可复现的基准结论。
作者把护栏拆成三层。规则层(CLAUDE.md、.claude/rules/、Skills、Commands)强制力弱,回答「它知不知道项目规矩」;分工层(SubAgent、工具白名单)强制力中,回答「有没有人出来唱反调」;反馈层(Hooks、MCP、Checkpoint、验收脚本)强制力强,回答「改好了三个字谁来验证」。一句话概括:规则层管愿意怎么做,分工层管被允许怎么做,反馈层管做不到就走不掉。作者认为只有第三层是真正的强制力。
一、三堵墙:作者观察到的三类反复出现的问题
上下文漂移:规划与中途约定只存在于对话历史里,历史被压缩后「刚才说好用 A 方案」就丢了,它按重新推断的方案继续写,而你毫无察觉。
自我宽容:同一个 Agent 先写代码再自己审查,等于让考生给自己阅卷。
口头验收:「改好了」的命中率取决于它有多想收尾,而不是代码的真实状态。
作者认为共同点是:缺的不是模型能力,是工程约束。
二、规则层:把口头约定写成可判定文件
作者主张 CLAUDE.md 不是备忘录,而是编译期约束。每一条都必须可被判定——要么通过,要么违反。判断标准只有一条:能不能被 grep 出来、被脚本检查。不能,就重写。作者批评大多数人的版本是「注意事项」下挂三条:高质量、规范、最佳实践,这类内容约束力接近于零,因为模型无法判定自己是否违反了「高质量」。
以 acme-api 这个示例为例,作者给出的写法是:运行时 Node.js 20 LTS + TypeScript 5.x ESM,禁止出现 require();包管理只用 pnpm,npm install / yarn add 一律视为错误;src/routes/ 只放路由声明,业务逻辑下沉到 src/services/;错误码必须引用 ErrorCode 枚举;新增 HTTP 端点必须挂 requireAuth(),确需放开写 allowAnonymous。再列清楚不要做的事:不改 src/legacy/**、不改测试断言去迁就实现、不做计划外重构。验收标准四条同时满足:pnpm typecheck 通过、pnpm test 全绿、pnpm lint 无 error,接口类改动必须用真实请求打过一次并贴出响应。
作者的一个关键观察:禁止条款的效果明显好过倡导条款——有没有挂 requireAuth(),grep 一下就知道;符不符合「高质量」,谁也判不了。
作者还提到一条经验阈值:据其个人实践观察,CLAUDE.md 超过两百行后,每条规则被「注意到」的概率明显下降,因此建议按领域拆到 .claude/rules/,用 frontmatter 的 globs 限定生效范围。作者认为规则加载量会影响模型表现。需要强调,两百行这个数字是作者的经验判断,不是官方基准,也不构成普适阈值。
再往上是两个容易混淆的机制。按作者描述,Skill(.claude/skills/名称/SKILL.md)自动触发,管「做事的规矩」,核心是 frontmatter 的 description,写得含糊这个 Skill 就等于不存在;作者的 api-guard 跑一张六项检查清单,其中保留的「需你决策」一段,是让 Skill 学会承认自己不知道,否则它会用看似合理的猜测把业务语义堵上。Slash Command(.claude/commands/xxx.md)手动调用,管「做事的顺序」,最常用的 ship.md 是:只读调研、出方案并停下等确认、实现、真实执行并贴结果的验证、自审。
三、分工层:让批判与创造不共享上下文
作者认为规则是静态的,代码质量问题却是动态的,靠结构解决:让 critic 和 author 不处于同一个上下文。按作者描述,SubAgent 就是 .claude/agents/ 下的 Markdown,frontmatter 定义身份与能力边界,子 Agent 在隔离上下文里运行,这是整个设计的核心。
作者建议三个角色够用。planner 用最强档模型,tools 限 Read、Grep、Glob、WebSearch,只给计划不给代码,说不清的列成问题清单反问。coder 用中档模型,tools 为 Read、Write、Edit、Bash、Grep、Glob,只改计划内文件。reviewer 用小档且换模型系列,只读,tools 含受限 Bash(pnpm test:*、pnpm typecheck、pnpm lint),每条问题必须带文件加行号、0 到 100 置信度、具体后果,置信度低于 70 一律不输出。
作者列出三个容易被忽略的决策:reviewer 必须只读,能改代码的 reviewer 会自己动手,从审查者退化成第二个 coder;reviewer 用小模型反而更好,同模型自我审查的最大问题是过度认同,换个不同系列的小模型能打破这种共鸣,还便宜;置信度阈值比评分更重要,让人放弃 AI 审查的不是漏报,是误报淹没有效信息,报 30 条、28 条是废话等于报 0 条。这些均为作者经验判断。
作者把角色编成一条流程:planner 拆解,拿到待确认问题后转问人、不许替答;交给 coder 实现;调 reviewer 独立验证并让它真的跑命令;高于 70 分的问题交回 coder 修复,最多两轮。作者特别说明,这种编排的可靠性来自 prompt 写得够硬,不是引擎级保证。要更强的确定性,作者主张得上 Hook。
四、反馈层:把「改好了」翻译成「跑过了」
作者认为前面所有东西本质上都是提示,模型可以在某次采样里忘了遵守;只有 Hook 是确定会执行的——挂在工具调用事件上,由运行时触发,不由模型决定。作者用一句话概括:rules 是「它应该这样做」,hooks 是「它不得不这样做」。
按作者描述,最有用的事件是 Stop:每当 Claude 想结束回合,先跑你的脚本。作者称退出码 0 放行,2 拦截并把 stderr 原样喂回给 Claude——这不是报错,是把反馈传回去,它读到后会接着干活。需要提醒:退出码语义属于该社区作者的实践描述,具体行为请以官方文档为准。
作者给出的配置有四个要点:git push 必须禁掉,让 Agent 拥有推送权限是迟早出事的决定;Bash(pnpm:) 这种白名单粒度比 Bash() 安全得多;PostToolUse 防错误累积,Stop 防提前收工;个人偏好放 .claude/settings.local.json 并加进 .gitignore,团队规则放 settings.json 提交 Git。permissions 里同时 deny 掉 rm 与 .env 读取。
作者的 verify-app.mjs 分四步:跑 typecheck、test、lint 收集失败;用 git diff --name-only HEAD 取改动文件;检查 src/routes/ 下有没有 requireAuth 或 allowAnonymous;任一失败就写 stderr 并 process.exit(2)。作者强调三个 pnpm 命令不是重点,最后那段项目专属检查才是——它把「新增端点必须挂鉴权」从软约定变成会拦截的硬约束。
作者认为 MCP 解决另一半问题:它能看到什么。作者常驻 context7 治过时的 API 写法、playwright 自己开浏览器验证、filesystem 看见项目全貌、数据库 MCP 查真实数据。作者提醒三个坑:数据库一律只读账号,给一个能 DROP TABLE 的连接串等同于把生产库 root 密码贴出来;filesystem 路径必须绝对;改完配置必须重启。
五、实战:给一个 Express 服务做鉴权收口
作者举的例子起点很典型:每个路由自己写 jwt 校验,缺 header 返回 401,jwt.verify 失败返回 403,而另一个路由同样情况却返回 401;参数校验写成 500;退款逻辑没有任何幂等保护;同样的逻辑在另外两个路由里是另外两个版本。
按作者描述,planner 会 grep 出全部 jwt.verify 调用点,真正的价值是它会反问:三个路由角色要求不同,是否按角色收口;能否一次性替换;token 过期与非法是否返回同一个错误(返回不同可能泄漏用户是否存在)。作者强调这三个问题不回答,它就不该动手。
作者抽出 requireAuth 中间件时有四个刻意取舍:中间件自己不写响应,错误统一走 next(err),让「错误长什么样」只有一个地方定义,且只对 status 大于等于 500 打 error 日志;JWT_SECRET 缺失时启动即失败;token 过期与非法返回同一错误;职责单一,只做认证。
作者还要求覆盖失败路径,不止 happy path:无 Authorization 头返回 401;scheme 写成 Basic 返回 401;token 过期返回 401 且响应体结构与无 token 时完全一致(这条防的不是 bug 是信息泄漏);角色不足返回 403;缺 amount 返回 400 而非 500;相同幂等键重复提交两次 refundId 相同。
验收别问「你确定没问题吗」,作者建议直接下指令:用 playwright 走一遍登录,验证 401 时前端跳登录页而不是白屏,把截图给我。最后把同一份 verify-app.mjs 放进 CI——作者指出本地 Hook 可以绕过,CI 绕不过。
复盘时作者承认边界:退款要不要幂等、幂等键什么粒度、能否一次性切过去,这三件业务语义的事它猜不中。作者总结:流程能保证已知的正确被稳定执行,不能保证未知的语义被正确猜中。
结语
作者认为这套东西真正改变的不是代码写得快不快,而是你敢不敢把活交给它。裸用只能做随时能检查的小事;配好三层之后,你才敢让它做那种要跑二十分钟、而你能去接杯咖啡的任务。
再次说明:本文为社区实践手册整理,非官方文档解读。文中所有机制细节、阈值与退出码语义均来自该社区作者经验,落地前请以官方文档为准。