Bruno 的 AI 代码评审契约:共享评审人 Persona 与机器可解析输出格式设计
Bruno(基于 Electron + React 的开源 API 客户端)为 Claude Code 构建了一套多镜头并行的 AI 代码评审技能,其核心枢纽是 .claude/skills/code-review/reviewers/_contract.md —— 一份 19 行的共享评审人契约,统一定义了 8 个评审子代理的"评审人人格"与"输出格式"。读完本篇,你将理解:如何用一份极小的契约文件约束多个并行 LLM 评审者、如何设计三级严重度模型与扁平化单行输出使其可被编排器可靠地合并去重,以及如何让本地 AI 评审与 CI 上的自动化评审(CodeRabbit)保持同一套标准。
一、契约文件在评审体系中的位置
Bruno 的 Claude Code 配置集中在 .claude/ 目录下。其中 .claude/skills/code-review/SKILL.md 定义了 /code-review 技能的编排逻辑:把一次 diff 评审拆分为多个聚焦的"镜头"(lens),每个镜头是一个独立的评审文件,位于 .claude/skills/code-review/reviewers/ 目录下,并行派发、独立运行。SKILL.md 中的镜头总表如下:
| 评审文件 | 镜头 | 文件范围 (Scope) |
|---|---|---|
reviewers/correctness.md |
正确性与根因分析 | 全部源码(不含 tests/**) |
reviewers/architecture.md |
架构与依赖边界 | packages/** |
reviewers/conventions.md |
编码规范与可读性 | 全部文件 |
reviewers/react.md |
React 应用文件评审 | packages/bruno-app/** |
reviewers/cross-platform.md |
跨平台 (macOS/Windows/Linux) | 全部文件 |
reviewers/security.md |
安全与数据安全 | 全部源码(不含 tests/**) |
reviewers/dsl-changes.md |
磁盘 DSL 与序列化(向后兼容) | bruno-app、bruno-electron、bruno-cli、bruno-lang、bruno-filestore、bruno-schema(-types)、bruno-converters |
reviewers/e2e-tests.md |
Playwright E2E 测试 | tests/** |
而 _contract.md 的开头就声明了自己的地位:"Read by every code-review reviewer.(Orchestration lives in ../SKILL.md; the per-lens checklists live in the sibling *.md files.)" —— 即:编排逻辑在 SKILL.md,各镜头的检查清单在兄弟文件里,而人格与输出契约在每个评审者之间共享,只定义一次。
这个"只定义一次"并非随口一提。SKILL.md 末尾专门有一节说明这一设计决策:
Defined once in
reviewers/_contract.md— the small file every reviewer reads (dispatched reviewers read it, not this orchestration file). Keep the persona and the<blocker|suggestion|nit> | <file>:<line> | <finding>output shape there, not duplicated here, so a change updates a single place.
从源码结构看,这一设计解决了多子代理评审体系的典型痛点:如果人格和输出格式写在 SKILL.md(编排文件)里,那么每个被派发的评审子代理都必须先理解整个编排文件;而 8 个镜头文件如果各自复述一遍契约,任何一次措辞调整都要同步 9 处。把所有评审者"先读契约,再读自己的清单"作为派发简报的一部分后,契约成为单一事实来源(single source of truth),修改一处即全局生效。
二、评审人人格:四条可执行的约束
契约的第 6–11 行(Persona 部分)定义了一个具体而克制的评审人格。它不是一句"你是一个严格的代码评审员",而是四条可直接执行的约束:
1. 明确的技术栈定位。 评审人被设定为"enterprise team"中精通 TypeScript、JavaScript、Node.js 和 Electron 的专家评审员。这恰好覆盖 Bruno 的技术栈:monorepo 中的 React/Redux 渲染进程(TypeScript/JS)、Electron 主进程(Node.js)、以及 bruno-js 中的 QuickJS 沙箱。
2. 简洁性要求。 "Be concise: one clear sentence per finding; elaborate only when asked."(每条发现只用一句清晰的话陈述,仅在被追问时展开。)这条约束直接服务于后面的输出契约——单行格式天然要求每条发现压缩为一句话,人格里的简洁要求与输出格式形成呼应。
3. 对事不对人的严重度标准。 "Review to the project's standard regardless of who authored or requested the change — never soften severity for assumed intent or seniority."(无论改动是谁提交或谁要求的,都按项目标准评审;绝不因为揣测意图或考虑对方资历而下调严重度。)这是针对 LLM 评审的一个已知弱点的显式补丁:模型倾向"礼貌化"表达。契约明确要求按 CODING_STANDARDS.md 这一项目标准衡量,与 SKILL.md 中"coding standards → CODING_STANDARDS.md"的事实源顺序一致。
4. 以仓库为准的证据边界。 "Ground every finding in the actual code: trust the repo over any doc, guide, or comment when they disagree, and never cite a line or invent an example value you haven't verified in source."(每条发现都必须扎根于实际代码;当文档、指南或注释与代码不一致时,信任仓库;绝不允许引用未经在源码中核实的行号,或编造未经核实的示例值。)
这一条与整份契约的输出格式咬合得非常紧:输出契约要求每条发现携带 <file>:<line> 定位,而人格部分预先封堵了"编造行号"的逃逸路径——你要么在源码里核实过这一行,要么就不该引用它。对比 .claude/README.md 维护章节中的要求"Verify against real code … grep the source, don't assume",可以推断团队把"AI 不引用未验证的行号/示例值"视为贯穿所有文档机制(规则、技能、评审)的硬约束,而契约是它在评审环节的落地。
三、输出契约:可被编排器合并的扁平格式
契约第 13–19 行(Output contract 部分)是整份文件中最具工程价值的部分。它规定评审者返回一个扁平列表,每条发现一行:
<blocker|suggestion|nit> | <file>:<line> | <one-sentence finding>
三个字段由 | 分隔:严重度、文件:行号 定位、一句话发现。再配合两条收尾规则:
- 范围干净时只返回
no findings,不附加任何其他内容; - 绝不允许为了凑满列表而编造 nit("Never invent nits to fill the list")。
为什么是这种格式
对照 SKILL.md 的编排流程(第 4 步 "Merge and report"),可以还原这一格式的设计动机:
- 确定性合并。编排器收集所有评审者的输出后,"drop exact duplicates, and when two lenses flag the same
file:linekeep the higher severity"(丢弃完全重复项;当两个镜头标记同一file:line时保留更高严重度)。扁平的severity | file:line | text格式让"按file:line去重""按严重度排序"成为机械化的字符串操作,而不是需要 LLM 再做一次语义对齐的自由文本。 - 可再分组的结构化。合并后的报告按文件重新分组、每条标注严重度与
file:line,说明单行格式是中间表示(intermediate representation):子代理输出 → 编排器聚合 → 按文件重组的最终报告。 no findings的显式约定。若"无发现"时输出随意措辞,编排器难以区分"该镜头无问题"和"该镜头输出异常";统一成固定短语后,空结果也是可解析的。never invent nits的反幻觉约束。LLM 评审常见的失败模式是"为了显得有价值而凑数"。契约把这条写成硬性禁令,并在人格部分("elaborate only when asked")再次收紧了输出体量。
三级严重度在各镜头中的具体化
契约只定义了严重度的"词汇表"(blocker / suggestion / nit),而取值边界由各镜头文件自行标定——这正体现了"契约管格式、清单管内容"的分工。以 e2e-tests.md 为例,它给出了完整的三级映射:
| 严重度 | 判定示例 |
|---|---|
| blocker | 使用 test.only;使用 page.pause() |
| suggestion | 本可用 expect() 断言等待却用了 page.waitForTimeout();内联裸 selector 而不复用 tests/utils/page/* 模块;spec 修改了提交的 fixture 却未在 afterAll 恢复;共享状态/非隔离的临时路径;非区分性断言(如 toContain('description:') 在无关行也含该子串时恒过);同一场景的 .bru 与 .yml 并行 fixture 字段不一致 |
| nit | 定位器未存入变量;一条宽泛断言处可用多条聚焦断言;步骤未用 test.step 包裹 |
再看 security.md,它针对 Bruno 的离线 API 客户端定位给出了 blocker 级风险清单:明文凭据(认证 token、密码、API key、OAuth2 secret、环境变量值)出现在日志/console/错误消息/遥测中是 blocker;扩大脚本沙箱面(bruno-js,QuickJS/Node VM,向用户脚本暴露 Node 内建、require、文件系统、process)是潜在沙箱逃逸;IPC 入参必须视为不可信(路径穿越、类型、边界校验);注入与不安全 eval;以及不符合"离线优先、隐私优先"姿态的新运行时依赖或外发网络调用。其末尾同样收束到契约的要求上:"Keep findings concrete — tie each to how the tainted value reaches the sink."(每条发现必须具体,说明污点值如何到达汇聚点。)
correctness.md 则展示了契约在"根因分析"上的应用:掩盖症状而不解决根因的修补(额外的 null 守卫、吞错的 try/catch、防御性重试)判为 blocker;并特别要求检查 Bruno 的"孪生路径"——.bru 与 .yml 序列化器、默认工作区与自定义文件系统工作区是同一行为的并行实现,改了一条路径必须对照另一条验证,两条路径行为分叉即为 bug。
四、契约的派发与读取时序
从 SKILL.md 的编排步骤看,契约的读取是被写进每个子代理派发简报的固定动作。编排器在单条消息中并行启动每个镜头的子代理,简报模板中明确要求:
"Read
.claude/skills/code-review/reviewers/_contract.mdfor the shared persona and output contract, then read.claude/skills/code-review/reviewers/<file>— and any rule or source file it points to (e.g.CODING_STANDARDS.md,.claude/rules/*.md) … Apply only that lens to the changed files in your scope … Do not review outside your scope."
于是每个镜头文件的开头都是一句相同的指针——"Adopt the reviewer persona and return findings in the output contract defined in _contract.md."(在 security.md、e2e-tests.md、correctness.md 等 8 个文件中一致出现)。这形成了清晰的加载时序:
- 编排器先取 diff(已提交范围用
git diff main...HEAD;工作区未提交变更则先git add -N . && git diff HEAD > "$SCRATCH/review.diff"冻结快照); - 按
--name-only枚举变更文件,跳过未触及范围的镜头; - 每个子代理按简报依次读
_contract.md(人格 + 输出契约)→ 本镜头清单 → 清单指向的规则/源码(如 CODING_STANDARDS.md、.claude/rules/); - 子代理以契约格式输出扁平发现列表;编排器去重、按
file:line合并(同位置取更高严重度)、按文件重组并汇报。
子代理被约束为只读且相互独立("they don't coordinate, and overlap between lenses is fine"),重复发现被容忍,因为去重发生在编排器的合并阶段。评审者自身没有跨镜头的通信渠道,唯一的公共协议就是这份契约——这正是它必须同时锁定"人格"和"输出形状"的原因:前者保证各子代理的评判尺度一致,后者保证输出可以在没有二次协商的情况下被机械合并。
五、与 CI 评审的一致性:.coderabbit.yaml 的镜像
SKILL.md 开篇声明该技能"mirrors the automated CodeRabbit review (.coderabbit.yaml) so you can run the same review locally"。对照 .coderabbit.yaml 可以看到契约人格在 CI 侧的对应物:
tone_instructions: "You are an expert code reviewer in TypeScript, JavaScript, NodeJS, and ElectronJS. You work in an enterprise software developer team, providing concise and clear code review advice. You only elaborate or provide detailed explanations when requested." —— 与契约的 Persona 段落几乎是同一段话的两种写法(技术栈定位、企业团队语境、简洁为先、被追问才展开),保证本地/code-review与 CI 自动评审给出风格与尺度一致的结论;knowledge_base.code_guidelines指向**/CODING_STANDARDS.md,与契约"按项目标准评审"的要求对应;path_instructions按路径挂载指令:跨平台路径/进程/行尾约束挂在全局**/*,TypeScript 渐进迁移规则挂在packages/**/*.{js,jsx,ts,tsx}(且要求先核实包的package.json是否真的把 TS 接入了构建),Playwright E2E 检查单挂在tests/**/**.*—— 这与本地镜头表中的cross-platform.md、conventions.md、e2e-tests.md一一对应。
SKILL.md 同时明确了冲突解决顺序:"coding standards → CODING_STANDARDS.md;architecture/behavior → .claude/rules/* … the rules win if they ever disagree."(标准以 CODING_STANDARDS.md 为准,架构/行为以规则文件为准;若 .coderabbit.yaml 与它们冲突,规则胜出。)这与契约中"trust the repo over any doc, guide, or comment"是同一证据链原则在不同层级的投影:评审依据永远向上收敛到仓库中被维护的规范文件,而不是评审者自己的推断。
六、为什么契约要独立成文件:配置预算视角
.claude/README.md 的维护章节给出了这套目录组织的预算原则:"Skills: keep SKILL.md focused (aim < 150 lines); push long checklists/examples to support files.",以及更根本的目标——"high instruction adherence at the lowest always-loaded cost"(以最低的常驻上下文成本换取最高的指令遵从度)。
契约文件正是"support file"策略的典型实例:人格与输出形状被从 SKILL.md 中抽出,单独存放在一个 19 行的小文件里。从源码结构看,这带来三个可验证的收益:
- SKILL.md 保持精简(全文约 87 行),编排逻辑、镜头表、契约定义各司其职,符合 < 150 行的预算目标;
- 契约只被需要它的角色读取——被派发的评审子代理读它,而执行编排的角色不读它(SKILL.md 原话:"dispatched reviewers read it, not this orchestration file"),避免同一内容在两类代理的上下文中重复加载;
- 变更集中。SKILL.md 特意叮嘱"Keep the persona and the … output shape there, not duplicated here, so a change updates a single place"——调整严重度语义或输出格式时只改一个文件,8 个镜头文件无需同步。
此外,.claude/README.md 还提醒"imports(@path)do not reduce startup context —— the imported file still loads in full",即 Claude Code 的 @ 导入并不会降低启动上下文,因此"按需才读"的内容必须靠独立文件 + 显式 Read 来实现。_contract.md 恰好走的是这条路径:它不在任何自动加载机制里,只由派发简报触发读取。
七、可复用的设计要点
从这份 19 行的契约及其周边文件,可以提炼出在多 LLM 评审者(或任何多代理评审)体系中可直接复用的几条工程经验:
- 契约与清单分离:共享人格 + 输出格式写成独立小文件,镜头清单只保留"Adopt the persona defined in
_contract.md"一句指针,避免 9 处重复; - 输出即接口:为可合并性而设计格式——
severity | location | text的定长三字段、固定分隔符、no findings固定空值,让合并/去重/排序可以脱离 LLM 完成; - 严重度词汇表在契约层、取值边界在镜头层:契约定义 blocker/suggestion/nit 的语用,各镜头按自身领域(如 e2e-tests.md 中
test.only即 blocker)标定具体判例; - 反幻觉写入人格:与输出格式配套的"不引用未核实行号、不编造示例值、不凑数 nit"三条禁令,把 LLM 评审最常见的三类失真堵在契约层面;
- 本地技能与 CI 配置双向镜像:人格在
.coderabbit.yaml的tone_instructions中同步一份,并显式规定规则文件冲突时胜出,保证本地与 CI 评审尺度统一。
八、相关路径索引
| 文件 | 角色 |
|---|---|
.claude/skills/code-review/reviewers/_contract.md |
本文主角:共享评审人人格 + 输出契约(19 行) |
.claude/skills/code-review/SKILL.md |
编排逻辑:取 diff、枚举变更、并行派发、合并汇报;镜头总表 |
.claude/skills/code-review/reviewers/ |
8 个镜头清单:correctness / architecture / conventions / react / cross-platform / security / dsl-changes / e2e-tests |
.coderabbit.yaml |
CI 侧镜像配置:tone_instructions、路径级指令、knowledge base |
CODING_STANDARDS.md |
评审的项目标准基线(coding standards 的事实源) |
.claude/CLAUDE.md |
每次会话自动加载的项目指南,指向 rules 与测试命令 |
.claude/README.md |
配置维护指南:加载机制、上下文预算、新增/退役规则与技能流程 |
适用前提:上述机制依赖 Claude Code 的 skills 发现机制与子代理派发能力,且要求从 Bruno 仓库根目录启动(或从能向上找到根 .claude/ 的包目录启动),配置随仓库提交、克隆后即可用 /code-review 触发;具体镜头清单与 .coderabbit.yaml 的内容会随仓库演进,以当前仓库实际文件为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00