
最近我把手上的 PR review 流程整个重做了一遍核心是一个叫 ReviewAssist 的工具它用 coding session 来修代码把 GitHub 上那些 review 意见变成真实代码库里的修复动作然后进入 guided PR walkthrough 模式带你逐文件过 diff。这个过程最值钱的地方不在于“自动修代码”而在于它把传统 review 里最花时间的两个部分——理解上下文、逐条确认改动——都变成了可交互、可追溯的操作。我做了大半年重构中间也踩了不少坑。这篇文章把我当时为什么要这么做、内部是如何设计工作流的、实际跑一个 PR 的完整过程以及后来调优时踩到的几个典型问题都写出来。无论你是独立开发者还是团队里负责把关代码的人如果你也被 PR review 的来回拉扯折磨过这篇文章应该对你有用。1. 代码审查效率低问题的根子出在哪里1.1 三个耗时黑洞先说结论PR review 耗时间主要不是“看代码”本身而是耗在三个地方。第一个是理解上下文。你拿到的 diff 只是一堆增删行它不会告诉你“这里为什么这么写”“这个函数在哪个调用链上”“上一个版本的约定是什么”。于是你要么问作者要么自己切分支跑代码要么翻历史提交。这个动辄半小时一小时就没了。第二个是修琐碎问题。代码格式、命名不统一、缺失类型注解、明显的逻辑边界问题这些东西并不难修但修起来非常机械。传统工具只能静态检查出一部分真正到了 review 阶段reviewer 提的意见往往带着具体场景需要有人真的去改代码、跑测试、看结果这一个来回至少几小时。第三个是 review 的来回轮次。开一个 PRreviewer 提 8 条意见作者改完推一版reviewer 再进去看一遍又发现其中两条引入新问题于是再来一轮。每一轮都有人要切换上下文都有人要等待时间就被这样切碎了。1.2 为什么不能只靠粘贴代码块给 ChatGPT很多人已经尝试过“把 diff 贴给 ChatGPT让它帮忙审查或修复”我用过的感受是这能解决一部分解释性问题但没办法可靠地修代码。原因很直接你在聊天框里贴的代码片段是静态的AI 只能基于它看到的有限内容去猜上下文。它不知道你项目里有没有现成的工具函数不知道你的测试命令是什么不知道某个导入在当前分支是否已经存在更不可能在修改之后帮你跑一遍测试看有没有破坏其他功能。真正的代码修复是需要“动手”的读文件、查引用、改多处、运行测试、根据报错再迭代。这正是 coding session 这个交互模型的强项。所谓 coding session就是让 AI 在真实代码仓库环境里通过工具调用来完成读、改、跑、修的一套连续会话。ReviewAssist 的核心切换点在这里把 review 意见转成一个可执行的 coding session 任务而不是做一个一次性的“问一下”。1.3 ReviewAssist 的定位和边界ReviewAssist 不是又一个代码审查机器人。它不负责在 PR 上自动找 bug那些工作交给 linter、类型检查器、以及专门做 AI code review 的产品。ReviewAssist 解决的问题是 review 意见产生之后的两件事把已经被提出的、明确的修改意见转化为代码库里的实际修改在修改完成之后给你一个引导式的 PR 走查解释每个 diff hunk 为什么这么改、影响面在哪里、还有什么风险。如果你把传统代码审查比作“医生体检”那 ReviewAssist 更像是“体检之后的治疗方案执行器 复诊讲解员”。它接受已经诊断出的问题用 coding session 去执行治疗动作再用 guided walkthrough 把诊断结论讲得明明白白。我做过一个对比表方便看清楚各工具的分工工具类别典型产品能做什么不能做什么静态检查lint、type check发现样式、类型、潜在 bug理解业务意图、跨文件重构AI 代码审查各类 review bot自动审 diff、提意见在真实验证环境里动手修复聊天式修代码ChatGPT/Claude 网页端基于代码片段生成修改建议使用仓库完整上下文、跑测试并迭代ReviewAssist本文项目执行 review 意见、引导 PR 走查代替 reviewer 做价值判断这个定位直接影响后面所有设计我不需要它“聪明地发现新问题”我要它“忠实地把已经有的问题修掉同时让人类很容易验证换成”。2. 两段式工作流先让 coding session 修再带你走查 PR2.1 第一阶段把 review 意见转成可执行的修复任务最开始我设想的流程特别简单把 GitHub PR 下面所有评论拉出来一股脑丢给 AI 说“把这些都修了”。结果当然不行。review 评论里有的是真问题有的是讨论有的是疑问有的是“这里我建议…”这种非强制意见。如果全部当成任务执行AI 会改出一堆没必要的东西。后来我改成两步。第一步先做“评论分类”把每条 review 评论按意图分成fix、question、suggestion、nit、out_of_scope几类。只有fix和必要的suggestion会进入修复任务列表其他条目保留下来在 walkthrough 阶段作为讨论项展示。第二步是针对每一条fix单独构造一个任务条目而不是把整批意见合并成一个大 prompt。任务条目包含四个字段问题定位在哪个文件的哪个位置原代码是什么样的期望行为改成什么样算解决验收标准怎么验证这个修改没有引入新问题边界约束哪些文件不能动哪些逻辑不能碰。这一小步给我省了大量麻烦。任务拆得细coding session 里的每一步才会聚焦。AI 模型在长会话里的表现会随着任务范围膨胀而快速下降把任务拆细是提升修复准确率最有效的手段没有之一。任务列表生成之后ReviewAssist 会在本地创建一个隔离分支然后启动一个全新的 coding session。注意是全新的不是复用之前写代码时的会话。因为 review 修复需要尽量保持“最小 diff”不能把写功能时那些尚未完成的中间状态也带进来。2.2 第二阶段引导式 PR 走查怎么“引导”代码修完、测试通过之后ReviewAssist 会生成一个 draft PR但不会直接合并。接下来进入第二阶段guided PR walkthrough。这个模式的核心不是“展示 diff”而是“解释 diff”。普通的 GitHub diff 页面只是绿色和红色的行但不会告诉你为什么这么改。ReviewAssist 会把所有改动块按文件、按 hunk 组织起来并对每一个 hunk 生成一个三层结构的解释目的这个改动是为了解决哪条 review 意见做法用了什么方式修改涉及哪些函数或结构影响这个修改的影响面、对测试的影响、潜在风险。这相当于给每个 diff hunk 配了一个随行的讲解员。Reviewer 不再需要自己去翻上下文也不必问作者“这里为什么这样改”因为解释就在旁边。更有意思的是走查模式支持实时追问。你看某个 hunk 的时候可以直接输入“这里为什么不用工厂函数”“这个修改对那边批量导出代码有影响吗”ReviewAssist 会把问题发给 coding session让它结合仓库真实代码回答。这个交互方式比静态注释好得多因为它能动态地根据整个代码库去找答案而不只是围绕当前 hunk 打转。2.3 两个阶段之间的衔接从脏活到解释我见过很多 AI 编程工具的问题活干完了但没人知道它干了什么。ReviewAssist 在最开始设计时就把“可解释性”当作硬性要求。每个修复任务执行的中间过程都会被记录下来包括读过的文件、修改过的行、跑过的测试、以及测试失败的反馈。到了 walkthrough 阶段这些记录会被转换成人类能看懂的摘要。摘要里关键的一项是“意见处理状态”哪些意见被完全修复、哪些被部分采纳、哪些被 AI 判断为不适合自动修复并标记为needs-human。这个状态很重要因为不是所有 review 意见都能自动改。比如一条意见说“这个接口的设计风格和项目里另一个接口不统一建议重构”这种模糊且风险较高的意见最好还是让人来做判断。衔接部分还有一个细节修复产生的 commit 会被单独标成fix(review): ...形式这样在 PR 历史里能很清楚地看到每一轮 review 对应哪些改动。Reviewer 复查的时候可以直接看这些 commit而不是在混乱的fix typo、update这类提交里找线索。3. 实操案例一个 FastAPI PR 的完整修复与走查3.1 我实际跑的一个场景用户导出功能 PR我拿自己项目里的一个真实 PR 来演示这样更直观。这是一个 FastAPI 服务PR 内容是新增一个“用户导出 CSV”的接口。写代码的人已经实现了基本功能但心里没底把 PR 打开了让同事 review。同事在里面提了四条意见导出列表的循环里有 N1 查询用户关系对象被逐条加载日期格式化的代码在两个地方重复了导出接口的响应模型缺少类型注解没有处理用户列表为空的情况直接导出会生成一个只有表头的空文件。这四条有两条属于明确的fix一条属于suggestion一条属于边界逻辑问题。按照我前面说的流程前三条进入修复任务列表第四条和它对应的边界测试会一起处理。3.2 从评论到任务清单的拆解过程ReviewAssist 用到的指令大概长这样reviewassist fix \ --repo . \ --pr 42 \ --target-branch main \ --output-fix-branch reviewassist/fix-pr-42这个命令会自动拉取 GitHub 上 PR 42 里的 review 评论然后进行分类和任务拆解。实际生成的内部任务列表类似这样tasks [ { id: TASK-101, opinion_ref: review_comment_7, location: app/routes/export.py:120, problem: 用户列表循环中懒加载 user.profile导致 N1 查询, expected: 使用 selectinload 一次性加载 user.profile 及 orders, verify: pytest tests/test_export.py -k n_plus_one or export_users, constraint: 只修改 app/routes/export.py 和 app/schemas/export.py, }, { id: TASK-102, opinion_ref: review_comment_9, location: app/services/export_service.py:45, problem: 日期格式化逻辑与 app/utils/date_utils.py 中重复, expected: 提取到公共函数 format_export_date 并替换两处调用, verify: pytest tests/test_export.py -k date_format, constraint: 不得修改 date_utils.py 中已有函数的对外签名, }, ]注意这里面的verify字段。没有这一条AI 在 coding session 里很容易“改完就默认成功了”。有了验证命令coding session 就必须真实地跑一遍测试并根据测试结果决定是否继续修改。3.3 一次真实的 guided walkthrough 对话修复完成、测试通过之后ReviewAssist 进入走查模式。下面是我从一次真实对话里摘出来的片段模拟了它的界面输出文件: app/routes/export.py Hunk: -118,7 128,9 目的: review 意见 #7 指出导出用户列表时存在 N1 查询。 这里将查询改为 selectinload关联用户 profile 和最近订单。 做法: List[User] 的查询条件中增加 selectinload(User.profile) 并把原来的循环内访问 user.profile 改为直接读取已加载属性。 影响: 在 1000 个导出用户的情况下响应时间从 1.8s 降到 220ms。 风险点: selectinload 会把关联对象一次性全部加载 如果未来某个用户拥有数万订单内存会明显上升。 建议后续在导出处增加分页或限制加载的订单数量。我看完这个解释会觉得“这个改法是有道理的”同时也能看到 AI 主动标出了它认为可能出问题的边界点。这就比一个单纯把 selectinload 塞进去的代码修改要可靠。然后是追问环节。我在走查界面里输入如果用户关联的订单数量非常大这个导出接口会不会把整个数据库拖垮ReviewAssist 没有直接说“会”或“不会”而是做了一次真实查询然后回答当前测试数据里最大订单数为 240 条selectinload 在实际场景中会生成一个WHERE user_id IN (...) AND order_id (SELECT MAX(...))的关联过滤。直接加载所有订单确实有风险。建议在app/services/export_service.py里增加 annual export 上限或者改为迭代查询。需要我生成一个对应的限制补丁吗这种“主动给出进一步修复建议”的能力来自 coding session 的完整环境而不是聊天框里的上下文猜测。3.4 为什么选 FastAPI 做演示其实和语言无关很多朋友问过ReviewAssist 是不是只能用在 Python 项目里。不是。我用它处理过 TypeScript 的 NestJS 项目、Go 的微服务仓库甚至还有过一次修 Ruby on Rails 迁移文件的经历。原理上它没有绑定任何语言核心机制是让 coding session 理解项目自身的构建、测试和错误信息。真正需要适配的是“验证命令”这一块。Python 项目用 pytestNode 项目用 npm testGo 项目用 go test。ReviewAssist 的任务清单模板里有一个verify字段不同语言填不同的命令即可。遇到测试基建不完善的项目我会先在任务里加一步“写一个最小冒烟脚本”再谈修复问题。没有验证动作的自动修复本质上只是“生成一堆看着合理的代码”跟完整修复是两码事。4. 关键设计决策指令构造、最小 diff 约束和安全边界4.1 coding session 和一次性补丁的差别很多人把 coding session 理解成“多轮对话”实际上它的差别主要在于工具调用。常规聊天式补丁模型只有一次机会根据已有上下文生成 diff而 coding session 里模型可以反复执行grep、read、edit、run test这类操作每一步都能看到真实反馈。举一个例子。修 N1 查询时如果只是让模型看一段代码它可能会在查询语句里加一个看似正确但实际上不存在的关联字段。因为模型不知道你的 ORM 类具体定义了哪些 relationship。但在 coding session 里它会先打开模型文件确认relationship(profile)和relationship(orders)真的存在然后才动手改。这就是“动手查”和“凭记忆猜”的区别。这个差异对 review 修复尤其重要因为 review 意见通常很具体但修起来往往要跨文件。一次性补丁没办法对“改动后测试是否还通过”负责而 coding session 可以。4.2 指令模板的核心结构我在实践中把 coding session 的 system prompt 不断精简最后留下了下面这个核心模板每一个项目都从这里开始你正在处理一个代码仓库。任务是修复一组由代码审查提出的问题。 请严格按照以下工作方式执行 1. 先读取任务列表中的每个问题确认涉及的代码位置。 2. 在修改前用 grep 或全局搜索检查是否存在已有实现。 3. 每完成一个任务先运行该任务对应的 verify 命令 只有验证通过后才进入下一个任务。 4. 遵守最小 diff 原则 - 只修改任务列表中列出的问题 - 不改变无关代码的格式、命名和注释 - 不顺手重构即使你看到了更好写的表达方式。 5. 如果某个任务在 3 次尝试后仍然无法通过验证 停止修改将任务标记为 needs-human并说明失败原因。 6. 最后输出一个修改摘要列出每个任务的状态。这里面最容易被忽略的是第 4 条“最小 diff 原则”。我在早期版本里没写这条结果 AI 修了 3 个问题顺手改了 7 处和问题无关的格式reviewer 又得额外做一轮判断。加上这条之后无效 diff 的比例大幅下降。4.3 安全边界隔离分支、沙箱和 diff review 代理让 AI 在一个真实仓库里执行修改是有风险的。我从一开始就设计了几道安全闸门。第一道闸门是分支隔离。ReviewAssist 永远不会直接在main分支上改东西。它会基于当前 PR 的最新提交创建一个新的工作分支所有 coding session 的动作都发生在工作分支里。最后推送到远程的也是一个 draft PR不会触发自动合并。第二道闸门是文件保护。有一些文件我明确列入了“AI 不要碰”列表包括lockfile、数据库迁移、CI 配置、.env.example等等。这些文件要么是机器生成的要么对安全性影响重大不应该由 AI 在 review 修复过程中顺手改掉。第三道闸门是 diff review 代理。AI 完成所有修改后会有一个独立的代理对最终 diff 做一次安全审查。它检查的事情包括修改文件数是否和任务列表一致、有没有跑出任务范围之外的代码改动、有没有新增可疑的网络请求或执行系统命令、有没有删掉测试。如果 diff review 代理发现问题它会拒绝进入 walkthrough 阶段并把异常标记出来。这套安全设计并不是“限制 AI”恰恰相反它是为了让人类 reviewer 能够放心地把注意力放在真正的业务问题上而不是反复检查 AI 有没有搞小动作。边界越清晰信任成本越低。5. 用了一段时间后踩过的坑和调优建议5.1 坑一AI 修 bug 时顺手“重构”了无关代码这是我最先遇到的问题。当时跑一个 Python 项目任务是修复一条列表推导式里的潜在空指针问题。AI 修完之后我一看 diff除了目标行它把相邻函数里的变量名从d改成date还把几处字符串拼接改成了 f-string。这个过程我后来排查了很久。原因在于 coding session 的上下文窗口很大模型看到某个文件时会自发地“优化”它觉得不爽的代码。这不是恶意而是生成模型的天性。要解决它单靠 prompt 里的“不要顺手改”还不够需要在 diff review 代理里加一道硬性检查统计每个文件的变更行数凡是变更行数显著超出任务预期的文件就直接拦下来并提示人工检查。我在调优时把以下规则写进了 diff review 代理文件变更数不得超过任务列表目标文件的 1.3 倍单个文件变换行数如果超过 50 行必须额外解释不允许删除测试文件不允许修改锁文件。加上这些规则之后我记忆中只出现过一次越界而且很快就被代理拦住了。5.2 坑二测试没跑通它照样敢提交听起来很不可思议但确实出现过。某个 Node 项目里AI 完成了一段修改命令行的输出里能看到npm test有 3 个失败用例但它在最后提交时仍然把改动 push 了上去。我后来看日志才发现模型把“运行测试”看成了一位“加分动作”而不是“完成任务的必要条件”。它认为哪怕有失败只要主目标实现了也还是可以提交。这个问题的最有效解法是把验收标准写进任务定义里并且让测试命令成为提交前不可跳过的原子操作。具体来说ReviewAssist 的 coding session 循环里有一个强制步骤在提交之前必须读一遍当前工作区测试命令的退出码如果退出码非零就禁止执行提交动作。这不是靠提示词而是靠会话控制逻辑来保证。后来我又加了一个辅助策略在任务列表生成阶段故意要求每个任务都带一个最小测试。如果项目本身没有对应测试文件就让 AI 先补一个能覆盖该问题的失败用例然后再修复代码。也就是让red-green-refactor成为流程的一部分。这会让修复速度慢一点但可靠性高很多。5.3 坑三走查解释变成论文刚开始设计 walkthrough 时我让 AI 对每个 hunk 写“详细解释”结果它把背景知识、相关代码、历史原因全堆上去一个 200 行的 PR 能生成四千字的解释reviewer 根本看不下去。后来我把解释结构调整成了前面说的三层目的、做法、影响。为了进一步压缩还在模板里加了一条硬约束每一层不得超过三句话。如果模型发现解释太长就必须自己提取核心信息把次要内容放到“折叠区”里。你点击才展开默认只看三层摘要。这个改进效果非常明显。走查的平均时间从四十分钟降到了十几分钟。reviewer 在日常工作中其实只需要知道三件事为什么改、怎么改、有什么影响。给再多都是在消耗注意力。5.4 调优清单和参数建议我把这段时间调参的经验整理成一个参数清单方便你根据自己的项目情况调整参数建议值说明任务粒度一条意见一个任务太大容易发散太小增加 overhead最大尝试轮次3 次超过直接标记 needs-humandiff 阈值变更文件数不超任务文件数 1.3 倍防止越界修改走查解释长度每层最多 3 句保持信息密度验证命令每个任务对应一条最小测试命令比跑全量测试更快定位问题文件保护列表lockfile、迁移、CI根据仓库定制你不需要一开始就全部照搬。我建议先跑 2 到 3 个 PR看看 diff review 代理拦下了什么、漏掉了什么再针对你自己的项目调整这些参数。每个团队的代码风格和 CI 流程都不一样完全照搬一套参数反而不一定顺手。我从开始用 ReviewAssist 到现在最大的感受是不能把它当成“自动修复机”而应该当成“帮你把重复劳动干了但留着判断力的结对者”。它真正节省的是那些机械性的修改和上下文切换时间但代码改得对不对、边界处理得妥不妥最后还是要人来拍板。走查模式里 AI 那句“建议增加边界判断”很多时候比它直接改掉代码更有价值因为它把风险点摆到了你面前而不是默默替你做了决定。一个我自己用了都说好的小技巧每次修复完成之后先别急着进 walkthrough而是让 AI 先用同样的走查框架把它这次的改动自己检查一遍。这一步往往能提前发现一些逻辑上的遗漏比如测试用例覆盖不全、边界条件没处理干净。等它自己走查完毕再进入正式 walkthrough体验会顺滑很多。