如何实现高质量的 AI Code Review
第 4 章 · 85–105 分钟

采纳率低:问题为什么看起来对,却没人采用

低采纳率通常不是一个模型问题,而是一组可以沿规则、上下文、建议和反馈证据逐层定位的系统问题。

查看 Markdown 版本

先看四条“没有明显错误”的建议

同一批代码审查里出现了四条候选问题。为了公开教学,代码与业务名已经改写,但拒绝理由保留了原始结构。

第一条说:“请求头可能为空,直接访问字段会触发 panic,建议提前返回。”开发者拒绝,因为空请求头在这条业务链里并不表示非法输入,转换函数仍需产出一个可继续处理的对象。AI 发现了语言层面的空指针风险,却把业务允许的空状态当成了异常状态。

第二条说:“某个对象可能为空,应在调用前补一次检查。”开发者指出,被调用方法内部已经处理了空值。建议如果被执行,不会改善正确性,只会让同一条件在两层代码里重复出现。

第三条说:“表单校验仍使用 callback,应该改成 Promise。”这符合常见开源组件的新版本写法,但当前项目使用的组件封装仍推荐 callback。AI 把公共生态的默认知识覆盖到了本仓库的真实契约上。

第四条正确指出一个异步调用没有 await,但给出的修复是直接加 await。开发者实际需要的是不阻塞主流程,并在被调用方法内部捕获错误。问题方向有价值,修复方案却改变了时序语义。

这四条评论都能写得很像专业 Code Review。词句流畅,风险描述也有因果关系。它们仍然不值得直接采纳。真正缺少的不是“再解释得详细一点”,而是能证明建议适用于当前改动的仓库证据。

上一章把采纳率定义成 Precision 的工程代理。本章开始处理一个更棘手的事实:未采纳不是单一标签。它可能表示误报、已有处理、业务不同意、修复不可用、问题价值太低,也可能只是反馈来得太早。先把这些原因拆开,后面的优化才不会变成盲目改 Prompt。

44 条拒绝样本告诉了我们什么

课程案例包含一组定向收集的 44 条拒绝建议。其中 43 条被归为健壮性问题,1 条被归为安全问题。这个分布不能代表真实生产总体,因为样本就是为了收集 Bad Case 和未采纳案例而建立的。它适合回答“常见拒绝机制有哪些”,不适合回答“某类问题在生产中占多少”。

逐条阅读拒绝证据后,可以把样本归入下面七类。一个样本可能同时命中两类,分类目的是选择验证动作,不是制造互斥统计。

诊断类型候选建议的典型说法开发者提供的反证首先检查什么
场景不存在“如果字段为空会失败”类型、入口或数据生成逻辑保证该状态不会出现类型定义、构造路径、系统边界
已有处理“这里缺少校验或提示”上游、下游或公共方法已处理调用链与被调用方法
业务语义误判“应该按角色返回不同值”业务明确要求取更晚时间或允许空状态继续需求、测试、领域命名
框架契约误判“旧 API 必须迁移到新写法”项目封装、组件版本或注解定义了不同契约依赖版本、封装源码、项目规则
修复不可用“增加 await、默认值或互斥锁”改变时序、破坏配置语义,或保护了并不共享的数据建议代码的行为差异
重复或过时“再次报告相同风险”历史会话已报告,或当前版本已用其他方式修复历史 Issue 与当前文件
价值不足“补充额外防御、注释或抽象”风险很低,修复成本和噪音高于收益严重度、触发概率、影响范围

这一组样本有一个很稳定的特征:许多评论不是语言知识错了,而是适用条件错了。nil 会导致解引用失败是语言事实;“这个对象在此路径可能为 nil”是仓库事实;“遇到 nil 应直接返回”又是业务决策。AI 经常把第一层事实直接跳到第三层结论。

flowchart TD
  A[候选建议] --> B{语言或框架事实正确吗}
  B -- 否 --> R1[知识错误]
  B -- 是 --> C{触发条件在当前仓库成立吗}
  C -- 否 --> R2[场景误判]
  C -- 是 --> D{问题已在其他位置处理吗}
  D -- 是 --> R3[重复或已有处理]
  D -- 否 --> E{建议代码保持业务语义吗}
  E -- 否 --> R4[修复不可用]
  E -- 是 --> F{收益高于噪音与改动成本吗}
  F -- 否 --> R5[低价值]
  F -- 是 --> K[值得保留]
图中关系须结合相邻正文与来源理解。

这棵树比“采纳/未采纳”多走了几步,却给出了可行动的结果。知识错误需要修规则或模型;场景误判需要上下文;已有处理需要调用链和历史过滤;修复不可用要把问题判断与建议代码分开验收;低价值则要校准严重度和报告门槛。

采纳率低不是一个根因

规则缺失:系统不知道团队认为什么重要

不同仓库对同一写法可能有相反判断。某个前端项目要求所有用户可见文案进入国际化函数;另一个项目的管理端脚本可能允许硬编码。某个后端团队禁止手写 SQL;另一个模块可能因为兼容旧查询层而暂时允许。若审查器只收到通用规则,它只能用训练数据里的常见偏好填空。

规则缺失会产生两种噪音。第一种是把团队允许的写法判错,例如强制把 callback 改成 Promise。第二种是把真正重要的项目约束降成普通建议,例如仓库明确禁止动态拼接国际化文案,模型却只给 2 分风格评论。

诊断规则缺失不能只问“有没有规则文件”。还要看三个证据:当前问题是否命中明确规则 ID;规则是否描述了适用条件;规则与正反例是否进入了这一批 Prompt。一个文件存在磁盘上,但加载失败、解析为空或没有被注入,运行效果仍等于没有规则。

课程案例的 promptBuilder.ts 会组合基础角色规则、规则库中的系统条目和仓库级 Cursor Rules。这个实现说明规则不是一段大 Prompt,而是多个来源在运行时装配。第 5 章会继续拆这三层怎样分工。

上下文不足:模型看见了 Diff,却没看见事实

Diff 能回答“这几行改了什么”,很难独立回答下面的问题:

  • 这个字段由谁构造,是否真的可能为空?
  • 被调用方法是否已经处理错误或设置了默认值?
  • 当前组件是公共版本,还是团队二次封装?
  • 这个配置允许为 0,是异常还是有意关闭?
  • 同一需求的上一轮修改是否已经解决该问题?

Bad Case 中的“已有可选链”“方法内部已有空处理”“配置会在启动时校验”“只有当前协程更新该字段”,都需要 Diff 之外的证据。只把更多相邻行塞给模型并不够。需要的是与待验证假设有关的上下文:符号定义、一层调用方和被调用方、类型注解、项目规则、依赖版本、测试与需求约束。

可以把上下文不足分成检索失败和判断失败。检索失败是没有拿到正确文件;判断失败是拿到了文件,却没有把其中的保证用于结论。两者的修复完全不同。前者要改文件关联或工具调用,后者要让 Recheck 明确验证假设,并保存使用了什么证据。

flowchart LR
  D[Diff 中的可疑代码] --> H[提出触发假设]
  H --> T[类型与注解]
  H --> U[上游构造与校验]
  H --> V[下游处理与副作用]
  H --> P[项目规则与组件契约]
  T --> J{证据是否支持触发}
  U --> J
  V --> J
  P --> J
  J -- 支持 --> I[保留 Issue]
  J -- 不支持 --> X[标记无效并记录原因]
图中关系须结合相邻正文与来源理解。

图里最重要的是“提出触发假设”。如果只让 Agent 漫无目的地搜索仓库,它会收集很多相关文本,却未必能推翻自己的第一判断。好的验证问题应该可以被证伪,例如:“accountInfo 是否存在任何返回成功但字段为空的构造路径?”而不是“请检查更多上下文”。

问题判断和修复建议绑得太紧

一条评论可能发现方向正确,建议代码却不适用。异步调用缺少错误处理是问题;是否加 await 取决于调用方是否应等待。配置缺少检查可能值得关注;是否提供默认值取决于配置错误时系统应该失败还是继续。共享对象的写入值得检查;是否加锁取决于是否真有多个并发写者。

如果评估只问“是否照着 improve_code 修改”,这类评论会被当成完全错误。更合理的内部判断至少拆成四项:

维度判断问题可能结果
定位文件和代码位置对吗正确、偏移、与本次变更无关
风险描述的失败机制成立吗成立、不成立、证据不足
严重度触发概率和影响是否配得上分数合理、过高、过低
修复建议保持当前业务与时序语义吗可直接用、需改写、不可用

采纳反馈目前往往只有一个状态,因此诊断时要结合拒绝原因和最终代码。若大量样本是“方向对,修复不对”,继续增加规则可能没有帮助;应该让审查器先给出证据和最小修复约束,再生成代码建议,或者把修复建议交给独立步骤复核。

范围错位:评论没有属于当前变更

现代仓库常有大量历史问题。AI 读取整个文件后,可能发现一个真实缺陷,但它位于未修改的旧代码里,与当前 MR 没有直接关系。对代码质量而言它“是真的”;对本次审查而言它仍可能是噪音,因为开发者没有上下文、预算或授权在当前需求里处理。

课程案例用 diff_scope 保存问题位置与差异范围的关系:

  • changed_line:定位在新增或修改行。
  • context_line:位于 Diff 上下文行,需要证明本次改动直接触发它。
  • out_of_diff_scope:不在当前 Diff hunk 中,Recheck 直接判为无效。
  • unknown:无法确定范围时,必须用文件、原代码和建议代码证明关联,否则不保留。

范围判断不是“只允许评论绿色行”。跨文件依赖、接口变化和数据结构影响可能落在未修改文件中。区别在于是否有一条可解释的变更因果链。没有因果链的全仓扫描结果应该进入独立治理任务,而不是混进当前 MR 的阻断评论。

重复、冲突与已经修复的问题

同一需求可能多次运行审查。第一次建议从 A 改为 B,第二次又因上下文变化建议改回 A;或者两次都报告同一个位置的同一风险。即使每条单独看都有理由,连续展示会让使用者觉得工具没有记忆。

重复有三个层级。文本重复可以用哈希或相似度处理;根因重复需要理解两个描述是否指向同一失败;历史重复还要判断旧问题是否已被当前代码用另一种方式修复。简单把历史 Issue 全部拼进 Prompt,既浪费上下文,也可能让模型机械地重复旧结论。

当前案例的最终 filter 会在远端模式最后一批比较当前与同一需求的历史有效问题,支持 duplicate_of_historyconflict_with_historyoverlap_with_historyhistory_resolvedhistory_changed。这是一层收尾过滤,不等于所有本地审查都自动拥有相同能力。

严重度和价值没有校准

如果每个“理论上可能”都标 4 分,开发者很快会忽略整个报告。风险分数至少需要回答触发可能性、影响范围、现有防护与恢复成本。一个只在不可能输入下触发的 panic,不应仅凭“panic 很严重”得到最高分。

课程案例中的已采纳样本也提供了对照。后端样本里,错误变量引用、nil && len(x) 的逻辑错误、分页字段误赋、循环层级错误等问题具有直接证据,修复范围也清楚。前端样本里,国际化规则、拼写错误、行键不稳定等建议同样可以由项目规则或可观察行为支撑。它们和拒绝样本的差异不是措辞更强,而是证据链更短、更可复核。

严重度校准可以从“同类已采纳与已拒绝对照”开始。不要先设一个普适阈值。对每个仓库分别观察:4–5 分问题中哪些有明确触发路径,哪些总被判为防御性建议;修正规则与评分后,再看分层采纳率和召回护栏。

从数据对象里找证据

诊断不能停在阅读评论。一次低采纳调查至少需要把 Issue、Session、代码版本和反馈连起来。课程案例的关键字段可以抽象成下面的证据链。

erDiagram
  SESSION ||--o{ ISSUE : produces
  ISSUE }o--o{ RULE : matches
  ISSUE ||--o{ FEEDBACK : receives
  ISSUE ||--o{ CONTEXT_EVIDENCE : validated_by
  SESSION {
    string session_id
    string project_id
    string source_branch
    string target_branch
  }
  ISSUE {
    string file_path
    string diff_scope
    int score
    int is_valid
    int is_applied
    string reject_type
    string source_rule_ids
  }
  FEEDBACK {
    string source
    string reason
    datetime checked_at
  }
  CONTEXT_EVIDENCE {
    string symbol
    string file_revision
    string conclusion
  }
图中关系须结合相邻正文与来源理解。

当前实现已经保存了 Issue 的规则命中、有效性、无效原因、采纳状态和拒绝类型等字段,但“本次 Recheck 具体读取了哪些符号与版本”仍主要存在 Agent 过程里。若团队经常争论上下文是否充分,可以增加结构化证据引用,例如 evidence_filesevidence_symbolsassumptionverification_result。这会增加存储和 Prompt 约束,收益是 Bad Case 可以复盘,而不是只剩一句“模型判断错误”。

reject_type 也不能完全依赖关键词。当前自动归因会把“误报、不是问题”映射为 false_positive,把“建议不可用、修复不对”映射为 bad_fix,把“不重要、忽略”映射为 low_value,还识别风格分歧与已经处理。它适合做粗分桶,不适合替代人工抽样。拒绝理由很短时,unknown 往往比武断分类更诚实。

五个根因工作台:怎样从评论走到证据

根因树只有在能指导取证时才有价值。下面选取五类高频拒绝,完整走一遍“现象、假设、证据、判断和改造归属”。案例均经过等价改写。

工作台一:理论上的空值,真实链路里的合法空状态

候选评论认为 header 可能为空,建议直接返回 nil。开发者的反证是:“空 header 也要继续。”这句话至少包含两种解释。第一种是 header 的字段有默认零值,转换函数应该继续生成参数;第二种是当前调用者会在更高层补齐 header,提前返回反而破坏流程。仅凭拒绝文本无法选择。

先画出对象生命周期:外部请求怎样反序列化;转换函数是否处于系统边界;空 header 由协议允许、兼容历史请求,还是只在测试替身里出现;转换结果的字段是否允许零值;下游在哪一步完成最终校验。然后构造两个最小测试:header 为空时当前实现输出什么;加入提前返回后调用方行为怎样变化。

若空 header 是协议允许状态,问题属于业务语义误判,修复建议有害。规则层应写清“此转换器允许缺少 header,并以零值继续”。若空 header 本应被入口拒绝,只是当前测试没有覆盖,那么开发者拒绝并不能推翻风险,问题应保留但修复位置可能从转换函数移到入口。

这个案例提醒我们,反馈不是天然真值。开发者最了解局部业务,也可能只描述了当前习惯。诊断需要把反馈转换成可验证假设。

工作台二:忽略解析错误,还是有意使用零值兜底

候选评论看到 strconv.ParseInt 的错误被忽略,建议显式检查 err。拒绝理由是:“ID 一定是整数,并且后续 > 0 已经安全兜底。”

这里有三层证据。类型层:ID 在上游是字符串还是数字,为什么需要解析。数据层:该字符串是否只来自数据库整数列,还是也来自用户输入、消息和旧数据。行为层:解析失败变成 0 后,> 0 分支跳过是否就是期望结果,还是会静默丢数据。

如果字符串完全由受控整数列生成,且 0 与“无有效 ID”语义一致,显式处理错误可能只增加日志噪音。若数据穿过外部边界,静默跳过会隐藏污染,建议方向成立。还有第三种情况:风险成立,但正确修复是在反序列化入口保证类型,而不是在每个使用点检查。

因此标注不能只写 false_positive。可以记录 valid_risk_wrong_location,让规则学习“报告系统边界缺失校验”,不要在所有消费点重复报告。

工作台三:公共框架知识和项目封装冲突

候选评论看到表单 validator 的 callback 风格,认为旧 API 会导致校验失败,建议返回 Promise。拒绝理由是“组件库推荐用法”。

验证顺序应从仓库向外,而不是先搜索公共文档。先确定导入路径,找到项目封装的类型声明和版本;再查看同仓库中由维护者编写的例子;必要时运行最小表单测试,观察 callback 是否被调用、错误是否显示。只有项目代码无法解释时,才用上游文档补充。

若项目封装确实支持 callback,这条评论属于框架契约误判。解决方案不是把“callback 永远允许”写进通用规则,而是在仓库规则中声明具体组件与版本。组件升级后,规则必须同步失效或迁移。没有版本字段的规则会把今天的正确经验变成明天的误报来源。

这种案例还暴露了检索优先级。Agent 如果先访问公共知识,容易形成锚定;后续即使读到项目封装,也可能把它当作落后实现。Recheck 应明确规定“实际依赖和仓库封装优先于无版本的通用最佳实践”。

工作台四:已经处理,但处理位置与形式不同

候选评论说“照片上传失败时没有提示”,开发者回答“调用方法已有统一错误提示”。要验证的不只是有没有 Toast 字符串,还要看失败是否沿调用链传播。

需要检查上传方法失败时返回什么;循环是否吞掉错误;调用者如何区分部分成功和全部失败;统一提示是否覆盖当前路径;重复提示会不会造成两个弹窗。若底层抛错且上层统一 catch,当前建议会重复处理。若底层只返回空结果而不上抛,开发者以为已有提示,实际路径可能仍静默失败。

“已有处理”根因最适合保存证据引用:callee 的错误分支、caller 的 catch、统一提示组件和对应测试。没有这些引用,下一轮模型无法区分真正已有处理和开发者口头声称。

还要注意修复层级。即使当前路径缺少提示,把 Toast 加在数据访问层也可能违反架构边界。AI 评论应该指出缺失的用户可见反馈,而不是强制某一层直接调用 UI API。

工作台五:并发风险存在于变量,还是存在于执行模型

候选评论发现共享 Map 的字段在 goroutine 中被写入,建议加互斥锁。开发者说:“只有这个协程更新该字段。”

看到共享对象不等于存在数据竞争。需要找出 goroutine 的创建位置、对象是否被多个任务复用、其他路径是否读写同一字段,以及当前容器是否线程安全。单一 goroutine 内部顺序写入普通 Map 不需要锁;多个 goroutine 写不同 key 仍可能不安全;单写多读也要看读写是否并发。

最小验证可以运行竞态检测或构造并发测试,但工具结果必须和覆盖路径一起解释。测试没有报 race 可能是分支未命中,不代表不存在;代码证明只有单写者时,加锁反而增加复杂度并可能引入锁顺序问题。

若此类误报反复出现,规则不应写成“不要报告 Map 并发问题”。应写成条件化判断:先定位并发创建点和所有写者;无法证明至少两个并发访问时,不给高分确定性结论,可以提示需要人工确认。

采纳失败和反馈失败要分开

有些低采纳不是建议质量问题,而是反馈系统没有正确观察到采用。开发者可能手工实现了语义等价修复,没有点击采纳;外部生码平台可能覆盖代码却未回传状态;建议只改了配置或测试,自动检测读取了错误分支;合并后文件重命名导致路径匹配失败。

可以把调查分成两个连续问题:

  1. 建议在当时是否值得采用?
  2. 现有反馈机制是否正确记录了采用结果?

第一个问题需要代码与业务评审,第二个问题需要状态来源、时间戳和最终版本。若两者混在一起,团队可能为了修检测器而改规则,也可能把检测漏报误认为开发者不信任系统。

flowchart TD
  I[候选 Issue] --> Q{当时是否成立且值得处理}
  Q -- 否 --> FP[建议质量问题]
  Q -- 是 --> A{最终代码是否解决根因}
  A -- 否 --> NA[真实未采纳]
  A -- 是 --> S{状态是否记录为采纳}
  S -- 是 --> OK[反馈一致]
  S -- 否 --> OBS[反馈观测失败]
  OBS --> O1[语义等价修复]
  OBS --> O2[路径/分支/时间错误]
  OBS --> O3[外部反馈未回传]
图中关系须结合相邻正文与来源理解。

调查反馈失败时,要保存 Issue 创建 commit、检测所用 branch、最终 merge commit、文件重命名映射和状态更新时间。尤其不要用当前主干去判断几周前的建议,文件可能已经经历其他修改。

无反馈不是未采纳

is_applied=null 表示没有明确判断。它可能是开发者尚未处理、没有看到评论、审查任务中断,或反馈入口不可用。把 null 直接算未采纳,会惩罚反馈覆盖差的仓库;直接排除又可能只留下愿意互动的用户。

诊断报告应并列显示明确反馈率、反馈延迟和未反馈样本抽查。若优化后采纳率升高而反馈覆盖率下降,不能直接宣布系统变好。可能只是更少的人愿意点击。

自动未采纳也不是拒绝理由

行级检测发现建议代码没有出现在最终文件,只能说明文本没有按当前算法匹配。它不知道开发者为何没有采用,也不知道是否做了语义等价修复。自动状态适合触发补充核验,不应直接生成“开发者认为建议错误”的负向规则。

标签争议:谁有权决定一条评论是否正确

代码审查包含技术事实、业务语义和团队选择。单一评审者很难覆盖全部。可以按争议类型指定仲裁:

争议首要评审者必需证据不能单独决定的人
语言/并发/类型事实对应技术栈维护者可运行测试、类型或规范只凭业务习惯的使用者
业务允许状态模块 Owner/TL需求、接口契约、测试只看公共最佳实践的模型
组件封装行为组件维护者版本、源码、推荐用法未确认版本的外部资料
严重度模块 Owner + 质量角色触发概率、影响与恢复只依据错误名称的评审者
修复可用性代码作者 + 维护者行为差异、回归测试只检查语法可编译的人

仲裁不需要每条 Issue 都开会。可以先抽样建立规则,再把明确案例自动化。对高风险、意见冲突或证据不足的样本保留 uncertain,不要为了报表完整强制二选一。

一致性也要测。让两位评审者独立标注同一批样本,观察分歧集中在哪些类别。若“业务语义误判”长期分歧大,说明标签说明不足或需要模块 Owner 参与;若“范围错位”分歧大,可能是 Diff 归因规则不清。

观察当前实现时,应该问哪些问题

阅读采纳率链路的实现代码,不必把几千行代码逐行抄进课程。沿控制点提出问题更有效。

promptBuilder.ts 检查:规则来自哪里;项目规则解压失败是否可见;规则与示例是否有 ID;Issue 是否回写命中规则。在 extensionChecker.ts 检查:Recheck 何时执行;diff_scope 怎样影响结论;无效原因是否保留;检查器是否可能被配置关闭。

filterChecker.ts 检查:历史任务怎样关联;本地与远端是否行为一致;重复、冲突和已修复分别怎样落状态。在 memoryDistiller.tsmemoryChecker.ts 检查:拒绝信号怎样去重;项目级与用户级怎样隔离;规则是否需要确认、是否过期、命中后如何审计。

最后在 adoptionChecker.ts 检查:检测读取哪个分支和文件;注释、空白、重命名和短代码怎样处理;部分采纳是否进入正式指标;拒绝类型是事实字段还是关键词推断。

这些问题能把“采纳率低”映射到具体代码对象。若调查只停在模型输出文本,后续团队很难判断应该改 Prompt、检索、状态机、数据模型还是反馈入口。

建立一张可复盘的根因仲裁卡

调查结束后,不要只在会议纪要里写“AI 误报”。可以为每个抽样 Issue 保存一张仲裁卡,让同类问题能够比较。

text
issue_id: teaching-042
review_revision: commit-a
feedback_revision: commit-b
candidate: 建议为内部对象字段增加空值检查

scope_decision: caused_by_change
trigger_hypothesis: 成功构造对象的字段可能为空
evidence_for:
  - 字段类型允许空值
evidence_against:
  - 只有一个成功构造入口
  - 构造入口在返回前填充该字段
  - 测试覆盖空输入并在边界拒绝

issue_validity: invalid
fix_quality: harmful
root_cause: missing_repository_context
recommended_control: recheck_callee_and_constructor
reviewer_roles: module_owner, backend_reviewer
label_version: adoption-diagnosis-v1

这张卡把结论和证据分开。以后构造入口改变时,可以重新判断旧标签;Recheck 改造后,也可以检查它是否读取了相同证据。若只保存“误报”,规则提炼只能模仿一句结论,无法知道何时不适用。

仲裁卡还应记录反事实:什么变化会让当前结论翻转。上例中,只要发现另一个能绕过成功构造函数的反序列化入口,空值风险就需要重新评估。可翻转条件让规则保持开放,而不是把一次拒绝变成永久事实。

用证据强度决定自动化程度

不同证据的确定性不同。编译错误、类型不匹配、确定的数据流通常能自动验证;业务允许状态、用户体验和未来扩展需要人参与;“可能发生”但检索失败的情况只能标不确定。

证据等级示例适合的动作
确定证据编译失败、明确类型约束、可复现竞态自动保留或过滤,并保存工具结果
强仓库证据单一构造路径、项目规则、稳定测试Recheck 决定,定期抽样
业务证据需求允许空值、模块 Owner 解释人工确认后形成项目规则
弱推断常见最佳实践、相似代码、模型常识只能提出假设,不应给高分结论
证据缺失文件无权限、符号检索失败标记不确定,不能当作不存在

这张表也约束负向反馈。弱推断被拒绝可以帮助建立检索计划,不能直接生成绝对过滤规则。只有稳定、可复核、范围清楚的证据才适合自动化。

把修复位置也纳入仲裁

不少建议的问题方向成立,修复位置不对。例如入口缺少校验,却在每个消费点增加防御;统一错误处理缺失,却在底层库直接弹 Toast;异步任务需要观测,却在调用方强行 await。仲裁卡应记录 recommended_layer:boundary、domain、service、caller、callee、UI 或 infrastructure。

这样做能区分两种改造。若 issue_validity=validfix_quality=harmful 集中出现,优先改修复生成和架构约束;若 issue_validity=invalid,才去改规则、上下文与 Recheck。把两类都算成“未采纳”会浪费工程投入。

一套可复用的低采纳诊断流程

第一步:固定指标版本与样本窗口

先记录采纳率口径、反馈成熟窗口、仓库、语言、模型版本、规则版本和审查入口。不要把自动状态、人工状态和外部反馈随意混在一起。第 3 章已经说明,当前正式口径只纳入明确状态 0/1/4/5,并过滤低分和明确无效问题。

还要记录反馈覆盖率。假设 1,000 条高分候选中只有 120 条得到明确反馈,60% 采纳率只能描述这 120 条。若反馈者只在非常满意或非常不满时点击,样本仍然偏斜。

第二步:抽取“成对证据”

每个样本至少包含候选问题、当时 Diff、相关仓库上下文、开发者反馈和最终代码。缺少最终代码时,不能断言建议未实施;缺少当时版本时,也不能用今天的文件反推当时上下文。

抽样要同时包含已采纳与未采纳。只看 Bad Case 会把系统描绘得过度悲观,也无法找出同类问题何时值得报告。课程案例的三份定向数据正好说明这个边界:34 条已采纳与 44 条拒绝可以做对照教学,却不能拼成总体比例。

第三步:先判问题,再判修复

对每条建议依次回答:位置属于本次变更吗;触发条件能否由当前代码证明;是否已有处理;严重度是否合理;建议代码是否保持业务语义。评审者意见不一致时,记录分歧点并仲裁,不要强行压成一个模糊标签。

可以使用如下最小标注结构:

text
issue_validity: valid | invalid | uncertain
scope: changed | caused_by_change | historical | unknown
root_cause: rule | context | duplicate | scoring | none
fix_quality: usable | needs_revision | harmful | absent
evidence: [type_or_annotation, caller, callee, test, requirement]

这段结构是教学建议,不是当前数据库字段。它的价值在于把“没采纳”拆成可修的原因。

第四步:按可控制层聚合

聚合维度应直接对应工程动作:

  • false_positive + scene_impossible:检查类型、边界和 Recheck。
  • already_handled:补调用链与重复检测。
  • bad_fix:分离风险判断和修复生成。
  • style_disagree:把团队偏好写入仓库规则,或降低严重度。
  • low_value:调整报告门槛与分层展示。
  • duplicate/history:改历史 Issue 过滤和问题身份键。

如果团队只能看到“健壮性问题采纳率 32%”,仍然不知道下一步做什么。根因桶应该稳定到足以观察趋势,又不能细到每个样本一个标签。

第五步:只改变一个控制点

规则、上下文、Recheck 和记忆同时上线,指标变化后无法归因。先选择最大的根因桶做最小实验。例如:

假设:already_handled 占比高,是因为首轮审查没有读取被调用方法。处理组在 Recheck 中强制读取 callee 与一层 caller;对照组保持不变。主要指标看该根因桶的明确反馈采纳率,护栏看高分 Recall、时延、工具调用次数和无法判断率。

若处理组减少了误报,却让审查时间翻倍,需要评估是否只对高分候选或特定子类触发。工程优化不是把所有证据都塞进每一次请求,而是让证据成本随风险变化。

flowchart LR
  S[固定版本与窗口] --> P[采纳和拒绝成对抽样]
  P --> L[问题/范围/修复分层标注]
  L --> G[按可控制根因聚合]
  G --> H[提出可证伪假设]
  H --> E[**单变量实验**]
  E --> M[采纳率 + Recall + 成本护栏]
  M --> G
图中关系须结合相邻正文与来源理解。

诊断练习:不要急着给解决方案

下面五条拒绝理由来自脱敏后的教学样本。先写出最可能的根因,再指定一项能推翻你判断的证据。

  1. “这个字段不会为空,类型构造时已保证。”不要直接归为“模型不懂业务”。先查看构造函数是否覆盖全部入口,以及反序列化或测试替身能否绕过保证。
  2. “方法内部已经做了空处理。”检查被调用方法的具体版本和返回语义。若内部只记录日志但仍解引用,开发者反证可能不完整。
  3. “组件库就推荐 callback。”检查实际依赖版本、封装文档和仓库中的同类代码。公共文档不能代替项目契约。
  4. “不需要 await,内部会捕获错误。”检查调用者是否依赖完成顺序、内部是否真的覆盖全部拒绝路径,以及未等待 Promise 是否会产生未处理拒绝。
  5. “只有这个协程写该字段。”寻找所有闭包捕获、共享引用和异步入口。若确实只有单写者,加锁建议应判无效;若读取方与写入方并发,还要判断数据结构是否允许。

练习的完成标准不是猜中开发者答案,而是给出可判读的证据。证据显示假设不成立时,应允许改变结论。

完成练习后,再检查自己的取证顺序。若第一步总是搜索公共最佳实践,说明还没有把仓库事实放在优先位置;若只接受开发者一句“不会发生”,说明没有验证业务反证;若发现风险成立就默认建议代码正确,又把问题判断和修复质量合并了。成熟的诊断允许三种结论并存:问题成立且建议可用,问题成立但建议需重写,问题在当前范围不成立。

把五条练习的证据放进根因仲裁卡,然后尝试聚合。只有至少两条样本共享相同触发条件、排除条件和改造动作时,才考虑形成规则。表面都在讨论空值,不代表它们应进入同一条规则。

单变量实验设计

采纳率低时最容易犯的错误是一口气改 Prompt、加规则、调上下文,然后看到数字变了就说"优化有效"。你不知道哪个动作真正起效,也不知道哪个动作在帮倒忙。

正确的诊断顺序是:先用拒绝原因把问题分层,找到最粗的根因桶,然后一次只改一个变量,在固定样本上验证效果。

假设分层后发现 60% 的拒绝来自"已有处理"和"场景不存在"。这两类都与上下文不足有关——模型没看到被调用方法的内部实现,没看到入口的类型约束。那第一个实验应该是增加上下文(读取被调用方法的签名和文档),不改 Prompt、不改规则、不改模型。如果采纳率提升,说明根因判断正确;如果没有,说明这两类拒绝另有原因。

实验单位必须防止污染。对同一 MR 同时运行两版而把两版评论都展示给开发者,反馈会互相影响。可以离线双跑,或按仓库/用户分流;任何在线实验都需要说明风险与人工兜底。

结果也要区分统计显著与工程意义。即使样本足够让 1 个百分点差异稳定,如果它换来两倍成本,未必值得上线;样本较小但发现高危漏报,则可能先采取保守防护再继续收集。指标辅助决策,不替代风险责任。

发布门槛

门槛应由仓库基线决定,下面是一组结构,不是固定数字:

  • 目标误报子类相对下降,并达到最小样本量。
  • 固定 Benchmark 的高风险 Recall 不低于基线容忍区间。
  • 被 Recheck 或负向规则过滤的高分问题完成人工抽查。
  • 审查时延和 Token 成本没有超过服务预算。
  • 规则命中、过滤原因和版本可以追溯。
  • 无法判断率没有因"更谨慎"大幅上升。

采纳率上升只能算一个条件。Recall、成本、反馈覆盖与可追溯性共同决定能否推广。

迁移任务:为另一个仓库设计降噪闭环

选择一个你熟悉的仓库,找出一种经常被拒绝的评论。不要直接写新 Prompt,先提交一页设计:

  1. 给出三条成对样本:候选问题、拒绝证据、最终代码。
  2. 判断根因属于规则、上下文、范围、修复、历史还是价值。
  3. 写一条带触发条件、排除条件和证据要求的规则。
  4. 设计 Recheck 需要读取的最小上下文与停止条件。
  5. 决定这条经验应是组织、项目还是个人范围,保存多久,谁能禁用。
  6. 定义主要指标、Recall 护栏、成本预算与回退条件。

一个合格方案应该允许评审者指出"哪项证据会让这条规则不再成立"。如果规则无法被证伪,它很可能只是偏好口号。

本章收束

低采纳率不是“评论写得不像人”这么简单。课程案例的拒绝样本反复出现同一条因果链:模型掌握通用风险,缺少当前仓库的适用条件,于是把可能性写成事实,再给出改变业务语义或重复现有防护的修复。

诊断时先固定指标口径,收集候选问题、当时上下文、反馈与最终代码。然后把位置、触发条件、已有处理、严重度和修复质量分开判断。规则缺失、上下文不足、Recheck 失效、历史重复和低价值问题需要不同控制点,不能都靠扩写 Prompt。

第 5 章会把这些根因映射成一条分层改造链:先建立可版本化规则和最小上下文,再用 Recheck 过滤首轮候选,用负向记忆吸收反复拒绝,用历史过滤减少重复,最后用人工反馈与自动检测验证效果。每层都要保留开关、证据和 Recall 护栏。

参考文献

本章以课程案例当前代码、脱敏后的 44 条定向拒绝样本和已采纳对照样本为主要事实来源。样本用于诊断机制教学,不代表生产总体分布,也不用于计算总体采纳率。