open-code-review:基于规则引擎与静态分析的自动化代码审查实践 1. 从“PR审查靠人肉”到“机器先看一遍”open-code-review的诞生动机工作这几年我越来越觉得代码审查是一件“不得不做、又很难做好”的事情。说它不得不做是因为大部分线上事故、隐患、坏味道如果在合并前能被review环节拦下来成本是最低的说它很难做好是因为指望每个开发者都带着足够的上下文去认真读diff本身就是反人性的。尤其当PR越来越大、改动涉及多个模块时reviewer打开文件列表的心态基本就是“赶紧过一眼别出大岔子就行”。我见过太多这样的时候一个本质上很容易被静态规则发现的错误比如把生产环境的API地址硬编码进去了、资源没有释放、日志里直接拼了用户输入非要在代码上线之后由监控告警来提醒大家“这里不对劲”。这中间的时延可能是一个小时也可能是一个通宵。问题在于人力review天然有注意力上限而机器没有。所以我就想能不能做一个开源的自动化代码审查工具在开发者提交PR之后、人工review之前先让机器“读”一遍改动把那些有客观依据的问题直接以评论的形式贴在对应代码行上。这就是open-code-review这个项目最初的起点。坦白说这个想法并不新鲜市面上有SonarQube、CodeQL、Reviewdog之类的工具但我在实际使用中总觉得差点意思有的太重跑一次要起一堆服务有的规则写起来门槛太高团队里没人愿意维护还有的根本没法按“只检查本次改动”来工作动不动就全量扫描在稍微大一点的仓库里体验很糟。我想要的不是一个大而全的代码分析平台而是一个轻量的、能融入日常开发流程的“PR审查助手”装起来简单规则容易写默认行为不烦人。open-code-review最终补齐了我工作流里最拧巴的那一段。它不是一个替代人工review的银弹而是一个把机械劳动前置的过滤器。它负责那些“有明确依据、无需主观判断”的检查项把人解放出来去关注设计合理性、可维护性、业务逻辑这些机器难以判断的问题。写这篇文章是希望把这套工具从设计思路到落地实践完完整整地讲清楚。如果你正在纠结“自动化代码审查到底怎么落地”“怎么让机器人评论不被开发者讨厌”这篇应该能给你不少参考。2. 核心架构与审查流水线一个PR进来之后发生了什么2.1 技术栈选型为什么是Python FastAPI tree-sitter第一个要解释清楚的问题是技术栈。open-code-review的服务端用Python 3.11 FastAPI搭建语法解析用tree-sitter存储先用SQLite兜底。选Python而不是Go或者Node主要理由是生态和迭代速度。Python在处理文本、脚本化逻辑、写小工具方面非常顺手FastAPI的异步能力和自动文档在接收Webhook请求、回调GitHub API这些场景下足够好用。虽然Python的运行时性能不如编译型语言但代码审查这个场景并不是高并发低延迟的在线搜索瓶颈在分析逻辑本身而不在HTTP框架FastAPI完全够用。语法解析这块tree-sitter是真正改变游戏规则的东西。早年的静态分析工具普遍依赖正则表达式或者Perl风格的文本匹配效果很差正则无法理解嵌套结构稍微绕一点的表达式就漏报。tree-sitter能在线性时间内把源码解析成具体的语法树而且增量解析能力非常强非常适合“只分析PR里改动的代码”。它原生支持几十种语言每种语言都有官方的grammar包这让我不需要为每一种语言单独写词法/语法分析器。举个例子要判断“新增的这行代码里有没有条件判断里漏了大括号”用正则去嗅探花括号是不靠谱的但用tree-sitter拿到AST之后直接查if节点的consequence子节点是不是block——这样的逻辑是确定性的、可测试的。2.2 Webhook接收与事件过滤让服务只关心“该管的事”服务的入口是一个Webhook端点接收GitHub App推送过来的事件。这里必须要做事件过滤否则你的服务会被各种无关事件淹没push、issue、star、workflow_run……这些事件里绝大部分跟代码审查没关系。我只关心两类事件pull_requestPR的opened、synchronize、reopened。这对应“新PR提交”和“新commit推送”两种需要发起审查的时机。pull_request_review_comment这用来监听开发者在评论区的回复方便实现“按评论回复重新检查”的交互。每次收到事件后先验证签名确认请求确实来自GitHub再解析事件类型。如果是无关事件直接返回200并忽略。这一个简单的过滤层能省掉后面整个分析流程的大量无谓计算。2.3 增量审查只分析diff里的“新增行”open-code-review最重要的设计原则之一是永远只审查本次改动新增的行。这里说的“新增行”是GitHub diff语义下的概念即改动后文件中那一行前面带的行。为什么强调这个因为代码审查的对象是“这次改动引入的变化”改动之前就存在的历史问题不应该作为这次PR的阻塞项。全量扫描的历史遗留问题应该由重型的定时扫描工具去发现并跟踪而不是堵在某个feature PR的门口。实现上我调用GitHub的GET /repos/{owner}/{repo}/pulls/{pull_number}/files接口拿到本次PR的改动文件列表和patch内容然后解析每个文件的patch提取出每一个“新增行”对应的行号、代码文本以及该行所在的文件路径。这个“三元素组合”是后续规则引擎的输入。不过这里有个细节值得展开说说。GitHub的patch格式对“新增行”的定位是准确的吗基本准确但要注意一种情况pr的diff默认是不包含上下文的也就是说你拿到的是一个精简的patch。GitHub API允许通过Accept: application/vnd.github.v3.diff拿完整diff但更稳妥的做法不是依赖patch文本本身而是基于patch计算出每个新增行在当前文件最新版本中的准确行号。因为在PR合并过程中文件可能已经被其他commit改动过用旧的patch去生成评论会定位到错误的位置。我的做法是把base_sha和head_sha范围里的文件逐一处理计算出来的是“head版本中的真实行号”这样发评论的时候GitHub能正确识别行号并挂在对应代码上。2.4 语法树构建与规则匹配从“看到一行”到“理解一段”拿到新增行之后下一步是把整份文件解析成语法树然后“挂”上所有新增行的信息。这里的关键点是虽然规则引擎只检查新增行但新增行所在的上下文比如它所属的函数、类、代码块必须要能获取到。只拿一行的文本来做检查效果等同于正则扫描很多需要上下文判断的规则根本做不了。tree-sitter的语法树会给出每个节点的起始行和结束行。我把所有新增行号整理成一个集合然后遍历语法树里每个有逻辑意义的节点函数定义、函数调用、赋值表达式、if/while/for分支等凡是这个节点的起始行落在新增行集合里或者节点本身跨越了某个新增行就认为这个节点是“本次改动涉及的节点”需要被送入规则引擎检测。这一层抽象很值得多说两句。它让规则的编写方式从“检查某行文本”升级为“检查某个AST节点”规则因此可以访问到节点的类型、子节点、父节点、属性等信息。比如要检查“新引入的ERRO日志里是否拼接了动态字符串”规则拿到的是一个call_expression节点能精确提取出日志宏名的参数列表再从参数里找有没有字符串字面量之外的表达式节点。这种检查的准确度和可解释性远远不是正则能比的。2.5 评论生成与去重别让机器人变成“噪音制造机”规则引擎跑完之后会得到一组“发现项”。这里面临的第一个问题就是要不要全部都发到GitHub上我的答案很明确不要。这也是open-code-review和我早期写的一些一次性脚本之间最大的区别。早期版本我图省事每个发现项都发一条review comment结果第一个试用周就被同事吐槽“机器人疯了”。噪音评论会导致两个后果一是开发者产生“狼来了”疲劳忽略所有机器人评论二是review页面被垃圾信息占满真正重要的提醒反而看不到。所以现在的管线是发现项先进入“去重和聚合”模块同一文件的同一行只保留一条评论按检查规则分组把同类型的小问题聚合为一条汇总评论超出阈值的高优问题才单独作为review comment发送。去重逻辑在实现上是用一个字典以(file_path, line_number, rule_id)为键做唯一性约束。处理完后按规则severity排序P0/P1级的发现项单独发review commentP2/P3级别合并成一个带分点的review summary发在PR的时间线上。3. 本地部署与GitHub App接入跑通首条自动审查3.1 部署形态Docker Compose一个命令起服务open-code-review的服务端我打包成了Docker镜像部署方式用的是Docker Compose。之所以坚持容器化是因为这套东西本质上是个“常驻后台等待Webhook”的服务如果部署形态过于复杂团队里的同事大概率不会去碰它。docker-compose.yml的核心配置非常简洁services: ocr-server: image: ghcr.io/yourname/open-code-review:latest ports: - 8420:8420 volumes: - ./data:/app/data - ./config:/app/config environment: OCR_PLATFORM: github OCR_PLATFORM_TYPE: app OCR_WEBHOOK_SECRET: ${OCR_WEBHOOK_SECRET} OCR_APP_ID: ${OCR_APP_ID} OCR_APP_PRIVATE_KEY_PATH: /app/config/private-key.pem restart: unless-stopped这里有两个环境变量值得专门提醒。OCR_WEBHOOK_SECRET是Webhook签名校验的密钥OCR_APP_PRIVATE_KEY_PATH指向GitHub App生成的私钥文件。私钥的安全性是这套系统里最需要注意的部分——它相当于你这个机器人在GitHub上的身份证泄露了别人就能以你的App身份读写仓库。我的建议是私钥通过环境变量或secret文件注入不要提交到Git仓库。即使你的代码仓库是私有的也不要做这个假设。3.2 创建GitHub App权限和事件订阅别搞错要让open-code-review以“应用”的身份自动发评论需要在GitHub上创建一个GitHub App。这一步有几个权限项不能配错否则会出现“能收到事件但发不出评论”的诡异问题。在GitHub App设置页面里关键的配置如下Webhook URL指向你部署服务器的/webhook路径。Webhook Secret填写一个随机字符串后面在服务端配置同一个值。PermissionsPull requests: Read Write——这是发评论必需的Checks: Write——如果要用Check Run展示审查结果需要这个权限Metadata: Read——GitHub App的基础权限。Subscribe to eventsPull requests——这是核心事件源Pull request review comment——如果用“评论触发重新检查”的交互需要这个。创建好App之后会得到一个App ID和一个私钥文件.pem。App ID填到环境变量里私钥文件放到服务器上。然后在仓库页面的Settings → GitHub Apps里安装你创建的App授权给它所有仓库或者指定仓库。这里有一个新手最容易踩的坑安装App的时候如果仓库是org级别的需要org管理员来批准安装。而且App安装完成后要检查一下仓库的Settings → GitHub Apps里是否已经能看到这个App并确认它能访问该仓库。如果App装了但权限不够GitHub会静默拒绝API调用日志里只会出现403。3.3 事件签名校验防止伪造Webhook请求Webhook端点没有认证裸奔在公网上是一件极度危险的事。攻击者如果知道你服务的地址完全可以伪造一个PR事件诱导你的机器人评论或拒绝评论甚至收集你仓库的元数据。GitHub的Webhook签名校验做得比较完善每个请求头会带X-Hub-Signature-256值是HMAC-SHA256计算的摘要密钥就是你在App里配置的Webhook Secret。服务端收到请求后用同样的Secret计算一遍比对是否一致不一致的直接拒绝。import hmac import hashlib def verify_signature(payload_body, received_signature, secret): expected hmac.new( secret.encode(utf-8), payload_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(fsha256{expected}, received_signature)这里我用hmac.compare_digest而不是直接用比较字符串是为了规避时序攻击。虽然对Webhook场景理论上不构成实际威胁但养成这个习惯不是坏事。3.4 首条审查的完整运行链路从PR到评论跑通全流程之后一条PR从提交到收到机器人评论大致会经历下面这个链路开发者在分支上提交代码push到远端然后创建PR。GitHub检测到pull_request事件向你的Webhook地址发送POST请求。open-code-review验证签名确认事件合法。服务端调用GitHub API获取PR的改动文件列表和patch。解析每一个文件的新增行构建“待检查项”。用tree-sitter解析改动目录下的源码文件把新增行映射到AST节点。规则引擎逐条匹配产出发现项集合。去重聚合后把P0/P1评论直接贴在对应代码行上P2/P3写入PR summary。PR页面上出现机器人的review评论。我建议第一步先用一个最小的测试仓库跑通这9步。故意写一个有明显问题的PR比如在Python文件里import os后直接用os.system(user_input)或者一个文件里留下print调试输出。确认评论能成功发送并且定位准确再去接到真实仓库上。小范围验证能帮你隔离问题避免一上来就被大量真实仓库的噪音淹没。4. 规则引擎设计从默认策略到团队定制化风格4.1 规则配置的一切起点YAML声明式文件open-code-review的规则引擎是插件化的每一条规则是一个独立的检测函数但规则的开关、参数、严重级别都是通过YAML配置文件来声明。一个完整的规则配置看起来长这样rules: - id: no-missing-f-string name: 检测可能遗漏f-string的字符串拼接 category: correctness severity: P1 languages: [python] enabled: true settings: min_length: 40 require_format_chars: true设计规则时我主要遵守几条原则规则必须可解释。发现的每个问题都要能输出清晰的原因说明和修复建议不能让开发者看到评论后一脸茫然。规则必须可关闭。任何规则都可能在某团队里不适用配置里关掉总比改代码省事。默认只开最有把握的规则。新加的规则先以severity: P2灰度一段时间观察误报率稳定后再提升级别。4.2 内置规则库的四个分类open-code-review内置了一批规则按类别分主要是四块类别典型规则说明正确性correctness空指针/空值检查缺失、使用未定义变量这类规则基本不给误报留空间值得优先打开安全security检测os.system、eval、SQL字符串拼接、日志注入能拦住一批初级但后果严重的错误性能performance循环内重复调用固定结果函数、无效的数据库N1模式依赖少量AST结构判断不适合复杂跨过程分析风格style调试代码残留、TODO未关联工单、导入顺序混乱误报率极低适合作默认开启项正确性类规则是这四类里性价比最高的。比如在Python里检查“调用了可能返回None的函数的返回值接下来直接取属性”这种检查在AST层面能做到非常高的准确度因为模式非常明确先捕获一个call_expression再看它是否作为attribute节点的对象被使用最后确认被调用函数的返回值有Optional标注。安全类规则需要小心。os.system这类高危函数的调用检测很简单但“SQL注入”这种模式如果规则写得太宽会把很多ORM调用误报成问题。我的经验是安全类规则宁可保守不要激进。低误报率对工具的口碑太重要一旦机器人被当成“乱叫的看门狗”后面它说的话就没人信了。4.3 分析一个自定义规则的实现secrets硬编码检测规则不局限于AST模式匹配也可以做自定义的数据流分析。这里拿“检测API密钥硬编码”这条规则来走一遍实现思路。AST上“硬编码字符串”就是一个string节点但它大概率是配置里的固定文案不是密钥。要减少误报需要一个启发式判断字符串的值看起来像密钥而不是普通文本。我采用的启发式规则是字符串长度≥20字符字符串熵值较高说明不是人工可读的普通词汇赋值目标的变量名包含key/token/secret/password/credential等特征词字符串出现在配置初始化之外的位置。在tree-sitter的语法树里赋值语句的左侧是assignment[left]节点右侧是assignment[right]节点。规则函数拿到赋值表达式后检查left里有没有特征词再检查right的类型是不是string最后对字符串内容算一个信息熵超过阈值就判定为疑似密钥触发P1警告。这个规则在真实仓库里跑误报率能控制在几个百分点以内偶尔会把数据库连接串、长URL误判成密钥但已经足够有用。要让误报率再下降可以引入正则白名单把https://、eyJJWT前缀这些明确模式排除掉。4.4 抑制机制如何得体地处理“你说的对但这个不用改”规则引擎再好也无法替代人的判断。代码里总会有一些“故意的偏离规范”比如访问一个老系统接口必须要拼参数、某个遗留模块的编码风格和团队规范不一致。这些情况如果机器人每次都跑来评论开发者就会开始恨这个机器人。所以open-code-review支持行内抑制注释。开发者在想让机器人安静的那一行代码下面可以加一行注释# open-code-review-ignore: legacy-api-param-format resp legacy_api(paramraw_str)规则引擎在命中某个节点之后会先检查这个节点对应的代码行以及往上两行内是否存在声明了当前规则ID的抑制注释。如果有就把这条发现项标记为“suppressed”既不会发评论也不会记入统计。加上这个机制之后我在团队内部推行的阻力明显下降了。开发者不会觉得机器人是个“强制规范警察”而是可以与之达成协议“这片区域我承认不规范但这是我特意保留的。”5. 误报治理与阈值调优实测数据的完整复盘5.1 误报来源分析规则是死的代码是活的任何静态分析工具都会产生误报false positiveopen-code-review也一样。我跑了一整个季度的实际数据统计下来误报主要来自三个源头。第一类是上下文信息不足导致的误判。比如我只看到user.name这个字段的赋值并不知道user实际上是一个dict而不是对象也不知道这个user在另一个模块里可能为None。AST层面能拿到的是语法结构拿不到类型信息。这类误判只能靠增加类型推断或引入更重的分析来缓解代价很高所以我目前的策略是规则只处理“不需要类型信息也能确认模式”的场景。第二类是模式匹配过于宽松。有些规则为了减少漏报把匹配条件写宽了导致大量本不该被检查的代码进入检测范围。这种情况的根因在规则本身解决办法是把匹配pattern细化。第三类是对框架/库的行为理解不足。比如Django的ORM查询很多安全检查会把User.objects.filter(usernamerequest.GET[name])误判为SQL注入风险。但这行代码在Django里是参数化查询不会有注入问题。这类误报积累多了会让开发者对整个工具的信任度崩塌。5.2 通过一个真实误报案例看根因与修复有一个印象很深的误报来自初始版本的no-subprocess-shell-true规则。规则的目标是检测Python代码里subprocess.Popen或subprocess.run的调用中shellTrue的情况因为这是经典的命令注入风险点。规则实现时我检查call_expression节点看函数名是不是Popen或run再找关键词参数shell的值是不是字面量True。听起来没问题但上线后误报率极高。原因出在很多人会用包装函数def run_cmd(cmd, use_shellFalse): return subprocess.run(cmd, shelluse_shell)调用run_cmd(ls)时规则看到的是subprocess.run被调用shell参数的值是use_shell这个变量不是字面量的True。但规则内部对“不是True字面量的值”统一视为不命中所以这条规则漏报更麻烦的是有一种写法FLAG True ... subprocess.run(cmd, shellFLAG)这种shell的值从变量传递我的第一版规则把它当成了“潜在风险”命中。但从数据流分析的角度看FLAG是模块级常量值恒为True风险是真实的。问题在于规则没法区分“运行时才确定的变量”和“常量折叠后的True”之间的区别。最终修复办法是引入一个非常轻量的“模块级常量追踪”如果shell对应的变量在模块顶层被赋值为True或False字面量则追踪到值如果变量在函数内被参数赋值且调用点的参数不是字面量则降级为P3提醒而不是P1告警如果完全无法追踪则不再触发避免误报。这个修复让该规则在后续测试中的误报下降了大约七成。5.3 阈值调优方法论先低阈值跑数据再逐步收紧很多人以为规则引擎上线时就应该全量打开所有规则把severity全部调到最高。我的经验恰恰相反先把所有规则开在P2/P3跑两个星期的真实数据统计每条规则的触发次数和人工确认的误报率再决定哪些提升为P1、哪些直接关闭。我整理的一套流程是这样的灰度观察期所有默认规则以P3级别运行只发汇总评论不单独评论每一行。这样即使有误报对开发者的打扰也小。垃圾邮件率统计每周抽检一定比例的评论让2-3个核心维护者给每条评论打标有用/误报/无效。规则分级调整当某条规则的误报率低于5%提升为P1低于15%维持P2超过30%降级P3并标记“待优化”。一次性清理对历史评论不做回溯只对后续新改动生效。这套流程看似保守实际效果非常稳。开发者不会因为刚接入机器人就抱怨连连反而是缓慢地接受并开始信任它。信任这个东西建立起来很慢崩塌只在一瞬间误报治理的本质就是在维护信任。5.4 指标采集用什么数字判断这个工具好不好运行一段时间后如果不看数据你根本不知道这套工具到底有没有在创造价值。我会关注几个关键指标每条PR平均评论数理想状态是1~5条再多了就说明规则太吵。P1评论的采纳率开发者是否真的按照机器人的建议修改了代码。GitHub API可以查评论对应的commit查看开发者后续是否改动这一行。平均审查前置时间从PR创建到机器人给出评论的时间目标是30秒内。超过这个数开发者的等待感会很明显说明分析性能需要优化。规则误报率通过人工抽检来估算。我见过不少团队把“发现多少问题”当成KPI这其实是个误区。机器发现的问题再多如果开发者不看、不采纳价值就是零。真正重要的指标是“采纳率”和“被采纳的问题里有多少是严重问题”。这个认识和我在做code review时积累的经验一脉相承问题的价值不在于数量而在于是否在正确的时间用正确的方式被解决。6. 在CI/CD流水线中的落地经验与延伸玩法6.1 与GitHub Actions集成让检查结果变成正式Check Runopen-code-review作为GitHub App运行天然支持把审查结果同步为一个Check Run。这样在PR页面的Checks页签里开发者能看到“open-code-review: completed successfully”或者“action required”的状态而不只是在评论区看到一堆评论。Check Run的API是POST /repos/{owner}/{repo}/check-runs需要在请求体里传name、head_sha、status、conclusion这几个字段。审查完成后如果存在P1以上发现项conclusion设为action_required否则设为success。把阈值判断交给CI集成层规则引擎本身只负责产出发现项这个解耦让同一套引擎能适配不同的工作流。6.2 本地开发阶段的前置拦截pre-commit钩子服务端检查是在PR阶段才发生的但有些简单问题完全可以在开发者本地提交时就被拦住不用等到PR。给open-code-review配一个本地模式本质上就是跑一部分轻量规则。实现上我提供了一个CLI子命令open-code-review local --lang python --files src/main.py它只解析传入的文件跑正确性和风格类规则输出到stdout。这个命令可以挂进pre-commit框架- repo: local hooks: - id: open-code-review name: open-code-review entry: open-code-review local --output-format compact language: system files: \.(py|js|ts|go)$这个模式的定位不是完整的代码审查而是一个快速反馈。它只检查“当前分支相对main的改动”需要先把diff文件传给它。实现时用git diff --unified0 main...HEAD --拿到新增行集合再基于这些行做检测。6.3 Monorepo场景下的挑战增量范围识别比想象中复杂在Monorepo里一次PR可能涉及几十个包、几百个文件。如果把整个仓库所有文件都解析一遍树都要建很久而且会产生大量跨模块的噪声评论。我的做法是引入“改动边界识别”先根据PR改动的文件路径找到对应的子项目目录比如packages/foo只解析这些子项目目录下的源码文件如果改动的文件涉及共享依赖目录比如packages/core则对该目录做一次全量化解析但只针对“依赖该目录的已改动模块”做检查。这个策略能在合理的时间内完成Monorepo的增量审查但成本是不能准确分析“跨包的类型影响”。比如packages/foo里的函数签名变了packages/bar里调用了这个函数的地方会编译失败但open-code-review看不到这种类型级的影响。这类问题通常要交给CI的构建阶段去发现而不是靠审查工具。6.4 从“PR机器人”到“提交信息Bot”另一个实用扩展最后说一个我在实际使用中觉得价值很高的扩展把open-code-review的检测能力延伸到commit message层面。很多团队PR非常规范但commit message一团糟导致后续git bisect、版本发布生成变更日志时很痛苦。我给open-code-review加了一个简单的commit message检查器在每次push事件里扫描新增commit的message看是否满足约定非空且长度在10~100字符之间以类别前缀开头feat/fix/refactor/docs等不包含“fix issue”这种无意义描述。这条规则不依赖AST属于纯文本分析但因为它挂了独立的规则ID可以在配置里单独开关。我在实际项目中看到不少因为“commit message太拉胯”导致的发布日志残缺问题这个功能虽然朴素的像小工具但弥补了常规代码审查工具的一个盲区。6.5 团队的采纳曲线从“机器人好烦”到“等等它怎么还没评论”工具上线过程中最难的从来不是技术而是团队文化和习惯的转变。我在团队里推open-code-review时前期最有效的三个策略是第一先在小仓库试点找一个维护积极、PR量适中的项目跑两周攒一批正向体验数据。不要在第一天就推到全组织所有仓库那只会让所有人同时感受到“被机器人盯着”的压力。第二让机器人“学会说人话”。评论内容我默认不是冷冰冰的“Detected violation: XXX”而是附上具体的修复示例和代码片段。开发者看到一条评论能快速理解“为什么这里是问题、怎么改”配合意愿会大幅上升。第三给P1评论设一个“静默失败”的兜底机制。如果审查服务本身出了故障宁可静默失败也不要发错误评论。给Webhook加上X-GitHub-Delivery日志、给Check Run加上超时态确保机器人挂了不影响CI真实性。到现在团队成员反而会主动问“这个PR怎么没收到机器人评论”因为他们的安全感已经建立在“机器先看一遍人再看一遍”这个双重保险上。这个转变大概就是工具做得“不烦人”的最好证明。