先看四条“没有明显错误”的建议
同一批代码审查里出现了四条候选问题。为了公开教学,代码与业务名已经改写,但拒绝理由保留了原始结构。
第一条说:“请求头可能为空,直接访问字段会触发 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_history、conflict_with_history、overlap_with_history、history_resolved 和 history_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_files、evidence_symbols、assumption 与 verification_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 并发问题”。应写成条件化判断:先定位并发创建点和所有写者;无法证明至少两个并发访问时,不给高分确定性结论,可以提示需要人工确认。
采纳失败和反馈失败要分开
有些低采纳不是建议质量问题,而是反馈系统没有正确观察到采用。开发者可能手工实现了语义等价修复,没有点击采纳;外部生码平台可能覆盖代码却未回传状态;建议只改了配置或测试,自动检测读取了错误分支;合并后文件重命名导致路径匹配失败。
可以把调查分成两个连续问题:
- 建议在当时是否值得采用?
- 现有反馈机制是否正确记录了采用结果?
第一个问题需要代码与业务评审,第二个问题需要状态来源、时间戳和最终版本。若两者混在一起,团队可能为了修检测器而改规则,也可能把检测漏报误认为开发者不信任系统。
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.ts 与 memoryChecker.ts 检查:拒绝信号怎样去重;项目级与用户级怎样隔离;规则是否需要确认、是否过期、命中后如何审计。
最后在 adoptionChecker.ts 检查:检测读取哪个分支和文件;注释、空白、重命名和短代码怎样处理;部分采纳是否进入正式指标;拒绝类型是事实字段还是关键词推断。
这些问题能把“采纳率低”映射到具体代码对象。若调查只停在模型输出文本,后续团队很难判断应该改 Prompt、检索、状态机、数据模型还是反馈入口。
建立一张可复盘的根因仲裁卡
调查结束后,不要只在会议纪要里写“AI 误报”。可以为每个抽样 Issue 保存一张仲裁卡,让同类问题能够比较。
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=valid 但 fix_quality=harmful 集中出现,优先改修复生成和架构约束;若 issue_validity=invalid,才去改规则、上下文与 Recheck。把两类都算成“未采纳”会浪费工程投入。
一套可复用的低采纳诊断流程
第一步:固定指标版本与样本窗口
先记录采纳率口径、反馈成熟窗口、仓库、语言、模型版本、规则版本和审查入口。不要把自动状态、人工状态和外部反馈随意混在一起。第 3 章已经说明,当前正式口径只纳入明确状态 0/1/4/5,并过滤低分和明确无效问题。
还要记录反馈覆盖率。假设 1,000 条高分候选中只有 120 条得到明确反馈,60% 采纳率只能描述这 120 条。若反馈者只在非常满意或非常不满时点击,样本仍然偏斜。
第二步:抽取“成对证据”
每个样本至少包含候选问题、当时 Diff、相关仓库上下文、开发者反馈和最终代码。缺少最终代码时,不能断言建议未实施;缺少当时版本时,也不能用今天的文件反推当时上下文。
抽样要同时包含已采纳与未采纳。只看 Bad Case 会把系统描绘得过度悲观,也无法找出同类问题何时值得报告。课程案例的三份定向数据正好说明这个边界:34 条已采纳与 44 条拒绝可以做对照教学,却不能拼成总体比例。
第三步:先判问题,再判修复
对每条建议依次回答:位置属于本次变更吗;触发条件能否由当前代码证明;是否已有处理;严重度是否合理;建议代码是否保持业务语义。评审者意见不一致时,记录分歧点并仲裁,不要强行压成一个模糊标签。
可以使用如下最小标注结构:
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
诊断练习:不要急着给解决方案
下面五条拒绝理由来自脱敏后的教学样本。先写出最可能的根因,再指定一项能推翻你判断的证据。
- “这个字段不会为空,类型构造时已保证。”不要直接归为“模型不懂业务”。先查看构造函数是否覆盖全部入口,以及反序列化或测试替身能否绕过保证。
- “方法内部已经做了空处理。”检查被调用方法的具体版本和返回语义。若内部只记录日志但仍解引用,开发者反证可能不完整。
- “组件库就推荐 callback。”检查实际依赖版本、封装文档和仓库中的同类代码。公共文档不能代替项目契约。
- “不需要 await,内部会捕获错误。”检查调用者是否依赖完成顺序、内部是否真的覆盖全部拒绝路径,以及未等待 Promise 是否会产生未处理拒绝。
- “只有这个协程写该字段。”寻找所有闭包捕获、共享引用和异步入口。若确实只有单写者,加锁建议应判无效;若读取方与写入方并发,还要判断数据结构是否允许。
练习的完成标准不是猜中开发者答案,而是给出可判读的证据。证据显示假设不成立时,应允许改变结论。
完成练习后,再检查自己的取证顺序。若第一步总是搜索公共最佳实践,说明还没有把仓库事实放在优先位置;若只接受开发者一句“不会发生”,说明没有验证业务反证;若发现风险成立就默认建议代码正确,又把问题判断和修复质量合并了。成熟的诊断允许三种结论并存:问题成立且建议可用,问题成立但建议需重写,问题在当前范围不成立。
把五条练习的证据放进根因仲裁卡,然后尝试聚合。只有至少两条样本共享相同触发条件、排除条件和改造动作时,才考虑形成规则。表面都在讨论空值,不代表它们应进入同一条规则。
单变量实验设计
采纳率低时最容易犯的错误是一口气改 Prompt、加规则、调上下文,然后看到数字变了就说"优化有效"。你不知道哪个动作真正起效,也不知道哪个动作在帮倒忙。
正确的诊断顺序是:先用拒绝原因把问题分层,找到最粗的根因桶,然后一次只改一个变量,在固定样本上验证效果。
假设分层后发现 60% 的拒绝来自"已有处理"和"场景不存在"。这两类都与上下文不足有关——模型没看到被调用方法的内部实现,没看到入口的类型约束。那第一个实验应该是增加上下文(读取被调用方法的签名和文档),不改 Prompt、不改规则、不改模型。如果采纳率提升,说明根因判断正确;如果没有,说明这两类拒绝另有原因。
实验单位必须防止污染。对同一 MR 同时运行两版而把两版评论都展示给开发者,反馈会互相影响。可以离线双跑,或按仓库/用户分流;任何在线实验都需要说明风险与人工兜底。
结果也要区分统计显著与工程意义。即使样本足够让 1 个百分点差异稳定,如果它换来两倍成本,未必值得上线;样本较小但发现高危漏报,则可能先采取保守防护再继续收集。指标辅助决策,不替代风险责任。
发布门槛
门槛应由仓库基线决定,下面是一组结构,不是固定数字:
- 目标误报子类相对下降,并达到最小样本量。
- 固定 Benchmark 的高风险 Recall 不低于基线容忍区间。
- 被 Recheck 或负向规则过滤的高分问题完成人工抽查。
- 审查时延和 Token 成本没有超过服务预算。
- 规则命中、过滤原因和版本可以追溯。
- 无法判断率没有因"更谨慎"大幅上升。
采纳率上升只能算一个条件。Recall、成本、反馈覆盖与可追溯性共同决定能否推广。
迁移任务:为另一个仓库设计降噪闭环
选择一个你熟悉的仓库,找出一种经常被拒绝的评论。不要直接写新 Prompt,先提交一页设计:
- 给出三条成对样本:候选问题、拒绝证据、最终代码。
- 判断根因属于规则、上下文、范围、修复、历史还是价值。
- 写一条带触发条件、排除条件和证据要求的规则。
- 设计 Recheck 需要读取的最小上下文与停止条件。
- 决定这条经验应是组织、项目还是个人范围,保存多久,谁能禁用。
- 定义主要指标、Recall 护栏、成本预算与回退条件。
一个合格方案应该允许评审者指出"哪项证据会让这条规则不再成立"。如果规则无法被证伪,它很可能只是偏好口号。
本章收束
低采纳率不是“评论写得不像人”这么简单。课程案例的拒绝样本反复出现同一条因果链:模型掌握通用风险,缺少当前仓库的适用条件,于是把可能性写成事实,再给出改变业务语义或重复现有防护的修复。
诊断时先固定指标口径,收集候选问题、当时上下文、反馈与最终代码。然后把位置、触发条件、已有处理、严重度和修复质量分开判断。规则缺失、上下文不足、Recheck 失效、历史重复和低价值问题需要不同控制点,不能都靠扩写 Prompt。
第 5 章会把这些根因映射成一条分层改造链:先建立可版本化规则和最小上下文,再用 Recheck 过滤首轮候选,用负向记忆吸收反复拒绝,用历史过滤减少重复,最后用人工反馈与自动检测验证效果。每层都要保留开关、证据和 Recall 护栏。
参考文献
本章以课程案例当前代码、脱敏后的 44 条定向拒绝样本和已采纳对照样本为主要事实来源。样本用于诊断机制教学,不代表生产总体分布,也不用于计算总体采纳率。