首页
/ LobeHub Deep Review 技能深度解析:多 Agent 代码审查中的“全局发现合并”步骤——同根合并语义、触发条件与 JSON 输出契约

LobeHub Deep Review 技能深度解析:多 Agent 代码审查中的“全局发现合并”步骤——同根合并语义、触发条件与 JSON 输出契约

2026-09-04 15:00:28作者:史锋燃Gardner

本文围绕 LobeHub 仓库中 deep-review 技能的 consolidate-prompt.md 展开,完整讲解深度多 Agent 代码审查流水线中“全局发现合并”(Global Finding Consolidation)这一步骤的触发条件、输入实例化、同根判定语义、JSON 输出契约,以及它如何被 Zod 校验脚本验证并最终折叠进审查报告。读完之后,你将理解一个不改动任何审查结论、只做跨审查器去重的独立 Prompt 工程化设计,并能在本仓库中找到它的完整实现与测试证据。

1. 合并步骤在 Deep Review 流水线中的位置

deep-review 是 LobeHub 仓库内置的一个 Agent 技能(入口为 SKILL.md),用于对 PR、分支或粘贴的 diff 做多维度代码审查。其 Deep 模式的完整编排是:

dimension review agents → pipelined verification → global consolidation → structured report → interactive fix flow

“全局发现合并”就是其中的第三环,由两份环境手册分别规定为 Step 4:

两份手册对该步骤的约束一致,可以归纳为三条:

  1. 触发门槛:只有当队列排空后仍然至少存在 2 条 confirmed 发现时才运行;0 条或 1 条 confirmed 发现直接跳过(Claude Code 手册明确写着 "Zero or one confirmed finding skips this step")。
  2. 一次性、验证之后:该步骤在整个 Deep 运行中只跑一次,且必须等待 pipelined verification 阶段完全结束。SKILL.md 的预算规则还规定,同一次 Deep 运行内的 review / verify / consolidate 三波全部计入一次 Deep 预算。
  3. 职责边界:正如 consolidate-prompt.md 开头所言——"This pass does not re-verify correctness; it identifies roots duplicated across verifier payloads"。它不做第二次正确性验证,只识别跨审查器(verifier)payload 之间重复出现的根因。

为什么要单独开一个步骤而不是让验证器顺手去重?从 review-prompt.md 的设计看,每个审查子代理只被允许报告自己维度(dimension)内的发现("do not report findings outside your assignment");在 Claude Code 环境里甚至一个维度就是一个独立 Task。这意味着同一个根因很可能被不同维度的审查器各自独立发现——例如文档自带示例中的 style-2ai-1 就分属 code-style 与 ai-coding-bad-habits 两个维度。任何单个审查器都看不到全局,只有把所有 confirmed 发现集中起来的独立合并步骤才能完成这种跨维度去重。

2. 输入:{confirmed_findings} 的实例化规则

consolidate-prompt.md 对输入有明确的实例化约定:

Instantiate {confirmed_findings} with the confirmed findings after applying verifier overrides, including id, dimension, location, summary, evidence, and fix options.

拆开来看有三个要点:

  • 来源:只取 verify 阶段结论为 confirmed 的发现。验证器的三向判定(confirmed / false_positive / need_more_context)定义在 verify-prompt.md 的返回契约中;false_positive 只进统计,need_more_context 进入 needsContext 池,二者都不会进入合并输入。
  • 应用 override 之后:验证器允许输出 severity_overridenature_overrideexposure_overridelikelihood_overridefix_options_override 等纠正字段。主 Agent 必须在喂给合并步骤之前应用这些 override,即合并步骤看到的是"生效值"而非审查器原始值。
  • 字段集:每条输入至少携带 iddimensionlocationsummaryevidencefix_options。其中 fix_options 尤其关键——第 4 节会说明,同根判定的核心测试正是基于"能否用一个具体修复同时解决两条发现",而判断这一点必须能看到候选修复项。

两份环境手册还规定了输入顺序:"Pass confirmed findings after verifier overrides, in report order." 即按照最终报告的排序(P0 → P1 → P2,维度按 SKILL.md 维度表顺序)传入。这个顺序不是随便选的:它决定了下一节中"根"的归属规则。

3. 完整 Prompt 契约:同根判定的语义规则

以下是 consolidate-prompt.md 中模板的完整内容(占位符 {confirmed_findings} 由主 Agent 注入):

Consolidate duplicate code-review findings. Do not change verdicts, severity, likelihood, evidence,
or fix options.

## Confirmed findings
{confirmed_findings}

Two findings share a root only when one concrete fix resolves both. Similar location, theme, or
dimension is not enough.

Choose the earliest finding in input order as the root. Return only later duplicate ids. Never:

- map an id to itself;
- invent an id;
- create a cycle;
- merge findings that require separate fixes; or
- merge a release-risk consequence with a code defect when fixing the defect would not also remove
  the release consequence.

Return exactly one valid JSON object in a `json` fence. No prose or comments.

```json
{
  "same_root": [
    {
      "id": "style-2",
      "same_root_as": "ai-1"
    }
  ]
}
```

When no duplicates exist, return `{"same_root":[]}`.

这段不到 30 行的 Prompt 实际上是一份非常严格的语义契约,值得逐条拆解:

3.1 不可变性约束:"Do not change verdicts, severity, likelihood, evidence, or fix options"

合并步骤对五条核心字段只读。这体现了流水线中"职责分离"的原则:判定正确性属于 verify 阶段,去重属于 consolidate 阶段,渲染属于 report 阶段。如果合并步骤被允许顺手调整 severity 或 fix options,整个"验证器是唯一裁判"的审计链条就被破坏了。这一约束与 SKILL.md 核心原则 1(Anti-hallucination)一脉相承——每一个字段的"所有权"都归属产生它的那个阶段。

3.2 同根判定:唯一的充分条件是"一个具体修复同时解决两者"

Two findings share a root only when one concrete fix resolves both. Similar location, theme, or dimension is not enough.

这是整个 Prompt 中最关键的一句。它把"同一根因"从模糊的主题相似度问题,收敛成一个可操作的判据:是否存在一个具体修复(concrete fix),使得两条发现同时消失。仅满足以下任一条件都不算同根:

  • 位置相近(同一个文件或相邻函数);
  • 主题相似(都关于"错误处理");
  • 维度相同(都出自 code-style)。

这个判据的实战价值在于防止"过度合并"。两条 P1 发现可能位于同一文件、描述相似,但一个需要改输入校验、另一个需要修状态机——它们共享的是位置而非修复。若按位置合并,报告里会丢失一条独立的修复项,导致其中一个缺陷永远不会被修。把判据锚定在 fix_options 上,恰好利用了第 2 节要求输入携带该字段的设计。

3.3 根的选择与输出形态:"最早者即根"

Choose the earliest finding in input order as the root. Return only later duplicate ids.

同根组内,输入顺序上最早的那条发现被选为根,输出里只需要列出"更晚的重复 id",每个重复项用 same_root_as 指回根。这套设计有两个工程上的好处:

  1. 确定性。"最早者"规则不依赖模型做任何选择,重跑同一输入必然得到同一输出,避免了 LLM 在并列候选间摇摆导致的输出不稳定。
  2. 最小输出面。输出是一个部分映射(partial map):没有被映射到的 confirmed id 隐式就是根。输出条目越少,模型"凭空捏造 id"的空间越小——这也呼应了下文 Never 清单。

3.4 Never 清单:五类硬性禁止

Prompt 用五条 "Never" 封死了合并步骤所有典型的失败模式:

禁止行为 防御的失败模式
map an id to itself 自映射(same_root_as 指向自己),无意义且说明模型对映射语义理解错误
invent an id 输出中出现输入里不存在的 id——典型的 LLM 幻觉
create a cycle 出现 a→bb→a(或更长环)的映射,渲染时无法确定根
merge findings that require separate fixes 违反 3.2 节的同根判据,把需要独立修复的发现强行合并
merge a release-risk consequence with a code defect when fixing the defect would not also remove the release consequence 见下节专门分析

最后一条是针对 LobeHub 这种"代码 + 发布风险"双维度审查场景的定制规则。release-risk 维度既产出代码缺陷类发现,也产出 release_checks(预部署确认项)。一条发布风险"后果"(例如"某配置未在某环境生效")可能由一个代码缺陷引起,但修复该缺陷并不必然消除该后果(比如还依赖人工核对生产数据形态)。此时若把两者合并,后果项会被"折叠"进缺陷条目,预部署检查项就永远无人执行。Prompt 因此要求:只有当"修掉缺陷也同时消除了该发布后果"时,二者才允许同根。

3.5 输出契约:单个 JSON 对象 + json fence + 无重复时的空数组

  • 必须返回恰好一个有效 JSON 对象,包在 json fence 里,"No prose or comments"——这为第 4 节的机器提取器留出了确定性的解析入口;
  • 结构固定为 same_root 数组,元素只有 idsame_root_as 两个字符串字段;
  • 无重复时返回 {"same_root":[]},而不是省略字段或输出其他变体。

文档自带的示例 {"id": "style-2", "same_root_as": "ai-1"} 恰好跨维度(code-style 的 style-2 指回 ai-coding-bad-habits 的 ai-1),再次印证了第 1 节的判断:这个步骤存在的意义就是处理"单个审查器看不见的跨维度重复"。

4. 程序化验证:validate-output.ts 如何守住这道门

Prompt 约束只是"软契约",真正保证输出可用的是仓库内的校验脚本。validate-output.ts 为 Deep 模式的三种子代理输出(review / verify / consolidate)分别定义了 Zod schema,合并步骤对应的是 ConsolidationOutputSchemavalidate-output.ts#L144-L161):

const ConsolidationOutputSchema = z
  .object({
    same_root: z.array(
      z
        .object({
          id: z.string().min(1),
          same_root_as: z.string().min(1),
        })
        .strict(),
    ),
  })
  .strict()
  .superRefine(({ same_root }, context) => {
    const ids = same_root.map(({ id }) => id);
    if (new Set(ids).size !== ids.length) {
      context.addIssue({ code: 'custom', message: 'consolidation ids must be unique' });
    }
  });

它提供了三层机器检查:

  1. 顶层 .strict():拒绝 same_root 以外的任何多余顶层字段(比如模型顺手加的 "note");
  2. 元素 .strict() + min(1):每个条目只允许 id / same_root_as,且都不能是空串——从结构上杜绝了输出里夹带解释性字段;
  3. superRefine 唯一性约束:同一个 id 不允许出现两次(即一条发现不能同时被映射到两个不同的根),对应错误信息为 consolidation ids must be unique

对应的测试用例在 validate-output.test.ts#L109-L118

it('rejects duplicate consolidation ids', () => {
  expect(() => validateOutput('consolidate', {
    same_root: [
      { id: 'style-2', same_root_as: 'ai-1' },
      { id: 'style-2', same_root_as: 'logic-1' },
    ],
  })).toThrow();
});

值得注意的是,schema 只覆盖结构,不覆盖语义。"Never 清单"里的后三条——捏造 id、自映射、成环、根出现在重复项之后——无法用静态 schema 表达(需要对照输入 id 集合和拓扑分析),因此由环境手册规定为主 Agent 的额外运行时检查。以 claude-code/main.md#L73-L75 为例:

Validate the result with validate-output.ts consolidate, then reject any invented id, self-map, cycle, or root that occurs after its duplicate. Invalid output → re-spawn consolidation. Apply the map only after validation.

即完整的验收流程是"两级闸门":

  1. 第一级(机器)bun run .agents/skills/deep-review/scripts/validate-output.ts consolidate(stdin 或临时文件传入子代理响应)。脚本先经 extractJsonPayloadvalidate-output.ts#L171-L185)提取 fence 内 JSON——发现多个 json fence 抛 Multiple JSON fences found,fence 未闭合抛 JSON fence is not closed——再走 Zod 解析;
  2. 第二级(主 Agent):对照输入 id 集合逐一核对是否存在捏造 id、自映射、环、以及"根晚于其重复项"(违反"earliest finding in input order as the root"的确定性规则)。

任何一级失败都触发用同一 Prompt 重新派发合并子代理,且 "Apply the map only after validation"——校验未过之前,映射一律不生效。

5. 合并结果如何折叠进最终报告

合并映射通过校验后,被 report-template.md 的消费规则直接决定报告形态(report-template.md#L21-L24):

Same-root merge: findings mapped to same_root_as: X by the global consolidation pass fold into X's entry — no separate number; add **Same root**: id (location, issue_type), ... at the entry's end; entry severity upgrades to the highest among merged; statistics count merged entries once.

落到报告上有四个具体效果:

  1. 折叠:重复项不再拥有独立编号,并入根的条目,条目末尾追加一行 **Same root**: id (location, issue_type)(模板渲染格式见 report-template.md#L114);
  2. severity 升级:合并后条目的严重级别取组内最高值;
  3. blocks_release 取值:报告渲染规则明确 "Same-root merged entries take the value belonging to the highest merged severity"(report-template.md#L16),保证合并不弱化 ship/no-ship 信号;
  4. 统计口径:Statistics 一节中合并条目只计一次,避免同根缺陷在确认数里虚高。

在 PR 模式下,这一口径继续传导到 Merge verdict:判定表只统计 in-scope 的 confirmed P0/P1,合并升级后的 severity 决定该条目是否构成 "fix before merge" 的前置项。也就是说,consolidate 步骤虽然不改变单条发现的任何字段,却通过"折叠 + 升级"间接影响了合并结论与报告篇幅——这正是它必须在渲染之前完成、且"校验未过绝不应用"的原因。

6. 设计要点归纳:为什么是这样一个步骤

consolidate-prompt.mdSKILL.md 的核心原则对照起来,可以提炼出这个 Prompt 的几个设计取向:

  • 用规则代替模型判断(对应原则 3 "Rules over model")。同根判定、根的选择、输出形态全部被写成无歧义的硬规则,模型只需执行不需要权衡,把"聪明"省给了真正需要判断的 verify 阶段。
  • 确定性优先。"最早者即根 + 只输出较晚的 id"使输出成为输入的确定函数,天然可复现、可测试,也让主 Agent 的运行时检查(环、自映射)保持廉价。
  • 最小化输出面以抑制幻觉。单 JSON fence、无 prose、无重复时返回空数组而非省略——配合 extractJsonPayload 对 fence 数量的硬拒绝(见 validate-output.test.ts#L37-L41rejects multiple JSON fences 用例),任何多嘴的输出都会被直接拦下重跑。
  • 不重复做昂贵的事。"does not re-verify correctness" 使该步骤的上下文只需携带 6 个字段的发现摘要,而不需要重新打开源文件;从成本结构看,这是一次廉价的"报告整形",与 deep-mode 预算规则(一次逻辑需求至多一次 Deep)相契合。

7. 在本仓库中查看与验证该步骤

实际验证方式:可以直接对校验脚本投喂样例载荷,观察两级闸门的分工——

# 通过 stdin 校验一个无重复的合法输出
echo '```json
{"same_root":[{"id":"style-2","same_root_as":"ai-1"}]}
```' | bun run .agents/skills/deep-review/scripts/validate-output.ts consolidate
# 期望输出: Valid consolidate output

# 同一 id 映射两个根 → 触发 "consolidation ids must be unique" 并以非零码退出
echo '```json
{"same_root":[{"id":"style-2","same_root_as":"ai-1"},{"id":"style-2","same_root_as":"logic-1"}]}
```' | bun run .agents/skills/deep-review/scripts/validate-output.ts consolidate

需要说明适用前提:consolidate-prompt.md 只在 Deep 模式下、且 confirmed 发现数 ≥ 2 时才会被实例化;Light 模式没有 verify 阶段,自然也不存在合并步骤。此外该技能支持扩展包机制(任意 deep-review-* 兄弟目录可作为扩展包注入自有维度文件,见 SKILL.md 的 Extension packs 一节),但合并步骤本身的语义契约不受扩展包影响——它消费的是所有维度最终汇总出的 confirmed 发现集合。

登录后查看全文
热门项目推荐
相关项目推荐