Skill-Agent 协作:消除 LLM 注意力稀释

1 阅读1小时+

核心结论:当一个 Skill 覆盖的审查维度过多时,High 级别 Findings 会抢占模型注意力,导致其他维度的 checklist 被跳过——即使 Skill 明确规定"不可跳过"。解决方案不是写更好的提示词,而是改变架构:将重度 Skill 拆分为垂直领域的轻量 Skill,每个由独立子代理在隔离的上下文中加载执行;主会话加载 go-review-lead Skill 仅负责分诊和汇总(平台限制决定编排不能在子代理中执行)。

想了解更多 Skill 的最佳实践,参考:github.com/johnqtcg/aw…

这个项目的核心目标不是”展示 prompt 怎么写”,而是回答三个更难的问题:

  • 高质量 Skill 到底应该如何设计?
  • 写出来之后如何证明它真的有效?
  • 如何把它融入日常开发与团队工作流?

欢迎大家提交 PR,如果觉得有用,请给 star。

目录

  1. 问题:注意力稀释
  2. 根因分析
  3. 解决方案:Skill-Agent 协作架构
  4. Skill 与 Agent 的关系
  5. 实施指南:拆分 go-code-reviewer
  6. Lead Agent 的分诊机制
  7. 常见问题
  8. 首次改造与验证——发现新的问题
  9. 第二轮改进:Grep-Gated 执行协议
  10. 最终验证通过——完整审查输出
  11. 追加验证:跨域案例 — ListLayout
  12. Token 效率优化与审查粒度分层(含 §12.6 验证)

1. 问题:注意力稀释

1.1 真实案例

以下 Go 代码交给 go-code-reviewer Skill 审查:

func getBatchUser(ctx context.Context, userKeys []*UserKey) ([]*User, error) {
    userList := make([]*User, 0)  

    var wg sync.WaitGroup
    for i, u := range userKeys {
        if u == nil { continue }
        wg.Add(1)
        go func() {
            defer wg.Done()
            user, err := redis.GetGuest(ctx, u.Id)
            if err != nil {
                log.WarnContextf(ctx, "no found guest user: %v", u)
                continue 
            }
            userList = append(userList, user)  
        }()
    }
    return userList, nil 
}

1.2 审查结果

go-code-reviewer 发现了 4 个 High 级并发缺陷

ID严重度问题
REV-001Highcontinue 在 goroutine 内,编译错误
REV-002High并发 append 无锁,数据竞争
REV-003High缺少 wg.Wait(),goroutine 被遗弃
REV-004High循环变量捕获(Go < 1.22)

但遗漏了一个 Medium 级性能问题

// 实际代码
userList := make([]*User, 0)

// 应该写成(已知上界 len(userKeys),应预分配容量)
userList := make([]*User, 0, len(userKeys))

这完全符合 go-code-reviewer Skill 中 Performance checklist 的 "Slice Pre-allocation" 检查项。Skill 自身也明确规定:

Execute ALL checklist categories regardless of how many High findings have already been identified

但模型仍然漏报了。

1.3 漏报的根因

人工指出遗漏后,模型立即识别并承认了问题:

"当 4 个 High 级别的并发缺陷占据了注意力后,我在走 Performance checklist 时不够仔细,错误地将这个问题归入了'可忽略的小问题'而没有正式报告。这违反了 Skill 的 Review Discipline 规则。"

关键发现:问题不在于模型"不会"——它知道 Slice Pre-allocation 规则,指出后立即承认。问题在于单次调用中同时处理 5 个审查维度,High 级别的 Findings 天然更"吸引"注意力,压缩了模型分配给其他维度的认知资源。


2. 根因分析

2.1 单 Agent + 重度 Skill 的结构性缺陷

2:架构 A 的注意力挤压效应(单 Agent + 重度 Skill)
符号说明:← 被挤压 = 注意力分配比例下降;[High] 块 = 占用主要注意力

[单个 Agent 的上下文窗口]
┌─────────────────────────────────────────┐
│ go-code-reviewer SKILL.md(全量知识)    │
│ ├── 安全规则(SQL 注入、XSS...)         │
│ ├── 并发规则(竞态、死锁、泄漏...)      │
│ ├── 性能规则(预分配、N+1...)           │  ← 被挤压
│ ├── 错误处理规则(wrapnil check...)   │  ← 被挤压
│ └── 质量规则(命名、结构...)            │  ← 被挤压
│                                         │
│ 待审查代码                               │
│                                         │
│ 已发现的 Findings:                       │
│ ├── [High] REV-001 编译错误 ←────────┐  │
│ ├── [High] REV-002 数据竞争 ←────────┤  │
│ ├── [High] REV-003 goroutine 泄漏 ←──┤  │ 注意力集中在这里
│ └── [High] REV-004 循环变量捕获 ←────┘  │
│                                         │
│ Performance checklist:                   │
│   Slice Pre-allocation → ??? (未检查)    │ ← 注意力不足
└─────────────────────────────────────────┘

这和人类审查代码时的情况完全一样:一个 Reviewer 看到 4 个严重的并发 bug 后,很难同时保持对 make 容量优化的敏感度。

Anthropic 的内部研究对这一现象提供了量化支撑:在多 Agent 研究系统评测中,token 使用量解释了 80% 的性能差异,核心原因是每个 Agent 在干净的上下文窗口中执行,token 使用效率更高——这正是上下文污染(Context Rot)会拉低单 Agent 性能的直接证据。

2.2 问题本质:不是提示词的问题,是架构的问题

尝试过的缓解措施及其局限:

缓解措施效果局限
在 Skill 中强调"不可跳过 checklist"部分有效规则本身也在同一上下文中竞争注意力
写入 Memory "High findings 不能跳过其余 checklist"下次有帮助无法根本解决单上下文多维度的注意力竞争
增加 checklist 的强制性语言有限改善LLM 的注意力机制是概率性的,无法仅靠指令稳定覆盖

结论:在当前案例及验证中,当单个上下文窗口内聚集多个 High Findings 时,其他维度的检查质量会明显下降。这更像是 LLM 注意力分配的结构性限制,而不是单纯的 Skill 设计失误。

在当前 Go 代码审查场景下,更有希望的改进方向是改变架构:让每个审查维度在独立的上下文中执行,使 Findings 之间的注意力竞争显著降低。

2.3 什么是 Multi-Agent,为什么它优于 Single-Agent

确定方向之前,先回答两个基础问题:Multi-Agent 架构是什么,以及为什么需要它而不是继续优化单 Agent。

Multi-Agent 的定义

Multi-Agent 架构是指多个 AI Agent 在明确的角色分工和协作协议下,协同完成一个复杂任务。每个 Agent 拥有独立的上下文窗口、专用的工具集和清晰的职责边界。

这与软件工程中的微服务演进高度类似:

软件演进AI Agent 演进
单体应用代码库太大,难以维护单体 Agent 上下文窗口积累太多,性能下降
单点故障影响全局单个维度失误影响整条审查链路
无法独立扩展模块无法为不同任务选择最优模型
职责边界模糊Agent 角色混乱导致输出质量下降

就像单体应用最终需要被拆分为微服务一样,单体 Agent 在任务足够复杂时,也需要被拆分为多个专职 Agent。

来自 Anthropic 的实证数据

Anthropic 多 Agent 研究系统实测 (来源:How we built our multi-agent research system,Anthropic Engineering Blog,2025-06-13):

  • Claude Opus 4(Lead)+ Claude Sonnet 4(Workers)的多 Agent 系统,在内部研究评测中比单 Agent Opus 4 性能高出 90.2%
  • Token 使用量解释了 80% 的性能差异——关键不在于模型更强,而在于每个 Agent 在干净的上下文中专注完成单一任务
  • 工具调用次数和模型选择是另外两个关键因素

AgentCoder 学术研究(来源:arxiv.org/pdf/2312.13…

  • 多 Agent 代码生成(Programmer + Test Designer + Test Executor)在 HumanEval 上达到 96.3% pass@1,而单 Agent SOTA 为 90.2%
  • 使用更少的 token(56.9K vs 138.2K)达到更高的准确率,证明专业化分工可以同时提升质量和效率

这组数据揭示了一个反直觉的结论:Multi-Agent 的优势不来自"用了更多算力",而来自让每个 Agent 在干净的上下文中做更专注的事

四大优势机制:与本场景的对应关系

优势机制对 Go 代码审查的意义
专注的上下文窗口每个 sub-Agent 在全新、干净的上下文中运行,不受其他维度 Findings 污染并发审查发现 4 个 High 不会影响性能审查对 make([]*User, 0) 的敏感度
深度专业化每个 Agent 的系统提示聚焦单一领域,工具集精简Security Agent 只看注入/加密等安全方面的缺陷;Performance Agent 只看 N+1/预分配容量等性能问题——无需兼顾其他
多视角质量保障多个 Agent 独立评估,彼此不知道其他 Agent 的 Findings并发、错误、性能审查各自独立得出结论,交叉验证相互强化
灵活的模型配置Lead 用强模型做分诊和汇总,Workers 用快速模型执行审查Lead Agent 负责分诊 + 去重,Workers 使用 Haiku/Sonnet 控制成本

第一个优势直接对应本文的核心问题:当单个上下文窗口同时持有多个高严重度 Findings 时,模型对低优先级检查项的覆盖率会系统性下降。这是提示词难以修复的结构性缺陷——Multi-Agent 通过让每个维度在独立上下文中运行,显著降低了跨维度的注意力竞争。


2.4 编排模式选型:为什么是 Orchestrator-Workers

明确了 Multi-Agent 的优势之后,下一个问题是:选哪种编排模式?

Anthropic 定义了五种基础编排模式。逐一评估它们对 Go 代码审查场景的适用性:

模式核心机制对本场景的评估适用?
模式一:Prompt Chaining线性步骤序列,上一步输出是下一步输入安全/并发/性能各维度之间没有依赖关系,不是顺序问题
模式二:Routing分类输入,路由到某一个专门处理器一次审查需要同时覆盖多个维度,而不是选择其中一个
模式三:Parallelization多路并行,子任务预先固定接近需求,但子任务固定,每次必须跑所有分支,无法按内容裁剪
模式四:Orchestrator-Workers中央编排者动态分解任务,按需调度 Workers在当前场景下最匹配——审查维度随代码内容动态决定
模式五:Evaluator-Optimizer生成→评估→改进的迭代循环代码审查是诊断任务,不是迭代生成任务

模式三与模式四的关键区别是本次选型最需要厘清的点。两者都支持并行,差异在于子任务的来源:

Parallelization(模式三):
  代码 → [固定派发: Security + Performance + Quality + Logic + ...] → 汇总
  子任务在设计时确定,每次审查都跑完整的 N 路

Orchestrator-Workers(模式四):
  代码 → [Lead Agent 分析 diff] → 动态决策 → 按需派发 K 路(K ≤ N)→ 汇总
  子任务在运行时确定,根据代码内容决定需要哪些维度

对 Go 代码审查而言,需要派发哪些 Agent 取决于被审查代码的内容

  • 代码只改了变量名 → 只需 Quality + Logic(2 个 Agent)
  • 代码引入了 go func + sync.WaitGroup → 还需 Concurrency + Error(4 个)
  • 代码包含 make([]*T, 0) + 批量函数名 → 还需 Performance(5 个)
  • 代码有 _test.go 变更 → 还需 Test(6 个)

这种"内容驱动的维度选择"无法在设计时预知,必须由编排者在运行时根据输入动态决策——这正是 Anthropic 对 Orchestrator-Workers 适用场景的定义:"无法提前预测需要哪些子任务,需要 Orchestrator 根据输入动态决策"

如果强行用 Parallelization,代价是每次审查都启动所有 7 个子代理,一次 5 行变量名修改也要承担全量 Agent 的 token 消耗和延迟,性价比严重失衡。分诊本身就是 Orchestrator 最核心的价值。


3. 解决方案:Skill-Agent 协作架构

3.1 架构总览

图 1:Skill-Agent 协作架构全景(主会话 go-review-lead Skill + 7 个垂直子代理)
符号说明:─┬─ 并行分叉  ↓ 顺序流向  [  ] 节点  └ 合并汇聚

                          PR Diff / 代码片段
                                 │
                                 ↓
                [主会话 + go-review-lead Skill]
                   职责:分诊 + 派发 + 汇总
                   加载 go-review-lead Skill
                   不加载任何垂直审查 Skill
                   不直接审查代码
                                 │
                         Phase 1-4: 分诊
                         grep + 模式匹配,判断涉及哪些维度
                                 │
      ┌────────┬────────┬────────┼────────┬────────┬────────┐
      ↓        ↓        ↓        ↓        ↓        ↓        ↓
  [Security] [Concurr] [Perf]  [Error] [Quality] [Test]  [Logic]
   Agent      Agent    Agent   Agent    Agent    Agent    Agent
     │          │        │       │        │        │        │
  启动时经   启动时经  启动时经  启动时经  启动时经  启动时经  启动时经
  skills:字   skills:  skills:  skills:  skills:  skills:  skills:
  段注入安全  字段注入 字段注入 字段注入 字段注入 字段注入 字段注入
   Skill     并发Skill 性能Skill 错误Skill 质量Skill 测试Skill 逻辑Skill
     │          │        │       │        │        │        │
  独立审查  独立审查  独立审查  独立审查  独立审查  独立审查  独立审查
     │          │        │       │        │        │        │
     └──────────┴────────┴───────┴────────┴────────┴────────┘
                                 │
                                 ↓
                主会话 go-review-lead Skill 汇总
                合并 Findings + 去重 + 按严重度排序
                                 │
                                 ↓
                             最终报告

3.2 核心设计原则

原则一:每个子代理只加载一个维度的 Skill

Performance Agent 的上下文中主要只有性能相关知识和待审查代码,没有其他维度的规则,也不接收其他 Agent 的 Findings。这会显著提高它把注意力放在 Performance checklist 上的概率。

原则二:主会话(编排角色)不审查代码

主会话加载 go-review-lead Skill 后扮演编排角色,只做分诊和汇总,不加载任何垂直审查 Skill,不直接分析代码逻辑。若主会话自己也做审查,它的发现会影响对其他 Agent 结果的汇总判断——和重度 Skill 是同一个问题。

原则三:垂直子代理通过 skills: 字段在启动时自动加载领域知识

Agent 定义文件是轻量的(几十行提示词),审查知识保存在独立的 Skill 文件中。每个垂直 Agent 通过 frontmatter 中的 skills: 字段声明所依赖的 Skill——平台在 Agent 启动时自动注入完整 Skill 内容,无需运行时手动调用 Skill() 工具。不存在内容重复,Skill 文件可被多个 Agent 复用。

原则四(平台约束 + 工程决策):编排逻辑必须以 Skill 形式封装,在主会话执行

结论先行,推理如下:

平台约束:subagent 不能生成 subagent
  → 编排必须在主会话执行
  → 选项 A:每次用临时 prompt 编排?
      → 否:流程太复杂 + 不可复用
  → 选项 B:封装为 Skill,主会话调用
      → 是:复杂度可控 + 标准化 + 可复用
      → Skill 是唯一合适的载体

第一层:为什么编排不能由 subagent 执行? Claude Code 明确规定子代理不能生成子代理("Subagents cannot spawn other subagents")。如果把 go-review-lead 配置为 .claude/agents/ 中的 agent 定义文件,它被调用时会作为子代理运行,此时它对 Agent 工具发出的并行调用请求将被平台静默忽略——7 个垂直 Agent 退化为串行调用或根本不被派发。

第二层:为什么不直接在主会话用 prompt 编排,而必须封装成 Skill?

  1. 复杂度不可接受:编排流程是流水线+并行的混合模式——作用域识别→审查深度选择→编译预检→四阶段分诊→动态派发→去重汇总。写成临时 prompt 篇幅远超可接受范围。
  2. 无法复用:每次代码审查都要重新提供这段复杂 prompt。标准化流程应当封装,不是每次重写。

这一原则使架构在两个层次上自洽:宏观层——主会话调用编排 Skill → Skill 派发 7 个子代理并行 → Skill 去重汇总;微观层——每个子代理加载自己专属的垂直审查 Skill。编排用的是 Skill,执行也用的是 Skill,而连接二者的是 Agent。

3.3 回到案例:为什么在当前案例中更不容易漏报

角色上下文内容输出
Concurrency Agent只有并发规则 + 代码4 个 High(竞态、泄漏、Wait、循环变量)
Performance Agent只有性能规则 + 代码make([]*User, 0) 缺少预分配 ← 在当前案例中被发现的概率显著提高
Error Agent只有错误处理规则 + 代码错误被吞、continue 编译错误
Logic Agent只有逻辑规则 + 代码返回合同违反、边界条件
Quality Agent只有代码质量规则 + 代码具名返回遮蔽、未使用变量
主会话(go-review-lead Skill)只看 5 份结构化报告合并去重,输出在当前案例中更完整的报告

Performance Agent 不需要感知那 4 个 High 并发 bug。它的上下文里只有 Performance checklist 和待审查的代码。对 Slice Pre-allocation 这样的性能模式,这种隔离能明显降低被其他维度抢占注意力的风险,但仍不应表述为绝对不会漏报。


4. Skill 与 Agent 的关系

4.1 不是替代,是互补

维度SkillAgent
本质知识包——工作流、checklist、参考资料执行单元——独立上下文中的 LLM 实例
回答的问题"做什么"和"怎么做""谁来做"和"在哪做"
载体SKILL.md + references/ + scripts/Agent 定义(角色 + 工具 + 要加载的 Skill)
核心价值沉淀领域专业知识,让 AI 超越通用能力提供执行隔离,让每个任务在干净上下文中完成

4.2 三种架构对比

架构 A:只有 Skill,无 Agent 隔离(go-code-reviewer 现状)

主会话 + 全量 Skill 知识(单一上下文)
→ 知识完备,但注意力被稀释
→ 已证实漏报

架构 B:只有 Agent 隔离,无 Skill 知识

主会话 go-review-lead Skill 编排 + 7 个垂直子代理,仅靠提示词(无 Skill 注入)
→ 上下文干净,不会被其他维度干扰
→ 但缺少项目特定的审查规则
→ 只能依赖 AI 通用知识,可能漏掉领域规则

架构 C:Skill-Agent 协作(推荐)

主会话加载 go-review-lead Skill 负责分诊与汇总
+ 7 个垂直子代理各自通过 skills: 字段加载领域 Skill
→ 上下文干净(Agent 隔离)
→ 知识精准(Skill 赋能)
→ 在当前案例中覆盖更完整,遗漏风险更低

4.3 加载机制

两种角色使用两种不同的 Skill 加载方式:

主会话(编排角色):通过 Skill() 工具显式调用

用户发起代码审查请求
    │
    │ 主会话调用 Skill("go-review-lead")
    │                    ↓
    │         go-review-lead SKILL.md 内容注入主会话上下文
    │         (分诊规则、派发表、汇总逻辑)
    │
    │ 主会话按 Skill 指令执行分诊,并行派发子代理
    ↓
    汇总各子代理报告 → 输出最终审查结果

垂直子代理(审查角色):通过 skills: frontmatter 字段在启动时自动注入

Agent 不是把 Skill 内容复制到自己的定义文件中,也不需要运行时手动调用 Skill() 工具。Agent 通过 frontmatter 中的 skills: 字段声明依赖,平台在 Agent 启动时自动将完整 Skill 内容注入到 Agent 的上下文中

Performance Agent(独立上下文窗口)启动
    │
    │ 平台读取 Agent frontmatter 中的 skills: [go-performance-review]
    │                    ↓
    │         平台自动将 go-performance-review SKILL.md 内容注入
    │         到该 Agent 的上下文(无需 Agent 主动调用工具)
    │
    │ 此时 Agent 上下文中有:
    │   ✓ Performance checklist(来自 Skill,平台注入)
    │   ✓ 待审查代码(来自 dispatch prompt)
    │   ✗ 并发规则(未声明,不注入)
    │   ✗ 其他 Agent 的 Findings(隔离,不共享)
    │
    ↓
    执行审查 → 返回结构化结果给主会话

这意味着:

  • 垂直 Agent 定义文件始终是轻量的(角色描述 + 工具列表 + skills: 字段声明)
  • Skill 文件保持独立,可被多个 Agent 复用
  • 不存在内容重复

5. 实施指南:拆分 go-code-reviewer

5.1 Skill 拆分

从原有的 go-code-reviewer 中拆出 7 个垂直 Skill:

skills/
├── go-security-review/
│   └── SKILL.md              # SQL 注入、XSS、密钥泄露、权限
│
├── go-concurrency-review/
│   └── SKILL.md              # 竞态、goroutine 泄漏、死锁、WaitGroup
│   └── references/
│       └── go-concurrency-patterns.md
│
├── go-performance-review/
│   └── SKILL.md              # 预分配、N+1、索引、内存
│   └── references/
│       └── go-performance-patterns.md
│
├── go-error-review/
│   └── SKILL.md              # 错误包装、资源关闭、panic 处理
│
├── go-quality-review/
│   └── SKILL.md              # 命名、结构、lint 规则、注释规范
│
├── go-test-review/
│   └── SKILL.md              # 测试覆盖率、断言质量、测试隔离
│
└── go-logic-review/
    └── SKILL.md              # 业务逻辑、边界条件、nil、错误传播
    └── references/
        └── go-error-and-quality.md

拆分原则:每个 Skill 的 checklist 项不超过 15 条。如果超过,说明维度还可以继续拆分。

5.2 Agent 定义与 Skill 组织

重要go-review-lead 不是 .claude/agents/ 中的 Agent 定义,而是一个 Skill(见原则四)。平台限制子代理不能派发子代理,因此编排逻辑必须在主会话中以 Skill 形式执行。

.claude/agents/                            # 7 个垂直子代理定义文件
├── go-security-reviewer.md       # skills: [go-security-review]
├── go-concurrency-reviewer.md    # skills: [go-concurrency-review]
├── go-performance-reviewer.md    # skills: [go-performance-review]
├── go-error-reviewer.md          # skills: [go-error-review]
├── go-quality-reviewer.md        # skills: [go-quality-review]
├── go-test-reviewer.md           # skills: [go-test-review]
└── go-logic-reviewer.md          # skills: [go-logic-review]

skills/go-review-lead/                     # 主会话编排 Skill(不是 Agent)
└── SKILL.md                               # 分诊规则 + 派发表 + 汇总合同

5.3 Agent 定义示例

注意:以下示例反映的是当前实际落地的架构(含 Grep-Gated Execution Protocol)。§9 详细说明了该协议的设计背景和完整实施内容。

go-review-lead Skill(主会话编排,不是 Agent 定义)

go-review-lead 是一个 Skill,保存在 skills/go-review-lead/SKILL.md,由主会话通过 Skill("go-review-lead") 工具调用加载。它不存在于 .claude/agents/ 目录中——平台限制 subagent 不能派发 subagent,编排必须在主会话执行(见原则四)。

# skills/go-review-lead/SKILL.md

---
name: go-review-lead
description: Go 代码审查编排者,对代码变更做分诊,将安全、并发、错误、性能、质量、测试、逻辑七个维度的审查 Agent 并行派发,并将结果汇总为统一报告。适用于完整 Go PR 审查、全面代码审查,或用户说"审查 Go 代码"但未指定具体维度的场景。以聚焦的并行分析取代单体审查器,系统性降低注意力稀释。
---

你是 Go Review Lead——一个在主会话中运行的编排者。你自己不审查代码,所有代码审查由专业 Agent 完成。

## Startup

1. 本 Skill 由主会话通过 `Skill("go-review-lead")` 工具调用加载
2. 加载后立即进入分诊阶段,按以下指令执行

## 核心规则

1. **自己永远不审查代码**——你是中立的裁判,只做分诊和汇总
2. **并行派发 Agent**——使用 Agent 工具同时启动多个审查 Agent
3. **每个 Agent 在隔离上下文中运行**——绝不将一个 Agent 的 Findings 分享给另一个
4. **高严重度 Findings 永远不丢失**——数量上限只影响 Medium/Low

## 工作流

### 1. 分析范围
- 运行 `git diff -- '*.go'` 识别变更的 Go 文件
- 区分生产代码与 `_test.go` 文件
- 识别影响半径(接口变更 → 搜索实现者)

### 2. 分诊——选择 Agent(4 阶段)

始终派发:**go-quality-reviewer****go-logic-reviewer**。

其余 5 个,运行 4 阶段分诊,每阶段独立贡献派发决策:

**Phase 1 — Import 扫描**(grep 所有变更文件的 import 块):
- `"database/sql"` → security + error + performance
- `"os/exec"` → security
- `"net/http"` → security(若为 http.Client 还加 performance)
- `"sync"`, errgroup, singleflight → concurrency
- `"crypto/md5"`, `"math/rand"` → security

**Phase 2 — Diff Pattern 扫描**(仅扫描新增/修改行):
- `go func`, `make(chan`, `sync.Mutex/RWMutex/WaitGroup` → concurrency
- `sql.Rows`, `tx.Begin`, `resp.Body`, `defer.*Close`, `panic(` → error
- 零容量/省略容量 `make([]` + 已知上界,循环内 DB/cache 调用,`strings.Builder` → performance
- 显式容量 `make([]` + 后续 resize/copy 模式 → performance
- 硬编码密钥、`filepath.Join` + 用户输入、`tls.Config{` → security

**Phase 3 — 文件路径 heuristic**- `auth/`, `middleware/`, `handler/`, `router/` → security
- `worker/`, `pool/`, `queue/` → concurrency
- `repo/`, `store/`, `db/` → error + performance
- `cache/`, `redis/` → performance
- 函数名含 `Batch`, `Multi`, `Bulk`, `GetAll`, `FetchAll`, `ListAll`(批量语义命名)→ performance

**Phase 4 — 变更范围评估**- diff 中有 `_test.go` → go-test-reviewer
- 新增 `.go` 文件 → 确保 error 已派发
- `go.mod` 变更 → 对新依赖重跑 Phase 1

**无兜底派发**:若某 Agent 未被任何阶段触发,则跳过并明确记录原因。

### 3. 派发

通过 Agent 工具并行启动选中的 Agent。每个 Agent 的 prompt 必须包含:
- 待审查的文件列表
- 指令:"你的领域 Skill 已通过 skills: 字段自动注入,直接使用其中的 checklist 审查这些文件"
- diff 上下文或直接读文件的指令

### 4. 汇总

所有 Agent 返回后:
1. **去重合并**——同位置不同问题保留两者;同问题不同维度保留更具体的;**同问题不同严重度取最高**,合并证据
2. **按严重度降序排列**——High → Medium → Low;同严重度内:`introduced` 优先于 `pre-existing`,再按类别字母序
3. **数量上限**——ALL High Findings(永不截断),然后填至总数约 15 条(Medium),最后 Low
4. **标注来源**——`introduced`(新增/修改代码)vs `pre-existing`(未变更代码)
5. **统一编号**——按严重度降序编 REV-001、REV-002……,REV-001 始终是最高严重度

### 5. 报告

按 go-review-lead skill 指定的格式输出统一报告。

## Agent 派发模板

对每个专职 Agent,使用以下 prompt 模式:

审查以下 Go 代码变更中的 {domain} 问题。

待审查文件: {文件列表}

指令:

go-{domain}-review Skill 已通过 skills: 字段自动注入到你的上下文,直接使用其中的 checklist 和门禁
在任何语义分析之前,对所有 grep-gated checklist 项执行 grep pre-scan
严格按照 Skill 的 checklist 和门禁执行
返回完整 Findings 报告,使用 {PREFIX}-NNN 编号 Execution Status 中包含:Grep pre-scan: X/Y items hit, Z confirmed as findings
上下文:本次审查是 go-review-lead Skill 协调的并行多子代理审查的一部分。

Performance Agent(审查者,含 Grep-Gated 协议)

# .claude/agents/go-performance-reviewer.md

---
name: go-performance-reviewer
description: Go 性能审查专家,覆盖切片/Map 预分配、循环内字符串拼接、N+1 数据库查询、连接池调优、sync.Pool 使用、内存对齐、锁作用域优化、缓冲 I/O 和 HTTP Transport 配置。当 Go 代码变更包含 make()、append()、循环内 DB/Redis 调用、strings.Builder、sync.Pool、http.Client 或热路径操作时触发。可由 go-review-lead 派发,也可直接调用进行性能专项审查。
tools: ["Read", "Grep", "Glob", "Bash"]
model: haiku
skills:
  - go-performance-review
---

你是专职 Go 代码性能审查专家。你唯一的工作是发现 Go 代码中的性能反模式和优化机会。

> `go-performance-review` Skill 已由平台通过 `skills:` 字段在启动时自动注入到你的上下文中,无需手动调用 Skill 工具。直接使用其中的 checklist、门禁和反例执行审查。

## Startup

1. go-performance-review Skill 内容已自动加载(平台通过 skills: 字段注入)
2. 严格按照 Skill 的指令执行——它定义了 checklist、输出格式和抑制规则
3. 只审查 dispatch prompt 中指定的文件/diff

## Execution Order

调用 skill 后:
1. 确认目标文件(来自 dispatch prompt,或将裸代码片段写入 `$TMPDIR/review_snippet.go`2. 对所有 grep-gated checklist 项执行 grep pre-scan(模式列于 skill 中)
3. **HIT** → 语义分析确认(true positive vs false positive)
4. **MISS** → 自动标记 NOT FOUND,跳过语义分析
5. 纯语义项:需要完整的模型推理
6. 只上报 FOUND 项——NOT FOUND 项不出现在输出中
7. Execution Status 包含:`Grep pre-scan: X/Y items hit, Z confirmed as findings`

## 职责范围

**只审查**以下性能问题:
- 切片/Map 预分配(缺少容量提示)
- 循环内字符串拼接(应使用 strings.Builder)
- N+1 数据库查询
- 连接池配置(sql.DB、Redis、HTTP)
- 高分配热路径的 sync.Pool 使用
- 内存对齐和结构体填充
- 锁作用域优化(持锁跨越 I/O)
- 缓冲 I/O(文件/网络操作使用 bufio)
- HTTP Transport 调优(超时、keep-alive、连接数限制)

**不审查**- 安全漏洞 → go-security-reviewer 负责
- 并发正确性 → go-concurrency-reviewer 负责
- 错误处理 → go-error-reviewer 负责
- 代码风格 → go-quality-reviewer 负责
- 测试质量 → go-test-reviewer 负责
- 业务逻辑 → go-logic-reviewer 负责

## 输出

按 go-performance-review skill 指定的格式返回 Findings,使用 PERF- 前缀编号。若无性能问题,明确说明"未发现性能问题"——不捏造 Findings。

6. Lead Agent 的分诊机制

6.1 为什么需要分诊

如果每次审查都无差别地启动全部 7 个子代理,会产生不必要的开销:

PR 类型全量启动按需启动
只改了变量名7 个子代理(浪费 5 个)2 个(Quality + Logic,始终派发)
涉及并发+性能7 个子代理(浪费 2 个)5 个(Concurrency + Performance + Error + Quality + Logic)
全方位重构7 个子代理(合理)7 个

6.2 两级分诊

Level 1:文件类型分诊(无需 LLM,用 grep 即可)

# 有 .go 文件变更?→ 可能需要代码审查
# 有 _test.go 变更?→ 需要 Test Agent(go-test-reviewer)
# 有 .sql / migration 文件?→ 需要 Security Agent
# 有 go.mod 变更?→ 需要 Security Agent(依赖审查)

Level 2:内容分诊(Haiku 快速扫描 diff)

diff 包含 go func / channel / syncConcurrency Agent
diff 包含 db.Query / fmt.Sprintf    → Performance + Security Agent
diff 包含 make([]T, 0)              → Performance Agent
diff 包含 if err != nil              → Error Agent

Level 1 不消耗 token。Level 2 用 Haiku 模型(1/MTok),成本约1/MTok),成本约 0.001/次,可忽略不计。

6.3 分诊成本对比

方案简单风格 PR复杂并发 PR全方位重构
全量 7 子代理~$0.14~$0.14~$0.14
分诊 + 按需~$0.02~$0.07~$0.10
原始单 Skill~$0.03~$0.03(但漏报)~$0.03(但漏报)

按需调度在简单 PR 上节省约 80% 成本,在复杂 PR 上成本与全量启动持平,但审查质量显著优于单 Skill 方案。

:以上成本为近似估算,基于 Claude Haiku 4.5 / Sonnet 4.6 的 2026-03 官方定价,随定价调整会有变化。实际成本还受代码量(token 数)影响,仅作数量级参考。


7. 常见问题

Q1:拆分后 Skill 之间有交叉怎么办?

例如"未绑定的 goroutine 创建"既是并发问题也是性能问题。

处理方式:允许两个 Agent 从各自角度同时报告同一问题。Lead Agent 在汇总时去重合并,取较高严重度。交叉报告比遗漏好——去重的成本远低于漏报的代价。

Q2:是不是所有重度 Skill 都应该拆?

不是。拆分的判断标准:

信号是否应该拆分
Skill 的 checklist 覆盖 3 个以上独立维度是——维度间会互相抢注意力
单次审查经常产生 5+ 个 High Findings是——High Findings 越多,其他维度被挤压越严重
审查后用户经常指出"这个应该发现但没发现"是——典型的注意力稀释症状
Skill 只覆盖 1 个维度,且 checklist < 15 项否——上下文负担不大,单 Agent 足够

Q3:Lead Agent 要不要用 Opus 模型?

不需要。 Lead Agent 的工作是分诊(模式匹配)和汇总(合并排序),不需要深度推理。Haiku 或 Sonnet 足够。用 Opus 做分诊是过度配置。

专业审查的 Agent 用 Sonnet(需要理解代码逻辑和领域规则),特别复杂的架构级审查可以用 Opus。

Q4:如何验证拆分后的效果?

用漏报案例做回归测试。以 getBatchUser 为例:

  1. 将同一段代码分别交给拆分后的 Multi-Agent 架构审查
  2. 检查 Performance Agent 是否报告了 Slice Pre-allocation 问题
  3. 检查所有 Agent 的 Findings 合集是否覆盖了原始单 Skill 发现的所有问题 + 原始漏报的问题

通过标准:拆分后的总 Findings ⊇ 原始 Findings + 已知漏报项。

Q5:某个子代理超时或返回空结果时,go-review-lead Skill 怎么处理?

Multi-Agent 架构引入了额外的故障点——单 Skill 只会整体失败,而 7 个子代理并行时,任何一个都可能出现超时、Skill 注入失败或返回格式错误的情况。

降级策略按优先级排列

故障类型Lead Agent 处理方式
sub-agent 超时(>120s)标记该维度为 SKIPPED (timeout),继续汇总其他维度结果
sub-agent 返回空 Findings正常,视为该维度无发现,在 Execution Status 中记录 0 findings
sub-agent 返回格式错误标记为 PARSE_ERROR,在 Residual Risk 中注明"X 维度未完成,建议单独重跑"
Skill 文件缺失(加载失败)sub-agent 自行报告错误,Lead 在最终报告的 Execution Status 中注明

核心原则部分成功优于全量失败。Lead Agent 应始终输出当前已有的发现,而不是因为一个 Agent 失败就放弃整份报告。

如果某个关键维度(如 Concurrency)的 Agent 失败,建议在报告尾部加注:

[Residual Risk] go-concurrency-reviewer did not complete (timeout).
Concurrency dimension not covered in this review — recommend re-running
with: "使用 go-concurrency-reviewer agent 审查 <file>"

以上是理论方案的完整设计。§8-10 记录了随后的实施与验证过程——第一次验证并不顺利,改进经历了两轮迭代才最终通过。


8. 首次改造与验证——发现新的问题

8.1 初步改造:从单 Skill 到 go-review-lead Skill + 7 个子代理

理论方案确定后,着手实施。按照第 5 节的指南,将 go-code-reviewer 这个重度 Skill 拆分为 7 个垂直领域 Skill,并为每个 Skill 创建对应的 Agent 定义文件;同时将编排逻辑封装为独立的 go-review-lead Skill 供主会话调用。形成 1 个主会话编排 Skill + 7 个垂直子代理 的协作体系:

~/.claude/agents/                          # 7 个垂直子代理定义文件
├── go-error-reviewer.md           # skills: [go-error-review]
├── go-concurrency-reviewer.md     # skills: [go-concurrency-review]
├── go-security-reviewer.md        # skills: [go-security-review]
├── go-performance-reviewer.md     # skills: [go-performance-review]
├── go-quality-reviewer.md         # skills: [go-quality-review]
├── go-test-reviewer.md            # skills: [go-test-review]
└── go-logic-reviewer.md           # skills: [go-logic-review]

~/.claude/skills/
├── go-review-lead/SKILL.md        # 主会话编排 Skill(不是 Agent)
│                                  # 分诊规则 + 汇总合同 + 派发模板
├── go-error-review/SKILL.md
├── go-concurrency-review/SKILL.md
├── go-security-review/SKILL.md
├── go-performance-review/SKILL.md
├── go-quality-review/SKILL.md
├── go-test-review/SKILL.md
└── go-logic-review/SKILL.md

架构改造完成后,用同一段 getBatchUser 代码进行验证。

8.2 第一次验证:仍有遗漏

结果令人失望:多次测试发现,Slice Pre-allocation 问题仍然会被漏报,有时 Unbounded Goroutine Spawning(无界 goroutine 创建)也会漏报。

逐步排查发现,根因在两个层面:

层面一:分诊逻辑有盲区

go-review-lead 的 Phase 2(Diff Pattern Scan)中,performance 的触发条件是:

make([] with capacity, loops + DB/cache calls, strings.Builder → performance

这条规则只对"已有 capacity 参数的 make"触发,而 make([]*User, 0) 恰好是没有 capacity 的情况——规则反向匹配失败,go-performance-reviewer 直接被跳过,漏报无从避免。

Phase 3 的文件路径规则(cache/, redis/ → performance)本可兜底,但代码以裸片段(raw snippet)方式提交,没有文件路径,Phase 3 同样失效。

层面二:即使分诊正确,模型仍可能漏检

Unbounded Goroutine Spawning 的漏报揭示了更深的问题:即便 go-concurrency-reviewer 被正确分诊并派发,当上下文中存在多个 High 级别的编译错误和数据竞争时,"无界 goroutine 创建"这类 Medium 级别的问题仍会被模型降低优先级,最终只出现在 Residual Risk 中,而不是作为正式 Finding 上报。

架构已经将不同审查维度隔离到独立的上下文,但在同一个 Agent 的上下文内,多个 High 级别 Findings 仍然会压制 Medium 级别的项。注意力稀释问题在垂直维度内部依然存在。

8.2.1 第一次验证的原始输出

以下是第一次验证时 go-review-lead 的完整汇总报告。关键证据在 Skipped 行和 Residual Risk 块:go-performance-reviewer 被跳过(分诊盲区),Unbounded Goroutine Spawning 只出现在 Residual Risk 中,未被正式上报(层面二问题的直接体现)。

Review Mode

- Dispatched skills: go-concurrency-reviewer, go-error-reviewer, go-quality-reviewer, go-logic-reviewer
- Skipped skills: go-security-reviewer (no SQL/HTTP/crypto patterns),
                  go-performance-reviewer (no hot-path loops or DB patterns),   分诊盲区导致跳过
                  go-test-reviewer (no test code)
- Triage rationale: Raw snippet fallback applied. go func + sync.WaitGroup triggered concurrency;
  new code triggered error reviewer; quality and logic are always dispatched.

---
Findings

[High] Compile Error: `continue` Inside Goroutine Body
- ID: REV-001 (original: CONC-004 / ERR-003 / LOGIC-001  merged)
- Location: goroutine closure, error branch: `continue`
- Impact: The binary cannot be produced. `continue` is only valid in a for/switch/select in
  the enclosing scope; inside a goroutine function literal, no loop is in scope.
- Recommendation: Replace `continue` with `return`.
- Action: must-fix

[High] Compile Error: Mixed Named/Unnamed Return Values
- ID: REV-002 (original: ERR-008 / LOGIC-005 / QUAL-005  merged)
- Location: func getBatchUser(...) (users []*User, error)
- Impact: `users` is named; `error` is an unnamed type  Go forbids mixing. Compile blocker.
- Recommendation: ([]*User, error)  all unnamed.
- Action: must-fix

[High] Compile Error: Unused Loop Variable `i`
- ID: REV-003 (original: QUAL-004 / LOGIC-007  merged)
- Location: for i, u := range userKeys  `i` never referenced
- Impact: Third compile-time blocker. Go does not allow declared-but-unused variables.
- Recommendation: Replace `i` with `_`.
- Action: must-fix

[High] Data Race: Concurrent Unsynchronized Writes to `userList`
- ID: REV-004 (original: CONC-001 / ERR-001 / QUAL-001 / LOGIC-003  merged)
- Location: goroutine closure: userList = append(userList, user)
- Impact: Multiple goroutines write to the same slice header without synchronization. Silent
  data loss, corruption, or panic. go test -race flags this unconditionally.
- Action: must-fix

[High] Missing `wg.Wait()`: Function Always Returns Empty Slice, Goroutines Leak
- ID: REV-005 (original: CONC-003 / ERR-005 / QUAL-002 / LOGIC-002  merged)
- Location: end of function body, before return userList, nil
- Impact: Goroutines in flight when function returns. Caller always receives empty slice.
- Action: must-fix

[High] All Goroutine Errors Silently Discarded: Caller Always Sees `nil` Error
- ID: REV-006 (original: ERR-004 / QUAL-003 / LOGIC-006  merged)
- Location: goroutine error branch; return userList, nil
- Impact: Redis failures are logged as warnings then dropped. Caller cannot distinguish
  "all found", "partial success", or "Redis unavailable"  silent data loss.
- Action: must-fix

[High] Loop Variable Capture: `u` Captured By Reference (Go < 1.22)
- ID: REV-007 (original: CONC-002 / ERR-002 / LOGIC-004  merged)
- Location: go func() referencing u from for _, u := range userKeys
- Impact: On Go < 1.22, all goroutines share the same u address  may fetch wrong/last key.
- Recommendation: Shadow before goroutine: u := u
- Action: must-fix (unless go.mod confirms 1.22+)

[Medium] Goroutines Missing Panic Recovery
- ID: REV-008 (original: ERR-006)
- Action: follow-up

[Medium] Context Cancellation Not Checked Before Goroutine Work Begins
- ID: REV-009 (original: CONC-005 / QUAL-006  merged)
- Action: follow-up

[Low] Log Message Format Violates Go Convention
- ID: REV-010 (original: ERR-007)
- Action: follow-up

---
Residual Risk / Testing Gaps

1. Verification gaps: Code does not compile  no static analysis or race detector could run.
2. Unbounded goroutine spawning: If userKeys is large (thousands), this spawns an equal number
   of goroutines simultaneously. Not flagged as a finding since expected batch size is unknown,
   but consider errgroup.SetLimit(n) or a worker pool.     未正式上报,藏在 Residual Risk 
3. Pre-existing issues: None (raw snippet, no broader codebase context).

---
Summary: 7 High / 2 Medium / 1 Low.
3 compile errors prevent the binary from being built.

对照 §10 的最终验证结果(7 High / 6 Medium),两轮之间的差距一目了然:

第一轮(§8)最终轮(§10)
派发 Agent 数4(跳过 performance)5(含 performance)
Slice Pre-allocation未发现(跳过 performance)PERF-001 [Medium]
Unbounded Goroutine仅在 Residual Risk 中提及REV-007 [High] 正式上报
Medium Findings 总数26

8.3 第一轮针对性改进及其局限

针对分诊盲区,将 go-review-lead 的 Phase 2 触发条件调整为:

- make([] (with or without capacity), loops + DB/cache calls, strings.Builder → performance

同时在 Phase 3 补充了批量语义函数名 heuristic:

- 函数名包含 Batch、Multi、Bulk、GetAll、FetchAll、ListAll → performance

调整后,getBatchUser 函数名直接命中 Phase 3 规则,go-performance-reviewer 可以被正确分诊。

但问题依然没有根本解决:即使 go-performance-reviewer 被正确派发,make([]*User, 0) 在 12 个 performance checklist 项中只是其中一项,模型仍然依赖"阅读代码→对照 checklist"的方式去发现它。在同一个上下文内存在多个 High 级别发现时,这个 Medium 级别的模式依然可能被概率性地遗漏。


9. 第二轮改进:Grep-Gated 执行协议

9.1 问题本质的重新认识

两轮测试暴露了一个根本性的设计失误:把模型当成了人类代码审查员

人类审查员的工作方式是:读 checklist,然后用"眼睛"扫代码,靠注意力和经验发现问题。模型也在被迫做同样的事——用"注意力"扫描 checklist,然后在代码中寻找匹配。这种方式的问题在于,模型的注意力是概率性分配的;而 grep 对显式模式的检出更接近规则驱动的机械扫描。

但模型不是人,它有工具可以用。

核心解法:工具辅助检测(tool-assisted detection)+ 模型判断(model judgment)

对于有明确语法特征的 checklist 项,让模型先用 grep 机械扫描,再对 HIT 结果做语义确认。只有真正需要推理的语义项,才交给模型全量分析。

9.2 Grep-Gated 执行协议

对每个 sub-agent,执行流程变为:

1. 加载 Skill(checklist)
2. 对每个有 grep pattern 的检查项,先跑 grep
3. grep HIT  → 模型做语义确认(true positive vs false positive)
4. grep MISS → 自动标记 NOT FOUND,跳过语义分析(对当前 pattern 集合而言可稳定排除)
5. 无 grep pattern 的项 → 纯语义分析
6. 只上报 FOUND 项
7. Execution Status 中包含审计行:Grep pre-scan: X/Y items hit, Z confirmed

关键设计决策

决策内容原因
宽门槛 grep pattern宁可多 HIT 不可漏检go\s+func 同时触发 6 个并发项,false positive 交给模型过滤
go-logic-review 不改全部 10 项都是纯语义分析grep 对逻辑正确性无意义
NOT FOUND 不上报 LeadLead 只收 FOUND 结果减少 Lead 汇总时的噪音,同时保留 grep 审计行供覆盖率核查
snippet 模式将代码片段写入 $TMPDIR/review_snippet.go确保裸片段也能执行 grep,不降级为纯语义模式

覆盖率统计:7 个 Skill 共 86 条 checklist 项,其中 65 条(75%)可以 grep 化:

Skill总条数Grep 化纯语义
go-concurrency-review14131
go-performance-review12102
go-error-review12120
go-security-review16142
go-quality-review1284
go-test-review1082
go-logic-review10010
合计8665 (75%)21 (25%)

在当前设计中,75% 的检查项转化为了规则驱动的预筛选,模型的注意力可以更多集中在剩余 25% 的语义项上。

9.3 实施内容

此次改造涉及 15 个文件(7 个 Skill + 1 个 Lead Skill + 7 个 Agent 定义)。核心改动分三类:

A. 各垂直 Skill:在 checklist 表格中增加 Grep Pattern

go-performance-review 为例:

| ID | Item | Severity | Grep Pattern |
|------|------|----------|--------------|
| PERF-01 | Slice 未预分配 | Medium | `make([][^]]+,\s*0)` 且无第三个参数 |
| PERF-02 | 循环内字符串拼接 | Medium | `+=.*string|fmt.Sprintf` in `for` |

对"缺少防护"类问题使用复合 pattern(有 A 且无 B):

CONC-14: Unbounded goroutines
   grep: go\s+func
   AND NOT: SetLimit|semaphore|maxConcurrency|worker.*pool

B. 各垂直 Skill:增加 Grep-Gated Execution Protocol 节

每个 Skill 中新增统一的执行协议说明,规定 grep 先行、模型后判、NOT FOUND 不上报的执行顺序。

C. 所有 Agent 定义:增加 Execution Order 节

## Startup 之后插入:

## Execution Order

The domain Skill has been auto-injected via the skills: field in this agent's frontmatter.
1. Identify target files (or write snippet to $TMPDIR/review_snippet.go)
2. Run grep pre-scan for ALL grep-gated checklist items
3. HIT → semantic analysis to confirm or reject
4. MISS → auto-mark NOT FOUND, skip semantic analysis
5. For semantic-only items: full model reasoning required
6. Report ONLY FOUND items
7. Include: `Grep pre-scan: X/Y items hit, Z confirmed as findings`

go-logic-reviewer 使用语义专用版本:

## Execution Order

The domain Skill has been auto-injected via the skills: field in this agent's frontmatter.
1. Identify target files (from dispatch prompt)
2. All checklist items are semantic-only — no grep pre-scan applicable
3. Apply full model reasoning to each item
4. Report FOUND items only
5. Include: `Semantic-only skill: 10/10 items evaluated`

此外,go-review-lead Skill 的 dispatch template 同步更新,要求每个子代理在 Execution Status 中包含 grep 审计行,主会话汇总时据此核查覆盖率。


10. 最终验证通过——完整审查输出

本节导读:内容较长,建议按需跳读。

  • 想看结论:直接看 §10.2(3 行 Summary)
  • 想看完整过程:§10.2 Triage 决策 → §10.3 各 sub-agent 原始输出 → §10.4 去重分析 → §10.5 最终报告
  • 想验证改进效果:对比 §8.2.1(第一轮失败输出)与 §10.5(最终成功报告)

10.1 验证方法

改造完成后,用同一段 getBatchUser 代码进行第三次完整验证:

  • 代码:与第 1 节完全相同(含 make([]*User, 0) 无容量预分配)
  • 模式:裸片段(raw snippet),无文件路径,触发 snippet 模式(写入 $TMPDIR/review_snippet.go
  • 基准:第一轮单 Skill 审查发现 8 个问题(REV-001 至 REV-008),其中漏报了 Slice Pre-allocation

验证通过标准

  1. Slice Pre-allocation 被正确发现并作为正式 Finding 上报
  2. Unbounded Goroutine Spawning 被正确发现
  3. 所有原始 8 个 Findings 无退化
  4. 每个 sub-agent 的 Execution Status 包含 grep 审计行

10.2 验证结果

在本次基准案例的第三轮验证中,13 个预期发现均被捕获,未观察到新的遗漏。

这里的“通过”仅指当前基准案例满足 §10.1 中定义的标准,不代表该方案已在更广泛的 Go 代码审查场景中完成统计意义上的稳定性证明。

分诊过程:

Triage result:
- Always: go-quality-reviewer, go-logic-reviewer
- Phase 1 (imports): go-concurrency-reviewer ("sync" found)
- Phase 2 (diff patterns): go-performance-reviewer
    make([]*User, 0) 零容量 + append( 共现
- Phase 3 (file paths): go-performance-reviewer
    函数名 getBatchUser → 批量语义命中新增规则
- Phase 4 (scope): go-error-reviewer (新增代码,强制覆盖 error 维度)
- Dispatch validation: make([], append(, go func, sync. 全部命中,无需额外 override
- Skipped: go-security-reviewer(无 SQL/HTTP/exec/secret 模式)
           go-test-reviewer(无 _test.go

与上一轮相比的关键改进:

问题上一轮(单 Skill)第一轮(Multi-Agent v1)本轮(Multi-Agent + Grep-Gated)
Slice Pre-allocation❌ 漏报❌ 漏报(performance 未被分诊)✅ REV-009 正确上报
Unbounded Goroutines❌ 仅在 Residual Risk⚠️ 有时遗漏✅ REV-008 正确上报
No defer recover()✅ REV-006 正确上报
所有 High 级别问题✅ 4 个✅ 7 个(含更多维度)

10.3 并行派发:5 个 Sub-Agent 原始输出

分诊完成后,Lead Agent 并行派发 5 个 vertical agent。每个 agent 在独立上下文中加载各自的 Skill,互不感知其他 agent 的 Findings。以下为各 agent 未经整合的原始输出,可按需查阅;如只关注最终结论,可直接跳至 §10.4 去重分析和 §10.5 最终汇总报告。


go-concurrency-reviewer 原始输出

## Go Concurrency Review — getBatchUser

Findings

[High] Missing wg.Wait() — Function Returns Before Goroutines Complete
- ID: CONC-001
- Location: review_snippet.go:34 (return) vs. review_snippet.go:23 (goroutine launch)
- Impact: The function always returns an empty or partial userList. All goroutines are
  still running when the caller receives the return value — use-after-return data race.
- Evidence: wg.Add(1) at L20, goroutines at L23–L32, wg.Wait() absent.
  return userList, nil fires at L34 unconditionally.
- Recommendation: Insert wg.Wait() before the return statement.
- Action: must-fix

[High] Data Race on userList Slice — Concurrent Unsynchronized Appends
- ID: CONC-002
- Location: review_snippet.go:10 (creation), review_snippet.go:30 (concurrent write)
- Impact: Multiple goroutines call append(userList, user) concurrently on the same
  slice header — classic concurrent-write race: silent data loss or runtime panic.
  go test -race flags this immediately.
- Evidence: userList created at L10; each goroutine appends at L30 with no mutex or
  channel. wg.Wait() absence (CONC-001) compounds this.
- Recommendation: Use buffered result channel or pre-allocate indexed slice.
- Action: must-fix

[High] Loop Variable Capture — Goroutine Closure Captures u
- ID: CONC-003
- Location: review_snippet.go:14 (for loop), review_snippet.go:23, review_snippet.go:25
- Impact: In Go < 1.22, all goroutines share the same u variable. By execution time
  u holds the last loop value — all lookups query the wrong user ID.
- Evidence: go func() { ... u.Id ... }() at L23 — closure references u from for-range
  without rebinding. Go version unknown.
- Recommendation: go func(u *UserKey) { ... }(u) — safe for all Go versions.
- Action: must-fix (pending Go version confirmation)

[High] Unrecovered Goroutine Panic — No recover Guard
- ID: CONC-004
- Location: review_snippet.go:23
- Impact: Any panic inside redis.GetGuest kills the entire process. Go panics do not
  cross goroutine boundaries.
- Evidence: go func() at L23; recover() absent from entire file (grep: MISS);
  defer wg.Done() is the only defer.
- Recommendation: Add defer func() { if r := recover(); r != nil { log.Error... } }()
  as the first defer inside the goroutine.
- Action: must-fix

[Medium] Unbounded Goroutine Creation — One Goroutine Per Input Key
- ID: CONC-005
- Location: review_snippet.go:14, review_snippet.go:23
- Impact: For large batches this exhausts connection pool slots, goroutine memory,
  and scheduler capacity, causing OOM.
- Evidence: for i, u := range userKeys { ... go func() { ... } } — goroutine count
  scales linearly. No SetLimit, semaphore, or worker pool present (grep: MISS).
- Recommendation: errgroup.WithContext + g.SetLimit(n).
- Action: follow-up

[Medium] Missing errgroup — Redis Errors Silently Dropped
- ID: CONC-006
- Location: review_snippet.go:12 (WaitGroup), review_snippet.go:26-28 (error discarded)
- Impact: When redis.GetGuest fails, error is logged and goroutine exits. Caller
  never knows some users could not be fetched — always sees err == nil.
- Evidence: var wg sync.WaitGroup; no error channel or errgroup.Group.
  if err != nil { log.WarnContextf(...); continue } — error never stored.
- Recommendation: Replace WaitGroup with errgroup.Group; propagate first error.
- Action: follow-up

Suppressed Items
[Suppressed] Misplaced wg.Add — wg.Add(1) at L20 is before go func() at L23,
  correct placement. Anti-example does not apply.
[Suppressed] singleflight — fan-out over distinct keys, not repeated requests
  for same key. Not applicable.

Execution Status
- Grep pre-scan: 7/13 items hit, 5 confirmed as findings
- go test -race: not run (raw snippet, missing dependencies)

go-performance-reviewer 原始输出

## Go Performance Review — review_snippet.go

Findings

[Medium] Missing Slice Pre-allocation in Batch Hot Path
- ID: PERF-001
- Location: review_snippet.go:10
- Impact: append(userList, user) inside N concurrent goroutines causes repeated
  backing-array grow-and-copy cycles. For 64 keys: ~6 reallocation events;
  for 1024 keys: ~10. len(userKeys) is available as the upper bound.
- Evidence: userList := make([]*User, 0)  zero capacity, no second argument.
  len(userKeys) in scope at L9. Function name getBatchUser signals batch hot path.
- Recommendation: userList := make([]*User, 0, len(userKeys))
- Action: follow-up

[Medium] N Individual Redis Round-trips Instead of Batch/Pipeline
- ID: PERF-002
- Location: review_snippet.go:25
- Impact: N total individual Redis round-trips, each with independent connection
  acquisition and network latency. MGET or pipeline reduces to 1 round-trip.
  At 100 keys with 0.5ms latency: drives N request/response cycles through Redis.
- Evidence: redis.GetGuest(ctx, u.Id) inside for range  single-key lookup in loop.
  Function name getBatchUser confirms batch-workload intent.
- Recommendation: Extract IDs, call redis.MGetGuest(ctx, ids...) in one shot.
- Action: follow-up

Execution Status
- Grep pre-scan: 2/10 items hit, 2 confirmed as findings
- Grep hits: make([] at L10  PERF-001; redis.GetGuest inside for loop  PERF-002
- Grep misses (auto-skipped): 8 items auto-marked NOT FOUND

go-error-reviewer 原始输出

## Go Error Review — review_snippet.go

Findings

[High] continue Inside Goroutine — Compile Error
- ID: ERR-001
- Location: review_snippet.go:28
- Impact: Does not compile. continue is only valid inside a for/switch/select in
  the same goroutine scope. Inside go func() literal, no enclosing loop exists.
- Evidence: go func() { ... if err != nil { log.WarnContextf(...); continue } ... }()
- Recommendation: Replace continue with return.
- Action: must-fix

[High] Mixed Named and Unnamed Return Values — Compile Error
- ID: ERR-002
- Location: review_snippet.go:8
- Impact: Does not compile. func getBatchUser(...) (users []*User, error) —
  users named, error unnamed. Compiler rejects with syntax error.
- Evidence: staticcheck: "syntax error: missing parameter type" at L8:82.
- Recommendation: ([]*User, error) — all unnamed.
- Action: must-fix

[High] Missing wg.Wait() — Goroutines Not Awaited Before Return
- ID: ERR-003
- Location: review_snippet.go:34
- Impact: return userList, nil executes before any goroutine completes.
  userList is always empty. All concurrent work is silently discarded.
- Evidence: wg.Add(1) at L20; go func() at L23; wg.Wait() never called.
- Recommendation: wg.Wait() before the return.
- Action: must-fix

[High] Error Silently Swallowed — Function Always Returns nil Error
- ID: ERR-004
- Location: review_snippet.go:26-29, review_snippet.go:34
- Impact: Redis lookup failures are logged and discarded. Callers receive
  (partialList, nil) — cannot distinguish success from total failure.
- Evidence: if err != nil { log.WarnContextf(...); continue } — error not stored.
  return userList, nil hardcodes nil regardless.
- Recommendation: Use errgroup to propagate errors; or return partial-failure
  signal explicitly.
- Action: must-fix

[Medium] Nil User Appended Without Guard
- ID: ERR-005
- Location: review_snippet.go:30
- Impact: If redis.GetGuest returns (nil, nil), a nil *User enters the result slice.
  Caller dereference panics.
- Evidence: userList = append(userList, user) — no nil guard on user.
- Recommendation: if user != nil { userList = append(userList, user) }
- Action: follow-up (confirm redis.GetGuest contract)

Suppressed Items
[Suppressed] err != nil direct comparison — standard nil guard, not sentinel comparison.
[Suppressed] Log-and-return compound pattern — pattern here is worse (log-and-swallow),
  already covered by ERR-004.

Execution Status
- Grep pre-scan: 3/12 items hit, 1 confirmed as finding (ERR-005)
- ERR-001/ERR-002/ERR-003/ERR-004: confirmed via semantic analysis of grep hits
- go vet: not run (raw snippet)

go-quality-reviewer 原始输出

## Go Quality Review — review_snippet.go

Findings

[Medium] Malformed Return Signature — Mixed Named and Unnamed Results
- ID: QUAL-001
- Location: review_snippet.go:8
- Impact: Does not compile. func getBatchUser(...) (users []*User, error) — syntax error.
- Evidence: staticcheck: "syntax error: missing parameter type" at L8.
- Recommendation: ([]*User, error) all unnamed, or (users []*User, err error) all named.
- Action: follow-up

[Medium] continue Used Outside a Loop — Compile Error in Goroutine Body
- ID: QUAL-002
- Location: review_snippet.go:28
- Impact: Does not compile. continue inside go func() literal.
- Evidence: staticcheck: "continue is not in a loop (compile)" at L28.
- Recommendation: Replace with return.
- Action: follow-up

[Medium] Named Return users Shadowed by Local userList — Dead Declaration
- ID: QUAL-003
- Location: review_snippet.go:8, review_snippet.go:10
- Impact: Named return users never assigned. Separate userList created and used,
  creating reader confusion with two names for same intent.
- Evidence: users never written; return userList, nil bypasses named result.
- Recommendation: Use ([]*User, error) with local var userList.
- Action: follow-up

[Low] Unused Loop Variable i
- ID: QUAL-004
- Location: review_snippet.go:14
- Impact: Compile error — i declared and not used.
- Evidence: for i, u := range userKeys — i never referenced.
- Recommendation: for _, u := range userKeys {
- Action: follow-up

Suppressed Items
[Suppressed] defer in Loop — defer wg.Done() inside go func() literal, not
  getBatchUser's for-loop scope. Idiomatic, correct.
[Suppressed] log → slog migration — WarnContextf is a custom context-aware logger,
  not stdlib log. Migration inapplicable.

Execution Status
- Grep pre-scan: 2/8 items hit, 0 confirmed (both rejected by anti-example gates)
- staticcheck: FAIL — 2 compile errors confirmed (QUAL-001, QUAL-002)
- golangci-lint: FAIL (file does not compile)

go-logic-reviewer 原始输出

## Go Logic Review — getBatchUser

Findings

[High] Function Always Returns Empty Slice — Missing wg.Wait()
- ID: LOGIC-001
- Location: return userList, nil at end of getBatchUser
- What it does: Spawns goroutines and immediately returns. userList always empty.
- What it should do: Wait for all goroutines before returning populated slice.
- Evidence: wg.Add(1) and wg.Done() present, wg.Wait() never called.
- Recommendation: wg.Wait() before return.
- Action: must-fix

[High] continue Inside Goroutine — Compile Error
- ID: LOGIC-002
- Location: continue inside go func() literal
- What it does: continue in anonymous function has no enclosing loop — compile error.
- What it should do: return to exit the goroutine function.
- Evidence: go func() { ... if err != nil { ... continue } ... }() — no loop in scope.
- Recommendation: Replace continue with return.
- Action: must-fix

[High] Loop Variable Capture — All Goroutines May Fetch Same User (Pre-Go 1.22)
- ID: LOGIC-003
- Location: go func() { ... redis.GetGuest(ctx, u.Id) ... }()
- What it does: Closure captures u by reference. All goroutines may use last value.
- What it should do: Each goroutine should process its own distinct key.
- Evidence: go func() { ... }() — zero parameters, u closed over by reference.
- Recommendation: go func(key *UserKey) { ... }(u)
- Action: must-fix

[High] Concurrent Unsynchronized append — Silent Data Loss
- ID: LOGIC-004
- Location: userList = append(userList, user) inside goroutine
- What it does: Multiple goroutines append to same userList without sync.
  Results silently lost; returned slice incomplete.
- Evidence: No mutex, channel, or atomic guards the append.
- Recommendation: mutex around append, or channel-based collection.
- Action: must-fix

[Medium] Error Always Returned as nil — Silent Partial Failure
- ID: LOGIC-005
- Location: return userList, nil
- What it does: Returns nil error unconditionally despite per-key fetch failures.
- What it should do: Surface failure signal to callers.
- Evidence: Redis errors caught, logged, goroutine exits. Outer function ignores errors.
- Recommendation: Collect errors, return multi-error on partial failure; or document
  best-effort semantics explicitly.
- Action: needs-clarification

[Medium] Loop Variable i Declared but Never Used — Compile Error
- ID: LOGIC-006
- Location: for i, u := range userKeys
- What it does: i declared but never used — compile error.
- Evidence: i never appears after loop header.
- Recommendation: for _, u := range userKeys {
- Action: must-fix

Execution Status
- Semantic-only skill: all items evaluated

10.4 去重分析与合并

5 个 agent 共产出 23 条原始 Findings(CONC ×6、PERF ×2、ERR ×5、QUAL ×4、LOGIC ×6)。Lead Agent 按以下规则去重合并:

原始 Finding涉及 Agent严重度冲突合并决策
continue 编译错误ERR-001 [H] + QUAL-002 [M] + LOGIC-002 [H]H vs M → 促升 H保留最详细的 ERR-001;合并 3 条为 REV-001
混合具名/匿名返回ERR-002 [H] + QUAL-001 [M]H vs M → 促升 H保留 ERR-002;合并为 REV-002
缺少 wg.Wait()CONC-001 [H] + ERR-003 [H] + LOGIC-001 [H]同为 H合并证据;以 CONC-001 为主;REV-003
并发 append 竞争CONC-002 [H] + LOGIC-004 [H]同为 HCONC-002 证据更详细;REV-004
循环变量捕获CONC-003 [H] + LOGIC-003 [H]同为 HCONC-003 为主;REV-005
goroutine panic 无 recoverCONC-004 [H]仅 1 条直接收录;REV-006
错误被吞噬ERR-004 [H] + CONC-006 [M] + LOGIC-005 [M]H vs M → 保持 HERR-004 为主;合并 3 条;REV-007
无界 goroutine 创建CONC-005 [M]仅 1 条直接收录;REV-008
Slice 未预分配PERF-001 [M]仅 1 条直接收录;REV-009
N 次 Redis 单独请求PERF-002 [M]仅 1 条直接收录;REV-010
nil *User 未过滤ERR-005 [M]仅 1 条直接收录;REV-011
具名返回遮蔽QUAL-003 [M]仅 1 条直接收录;REV-012
循环变量 i 未使用QUAL-004 [L] + LOGIC-006 [M]M vs L → 促升 MLOGIC-006 为主;REV-013

去重后:23 条原始 Finding → 13 条合并 Finding(7 High / 6 Medium)。

10.5 最终汇总报告

以下是本轮 go-review-lead 的完整汇总报告,未经任何修改,作为验证结果的原始证据:

---
Review Mode

- Dispatched skills: go-concurrency-reviewer, go-performance-reviewer,
  go-error-reviewer, go-quality-reviewer, go-logic-reviewer
- Skipped skills: go-security-reviewer (no SQL/HTTP/exec/secret patterns),
  go-test-reviewer (no _test.go)
- Triage rationale: "sync" import + go func + sync.WaitGroup triggered concurrency;
  make([]*User, 0) + append( + getBatchUser batch-semantic name triggered performance
  (Phase 2 + Phase 3 new rule); new code  error dispatched (Phase 4);
  dispatch validation grep confirmed all overrides; quality + logic always
  dispatched as baseline.

---
Findings

[High] continue Inside Goroutine  Compile Error
- ID: REV-001 (original: ERR-001 / QUAL-002 / LOGIC-002)
- Origin: uncertain
- Category: Error / Quality / Logic
- Location: review_snippet.go:28
- Impact: The file does not compile. continue is only valid inside a for/switch/select
  in the current function scope. Inside a go func() literal the goroutine is a separate
  function  no enclosing loop exists.
- Evidence: go func() { ... if err != nil { log.WarnContextf(...); continue } ... }()
  staticcheck confirms: "continue is not in a loop (compile)".
- Recommendation: Replace continue with return.
- Action: must-fix (compile blocker)

[High] Mixed Named and Unnamed Return Values  Compile Error
- ID: REV-002 (original: ERR-002 / QUAL-001)
- Origin: uncertain
- Category: Error / Quality
- Location: review_snippet.go:8
- Impact: The file does not compile. Go forbids mixing named and unnamed return
  parameters. users is named, error is unnamed.
- Evidence: func getBatchUser(...) (users []*User, error) {
  staticcheck: "syntax error: missing parameter type".
- Recommendation: Use all unnamed: ([]*User, error) or all named: (users []*User, err error)
- Action: must-fix (compile blocker)

[High] Missing wg.Wait()  Function Always Returns Empty Slice
- ID: REV-003 (original: CONC-001 / ERR-003 / LOGIC-001)
- Origin: uncertain
- Category: Concurrency / Logic
- Location: review_snippet.go:34
- Impact: All goroutines are still in-flight when return userList, nil executes.
  userList is always empty at return time. Total silent data loss.
- Evidence: wg.Add(1) at L20, go func() at L23, but wg.Wait() is absent.
- Recommendation: Add wg.Wait() before the return statement.
- Action: must-fix

[High] Data Race: Concurrent Unsynchronized Append to Shared Slice
- ID: REV-004 (original: CONC-002 / LOGIC-004)
- Origin: uncertain
- Category: Concurrency
- Location: review_snippet.go:10 (creation), review_snippet.go:30 (concurrent write)
- Impact: Multiple goroutines concurrently read and write the same slice header without
  synchronization. Silent data loss, duplicate entries, or runtime panic.
  go test -race will flag this immediately.
- Evidence: userList declared at L10; every goroutine calls append(userList, user)
  at L30 with no mutex or channel.
- Recommendation: Use buffered channel, or pre-allocate indexed result slice
  (each goroutine owns its index).
- Action: must-fix

[High] Loop Variable u Captured by Goroutine Closure (Pre-Go 1.22)
- ID: REV-005 (original: CONC-003 / LOGIC-003)
- Origin: uncertain
- Category: Concurrency / Logic
- Location: review_snippet.go:14, review_snippet.go:23, review_snippet.go:27
- Impact: On Go < 1.22, all goroutines share the same u loop variable. All goroutines
  may fetch the same user key  wrong results silently.
- Evidence: go func() { ... u.Id ... }()  no argument pass, no u := u rebind.
- Recommendation: go func(u *UserKey) { ... }(u)  safe for all Go versions.
- Action: must-fix (verify go.mod)

[High] No defer recover() in Goroutine  Unhandled Panic Terminates Process
- ID: REV-006 (original: CONC-004)
- Origin: uncertain
- Category: Concurrency
- Location: review_snippet.go:23
- Impact: A panic inside redis.GetGuest kills the entire process. Go panics do not
  cross goroutine boundaries.
- Evidence: go func() at L23; recover() absent from entire file; only defer wg.Done()
  present.
- Recommendation: Add recover guard as first defer inside goroutine.
- Action: must-fix

[High] All Errors Silently Swallowed  Caller Always Receives nil Error
- ID: REV-007 (original: ERR-004 / CONC-006 / LOGIC-005)
- Origin: uncertain
- Category: Error / Concurrency / Logic
- Location: review_snippet.go:26-29, review_snippet.go:34
- Impact: Any Redis lookup failure is logged and discarded. Function unconditionally
  returns (partialList, nil). Callers cannot distinguish total success from total failure.
- Evidence: if err != nil { log.WarnContextf(...); continue }  error never stored.
  return userList, nil hardcodes nil regardless.
- Recommendation: Replace manual WaitGroup with errgroup.WithContext. This single
  rewrite also resolves REV-003, REV-004, REV-008 together.
- Action: must-fix

[Medium] Unbounded Goroutine Spawning  Resource Exhaustion Under Large Batches
- ID: REV-008 (original: CONC-005)
- Origin: uncertain
- Category: Concurrency / Performance
- Location: review_snippet.go:14, review_snippet.go:23
- Impact: One goroutine per input key with no concurrency ceiling. For large batches
  this exhausts connection pool slots, goroutine memory, and scheduler capacity.
- Evidence: for i, u := range userKeys { ... go func() { ... } }  goroutine count
  scales linearly with len(userKeys). No SetLimit, semaphore, or worker pool present.
- Recommendation: Use errgroup.SetLimit(n)  subsumed by REV-007 fix.
- Action: follow-up

[Medium] Missing Slice Pre-allocation  Repeated Reallocation in Batch Hot Path
- ID: REV-009 (original: PERF-001)
- Origin: uncertain
- Category: Performance
- Location: review_snippet.go:10
- Impact: make([]*User, 0) has zero capacity. Each append that exceeds capacity triggers
  a grow-and-copy cycle: ~6 reallocations for 64 keys, ~10 for 1024 keys.
  len(userKeys) is available as a function parameter.
- Evidence: userList := make([]*User, 0) at L10  no capacity argument.
  Function name getBatchUser signals large-batch hot-path execution.
- Recommendation: userList := make([]*User, 0, len(userKeys))
- Action: follow-up

[Medium] N Individual Redis Round-trips  Batch API Not Used
- ID: REV-010 (original: PERF-002)
- Origin: uncertain
- Category: Performance
- Location: review_snippet.go:25
- Impact: N total individual Redis round-trips. A Redis MGET or pipeline reduces N
  round-trips to 1, eliminating the goroutine-per-key pattern entirely.
- Evidence: redis.GetGuest(ctx, u.Id) inside for range  single-key lookup in a loop.
- Recommendation: Extract all non-nil IDs, call redis.MGetGuest(ctx, ids...) in one shot.
- Action: follow-up

[Medium] Nil *User May Be Appended  Caller Dereference Panic
- ID: REV-011 (original: ERR-005)
- Origin: uncertain
- Category: Error
- Location: review_snippet.go:30
- Impact: If redis.GetGuest returns (nil, nil), a nil *User pointer enters the result
  slice. Caller dereference will panic.
- Evidence: userList = append(userList, user)  no nil guard on user.
- Recommendation: if user != nil { userList = append(userList, user) }
- Action: follow-up (confirm redis.GetGuest contract first)

[Medium] Named Return users Shadowed by Local userList  Dead Declaration
- ID: REV-012 (original: QUAL-003)
- Origin: uncertain
- Category: Quality
- Location: review_snippet.go:8, review_snippet.go:10
- Impact: Named return users is never assigned. A separate userList shadows it,
  creating reader confusion.
- Evidence: users never written; return userList, nil bypasses named result entirely.
- Recommendation: func getBatchUser(...) ([]*User, error) { var userList []*User ... }
- Action: follow-up

[Medium] Unused Loop Variable i  Compile Error
- ID: REV-013 (original: QUAL-004 / LOGIC-006)
- Origin: uncertain
- Category: Quality / Logic
- Location: review_snippet.go:14
- Impact: In Go, declaring a variable and not using it is a compile error.
- Evidence: for i, u := range userKeys  i never referenced.
- Recommendation: for _, u := range userKeys {
- Action: must-fix (compile blocker)

---
Execution Status
- Go version: unknown (no go.mod provided)
- Dispatch validation grep: 0 overrides needed
- Skills dispatched: go-concurrency-reviewer, go-performance-reviewer,
  go-error-reviewer, go-quality-reviewer, go-logic-reviewer
- Per-skill grep audit:
  - go-concurrency-reviewer:  Grep pre-scan 7/13 hit, 5 confirmed
                              go test -race: not run (raw snippet)
  - go-performance-reviewer:  Grep pre-scan 2/10 hit, 2 confirmed
  - go-error-reviewer:        Grep pre-scan 3/12 hit, 1 confirmed
  - go-quality-reviewer:      Grep pre-scan 2/8 hit, 0 confirmed (anti-example rejected)
                              staticcheck: FAIL (2 compile errors confirmed)
  - go-logic-reviewer:        Semantic-only skill: all items evaluated
- Excluded (generated): None

---
Risk Acceptance / SLA
- High (REV-001, REV-002, REV-013): Immediate  compile blockers
- High (REV-003 through REV-007): Must-fix before merge
- Medium (REV-008 through REV-012): Follow-up within 14 days

---
Open Questions
1. Go version  if go.mod confirms Go >= 1.22, REV-005 resolved by runtime.
2. redis.GetGuest contract  does it ever return (nil, nil)? Determines REV-011 risk.
3. Error contract  fail-fast or best-effort? Determines fix strategy for REV-007.

---
Residual Risk / Testing Gaps
1. go test -race not run  recommended after fixing REV-004.
2. Volume-cap overflow: None  13 findings, within cap.
3. redis.GetGuest internals unknown  potential panic behavior unassessable.

---
Summary

7 High / 6 Medium / 0 Low  all uncertain origin (standalone snippet).

The code has 3 compile errors (REV-001, REV-002, REV-013)  cannot build at all.
Even if compiled: always returns empty slice (REV-003), silently corrupts results
under concurrency (REV-004), swallows all errors (REV-007). Recommended rewrite:
replace manual WaitGroup + shared-slice with errgroup.WithContext + SetLimit(n) +
indexed pre-allocated results slice  resolves REV-003, REV-004, REV-007, REV-008,
REV-009 in one clean structure.

10.6 结论与闭环

至此,改进闭环完整成立:

[问题发现]
单 Skill 注意力稀释 → Slice Pre-allocation 漏报

        ↓

[第一次架构改造]
Multi-Agent 架构(7 vertical + 1 Lead)
→ 验证:仍有漏报(分诊盲区 + 维度内注意力稀释)

        ↓

[第一轮改进]
调整 Phase 2/Phase 3 分诊规则
→ 验证:仍不稳定

        ↓

[洞察升级]
不是 checklist 不够好,是执行机制错误
模型≠人类审查员,模型有工具

        ↓

[第二次架构改进]
Grep-Gated Execution Protocol
75% checklist 项变为规则驱动的 grep 预检测

        ↓

[最终验证]
当前基准案例中得到 7 High + 6 Medium,13 个预期问题均被捕获
包括原始漏报的 Slice Pre-allocation(REV-009)
和 Unbounded Goroutines(REV-008

在当前案例和当前实现范围内,Multi-Agent 架构显著缓解了跨维度注意力竞争,Grep-Gated 协议进一步降低了维度内的概率性遗漏。两层改进共同构成了本文所述 Skill-Agent 协作体系的当前版本解法。

10.7 适用范围与未验证边界

本文的结论强度应限定在以下范围内:

  • 已验证范围:Go 代码审查场景;以 getBatchUser(并发/goroutine 类)为主案例,以 ListLayout(安全/ORM/设计类,见 §11)为跨域追加案例;当前文中列出的 7 个垂直 Skill + 1 个 Lead Agent 实现;当前 Grep-Gated checklist 覆盖率(65/86,约 75%)。两个案例跨越不同代码域,均未观察到漏报。
  • 尚未充分验证:其他语言;跨文件、超大 diff、真实 PR 历史上下文;测试维度更复杂的案例;不同模型版本下的稳定性;多案例上的误报率、漏报率和成本曲线。
  • 不应直接推出的结论:不能凭两个案例就断言"所有重度 Skill 都应拆分为 Multi-Agent",也不能断言"Grep-Gated 一定优于所有其他执行协议"。更准确的说法是:两个案例共同表明这类问题确实存在,并初步展示了该解决路径在不同代码域的有效性。
  • 后续应补的证据:一组测试类案例、一组跨文件 PR 案例;记录多次重复运行结果;同时统计误报、漏报、延迟和 token 成本,才能更有把握地讨论"广泛稳定成立"。

11. 追加验证:跨域案例 — ListLayout

11.1 验证背景

§10 的基准案例 getBatchUser 集中于并发/goroutine 类缺陷,属于单一维度的高密度错误场景。为初步检验架构在不同代码域的适用性,选取一个典型业务服务层方法作为第二案例。

维度getBatchUser(§10 主案例)ListLayout(本节)
代码域并发 / goroutine安全 / ORM / API 设计
主要风险数据竞争、生命周期管理SQL injection、数据一致性、API 契约
业务层级并发工具函数服务层 API 方法

11.2 被审查代码

func (s *LayoutService) ListLayout() (layouts []*db.Layout, total int64, err error) {
    layouts = make([]*db.Layout, 0)

    whereClause := fmt.Sprintf("uid = %v and corp_id = %v", s.uid, s.corpId)
    if err := s.orm.Model(&db.Layout{}).Where(whereClause).Order("updated_at desc").Find(&layouts).Error; err != nil {
        return nil, 0, err
    }

    if err := s.orm.Model(&db.Layout{}).Where(whereClause).
        Count(&total).Error; err != nil {
        return nil, 0, err
    }

    return layouts, total, nil
}

表面上是一个普通的列表查询方法,实际上存在跨越安全、逻辑、性能、质量四个维度的问题。

11.3 分诊结果

Triage result:
- Always: go-quality-reviewer, go-logic-reviewer
- Phase 2 + Dispatch validation: go-security-reviewer(fmt.Sprintf 构造 SQL WHERE clause)
- Phase 2 + Dispatch validation: go-performance-reviewer(make([]+Find/Count)
- Phase 4(新增代码): go-error-reviewer
- Skipped: go-concurrency-reviewer(无 go func/sync),go-test-reviewer(无 _test.go

5 个 Agent 并行执行。

11.4 审查结果摘要

ID级别类别描述
REV-001HighSecurityfmt.Sprintf 拼接 SQL + GORM 单参数 .Where() 构成 SQL injection 注入面
REV-002HighLogicFindCount 无事务包裹,并发写入可使 totallen(layouts) 静默分叉
REV-003HighLogic返回 (layouts, total) 的分页契约,但函数无 limit/offset 参数,Count 在当前实现中逻辑冗余
REV-004 ★MediumQuality函数执行 DB I/O 但签名无 ctx context.Context,查询不可取消(新增 Item 13 触发)
REV-005 ★MediumPerformance查询顺序倒置(Find 先于 Count),缺少 Count-First zero-guard(新增 Item 13 触发)
REV-006MediumError裸 ORM 错误传播,调用方无法区分 Find / Count 失败,缺少操作上下文

3 High / 3 Medium / 0 Low — 未观察到漏报。

11.5 本案例的附加价值:架构迭代活性的证明

本案例不仅验证了审查结果的完整性,还触发了一次 checklist 迭代:

第一轮审查结束后,识别出两处检查项空白:

  • go-quality-review 缺少"I/O 函数无 ctx 参数"规则
  • go-performance-review 缺少"Count-First zero-guard"规则

填补空白后重新审查,立即捕获了 REV-004 和 REV-005——两个原先会被遗漏的问题。

这一闭环说明:架构不仅能稳定执行既有 checklist,还能通过实际案例反馈持续改进覆盖率。单 Skill 架构下,一旦某项被注意力稀释跳过,该缺口往往不可见;Multi-Agent 架构下,每个 Agent 的 Execution Status 明确报告 Grep pre-scan: X/Y items hit,使 checklist 空白可被观测、可被修复。

11.6 跨案例小结

案例代码域结果代表性发现
getBatchUser(并发版)并发 / goroutine7H + 6M数据竞争、缺少 wg.Wait()、unbounded goroutine、Slice Pre-allocation
ListLayout安全 / ORM / 设计3H + 3MSQL injection、非原子查询、API 契约缺陷、缺失 ctx

两个案例覆盖不同代码域,均未观察到漏报。在追加的 ListLayout 案例中,架构在不同代码模式下表现稳定,未观察到新的遗漏。三个审查维度(并发安全、安全注入、ORM 设计)的覆盖初步表明,方案的有效性不局限于特定 bug 类型。

但样本量仍然有限,外部有效性的完整建立需要更多独立场景下的持续验证。


12. Token 效率优化与审查粒度分层

12.1 问题:43.5% 的 Finding 重复率

§10.4 的去重分析揭示了一个不容回避的效率问题:

5 个 Agent 共产出 23 条原始 Finding,去重合并后为 13 条——重复率 43.5%

这意味着近一半的 sub-agent token 消耗在产出最终被丢弃的重复 Finding 上。重复并非随机分布,而是集中在两个结构性来源:

重复来源典型案例占重复总量
编译/语法错误continue 编译错误被 ERR、QUAL、LOGIC 三个 Agent 分别报出~50%(5/10 条重复)
跨维度语义交叉wg.Wait() 缺失既是并发问题(CONC)也是逻辑问题(LOGIC)~50%(5/10 条重复)

第一类重复的根因是:编译错误不需要领域专业知识,任何能读 Go 代码的 Agent 都会发现它。第二类重复的根因是:go-logic-reviewer 的 10 项 checklist 中有多项与专职 Agent 的 checklist 存在结构性重叠——当这些专职 Agent 已被派发时,Logic Agent 报出的同一 Finding 就是纯粹的 token 浪费。

12.2 三层优化策略

策略 A:编译预检(Compile Pre-check)

在分诊之前,Lead Agent 先运行 go build。如果存在编译错误:

  1. Lead Agent 自行报告编译错误为 High 级别 Finding(这不需要领域专业知识)
  2. 在每个 sub-agent 的 dispatch prompt 中附加:"编译错误已由 Lead 报告,请跳过以下位置:[列表]"
  3. sub-agent 仍然审查非编译问题
                ┌─────────────┐
                │  go build   │
                └──────┬──────┘
                       │
              ┌────────┴────────┐
              │                 │
        编译通过            编译失败
              │                 │
        正常分诊         Lead 直接上报
                          编译错误
                                │
                         sub-agent 跳过
                         已知编译位置

预估收益:在 getBatchUser 案例中,3 个编译错误被 4 个 Agent 重复报出共 12 条原始 Finding → 3 条合并。编译预检后,这 9 条冗余 Finding 直接消除,重复率从 43.5% 降至约 25%。

策略 B:条件性 Scope 收窄

当专职 Agent 已被派发时,通知 go-logic-reviewer 跳过与其重叠的 checklist 项:

已派发的专职 Agentgo-logic-reviewer 跳过的项
go-concurrency-reviewergoroutine 生命周期 / 同步正确性
go-error-reviewer错误传播路径完整性
go-performance-reviewer资源分配效率

关键约束:这是一个条件性规则,不是无条件裁剪。当某个专职 Agent 未被派发时(例如代码不涉及并发,go-concurrency-reviewer 被跳过),go-logic-reviewer 必须保留对应的 checklist 项——它是最后的兜底。

预估收益:在 getBatchUser 案例中,LOGIC-001/002/003/004/005 共 5 条全部与 CONC 或 ERR 重复。条件收窄后可削减约 50% 的 Logic Agent 冗余输出。

策略 C:模型分层(已有,显式声明)

当前架构已在实践模型分层:

角色模型理由
Lead Agent(分诊 + 汇总)Sonnet需要准确的分诊判断和跨 Agent 去重推理
7 个垂直 sub-agentHaiku在 Skill + Grep-Gated 协议的约束下,执行型任务不需要最强推理

即使存在重复,Haiku 的 token 单价(约为 Sonnet 的 1/10)使得浪费的绝对成本可控。但这不是不优化的理由——策略 A 和 B 从源头减少重复,策略 C 从单价控制兜底。

12.3 三层策略的协同效果

策略机制预估重复率变化实现复杂度
A. 编译预检Lead 先跑 go build,编译错误自行上报43.5% → ~25%
B. 条件性 scope 收窄dispatch prompt 中排除已覆盖的交叉项~25% → ~15%
C. 模型分层Haiku 执行、Sonnet 编排不降重复率,降绝对成本已完成

预期最终状态:重复率从 43.5% 降至约 15%。剩余的 ~15% 是有价值的交叉验证——同一问题在不同维度的严重度评估可能不同(例如 Error Agent 评为 Medium,Concurrency Agent 评为 High),这种"冲突"恰好是合并规则的输入,不是浪费。

12.4 审查粒度分层:Lite / Standard / Strict

原有的单体 go-code-reviewer skill 根据代码变更范围和风险信号自动选择审查粒度(Lite: ≤5 findings、Standard: ≤10、Strict: ≤15),而 Multi-Agent 架构一直缺少这一能力——等效于永远运行 Strict 模式。

这意味着一个只改了变量名的 2 行 PR,也会启动 2~7 个 Agent、加载完整 Skill、执行完整 checklist。这是另一种层面的资源浪费。

深度选择规则

深度触发条件Agent 数量Finding Soft Cap
Lite变更 ≤3 文件 无高风险信号2(Quality + Logic)5
Standard默认——不满足 Lite 也不满足 Strict2~7(按需派发)10
Strict存在任一高风险信号 > 15 文件 用户明确要求2~7(全量派发 + 验证 grep)15

高风险信号:security/auth 变更、新增并发原语、HTTP/API 契约变更、persistence/schema 变更、exported 签名变更、大范围重构(> 15 文件)。

Lite 安全网:如果 2 个 Agent 合计报出 ≥ 3 个 High,自动升级为 Standard 并重新分诊。

深度模式与优化策略的协同

深度策略 A(编译预检)策略 B(scope 收窄)设计意图
Lite不执行不执行2 个 Agent 本身已足够精简,额外开销 > 收益
Standard执行执行平衡效率与覆盖率
Strict执行不执行安全/发布门禁场景下,交叉验证的冗余是"互相兜底"的安全网

注意 Strict 模式下刻意不应用策略 B——这是一个明确的设计决策。在发布门禁场景中,宁可多花 token 做交叉验证,也不接受因 scope 收窄导致某个维度的 Finding 被错误排除的风险。

12.5 效率与完整性的平衡

Multi-Agent 架构的核心价值是消除注意力稀释带来的系统性遗漏。本章的优化策略在保留这一核心价值的前提下,通过三个层面削减不必要的 token 消耗:

  完整性保障                              效率优化
  ┌─────────────────┐                    ┌─────────────────┐
  │ 独立上下文       │  ← 不变 →          │ 编译预检         │ ← 消除确定性重复
  │ Grep-Gated 协议  │  ← 不变 →          │ 条件性 scope 收窄│ ← 消除交叉重复
  │ 跨维度覆盖       │  ← 不变 →          │ 模型分层         │ ← 降低单价
  │ Strict 模式兜底  │  ← 不变 →          │ 深度分层         │ ← 按需投入
  └─────────────────┘                    └─────────────────┘

这不是一个"效率 vs 质量"的取舍问题,而是识别并消除不产生信息增量的冗余计算。真正产生信息增量的交叉验证(如同一问题在不同维度的严重度判断差异)被完整保留。

12.6 验证:优化协议首次运行

§12.1–12.5 是分析与设计,方案改进后需要和 §10 一样经历实际审查的验证。验证对象选择 §10 的原始基准案例——相同的 getBatchUser 并发错误版本,以便与旧协议进行直接对比。

12.6.1 验证设置

  • 被审代码:§10 基准案例中的并发版 getBatchUser(含 go funcsync.WaitGroup、循环变量捕获等多维度缺陷)
  • 新协议要素:Strict 深度选择 + 编译预检(策略 A)+ Strict 模式下不收窄 scope
  • 对照基准:§10.4 的审查结果(旧协议,无优化)

12.6.2 新协议执行过程

Step 1.5 深度决策:代码含 go func + sync.WaitGroup → **高风险信号(并发原语)**→ Strict 模式

Step 1.5b 编译预检:运行 go build review_snippet.go,得到:

./review_snippet.go:19:82: syntax error: missing parameter type
./review_snippet.go:35:5: continue is not in a loop

→ Lead 直接上报 2 条 High/Compile Finding(REV-001, REV-002) → 向 5 个 sub-agent 发送 dispatch prompt,附加指令:"以上编译位置已由 Lead 上报,不要重复报告,聚焦运行时问题"

Step 2 分诊:sync import + go func + make([]) + append + getBatchUser 批量命名 → 触发 5 个 Agent(与旧协议相同)

Strict 模式说明:go-logic-reviewer 不收窄 scope,全量执行 10 项 checklist(与旧协议相同维度覆盖)

原始审查结果如下:

---
Review Mode

- Review depth: Strict(高风险信号:go func + sync.WaitGroup → 并发原语新增)
- Compile pre-check: FAIL — 2 处编译错误,已由 Lead 直接上报(REV-001, REV-002)
- Dispatched agents: go-concurrency-reviewer, go-performance-reviewer, go-error-reviewer,
  go-quality-reviewer, go-logic-reviewer
- Skipped agents: go-security-reviewer(无 SQL/HTTP/exec 模式); go-test-reviewer(无 _test.go)
- Scope narrowing: Not applied(Strict 模式)
- Triage rationale: sync import + go func + WaitGroup 触发 concurrency;make([]*User, 0) + append
  + getBatchUser 批量语义命名触发 performance;新增代码触发 error;quality + logic 始终派发

---
Findings

[High] Mixed Named/Unnamed Return Parameters — Compile Error
- ID: REV-001 (compile error: Lead-reported)
- Origin: uncertain
- Category: Compile
- Location: review_snippet.go:19
- Impact: 文件无法编译。Go 禁止混合具名与匿名返回参数;`users` 是具名返回,`error` 是匿名
  类型标记,两者不能共存。编译器输出:`syntax error: missing parameter type`
- Evidence: func getBatchUser(...) (users []*User, error)
- Recommendation: 统一为全匿名 ([]*User, error),或全具名 (users []*User, err error)。对短
  函数推荐全匿名。
- Action: must-fix (compile blocker)

[High] `continue` Inside Goroutine Body — Compile Error
- ID: REV-002 (compile error: Lead-reported)
- Origin: uncertain
- Category: Compile
- Location: review_snippet.go:35
- Impact: 第二处编译错误。`continue` 仅在 for/switch/select 的直接封闭作用域内有效;
  goroutine 闭包是独立函数,其内部没有封闭循环。编译器输出:`continue is not in a loop`
- Evidence: go func() { ... if err != nil { ...; continue } ... }()
- Recommendation: 将 `continue` 替换为 `return`。`defer wg.Done()` 在 return 时会正常触发。
- Action: must-fix (compile blocker)

[High] Missing `wg.Wait()` — Function Always Returns Empty Slice
- ID: REV-003 (original: CONC-001 / LOGIC-001)
- Origin: uncertain
- Category: Concurrency / Logic
- Location: review_snippet.go:30 (wg declared), :48 (return without Wait)
- Impact: 函数在所有 goroutine 完成之前就返回 `userList`,调用方始终收到空切片。goroutine
  继续运行并向已逃逸的切片写入,与调用方发生 data race。WaitGroup 的唯一目的就是同步
  goroutine,不调用 Wait() 使整个并发结构失去意义。
- Evidence: wg.Add(1) 在 L35 加,goroutine 在 L37 启动,defer wg.Done() 在 L38,
  但 wg.Wait() 在 return userList, nil (L48) 之前完全缺失。
- Recommendation: 在 return 语句前插入 wg.Wait()
- Action: must-fix

[High] Data Race: Concurrent Unsynchronized Append to Shared Slice
- ID: REV-004 (original: CONC-002)
- Origin: uncertain
- Category: Concurrency
- Location: review_snippet.go:28 (declaration), :44 (concurrent write)
- Impact: 多个 goroutine 并发读写同一切片头(pointer, length, capacity),无任何同步保护。
  并发 append 导致静默数据丢失、重复元素或 runtime panic。go test -race 会立即标记。
- Evidence: userList := make([]*User, 0) 在 L28(共享);每个 goroutine 在 L44 执行
  userList = append(userList, user),无 mutex、无 channel。
- Recommendation: 方案 A — 使用带缓冲 channel:resultCh := make(chan *User, len(userKeys)),
  goroutine 内 send,wg.Wait() 后 drain;方案 B — 预分配 results[i] = user(每个 goroutine
  独占索引)。推荐方案 A。
- Action: must-fix

[High] Loop Variable `u` Captured by Goroutine Closure (Go < 1.22)
- ID: REV-005 (original: CONC-003 / LOGIC-002 / QUAL-004 — merged, promoted from M to H)
- Origin: uncertain
- Category: Concurrency / Logic / Quality
- Location: review_snippet.go:31 (loop), :39,:41 (goroutine captures)
- Impact: Go ≤ 1.21 中,所有 goroutine 共享同一个 u 变量地址;循环结束时 u 已指向最后
  一个非 nil 元素(或 nil),所有 goroutine 都对同一 key 调用 redis.GetGuest,产生静默
  的错误数据(全是同一用户 + 其他用户丢失)。
- Evidence: for i, u := range userKeys { go func() { redis.GetGuest(ctx, u.Id) }() }
  — goroutine 通过闭包引用 u,未通过参数传递,未做 u := u rebind。
- Recommendation: go func(u *UserKey) { ... }(u) — 兼容全部 Go 版本;
  若 go.mod 确认 ≥ 1.22 可省略,但仍建议保留以显式表达意图。
- Action: must-fix (verify go.mod)

[High] Unrecovered Goroutine Panic — Process Terminates on Any Panic
- ID: REV-006 (original: CONC-004)
- Origin: uncertain
- Category: Concurrency
- Location: review_snippet.go:37
- Impact: goroutine 内的任何 panic(redis.GetGuest 内的 nil pointer、网络层 assertion、
  连接池 panic)均无法被父 goroutine 捕获,直接终止整个进程,影响所有并发请求。
- Evidence: go func() { defer wg.Done() ... }()——唯一的 defer 是 wg.Done(),无 recover 守卫。
- Recommendation: 在 goroutine 体内最前面(LIFO,最后执行)添加:
  defer func() { if r := recover(); r != nil { log.WarnContextf(ctx, "panic: %v", r) } }()
- Action: must-fix

[High] Goroutine Errors Permanently Swallowed — Caller Always Receives nil Error
- ID: REV-007 (original: ERR-001 / LOGIC-003 / CONC-006 — merged, promoted to H)
- Origin: uncertain
- Category: Error / Logic / Concurrency
- Location: review_snippet.go:40-42 (error path), :48 (unconditional nil return)
- Impact: Redis 查找失败仅被 warn log,错误永不到达调用方。函数无条件返回 nil 错误;Redis
  完全不可用时调用方收到 ([], nil)——空的成功,无法与真正的空集合区分,静默数据丢失。
- Evidence: if err != nil { log.WarnContextf(...); continue } — 错误在 goroutine 内被丢弃;
  return userList, nil 硬编码 nil,无论多少次 fetch 失败。函数签名含 error 返回表明设计上
  意图传播错误,但实现完全违背了这一意图。
- Recommendation: 用 errgroup.WithContext 替换手工 WaitGroup,同时解决 REV-003/007/009:
  g, ctx := errgroup.WithContext(ctx); g.SetLimit(n); goroutine 内 return fmt.Errorf(...);
  最后 err := g.Wait()。
- Action: must-fix

[High] Nil *User Appended Without Guard — Deferred Caller Panic
- ID: REV-008 (original: ERR-002)
- Origin: uncertain
- Category: Error
- Location: review_snippet.go:44
- Impact: redis.GetGuest 可合法返回 (nil, nil)(cache miss)。nil *User 被无条件 append
  进结果切片;任何访问 users[i].Id 的调用方将触发 nil pointer dereference panic。
  输入 nil guard(L32)保护了输入切片,但输出侧完全缺少对应的 nil 检查。
- Evidence: user, err := redis.GetGuest(...) 后直接 userList = append(userList, user),
  无 if user == nil { continue } 或等价检查。
- Recommendation: 在 append 前添加 if user == nil { continue }(或按业务语义决定是否
  视为错误)。
- Action: must-fix

[Medium] Unbounded Goroutine Spawning — Resource Exhaustion Under Load
- ID: REV-009 (original: CONC-005)
- Origin: uncertain
- Category: Concurrency / Performance
- Location: review_snippet.go:37 (inside for range at L31)
- Impact: goroutine 数量与 len(userKeys) 线性增长,无并发上限。大批量输入(数百/数千 key)
  会同时产生等量 goroutine,耗尽 Redis 连接池、goroutine 栈内存和调度器容量。
- Evidence: for range userKeys { go func() {...} },无 errgroup.SetLimit、semaphore 或
  worker pool。
- Recommendation: 用 errgroup.SetLimit(n)(Go 1.20+),n 通常取 Redis 连接池大小(10-20)。
  此修改与 REV-007 的 errgroup 重构可合并完成。
- Action: follow-up

[Medium] Missing Slice Pre-allocation — Repeated Realloc in Batch Hot Path
- ID: REV-010 (original: PERF-001)
- Origin: uncertain
- Category: Performance
- Location: review_snippet.go:28
- Impact: make([]*User, 0) 零容量;每次 append 超出容量时触发 grow-and-copy(分配新数组 +
  拷贝旧元素)。len(userKeys) 已知为上界,应作为初始容量参数传入。
- Evidence: userList := make([]*User, 0) — 无容量参数;函数参数 userKeys []*UserKey 提供
  了已知上界。
- Recommendation: make([]*User, 0, len(userKeys))
- Action: follow-up

[Medium] N+1 Redis Round-trips — Batch Call Not Used
- ID: REV-011 (original: PERF-002)
- Origin: uncertain
- Category: Performance
- Location: review_snippet.go:39 (inside loop)
- Impact: N 次独立 redis.GetGuest 调用,即使并发执行,仍需 N 个网络往返。Redis MGET 或
  pipeline 可将其折叠为 1 次往返,节省 (N-1) × RTT 延迟;对 100 个 key,典型 datacenter RTT
  1ms 场景下节省约 99ms。还顺带消除 N 个 goroutine 的栈分配和调度开销。
- Evidence: for range userKeys { redis.GetGuest(ctx, u.Id) } — 无 MGet / Pipeline / 
  batch 调用。
- Recommendation: 收集所有 id → redis.MGetGuest(ctx, ids) 单次批量调用 → 过滤 nil 结果。
- Action: follow-up

[Medium] Named Return `users` Declared but Never Assigned — Shadow Local Variable
- ID: REV-012 (original: QUAL-001 / QUAL-002 — merged)
- Origin: uncertain
- Category: Quality
- Location: review_snippet.go:26,28,48
- Impact: 签名声明 users []*User 为具名返回,函数体立即用 userList := make([]*User, 0)
  遮蔽,users 从未被赋值,最后显式 return userList, nil。读者扫描函数时合理预期 users
  持有结果,实际上它是死变量。两套命名并存增加了维护者的认知负担。
- Evidence: L26 声明 users; L28 创建独立局部变量 userList; L44 向 userList append;
  L48 return userList, nil — users 整个函数生命周期内未被写入。
- Recommendation: 修复 REV-001(改为全匿名返回)后,删除具名返回,统一使用 userList。
- Action: follow-up

[Medium] Mutable Package-Level Variables as Dependencies
- ID: REV-013 (original: QUAL-005)
- Origin: uncertain
- Category: Quality
- Location: review_snippet.go:17,22 (redis, log globals)
- Impact: redis 和 log 作为可变包级 var,任何包可随时替换;并发测试无法注入 fake 而不
  修改全局状态(造成 flaky parallel test)。log 命名与标准库 `log` 冲突,读者误认为
  调用的是 stdlib,实际是自定义结构。
- Evidence: var redis = struct{...}{}; var log = struct{...}{}
- Recommendation: 通过 struct receiver 或函数参数注入依赖;logger 变量名改为 logger
  以消除与 stdlib 的命名冲突。
- Action: follow-up

[Low] Unused Loop Variable `i` — Compile Error Once Syntax Fixed
- ID: REV-014 (original: QUAL-003)
- Origin: uncertain
- Category: Quality
- Location: review_snippet.go:31
- Impact: `i` 声明但从未使用;Go 编译器在修复 REV-001 语法错误后会将其报告为编译错误。
- Evidence: for i, u := range userKeys — i 在函数体内无任何引用。
- Recommendation: for _, u := range userKeys
- Action: must-fix (compile blocker once REV-001 fixed)

[Low] `Id` Field Violates Go Initialisms Convention
- ID: REV-015 (original: QUAL-006)
- Origin: uncertain
- Category: Quality
- Location: review_snippet.go:9,13
- Impact: Go CodeReviewComments 要求首字母缩写全大写:ID 而非 Id。golangci-lint
  的 stylecheck/revive 会标记此项。
- Evidence: type UserKey struct { Id string }; type User struct { Id string }
- Recommendation: 改为 ID,并更新所有引用 u.Id → u.ID。
- Action: follow-up

---
Suppressed Items

[Suppressed] wg.Add inside goroutine (CONC Item 11)
- Reason: CONC agent confirmed wg.Add(1) 位于 L35(for 循环体内,goroutine 闭包之外)——
  正确位置。Anti-example "Add inside goroutine" 不适用。

[Suppressed] singleflight deduplication (CONC Item 13)
- Reason: 每个 goroutine 查询不同的 u.Id,无重复 key 并发场景,不适用。

[Suppressed] errors.Is/As 使用 (ERR Item 4)
- Reason: err != nil 是标准 nil guard,非 sentinel 比较,False positive。

[Suppressed] MD5/math/rand (SEC)
- Reason: 无 database/sql、os/exec、http.Handle、Sprintf.*SELECT 等模式,go-security-reviewer
  未派发。

---
Execution Status

- Go version: Unknown(无 go.mod,loop variable capture 按保守方式标注 Go < 1.22 适用)
- Compile pre-check: FAIL — 2 compile errors caught at L19, L35(已作 REV-001/002 上报)
  → sub-agents 收到指令跳过这两处,消除预估 6+ 条冗余原始 Finding
- Dispatch validation grep: 4 patterns confirmed (make([]), append(, go func, sync.);
  no override needed — Phase 1-4 已覆盖所有触发
- Per-agent grep audit:
  - go-concurrency-reviewer: Grep pre-scan 7/13 items hit, 6 confirmed as findings
  - go-performance-reviewer: Grep pre-scan 2/11 items hit, 2 confirmed as findings
  - go-error-reviewer: Grep pre-scan 2/12 items hit, 1 confirmed (1 semantic-only confirmed)
  - go-quality-reviewer: Grep pre-scan 4/8 items hit, 1 confirmed (3 rejected as FP)
  - go-logic-reviewer: Semantic-only skill: 10/10 items evaluated, 3 findings
- Raw findings from agents: 19 条(CONC ×6, PERF ×2, ERR ×2, QUAL ×6, LOGIC ×3)
- Compile errors from Lead: 2 条(非 agent 原始 Finding)
- Merged total: 15 条(4 merges 消除 6 条重复;agent 层重复率 31.6% vs 上轮 43.5%)
- Excluded (generated): None

---
Risk Acceptance / SLA
- High (REV-001, REV-002): Immediate — compile blockers, binary cannot be produced
- High (REV-003 through REV-008): must-fix before merge
- REV-014 (Low/Compile): must-fix once REV-001 fixed
- Medium (REV-009 through REV-013): follow-up within 2 weeks
- Low (REV-015): discretionary

---
Residual Risk / Testing Gaps

1. Verification gaps: go test -race 未运行(snippet 未编译);修复编译错误后强烈建议运行
   go test -race ./... 验证 REV-004(data race)
2. No test coverage: nil-skip path、error path、并发结果正确性均无 unit test
3. Volume-cap overflow: None — 15 条 Finding,恰好达到 Strict 模式上限
4. Redis MGetGuest API: PERF-002 建议的批量 API 需确认 Redis client 是否支持;若不支持
   需改用 Pipeline 模式

---
Summary

8 High / 5 Medium / 2 Low(含 2 条 Lead 直接上报的编译错误)。

函数从根本上无法正常工作:2 处编译错误使其无法构建(REV-001/002);即便编译通过,
缺少 wg.Wait()(REV-003)使其始终返回空切片;循环变量捕获(REV-005)使 goroutine
查询错误用户;并发 append(REV-004)引发 data race;所有错误被吞噬(REV-007)使
失败不可见。推荐重构路径:用 errgroup.WithContext + SetLimit(n) 替换整个手工
WaitGroup + 共享 slice 结构,一次性解决 REV-003/004/007/009。

12.6.3 结果对比

指标旧协议(§10,无优化)新协议(本次验证)变化
派发 Agent 数55不变
Agent 原始 Finding 数2319-4
Lead 直接上报(编译错误)02+2(Lead 自行处理)
总输入 Finding 数2321-2
Agent 层去重后保留1313不变
Lead 编译 Finding 并入后总计1315+2(新发现)
Agent 层重复率10/23 = 43.5%6/19 = 31.6%-11.9 pp

重复率下降来源:旧协议中,continue 编译错误和混合返回值两处编译错误被 ERR、QUAL、LOGIC 三个 Agent 各自报出,产生约 6 条冗余原始 Finding。新协议中,编译预检将这些错误提前截获,sub-agent 的 prompt 中明确排除,这 6 条冗余 Finding 被从源头消除。

12.6.4 意外收获:Agent 解放后发现更多问题

编译预检带来了一个预期之外的收益:sub-agent 从"处理编译错误"的负担中解脱出来后,语义分析质量有所提升,本轮新发现了 3 个旧协议遗漏的 Finding:

Finding内容发现 Agent
REV-008Nil *User 未做 nil 守卫即 append,调用方潜在 panicERR Agent
REV-013可变包级全局变量作为依赖,并发测试不可注入QUAL Agent
REV-015Id 字段违反 Go 首字母缩写规范(应为 IDQUAL Agent

这三处在旧协议中同样存在于代码里,但 QUAL Agent 当时将 token 消耗在编译错误的质量维度解读上,ERR Agent 也有类似分心效应,导致这些问题被遗漏。

这一现象与 §2.1 提出的注意力稀释假说在更小粒度上得到了印证:不仅跨维度的 High Finding 会压制低优先级检查,同一 Agent 内部的"伪任务"(即应由 Lead 承担的编译错误上报)同样会挤占真正需要语义推理的 checklist 项的注意力。

12.6.5 与预估值的对照

指标预估值(§12.3)实测值评估
策略 A 后重复率~25%31.6%接近,略高于预估
策略 B 后重复率~15%未应用(Strict 模式不收窄)符合设计:Strict 下刻意保留交叉验证
最终合并 Finding 数15(较旧协议 +2)超出预期:新发现 Finding

策略 A 实测重复率(31.6%)略高于预估(25%),原因在于旧协议的 10 条重复中有 5 条是跨维度语义交叉(wg.Wait 缺失同时被 CONC 和 LOGIC 报出),这部分在 Strict 模式下不做收窄,因此仍保留。这是合理的:Strict 模式下这 5 条重复本身就是"有价值的跨域交叉验证",不应计入浪费。

若改为 Standard 模式(应用策略 B 收窄 scope),则 LOGIC-001/002/003 这 3 条与 CONC/ERR 重复的 Finding 将被削减,预计 Agent 层重复率进一步降至约 15%,与 §12.3 预估一致。

12.6.6 小结

发现问题(§12.1)
  旧协议 43.5% 的 Finding 重复率,近一半 token 浪费在冗余报告上
        ↓
根因分析(§12.1)
  两类结构性重复:编译错误被多 Agent 重复报出;
  Logic Agent checklist 与专职 Agent 存在跨维度交叉
        ↓
修复方案(§12.212.4)
  策略 A:编译预检,Lead 先截获,sub-agent 跳过已知位置
  策略 B:条件性 scope 收窄(Standard 模式)
  策略 C:模型分层(已有)
  深度分层:引入 Lite / Standard / Strict 三级审查深度
        ↓
验证(§12.6,本节)
  相同代码,新协议首次运行
  Agent 层重复率:43.5% → 31.6%(Strict 下策略 A 单独贡献 -11.9 pp)
  新发现 Finding:+3(agent 从编译错误负担中解脱后的语义分析质量提升)
  Strict 模式下刻意保留跨维度交叉(~16%),符合设计意图