1. 为什么 Claude Code 写代码快,但质量总在 PR 阶段翻车
Claude Code 是我用过出活最快的编程助手,一个功能框架几分钟就能搭起来,测试和 PR 也能顺手提交。但用久了你会发现一个规律:代码一开始跑得挺好,过一阵就出问题。状态转换里的竞态条件、本该是常量的硬编码字符串、回滚了本该保留的审计记录、断言写成assert(true)而不是校验真实值——这些坑我几乎每个项目都踩过。
问题不在 Claude 的能力,而在于那些写在 Markdown 里的好习惯不会自动变成习惯。每次都得盯着它:这次别忘了 TDD,提交 PR 前先跑测试,Bug Bot 的评论记得处理。每一句提醒都像在跟一个才华横溢但记忆只有七秒的人对话。流程本身不稳定、容易遗忘,最后我自己成了流程本身。
后来我换了个思路:不改 Claude 的输出,改它的工作方式。具体做法是在项目根目录放一个CLAUDE.md,把 TDD 和 PR 检查流程用一条条约束固化进去,再配合 GitHub Actions 做自动验证。这样 Claude 每次动手前都会先读这份规范,按固定节奏走:先计划、再探索、写失败测试、最小实现、跑回归、对抗性审查、最后过质量门控。
这篇文章就给你一套可以直接复制的方案:一份CLAUDE.md片段、一份 GitHub Actions 配置,以及一次从提交到 PR 的完整验证演示。适合已经在用 Claude Code、但被代码质量反复折磨的开发者。核心检索词就三个:Claude Code、CLAUDE.md、TDD 与 PR 检查。下面按步骤来,每一步都能直接落地。
2. 前置准备:TaoToken 接入 Claude Code 与 CLAUDE.md 定位
在写CLAUDE.md之前,得先让 Claude Code 能稳定跑起来。我这边用的是 TaoToken 作为模型接入层,它提供兼容 Anthropic 的 API 端点,Claude Code 直接改 Base URL 就能用。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数。
先说清楚CLAUDE.md是什么。它是 Claude Code 在项目里自动读取的规范文件,放在仓库根目录,每次会话开始时被加载。你可以把它理解成给 Claude 看的"项目说明书 + 工作纪律"。它和系统提示不同,系统提示管的是通用行为,CLAUDE.md管的是你这个项目的具体规矩:用什么测试命令、提交前必须做什么、哪些模式禁止出现。
我试过把 TDD 和 PR 检查写进CLAUDE.md后,最大的变化是 Claude 不再需要我每次口头提醒。它读到"先写失败测试"这条约束,就会在实现之前先产出测试文件并运行确认失败。这不是玄学,是文件被加载后进入了它的上下文。
接入 Claude Code 的配置,我建议用环境变量方式,避免把 Key 写进仓库。先拿 Key:进入控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在 API Keys 页面创建一个新 Key,复制出来。然后设置环境变量:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="sk-你的Key"如果你用的是 Claude Code 的 settings 文件,也可以写进~/.claude/settings.json:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的Key" } }这里有个细节:Base URL 填https://taotoken.net/api,不要带结尾斜杠,也不要带 UTM。Claude Code 会在这个地址后面拼接/v1/messages之类的路径。Key 建议用环境变量注入,CI 里用 GitHub Secrets,本地用 shell profile,别硬编码进CLAUDE.md或仓库文件。
模型 ID 这块,Claude Code 默认会请求 Claude 系列模型,TaoToken 侧做了映射,你不需要额外指定。如果要在配置里显式写,可以在 settings 里加ANTHROPIC_MODEL,值填你控制台里看到的模型名。三件套记牢:Base URL、Key、Model ID,缺一个都跑不通。
CLAUDE.md的定位要摆正:它不是替代测试框架,也不是替代 CI。它是一份流程提示,把资深工程师的习惯编码进去。真正兜底的是 GitHub Actions,CLAUDE.md负责让 Claude 在写代码时就按规矩来,Actions 负责在 PR 阶段再验一遍。两者配合,质量才稳定可复现。
3. 可复制配置:CLAUDE.md 片段与 GitHub Actions 工作流
这一节是全文的核心,给你两份可以直接抄的配置。先看CLAUDE.md。我把它放在仓库根目录,内容分三块:TDD 约束、PR 检查清单、项目特定命令。下面这份是我实际在用的精简版,你可以按项目改。
# 项目工作规范 ## 测试驱动开发(TDD)强制约束 - 任何功能实现前,必须先编写会失败的测试用例,并运行确认其失败。 - 测试断言必须校验真实值,禁止使用 assert(true) 或等价的无意义断言。 - 实现阶段只写让测试通过的最小代码,禁止提前引入未被测试覆盖的抽象。 - 每次实现后运行完整相关测试套件,确保零回归。 ## PR 提交前检查清单 - [ ] 新增测试全部通过,且断言具备抗变异能力 - [ ] 无硬编码字符串,枚举/常量已抽取 - [ ] 涉及并发写入的操作已加数据库锁 - [ ] 可空字段访问已做空安全处理 - [ ] 变更日志已更新 - [ ] 对抗性审查:假设自己是攻击者,列出可能失败的边界 ## 项目命令 - 运行测试:`npm test` 或 `./vendor/bin/sail test` - 代码检查:`npm run lint` - 类型检查:`npm run typecheck`这份文件的关键在于"强制"两个字。Claude 读到"必须先编写会失败的测试用例,并运行确认其失败",就会在实现前先产出测试。读到"禁止 assert(true)",就会写assertEquals('completed', result.status)这种能捕获真实 bug 的断言。抗变异断言和assert(true)的区别很致命:前者能发现代码什么都没做,后者在代码空转时照样通过。
再看 GitHub Actions。这份工作流在 PR 打开和更新时触发,跑测试、lint、类型检查,任何一项失败就阻断合并。
name: PR Quality Gate on: pull_request: branches: [main, develop] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Lint run: npm run lint - name: Type check run: npm run typecheck如果你用 PHP/Laravel,把setup-node换成shivammathur/setup-php,测试命令换成./vendor/bin/sail test即可。工作流本身和框架无关,核心是"PR 阶段必须过测试"。
把这两份文件放进仓库后,Claude Code 的工作流就变了。它读CLAUDE.md知道要 TDD,写完代码提交 PR,Actions 自动跑测试。如果测试挂了,PR 页面直接标红,Claude 也能读到失败日志去修。这就是"固化"的含义:流程不再依赖你的记忆,而是写进了文件和流水线。
有个坑要提醒:CLAUDE.md里的命令必须和package.json或composer.json里的脚本名一致。我见过有人写npm run test:unit,但项目里根本没这个脚本,Claude 跑命令直接报错,然后它就跳过测试继续实现了。命令名对不上,约束就是废纸。
4. 验证请求:一次从提交到 PR 的完整验证动作
配置写好了,得验证它真的生效。这一节演示一次完整动作:从 Claude Code 接到任务,到 PR 通过质量门控。我用一个真实场景——给订单状态加一个"已取消"转换,并发送通知。
第一步,确认 Claude Code 能连上。在终端里跑一个最小请求,验证 Base URL 和 Key 没问题:
curl https://taotoken.net/api/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "max_tokens": 128, "messages": [{"role": "user", "content": "回复 OK 两个字母"}] }'返回里能看到content数组和OK字样,说明接入正常。如果这里就报 401,先别往下走,去第 5 节排障。
第二步,在项目里启动 Claude Code,给它任务:"实现订单取消状态转换,按 CLAUDE.md 规范执行"。它会先读CLAUDE.md,然后按 TDD 走:先写测试文件order-cancel.test.ts,里面是类似这样的断言:
test('取消订单后状态为 cancelled', async () => { const order = await createOrder({ status: 'pending' }); await cancelOrder(order.id); const updated = await getOrder(order.id); expect(updated.status).toBe('cancelled'); }); test('取消订单会发送通知', async () => { const order = await createOrder({ status: 'pending' }); await cancelOrder(order.id); expect(notificationService.lastSent()).toBe('order_cancelled'); });第三步,让它运行测试,确认失败。这一步很关键,测试必须先红。如果测试一上来就绿,说明测试没覆盖到真实逻辑,得让它重写。
第四步,实现最小代码让测试通过。Claude 会写cancelOrder函数、状态转换逻辑、通知调用。然后跑完整测试套件,确认零回归。
第五步,提交并开 PR。Claude 会执行git checkout -b feat/order-cancel、git commit、git push,然后开 PR。PR 一开,GitHub Actions 自动触发,跑npm test、npm run lint、npm run typecheck。
第六步,看 Actions 结果。如果全绿,PR 页面显示可合并。如果有失败,点进日志能看到具体哪个测试挂了。Claude 读到失败信息后可以继续修,修完再推,Actions 再跑一遍。
我实测下来,这套流程跑通后,PR 里被 Bug Bot 或人工审查发现的低级问题明显少了。因为测试在提交前就拦掉了一批,Actions 又拦掉一批。真正到人眼前的,是逻辑层面的问题,而不是"忘了跑测试"这种流程问题。
验证成功的标志很简单:PR 页面 Actions 全绿,测试数量比改动前多,且新增测试的断言不是assert(true)。你可以打开测试文件扫一眼,如果看到expect(result).toBe(true)这种,就得警惕——它可能什么都没验证。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
配置和验证过程中,最容易卡在几个固定报错上。这一节按真实报错给你排查路径。
401 Unauthorized。最常见的原因是 Key 没生效或 Base URL 写错。先确认环境变量:echo $ANTHROPIC_API_KEY看有没有值,echo $ANTHROPIC_BASE_URL看是不是https://taotoken.net/api。如果 Key 是从控制台复制的,注意别带多余空格。还有一种情况是 Key 被撤销了,去控制台 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 重新生成一个。401 基本就是认证问题,和模型、网络无关。
local proxy failed。这个报错通常出现在你本地配了代理,但代理没起来或端口不对。Claude Code 会读HTTP_PROXY/HTTPS_PROXY环境变量。先unset HTTP_PROXY HTTPS_PROXY再试。如果公司网络必须走代理,确认代理地址和端口正确,且代理允许访问taotoken.net。这个报错和 TaoToken 本身无关,是本地网络层的问题。
reading choices 相关报错。这类报错一般出现在响应解析阶段,提示读取choices字段失败。原因是请求打到了 OpenAI 兼容端点,但 Claude Code 期望的是 Anthropic 格式。检查你的 Base URL 是不是误填成了别的路径。Claude Code 走的是/v1/messages,返回结构是content数组,不是choices。把 Base URL 改回https://taotoken.net/api即可。
OAuth 相关报错。如果你在 Claude Code 里走了 OAuth 登录流程,但报 token 无效,通常是本地缓存的凭证过期了。清掉~/.claude下的凭证缓存,改用 API Key 方式接入。API Key 方式更稳定,CI 里也只能用 Key,OAuth 在无头环境跑不起来。
排查时记住一个顺序:先确认环境变量,再确认 Base URL,再确认 Key 有效性,最后看网络。90% 的问题在前两步就能定位。如果三件套(Base URL、Key、Model ID)都对还是不通,去接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 对照最新配置,或者用模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 单独测一下 Key 能不能用,把问题范围缩小到"Key 问题"还是"Claude Code 配置问题"。
还有一个隐蔽的坑:CLAUDE.md里的测试命令写错,Claude 跑测试报"command not found",然后它可能不报错就继续实现了。这种情况不会在终端报 401 之类的显式错误,但流程已经断了。所以每次改完CLAUDE.md,手动跑一遍里面的命令,确认都能执行。
6. 把流程固化下来:从 CLAUDE.md 到长期编码习惯
写到这里,方案已经完整了:CLAUDE.md固化 TDD 和 PR 检查,GitHub Actions 做自动验证,TaoToken 提供稳定的模型接入。三者配合,代码质量从"靠人盯"变成"靠流程兜"。
如果你打算长期用这套方案做编码和 Agent 任务,可以考虑 Coding Plan,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它适合需要持续跑 Claude Code、频繁提交 PR 的场景,比按量调用更省心。
最后分享一个我踩过的坑:CLAUDE.md不要写太长。我一开始塞了三十多条规则,结果 Claude 读到后面就"疲劳"了,前面的约束执行得也不稳。后来砍到十条以内,每条都短、都可执行,效果反而更好。规则不在多,在于每条都能被验证——要么有对应测试,要么有对应 CI 步骤。写不进验证的规则,等于没写。
还有一点,CLAUDE.md是活的。每次 PR 里发现一类新问题,就补一条约束进去。比如发现硬编码字符串反复出现,就加一条"禁止硬编码,枚举必须抽取"。跑上几周,这份文件就成了你项目的质量沉淀,换谁用 Claude Code 都能继承这套习惯。这比任何口头提醒都靠谱。