核心结论:当一个 Skill 覆盖的审查维度过多时,High 级别 Findings 会抢占模型注意力,导致其他维度的 checklist 被跳过——即使 Skill 明确规定"不可跳过"。解决方案不是写更好的提示词,而是改变架构:将重度 Skill 拆分为垂直领域的轻量 Skill,每个由独立子代理在隔离的上下文中加载执行;主会话加载 go-review-lead Skill 仅负责分诊和汇总(平台限制决定编排不能在子代理中执行)。
想了解更多 Skill 的最佳实践,参考:github.com/johnqtcg/aw…。
这个项目的核心目标不是”展示 prompt 怎么写”,而是回答三个更难的问题:
- 高质量 Skill 到底应该如何设计?
- 写出来之后如何证明它真的有效?
- 如何把它融入日常开发与团队工作流?
欢迎大家提交 PR,如果觉得有用,请给 star。
目录
- 问题:注意力稀释
- 根因分析
- 解决方案:Skill-Agent 协作架构
- Skill 与 Agent 的关系
- 实施指南:拆分 go-code-reviewer
- Lead Agent 的分诊机制
- 常见问题
- 首次改造与验证——发现新的问题
- 第二轮改进:Grep-Gated 执行协议
- 最终验证通过——完整审查输出
- 追加验证:跨域案例 — ListLayout
- 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-001 | High | continue 在 goroutine 内,编译错误 |
| REV-002 | High | 并发 append 无锁,数据竞争 |
| REV-003 | High | 缺少 wg.Wait(),goroutine 被遗弃 |
| REV-004 | High | 循环变量捕获(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...) │ ← 被挤压
│ ├── 错误处理规则(wrap、nil 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?
- 复杂度不可接受:编排流程是流水线+并行的混合模式——作用域识别→审查深度选择→编译预检→四阶段分诊→动态派发→去重汇总。写成临时 prompt 篇幅远超可接受范围。
- 无法复用:每次代码审查都要重新提供这段复杂 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 不是替代,是互补
| 维度 | Skill | Agent |
|---|---|---|
| 本质 | 知识包——工作流、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 / sync → Concurrency 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 模型(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 为例:
- 将同一段代码分别交给拆分后的 Multi-Agent 架构审查
- 检查 Performance Agent 是否报告了 Slice Pre-allocation 问题
- 检查所有 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 总数 | 2 | 6 |
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 不上报 Lead | Lead 只收 FOUND 结果 | 减少 Lead 汇总时的噪音,同时保留 grep 审计行供覆盖率核查 |
| snippet 模式 | 将代码片段写入 $TMPDIR/review_snippet.go | 确保裸片段也能执行 grep,不降级为纯语义模式 |
覆盖率统计:7 个 Skill 共 86 条 checklist 项,其中 65 条(75%)可以 grep 化:
| Skill | 总条数 | Grep 化 | 纯语义 |
|---|---|---|---|
| go-concurrency-review | 14 | 13 | 1 |
| go-performance-review | 12 | 10 | 2 |
| go-error-review | 12 | 12 | 0 |
| go-security-review | 16 | 14 | 2 |
| go-quality-review | 12 | 8 | 4 |
| go-test-review | 10 | 8 | 2 |
| go-logic-review | 10 | 0 | 10 |
| 合计 | 86 | 65 (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
验证通过标准:
- Slice Pre-allocation 被正确发现并作为正式 Finding 上报
- Unbounded Goroutine Spawning 被正确发现
- 所有原始 8 个 Findings 无退化
- 每个 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] | 同为 H | CONC-002 证据更详细;REV-004 |
| 循环变量捕获 | CONC-003 [H] + LOGIC-003 [H] | 同为 H | CONC-003 为主;REV-005 |
goroutine panic 无 recover | CONC-004 [H] | 仅 1 条 | 直接收录;REV-006 |
| 错误被吞噬 | ERR-004 [H] + CONC-006 [M] + LOGIC-005 [M] | H vs M → 保持 H | ERR-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 → 促升 M | LOGIC-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-001 | High | Security | fmt.Sprintf 拼接 SQL + GORM 单参数 .Where() 构成 SQL injection 注入面 |
| REV-002 | High | Logic | Find 与 Count 无事务包裹,并发写入可使 total 与 len(layouts) 静默分叉 |
| REV-003 | High | Logic | 返回 (layouts, total) 的分页契约,但函数无 limit/offset 参数,Count 在当前实现中逻辑冗余 |
| REV-004 ★ | Medium | Quality | 函数执行 DB I/O 但签名无 ctx context.Context,查询不可取消(新增 Item 13 触发) |
| REV-005 ★ | Medium | Performance | 查询顺序倒置(Find 先于 Count),缺少 Count-First zero-guard(新增 Item 13 触发) |
| REV-006 | Medium | Error | 裸 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(并发版) | 并发 / goroutine | 7H + 6M | 数据竞争、缺少 wg.Wait()、unbounded goroutine、Slice Pre-allocation |
| ListLayout | 安全 / ORM / 设计 | 3H + 3M | SQL 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。如果存在编译错误:
- Lead Agent 自行报告编译错误为 High 级别 Finding(这不需要领域专业知识)
- 在每个 sub-agent 的 dispatch prompt 中附加:"编译错误已由 Lead 报告,请跳过以下位置:[列表]"
- 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 项:
| 已派发的专职 Agent | go-logic-reviewer 跳过的项 |
|---|---|
| go-concurrency-reviewer | goroutine 生命周期 / 同步正确性 |
| 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-agent | Haiku | 在 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 也不满足 Strict | 2~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 func、sync.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 数 | 5 | 5 | 不变 |
| Agent 原始 Finding 数 | 23 | 19 | -4 |
| Lead 直接上报(编译错误) | 0 | 2 | +2(Lead 自行处理) |
| 总输入 Finding 数 | 23 | 21 | -2 |
| Agent 层去重后保留 | 13 | 13 | 不变 |
| Lead 编译 Finding 并入后总计 | 13 | 15 | +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-008 | Nil *User 未做 nil 守卫即 append,调用方潜在 panic | ERR Agent |
| REV-013 | 可变包级全局变量作为依赖,并发测试不可注入 | QUAL Agent |
| REV-015 | Id 字段违反 Go 首字母缩写规范(应为 ID) | QUAL 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.2–12.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%),符合设计意图