把代码评审变成一场戏

Problem: 你的团队有 ESLint、SonarQube、Semgrep、CodeQL。每个每天都发一堆 warning。没人看。工具在技术上全对,在实践上全被忽略。为什么?

这篇是我最"不正经"的一个项目,但它解决的问题特别真实:再准的工具,只要人不读,准确率就等于零。 CodeTribunal 干脆放弃"更准",转去解决"让人愿意读"。

先承认:这是注意力问题,不是准确率问题

代码评审工具有个根本缺陷:它们产出结论,不产出对话

一个静态分析工具说:"Line 42: potential null pointer dereference." 开发者看一眼 42 行,发现那个 null 分支在上下文里不可能走到,于是关掉警告。工具无法反驳,无法解释为什么这个 null 在更大语境里重要,也无法从这次忽略里学到东西。

结果就是 alert fatigue:

当每条 finding 都是同样的严重度、同样的格式,开发者就不读了。准确率 99% 的工具,被 1% 的忽略率杀死——因为人根本不点开。

如果工具能跟你吵呢

换一个场景。你提交代码给一个评审面板,八个评审员分析它:

  • 架构师说抽象错了;
  • 安全守卫标记一个潜在注入向量;
  • 性能老手质疑算法复杂度;
  • 测试怀疑者说测试覆盖在骗人。

然后安全守卫和性能老手吵起来。守卫说注入风险压过性能顾虑,老手反驳"这个注入只可能发生在每小时执行一次的代码路径上"。其他评审员带着证据加入。

这跟 warning 列表本质不同。它是带证据、带反证、带裁决的结构化分歧

难点:一个 LLM 怎么造出 8 个会吵架的人

这是整个项目最反直觉的工程点。如果你用同一个 prompt 让 GPT 评审 8 次,你会得到 8 份几乎一样的评审——LLM 被训练成乐于助人、保持一致,这恰恰是辩论最不需要的东西

fine-tune 8 个模型不现实。CodeTribunal 的解法是纯 prompt 工程,每个 persona 用四层约束把 LLM"锁"进角色(internal/roundtable/persona.go):

type PersonaDefinition struct {
    Name        string
    Description string
    Keywords    []string   // 1. 内容焦点:这个角色会注意什么
    OutputRules []string   // 2. 结构约束:JSON 字段、字数上限
    StylePrompt string     // 3. 语气人格:措辞和声音
    Example     string     // 4. few-shot 参考:一个具体示例
}

八个角色各有各的偏执:

Persona关注点语气
Architect抽象、模块化、耦合克制,爱引设计模式
Security Guardian注入、鉴权、数据暴露偏执,张口 OWASP
Performance Guru复杂度、分配、热路径数字驱动,benchmark 脑
Test Skeptic覆盖质量、边界、mock质疑一切,要证据
DX Advocate可读性、命名、API 手感共情,替下一个人着想
Error Philosopher错误处理、恢复、韧性对失败模式很哲学
Dependency Watcher供应链、版本、传递依赖谨慎,想长期维护
Simplicity Zealot过度设计、多余抽象对复杂度冷酷

光有 prompt 还不够,输出会跑偏。ValidatePersonaOutput 检查输出里命中了几个角色关键词,命中 <2 就用强化 prompt 重试,最多 3 次(session.go:330):

func ValidatePersonaOutput(output string, def PersonaDefinition) bool {
    hitCount := 0
    lower := strings.ToLower(output)
    for _, kw := range def.Keywords {
        if strings.Contains(lower, strings.ToLower(kw)) { hitCount++ }
    }
    return hitCount >= 2  // 至少命中 2 个关键词,否则重试
}

一个我很喜欢的取舍:评审用自由文本,投票用 JSON。 因为——

Security Guardian 被强制吐 JSON,就会和其他吐 JSON 的 agent 声音一模一样。结构化输出锁住格式,却杀死人格。所以"评审该像人说话,投票该像机器计票"。

往里塞一个"故意说错话"的人

CodeTribunal 走得更远(详见 04-troublemaker-mechanic05-debate-and-voting)。八个评审员里,有一个是偷偷藏着的 Troublemaker——它被指示给出"看起来有用、实则微妙有害"的建议,其他 agent 不知道谁是小人,而且它的伪装策略每局从 5 种里随机挑一个,你学不会固定套路。

初审之后进入辩论阶段DebateRoundsession.go:530):每个 agent 指控另一个,且被强制引用证据:

When accusing a player, you MUST:
1. Quote their specific statement
2. Reference specific code lines
3. Explain why their advice is wrong or suspicious
4. Provide a counter-argument they should have made

没有这条约束,agent 只会含糊地说"Player 3 好像有问题";有了它,就变成"Player 3 说'错误处理很扎实',但这函数第 15-20 行根本没有错误处理"。然后并行投票AIVoteAndEliminatesession.go:671,用 errgroup 并发、JSON 结构化)驱逐得票最高者——并行是为了防止跟风效应:没有 agent 在投票前看到别人的票。

sequenceDiagram participant User participant Panel as 8 Agents participant TM as Troublemaker User->>Panel: Submit code Panel->>Panel: Individual analysis Note over TM: Gives subtly bad advice Panel->>Panel: Speaking round loop Up to 3 cycles Panel->>Panel: Cross-examination debate Panel->>Panel: Parallel vote + elimination end Panel->>User: Reveal + debate transcript User->>User: Identify the real Troublemaker

辩论最多 3 轮(MaxDebateCycles),每轮上下文越滚越厚,Troublemaker 越来越难藏。真实的命中分布是:

结果概率
第 1 轮就被揪出~40%
第 2 轮被揪出~35%
撑过全部 3 轮~25%

有四分之一的概率它能骗过所有 AI——这个"骗得过"恰恰是设计的一部分,因为它逼着最后由你(用户)来做判断。

为什么这招管用

三件事让这个反直觉的设计成立:

  1. 分歧比一致更有信息量。 两个 agent 为"这个 finding 是不是真的"吵起来时,争吵本身就在教读者为什么这段代码安不安全。warning 列表给你结论,辩论给你推理。
  2. Troublemaker 制造主动阅读。 你知道有一个评审员在暗中使坏,于是你逐条较真,不敢自动忽略任何一条。这正好是 alert fatigue 的反面。
  3. 游戏产出结构化输出。 辩论记录不是娱乐,是同一段代码上多种视角 + 证据链 + 反论证的结构化档案,比任何单一工具的报告都有用。

它"不是"什么(我得划清边界)

我不想把它吹成银弹,所以明确说清它不干什么:

  • 不替代静态分析。它不找 null pointer dereference 或 buffer overflow——那些交给 Semgrep。它工作在另一个层:代码质量推理(架构决策、命名、测试策略、安全姿态、性能权衡)。
  • 不是生产级评审工具,是一个思考工具。它适合"启发思考",不适合"当裁判"。

这个决策欠了什么债

  • 成本债:8 个 LLM 实例 + 多轮辩论 + 投票,token 消耗是单次静态扫描的几十倍。它本就不该用在 CI 的每条 PR 上,而是用在你真正想"想清楚"的时候。
  • 不确定性债:Troublemaker 的"微妙有害"很难拿捏——太明显就成搞笑,太隐蔽就没人发现。这是 prompt 工程里最细的一根针。
  • 可复现债:每轮辩论有随机性,同一段代码两次跑结果可能不同。persona 分配和 Troublemaker 伪装都是随机的,这是特性,不是 bug。
  • 解析债:为了保住人格用了自由文本,代价是 ParseReviewOutput 得靠分隔符和正则从自由文本里抠字段——脆弱,但为了真实感值得。

但传统 warning 列表的债更致命:它产出的结论,被人的忽略率清零了。CodeTribunal 把"忽略"这件事变得昂贵——因为你知道有人在骗你,你就不敢不读。

结论

47 条 ESLint warning 没人看,因为结论是平的。我反过来,往评审团里塞一个故意说错话的 agent,逼你逐条较真。一致性让人懈怠,分歧才让人清醒。

传统工具追求"更准的 warning";CodeTribunal 追求"让人愿意读 warning"。这是两个不同的问题——前者是技术精度,后者是人类注意力。大多数团队死在后者上,却一直在优化前者。

这呼应 03「错误是设计的镜子」:系统怎么对待失败,暴露的是设计哲学;CodeTribunal 对待"可能被骗"的方式,是主动制造一场戏,而不是默默列一张没人看的清单。而它那套"多 agent + 随机 + 对抗"的哲学,走到尽头就是下一篇——当"选哪个策略最好"本身没有唯一答案时,干脆让策略自己交配、自己淘汰。


Next: 用遗传算法给 Agent 选策略 -- 同一套多 agent 哲学走到尽头:当'选哪个策略最好'本身没有唯一答案,让策略自己交配、自己淘汰,比任何手调都诚实。