
如果你和我一样天天在MR/PR里被几百行diff淹没大概率也经历过这种至暗时刻代码跑得好好的上线半小时后日志开始飘红。我遇到过最离谱的一次团队里一位老哥在改条件判断时把手滑改成了四个数字直接从结果里消失。人工review的时候每个人都看了一遍愣是没发现。后来我基于Claude API写了一套小工具内部代号就叫Claude-Red专门让Claude盯着git diff里被删掉的红色行和新增的绿色行从变更里找逻辑漏洞。这篇文章就把这个工具的完整思路、踩坑记录和上线效果分享出来给同样被Code Review折磨的团队一个参考。1. Claude-Red的诞生一次看走眼的Code Review1.1 那个半夜上线的符号那个事故其实特别朴素。业务方要调整一个返利区间原来的逻辑是if (score 90)同事为了改成“90分以上才享受最高档”很自然地写成了if (score 90)。变量名没问题、缩进没问题、注释也更新了单测也过了。因为当时的测试用例只覆盖了91分和89分恰好没覆盖90分这个临界值。结果上线后一批正好卡在90分的用户全部落到了下一档损失不大但影响很恶劣。事后复盘时我们发现问题不是“没人review”而是那次MR包含了210行diff真正逻辑有关的只有5行。人在看一大片绿色新增行和红色删除行的时候注意力天然会被“新代码”吸引对修改点反而容易一带而过。更麻烦的是很多代码检查工具只能告诉你“这里语法可能有问题”根本不知道业务上和的区别。那个周末我就在想是不是可以让一个足够懂代码的模型像同事一样站在旁边专门盯这些红色删除行和周围的新增行1.2 为什么我决定用Claude来盯diff当时团队里其实有SonarQube这类静态扫描工具但它更擅长检查坏味道、重复代码、未使用变量对“语义层面的行为变化”几乎无能为力。我也试过自己写规则去匹配常见错误比如检测等号改成不等号、数组长度减一之类的但规则越写越厚换个业务场景就失效维护成本实在太高。后来我拿Claude API做了几个实验发现它在以下三个方面的表现远超预期长上下文理解把一整段函数挪到prompt里它能理解这个函数在整体逻辑中的位置。变更语义比较给它一段修改前后的代码它能准确说出“行为发生了什么变化”。自然语言解释它不仅能报错还能用人类能看懂的话解释为什么有问题这对reviewer来说非常有用。我也对比过本地部署的开源代码模型不是不能用但需要单独一张显卡和一堆预处理流程而且对中文注释的理解明显差一截。正好公司已经有Claude API的配额我就直接基于它搭了第一版Claude-Red。1.3 Claude-Red的目标边界第一版Claude-Red的目标非常克制输入是git diff输出输出是一份按文件、按行号排列的风险清单。它不打算替代人工review只做“第二双眼睛”。我在设计时给自己定了三条铁律后来也帮了这个项目大忙只审查变更不跑全项目代码扫描既省token也避免模型回答一堆和本次改动无关的历史债。不阻塞流水线Claude-Red只往MR下面发评论不设置硬性门禁。理由是模型偶尔会有误报让CI直接挂掉会很快失去大家的耐心。只输出结构化结果每条结论必须附带文件、行号、严重级别和具体原因。没有行号的“建议优化”等同废话。这个工具真正有趣的部分其实都在后面的工程细节里。接下来我会把prompt设计、diff压缩、CI集成这几个最关键的步骤展开说清楚。2. 先处理喂给Claude的料diff压缩与上下文拼装2.1 git diff的原始格式和痛点直接从GitHub或GitLab拉下来的MR diff大概长这样diff --git a/src/payment/calculator.py b/src/payment/calculator.py index 7f3a5c2..e4b9d01 100644 --- a/src/payment/calculator.py b/src/payment/calculator.py -81,7 81,7 def get_discount(score, level): if level gold: rate 0.8 - if score 90: if score 90: rate 0.7 return rate这段格式对人来说很友好但直接丢给Claude有几个问题第一diff里的context行只有零星几行模型看不到完整函数没法判断score是什么取值范围、前面是否做过归一化处理。第二一个MR的diff可能横跨几十个文件加上上下文后轻松超过上下文窗口。第三diff里的路径信息虽然明显但模型经常会忽略导致它把A文件的结论错挂到B文件上。所以我在Claude-Red里做了一层“预处理管道”先把毛糙的diff加工成模型更容易理解的“审查料”。2.2 变更过滤不是所有diff都值得审很多团队用AI审查代码时第一版都喜欢把全部文件一股脑塞给模型。我调试完第一版崩溃了两次之后学乖了先人工设定过滤规则把明显不重要的文件踢出去。Claude-Red目前用一套非常简单的规则做初筛规则写在配置文件里review: include_extensions: - .py - .js - .ts - .go - .java exclude_paths: - **/generated/* - **/migrations/* - **/*.lock - **/package-lock.json - docs/* max_hunk_lines: 300我特意把package-lock.json这类锁文件排除掉因为它们的diff经常是几千行占了大量token却没有任何审查价值。generated目录同理机器生成的代码就算有问题模型指出了我们也没法直接改。max_hunk_lines这个参数救过我很多次单个hunk超过300行说明它可能是一次机械重构或者大文件格式化Claude-Red会跳过并打上“需要人工重点确认”的标记。以我们一个中型服务为例一个MR平均有5000行diff过滤后真正需要模型看的通常只剩800-1200行。这个量级Claude处理起来又快又稳。2.3 函数级上下文提取与prompt模板只给diff里的几行上下文是不行的。比如前面那个改成的例子模型如果不看整个函数很难判断90是不是一个临界值。所以在Claude-Red里我用unidiff解析diff再结合AST找到每个变更点所属的函数或类然后把“函数完整源码”提取出来拼进prompt。下面是简化版的prompt模板你是一个资深的Code Reviewer。请分析下面这次代码变更。 ## 变更信息 文件路径src/payment/calculator.py 变更类型条件表达式修改 ## 变更前函数源码 def get_discount(score, level): rate 1.0 if level gold: rate 0.8 if score 90: rate 0.7 return rate ## 变更后函数源码 def get_discount(score, level): rate 1.0 if level gold: rate 0.8 if score 90: rate 0.7 return rate ## 具体要求 1. 只关注这次变更可能引入的行为差异。 2. 如果没有发现明显问题输出一个空数组。 3. 如果发现问题必须给出行号和具体原因。看到没有我不仅给了diff还把变更前后的函数源码都给了。这对模型来说相当于开了“上帝视角”它不用靠猜的可以直接对比函数前后两版找出语义变化。提取函数源码的方式也很简单先用git show拿到文件的旧版本和新版本然后用Python的ast模块定位变更行所在的函数节点截取lineno到end_lineno之间的源码。这里有个坑是end_lineno在Python 3.8才可靠老项目用的Python 3.7会拿到None这种情况我会用缩进推断或者退一步把整个文件的相关区域都带上。2.4 token成本估算与异步调用设计很多人在初期会忽略token成本等账单出来了才傻眼。我简单算过一笔账一个中型MR过滤后剩余800行代码加上函数上下文和prompt包壳大概会消耗6000到8000个输入token。如果调一次返回结果约500个token那么单个文件的平均成本可以控制在0.01到0.03美元。就算每天跑100个MR一个月也就几十美元比请一个兼职reviewer便宜得多。但成本不是最要命的并发才是。Claude API有速率限制如果一次性并发请求20个文件马上会触发限流报429。我在Claude-Red里写了一个异步调度器用asyncio.Semaphore控制并发数不超过3同时用一个简单的FIFO队列排队。实测下来一个20文件的MR能在40秒左右跑完初筛这个速度完全不影响CI体验。import asyncio async def review_file(sem, file_info): async with sem: return await claude_review(file_info) async def main(mr_files): sem asyncio.Semaphore(3) tasks [review_file(sem, f) for f in mr_files] return await asyncio.gather(*tasks)另外一个很重要的优化点是流式输出。如果等Claude把完整JSON生成完再返回一个长文件的等待时间会让人抓狂。我改用streamTrue边生成边解析至少能在视觉上让用户感受到“它正在干活”而不是卡死了。3. 让Claude按规矩说话结构化审查协议3.1 自然语言prompt为什么翻车第一版Claude-Red的prompt写得很宽松大意是“请帮我看看这段diff有没有问题”。结果Claude确实很热情返回了一大段套话什么“建议增加边界条件”“建议增强代码可读性”“注意并发场景”……但如果reviewer真的根据这些话去改代码会发现无从下手因为压根没有定位到具体行号。我意识到问题的关键在于模型默认情况下会倾向于“表现得有用”但它不知道我们想要的是“手术刀式的精确结论”而不是“教科书式的安全建议”。所以必须把输出格式彻底锁死。3.2 用JSON Schema约束输出格式Claude API支持在prompt里用json_schema或者tool calling来约束输出。我用的是结构化输出模式定义了一个JSON Schema{ name: review_findings, schema: { type: object, properties: { findings: { type: array, items: { type: object, properties: { severity: {type: string, enum: [high, medium, low]}, line: {type: integer}, title: {type: string}, reason: {type: string}, suggestion: {type: string} }, required: [severity, line, title, reason, suggestion] } } }, required: [findings] } }配合prompt里的“只输出JSON”声明Claude就老老实实按这个结构返回了。我见过很多团队在这里会偷懒让模型“自由发挥”结果下游解析脚本经常崩。用Schema约束还能过滤掉一部分幻觉——如果模型想瞎编一个行号它至少需要把自己的输出填进这个结构里我们在解析时如果发现line不在本次diff的变更行列表里直接丢弃。下面是实际解析时的一段简化逻辑def parse_review_response(raw_text, changed_lines): data json.loads(raw_text) findings [] for item in data.get(findings, []): line item.get(line) if line not in changed_lines: continue findings.append(item) return findings3.3 按变更类型切换审查视角同样的代码在不同变更场景下要关注的坑完全不一样。我后来给Claude-Red做了一套“审查视角”机制先用简单的规则判断这次变更的类型再在prompt里把对应的checklist塞进去。比如如果diff里修改了if、else、、、就标记为“条件变更”prompt里会追加一句重点关注等号方向是否改变、边界值是否被错误排除。如果是并发相关比如新增了lock、threading、async我会追加检查锁的范围是否正确、是否可能出现死锁。如果是配置修改会追加检查配置类型是否变化、默认值是否影响现有逻辑。变更类型检测特征审查重点条件变更if、else、、、边界值、等号方向、短路逻辑循环变更for、while、range索引越界、无限循环、步长错误并发变更lock、async、await、thread死锁、竞态、锁范围异常变更try、except、raise吞异常、异常类型是否匹配配置变更yaml、json、properties类型转换、默认值、key是否被正确使用这个切换不复杂但它让Claude的输出质量明显提升。原因也简单模型在大模型预训练阶段见过无数代码review范式给它一个明确的“审查checklist”它会自动往这个方向发力。如果不给它就随机发挥今天讲性能、明天将命名完全不可控。3.4 处理误报和空结果的策略即使有Schema约束Claude依然会误报。最常见的一种情况是老代码里本来有一段“看着像bug但实际是绕开旧坑”的逻辑开发者在这次变更中把它删除了Claude反而认为删除是有问题的。我处理这类误报的方法是二次确认。对于Claude给出的high级别发现不直接采纳而是做一个简单的一致性验证把这条发现连同“模型上一轮的结论”重新发给Claude问它“这个发现是否真的在新代码中触发”如果第二次返回仍然认为是问题再写入MR评论。这个双保险虽然增加了一点耗时却把high级别的准确率从70%提到了85%左右。空结果也很常见一个几百行的重构型MRClaude确实可能什么都没发现。这种时候我不会让它在MR上评论“暂无问题”因为这种空评论会淹没真正的告警。Claude-Red只会在有发现时才发评论没发现就什么都不做。这件事看起来简单但直接决定了大家会不会认真看它发的每一条消息——狼来了喊多了再好的工具也没人信。4. 接入CI一个月Claude-Red交出的实测报告4.1 GitHub Actions集成代码与缓存设计把Claude-Red接入CI比预期顺利因为它本质上就是一个命令行工具。我只做了两件事写了一个Python CLI入口配了一个GitHub Actions workflow。workflow的核心内容大概长这样name: claude-red-review on: pull_request: types: [opened, synchronize] jobs: claude-red: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Install deps run: pip install claude-red - name: Run review env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} run: claude-red review --base main --head ${{ github.event.pull_request.head.sha }} - name: Comment MR run: claude-red comment --output review_result.json有两个细节需要特别提醒fetch-depth: 0必须设置否则actions/checkout默认只拉取浅克隆git diff拿不到完整的base。API key放secrets不要直接明文写进workflow。CI日志偶尔会被演员截图发到群里key一旦泄露就要立刻轮换。缓存设计比我想象中重要。同一个MR经常会被反复push每次push都重新跑一遍Claude-Red会重复消耗token。所以我加了一层简单的缓存以base_sha head_sha workspace三个字段拼接成key把结果存到GitHub Actions的cache里。如果哈希没变直接就复用上次结果不再调API。4.2 64次MR里抓到的几个典型问题到目前为止Claude-Red跑在我们三个后端项目上正好一个月累计64次MR。它在其中11次MR里给出了至少一条有效发现虽然数量不算爆炸但每一条都是人工容易漏掉的真实问题。类型分布大致如下问题类型数量严重级别条件边界错误4high字典默认值误用3medium时区/日期问题2high逻辑短路错误1high无用代码遗漏1low举个例子有一次同事在Java代码里把map.getOrDefault(key, new ArrayList())用错了他以为这个新加的列表会被自动放回map里实际上不会。如果后面调用方再从map里取得到的永远是默认空列表新增的数据全丢了。Claude-Red在review时给了一条medium级别的提示“这个默认列表没有被放入原map后续消费方拿到的仍然是空集合。”我们顺着这条提示仔细排查确认是真的bug改了之后所有数据都正常了。另一个典型的发现是时区问题。前端传到后端的时间戳是ISO 8601字符串开发者在解析时直接用了本地时区没有转成UTC。当时所有测试环境时区都是东八区测试全过一上线海外节点就乱套。Claude-Red在高亮“新增的日期解析代码”时提醒了一句“这里解析结果是本地时区而订单数据全部存储为UTC建议显式设置时区。”如果不是它提醒这个问题大概率要等线上用户投诉才能暴露。4.3 它不擅长什么边界与人工兜底一个真实的工具必须有自知之明。Claude-Red跑了这一个月我也很清楚它不擅长什么。首先算法密集型的MR它基本帮不上忙。比如把排序算法从快速排序换成堆排序如果逻辑本身没错它会各种角度夸奖代码写得好偶尔还会给出一些和业务无关的优化建议。这类变更还是得靠真正懂算法的人review。其次超大型重构diff会出现幻觉。单个文件改动超过300行时模型为了满足“必须找到问题”的任务有时候会编造一个“可能越界”的结论。这就是为什么我在预处理阶段把超过300行的hunk都直接跳过不让模型硬着头皮审。最后架构层面的大方向问题它也发现不了。比如这个服务是不是该拆分了、缓存策略是不是整体该改了这种全局判断需要人类架构师的输入。Claude-Red的定位是“挑刺”不是“指方向”。所以团队里现在的规矩是high级别的发现必须由至少一个人工reviewer复核之后才能当作有效告警。我们要的是工具帮人省时间不是工具制造新的运维事故。4.4 后续可以扩展的玩法Claude-Red目前还只是个命令行工具但它其实值得扩展的地方很多。我现在最想做的是IDE本地模式。每次在编辑器中改动一个函数就自动把函数变更发给Claude做一次轻量检查类似实时lint但检测维度换成语义级bug。这样可以把反馈从MR阶段提前到开发阶段能省下更多来回讨论的时间。另一个方向是接入更多模型作为交叉验证。Claude-Red审完换个开源模型再审一遍两者结论一致的部分置信度会非常高结论冲突的部分再交给人工。虽然成本翻倍但对于金融、支付这类高敏感项目来说这笔钱花得值得。还有一个小改动我已经在实验了把severity: high的发现直接推送到IM群并且在消息里附上“这是Claude-Red自动发送请相关同学优先查看”。这么做的原因是MR评论太多很多人根本不会单独点开。推送能保证高优问题一定被看到。我在实际使用中最大的体会是给AI模型布置一个非常窄的任务远比让它全面审查要好。Claude-Red现在的价值恰恰在于它“只盯着红色删除行和新增行”不越界。它不试图替代任何人只是在人工reviewer快要麻木的时候安静地补上一句“等一下这里好像不对”。如果你也想做类似的事先别想着一步到位挑一个具体场景比如只分析条件语句变更、只检查配置变更跑通之后再慢慢加。窄任务才有稳定输出稳定输出才有人愿意用有人用才谈得上价值。