毕设想做一个自己的 ultrareview

返回

本科毕设的题目暂定是《基于多智能体编排与云端沙箱的软件缺陷检测与修复系统设计与实现》 名字很长 说白了就是想自己做一个会验证的 AI 代码审查

[出现]

开题草稿里的想法是这样的:先由一个 agent 看改动,决定这次要叫哪些专家;几个专家 agent 同时开工,从安全、代码风格、可用性、测试覆盖这些不同角度去看;缺测试的地方,按改动范围补上必要的单元测试和 E2E;最后汇总成一份反馈,再决定要不要给出具体的修改建议,由人拍板。

思路上没什么问题,真正要想清楚的是怎么做出来:用什么语言、拆成哪几块、仓库怎么放。这篇就是把这些想法理一遍。

先起个名字

Orrery 是太阳系仪,就是那种中间一个太阳、外面一圈圈行星靠齿轮带着转的机械模型。

它的样子很像我想要的结构:中间的齿轮带着各自的轨道转。

调度 agent 在中间,专家们各在自己的轨道上,互不干扰,又被同一套齿轮带着走。

另外它和 Vesper(晚星)也算一个家族,都是天上的东西。

后来还是改了名,叫 eyeful,出自 Linus 定律:「只要眼睛足够多,bug 都是浅显的。」大多数项目没有那么多眼睛,eyeful 帮它们补上。命令行就叫 eyeful,仓库里的配置放 .eyeful/,下文都用这个名字。

[思考]

先看看 ultrareview 怎么做

写着写着发现,Claude Code 的 /code-review ultra 和我想做的东西很接近。它会把当前分支打包传到 Anthropic 的云端沙箱(PR 模式下直接在沙箱里克隆),派出一队 reviewer agent 并行看改动。官方文档里最关键的一句是这个:

Every reported finding is independently reproduced and verified, so the results focus on real bugs rather than style suggestions.

Claude Code 文档 · https://code.claude.com/docs/en/ultrareview

平时写代码用本地的 /code-review,几秒到几分钟出结果;合并前的大改动才上 ultra,5 到 10 分钟,Pro 和 Max 账号有 3 次免费,之后每次 5 到 25 美元。

不过它只盯着 bug,也只报告不修改,几个 agent、怎么分工、怎么验证都没有公开。我想做的范围更宽一点:

  • 除了 bug,还看设计、安全、测试、可用性和可读性
  • 先弄清这次改动做了什么、分好组,再按组决定叫哪些专家
  • 专家认为有 bug 就写测试证明,由系统自己运行
  • 最后给出验证过、能一键采纳的修改建议
  • 再做一套公开、能复现的评估

多个专家 agent 加沙箱验证到底能找出多少真问题、误报多少、花多少钱,这才是论文想回答的问题。

光做一个能跑的审查工具还不够,评估部分才决定它有没有研究价值。

和 revmux 比

ultrareview 出来之后,GitHub 上冒出了不少类似的项目。我看下来最成熟、也最值得对照的是 revmux:作者 umputun 写过 remark42、reproxy,也是 Go,有完整的测试和发布流程。

它把自己定位成一个审查引擎,README 里说得很直白:

revmux runs a review and returns findings, and does nothing else. It performs no scope detection, no git operations, no PR fetching and no source modification.

revmux README · https://github.com/umputun/revmux

它的流程是固定的三段:

Architecture flowAn architecture diagram grouped by dependency.调用方 agent 写好scope.mdfind:按profile 并行派claude 和 codexsynthesize:合并去重,多来源印证就加置信度verify:按目录分组,agent读代码给出判定stdout:JSON 或MarkdownArchitecture flowAn architecture diagram grouped by dependency.调用方 agent 写好scope.mdfind:按profile 并行派claude 和 codexsynthesize:合并去重,多来源印证就加置信度verify:按目录分组,agent读代码给出判定stdout:JSON 或Markdown

两边放在一起:

revmuxeyeful
定位本地的审查引擎,由 coding agent 调用本地用自己的 coding agent 审改动;云端审 PR(计划中)
谁来选审查角度调用方选 profile,名单是固定的pulls.review 的 core 给改动分组,Go 按每组的类别挑专家
验证怎么做另一个 agent 读代码,给出 confirmed、rejected 等判定验证器运行复现测试:修复前失败、修复后通过,期望行为要有 agent 以外的来源
跑在哪被审仓库里,agent 有 shell,等于信任这个仓库agent 只有只读工具;本地的项目命令只在快照的一次性检出里跑,云端在不含凭据的沙箱里跑
缺测试有 tests 视角,会指出来tests 专家指出覆盖缺口,不替作者补测试
修改建议每条发现带一段文字 fix另一个 agent 写修复,验证器跑过测试后作为 suggested change 给出
输入和输出调用方写好的 scope 文件;stdout 和退出码本地未提交改动、分支、提交,云端 PR;终端、Markdown、SARIF、GitHub review、Check Run
评估stats 统计运行数据,没有公开的准确率内部消融:执行验证、只读评判、不验证

[评价]

它做得好的地方我不打算重新发明,直接借:多来源印证加置信度,单个 agent 挂了降级而不是整体失败,完整的运行存档,多轮审查把上一轮交给下一轮。

我想突出的是这几点:

  1. 可执行的证据。每条发现都带证据类型,强证据必须是跑出来的:复现测试修复前失败、修复后通过,而且测试期望的行为来自崩溃、原有测试或引用的需求原文,不是 agent 自己的理解。revmux 刻意不碰代码执行,验证只读代码。
  2. 按需调度。先弄清改动做了什么再挑专家,只花该花的钱;revmux 的名单写在 profile 里。
  3. 测试用来证明问题。专家写测试是为了证明 bug 存在,测试跟着它证明的修复一起交给作者,不为凑覆盖率去补测试。
  4. 先验证再建议。修改建议先打上去跑一遍测试,通过了才发出去。
  5. 安全地审别人的 PR。隔离容器、限制出站、凭据不进沙箱;revmux 的 README 自己也提醒,跑之前要先确认仓库可信。
  6. 两头可插拔。代码从哪来、结果交给谁都是适配器,接 GitHub 是一等公民。

发现不靠模型之间互相说服,而是用执行结果说话。

revmux 的验证是让另一个模型再读一遍代码;eyeful 的验证是把代码真的跑起来。

这也给论文一个很清楚的研究问题:靠执行验证,比靠另一个模型读代码验证,误报能少多少,又要多花多少时间和钱? 对照放在 eyeful 内部做,专家和模型都一样,只换验证方式;revmux 的验证也是读代码,作为外部参照。

整个流程怎么跑

一次审查规划: CI 校验、分诊,弄清改动做了什么并分组,按组挑专家 → 专家并行: 正确性、安全、设计、测试、可用性、可读性各看各的 → 验证: 运行专家写的复现测试,再验证另一个 agent 写的修复 → 汇总: 合并、打标签、排序,写成评审意见 → 人工: 采纳修复,或者驳回01规划CI 校验、分诊,弄清改动做了什么并分组,按组挑专家02专家并行正确性、安全、设计、测试、可用性、可读性各看各的03验证运行专家写的复现测试,再验证另一个 agent 写的修复04汇总合并、打标签、排序,写成评审意见05人工采纳修复,或者驳回

在这之前还有一步准备:先把要审的代码固定成一个快照,再装依赖。每一步失败了也都有退路,不会因为一个环节出错整次白跑:

  • 准备失败:依赖装不上就退成只读审查,所有发现标为未验证,报告里写清楚原因
  • CI 没过:不叫专家,只诊断失败原因
  • 规划失败:分组不合规就重试一次,再不行用默认的专家组合
  • 专家失败:单个专家超时只丢掉它自己,报告里注明谁缺席
  • 验证失败:复现没跑起来,这条发现保持未验证
  • 汇总失败:改用规则排版,至少信息不丢

每个阶段的产出都写进运行归档,进程中途挂了,从最后完成的阶段接着跑。

规划:先弄清改了什么

第一个 agent 不审代码。分诊先用规则把锁文件、生成代码、第三方代码放到一边,再跑 lint、类型检查这类不花 token 的工具。然后直接运行 pulls.review 的 @pulls.review/core:用它的提示词让 agent 说明这次为什么改,并按意图把文件分组,每组标出触及系统的哪一部分,需要格外小心的标 critical:

{
  "overallSummary": "会话在到期那一秒就算过期,登录页显示会话何时结束。",
  "groups": [
    {
      "key": "session-expiry",
      "label": "会话过期",
      "category": "security",
      "critical": true,
      "filePaths": ["app/auth/session.py"]
    },
    {
      "key": "login-page",
      "label": "登录页",
      "category": "ui",
      "filePaths": ["web/src/Login.svelte"]
    }
  ]
}

挑专家不交给 agent,由 Go 查一张固定的表,同样的分组总是得到同样的专家:

  • security → 正确性、安全
  • core、api、data、cli → 正确性
  • ui、i18n → 可用性
  • tests → 测试
  • docs、examples → 可读性
  • config、build、scripts、deps → 安全
  • 标了 critical 或碰到 risk 路径的核心组,再加上正确性和安全;deep 档的核心组再加设计

agent 决定改动是什么,Go 决定能不能做、怎么做:

Architecture flowAn architecture diagram grouped by dependency.分诊后的 diffpulls.reviewcore 的提示词分组 JSONcore 校验:每个文件恰好在一组里Go按类别挑专家并启动不合规:重试或用默认组合Architecture flowAn architecture diagram grouped by dependency.分诊后的 diffpulls.reviewcore 的提示词分组 JSONcore 校验:每个文件恰好在一组里Go按类别挑专家并启动不合规:重试或用默认组合

[敲]

专家:各看各的

专家按 ISO/IEC 25010 的质量特性分工,每个专家就是一个 Markdown 文件,frontmatter 里写清楚它用哪档模型、拿什么当证据、加载哪些 skill。skill 不自己写,从 skills.sh 上挑安装量高的放进来:

---
name: security
area: security
tier: strong
evidence: [repro_test, cve, trigger_path, argument]
skills:
  - name: security-review
  - name: golang-security
    files: ['**/*.go']
---
You are the security expert on a code review. You ask one question: does this change introduce a
vulnerability? Follow untrusted input from where it enters to where it is used.

现在的六个专家:

专家看什么证据从哪来模型
正确性逻辑错误、边界条件、并发、错误处理复现测试,修复前必须失败强
安全注入、鉴权绕过、密钥泄露、有漏洞的依赖复现测试、已知 CVE,不能复现就给出触发路径强
设计模块划分、耦合、不必要的抽象引用原则论证,复杂度工具强
测试改动里的测试有没有测到东西、哪些行没被覆盖覆盖率和 diff 逐行对照中
可用性错误提示、API 破坏性变更、界面和无障碍API diff、axe、E2E 截图中
可读性命名、注释、和项目风格指南是否一致项目自己的 lint,加上论证便宜

正确性和安全能用测试证明,其余很多时候只能讲道理,所以每条发现都带上证据类型,汇总时区别对待,不会把一条风格建议和一个能复现的 bug 摆在同一个级别。

一条复现测试大概长这样,它在 PR 的代码上必须先失败,才算证明了问题存在:

正确性专家写的复现测试
--- /dev/null
+++ b/tests/test_session_boundary.py
@@ -0,0 +1,5 @@
+from app.auth.session import Session, is_expired
+
+def test_expired_at_exact_boundary():
+    session = Session(expires_at=100)
+    assert is_expired(session, now=100)

专家之间互不通信,各自在单独的进程里跑,每个专家只跑一次,看完分给它的所有组。这样可以并行,也避免一个专家的判断带偏另一个。专家只有只读工具,测试交给验证器去跑。

验证:测试用来证明问题

测试失败只能说明代码和测试对不上,说明不了测试本身是对的。会话应该在 expires_at 那一秒过期还是晚一秒,代码回答不了,要看需求。所以每个复现测试都要声明期望的行为从哪来:崩溃、数据竞争、超时这类不需要规格的,仓库里原有的测试,或者 issue、PR 描述、文档里引用的原文,才算强证据;只是基础提交上行为不同,或者只有 agent 自己的理解,就变成一个向作者提的问题。

Architecture flowAn architecture diagram grouped by dependency.改动加上复现测试测试失败测试通过:未能复现,不发评论新副本上打上修复agent 写的修复测试通过、其他测试不变红:修复已验证仍失败:已复现,没有可用的修复Architecture flowAn architecture diagram grouped by dependency.改动加上复现测试测试失败测试通过:未能复现,不发评论新副本上打上修复agent 写的修复测试通过、其他测试不变红:修复已验证仍失败:已复现,没有可用的修复

修复由另一个 agent 来写,它能看到发现和测试,看不到专家的推理,免得修复只是把专家的假设再写一遍。只有验证器会运行项目代码,专家不能把自己的测试标成通过。

汇总:决定说什么

最后一个 agent 拿到所有专家的发现和验证结果,先合并,几个专家常常说的是同一件事;再排优先级;最后按 Conventional Comments 写成评论:

  • 强证据的 bug、漏洞或回归:issue (blocking),贴到对应行,验证过的修复用 suggested change 附上,一键采纳
  • 没能证明的疑点:question,附上失败的测试,让作者判断
  • 能改进的地方:suggestion,不阻塞
  • 风格小问题:nitpick,折叠到报告最后,不刷屏

比如前面那个边界问题,汇总给出的建议是:

汇总给出的修改建议
--- a/app/auth/session.py
+++ b/app/auth/session.py
@@ -10,2 +10,2 @@
 def is_expired(session, now):
-    return now > session.expires_at
+    return now >= session.expires_at

建议发出去之前,验证器会先在一份新副本上把它打上去,再跑一遍改动涉及的测试:复现测试要变绿,原有的测试不能变红,才算过关。

语言怎么分

  • Go 写后端:状态机、调度、容器管理、GitHub webhook、REST API,还有本地命令行和整套审查编排。Docker、containerd、gVisor 的官方 SDK 都是 Go 的,同时盯几十个容器、随时超时或取消,goroutine 加 context 处理起来很自然,服务端和命令行各编译成一个二进制
  • TypeScript 写网页控制台、桌面端前端和 pi 扩展
  • Python 只写评估:跑基准、统计、画图,几个基准的工具本来就是 Python 的
  • agent 这部分不自己写:云端跑在 pi 里,本地直接用用户已经登录的 Claude Code、Codex 或 pi

subagent 直接用 pi

云端的每个 agent 就是一个 pi 进程,模型密钥留在沙箱外的 pi 进程里,碰不到被审查的代码。翻了一下 pi --help,需要的东西都有。启动一个安全专家大概是这样:

pi --print --mode json --no-session \
  --model anthropic/claude-sonnet-5-5 \
  --tools read,grep,find_references,secret_scan,dependency_audit,cve_lookup,sast,submit_report \
  --extension eyeful-pi-extension.js \
  --append-system-prompt workflow/prompts/experts/security.md \
  "Review the files the plan assigned to you"
  • --mode json 让它不开界面、把过程输出成 JSON,Go 这边逐行解析
  • --tools 按专家文件限制工具,专家只能读代码和调用 eyeful 定好的工具,不能改文件、不能随便跑命令
  • --model 按角色选模型
  • --extension 加载自己写的专家工具和 submit_plan、submit_report,把输出约束成固定结构
  • --no-session 不留会话,每次都是干净的

模型的分配思路是:正确性、安全和设计专家用强一点的模型;可读性这类调用多又不太难的,用便宜模型,比如 DeepSeek,开题参考文献里留着 DeepSeek-V3 就是这个原因;测试和可用性专家用中档。

pi 不会写死在调度代码里。审查编排只认一个 Agents 接口:本地的实现已经有了,每次调用起一个 Claude Code、Codex 或 pi 的命令行进程,只给只读工具;云端的 pi 实现还在做。不过论文的实验只用 pi,因为只有在那里每个 token 都由 eyeful 发起、能算清楚花了多少钱。

测试交给项目自己

测试我不打算全包。被审查的项目本来就有自己的测试和 CI,那才是最后的标准。项目在 .eyeful/config.yml 里告诉 eyeful 怎么跑:

setup: pip install -e ".[test]"
lint: ruff check .
test: pytest -x -q --junitxml=report.xml
test_one: pytest -x -q {test}
coverage: pytest --cov --cov-report=json
e2e: npx playwright test
risk: ['auth/**', 'payments/**', 'migrations/**']
skip: ['docs/api/**']

这些命令要用户确认过才会跑,改过之后要重新确认。没有这个文件就不跑任何命令,所有发现都保持未验证;让一个 agent 推断这几条命令、人确认一次再存下来,还在计划中。

谁写、在哪跑分得很清楚:

  • 复现测试由正确性、安全专家写,修复由另一个 agent 写,都由验证器在快照的新副本里跑
  • 项目已有的单元测试也由验证器跑,用来回归:standard 档只跑改动涉及的包,deep 档跑完整测试套件
  • 审查开始前先看 CI:云端读 PR 对应的 GitHub Actions 结果,本地跑 lint;没过就不审,只诊断失败原因

复现测试的判定沿用 SWE-bench 的 FAIL_TO_PASS:修复前必须失败,修复后必须通过。这样系统不绑定语言,项目只要能用命令跑测试就能审。

按风险分档

ultrareview 其实是 Claude Code 审查的最高一档。我也想这样分档,而且越便宜的检查越先跑:先是规则,再是工具,然后是便宜模型,最后才是专家,每一层只处理上一层标出来的内容。档位决定一次审查最多走到哪一层、看得多仔细:

档位专家报告什么验证什么时候用
quick只在 lint、密钥扫描、类型检查这些工具发现问题的地方,最多 2 个便宜模型的专家改变行为或破坏契约的问题不验证测量用,不会被自动选中
standardGo 按分组挑选,没有数量上限改变行为或破坏契约的问题按顺序复现,跑改动涉及的包的测试普通改动
deep同上,用强模型,核心组另加设计专家另外报告命名、风格、措辞和小的简化全部复现,跑完整测试套件和 E2E高风险改动

档位和专家组合都写在 Go 里,提示词则和 skill 一起打进二进制。每个档位有 token 和费用两项预算,验证器跑的每条命令各有超时,总时长不设上限,因为项目自己的测试可能比任何固定上限都慢。

档位不用人选,由 eyeful 按改动自己定:

Architecture flowAn architecture diagram grouped by dependency.新的改动碰到 risk 路径或CODEOWNERS,或diff 超过 2000行其他情况deepstandardArchitecture flowAn architecture diagram grouped by dependency.新的改动碰到 risk 路径或CODEOWNERS,或diff 超过 2000行其他情况deepstandard

还有两处想和 ultrareview 做得不一样。一是能原地升档:跑完不放心,直接升档,复用已经跑过的部分,只补叫缺的专家,ultrareview 只能重新跑一次。二是预算用完时保留结果:ultrareview 中途停下不返回部分结果,eyeful 会把已经验证过的发现照样交出来,标明没跑完。

分档对论文也有帮助:同一套基准按不同档位各跑一遍,就能说清楚 standard 比 quick 多花的钱换来了什么。

[智慧]

从哪来,到哪去

换个角度看,整个系统就是一个生产者和消费者的模型:一头是要审查的东西从哪来,另一头是结果交给谁。中间的审查流程不关心两头具体是什么,只认统一的「审查对象」和「审查报告」。

ultrareview 其实也是这么分的:输入可以是当前分支(含未提交的改动)、指定基线、或者一个 PR;本地代码打包上传,.env 这类凭据文件另有规则,PR 则在云端直接克隆;输出可以是会话里的通知、--json 打出来的 bugs.json,或者 --post 发到 PR 上的一条评论,发不出去时结果留在终端。

eyeful 把两头都做成可以插拔的适配器,但本地和云端分得很开:本地只审自己电脑上的改动,云端只审 PR,两边之间不传代码、记录和密钥。

Architecture flowAn architecture diagram grouped by dependency.本地未提交改动本地分支或提交GitHub PR,云端PRwebhook,计划中快照审查流程:规划 /专家 / 验证 / 汇总审查报告终端Markdown 与SARIF桌面端GitHubCOMMENTreviewCheck Run,可选网页控制台Architecture flowAn architecture diagram grouped by dependency.本地未提交改动本地分支或提交GitHub PR,云端PRwebhook,计划中快照审查流程:规划 /专家 / 验证 / 汇总审查报告终端Markdown 与SARIF桌面端GitHubCOMMENTreviewCheck Run,可选网页控制台

生产者:审查的东西从哪来

本地这一头已经能用,直接用命令行,或者用桌面端:

eyeful connect claude                          # 也可以是 codex、pi
eyeful review                                  # 未提交的改动
eyeful review --base main                      # 当前分支相对 main,含未提交改动
eyeful review --head HEAD --base main          # 指定的提交
eyeful review security                         # 只叫指定的专家
eyeful review --verification execution         # 真的去跑复现测试

本地不打包、不上传,也不需要账号和服务端。审查前 eyeful 先把要审的内容固定成一个快照:基础提交,加上一个装着被审代码的提交,未提交的改动也做成这样一个提交,但不进仓库、不留任何分支。项目命令只在这个快照的一次性检出里跑,审完就删。

云端那边只审 PR:用户授权给 eyeful GitHub App 的仓库里,在控制台确认后发起,服务端直接克隆。webhook 自动触发和定时审查以后再做。

不管从哪来,进到流程里都先变成同一个快照,运行目录里存成 snapshot.json 和 change.diff。审未提交的改动时,head 留空,commit 是 eyeful 自己做出来的那个提交:

{
  "root": "/Users/me/app",
  "base": "9f2c1e4",
  "head": "",
  "commit": "a1b2c3d"
}

档位由 eyeful 按改动自己定,不用传。

消费者:结果交给谁

  • 终端:eyeful review 照 ultrareview 的样子打印,先说明这次改动干了什么,再按严重程度列出发现
  • 运行目录:.eyeful/runs/<时间>/ 里存 review.md、findings.json、results.sarif 和每个阶段的检查点
  • 桌面端:发现显示在 diff 旁边和对应的行上
  • SARIF:给其他 agent、脚本、GitHub code scanning 和评估用
  • GitHub review:问题贴到对应行,验证过的修复附 suggested changes
  • Check Run:可选,默认关闭,还有阻塞问题就失败,可以接到分支保护规则上
  • 网页控制台:看证据和报告

GitHub review 的状态规则写死,不交给 agent 判断:一律以普通的 COMMENT review 发出,阻塞的问题放在行内,其余放进一份排序报告。需要硬性门禁的仓库,打开 Check Run 就行。

eyeful 永远不发 approve。

批准合并是人的事,工具最多只能拦住,不能放行。

本地发起的审查不往 GitHub 发任何东西,结果只留在终端和运行目录里。

中间怎么接

生产者只管把审查对象放进队列,消费者只管订阅审查完成的事件,两头互不知道对方。

结果投递打算用 outbox 的做法(还没做):审查完成时,每个要投递的去向在数据库里记一条待发送记录,由单独的投递任务去发,失败了按退避重试。GitHub API 抽风只会让那一条重试,不影响其他去向;重试多次还失败,报告照样留在控制台和 SARIF 里,可以手动贴。

Architecture flowAn architecture diagram grouped by dependency.审查完成outbox:每个去向一条记录投递任务成功:标记已发送失败:退避重试多次失败:留在控制台和SARIF,手动处理Architecture flowAn architecture diagram grouped by dependency.审查完成outbox:每个去向一条记录投递任务成功:标记已发送失败:退避重试多次失败:留在控制台和SARIF,手动处理

本地:用自己的 coding agent

本地这一头不需要服务端,agent 就是平时在用、已经登录的 coding agent 的命令行。ultrareview 本来就是 Claude Code 里的一个命令,revmux 和 pulls.review 也是这样调用本地的 agent,说明这条路是通的。

每个专家、规划器、修复 agent 和汇总 agent 都是一次新的命令行进程,以非交互模式运行,只带只读工具:Claude Code 是 claude -p --tools Read,Grep,Glob,Codex 是 codex exec --sandbox read-only,pi 是 pi --mode json --tools read,grep,find,ls。用户自己的设置、hook、MCP 和插件都不带进来,提示词从标准输入传进去,agent 最后交出一段符合 schema 的 JSON。用量算在用户自己的订阅上,和平时用它一样。

Architecture flowAn architecture diagram grouped by dependency.eyeful review或桌面端取快照用户自己的 agent命令行,只读验证器:确认过的命令,只在一次性检出里跑终端报告和运行目录按发现接着改改完再审一轮Architecture flowAn architecture diagram grouped by dependency.eyeful review或桌面端取快照用户自己的 agent命令行,只读验证器:确认过的命令,只在一次性检出里跑终端报告和运行目录按发现接着改改完再审一轮
  1. eyeful 找到仓库,取快照,读 .eyeful/config.yml
  2. 列出要审的内容、专家、档位和要跑的命令,用户确认
  3. 规划、专家、验证、汇总依次跑完,同时最多四个专家
  4. 报告打印在终端,也存一份到 .eyeful/runs/
  5. 发现里有位置、失败场景和复现测试,可以直接照着修
  6. 改完再审,同一个改动开新的一轮,上一轮的发现交给这一轮,这是借 revmux 的做法

这样本地审查最大的好处是闭环:审出问题、改、再审,都在自己的机器上完成,代码不出本机。

桌面端是命令行外面的一层窗口,调用同样的本地代码,界面借了 GitHub Desktop 的样子:左边是改动的文件,中间是 diff,右边是审查结果。

pi 在这里可能出现两次,要分清:本地的 pi 是我平时用来写代码的那个,审查时被 eyeful 以只读方式调用;云端的 pi 是服务端起的专家进程,用服务端的模型密钥。两者互不相干。

API

POST   /reviews          发起审查
GET    /reviews          列出审查
GET    /reviews/{id}     状态和结果
DELETE /reviews/{id}     删除排队中或已完成的审查
GET    /repositories     用户授权的 GitHub 仓库
GET    /api/openapi.json OpenAPI 文档

报告和发现的接口、SSE 进度推送、补发到某个去向都还在计划中。审查开始后就不能取消,只能在排队时删除。

鉴权用 API key 或登录会话,登录只走 GitHub App 和 Gitee 的 OAuth。OpenAPI 文档这样生成:handler 上写 swag 注释生成 spec/,再用 openapi-ts 生成 TypeScript SDK,接口一改前端编译就会报错。

部署上,网页构建产物用 go:embed 打进二进制,服务端自己托管前端;默认合在一起、需要时再拆,所以 server 唯一的外部依赖是 PostgreSQL,单机一条 docker compose up 就能跑,评估要大量并发时再在 NAS 之类的机器上单独起 runner(计划中)。

仓库怎么放

单仓库,三个 Go 模块:根模块是服务端,workflow 是云端和本地共用的审查编排,local 是本地命令行和桌面端共用的部分,边界由编译器保证,本地代码写错了也导入不了服务端。TypeScript 部分分 apps/ 和 packages/,用 pnpm workspace 和 Vite+;任务入口统一走 mise。

[画不完了]

.
├── go.mod                    根模块:服务端
├── mise.toml                 开发、生成、测试的统一入口
├── cmd/
│   ├── server/               服务端:serve、migrate
│   └── eyeful/               本地命令行:review、provider、connect、commit
├── workflow/                 审查编排,独立模块,云端和本地共用
│   ├── prompts/              go:embed 进二进制:专家、各角色的提示词、工具定义、来自 skills.sh 的 skill
│   ├── pulls/                打包好的 @pulls.review/core,用 moejs 在 Go 里跑
│   ├── project/              读 .eyeful/config.yml
│   └── report/               Markdown 和 SARIF
├── local/                    本地模式,独立模块
│   ├── app/                  命令行和桌面端的组装入口
│   ├── agents/               调用 claude、codex、pi 的命令行,只读
│   ├── subject/              快照
│   ├── worktree/             快照的一次性检出,跑确认过的命令
│   └── archive/              阶段检查点
├── internal/                 服务端
│   ├── app/                  fx 装配和生命周期
│   ├── server/               echo、中间件、鉴权、限流
│   ├── handlers/             HTTP 路由,带 swag 注释
│   ├── auth/                 用户、会话、API key、OAuth、加密保存的 token
│   ├── repository/           读用户的 GitHub 仓库
│   ├── review/               审查记录和租约
│   ├── scheduler/            工作进程池
│   ├── dispatch/             模型路由
│   └── db/                   sqlc 生成的代码,不手改
├── db/                       迁移和 SQL 查询
├── spec/                     生成的 OpenAPI
├── apps/
│   ├── web/                  SvelteKit 控制台,构建产物嵌进 server
│   └── desktop/              Wails v3 桌面端,独立模块
├── packages/
│   ├── sdk/                  从 OpenAPI 生成的 TypeScript 客户端
│   ├── ui/                   shadcn-svelte 组件、diff 视图、文件树
│   └── pulls/                构建 workflow/pulls/core.js
└── docs/

计划中还有 internal/sandbox、internal/pi、internal/source、internal/sink、internal/outbox、cmd/runner、plugins/ 和 eval/。

workflow 自己不起 agent、不跑命令、不存东西,由调用方传进三个接口,云端和本地各实现一份:

Architecture flowAn architecture diagram grouped by dependency.workflow:规划 /专家 / 验证 / 汇总AgentsWorkspaceArchive本地:local/agents云端:pi,计划中本地:local/worktree云端:沙箱,计划中本地:.eyeful/runs云端:运行档案,计划中Architecture flowAn architecture diagram grouped by dependency.workflow:规划 /专家 / 验证 / 汇总AgentsWorkspaceArchive本地:local/agents云端:pi,计划中本地:local/worktree云端:沙箱,计划中本地:.eyeful/runs云端:运行档案,计划中

几个细节

[烫手]

  • 孤儿容器:每个沙箱打上所属审查的标签,server 启动时按标签对账,清掉已经结束的审查留下的沙箱
  • 复现互相影响:每次复现都在一份新副本上跑,一条发现的测试不会影响另一条
  • agent 互相污染:每个 pi 进程一个全新的 HOME,加 --no-session;被审仓库里提交的 agent 指令文件一律忽略
  • 沙箱网络:沙箱里没有任何凭据,只在 setup 装依赖时能访问包仓库,其他网络都不通;pi 进程和模型密钥留在沙箱外
  • GitHub 权限:token 只留在 server,不进沙箱;eyeful 不推任何提交,修复只作为 suggested change 给出
  • 进程挂了:审查用租约领取,每个阶段的产出写进运行归档,接手的进程从最后完成的阶段继续,已经完成的模型调用不重跑
  • 花了多少钱:从每次调用的输出里收集 token 和费用,记进运行归档

怎么评估

用哪些基准

用三套数据,各管一件事。

自建注入缺陷基准,跑主实验。bug 注入到模型训练截止日期之后的仓库和提交里,模型不可能见过,泄露的问题最小。

SWE-bench Verified 改造,作为第二套数据。改造方法是把官方修复补丁反过来用:在修好的版本上把「修复」倒回去,就得到一个「引入 bug 的 PR」。bug 一定存在,官方的 FAIL_TO_PASS 测试就是标准答案,Docker 环境也是现成的。这个改造是我自己的做法,不是 SWE-bench 官方的用法,而且模型可能在训练时见过这些仓库,论文里都要写清楚。

Code Review Bench 离线集,看整体审查质量,分专家的召回率和精确率,顺便和 Claude Code 这些现成工具横向比。

它是 Martian 开源的,专门评 AI 审查工具。离线部分有 50 个真实 PR,来自 Sentry、Grafana、Cal.com、Discourse、Keycloak 五个项目,覆盖 Python、Go、TypeScript、Ruby、Java;人工标注了 173 条「审查应该指出的问题」,每条带严重程度和类别,用 LLM 当裁判判断工具有没有说中。Claude Code、CodeRabbit、Copilot 都在它的榜上。

它的类别正好能和专家对上:bug、concurrency、data 归正确性,security 归安全,test_gap 归测试,api 归可用性,style、doc_defect 归可读性。

[呜哇]

三套都有短板:自建基准规模小,埋的 bug 可能不够真实;SWE-bench 改造题只有 Python,也有泄露风险;Code Review Bench 的五个项目都很大,测试环境很难全部搭起来,执行验证只能在能跑的那部分上做。

看哪些指标

  • 召回率:已知的 bug 找出了几个
  • 精确率:报出来的问题里有几个是真的。额外的发现要人工标注,我自己按一份写好的标注指南来标,三种条件的发现混在一起打乱,看不到来自哪种条件;大约 20% 隔两周再标一次,用 Cohen's κ 报告自己前后的一致性
  • 规划准确率:该叫的专家有没有叫,不该叫的有没有多叫,可以从 Code Review Bench 的类别推出标准答案
  • 修复通过率:修改建议里有多少能直接通过测试
  • 成本和耗时:每次审查的 token、钱和分钟数,只在 pi 上测

对比实验

主实验只有一个问题:执行验证 vs 读代码验证,误报差多少,成本差多少。

这是论文的核心实验,在 eyeful 内部做,只换验证方式;revmux 作为外部参照。

主实验只看能复现的发现,也就是正确性和安全专家给出的发现,三种条件的专家、提示词和模型都一样:

条件验证方式
execution验证器运行每个复现测试,强度看期望行为从哪来
read-only一个评判 agent 读代码和每条发现,给出结论
none不验证,按专家写的原样报告

revmux 的验证也是只读的,会在同样的数据上跑一遍当外部参照,但不当基线,因为它的提示词和切分方式都和 eyeful 不一样。

其余几组都是消融:去掉一个部分,重新跑,看哪些指标下降。

  1. 判定依据规则:任何失败的测试都算强证据,精确率会不会下降
  2. 按需启动:每次改动都叫全部专家,召回率有没有变化、成本涨多少
  3. 规划器:所有档位都用规则分派
  4. 分档:standard 比 quick 多发现了什么
  5. 依次去掉每个专家,看它负责的类别召回率掉多少

去掉之后没有任何可测量变化的部分,就从 eyeful 里删掉。

最后

写这篇的时候方案还停在纸面上,现在本地审查已经能用了:eyeful review 和桌面端用自己的 Claude Code、Codex 或 pi 跑完整个流程,规划、六个专家、验证和汇总都在。接下来的 v1.0 只做回答研究问题需要的部分:pi 里的 agent、SWE-bench 的 Docker 环境、运行归档和 eval/,先把主实验跑完;云端审 PR 等有了数据再做。很多数字还要等真跑起来才知道,比如规划选专家选得准不准、执行验证到底能去掉多少误报,到时候再回来更新这篇。

[期待]