2026-07-04 16:08:14
<blockquote><p>「任何傻瓜都能写出计算机能懂的代码。好的程序员写出人能懂的代码。」——Martin Fowler</p></blockquote><p>你有没有过这种感觉:打开一个文件,还没读懂逻辑,先皱起了眉头。说不上哪儿错了,但就是觉得"不对劲"。</p><p>这种"不对劲",有个专门的名字——<strong>代码坏味道(Code Smell)</strong>。</p><img src="/2026/07/04/ai-skill-sniffs-out-code-smells/image-20260703065726678.png" class=""><p>它不是 bug,代码跑得好好的;它也不是编译错误,测试全绿。它只是一种<strong>表面的征兆</strong>,暗示底下可能藏着更深的问题。就像厨房里飘来一丝馊味,东西还没坏透,但你知道该去冰箱里翻一翻了。</p><p>这篇文章讲三件事:这个词是怎么来的、坏味道到底有哪几类、以及——我怎么用一个 AI 技能,把一个真实开源项目的核心代码"闻"了个遍。</p><hr><span id="more"></span><h2 id="一、一个厨房比喻,是怎么变成行业术语的"><a href="#一、一个厨房比喻,是怎么变成行业术语的" class="headerlink" title="一、一个厨房比喻,是怎么变成行业术语的"></a>一、一个厨房比喻,是怎么变成行业术语的</h2><p>"代码坏味道"这个词,比很多人想的要年轻。</p><p>它的源头是 1990 年代末的 <strong>WardsWiki</strong>(c2.com,程序员圈最早的维基之一)。<strong>Kent Beck</strong>——极限编程和 JUnit 的作者——在帮 <strong>Martin Fowler</strong> 写《重构》这本书时,随口造了这个词。Fowler 自己在博客里写得很直白:</p><blockquote><p>「A code smell is a surface indication that usually corresponds to a deeper problem in the system. The term was first coined by Kent Beck while helping me with my Refactoring book.」<br>(坏味道是系统表面的一个信号,往往对应着更深层的问题。这个词是 Kent Beck 帮我写《重构》时造的。)</p></blockquote><p>有意思的是,据 c2 wiki 的记载,Beck 造这个词时的灵感,部分来自同事 Massimo Arnoldi 的"鼻子"——一种对烂代码的直觉嗅觉。</p><p>真正让这个词火遍全球的,是 <strong>1999 年出版的《Refactoring: Improving the Design of Existing Code》</strong>。Fowler 在书里专门用一章(和 Beck 合写)罗列了一份"坏味道目录",并给每种味道配了对应的"重构手法"。从此,"我觉得这段代码有点臭"这种模糊的工程师直觉,第一次有了<strong>可以命名、可以分类、可以按方子治疗</strong>的框架。</p><img src="/2026/07/04/ai-skill-sniffs-out-code-smells/image-20260703065559107.png" class=""><p>为什么这个比喻能活到今天?因为它精准地抓住了一个工程真相:</p><ul><li><strong>坏味道 ≠ 错误。</strong> 有味道的代码照样能上线、能赚钱。</li><li><strong>坏味道是概率信号,不是判决。</strong> 闻到味道,你该去查,但查完可能发现没事。</li><li><strong>味道会累积。</strong> 单个坏味道无所谓,一屋子坏味道,就是所谓的"技术债"和"大泥球"。</li></ul><p>二十多年后的今天,<strong>这个概念比 1999 年更重要了</strong>。因为写代码的,多了一位新玩家——AI。AI 生成代码的速度是人的几十倍,坏味道的<strong>产量</strong>也跟着水涨船高。"<strong>闻味道</strong>"这件事,从一门手艺,正在变成一道必须自动化的工序。</p><hr><h2 id="二、坏味道的家谱:从-Fowler-的-22-种,到今天的-8-大类-50"><a href="#二、坏味道的家谱:从-Fowler-的-22-种,到今天的-8-大类-50" class="headerlink" title="二、坏味道的家谱:从 Fowler 的 22 种,到今天的 8 大类 50+"></a>二、坏味道的家谱:从 Fowler 的 22 种,到今天的 8 大类 50+</h2><p>Fowler 的原始目录里有 <strong>22 种坏味道</strong>,refactoring.guru 后来把它们整理成经典的<strong>五大类</strong>。这套分类是所有后来者的地基,先看它:</p><table><thead><tr><th>类别</th><th>坏味道</th><th>主要重构手法</th></tr></thead><tbody><tr><td><strong>臃肿类</strong>(Bloaters)</td><td>过长方法、过大的类、基本类型偏执、过长参数列表、数据泥团</td><td>Extract Method / Extract Class / Introduce Parameter Object</td></tr><tr><td><strong>OO 滥用类</strong></td><td>Switch 语句、临时字段、被拒绝的遗赠、异曲同工的类</td><td>Replace Conditional with Polymorphism / Replace Inheritance with Delegation</td></tr><tr><td><strong>变更阻碍类</strong></td><td>发散式变化、霰弹式修改、平行继承体系</td><td>Extract Class / Move Method</td></tr><tr><td><strong>冗余类</strong>(Dispensables)</td><td>注释(本该自说明)、重复代码、冗赘类、纯数据类、死代码、夸夸其谈未来性</td><td>Extract Method / Inline Class / 直接删除</td></tr><tr><td><strong>耦合类</strong>(Couplers)</td><td>依恋情节、狎昵关系、消息链、中间人</td><td>Move Method / Hide Delegate / Remove Middle Man</td></tr></tbody></table><img src="/2026/07/04/ai-skill-sniffs-out-code-smells/image-20260703071026940.png" class=""><p>这五类的核心逻辑是「<strong>症状 → 处方</strong>」:你先嗅到一个症状(比如"过长方法"),目录再告诉你该用哪几种手法去治。这套映射关系明确、能机械执行——<strong>而"能机械执行"这四个字,恰恰是 AI Agent 能接得住的地方。</strong></p><p>但 Fowler 的目录写于 2000 年,它盯的都是<strong>函数和类这个尺度</strong>的问题。今天的软件更复杂,坏味道也长出了新的分支。我用的这个 <code>smell</code> 技能,把视野往上抬了一层,也往下探了一层,整理成 <strong>8 大类、50+ 种</strong>:</p><table><thead><tr><th>类别</th><th>代表坏味道</th></tr></thead><tbody><tr><td><strong>架构</strong></td><td>大泥球、分布式单体、贫血模型、CQRS 滥用、层边界违反、过度分层、过度抽象、"未来主义"架构</td></tr><tr><td><strong>耦合</strong></td><td>循环依赖、内容耦合、公共耦合(全局状态)、印记耦合</td></tr><tr><td><strong>内聚</strong></td><td>上帝对象、霰弹式修改、依恋情结、数据泥团、发散式变化</td></tr><tr><td><strong>设计</strong></td><td>抽象泄露、静态粘连、服务定位器滥用、SOLID 违反、Switch 类型分支</td></tr><tr><td><strong>代码</strong></td><td>重复代码、长方法、基本类型偏执、魔数魔串、死代码、深层嵌套(箭头反模式)</td></tr><tr><td><strong>测试</strong></td><td>零测试覆盖、测试-实现耦合、不稳定测试</td></tr><tr><td><strong>命名</strong></td><td>模糊命名(Manager/Helper/Util 满天飞)、命名不一致</td></tr><tr><td><strong>复杂度</strong></td><td>嵌套循环 O(n²)、N+1 查询、重复线性扫描、循环内排序、渲染重复计算、数据结构选错</td></tr><tr><td>注意最后两类——<strong>「测试」和「复杂度」是 Fowler 原目录里没有的</strong>。它们为什么被加进来?因为这正是 <strong>AI 时代新暴露的坑</strong>:</td><td></td></tr></tbody></table><ul><li>AI 生成的代码,经常<strong>零测试覆盖</strong>——它写得又快又像模像样,但一行测试没有。</li><li>AI 也经常埋下 <strong>N+1 查询、循环内排序</strong>这类"低速炸弹"——小数据量下一切正常,一上量就爆炸,代码评审时一眼还看不出来。</li></ul><p>换句话说,坏味道的家谱一直在生长。从 Kent Beck 的一个厨房比喻,到 Fowler 的 22 种,再到今天覆盖架构与性能的 50+ 种——<strong>每一次扩张,都是软件复杂度提升后,工程界给自己补的一课。</strong></p><hr><h2 id="三、把-嗅觉-装进-Agent:-smell-技能是怎么工作的"><a href="#三、把-嗅觉-装进-Agent:-smell-技能是怎么工作的" class="headerlink" title="三、把"嗅觉"装进 Agent:/smell 技能是怎么工作的"></a>三、把"嗅觉"装进 Agent:<code>/smell</code> 技能是怎么工作的</h2><p>概念讲完了,来看工具。<code>/smell</code> 是一个可以直接在命令行 AI Agent(Claude Code / Ducc 这类)里调用的技能,一句话概括它的定位:</p><blockquote><p><strong><code>/smell</code> 负责诊断,<code>/refactor</code> 负责治疗。一个出报告排优先级,一个照 Fowler 手法一处处动手术。</strong></p></blockquote><img src="/2026/07/04/ai-skill-sniffs-out-code-smells/image-20260703071315310.png" class=""><p>面对一个 AI 攒出来的几万行代码库,最难的往往不是"怎么改",而是**"先改哪"**。哪个模块烂得最厉害?是架构错了,还是只有某个函数太长?<code>/smell</code> 回答的就是这个问题。</p><p>它的工作流分四步:</p><p><strong>1. 确定范围。</strong> 全项目扫、还是只扫某个模块、还是只看 <code>git diff</code> 的最近改动。大项目默认只扫改动,小项目全量扫。</p><p><strong>2. 并行取证。</strong> 派出多个 Explore 子 Agent,用 <code>find</code> / <code>grep</code> 同时跑七类扫描:项目结构、依赖关系、模块内聚、模式识别、测试覆盖、命名清晰度、复杂度热点。技能内置了一整套<strong>检测启发式规则</strong>,比如:</p><ul><li>单文件 > 500 行、公共方法 > 20 个 → 疑似<strong>上帝对象</strong></li><li>循环体里出现 <code>fetch</code>/<code>query</code>/<code>exec</code> → 疑似 <strong>N+1 查询</strong></li><li>循环里用 <code>.includes()</code>/<code>.indexOf()</code> 做查找 → <strong>重复线性扫描</strong>,本该用 Set/Map</li><li>满屏的 <code>Manager</code>/<code>Helper</code>/<code>Util</code> → <strong>模糊命名</strong></li></ul><p><strong>3. 生成结构化报告。</strong> 不是简单列问题,而是产出一份带优先级的 Markdown:</p><ul><li>执行摘要 + 整体健康评估</li><li>检测到的架构风格,对比它"本该是"的样子</li><li>按 🔴 严重 / 🟡 警告 / 🔵 建议 三级分类的发现</li><li>依赖图分析 + 模块健康评分卡</li><li>8 大类坏味道的分布统计</li><li><strong>一份分「立即 / 短期 / 长期」的重构路线图</strong></li></ul><p><strong>4. 落盘 + 汇报。</strong> 报告存到 <code>tasks/smell-report-[时间戳].md</code>,同时给你一份口头摘要。</p><p>最有价值的是那份路线图。它把"这代码是不是该重构了"这种主观焦虑,变成了<strong>可量化的行动清单</strong>:先动哪、后动哪,一目了然。改完再跑一遍,健康评分涨了没有,前后一对比就知道这次重构有没有白干。</p><p>安装也简单:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add smallnest/goal-workflow --skill smell</span><br></pre></td></tr></table></figure><p>然后直接打 <code>/smell</code>,或者用自然语言触发:「找出代码坏味道」「检测架构反模式」「分析代码复杂度」。</p><hr><h2 id="四、实战:我用-smell-扫了-Gitlawb-zero-这个开源项目"><a href="#四、实战:我用-smell-扫了-Gitlawb-zero-这个开源项目" class="headerlink" title="四、实战:我用 /smell 扫了 Gitlawb/zero 这个开源项目"></a>四、实战:我用 <code>/smell</code> 扫了 <code>Gitlawb/zero</code> 这个开源项目</h2><p>光说不练假把式。我挑了一个真实的开源项目下手:<a href="https://github.com/Gitlawb/zero"><strong>Gitlawb/zero</strong></a>——一个用 Go 写的终端 AI 编程 Agent("A terminal coding agent you own"),支持 25+ 家模型提供商,代码量不小。它昨天刚发布,比较适合用来学习。</p><p>先上体检数据:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">项目:Gitlawb/zero</span><br><span class="line">语言:Go 1.25(toolchain 1.26)</span><br><span class="line">Go 文件总数:974</span><br><span class="line">代码总行数:~25 万行</span><br><span class="line">测试文件:500 个 | 非测试文件:474 个</span><br></pre></td></tr></table></figure><img src="/2026/07/04/ai-skill-sniffs-out-code-smells/image-20260703062722697.png" class=""><p>代码量非常的大,我先用它分析核心的agent的代码,下面是分析结果的总结:</p><h3 id="🟢-先说好的:这项目-体质-其实不错"><a href="#🟢-先说好的:这项目-体质-其实不错" class="headerlink" title="🟢 先说好的:这项目"体质"其实不错"></a>🟢 先说好的:这项目"体质"其实不错</h3><p>扫完第一个惊喜是——<strong>测试文件(500)比源码文件(474)还多</strong>。测试-代码比超过 1:1,这在开源项目里相当罕见。对照 <code>/smell</code> 的「测试」类坏味道(零测试覆盖、测试-实现耦合),zero 在这一维度上健康度很高。</p><p>第二个惊喜:<strong>全项目只有 1 处 TODO/FIXME 注释</strong>。"注释当除臭剂"(用 TODO 掩盖烂代码)这个坏味道,在这里基本不存在。目录结构也清晰——<code>cmd/</code> 放入口、<code>internal/</code> 按职责分了 60+ 个子包,不是"大泥球"。</p><h3 id="🔴-但也闻到了明确的坏味道"><a href="#🔴-但也闻到了明确的坏味道" class="headerlink" title="🔴 但也闻到了明确的坏味道"></a>🔴 但也闻到了明确的坏味道</h3><p><strong>1. 上帝对象(God Object)—— 🔴 严重</strong></p><p>最刺鼻的一处:<code>internal/tui/model.go</code>,<strong>4657 行</strong>,单个 <code>Model</code> 结构体扛着 <strong>173 个字段</strong>、<strong>111 个方法</strong>。</p><img src="/2026/07/04/ai-skill-sniffs-out-code-smells/image-20260703071243737.png" class=""><p>对照 <code>smell</code> 技能的启发式(> 500 行、> 20 个公共方法即疑似上帝对象),这个文件超标了 <strong>9 倍</strong>。它是典型的 TUI "全知全能对象"——把整个终端界面的所有状态、所有行为都塞进了一个结构体。</p><ul><li><strong>违反原则</strong>:单一职责(SRP)</li><li><strong>处方</strong>:<code>Extract Class</code>——把 173 个字段按关注点分组(会话状态、渲染状态、输入状态、模型选择……),拆成若干个内聚的小结构体,<code>Model</code> 只做组合。</li></ul><p><strong>2. 长文件扎堆 —— 🟡 警告</strong></p><p>除了那个 4657 行的巨无霸,超过 800 行的源文件还有 <strong>23 个</strong>,重灾区集中在 <code>internal/tui/</code> 和 <code>internal/agent/</code>:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">4657 internal/tui/model.go ← 上帝对象</span><br><span class="line">2736 internal/agent/loop.go ← Agent 主循环,84 个函数</span><br><span class="line">2493 internal/tui/rendering.go</span><br><span class="line">1709 internal/tui/onboarding.go</span><br><span class="line">1693 internal/tui/provider_wizard.go</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p><code>internal/agent/loop.go</code>(2736 行 / 84 个函数)是 Agent 的核心循环,长度可以理解,但也值得警惕<strong>发散式变化</strong>——一个文件因为太多不同原因被反复修改。</p><p><strong>3. 潜在复杂度热点 —— 🔵 建议(需人工确认)</strong></p><p>启发式扫描发现:</p><ul><li><strong>307 处</strong>"循环内做线性查找"的疑似点(<code>for range</code> 内出现 <code>strings.Contains</code>/<code>slices.Contains</code>/<code>.Index()</code>)</li><li><strong>63 处</strong>"循环内排序"疑似点</li></ul><p>这两个数字需要辩证看——<code>/smell</code> 技能本身也强调 <strong>"What NOT to Flag"</strong>:如果这些循环跑在冷路径(启动、配置加载)或数据量极小(N < 100),就是可读性优先的合理取舍,不值得改。但如果落在<strong>热路径</strong>(比如每次渲染、每个 token 都跑一遍),那就是实打实的性能坑,该换 Set/Map 或把排序提到循环外。<strong>这一步机器只能标记,真正的判断得人来做。</strong></p><h3 id="📋-给-zero-的重构路线图"><a href="#📋-给-zero-的重构路线图" class="headerlink" title="📋 给 zero 的重构路线图"></a>📋 给 zero 的重构路线图</h3><p><code>/smell</code> 的收尾,是把发现整理成一份可执行的路线图:</p><ul><li><strong>立即可做</strong>:拆分 <code>tui/model.go</code> 这个 4657 行的上帝对象——收益最高、风险可控(有 500 个测试兜底)。</li><li><strong>短期(1-3 月)</strong>:审查 23 个超长文件里的 <code>tui/</code> 一组,按关注点做 Extract Class。</li><li><strong>长期</strong>:给 307 处线性扫描 + 63 处循环内排序做一次热路径分析,确认哪些真在性能关键路径上,针对性优化数据结构。</li></ul><p><strong>整体健康评估:🟡 良好偏上。</strong> 架构清晰、测试充分、几乎无死代码,这是它的底子;主要问题集中在 TUI 层的"上帝对象"和长文件——这也是 TUI 类项目的通病,状态多、交互密,天然容易膨胀。</p><p><code>/smell</code>技能除了生成总结外,还会生成一个详细的分析报告:</p><img src="/2026/07/04/ai-skill-sniffs-out-code-smells/image-20260703063103024.png" class=""><img src="/2026/07/04/ai-skill-sniffs-out-code-smells/image-20260703063126184.png" class=""><hr><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>从 Kent Beck 1990 年代末的一个厨房比喻,到 Fowler 的 22 种坏味道目录,再到今天覆盖架构与性能的 50+ 种、并且能被 AI Agent 自动扫描的技能——<strong>"闻代码"这件事,走过了从直觉、到方法论、到自动化的完整二十年。</strong></p><p>AI 让写代码变快了,但也让坏味道产量暴增。这时候,一个能自动"闻味道、排优先级、给路线图"的工具,价值不在于它多聪明,而在于它把"这代码是不是该收拾了"这个含糊的焦虑,变成了一份<strong>你今天下午就能动手的清单</strong>。</p><p>代码必须变干净——而变干净的第一步,是先知道它哪儿臭了。</p><hr><p><strong>Sources / 参考资料:</strong></p><ul><li><a href="https://martinfowler.com/bliki/CodeSmell.html">Code Smell — Martin Fowler</a></li><li><a href="https://en.wikipedia.org/wiki/Code_smell">Code smell — Wikipedia</a></li><li><a href="http://c2.com/ppr/wiki/WikiPagesAboutRefactoring/CodeSmell.html">CodeSmell — c2 WardsWiki</a></li><li>《Refactoring: Improving the Design of Existing Code》, Martin Fowler & Kent Beck, 1999</li><li>refactoring.guru 坏味道与重构手法目录</li><li>《AI时代的软件工程》第 23 章「重构:AI 时代的代码进化」</li><li><code>smell</code> 技能:<a href="https://github.com/smallnest/goal-workflow">https://github.com/smallnest/goal-workflow</a></li><li>分析对象:<a href="https://github.com/Gitlawb/zero">https://github.com/Gitlawb/zero</a></li></ul>
2026-06-30 23:15:45
<p>这是百度网络监控工具 nettools 开源系列的第四篇。前三篇分别介绍了 bitflip/baize(UDP 丢包与改包检测工具和Agent)、lidar(TCP SYN 端口可达性探测),它们解决的都是「服务器之间」「点到点」的探测问题——前提是:探测机和被探测对象,至少有一端在我们手里。</p><p>但有一类设备,我们既无法在它上面装 agent,也没法在它对面的机房里放一台探测机。这就是今天的主角 evr 要解决的困境。</p><p>这也是我更深入的了解网络包的构造,把网络探测玩出花了来,对我的网络编程的功力大增的一个很好的场景。</p><blockquote><p>项目地址:<a href="https://github.com/baidu/nettools">https://github.com/baidu/nettools</a><br>文档:<a href="https://nettools.rpcx.io/">https://nettools.rpcx.io</a></p></blockquote><h2 id="一、evr-探测的困境:探测机进不去客户机房"><a href="#一、evr-探测的困境:探测机进不去客户机房" class="headerlink" title="一、evr 探测的困境:探测机进不去客户机房"></a>一、evr 探测的困境:探测机进不去客户机房</h2><p>先说一个真实的场景。</p><p>百度有大量的云客户,我们提供的 <strong>EVR(Edge Virtual Router,边缘虚拟路由器)</strong> 设备作为客户侧网络接入百度云网络的边界节点。EVR 往上连百度的骨干/城域网络,往下连客户自己的虚拟网络(VXLAN overlay)。</p><blockquote><p>EVR - 边缘虚拟路由器,通常用于在虚拟化环境中实现路由功能。EVR 位于网络的边缘,用于连接内部网络和外部网络(如客户机房)。</p></blockquote><p>现在问题来了:我们需要监控「百度网络 → EVR」这一段链路的健康度——有没有丢包、延迟多大、有没有改包。按照前几个工具的套路,我们的方案应该是:</p><ul><li>在 EVR 设备上装个 agent?—— <strong>不行</strong>。EVR 是网络设备/客户侧设备,我们没有权限往里塞监控程序。</li><li>在 EVR 对面(客户机房内)放一台探测机,做点到点探测?—— <strong>更不行</strong>。那是客户的机房,正常情况我们不可能在客户的物理环境里申请一台探测机常驻。</li></ul><p>lidar 那一套「发 SYN,靠对端内核 TCP 协议栈自动回 SYN-ACK/RST」的思路,在这里也不灵——EVR 不是一台服务器,它不会帮你跑 TCP 协议栈三次握手,或者说不允许我们高频的探测。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"> 百度侧 边缘设备</span><br><span class="line">┌──────────┐ ┌──────────────────────┐</span><br><span class="line">│ 探测机 │ ───── ??? ─────► │ EVR 设备 │</span><br><span class="line">│ (我们的) │ │ (装不了 agent) │</span><br><span class="line">└──────────┘ │ 对面也放不了探测机 │</span><br><span class="line"> └──────────────────────┘</span><br></pre></td></tr></table></figure><img src="/2026/06/30/baidu-network-monitor-evr-vxlan-probe/image-20260630042946520.png" class=""><p>困境的本质是:<strong>被探测对象不可控,且它对面也无法部署探测机。</strong> 我们需要一个 <strong>「单边」</strong> 就能完成的探测方案——只在百度侧放一台机器,让 EVR 设备自己「帮我们把包送回来」。</p><p>答案藏在 EVR 设备的工作原理里:它是一个 <strong>VXLAN VTEP(VXLAN Tunnel End Point)</strong>。而 VTEP 有一个非常好用的特性——它会忠实地按照内层 IP 头转发解封后的内层帧。这就给了我们「构造一个会被反射回来的 VXLAN 包」的可能。</p><p>要理解这个技巧,得先看懂 VXLAN 的包结构。</p><span id="more"></span><h2 id="二、VXLAN:把二层帧塞进-UDP-里"><a href="#二、VXLAN:把二层帧塞进-UDP-里" class="headerlink" title="二、VXLAN:把二层帧塞进 UDP 里"></a>二、VXLAN:把二层帧塞进 UDP 里</h2><p>VXLAN(Virtual Extensible LAN,RFC 7348)是数据中心 overlay 网络的事实标准。它要解决的核心问题是:传统 VLAN 只有 12 位 VLAN ID,最多 4096 个二层网络,在大规模多租户云环境里完全不够用。</p><p>VXLAN 的做法简单粗暴又有效:<strong>把一个完整的二层以太网帧,整个塞进一个 UDP 数据报里</strong>,通过三层网络传输。这样原本受限于物理二层域的网络,可以跨越任意三层网络延展,VNI(VXLAN Network Identifier)有 24 位,支持约 1600 万个 overlay 网络。</p><img src="/2026/06/30/baidu-network-monitor-evr-vxlan-probe/image-20260630053211131.png" class=""><p>它的包结构从外到内是这样的:</p><img src="/2026/06/30/baidu-network-monitor-evr-vxlan-probe/vxlan-packet.png" class=""><p>一层层拆开看:</p><table><thead><tr><th>层</th><th>大小</th><th>关键字段</th><th>作用</th></tr></thead><tbody><tr><td>外层 Ethernet</td><td>14 B</td><td>物理 MAC</td><td>underlay 二层转发</td></tr><tr><td>外层 IP</td><td>20 B</td><td>src / dst IP</td><td>underlay 三层路由(VTEP 之间)</td></tr><tr><td>外层 UDP</td><td>8 B</td><td><strong>dport = 4789</strong></td><td>VXLAN 标准端口,VTEP 据此识别</td></tr><tr><td><strong>VXLAN header</strong></td><td>8 B</td><td>flags + <strong>VNI(24 位)</strong></td><td>标识 overlay 网络</td></tr><tr><td>内层 Ethernet</td><td>14 B</td><td>租户 MAC</td><td>被封装的原始二层帧开始</td></tr><tr><td>内层 IP</td><td>20 B</td><td>内层 src / dst</td><td>overlay 里的真实通信地址</td></tr><tr><td>内层 UDP/TCP</td><td>8 B</td><td>内层端口</td><td>租户的真实流量</td></tr><tr><td>Payload</td><td>N B</td><td>业务数据</td><td>原始负载</td></tr><tr><td>其中 8 字节的 VXLAN header 结构是:</td><td></td><td></td><td></td></tr></tbody></table><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"> 0 1 2 3</span><br><span class="line"> 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1</span><br><span class="line">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class="line">|R|R|R|R|I|R|R|R| Reserved |</span><br><span class="line">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class="line">| VXLAN Network Identifier (VNI) | Reserved |</span><br><span class="line">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br></pre></td></tr></table></figure><p><code>I</code> 位(第 5 位)置 1 表示 VNI 有效,剩下的 24 位就是 VNI。</p><p><strong>VTEP 的工作模型</strong>:它从外层 UDP/4789 收到一个 VXLAN 包,剥掉外层 Eth/IP/UDP 和 VXLAN header,拿到内层的原始以太网帧,然后<strong>按内层 IP 头继续转发</strong>。</p><p>注意最后这句话——「按内层 IP 头继续转发」。这就是 evr 整个设计的命门所在:<strong>如果我把内层 dst IP 填成我自己,VTEP 解封后不就把包转回给我了吗?</strong></p><h2 id="三、evr-工具:让-EVR-自己把包反射回来"><a href="#三、evr-工具:让-EVR-自己把包反射回来" class="headerlink" title="三、evr 工具:让 EVR 自己把包反射回来"></a>三、evr 工具:让 EVR 自己把包反射回来</h2><h3 id="实现原理:自环内层帧-payload-内嵌-EVR-源-IP"><a href="#实现原理:自环内层帧-payload-内嵌-EVR-源-IP" class="headerlink" title="实现原理:自环内层帧 + payload 内嵌 EVR 源 IP"></a>实现原理:自环内层帧 + payload 内嵌 EVR 源 IP</h3><p>evr 的核心是一个反直觉但极其精简的设计——「<strong>自环内层帧</strong>」。</p><img src="/2026/06/30/baidu-network-monitor-evr-vxlan-probe/image-20260630055337562.png" class=""><p>先看一个绕不开的硬约束:VTEP 是<strong>按内层 IP 头转发</strong>解封后的帧的。所以要让反射的包能回到探测机,<strong>内层 dst IP 就必须填探测机本机</strong>——填别的地址,包就被转到别处去了,根本回不来。这一点没得选。</p><p>真正有选择空间的是<strong>内层 src IP</strong>。最「自然」的想法是:把每个目标 EVR 的源 IP 填进内层 src IP,回包时靠这个 src IP 区分「是哪个目标反射回来的」。多目标时,本机收到一堆 dst=本机、src=各个 EVR 的回包,按 src IP 分流即可。</p><p>但 evr 偏偏没这么做——它把内层 src IP 也填成了本机(<strong>src = dst = 本机,自环</strong>)。这样一来,那个本可用来标识目标的内层 src IP 就被「浪费」掉了,回包里全是 <code>src=本机, dst=本机</code>,彼此长得一模一样,没法区分目标了。</p><p>那为什么要主动放弃这个天然的标识位?<strong>这里其实有一个技巧</strong>:<br>当出现故障的时候,我们要使用traceroute的功能,通过设置TTL,回获取内层的回包经过的路径。通过设置src IP为探测机的IP,我们就能够让ICMP的回包发送给探测机,这样就可以把回程的路径都探测出来了。</p><p>evr 的三招合起来是这样:</p><p><strong>第一招:内层 src IP 和 dst IP 都填探测机的本机地址。</strong></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// agent.go:内层 src == 内层 dst == 本机 IP</span></span><br><span class="line">inner, err := codec.EncodeVxlanInner(a.conf.VNI, a.conf.SrcMAC, a.conf.DstMAC,</span><br><span class="line"> p.innerDstIP, p.innerDstIP, <span class="comment">// inner src == inner dst == local IP</span></span><br><span class="line"> newPort, a.conf.InnerDstPort, <span class="type">uint8</span>(a.conf.TOS), a.conf.TTL, payload)</span><br></pre></td></tr></table></figure><p>EVR 解封 VXLAN 后看到内层 dst = 本机,自然就把内层帧转回本机。本机的 raw socket 直接收到回包,<strong>不需要在对端起任何 server,也不需要单独申请回包用的 IP/端口</strong>。</p><p><strong>第二招:把「真正的目标标识」嵌进 payload。</strong></p><p>既然内层 src/dst 都被占用成本机了,那「这个包是探测哪个 EVR 的」靠什么区分?答案是把目标 EVR 的IP 写进 payload 的第 24~28 字节:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// codec/packet.go:EVRCHECK 协议头</span></span><br><span class="line"><span class="comment">// 偏移 0 : magic "EVRCHECK" (8B),校验合法报文</span></span><br><span class="line"><span class="comment">// 偏移 8 : seq (8B) 业务序列号,原子自增</span></span><br><span class="line"><span class="comment">// 偏移 16 : ts (8B) 发送时间戳(纳秒)</span></span><br><span class="line"><span class="comment">// 偏移 24 : evrIP (4B) EVR 源 IP —— 回包据此映射回 target</span></span><br><span class="line"><span class="comment">// 偏移 28+ : salt Salt 填充,用于 bitflip 检测</span></span><br></pre></td></tr></table></figure><img src="/2026/06/30/baidu-network-monitor-evr-vxlan-probe/image-20260630060840628.png" class=""><p>回包时解析 payload 里的这 4 字节,一步定位回目标:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// agent.go:handlePacket 收到回包</span></span><br><span class="line">seq, ts, evrIP := codec.DecodeWithSrcIP(payload)</span><br><span class="line">p := a.peerByEVRSrc[evrIP.String()] <span class="comment">// 直接 O(1) 定位 target</span></span><br></pre></td></tr></table></figure><p>本质上,evr 把「目标标识」从 IP 头搬到了 payload 里,让 EVR 的 VXLAN 反射动作天然变成一次「单源对多目标」的回包匹配。代价仅仅是 4 字节 payload + 解析时多读 4 字节。</p><p><strong>第三招:外层源 IP 可以 spoof。</strong></p><p>evr 用 <code>ipv4.NewRawConn</code> 包了一层(等价于开启 <code>IP_HDRINCL</code>),让内核不再自己生成外层 IP 头,而是原样发出我们手工拼好的外层 IPv4 头。这样外层 src IP 可以填成 <code>mock_src</code>——一个虚假的IP地址。当然这个功能是可选的。</p><p>这有什么好处呢?<br>为了在Evr上识别出来探测包,我们其实是在Evr中做了一些特殊的配置的,使用和客户不同的VNI,识别出『探测机』的源IP,有不同的路由策略。但这也带来的一个问题: 如果探测机有故障,需要切换探测机的时候,需要在Evr上改配置。<br>为线上的网络设备修改配置流程上很复杂,需要评审改配方案、需要等待变更窗口,需要在凌晨的时候操作等。所以不能及时的更改探测机。但是如果我们使用mockIP,那么就可以分分钟的把探测切到新的探测机上,不需要修改Evr的配置。这也是我们的一个技巧。</p><p>完整的一发一收流程:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"> 发送侧 回程侧</span><br><span class="line">┌────────────────────────────┐ ┌────────────────────────────┐</span><br><span class="line">│ 外层 IP : mock_src → vtepIP │ │ 内层 dst = 本机 │</span><br><span class="line">│ 外层 UDP: srcPort → 4789 │ 反射 │ ↓ 本机 raw socket 收到 │</span><br><span class="line">│ ┌ VXLAN(VNI) ───────────┐ │ ────► │ 解析 payload[24:28] │</span><br><span class="line">│ │ 内层 ETH/IPv4/UDP │ │ │ 得到 EVR 源 IP │</span><br><span class="line">│ │ src=本机, dst=本机 │ │ │ peerByEVRSrc[ip] → target │</span><br><span class="line">│ │ payload: ...+EVR srcIP│ │ │ 累加 stat(丢包/延迟/翻转) │</span><br><span class="line">│ └───────────────────────┘ │ └────────────────────────────┘</span><br><span class="line">└────────────────────────────┘</span><br></pre></td></tr></table></figure><p>整个过程<strong>只在百度侧部署一台 evr</strong>,EVR 设备本身充当了反射器——困境破解。</p><h3 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h3><p>evr 由 JSON 配置驱动,每个 target 用 <code>vtep#evrSrc[#mockSrc]</code> 三段式表达:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="string">"evr-probe-1"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"client_addr"</span><span class="punctuation">:</span> <span class="string">"203.0.113.10"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"targets"</span><span class="punctuation">:</span> <span class="string">"198.51.100.96#192.0.2.1#203.0.113.99,198.51.100.97#192.0.2.2#203.0.113.99"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"vni"</span><span class="punctuation">:</span> <span class="number">15990000</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"tos"</span><span class="punctuation">:</span> <span class="number">64</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"ttl"</span><span class="punctuation">:</span> <span class="number">64</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"client_port_range"</span><span class="punctuation">:</span> <span class="string">"63000,63999"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"rate_in_span"</span><span class="punctuation">:</span> <span class="number">2000</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"span"</span><span class="punctuation">:</span> <span class="string">"1s"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"delay"</span><span class="punctuation">:</span> <span class="string">"5s"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"msg_len"</span><span class="punctuation">:</span> <span class="number">1024</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"log_dir"</span><span class="punctuation">:</span> <span class="string">"./log"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"log_max_age_days"</span><span class="punctuation">:</span> <span class="number">3</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>三段式的含义:</p><table><thead><tr><th>段</th><th>必填</th><th>含义</th></tr></thead><tbody><tr><td><code>vtepIP</code></td><td>是</td><td>EVR VTEP 的 IP(外层目的地址)</td></tr><tr><td><code>evrSrcIP</code></td><td>是</td><td>「目标」标识,嵌入 payload 用于回包匹配</td></tr><tr><td><code>mockSrcIP</code></td><td>否</td><td>外层源 IP;为空则用 <code>client_addr</code></td></tr><tr><td>启动:</td><td></td><td></td></tr></tbody></table><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 配置文件启动(推荐,线上用 systemd 以 root 拉起)</span></span><br><span class="line"><span class="built_in">sudo</span> ./evr -c /etc/evr/evr.json</span><br><span class="line"></span><br><span class="line"><span class="comment"># 临时排查,全用命令行参数</span></span><br><span class="line"><span class="built_in">sudo</span> ./evr \</span><br><span class="line"> --client-addr 203.0.113.10 \</span><br><span class="line"> --targets 198.51.100.96#192.0.2.1#203.0.113.99 \</span><br><span class="line"> --rate-in-span 2000 --span 1s --msg-len 1024</span><br><span class="line"></span><br><span class="line"><span class="comment"># CLI 参数会覆盖配置文件里的同名字段(pflag.Visit 模式)</span></span><br><span class="line"><span class="built_in">sudo</span> ./evr -c /etc/evr/evr.json --rate-in-span 5000 --verbose</span><br></pre></td></tr></table></figure><p>evr 需要 <code>sudo</code>(或 <code>CAP_NET_RAW</code>)来创建 raw socket、启用 <code>IP_HDRINCL</code>、设置 IP TOS 并挂载 BPF。它<strong>仅在 Linux 上有意义</strong>,macOS 只能用于编译开发。</p><h2 id="四、evr-中的技巧与高频-FAQ"><a href="#四、evr-中的技巧与高频-FAQ" class="headerlink" title="四、evr 中的技巧与高频 FAQ"></a>四、evr 中的技巧与高频 FAQ</h2><p>evr 看似简单,但藏了几个值得拿出来单独说的工程技巧。</p><h3 id="技巧-1:BPF-内核层过滤,别让无关-UDP-流量打扰"><a href="#技巧-1:BPF-内核层过滤,别让无关-UDP-流量打扰" class="headerlink" title="技巧 1:BPF 内核层过滤,别让无关 UDP 流量打扰"></a>技巧 1:BPF 内核层过滤,别让无关 UDP 流量打扰</h3><p>读 socket 用的是 <code>ip4:udp</code> raw socket,它默认会收到本机<strong>所有</strong> UDP 报文。在万兆网卡、几十万 pps 的探测机上,内核把海量无关包拷到用户态,性能直接崩。</p><p>解法是在内核层用 cBPF 过滤,只放行三个条件同时命中的包:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(1) IPv4 协议号 = UDP (17)</span><br><span class="line">(2) IPv4 TOS = cfg.tos</span><br><span class="line">(3) UDP 目的端口 = inner_dst_port (默认 8972)</span><br></pre></td></tr></table></figure><p>只有这三条都满足才投递到用户态,几乎零开销。而<strong>写 socket</strong> 上则反过来装一个「全丢」的 BPF——阻止内核给这个发送 socket 排队任何回包。</p><h3 id="技巧-2:源端口轮转覆盖-ECMP-多路径"><a href="#技巧-2:源端口轮转覆盖-ECMP-多路径" class="headerlink" title="技巧 2:源端口轮转覆盖 ECMP 多路径"></a>技巧 2:源端口轮转覆盖 ECMP 多路径</h3><p>和 bitflip/lidar 一样,evr 也面临多路径覆盖问题。两个固定地址之间,五元组固定则 ECMP 哈希结果不变,永远只走一条链路。evr 通过 <code>client_port_range</code> 配置一段源端口,每发一轮就让 <code>srcPort + 1</code>,在统计意义上覆盖 ECMP 哈希全空间。</p><h3 id="技巧-3:4-种-Salt-模式抓-bitflip"><a href="#技巧-3:4-种-Salt-模式抓-bitflip" class="headerlink" title="技巧 3:4 种 Salt 模式抓 bitflip"></a>技巧 3:4 种 Salt 模式抓 bitflip</h3><p>payload 第 28 字节往后是 Salt 填充,按 <code>seq % 4</code> 在四种模式间轮换,专门用来检测链路上的位翻转:</p><ul><li><code>0xFF</code> 全 1 —— 暴露任何「1 变 0」的翻转</li><li><code>0x00</code> 全 0 —— 暴露任何「0 变 1」的翻转</li><li><code>0x5A</code> 交替位 (01011010) —— 适配 NIC 串行链路的奇偶错误</li><li>互补 <code>0xAAAA / 0x5555</code> —— 专治 <strong>1's complement 校验和漏检的「互补翻转」</strong>,这是普通 UDP/TCP checksum 唯一无法察觉的一类 bitflip</li></ul><p>回包时如果 payload 长度等于发送长度,就和对应 Salt 比对,命中差异即记一条 <code>[client bitflip]</code> 日志。这套 Salt 实现与 baize/kuiniu 完全共用。</p><h3 id="高频-FAQ"><a href="#高频-FAQ" class="headerlink" title="高频 FAQ"></a>高频 FAQ</h3><p><strong>Q:mock_src 是怎么生效的?内核为什么不会把它改回去?</strong><br>evr 在写端用 <code>ipv4.NewRawConn(conn)</code> 包了一层,等价于开启 <code>IP_HDRINCL</code>。开启后内核不再生成自己的 IP 头,而是原样发出我们手工拼的外层 IPv4 头,其中 src 就是 mock_src。如果不包这一层,内核会前置一份自己的 IP 头,导致双层 IP 封装且 mock_src 失效。</p><p><strong>Q:<code>rate_in_span</code> 是单 target 速率还是总速率?</strong><br>是<strong>所有 target 的总速率</strong>。比如 12 个 target、<code>rate_in_span=2000/s</code>,每个 target 平均只有约 166 pps。要提高单 target 速率,就减少 target 数量或调大 <code>rate_in_span</code>。</p><p><strong>Q:为什么内层 src 和 dst 都填本机 IP?</strong><br>这样 EVR 反射回来的内层帧 dst 就是本机,本机 raw socket 直接收得到,无需在对端起 server、无需单独申请回包 IP。这是 evr 破解「对面进不去」困境的核心招式。src IP也填写本机IP是为了定位的时候traceroute的需要。</p><p><strong>Q:为什么 evr 必须用 sudo?</strong><br>需要创建 <code>ip4:udp</code> raw socket、启用 <code>IP_HDRINCL</code>、设置 IP TOS/DSCP 并挂载 BPF。Linux 上需要 <code>CAP_NET_RAW</code>,最简单就是 sudo 或 systemd 以 root 启动。</p><p><strong>Q:和 baize / kuiniu 怎么选?</strong><br>普通业务网络长期监控用 <strong>baize</strong>;AI 训练的 GPU NIC 互联(RoCE)用 <strong>kuiniu</strong>;机房 VXLAN/EVR 路径与 EVR 设备本身的探测用 <strong>evr</strong>——关键区别是 evr <strong>不需要在对端起 server</strong>,EVR 设备本身就是反射器。</p><hr><p>evr 把一个看似无解的困境——「探测机进不去客户机房,被探测设备又装不了 agent」——通过对 VXLAN VTEP 反射特性的巧妙利用,变成了一个单边即可完成的探测。这背后是同一套技术栈在不同网络场景下的复用:raw socket 构造报文、BPF 内核过滤、源端口轮转覆盖 ECMP、Salt 检测 bitflip、时间桶统计。</p><p>这只是 nettools 的冰山一角。后续还有网关设备监控、定位工具,以及巨量监控数据的处理方案。</p><p>项目地址:<a href="https://github.com/baidu/nettools">https://github.com/baidu/nettools</a></p><p>欢迎 Star、试用、提 Issue 和 PR。</p>
2026-06-29 22:25:37
<p>我把 775 篇收藏的文章塞进一个 4MB 的向量库,然后问它:"我都收藏过哪些关于 loop engineering 的资料?"三秒钟,它把散在六七篇文章里的观点拼成一段答案,每条都带出处。</p><img src="/2026/06/29/chao-rag-wiki-personal-knowledge-base/image-20260629054807094.png" class=""><p>这不是什么 SaaS 产品,是我自己写的一个 skill,叫 <code>chao-rag-wiki</code>。今天聊聊它,顺便聊聊它背后那个问题:知识库越攒越大,你到底怎么"读"它?</p><p>得先从 Karpathy 的一个想法说起。</p><hr><span id="more"></span><h2 id="一、Karpathy-的-LLM-Wiki-:让-AI-替你维护知识库"><a href="#一、Karpathy-的-LLM-Wiki-:让-AI-替你维护知识库" class="headerlink" title="一、Karpathy 的"LLM Wiki":让 AI 替你维护知识库"></a>一、Karpathy 的"LLM Wiki":让 AI 替你维护知识库</h2><p>起点是 Andrej Karpathy 抛出的一句话:</p><blockquote><p>"The LLM writes and maintains the wiki; the human reads and asks questions."<br>(LLM 负责写和维护 wiki,人类负责读和提问。)</p></blockquote><p>有人把这句话做成了一个 skill —— <code>karpathy-llm-wiki</code>。玩法是这样:</p><p>你有两个目录。<code>raw/</code> 放原始素材,只读、永不修改——收藏的推文、文章、论文都丢这里。<code>wiki/</code> 放编译后的知识文章,AI 全权打理。</p><p>每往 <code>raw/</code> 加一篇新素材,AI 就把它"编译"进 wiki:判断这篇属于哪个已有主题,跟某篇是同一个核心论点就合并进去、更新段落,全新概念就新建一篇,顺便检查跟已有内容有没有冲突、有就标出来。最后维护一个 <code>wiki/index.md</code> 全局索引,每篇一行——链接、摘要、更新日期。</p><p>你查询的时候("我知道哪些关于 X 的东西?"),AI 先读 index.md 定位到相关文章,打开那几篇,再综合作答。</p><p>这个设计很优雅,本质是用 AI 把碎片素材沉淀成结构化的知识资产,而且会随时间复利增长。读起来像一本你自己的、不断长大的百科全书。</p><p>但它有个隐含前提:素材得先被"编译"进 wiki,才搜得到。下面这个 skill,想绕开的就是这件事。</p><hr><h2 id="二、chao-rag-wiki:不编译,直接检索"><a href="#二、chao-rag-wiki:不编译,直接检索" class="headerlink" title="二、chao-rag-wiki:不编译,直接检索"></a>二、chao-rag-wiki:不编译,直接检索</h2><p>我借了 <code>karpathy-llm-wiki</code> 的"raw/ 只读"哲学,但把后半段换成了 RAG(检索增强生成)。</p><p>一句话说清区别:<strong>karpathy-llm-wiki 是先把书写好再去翻书;chao-rag-wiki 是书原样不动,但给每一页都建了语义索引,随问随搜。</strong></p><img src="/2026/06/29/chao-rag-wiki-personal-knowledge-base/image-20260629054948985.png" class=""><p>技术栈是这么几块拼的:</p><p><strong>1. 向量检索用 <a href="https://github.com/RyanCodrai/turbovec">turbovec</a>。</strong> Google Research 的 TurboQuant 算法的 Rust 实现,能把向量压 16 倍还几乎不掉精度——上一篇我专门拆过。我的 775 篇切成 9089 个 chunk(去重后),整个索引才 4.8MB,常驻内存毫无压力。</p><p><strong>2. 嵌入默认使用千帆的 bge-large-zh(1024 维),但接口跟 provider 无关。</strong> 想换 OpenAI、Voyage 或本地模型都行,只改环境变量,不动代码。key 全走环境变量,绝不硬编码。</p><p><strong>3. 混合检索(Hybrid RAG),这是关键。</strong> 同时跑两路:稠密向量检索管语义,知道"自我迭代"和"循环"是一回事;BM25 关键词检索管字面,能精确命中 <code>rotate_writer.go</code> 这种代码标识符、人名、专有词。两路结果用 RRF(Reciprocal Rank Fusion,倒数排名融合)合并。语义检索擅长"意思对但用词不同",关键词检索擅长"就要这个词",融合之后两头都不漏。</p><img src="/2026/06/29/chao-rag-wiki-personal-knowledge-base/image-20260629055149668.png" class=""><p><strong>4. 可选的 LLM 重排(rerank)。</strong> 检索快但粗,再拉一个 LLM 当"相关性裁判"给候选打分重排,精度优先时加 <code>--rerank</code> 就行。</p><img src="/2026/06/29/chao-rag-wiki-personal-knowledge-base/image-20260629055320393.png" class=""><p><strong>5. 建索引时按 chunk 文本哈希去重。</strong> <code>raw/</code> 里同一段内容存了多份副本的话,只留一份进库,免得副本在结果里挤名额——我这 775 篇就压掉了 706 个重复 chunk。</p><img src="/2026/06/29/chao-rag-wiki-personal-knowledge-base/image-20260629055449657.png" class=""><h3 id="优缺点摆出来"><a href="#优缺点摆出来" class="headerlink" title="优缺点摆出来"></a>优缺点摆出来</h3><p>先说好的:</p><ul><li><p>零编译延迟。新素材丢进 <code>raw/</code>,跑一句增量索引就搜得到,不用 AI 逐篇消化。而且增量索引按内容哈希只处理新增和改动的文件,没变化就几秒退出。</p></li><li><p>召回更全。它检索的是原始全文的每个片段。哪怕某个话题你压根没想过给它单独建篇文章,只要素材里提过,就搜得到。</p></li><li><p>混合检索加重排加去重,对中英混排、专有名词、长尾问题都更稳。同一段内容在 <code>raw/</code> 里有多份副本,建索引时按 chunk 文本哈希自动合并,结果不会被副本占名额——但保留"另见 N 份副本"的来源信息,去重不丢信息。</p></li></ul><p>再说差的,这些恰好是 <code>karpathy-llm-wiki</code> 的强项:</p><ul><li><p>它没有知识沉淀,给你的是原文片段拼盘,不是一篇消化过、把内部矛盾理顺了的文章。<code>karpathy-llm-wiki</code> 那边的 wiki 文章是真正的二次创作,读起来成体系。</p></li><li><p>它只能精确去重。逐字相同的副本能干净合并,但"同一篇、有细微编辑差异"的近似重复就认不出来了——那得上 MinHash 或 SimHash,复杂得多。</p></li><li><p>它依赖在线 embedding,查询要联网(或者本地部署的大模型)、有 API 成本(BM25 那一路不用)。</p></li></ul><p>说白了,<code>karpathy-llm-wiki</code> 重"沉淀",<code>chao-rag-wiki</code> 重"召回"。前者像精心整理的笔记本,后者像一个全文搜索引擎。</p><hr><h2 id="三、实战对比:两个查询"><a href="#三、实战对比:两个查询" class="headerlink" title="三、实战对比:两个查询"></a>三、实战对比:两个查询</h2><p>光说不练假把式。同样两个问题,两个 skill 各跑一遍。</p><p>自从 Karpathy的llm-wiki理念提出来之后,我收集资料的时候特意使用Obsidian web clipper进行收集,最近几个月也收集了700多篇资料,也在Hermes中建立了llm-wiki,这次我使用<code>chao-rag-wiki</code> 建立了RAG索引,让我们比较一下这两个skill搜索资料的效果。</p><h3 id="查询一:-loop-engineering"><a href="#查询一:-loop-engineering" class="headerlink" title="查询一:"loop engineering""></a>查询一:"loop engineering"</h3><p><code>chao-rag-wiki</code>(hybrid + rerank)召回了散在多篇原文里的片段:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">[1] rerank=8.0 PART 2: WHAT LOOP ENGINEERING ACTUALLY IS</span><br><span class="line"> articles/...Loops What Every AI Engineer Needs to Know in 2026.md</span><br><span class="line">[2] rerank=7.0 工程化的三个层次</span><br><span class="line"> articles/...深度拆解:AI 智能体 Harness 的构造.md</span><br><span class="line">[3] rerank=7.0 Most Developers Do Not Need Agent Loops Yet</span><br><span class="line">[4] rerank=7.0 Agent Harness 拆解:AI Agent 真正的工程底座</span><br><span class="line">[5] rerank=6.0 > I don't talk to an agent anymore, I talk to a loop</span><br></pre></td></tr></table></figure><p>它直接定位到讲 loop engineering 五阶段(DISCOVER→PLAN→EXECUTE→VERIFY→ITERATE)的原文段落,连"大多数开发者其实还不需要 loop"这种唱反调的视角也捞了出来——因为那篇文章确实在我素材里。</p><img src="/2026/06/29/chao-rag-wiki-personal-knowledge-base/image-20260629060619850.png" class=""><p><code>karpathy-llm-wiki</code> 呢?它在 <code>wiki/index.md</code> 里一搜就命中一篇编译好的文章:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">| Loop Engineering: 从提示 Agent 到设计循环 | 2026 年 6 月爆发的 AI 编程</span><br><span class="line"> 新范式:五个原语(Automations/Worktrees/Skills/Connectors/Sub-agents)</span><br><span class="line"> +状态记忆,四条件测试,最小可行 Loop,刹车配置与四种死法 | 2026-06-24 |</span><br></pre></td></tr></table></figure><p>打开是一份消化整合过的结构化笔记:核心定义、Boris Cherny 的三阶段演化、五个必需品表格、引用了 8 个来源,读起来一气呵成。</p><p>这一局 <code>karpathy-llm-wiki</code> 赢在成品质量。loop engineering 是热门话题,我早让它编译过一篇。<code>chao-rag-wiki</code> 给的是原料,它给的是成菜。</p><h3 id="查询二:-grill-skill"><a href="#查询二:-grill-skill" class="headerlink" title="查询二:"grill skill""></a>查询二:"grill skill"</h3><p>这局反过来了。</p><p><code>chao-rag-wiki</code>(hybrid + rerank):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">[1] rerank/score Mattpocock Skills 系统:AI Agent 的能力单元 › SKILL.md 的文件结构</span><br><span class="line"> sources/AI时代的程序员/02 Mattpocock Skills系统.md</span><br><span class="line">[2] Productivity Skills</span><br><span class="line"> sources/AI时代的程序员/《AI时代的软件工程》-图书大纲.md</span><br><span class="line">[3] skill_manage 操作分发 (+1 dup copies)</span><br><span class="line"> articles/2026-04-12 -Hermes Agent 源码解析.md</span><br><span class="line">[5] Productivity —— grill-me / caveman / handoff...</span><br><span class="line"> articles/...mattpocock skills Skills for Real Engineers.md › #1: The Agent Didn't Do What I Want</span><br></pre></td></tr></table></figure><p>干净命中 mattpocock 技能库里 <code>grill-me</code>、<code>grill-with-docs</code> 的介绍——"写代码前先让 AI 反过来拷问你"。注意第 3 条后面那个 <code>(+1 dup copies)</code>:去重把那篇的副本合并成一条、留了来源标记,没让重复内容占掉好几个名额。</p><p><code>karpathy-llm-wiki</code> 呢?我去 <code>wiki/index.md</code> 里搜 "grill"、"mattpocock"、"拷问"……</p><p>一条都没有。</p><p>不是说我素材里没有 grill 相关内容——有,好几篇正文都提了 <code>/grill-me</code>。问题是这个话题从没被单独编译成一篇 wiki 文章,index.md 里就没它的入口。按 karpathy-wiki "先读 index 再定位文章"的路子,它要么直接漏掉这个查询,要么只能模糊地甩你一篇泛泛的"agent skills 框架"。</p><p>这一局 <code>chao-rag-wiki</code> 完胜。它检索全文,根本不在乎你立没立过条目。</p><hr><h2 id="四、所以到底用哪个?"><a href="#四、所以到底用哪个?" class="headerlink" title="四、所以到底用哪个?"></a>四、所以到底用哪个?</h2><p>两个查询跑下来,结论挺清楚:它俩不是替代关系,是互补关系。</p><table><thead><tr><th></th><th>karpathy-llm-wiki</th><th>chao-rag-wiki</th></tr></thead><tbody><tr><td>核心机制</td><td>AI 编译素材成文章</td><td>RAG 全文语义检索</td></tr><tr><td>查询方式</td><td>读 index → 定位文章</td><td>向量+BM25 混合检索+重排</td></tr><tr><td>强项</td><td>热门话题、成体系的沉淀</td><td>全量召回、长尾话题、专有名词</td></tr><tr><td>弱项</td><td>没编译过的话题会漏</td><td>给原料而非成品、只能精确去重</td></tr><tr><td>延迟</td><td>编译慢、查询快</td><td>索引快、查询需联网</td></tr><tr><td>想读一篇消化好的总结,问 <code>karpathy-llm-wiki</code>;想确认"我到底收藏过啥"、找长尾、找冷门词,用 <code>chao-rag-wiki</code>。</td><td></td><td></td></tr></tbody></table><p>我现在就是这么用的:<code>chao-rag-wiki</code> 当全文搜索引擎兜底召回,<code>karpathy-llm-wiki</code> 当精编笔记本沉淀热点。一个保证不漏,一个保证好读。</p><hr><h2 id="五、把它接进-Obsidian:从-看到-到-可搜"><a href="#五、把它接进-Obsidian:从-看到-到-可搜" class="headerlink" title="五、把它接进 Obsidian:从"看到"到"可搜""></a>五、把它接进 Obsidian:从"看到"到"可搜"</h2><p>前面讲的都是 skill 本身。可知识库要真转起来,得先解决一个更前面的问题:素材怎么进来?总不能每篇手动复制粘贴。</p><p>我这套链路全自动,分三段。</p><h3 id="第一段:Web-Clipper-抓取,落到-raw"><a href="#第一段:Web-Clipper-抓取,落到-raw" class="headerlink" title="第一段:Web Clipper 抓取,落到 raw/"></a>第一段:Web Clipper 抓取,落到 <code>raw/</code></h3><p><a href="https://obsidian.md/clipper">Obsidian Web Clipper</a> 是官方的浏览器插件,Chrome、Edge、Firefox 都有。看到一篇好文章、一条推文,点一下插件图标,它就把正文提取成干净的 Markdown,存进你的 Obsidian vault。</p><p>它能配模板和保存路径,这是关键。我把模板的目标文件夹设成 vault 里的 <code>raw/articles/</code>,文件名用 <code>{{date}} -{{author}}-{{title}}</code>,再让它自动带上来源 URL、抓取日期这些 frontmatter。于是浏览中随手一点,文章就躺进了 <code>raw/</code>——正好是两个 skill 都约定"只读、不改"的那个源目录。我那 775 篇,基本就是这么攒的。</p><h3 id="第二段:定时任务自动索引"><a href="#第二段:定时任务自动索引" class="headerlink" title="第二段:定时任务自动索引"></a>第二段:定时任务自动索引</h3><p>素材进来了,但还没"可搜"。这步交给定时任务——<code>raw/</code> 每天在长,那就让索引每天自己追上:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># crontab:每天凌晨 3:07 增量索引(错峰,别卡整点)</span></span><br><span class="line">7 3 * * * <span class="built_in">cd</span> ~/Obsidian-Vault && \</span><br><span class="line"> ~/.claude/skills/chao-rag-wiki/.venv/bin/python \</span><br><span class="line"> ~/.claude/skills/chao-rag-wiki/scripts/build_rag.py --raw raw --out .rag --update</span><br></pre></td></tr></table></figure><p>前面说了,<code>--update</code> 按内容哈希只处理新增和改动的文件,没变化就几秒退出、零 API 调用,所以天天跑也不浪费。Web Clipper 当天剪的新文章,第二天一早就进了向量库。</p><p>karpathy-llm-wiki 那一路同理,挂个任务把当天 <code>raw/</code> 的新文件编译进 wiki 沉淀下来就行。两条管道并行:chao-rag 让每篇立刻可搜,karpathy-wiki 把值得沉淀的编成文章。</p><p>实际上是在我的Mac mini m2上的Hermes中配置了定时任务,每天凌晨调用这个skill进行索引处理。</p><img src="/2026/06/29/chao-rag-wiki-personal-knowledge-base/image-20260629055738407.png" class=""><h3 id="第三段:随时调用-skill-搜索"><a href="#第三段:随时调用-skill-搜索" class="headerlink" title="第三段:随时调用 skill 搜索"></a>第三段:随时调用 skill 搜索</h3><p>闭环这就成了。想找东西,直接在 Claude Code 里调:</p><ul><li><code>/chao-rag-wiki 找出关于 X 的资料</code>——全文混合检索,长尾、冷门词都捞得到;</li><li><code>/karpathy-llm-wiki 我都知道哪些关于 X 的东西</code>——读编译好的精编文章。</li></ul><p>整条链路连起来就是:浏览时点一下 Web Clipper,文章落进 <code>raw/</code>,定时任务自动索引,想搜时一句话调 skill。你只管"看到好东西点一下"和"有问题问一句",中间的剪藏、入库、索引全自动。Karpathy 那句"人类负责读和提问",落地大概就是这个样子。</p><hr><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p><code>chao-rag-wiki</code> 已经开源,一行命令装上:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add smallnest/chao-rag-wiki</span><br></pre></td></tr></table></figure><p>配好一个 embedding 的 key(默认千帆,也支持 OpenAI、Voyage 或任意 OpenAI 兼容网关),素材丢进 <code>raw/</code>,跑一句 build 就能开问。再按第五节配好 Web Clipper 模板和定时任务,整个知识库就自己转起来了。</p><blockquote><p>最后留个彩蛋:写这篇时,"两个 skill 各跑两个查询、对比结果"那一步,就是 chao-rag-wiki 自己检索出来的。它检索自己被讨论的素材,多少有点自产自销。RAG 的尽头,没准就是让知识库学会自我介绍。</p></blockquote>
2026-06-28 18:00:00
<blockquote><p>"Clear is better than clever."<br>清晰胜于聪明。<br>—— Rob Pike, Go Proverbs</p></blockquote><p>第 23 章把重构讲完了。嗅坏味道、套 Fowler 手法、小步施工、每步测试,这套东西对 Java、Python、Go 一视同仁。但真到 Go 上手你会发现,Fowler 的目录够不着 Go 的好几层脾气。一段能跑的 Go 代码,可能还停在 Go 1.10 的写法,不地道;可能并发原语用错了,race detector 一开就红,不安全;也可能分配没控住,cache line 在 false sharing,不快。这些坏味道扫不出来,是 Go 二十年攒下来、只有老手才摸得到的门道。</p><p>门道都散在各处。Dave Cheney 的高性能工作坊讲一套,dgryski 的 go-perfbook 讲一套,《Go 并发编程实战》讲一套,Go 团队的 modernize 分析又讲一套,再加上无数生产事故换来的风格约定。以前你得一本书一本书读、一个 pprof 一个 pprof 啃。现在有人把这些蒸成一个 Skill,Agent 调一下就能用。</p><p>本章介绍五个 Go 专属的 Skill,正好覆盖 Go 工程的四个面:现代化(<code>/modern-go</code>)、性能(<code>chao-go-perf</code>)、并发(<code>chao-go-sync</code>)、风格(<code>go-style-guide</code>),外加一个把这几样打包、还顺带做了效果评估的全家桶(<code>cc-skills-golang</code>)。前三个是本书作者 smallnest 写的,对,写这本书的人和写这些 Skill 的人是同一个;后两个分别来自 madflojo(Benjamin Cane)和 samber。</p><span id="more"></span><h2 id="24-1-为什么-Go-需要专属技能"><a href="#24-1-为什么-Go-需要专属技能" class="headerlink" title="24.1 为什么 Go 需要专属技能"></a>24.1 为什么 Go 需要专属技能</h2><p>第 1 章立过本书的旗:从 Prompt-Driven 到 Skill-Driven。Prompt 临时,Skill 持久;Skill 等于可复用的方法论加结构化的知识加标准化的输入输出。这话对什么语言都成立,但 Go 尤其该说一遍,因为它有三样东西让通用 Agent 特别容易翻车。</p><p>一是 Go 的地道写法绑版本。同一件事,Go 1.10 和 Go 1.22 的标准答案不一样。循环找切片里有没有某个元素,1.10 要手写 for 循环,1.21 一行 <code>slices.Contains</code> 搞定;求最小值,1.20 要 if-else,1.21 有内置 <code>min</code>。Agent 训练数据里两种写法都有,随手给你写哪种全看运气。代码是能跑,可能是个十年前的化石。</p><p>二是 Go 的并发坑深,而且静默。一个 <code>map</code> 并发读写不会马上崩,要等压力上来、等到生产环境的某个深夜才 panic。一个 <code>sync.Mutex</code> 被复制了,<code>go vet</code> 查得出,Agent 不会主动跑。goroutine 泄漏更无声无息,程序能跑,内存曲线却悄悄爬坡。这些坑 Fowler 的坏味道目录里一个都没有。</p><p>三是 Go 的性能是测出来的,不是猜出来的。Dave Cheney 那句「You can't optimize what you don't measure」是 Go 性能圈的宪法。可 Agent 的默认行为恰恰是猜,看一眼代码凭直觉说「这里用 sync.Pool 优化一下」,既没 benchmark 也没 pprof。猜对的概率比你想的低。</p><p>三样合起来指向一个结论:Go 的资深经验得编码进 Skill,才能被 Agent 可靠复用。这不算锦上添花,是这门语言逼出来的刚需。下面五个 Skill,就是这件事的五个侧面。</p><p><img src="/images/image-20260627085111146.png"></p><h2 id="24-2-五个-Skill-一览"><a href="#24-2-五个-Skill-一览" class="headerlink" title="24.2 五个 Skill 一览"></a>24.2 五个 Skill 一览</h2><p>先给一张全景图,免得后面绕晕。</p><table><thead><tr><th>Skill</th><th>作者</th><th>定位</th><th>覆盖面</th><th>Stars(2026/06)</th></tr></thead><tbody><tr><td><code>/modern-go</code></td><td>smallnest</td><td>Go 代码现代化</td><td>35 条 gofix 风格规则,Go 1.0→1.26+</td><td>goal-workflow 套件内</td></tr><tr><td><code>chao-go-perf</code></td><td>smallnest</td><td>Go 性能分析专家</td><td>CPU/内存/GC/编译器/缓存/并发,含 PGO</td><td>~40</td></tr><tr><td><code>chao-go-sync</code></td><td>smallnest</td><td>Go 并发编程专家</td><td>全部 sync 原语 + 13+ 模式 + 分布式</td><td>~37</td></tr><tr><td><code>go-style-guide</code></td><td>madflojo (Benjamin Cane)</td><td>固执己见的 Go 工程契约</td><td>包设计/接口/错误/日志/布局/测试</td><td>~35</td></tr><tr><td><code>cc-skills-golang</code></td><td>samber</td><td>生产级 Go Skills 全家桶</td><td>28+ 原子 Skill,跨工具,带评估</td><td>~2300</td></tr></tbody></table><p>五个 Skill 的分工可以这样理解:<code>/modern-go</code> 管「代码是不是用了新版本的写法」,<code>go-style-guide</code> 管「代码符不符合工程契约」,<code>chao-go-sync</code> 管「并发安不安全」,<code>chao-go-perf</code> 管「跑得快不快」,<code>cc-skills-golang</code> 把这四件事各做一份,外加交叉引用和效果评估。前四个是专精一件的单点 Skill,最后一个是面面俱到的体系。</p><p>顺带说一句 <code>cc-skills-golang</code> 的体量,两千多 stars,是本章里唯一算得上「大项目」的。它的特别之处不在多,在于拿数据证明了 Skill 真的有用。这个留到 24.7 节细讲。</p><p><img src="/images/image-20260627085255642.png"></p><p>下面逐一展开。</p><h2 id="24-3-modern-go:让老代码跟上新版本"><a href="#24-3-modern-go:让老代码跟上新版本" class="headerlink" title="24.3 /modern-go:让老代码跟上新版本"></a>24.3 /modern-go:让老代码跟上新版本</h2><p><code>/modern-go</code> 在第 8 章作为 Goal Workflow 的 Bonus Skill 露过一面,第 23 章也提过它和 <code>/refactor</code>、<code>/smell</code> 一样是「保持代码库健康」的邻居。这里展开讲它的机制。(介绍页:<a href="https://goal.rpcx.io/index_cn.html#step-modern-go">https://goal.rpcx.io/index_cn.html#step-modern-go</a> )</p><p>我自己写它,是因为 goscapy 这种库要长期维护,go.mod 里的版本会一点点往前挪,可代码还停在五年前写它时的写法。靠人记得每条 <code>time.Since</code> 替换、记得 <code>interface{}</code> 该换成 <code>any</code>,不现实。<code>go fix</code> 官方工具又只覆盖一小部分。于是把 Go 团队 modernize 分析能找到的转换规则、加上社区常用的,凑成一份带版本门控的 Skill,让 Agent 替我盯着。它的定位很纯粹,像 <code>go fix</code> 一样,把老写法自动改成新版本的地道写法。装一行:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add smallnest/goal-workflow --skill modern-go</span><br></pre></td></tr></table></figure><p>触发可以直接打 <code>/modern-go</code>,或说「现代化」「modernize」「gofix」。</p><p><img src="/images/image-20260627085439804.png"></p><h3 id="24-3-1-35-条版本门控的转换规则"><a href="#24-3-1-35-条版本门控的转换规则" class="headerlink" title="24.3.1 35 条版本门控的转换规则"></a>24.3.1 35 条版本门控的转换规则</h3><p>Skill 内置 35 条转换规则,每条带一个版本门控,只在项目的 Go 版本够格时才启用。规则从 Go 1.0 一路排到 1.26+,挑几个有代表性的:</p><table><thead><tr><th>Go 版本</th><th>老写法</th><th>新写法</th></tr></thead><tbody><tr><td>1.0+</td><td><code>time.Now().Sub(start)</code></td><td><code>time.Since(start)</code></td></tr><tr><td>1.13+</td><td><code>err == io.EOF</code></td><td><code>errors.Is(err, io.EOF)</code></td></tr><tr><td>1.18+</td><td><code>interface{}</code></td><td><code>any</code></td></tr><tr><td>1.18+</td><td><code>strings.Index</code> + 手动切片</td><td><code>strings.Cut</code></td></tr><tr><td>1.19+</td><td><code>atomic.StoreInt32(&v, 1)</code></td><td><code>var v atomic.Int32; v.Store(1)</code></td></tr><tr><td>1.20+</td><td><code>fmt.Errorf("...: %w: %w", e1, e2)</code></td><td><code>errors.Join(...)</code></td></tr><tr><td>1.21+</td><td>手写 for 循环找元素</td><td><code>slices.Contains</code></td></tr><tr><td>1.21+</td><td><code>if a<b {v=a} else {v=b}</code></td><td><code>v = min(a, b)</code></td></tr><tr><td>1.21+</td><td><code>var once sync.Once; once.Do(...)</code></td><td><code>sync.OnceFunc(...)</code></td></tr><tr><td>1.22+</td><td><code>for i := 0; i < n; i++</code></td><td><code>for i := range n</code></td></tr></tbody></table><p>每条规则都配 before/after 代码对照,机械、可执行。这正是 Go 团队 modernize 分析能被 Agent 接住的地方。</p><h3 id="24-3-2-安全保护:读-go-mod,绝不越级"><a href="#24-3-2-安全保护:读-go-mod,绝不越级" class="headerlink" title="24.3.2 安全保护:读 go.mod,绝不越级"></a>24.3.2 安全保护:读 go.mod,绝不越级</h3><p>真正关键的不是这 35 条规则,是它怎么决定该用哪条。流程很死板:</p><ol><li>先读 <code>go.mod</code> 里的 <code>go</code> 指令,拿到项目声明的 Go 版本;</li><li>扫描目标范围内所有 <code>.go</code> 文件(排除 <code>vendor/</code>、<code>.git/</code>、<code>testdata/</code>);</li><li>对每个文件,只应用版本 ≤ 项目版本的规则,从老到新依次套;</li><li>最后打印一份改了什么、跳过了什么的汇总。</li></ol><p>铁律只有一条,但很硬:绝不应用需要比项目声明版本更高的规则。项目还停在 Go 1.20,就不会给你塞 1.21 的 <code>min</code> 和 <code>slices</code>,塞了编译不过。这条把「现代化」和「改坏」之间的边界钉死了。</p><p>这条保护和第 23 章 <code>/refactor</code> 的五阶段协议是同一种思路:把人最容易手滑的地方改成 Agent 必须遵守的护栏,这里是「图新」而越级升级。人升级 Go 时常犯的错,比如把目标版本改高了却忘了改 <code>go.mod</code>,或者用了新 API 却没加 import,<code>/modern-go</code> 用版本门控和 requires importing 的提示一条条堵上。</p><h3 id="24-3-3-实演:一段老代码怎么被现代化"><a href="#24-3-3-实演:一段老代码怎么被现代化" class="headerlink" title="24.3.3 实演:一段老代码怎么被现代化"></a>24.3.3 实演:一段老代码怎么被现代化</h3><p>光看规则表没感觉,跑一段。假设 <code>go.mod</code> 写的是 <code>go 1.21</code>,项目里有这么个函数,写法停在 Go 1.13:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">parseHeader</span><span class="params">(buf []<span class="type">byte</span>)</span></span> (key, val <span class="type">string</span>, ok <span class="type">bool</span>) {</span><br><span class="line"> <span class="keyword">if</span> i := bytes.Index(buf, []<span class="type">byte</span>(<span class="string">"="</span>)); i >= <span class="number">0</span> {</span><br><span class="line"> key = <span class="type">string</span>(buf[:i])</span><br><span class="line"> val = <span class="type">string</span>(buf[i+<span class="number">1</span>:])</span><br><span class="line"> ok = <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> cache atomic.Value</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">getConfig</span><span class="params">()</span></span> *Config {</span><br><span class="line"> cache.Store(&Config{})</span><br><span class="line"> <span class="keyword">return</span> cache.Load().(*Config)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>/modern-go</code> 进来先读 <code>go.mod</code>,拿到 1.21 这个版本上限,然后从老到新逐条套能用的规则。对上面这段,它会动三处:</p><ul><li><code>bytes.Index</code> 加手动切片 → 1.20+ 的 <code>bytes.Cut</code>(1.21 够格);</li><li><code>atomic.Value</code> 加类型断言 → 1.19+ 的 <code>atomic.Pointer[T]</code>;</li><li>顺手把 import 补上。</li></ul><p>出来是这样:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">parseHeader</span><span class="params">(buf []<span class="type">byte</span>)</span></span> (key, val <span class="type">string</span>, ok <span class="type">bool</span>) {</span><br><span class="line"> before, after, found := bytes.Cut(buf, []<span class="type">byte</span>(<span class="string">"="</span>))</span><br><span class="line"> <span class="keyword">if</span> found {</span><br><span class="line"> key, val, ok = <span class="type">string</span>(before), <span class="type">string</span>(after), <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> cache atomic.Pointer[Config]</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">getConfig</span><span class="params">()</span></span> *Config {</span><br><span class="line"> cache.Store(&Config{})</span><br><span class="line"> <span class="keyword">return</span> cache.Load()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意它没碰 1.22 的 range-over-int,项目是 1.21,那条规则被门控挡掉了。这是版本门控真正干活的样子:该改的改,不该碰的一行不动。要是谁手贱把 go.mod 提到 1.22,再跑一遍,那个 for 循环才会被改成 <code>for i := range n</code>。</p><h2 id="24-4-chao-go-perf:把-Dave-Cheney-的工作坊装进-Skill"><a href="#24-4-chao-go-perf:把-Dave-Cheney-的工作坊装进-Skill" class="headerlink" title="24.4 chao-go-perf:把 Dave Cheney 的工作坊装进 Skill"></a>24.4 chao-go-perf:把 Dave Cheney 的工作坊装进 Skill</h2><p><code>/modern-go</code> 解决「写法老不老」,<code>chao-go-perf</code> 解决「跑得快不快」。我写它,是因为 rpcx、goscapy 这种库对性能敏感,可每次让 Agent 帮忙优化,它张口就是「这里加个 sync.Pool」,没 benchmark、没 pprof,纯靠猜。猜错是常态。Go 性能圈其实有现成的规矩,Dave Cheney 和 dgryski 讲过很多遍,第一条都是「先测量」。这些规矩散在讲义和书里,Agent 不会自动遵守,那就把它们蒸成一个 Skill,逼它先拿数据再说话。它把 Go 性能圈几本「圣经」装进来:</p><ul><li>Dave Cheney 的 High Performance Go Workshop(GopherCon 2019)</li><li>dgryski 的 go-perfbook(中文版)</li><li>Effective Go</li><li>Go 101 的 Optimizations 101</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add smallnest/chao-go-perf</span><br></pre></td></tr></table></figure><h3 id="24-4-1-黄金法则:先测量,再优化"><a href="#24-4-1-黄金法则:先测量,再优化" class="headerlink" title="24.4.1 黄金法则:先测量,再优化"></a>24.4.1 黄金法则:先测量,再优化</h3><p><code>chao-go-perf</code> 开篇就把 Dave Cheney 和 go-perfbook 的两句话钉在墙上:</p><blockquote><p>"You can't optimize what you don't measure. Always benchmark before and after."<br>—— Dave Cheney</p><p>不要猜测性能瓶颈。用数据说话。先测量,再优化,最后验证。<br>—— go-perfbook</p></blockquote><p>由此引出五条黄金法则:先测量再优化、Benchmark 驱动、了解编译器、内存是瓶颈、优化最热路径。这五条不是建议,是 Skill 的工作前提,和第 16 章 agent-skills 的验证门禁、第 23 章 <code>/refactor</code> 的安全协议同源:Agent 得先有证据(benchmark、pprof)才能动优化。这一条直接把 Agent 最爱的毛病禁了,看一眼代码就凭直觉「优化」。</p><p><img src="/images/image-20260627085649794.png"></p><h3 id="24-4-2-三套分析框架:CPU-内存-并发"><a href="#24-4-2-三套分析框架:CPU-内存-并发" class="headerlink" title="24.4.2 三套分析框架:CPU / 内存 / 并发"></a>24.4.2 三套分析框架:CPU / 内存 / 并发</h3><p>Skill 给出三套可机械执行的分析框架,每套都是「命令 → 看什么 → 判断什么」:</p><table><thead><tr><th>维度</th><th>生成 profile</th><th>看什么</th><th>典型病因</th></tr></thead><tbody><tr><td>CPU</td><td><code>go test -bench -cpuprofile</code></td><td>火焰图最热函数</td><td>内联失败、不必要计算、算法复杂度</td></tr><tr><td>内存</td><td><code>go test -bench -memprofile</code> + <code>-gcflags="-m"</code></td><td>alloc_space 热点、逃逸分析</td><td>slice 未预分配、[]byte↔string 转换、接口装箱</td></tr><tr><td>并发</td><td><code>go test -race</code> + <code>runtime/trace</code> + mutex profile</td><td>锁等待、调度</td><td>锁粒度过大、false sharing、channel vs mutex 选错</td></tr></tbody></table><p>这套框架把性能优化从「拍脑袋改代码」变成「先定位瓶颈在哪一层,再用对应工具验证」。AI 时代尤其用得上,Agent 吐的代码经常埋着 N+1 分配、循环内排序这类量小看不出、上了量才爆的坑,<code>chao-go-perf</code> 的内存框架一眼能扫出来。</p><h3 id="24-4-3-覆盖面与参考资料"><a href="#24-4-3-覆盖面与参考资料" class="headerlink" title="24.4.3 覆盖面与参考资料"></a>24.4.3 覆盖面与参考资料</h3><p>Skill 还配 8 份参考资料,按需加载:benchmarking、memory-optimization、cpu-optimization、cache-optimization、concurrency-perf、pgo、tooling、version-changes。其中两块是 Go 圈的硬骨头:PGO(Profile-Guided Optimization)的完整工作流,和 Go 1.12 到 1.27 的关键性能变更(哪个版本改了 GC、哪个版本改了调度器,升级时要留意什么)。CPU 缓存那块也讲得细,cache line 对齐、false sharing 检测、AoS 对 SoA 的数据局部性取舍,这些资深 Go 工程师才碰得到,Agent 默认不会主动想。</p><h3 id="24-4-4-实演:先看-benchmark,再谈优化"><a href="#24-4-4-实演:先看-benchmark,再谈优化" class="headerlink" title="24.4.4 实演:先看 benchmark,再谈优化"></a>24.4.4 实演:先看 benchmark,再谈优化</h3><p>按 Skill 的规矩,它不会直接给优化方案,第一句话是问数据。你喂给它一段有锁竞争的计数器:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Counter <span class="keyword">struct</span> {</span><br><span class="line"> mu sync.Mutex</span><br><span class="line"> n <span class="type">int</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Counter)</span></span> Inc() {</span><br><span class="line"> c.mu.Lock()</span><br><span class="line"> c.n++</span><br><span class="line"> c.mu.Unlock()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>说「这个 Inc 在高并发下慢,优化一下」。<code>chao-go-perf</code> 不会立刻说「换 atomic」,它会先走 Step 1 的分类和 Step 2 的框架,让你先出证据:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">问题分类:并发竞争(吞吐量不随 CPU 增加)</span><br><span class="line">诊断步骤:</span><br><span class="line">1. go test -bench=. -cpuprofile=cpu.out</span><br><span class="line">2. go tool pprof -http=:8080 cpu.out → 看 mutex 热点</span><br><span class="line">3. go test -race → 确认无数据竞争</span><br><span class="line">4. 看锁竞争:临界区只有一个 n++,纯计数</span><br></pre></td></tr></table></figure><p>证据到位(profile 显示锁等待占大头),它才进锁选择决策树。Skill 里那张决策图长这样:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">是否只需要保护简单整数/布尔/指针?</span><br><span class="line"> └── Yes → atomic 操作</span><br><span class="line"> └── No → 读写比例?</span><br><span class="line"> ├── 几乎全读 (> 90%) → sync.RWMutex</span><br><span class="line"> ├── 读写均衡 → sync.Mutex</span><br><span class="line"> └── 偶尔写入一次 → sync.Once / sync.Map</span><br></pre></td></tr></table></figure><p><code>n</code> 是个整数,<code>Inc</code> 只做自增,正好走第一条分支,给方案:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Counter <span class="keyword">struct</span> {</span><br><span class="line"> n atomic.Int64</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Counter)</span></span> Inc() {</span><br><span class="line"> c.n.Add(<span class="number">1</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>并附一串近似量级让你心里有数:atomic 约 1-2 ns/op,无竞争 Mutex 15-30 ns/op,channel 50-200 ns/op。最后还不忘补一句:优化前后的 benchmark 都得跑,用 benchstat 验证。这套流程才是 Skill 想要的样子,先量、再分类、最后给方案,每一步都有数据兜着,而不是上来就 sync.Pool。</p><h2 id="24-5-chao-go-sync:并发是-Go-的灵魂,也是最容易翻车的地方"><a href="#24-5-chao-go-sync:并发是-Go-的灵魂,也是最容易翻车的地方" class="headerlink" title="24.5 chao-go-sync:并发是 Go 的灵魂,也是最容易翻车的地方"></a>24.5 chao-go-sync:并发是 Go 的灵魂,也是最容易翻车的地方</h2><p>Go 的并发是它的招牌,也是它最容易出事的地方。<code>chao-go-sync</code> 基于《Go 并发编程实战》一书,把 Go 并发的全套知识,从标准库原语到分布式同步,装进一个 Skill。这本书是我写的,写它的时候把踩过的坑、用过的第三方库、做过的分布式同步方案整理了一遍。写完发现,这些内容正好是 Agent 写并发代码最缺的那块。它知道 <code>sync.Mutex</code> 怎么用,但不知道哪里会复制、哪里会重入死锁、什么时候该上分片锁。于是把书的内容蒸成 Skill,让 Agent 调一下就能拿到一整本书的判断力。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add smallnest/chao-go-sync</span><br></pre></td></tr></table></figure><h3 id="24-5-1-覆盖面:从-stdlib-到-etcd"><a href="#24-5-1-覆盖面:从-stdlib-到-etcd" class="headerlink" title="24.5.1 覆盖面:从 stdlib 到 etcd"></a>24.5.1 覆盖面:从 stdlib 到 etcd</h3><p><code>chao-go-sync</code> 的覆盖面是五个 Skill 里最广的,横跨四个层次:</p><table><thead><tr><th>层次</th><th>内容</th></tr></thead><tbody><tr><td>标准库原语</td><td>Mutex、RWMutex、WaitGroup、Cond、Once、Pool、sync.Map、atomic、channel、context、synctest</td></tr><tr><td>官方扩展</td><td>信号量、SingleFlight、ErrGroup、限流器</td></tr><tr><td>第三方库</td><td>CyclicBarrier、断路器(sony/gobreaker)、Worker Pool(panjf2000/ants)、sourcegraph/conc、各类限流库</td></tr><tr><td>分布式同步</td><td>基于 etcd 的 Leader 选举、分布式锁、分布式队列/屏障、STM</td></tr></tbody></table><p>外加 13+ 种并发模式(半异步半同步、Reactor、Proactor、Per-CPU、活动对象……)和四个经典问题(哲学家就餐四种解法、理发师问题、水工厂、Fizz Buzz)。这个体量,基本上是把一本并发书搬进了 Skill。</p><h3 id="24-5-2-Bug-诊断框架:五个必查项"><a href="#24-5-2-Bug-诊断框架:五个必查项" class="headerlink" title="24.5.2 Bug 诊断框架:五个必查项"></a>24.5.2 Bug 诊断框架:五个必查项</h3><p>并发 Bug 最难的是定位。Skill 给了一套诊断框架,收到并发问题先过这五条:</p><ul><li>有没有数据竞争?(<code>-race</code> 输出)</li><li>锁的获取/释放顺序对不对?(防死锁)</li><li>有没有锁重入?(Go 的 Mutex 不可重入,这是 Java 转 Go 最常踩的坑)</li><li>有没有复制 sync 原语?(<code>go vet</code> 能查,Agent 不会主动跑)</li><li>WaitGroup 计数匹不匹配?goroutine 有没有泄漏?(Go 1.26+ 运行时自动检测)</li></ul><p>这五条本身就是一份并发代码审查清单。<code>go-style-guide</code> 里也有一份并发 reference,但深度不在一个量级,<code>go-style-guide</code> 讲怎么用对,<code>chao-go-sync</code> 讲出了问题怎么查、怎么治、还有哪些更高级的替代。</p><h3 id="24-5-3-实演:两个静默-Bug,诊断框架一眼揪出"><a href="#24-5-3-实演:两个静默-Bug,诊断框架一眼揪出" class="headerlink" title="24.5.3 实演:两个静默 Bug,诊断框架一眼揪出"></a>24.5.3 实演:两个静默 Bug,诊断框架一眼揪出</h3><p>并发 Bug 最阴的地方在于不崩。喂给它两段看着没毛病的代码,让它审。</p><p>第一段,忘记 Unlock。多分支 return,有一条路径漏了 Unlock,程序不会立刻死,锁会泄漏,到某个时刻所有 goroutine 卡住:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(f *Foo)</span></span> Bar() {</span><br><span class="line"> f.mu.Lock()</span><br><span class="line"> <span class="keyword">if</span> f.count < <span class="number">1000</span> {</span><br><span class="line"> f.count += <span class="number">3</span></span><br><span class="line"> <span class="keyword">return</span> <span class="comment">// 漏了 Unlock</span></span><br><span class="line"> }</span><br><span class="line"> f.count++</span><br><span class="line"> f.mu.Unlock()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>第二段,锁重入死锁。从 Java 转来的人最容易写,Go 的 Mutex 不可重入,<code>Bar</code> 里再 <code>Lock</code> 直接死锁:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *T)</span></span> Foo() {</span><br><span class="line"> t.mu.Lock()</span><br><span class="line"> <span class="keyword">defer</span> t.mu.Unlock()</span><br><span class="line"> t.Bar() <span class="comment">// Bar 内部又 Lock</span></span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *T)</span></span> Bar() {</span><br><span class="line"> t.mu.Lock() <span class="comment">// 死锁</span></span><br><span class="line"> <span class="keyword">defer</span> t.mu.Unlock()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>chao-go-sync</code> 的诊断框架对这两段走的路径不一样。第一段它先问「锁的获取/释放配不配对」,发现 <code>Lock</code> 后有分支提前 return 却没 <code>defer Unlock</code>,直接指出该用 <code>defer</code> 兜底;第二段它走的是「有没有锁重入」那条,点明 Go Mutex 不可重入,给的解法是重构,别让 <code>Foo</code> 持着锁调 <code>Bar</code>,要么把 <code>Bar</code> 拆成不加锁的内部方法 <code>barLocked</code>,由 <code>Foo</code> 在持锁状态下调。两个 Bug 都不是 race detector 能报的(race detector 只管数据竞争),靠的是 Skill 里那份「五必查」清单的经验。这就是它比通用 Agent 多出来的一层:知道去哪几个固定位置找问题。</p><h3 id="24-5-4-性能优化与版本迁移"><a href="#24-5-4-性能优化与版本迁移" class="headerlink" title="24.5.4 性能优化与版本迁移"></a>24.5.4 性能优化与版本迁移</h3><p>诊断之外,<code>chao-go-sync</code> 还给了一份锁选择决策:什么场景该用 RWMutex 替代 Mutex,什么场景该上分片锁、sync.Map、atomic,什么场景干脆 lock-free。它也覆盖 Go 1.20 到 1.27 的 sync 包变更,比如 <code>sync.OnceValue</code>/<code>OnceFunc</code>(1.21)、运行时 goroutine 泄漏检测(1.26)这些新东西,旧代码该不该迁移、怎么迁移。这跟 <code>/modern-go</code> 互补,<code>/modern-go</code> 管通用 API 的现代化,<code>chao-go-sync</code> 专管并发原语。</p><h2 id="24-6-go-style-guide:一份固执己见的-Go-工程契约"><a href="#24-6-go-style-guide:一份固执己见的-Go-工程契约" class="headerlink" title="24.6 go-style-guide:一份固执己见的 Go 工程契约"></a>24.6 go-style-guide:一份固执己见的 Go 工程契约</h2><p>前三个 Skill 偏「术」,现代化、性能、并发都是具体技术。<code>go-style-guide</code> 偏「道」,管的是 Go 代码的工程契约:包怎么设计、接口怎么定、错误怎么传、日志怎么打。作者是 madflojo(Benjamin Cane)。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add -g -y madflojo/go-style-agent-skill</span><br></pre></td></tr></table></figure><p>它最显眼的标签是 README 里那句 "Opinionated by design",刻意固执己见。它明确说,这不是通用、官方的 Go 风格指南,是个人偏好,要的是真实代码库里的一致性、可维护性和生产可用性。紧接着一句很关键:「如果你仓库已经有自己的约定,那些约定通常优先。」这一句把固执己见和尊重本地两边都顾上了,Skill 给默认值,但不覆盖既有规矩。</p><h3 id="24-6-1-十条-TL-DR"><a href="#24-6-1-十条-TL-DR" class="headerlink" title="24.6.1 十条 TL;DR"></a>24.6.1 十条 TL;DR</h3><p>Skill 用十条 TL;DR 浓缩了整套契约,挑几条最硬的。测试优先设计,先注入依赖、保持逻辑纯,再谈别的。Config 进、具体 struct 出,构造器吃 Config,显式校验和默认值。错误是契约,用 sentinel error 做持久分支判断,其余的用 <code>%w</code> 或 <code>errors.Join</code> 包裹。包要可复用,不藏全局变量、不藏默认日志、不搞意外副作用。标准库优先,第三方包必须靠「有意义、维护良好、被广泛采用」挣一席之地。accept interfaces, return structs,接口通常由消费者定义。覆盖率是信号不是证明,测边界和误用路径,别只测 happy path。声称性能提升前先 benchmark 热路径,并发代码跑 <code>-race</code>。</p><p>这些条款和 <code>chao-go-perf</code> 的「先测量再优化」、<code>chao-go-sync</code> 的并发安全说的是一回事,只是 <code>go-style-guide</code> 站在工程视角把它们串起来。</p><h3 id="24-6-2-执行协议:六步,先看仓库再动手"><a href="#24-6-2-执行协议:六步,先看仓库再动手" class="headerlink" title="24.6.2 执行协议:六步,先看仓库再动手"></a>24.6.2 执行协议:六步,先看仓库再动手</h3><p><code>go-style-guide</code> 不只是规则清单,它还有一份六步执行协议,规定 Skill 干活时的顺序:</p><ol><li>先看仓库:读现有包布局、构造器、测试、错误约定,再提新 API;</li><li>先定契约:包边界、Config 形状、返回类型、sentinel error、依赖缝隙、context/关停预期,写代码前先定;</li><li>尽早写测试:表格驱动单测起步,输入密集的加 fuzz,性能敏感的加 benchmark;</li><li>最小可维护改动:沿用仓库既有布局,<code>main.go</code> 保持薄,没明确边界别引入新抽象;</li><li>跑收尾检查:<code>gofmt</code>/<code>goimports</code>、相关 <code>go test</code>、并发跑 <code>-race</code>、声称性能就跑 benchmark;</li><li>验证面向人的契约:函数签名、Config 形状、错误行为、文档注释,和代码一样重要。</li></ol><p>这六步和第 16 章 agent-skills 的 Define→Plan→Build→Verify→Review→Simplify→Ship 是一个谱系,都是把资深工程师不会跳过的步骤固化成 Agent 必走的流程。配 10 份 reference(CONFIG、INTERFACES、ERRORS、LOGGING、DOCUMENTATION、LAYOUT、BENCHMARKS、TESTING、CONCURRENCY、REVIEW-CHECKLIST)按需加载,也就是第 2 章讲的渐进式信息披露。</p><h2 id="24-7-cc-skills-golang:拿数据说话的全家桶"><a href="#24-7-cc-skills-golang:拿数据说话的全家桶" class="headerlink" title="24.7 cc-skills-golang:拿数据说话的全家桶"></a>24.7 cc-skills-golang:拿数据说话的全家桶</h2><p>前四个 Skill 各管一面,<code>cc-skills-golang</code> 是把它们全做了一份、还做了交叉引用和效果评估的体系。作者 samber(samber/lo 的作者,Go 圈老熟人),两千多 stars,是本章体量最大的项目。</p><p>它的态度写在 README 最显眼的地方:</p><blockquote><p>Bootstrapped with Claude Code by distilling my Go project commits. <strong>Edited, tested, reviewed and reworked by a human.</strong><br><strong>No AI slop here.</strong> AI-made skills are useless.</p></blockquote><p>先用 Claude Code 把自己 Go 项目的提交蒸馏成 Skill 草稿,再由人编辑、测试、审查、返工。最后那句「No AI slop here. AI-made skills are useless.」是整个项目的主张:AI 生成的 Skill 没人把关就是废纸,人审过的才是 Skill。这跟本书第 2 章 Matt Pocock 的「real engineering, not vibe coding」、第 12 章的 maker-checker 分离是一个调子。</p><h3 id="24-7-1-28-原子-Skill,交叉引用"><a href="#24-7-1-28-原子-Skill,交叉引用" class="headerlink" title="24.7.1 28+ 原子 Skill,交叉引用"></a>24.7.1 28+ 原子 Skill,交叉引用</h3><p><code>cc-skills-golang</code> 把 Go 工程拆成 28+ 个原子、可交叉引用的 Skill,分四大类加一批框架/库 Skill:</p><table><thead><tr><th>大类</th><th>代表 Skill</th></tr></thead><tbody><tr><td>代码质量</td><td>code-style、naming、error-handling、safety、security、structs-interfaces、documentation、lint</td></tr><tr><td>架构与设计</td><td>concurrency、context、data-structures、database、dependency-injection、design-patterns、modernize</td></tr><tr><td>QA 与性能</td><td>testing、benchmark、performance、observability、troubleshooting</td></tr><tr><td>项目搭建</td><td>cli、continuous-integration、project-layout、popular-libraries、stay-updated、dependency-management</td></tr><tr><td>框架/库</td><td>grpc、graphql、google-wire、uber-dig/fx、spf13-cobra/viper、samber-lo/mo/ro/do/hot/slog/oops、testify</td></tr></tbody></table><p>注意里面的 <code>modernize</code>、<code>performance</code>、<code>concurrency</code>、<code>code-style</code>,和前四个 Skill 的职责正面重叠。samber 的处理方式是让 Skill 之间互相引用。比如错误处理影响日志的规则,就放在 <code>error-handling</code> 里,由 <code>observability</code> 引用,不各写一份。README 特意提醒,只装一部分会得到片面、可能不一致的视图,最好整套一起装。这套「原子 Skill 加交叉引用」的做法,和第 2 章的「小而可组合」、第 16 章的「24 个 Skill 覆盖完整生命周期」思路一样,只是规模更大。</p><h3 id="24-7-2-用数据说话:Skill-到底有没有用"><a href="#24-7-2-用数据说话:Skill-到底有没有用" class="headerlink" title="24.7.2 用数据说话:Skill 到底有没有用"></a>24.7.2 用数据说话:Skill 到底有没有用</h3><p><code>cc-skills-golang</code> 有一点别的前四个 Skill 都没有:它给 Skill 跑了评估,拿出了量化效果。</p><p><img src="/images/image-20260627085843606.png"></p><p>samber 给每个 Skill 跑了一套评估(evals),对比「装了 Skill」和「没装 Skill」时 Agent 的通过率,并算出「错误率下降幅度」(Error rate gap)。先看总账:</p><table><thead><tr><th></th><th>装了 Skill</th><th>没装 Skill</th><th>差距</th></tr></thead><tbody><tr><td>总体</td><td>3315/3395(98%)</td><td>1915/3395(56%)</td><td><strong>+41 个百分点</strong></td></tr></tbody></table><p>装了 Skill,Agent 通过率从 56% 跳到 98%。本书前面一直在主张 Skill 有用,到这里才头一回有硬数。再看单项,挑几个降幅最猛的:</p><table><thead><tr><th>Skill</th><th>错误率下降</th></tr></thead><tbody><tr><td><code>golang-modernize</code></td><td>-61%</td></tr><tr><td><code>golang-continuous-integration</code></td><td>-59%</td></tr><tr><td><code>golang-safety</code></td><td>-58%</td></tr><tr><td><code>golang-dependency-management</code></td><td>-54%</td></tr><tr><td><code>golang-documentation</code></td><td>-53%</td></tr><tr><td><code>golang-benchmark</code></td><td>-50%</td></tr></tbody></table><p><code>modernize</code> 降 61%,<code>safety</code> 降 58%,正好对应本章 <code>/modern-go</code>(现代化)和并发安全这两个最容易出错的领域,说明这些地方最需要 Skill 兜底。框架/库类里 <code>golang-samber-do</code>(依赖注入)最夸张,降 81%。这些数字替本书从第 1 章念到现在的「Skill 是 AI 软件工程的基石」补上了证据。</p><h3 id="24-7-3-token-预算:把渐进式信息披露做成了账"><a href="#24-7-3-token-预算:把渐进式信息披露做成了账" class="headerlink" title="24.7.3 token 预算:把渐进式信息披露做成了账"></a>24.7.3 token 预算:把渐进式信息披露做成了账</h3><p><code>cc-skills-golang</code> 还把第 2 章的渐进式信息披露做成了明账。每个 Skill 都标三档 token 权重:<code>description</code>(frontmatter 里的描述,常驻上下文,用于触发)、<code>SKILL.md</code>(触发后才加载的全文)、<code>Directory</code>(SKILL.md 加所有 reference)。比如 <code>golang-code-style</code> 是 115 / 2069 / 2685 token,<code>golang-security</code> 是 84 / 3036 / 21472 token。也就是说 security 这个 Skill 平时只占 84 token 趴在上下文里等触发,真用上了才把两万多 token 的全量知识加载进来。这比把所有规则一股脑塞进系统提示省得多。</p><h2 id="24-8-五个-Skill-怎么配合"><a href="#24-8-五个-Skill-怎么配合" class="headerlink" title="24.8 五个 Skill 怎么配合"></a>24.8 五个 Skill 怎么配合</h2><p>五个 Skill 有重叠,怎么选?给一张决策表:</p><table><thead><tr><th>你要解决的问题</th><th>首选 Skill</th><th>备选</th></tr></thead><tbody><tr><td>老代码升级到新 Go 写法</td><td><code>/modern-go</code></td><td>cc-skills-golang 的 <code>golang-modernize</code></td></tr><tr><td>代码慢、要优化</td><td><code>chao-go-perf</code></td><td>cc-skills-golang 的 <code>golang-performance</code>/<code>benchmark</code></td></tr><tr><td>并发 Bug 或并发设计</td><td><code>chao-go-sync</code></td><td>cc-skills-golang 的 <code>golang-concurrency</code></td></tr><tr><td>包设计、接口、错误契约</td><td><code>go-style-guide</code></td><td>cc-skills-golang 的 <code>golang-code-style</code> 等</td></tr><tr><td>想要一站式、且要效果有数据</td><td><code>cc-skills-golang</code>(整套)</td><td>——</td></tr></tbody></table><p>一个务实的组合:日常拿 <code>cc-skills-golang</code> 当底座(覆盖全、有评估、交叉引用),遇到性能和并发这两个深水区,再挂上 <code>chao-go-perf</code> 和 <code>chao-go-sync</code>,这两个的深度 samber 的全家桶比不了,一个搬了 Dave Cheney 工作坊,一个搬了整本并发书。<code>/modern-go</code> 嵌在 Goal Workflow 里,做提交前的现代化体检。<code>go-style-guide</code> 适合团队对齐工程契约时当起点。</p><p>重叠不可怕。<code>cc-skills-golang</code> 的 <code>golang-modernize</code> 和 <code>/modern-go</code> 规则高度相似,但 <code>/modern-go</code> 多了「读 go.mod、版本门控」这层保护;<code>golang-performance</code> 和 <code>chao-go-perf</code> 都讲 pprof,但后者多了一整本 go-perfbook 的方法论。重叠的地方正好见高下,比较着选就行。</p><h2 id="24-9-与全书方法论的对接"><a href="#24-9-与全书方法论的对接" class="headerlink" title="24.9 与全书方法论的对接"></a>24.9 与全书方法论的对接</h2><ul><li>第 2 章 Skills:这五个 Skill 是「原子 Skill」在 Go 领域的展开。<code>cc-skills-golang</code> 的 28+ 原子 Skill 加交叉引用,是「小而可组合」的体系级样本;<code>go-style-guide</code> 的 10 份 reference 按需加载,是渐进式信息披露的典型实现。</li><li>第 8 章 Goal Workflow:<code>/modern-go</code> 是 Goal Workflow 套件的 Bonus Skill,和 <code>/refactor</code>、<code>/smell</code> 同属代码库健康维护工具链。</li><li>第 16 章 agent-skills:<code>chao-go-perf</code> 的「先测量再优化」、<code>go-style-guide</code> 的六步执行协议,都是 agent-skills 验证门禁和反合理化表的同类,把 Agent 最爱跳过的步骤(不 benchmark 就优化、不看仓库就动手)钉成必须走的流程。</li><li>第 20 章 Anthropic 官方插件:官方插件注入领域知识和工程工作流,这五个 Go Skill 是社区版的 Go 领域知识插件,把 Go 二十年的门道封装成任何 Agent Skills 兼容工具都能用的能力。</li><li>第 23 章 重构:<code>/modern-go</code> 和 <code>/refactor</code>、<code>/smell</code> 是邻居,<code>/smell</code> 扫病灶,<code>/refactor</code> 套 Fowler 手法治,<code>/modern-go</code> 管版本现代化,三者一起维护代码库健康。</li></ul><h2 id="24-10-本章小结"><a href="#24-10-本章小结" class="headerlink" title="24.10 本章小结"></a>24.10 本章小结</h2><p>Go 这门语言表面小,门道深。地道写法绑版本,并发坑静默,性能靠测量,通用 Agent 写出的 Go 经常是能跑但不地道、不安全、不快。本章五个 Skill 把 Go 二十年的资深经验编码成 Agent 能可靠复用的能力:<code>/modern-go</code> 管现代化(35 条版本门控规则),<code>chao-go-perf</code> 管性能(先测量再优化),<code>chao-go-sync</code> 管并发(从 stdlib 到 etcd),<code>go-style-guide</code> 管工程契约(固执己见但尊重本地),<code>cc-skills-golang</code> 把这几样各做一份还顺带做了评估。<code>cc-skills-golang</code> 那张「装了 Skill 98%、没装 56%」的评估表,是本书到现在最硬的一块证据,给「Skill 是 AI 软件工程的基石」这句话补了数。</p><p>第 22 章读懂代码,第 23 章改好代码,这一章用 Go 专属技能把 Go 代码写地道、写安全、写快。但这些 Skill 解决的都还是「写」和「改」,代码写完了,谁来证明它真的对?下一章的 autoreview 和 Crabbox 接的就是这一棒,一个做自动化代码审查,一个在远程沙箱里真跑一遍验证。</p>
2026-06-28 17:30:00
<blockquote><p>「Any fool can write code that a computer can understand. Good programmers write code that humans can understand.」<br>任何傻瓜都能写出计算机能看懂的代码。好程序员写的是人能看懂的代码。<br>—— Martin Fowler</p></blockquote><p>第 22 章解决了「人怎么读懂 AI 写的代码」,用 UML 把代码画成图。</p><p>读懂之后呢?你打开 AI 生成的代码,能跑,但是一团乱麻:一个方法三百行,一个类管了八件事,同样的逻辑复制了五遍。这时候怎么办?</p><p>重构。</p><p>这件事本身不新鲜,Martin Fowler 1999 年就把它写成了一本书。变的是执行者。以前是人对着那本书一处一处手动改,现在是 AI 对着同一本书的目录自动改,人退到后面审查 diff。</p><span id="more"></span><p><img src="/images/image-20260626094732641.png"></p><p>本章分两半。前半讲重构的理论根基:什么是重构、技术债是怎么回事、什么时候该重构、Fowler 编了哪些坏味道和手法。这部分主要参考 Fowler 的《重构》,以及把这本书做成在线可检索版本的 <a href="https://refactoring.guru/refactoring">refactoring.guru</a>。后半讲 goal workflow 套件里的两个 Skill:<code>/refactor</code>(介绍页:<a href="https://goal.rpcx.io/index_cn.html#step-refactor">https://goal.rpcx.io/index_cn.html#step-refactor</a> )把 Fowler 第 2 版的整个目录封进了一个 AI Agent 能直接调用的能力里;<code>/smell</code>(介绍页:<a href="https://goal.rpcx.io/index_cn.html#step-smell">https://goal.rpcx.io/index_cn.html#step-smell</a> )则在它前面一步,负责扫出整个代码库到底哪里有问题。一个诊断,一个治疗。</p><h2 id="23-1-什么是重构,什么是技术债"><a href="#23-1-什么是重构,什么是技术债" class="headerlink" title="23.1 什么是重构,什么是技术债"></a>23.1 什么是重构,什么是技术债</h2><p>重构(refactoring)是在不改变外部行为的前提下改善代码内部结构。说白了,把一团乱麻(dirty code)整理成清爽的代码(clean code),但程序对外的表现一字不变。</p><ul><li><strong>脏代码</strong>是经验不足,再加上 deadline、管理混乱、开发途中走捷径,几样凑一起的产物。</li><li><strong>整洁代码</strong>易读、易懂、易维护,它让开发变得可预测。你大概知道改一处要花多久,而不是每次都掉进未知的坑。</li></ul><p>代码为什么会从整洁滑向肮脏?Ward Cunningham 的<strong>技术债</strong>这个说法解释得很到位。走捷径、跳过测试,就像找银行贷款:眼下买东西是快了,但要付利息。你不光还本金,还得还利息;利息攒够了,甚至会超过你的全部收入,永远还不清。代码也一样。不写测试能暂时提速,可它每天都在拖你后腿,直到哪天你补上测试,把债还掉。</p><p><img src="/images/image-20260626095233920.png"></p><p>refactoring.guru 列了一串技术债的成因。这些在 AI 时代值得重读,因为大多被放大了:</p><ul><li><strong>业务压力</strong>:功能没做完就得上线,于是补丁和权宜之计堆上去,盖住没收尾的部分。</li><li><strong>看不到后果</strong>:管理层不知道技术债有「利息」,债越欠越多、开发越来越慢,于是舍不得给重构留时间。</li><li><strong>强耦合没人管</strong>:项目变成一块铁板而不是一堆独立模块,动一处牵全身。</li><li><strong>缺测试</strong>:没有即时反馈,人就敢用又快又险的 workaround。最坏的情况是没测试直接上线,可能给几千个客户发出一封诡异的测试邮件,也可能直接把数据库清空。</li><li><strong>缺文档、缺沟通</strong>:新人上手慢;关键的人一走,开发就停摆。</li><li><strong>长期多分支并行</strong>:隔离的改动越多,合并时积下的债越大。</li><li><strong>重构一拖再拖</strong>:需求一直变,旧代码总有显得笨重的一天;可程序员每天还在往旧代码上接新代码,所以拖得越久,将来要返工的依赖就越多。</li></ul><p>「缺测试」和「重构一拖再拖」这两条在 AI 时代尤其要命。Agent 几分钟就能吐出几千行代码,没有特征测试兜底、没有及时整理,技术债的本金和利息会以人类时代见不到的速度滚起来。第 12 章讲 GSD Core 时提过的「反合理化表」,对抗的正是「以后再重构」「先跳过测试」这类自欺。它其实就是技术债成因的制度化解药。</p><h2 id="23-2-何时重构:三次法则"><a href="#23-2-何时重构:三次法则" class="headerlink" title="23.2 何时重构:三次法则"></a>23.2 何时重构:三次法则</h2><p>Fowler 的经验法则很简洁——<strong>三次法则(Rule of Three)</strong>:</p><ol><li><strong>第一次</strong>做某件事,直接做完。</li><li><strong>第二次</strong>做类似的事,心里别扭也照旧重复一遍。</li><li><strong>第三次</strong>再碰到同类的事,开始重构。</li></ol><p>第一次重复忍着,第三次出现就动手。除此之外还有三个天然时机:</p><ul><li><strong>加功能时</strong>:重构能帮你读懂别人的代码。面对一坨脏代码,先重构再加,整洁的代码好改得多。这一改不只方便你,也方便后面接手的人。</li><li><strong>修 bug 时</strong>:bug 跟现实里的虫子一样,专挑代码最暗最脏的角落待着。把代码清干净,错误几乎会自己冒出来。</li><li><strong>代码评审时</strong>:这往往是代码公开前最后一次整理的机会。最好跟作者结对来做,简单问题当场修掉,复杂的估个时间再说。</li></ul><h2 id="23-3-如何重构:正确的姿势"><a href="#23-3-如何重构:正确的姿势" class="headerlink" title="23.3 如何重构:正确的姿势"></a>23.3 如何重构:正确的姿势</h2><p>重构是一连串小改动,每一步让代码好一点,同时让程序始终能跑。refactoring.guru 的「做对了」清单只有三条,却是整套方法论的核心:</p><ul><li><strong>代码必须变干净。</strong> 重构完还是一团乱,那这一小时就白花了。这种事多半发生在你丢掉「小步改」、把一大堆重构混进一次大改动的时候。尤其赶 deadline 的时候,很容易把自己绕晕。</li><li><strong>重构期间不写新功能。</strong> 别把重构和加特性搅在一起,至少在单个 commit 里把两件事分开。</li><li><strong>重构后所有已有测试都得过。</strong> 测试挂了只有两种可能:要么你改错了,修就是;要么你的测试太底层了,比如去测了类的私有方法,这种情况是测试的锅,该把它重写成更高层、BDD 风格的测试。</li></ul><p>记住这三条。后半 <code>/refactor</code> Skill 的安全协议,基本就是把它们逐条机械化了一遍。</p><h2 id="23-4-Fowler-的目录:坏味道与手法"><a href="#23-4-Fowler-的目录:坏味道与手法" class="headerlink" title="23.4 Fowler 的目录:坏味道与手法"></a>23.4 Fowler 的目录:坏味道与手法</h2><p>refactoring.guru 把 Fowler 书里的内容编成了两张能检索的目录:</p><ul><li>**代码坏味道(Code Smells)**是问题的征兆,好发现,但它可能只是更深层问题露出的一角。</li><li>**重构手法(Refactoring Techniques)**是具体的改法。每种手法都有得有失,所以每次重构都该有明确的动机,谨慎施用。</li></ul><p>手法分六大类:组合方法(Composing Methods)、在对象间搬移特性(Moving Features Between Objects)、组织数据(Organizing Data)、简化条件表达式(Simplifying Conditional Expressions)、简化方法调用(Simplifying Method Calls)、处理泛化关系(Dealing with Generalization)。</p><p>整个流程是「症状到处方」:你先嗅到味道,比如「过长方法」或者「Feature Envy」,目录再告诉你该用哪几种手法去治。这套映射关系明确、能机械执行,也正是 AI Agent 接得住的地方。</p><h2 id="23-5-refactor:把-Fowler-的书变成一个-Skill"><a href="#23-5-refactor:把-Fowler-的书变成一个-Skill" class="headerlink" title="23.5 /refactor:把 Fowler 的书变成一个 Skill"></a>23.5 /refactor:把 Fowler 的书变成一个 Skill</h2><p>理论讲完,看看 <a href="https://goal.rpcx.io/index.html">goal workflow 套件</a>里的 <code>/refactor</code> 是怎么把这本书落成一个能调用的能力的。</p><p><code>/refactor</code> 基于 Fowler《重构》第 2 版的完整目录:识别坏味道,套用验证过的手法,在不动外部行为的前提下把代码改清爽。它在 goal workflow 主闭环(第 8 章)之外,是个 Bonus 技能。安装一行就够:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add smallnest/goal-workflow --skill refactor</span><br></pre></td></tr></table></figure><p>触发可以直接打 <code>/refactor</code>,也可以用自然语言或术语:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">重构:重构 UserManager 类,它太大了</span><br><span class="line">code smell:这个函数有 Feature Envy,修复它</span><br><span class="line">extract method:把这个长方法拆分成更小的函数</span><br></pre></td></tr></table></figure><h3 id="23-5-1-22-种坏味道(五大类别)"><a href="#23-5-1-22-种坏味道(五大类别)" class="headerlink" title="23.5.1 22 种坏味道(五大类别)"></a>23.5.1 22 种坏味道(五大类别)</h3><p>Skill 内置 22 种代码坏味道,按五大类组织,每种都关联到对应的重构手法:</p><table><thead><tr><th>类别</th><th>坏味道</th><th>主要重构手法</th></tr></thead><tbody><tr><td><strong>臃肿类</strong></td><td>过长方法</td><td>Extract Method, Replace Temp with Query</td></tr><tr><td></td><td>过大的类</td><td>Extract Class, Extract Subclass</td></tr><tr><td></td><td>基本类型偏执</td><td>Replace Data Value with Object</td></tr><tr><td></td><td>过长参数列表</td><td>Introduce Parameter Object</td></tr><tr><td></td><td>数据泥团</td><td>Extract Class</td></tr><tr><td><strong>OO 滥用类</strong></td><td>Switch 语句</td><td>Replace Conditional with Polymorphism</td></tr><tr><td></td><td>临时字段</td><td>Extract Class, Introduce Null Object</td></tr><tr><td></td><td>被拒绝的遗赠</td><td>Replace Inheritance with Delegation</td></tr><tr><td></td><td>异曲同工的类</td><td>Rename Method, Extract Superclass</td></tr><tr><td><strong>变更阻碍类</strong></td><td>发散式变化</td><td>Extract Class</td></tr><tr><td></td><td>霰弹式修改</td><td>Move Method, Move Field</td></tr><tr><td></td><td>平行继承体系</td><td>Move Method, Move Field</td></tr><tr><td><strong>冗余类</strong></td><td>注释(代码本应自说明)</td><td>Extract Method, Rename Variable</td></tr><tr><td></td><td>重复代码</td><td>Extract Method, Pull Up Method</td></tr><tr><td></td><td>冗赘类</td><td>Inline Class, Collapse Hierarchy</td></tr><tr><td></td><td>纯数据类</td><td>Move Method, Encapsulate Field</td></tr><tr><td></td><td>死代码</td><td>删除(Git 历史有记录)</td></tr><tr><td></td><td>夸夸其谈未来性</td><td>Inline Class, Remove Parameter</td></tr><tr><td><strong>耦合类</strong></td><td>依恋情节</td><td>Move Method</td></tr><tr><td></td><td>狎昵关系</td><td>Move Method, Move Field</td></tr><tr><td></td><td>消息链</td><td>Hide Delegate</td></tr><tr><td></td><td>中间人</td><td>Remove Middle Man</td></tr><tr><td></td><td>不完美的库类</td><td>Introduce Foreign Method</td></tr></tbody></table><h3 id="23-5-2-40-种重构手法(六大类)"><a href="#23-5-2-40-种重构手法(六大类)" class="headerlink" title="23.5.2 40+ 种重构手法(六大类)"></a>23.5.2 40+ 种重构手法(六大类)</h3><p><img src="/images/image-20260626095835777.png"></p><p>每种手法都附带机械步骤(mechanics)和 before/after 对比示例,这正是 refactoring.guru 那张目录的 AI 可执行版:</p><table><thead><tr><th>类别</th><th>手法数</th><th>代表性手法</th></tr></thead><tbody><tr><td>组合方法</td><td>9</td><td>Extract Method, Inline Method, Extract Variable, Replace Temp with Query, Substitute Algorithm</td></tr><tr><td>移动特性</td><td>7</td><td>Move Method, Move Field, Extract Class, Inline Class, Hide Delegate</td></tr><tr><td>组织数据</td><td>13</td><td>Replace Data Value with Object, Encapsulate Field, Replace Type Code with Subclasses, Replace Magic Number</td></tr><tr><td>简化条件</td><td>8</td><td>Decompose Conditional, Guard Clauses, Replace Conditional with Polymorphism, Introduce Null Object</td></tr><tr><td>方法调用</td><td>13</td><td>Rename Method, Separate Query from Modifier, Introduce Parameter Object, Replace Error Code with Exception</td></tr><tr><td>泛化</td><td>9</td><td>Pull Up Method, Push Down Method, Extract Interface, Form Template Method, Replace Inheritance with Delegation</td></tr></tbody></table><h3 id="23-5-3-五阶段安全协议:把「做对了」清单制度化"><a href="#23-5-3-五阶段安全协议:把「做对了」清单制度化" class="headerlink" title="23.5.3 五阶段安全协议:把「做对了」清单制度化"></a>23.5.3 五阶段安全协议:把「做对了」清单制度化</h3><p><code>/refactor</code> 真正关键的设计不是目录,而是<strong>安全协议</strong>。它把 23.3 节那三条清单变成了 Agent 必须照办的执行护栏:</p><ul><li><strong>准备阶段</strong>:写特征测试(characterization test),提交当前状态,开一个重构分支。</li><li><strong>每一步</strong>:一次只改一处,编译过,测试全过,提交。</li><li><strong>验证阶段</strong>:测试全过,手动冒烟测一遍,自己审 diff,最后提交。</li><li><strong>铁律</strong>:不动外部行为,不夹带功能改动,每一步都有测试兜底。</li></ul><p>对着 Fowler 的三条清单看,正好一一对上:「代码必须变干净」对应自审 diff,「不写新功能」对应不夹带功能改动,「测试必须全过」对应每步测试加特征测试兜底。</p><p>AI 时代的价值就在这。人在 deadline 底下,难免偷偷给自己找台阶,「这块以后再重构」「这次先跳过测试」。Agent 被钉死在协议上,没这个空子可钻。重构的纪律,从「靠人自觉」变成了「靠机器强制」。</p><h3 id="23-5-4-语言专属指南"><a href="#23-5-4-语言专属指南" class="headerlink" title="23.5.4 语言专属指南"></a>23.5.4 语言专属指南</h3><p>Skill 还针对主流语言给出特化建议:</p><table><thead><tr><th>语言</th><th>核心建议</th></tr></thead><tbody><tr><td>Java</td><td>final 局部变量、IDE 自动重构、Records、Sealed Classes</td></tr><tr><td>TypeScript</td><td>解构减少参数、const 优先、Union Types 替代类型码、<code>?.</code> 消除 null 检查</td></tr><tr><td>Python</td><td>Type Hints、dataclasses、<code>@property</code>、Context Managers</td></tr><tr><td>Go</td><td>小接口、命名返回值、表格驱动测试、early returns 消除嵌套</td></tr><tr><td>Rust</td><td>Result/Option 替代错误码和 null、Pattern Matching、From trait、Derive macros</td></tr></tbody></table><h3 id="23-5-5-在工作流中的位置"><a href="#23-5-5-在工作流中的位置" class="headerlink" title="23.5.5 在工作流中的位置"></a>23.5.5 在工作流中的位置</h3><p><code>/refactor</code> 不单干,它跟两个邻居配合,其中最重要的是下一节要单独讲的 <code>/smell</code>;另外 <code>/modern-go</code>(35+ 条 gofix 风格规则)跟它一样,都是用来保持代码库健康的工具。还有 <code>/review-it</code>——它的审查原则里写得很清楚:「Reject noise,拒绝不切实际的边界情况、投机性风险、过度重构」。这跟 <code>/refactor</code> 安全协议里「小修复优先」是一个意思——重构不是越多越好,闻到坏味道才动手。</p><h2 id="23-6-smell:先诊断,再开刀"><a href="#23-6-smell:先诊断,再开刀" class="headerlink" title="23.6 /smell:先诊断,再开刀"></a>23.6 /smell:先诊断,再开刀</h2><p><code>/refactor</code> 解决的是"知道哪里该改、怎么改"。但更常见的困境在前一步:面对一个 AI 攒出来的几万行代码库,你不知道该从哪下手。哪个模块烂得最厉害?是架构错了,还是只有某个函数太长?先动哪块?</p><p><code>/smell</code> 回答的就是这个。它跟 <code>/refactor</code> 同级,分工是:<code>/smell</code> 诊断,扫出代码库的病灶并排优先级;<code>/refactor</code> 治疗,照 Fowler 手法一处处修。一个出报告,一个动手术。</p><p>安装:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add smallnest/goal-workflow --skill smell</span><br></pre></td></tr></table></figure><p>触发同样可以直接打 <code>/smell</code>,或用自然语言:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">代码坏味道检测:找出代码坏味道</span><br><span class="line">架构审计:检测架构反模式</span><br><span class="line">复杂度扫描:分析代码复杂度</span><br></pre></td></tr></table></figure><h3 id="23-6-1-比-Fowler-更宽的视野:8-大类-50-坏味道"><a href="#23-6-1-比-Fowler-更宽的视野:8-大类-50-坏味道" class="headerlink" title="23.6.1 比 Fowler 更宽的视野:8 大类 50+ 坏味道"></a>23.6.1 比 Fowler 更宽的视野:8 大类 50+ 坏味道</h3><p>这里要留意 <code>/smell</code> 和 <code>/refactor</code> 在覆盖面上的区别。<code>/refactor</code> 盯的是 Fowler 那 22 种坏味道,基本都在函数和类这个尺度。<code>/smell</code> 往上抬了一层,把架构和复杂度也算进来,一共 8 大类、50+ 种:</p><table><thead><tr><th>类别</th><th>示例</th></tr></thead><tbody><tr><td>架构</td><td>大泥球、分布式单体、贫血模型、CQRS 滥用、层边界违反</td></tr><tr><td>耦合</td><td>循环依赖、内容耦合、公共耦合(全局状态)、印记耦合</td></tr><tr><td>内聚</td><td>上帝对象、霰弹式修改、依恋情结、数据泥团</td></tr><tr><td>设计</td><td>抽象泄露、静态粘连、服务定位器滥用、SOLID 违反</td></tr><tr><td>代码</td><td>重复代码、长方法、基本类型偏执、魔数、死代码</td></tr><tr><td>测试</td><td>零测试覆盖、测试-实现耦合、不稳定测试</td></tr><tr><td>命名</td><td>模糊命名、命名不一致</td></tr><tr><td>复杂度</td><td>嵌套循环 (O(n²))、N+1 查询、重复线性扫描、循环内排序、渲染重复计算</td></tr></tbody></table><p>最后两类,「测试」和「复杂度」,是 Fowler 的坏味道目录里没有的,而它们在 AI 时代恰好用得上。AI 生成的代码经常零测试覆盖,也经常埋着 N+1 查询、循环内排序这类一眼看不出、上了量才爆的性能坑。这些 <code>/smell</code> 能一并扫出来。</p><h3 id="23-6-2-输出:一份带优先级的重构路线图"><a href="#23-6-2-输出:一份带优先级的重构路线图" class="headerlink" title="23.6.2 输出:一份带优先级的重构路线图"></a>23.6.2 输出:一份带优先级的重构路线图</h3><p><code>/smell</code> 不只是列问题,它产出一份结构化的 Markdown 报告:</p><ul><li>执行摘要和整体健康评估</li><li>检测到的架构风格,对比它"应该是"的风格</li><li>按严重级别分类的发现:严重 / 警告 / 建议</li><li>依赖图分析和模块健康评分卡</li><li>8 大类坏味道的分布统计</li><li>一份重构路线图,分成「立即可做 / 短期 / 长期」</li></ul><p>最有价值的是最后那份路线图。它直接回答了开头那个问题:该先动哪块。你可以让它只跑「仅严重」模式做快速体检,也可以把范围缩到某个模块,或者只扫最近改动的文件。</p><h3 id="23-6-3-两个-Skill-怎么配合"><a href="#23-6-3-两个-Skill-怎么配合" class="headerlink" title="23.6.3 两个 Skill 怎么配合"></a>23.6.3 两个 Skill 怎么配合</h3><p><img src="/images/image-20260626100640873.png"></p><p>把 <code>/smell</code> 和 <code>/refactor</code> 串起来,就是一条完整的"健康维护"链路:</p><ol><li><strong><code>/smell</code> 出报告</strong>:定位病灶,排好优先级,知道先改哪、后改哪。</li><li><strong><code>/refactor</code> 逐条施工</strong>:针对报告里的具体坏味道,套 Fowler 手法,走五阶段安全协议,每步提交。</li><li>改完再跑一遍 <code>/smell</code>,看健康评分有没有真的涨上去。</li></ol><p>这正好呼应了 23.3 节那条铁律,「代码必须变干净」。<code>/smell</code> 的健康评分让"变干净"这件事从主观感受变成了可量化的前后对比:重构前一个分,重构后一个分,涨了才算没白干。</p><h2 id="23-7-小结"><a href="#23-7-小结" class="headerlink" title="23.7 小结"></a>23.7 小结</h2><p>重构的方法论二十年没怎么变:嗅坏味道、对着手法目录、小步施工、每步测试、不夹带功能。AI 时代变的只有一件事,把 Fowler 那本书从「人读的参考书」变成了「Agent 执行的 Skill」,再用安全协议把人最容易偷的懒,改成机器必须遵守的护栏。goal workflow 把这件事拆成两个 Skill:<code>/smell</code> 诊断,扫出整个代码库的病灶并排好优先级;<code>/refactor</code> 治疗,照着 Fowler 手法一处处修。先体检,再开刀,改完再体检,这就是 AI 时代维护代码库健康的闭环。</p><p>第 22 章用 UML 让你读懂 AI 写的代码,这一章用重构让你改好 AI 写的代码。读懂是前提,改好是落点。两件事合起来,才是「你可以外包思考,但不能外包理解」这句话在工程上真正站住脚的样子。</p>
2026-06-28 17:00:00
<blockquote><p>「A picture is worth a thousand words. A diagram is worth ten thousand lines of code.」<br>一图胜千言。一张图胜万行代码。</p></blockquote><p>第 13 章解决了一个问题:AI 写代码容易,读代码难。Understand-Anything 用知识图谱<strong>让 AI 理解现有代码</strong>。</p><p>反过来——代码写完了,作为人类<strong>你怎么理解它</strong>?毕竟,线上出了故障你还等着你背锅呢。</p><p>我前一段看到一句箴言:"𝐲𝐨𝐮 𝐜𝐚𝐧 𝐨𝐮𝐭𝐬𝐨𝐮𝐫𝐜𝐞 𝐲𝐨𝐮𝐫 𝐭𝐡𝐢𝐧𝐤𝐢𝐧𝐠, 𝐛𝐮𝐭 𝐲𝐨𝐮 𝐜𝐚𝐧𝐧𝐨𝐭 𝐨𝐮𝐭𝐬𝐨𝐮𝐫𝐜𝐞 𝐲𝐨𝐮𝐫 𝐮𝐧𝐝𝐞𝐫𝐬𝐭𝐚𝐧𝐝𝐢𝐧𝐠", 翻译过来就是"你可以外包你的思考(给AI),但是你不能外包你的理解"。 这句话被 Andrej Karpathy 多次引用,以至于大家认为是他说的,其实是kache说的:</p><p><img src="/images/image-20260604094614868.png"></p><p>这句话非常有哲理。Dex Horthy 在 2025 AI Engineer 大会上独立提出了:"Don't outsource the thinking" / "AI cannot replace thinking, it can only amplify the thinking you have done.",但是今年你看, AI已经外包了我们的思考,你只需说出的你需求,智能体就能帮助你生生成你要的程序,但是 AI 没有办法帮我们理解啊。</p><p>我最近就遇到了这样的困惑:我通过goal workflow很快的实现了一个大模型训推任务智能诊断系统,全是AI帮我生成的,但是在联调的前一个星期,我心虚了。</p><p>因为我知道,联调和上线的时候,必然有一些问题,比如当时的设计有些模糊的地方,设计上有gap, 实现上也难免有bug。如果我对生成的代码不熟悉,联调的时候出故障我都不知道啥原因咋修复,可能当时还得重新捋代码才能慢慢找根因,太影响联调的同学了。未来上线以后出现问题,想快速修复就更不可能了。</p><p>所以我专门花了两天时间,建了几个卡片,就为了学习代码理解代码。</p><p>那我是通过什么方式去理解AI生成的代码的呢?</p><p>答案藏在一个用了二十多年的老工具里:UML。区别只有一点:以前的 UML 是人画给团队的,现在是 AI 画给你的。十四种图,从类结构到部署拓扑,从序列交互到状态变迁。AI 生成代码,AI 再画图解释代码——你读图就够了。</p><p>为此,我专门创建了一个Skill,用来生成UML的十四种代码和架构图、流程图以及泳道图。此skill的介绍:<a href="https://goal.rpcx.io/index_cn.html#step-diagram%EF%BC%8C">https://goal.rpcx.io/index_cn.html#step-diagram,</a> 也集成到了goal workflow套件中了。</p><p>本章分两部分:第一部分过一遍 UML 十四种正式图形,外加三种 UML 规范没有但实际很常用的图。第二部分介绍 insight-diagram——一个在 goal.rpcx.io 上发布的 Skill,给任意代码库自动生成全套 UML 图、架构图和流程图。</p><span id="more"></span><h2 id="14-1-UML-简史:从三剑客到-OMG-标准"><a href="#14-1-UML-简史:从三剑客到-OMG-标准" class="headerlink" title="14.1 UML 简史:从三剑客到 OMG 标准"></a>14.1 UML 简史:从三剑客到 OMG 标准</h2><p>1994 年,Grady Booch 和 Jim Rumbaugh 在 Rational Software 相遇。Booch 有他的 Booch Method,Rumbaugh 有他的 OMT(Object Modeling Technique)。两种方法符号体系不同,画出来的图互不认识。</p><p>1995 年,Ivar Jacobson 加入,带来他的 OOSE(Object-Oriented Software Engineering)方法。三人被称为「三剑客」(Three Amigos),目标:统一面向对象建模的符号体系,让所有软件工程师用同一套语言画图。</p><p>1997 年,OMG(Object Management Group)采纳 UML 1.0。2005 年,UML 2.0 图形种类从 9 种扩展到 13 种。最新版本 UML 2.5.1(2017 年)定义了 14 种图,分为两大类:</p><ul><li><strong>结构性图形(Structure Diagrams)</strong>:描述系统的静态结构——有哪些组件、怎么组织的。</li><li><strong>行为性图形(Behavior Diagrams)</strong>:描述系统的动态行为——怎么运行、怎么交互。</li></ul><p>AI 时代之前,UML 的命运比较尴尬。敏捷宣言之后,很多人觉得「重文档轻代码」不靠谱,UML 跟着被冷落了。但有一个事实没变:代码由 AI 大量生成之后,人类比以往更需要可视化理解工具。UML 重新有了用武之地——角色从「先画图再写代码」的前置仪式,变成了「AI 写代码,你读图」的后置理解工具。</p><h2 id="14-2-结构性图形(Structure-Diagrams):系统长什么样"><a href="#14-2-结构性图形(Structure-Diagrams):系统长什么样" class="headerlink" title="14.2 结构性图形(Structure Diagrams):系统长什么样"></a>14.2 结构性图形(Structure Diagrams):系统长什么样</h2><p>结构性图形回答「系统由什么组成」——类、对象、组件、包、部署节点。它们是系统的静态 X 光片。</p><h3 id="14-2-1-类图(Class-Diagram)"><a href="#14-2-1-类图(Class-Diagram)" class="headerlink" title="14.2.1 类图(Class Diagram)"></a>14.2.1 类图(Class Diagram)</h3><p>最常用的 UML 图。展示系统中的类、接口、协作以及它们之间的关系。</p><p><strong>画什么:</strong></p><ul><li>类名、属性、方法——每个类一个矩形框,分成三格</li><li>关系类型:泛化(继承,空心三角箭头)、实现(虚线空心三角)、关联(实线)、聚合(空心菱形,整体-部分弱关系)、组合(实心菱形,整体-部分强关系,同生命周期)、依赖(虚线箭头)</li></ul><p><strong>什么时候用:</strong> 看项目的领域模型。agent-wrapper 项目的 <code>Agent</code>、<code>Task</code>、<code>Tool</code>、<code>Message</code> 等核心类型及其继承和组合关系。</p><p><strong>AI 时代的价值:</strong> AI 生成 50 个类之后,类图让你一眼看清谁继承了谁、谁组合了谁——不用在 50 个文件之间反复横跳。</p><h3 id="14-2-2-对象图(Object-Diagram)"><a href="#14-2-2-对象图(Object-Diagram)" class="headerlink" title="14.2.2 对象图(Object Diagram)"></a>14.2.2 对象图(Object Diagram)</h3><p>类图的运行时快照。展示特定时刻的对象实例及其属性值和链接关系。</p><p><strong>画什么:</strong></p><ul><li>对象名:类名(加下划线标注)、属性的具体值、实例间的链接</li><li>比如 <code>agent1: Agent { name="code-generator", model="claude-4" }</code> 连接到 <code>task42: Task { status="running" }</code></li></ul><p><strong>什么时候用:</strong> 调试时的内存快照。复杂对象图在某个时刻的状态——帮助看清递归结构、循环引用、单例模式的具体实例分布。</p><h3 id="14-2-3-组件图(Component-Diagram)"><a href="#14-2-3-组件图(Component-Diagram)" class="headerlink" title="14.2.3 组件图(Component Diagram)"></a>14.2.3 组件图(Component Diagram)</h3><p>比类图高一层——展示软件架构中可替换的模块化组件及其接口。</p><p><strong>画什么:</strong></p><ul><li>组件(矩形,带 <code><<component>></code> 标识)、提供的接口(棒棒糖符号)、需要的接口(插座符号)</li><li>组件间通过接口的依赖关系</li></ul><p><strong>什么时候用:</strong> 理解微服务架构或模块化系统中组件之间的契约关系。agent-wrapper 中 <code>LLM Provider</code> 组件提供 <code>chat()</code> 接口,<code>Agent Runtime</code> 组件消费这个接口。</p><h3 id="14-2-4-部署图(Deployment-Diagram)"><a href="#14-2-4-部署图(Deployment-Diagram)" class="headerlink" title="14.2.4 部署图(Deployment Diagram)"></a>14.2.4 部署图(Deployment Diagram)</h3><p>把软件映射到硬件。展示运行时处理节点及驻留在节点上的构件。</p><p><strong>画什么:</strong></p><ul><li>节点(三维立方体):服务器、容器、设备</li><li>构件(矩形):部署在节点上的软件模块</li><li>通信路径:节点间的网络连接,可标注协议(HTTP/REST、gRPC、消息队列)</li></ul><p><strong>什么时候用:</strong> 理解生产环境的物理拓扑。agent-wrapper 的部署:User's Machine 节点运行 CLI,Cloud VM 节点运行 Agent Runtime,外部节点 Anthropic API。</p><h3 id="14-2-5-包图(Package-Diagram)"><a href="#14-2-5-包图(Package-Diagram)" class="headerlink" title="14.2.5 包图(Package Diagram)"></a>14.2.5 包图(Package Diagram)</h3><p>按逻辑分组组织模型元素。包可以嵌套,包之间有依赖和导入关系。</p><p><strong>画什么:</strong></p><ul><li>包(文件夹图标或带标签的矩形框)、包间的依赖箭头、包的嵌套层次</li><li>体现分层架构:<code>domain/</code> → <code>application/</code> → <code>infrastructure/</code> → <code>presentation/</code></li></ul><p><strong>什么时候用:</strong> 大项目的模块组织概览。agent-wrapper 中 <code>core/</code> 包、<code>tools/</code> 包、<code>plugins/</code> 包、<code>cli/</code> 包的依赖方向。</p><h3 id="14-2-6-复合结构图(Composite-Structure-Diagram)"><a href="#14-2-6-复合结构图(Composite-Structure-Diagram)" class="headerlink" title="14.2.6 复合结构图(Composite Structure Diagram)"></a>14.2.6 复合结构图(Composite Structure Diagram)</h3><p>深入一个类的内部。展示类的部件(Part)、端口(Port)、连接器(Connector)以及它们之间的协作。</p><p><strong>画什么:</strong></p><ul><li>部件(类内部的组件实例)、端口(小方块,部件与外界的交互点)、连接器(部件间的连线)</li><li>比类图更微观——类图展示「哪些类」,复合结构图展示「这个类内部由哪些部分构成」</li></ul><p><strong>什么时候用:</strong> 理解复杂模式(如 Observer、Visitor)的运行时内部结构。agent-wrapper 中 <code>AgentLoop</code> 类的内部:<code>Planner</code> 部件 → <code>Executor</code> 部件 → <code>Verifier</code> 部件,通过端口相互连接。</p><h3 id="14-2-7-剖面图(Profile-Diagram)"><a href="#14-2-7-剖面图(Profile-Diagram)" class="headerlink" title="14.2.7 剖面图(Profile Diagram)"></a>14.2.7 剖面图(Profile Diagram)</h3><p>UML 的扩展机制。定义特定领域或平台的方言——自定义构造型(Stereotype)、标签值(Tagged Value)、约束(Constraint)。</p><p><strong>画什么:</strong></p><ul><li><code><<stereotype>></code> 定义、用构造型标注的元类扩展</li><li>比如定义一个 <code><<microservice>></code> 构造型,标注 <code>language</code>、<code>port</code>、<code>healthCheck</code> 等标签值</li></ul><p><strong>什么时候用:</strong> 当 UML 需要说团队自己的术语时。agent-wrapper 项目可以定义 <code><<skill>></code>、<code><<tool>></code>、<code><<hook>></code> 等自定义构造型。</p><h2 id="14-3-行为性图形(Behavior-Diagrams):系统怎么运行"><a href="#14-3-行为性图形(Behavior-Diagrams):系统怎么运行" class="headerlink" title="14.3 行为性图形(Behavior Diagrams):系统怎么运行"></a>14.3 行为性图形(Behavior Diagrams):系统怎么运行</h2><p>行为性图形回答「系统怎么工作」——流程、交互、状态变迁。它们是系统的动态录像。</p><h3 id="14-3-1-用例图(Use-Case-Diagram)"><a href="#14-3-1-用例图(Use-Case-Diagram)" class="headerlink" title="14.3.1 用例图(Use Case Diagram)"></a>14.3.1 用例图(Use Case Diagram)</h3><p>从用户视角展示系统能做什么。外行最容易看懂的 UML 图。</p><p><strong>画什么:</strong></p><ul><li>参与者(火柴人图标):人、外部系统、定时器</li><li>用例(椭圆):系统提供的功能</li><li>关系:<code><<include>></code>(必须包含)、<code><<extend>></code>(可选扩展)、泛化(参与者或用例的继承)</li></ul><p><strong>什么时候用:</strong> 和产品经理对齐需求。agent-wrapper 的用例:用户 → 「创建 Agent」、用户 → 「配置 Skill」、用户 → 「启动自主循环」、外部 API → 「响应 Tool Call」。</p><h3 id="14-3-2-活动图(Activity-Diagram)"><a href="#14-3-2-活动图(Activity-Diagram)" class="headerlink" title="14.3.2 活动图(Activity Diagram)"></a>14.3.2 活动图(Activity Diagram)</h3><p>增强版流程图。展示从活动到活动的控制流,支持并发分支和合并。</p><p><strong>画什么:</strong></p><ul><li>活动节点(圆角矩形)、决策节点(菱形)、fork/join(粗横线)、泳道(可选)</li><li>开始节点(实心圆)→ 活动序列 → 结束节点(实心圆+外圈)</li></ul><p><strong>什么时候用:</strong> 描述一个流程的多分支执行路径。agent-wrapper 中 Goal 命令的执行流程:解析 Goal → 生成 Plan → fork(并行执行 Sub-goals)→ join(汇总结果)→ 验证 → 输出。</p><h3 id="14-3-3-状态机图(State-Machine-Diagram)"><a href="#14-3-3-状态机图(State-Machine-Diagram)" class="headerlink" title="14.3.3 状态机图(State Machine Diagram)"></a>14.3.3 状态机图(State Machine Diagram)</h3><p>跟踪一个对象从生到死的完整生命周期——中间经历了哪些状态,什么事件触发了状态变迁。</p><p><strong>画什么:</strong></p><ul><li>状态(圆角矩形,可包含 entry/do/exit 动作)、转移(箭头上标注「事件[守卫]/动作」)</li><li>初始伪状态(实心圆)、终止状态(实心圆+外圈)</li></ul><p><strong>什么时候用:</strong> 理解有复杂生命周期的对象。agent-wrapper 中 <code>Task</code> 对象的状态机:pending → queued → running → (completed | failed | cancelled),每个转移上标注触发事件。</p><h3 id="14-3-4-序列图(Sequence-Diagram)"><a href="#14-3-4-序列图(Sequence-Diagram)" class="headerlink" title="14.3.4 序列图(Sequence Diagram)"></a>14.3.4 序列图(Sequence Diagram)</h3><p>最常用的交互图。按时间顺序展示对象间的消息交换——谁先调谁、传什么参数、返回什么。</p><p><strong>画什么:</strong></p><ul><li>生命线(纵向虚线,代表对象的时间线)、激活条(生命线上的矩形,代表对象正在执行)</li><li>消息(水平箭头):同步调用(实心箭头)、异步消息(开放箭头)、返回(虚线箭头)</li></ul><p><strong>什么时候用:</strong> 理解一个具体场景的完整调用链。agent-wrapper 中「创建并运行一个 Agent」的完整交互序列:CLI → AgentFactory.create() → Agent.run() → LLMProvider.chat() → ToolRegistry.execute() → 返回结果 → CLI 输出。</p><h3 id="14-3-5-通信图(Communication-Diagram)"><a href="#14-3-5-通信图(Communication-Diagram)" class="headerlink" title="14.3.5 通信图(Communication Diagram)"></a>14.3.5 通信图(Communication Diagram)</h3><p>和序列图共享同一批信息,视角不同。序列图强调时间顺序(从上到下),通信图强调对象间的组织关系(空间布局)。</p><p><strong>画什么:</strong></p><ul><li>对象(矩形节点)、链接(对象间的连线)、消息(连线上标注序号和箭头方向)</li><li>消息编号(1, 2, 3...)表示时间顺序,嵌套用 1.1, 1.2...</li></ul><p><strong>什么时候用:</strong> 关注「在某个时刻,哪些对象在互相通信」时——对象间的链接结构成为重点,时间线退居次要。</p><h3 id="14-3-6-定时图(Timing-Diagram)"><a href="#14-3-6-定时图(Timing-Diagram)" class="headerlink" title="14.3.6 定时图(Timing Diagram)"></a>14.3.6 定时图(Timing Diagram)</h3><p>展示对象状态随时间的变化曲线,含精确的时间约束。</p><p><strong>画什么:</strong></p><ul><li>时间轴(横向,精确刻度)、状态线(折线,展示每个对象的状态变化)、持续时间约束 <code>{duration < 500ms}</code></li><li>适合实时系统、嵌入式系统和性能关键路径</li></ul><p><strong>什么时候用:</strong> 性能分析和实时约束验证。agent-wrapper 中 Tool Call 的超时机制:<code>execute_tool()</code> 必须在 30s 内完成,否则触发重试——定时图展示这四个阶段的时间约束。</p><h3 id="14-3-7-交互概览图(Interaction-Overview-Diagram)"><a href="#14-3-7-交互概览图(Interaction-Overview-Diagram)" class="headerlink" title="14.3.7 交互概览图(Interaction Overview Diagram)"></a>14.3.7 交互概览图(Interaction Overview Diagram)</h3><p>活动图和序列图的混合体。用活动图的控制流框架,每个节点可以嵌入一个交互片段(序列图、通信图、定时图或其他交互概览图)。</p><p><strong>画什么:</strong></p><ul><li>框架是活动图(决策节点、fork/join、开始/结束)</li><li>节点内部嵌入 <code>sd</code>(序列图)等交互片段</li></ul><p><strong>什么时候用:</strong> 复杂流程的宏观概览——不展示每次函数调用的细节,而是展示「这个流程分几大阶段,每个阶段内部发生了什么交互」。agent-wrapper 完整运行流程:初始化(序列图)→ 规划(序列图)→ 执行循环(交互概览图)→ 完成(序列图)。</p><h2 id="14-4-超越-UML:三种实用的非-UML-图"><a href="#14-4-超越-UML:三种实用的非-UML-图" class="headerlink" title="14.4 超越 UML:三种实用的非 UML 图"></a>14.4 超越 UML:三种实用的非 UML 图</h2><p>上面 14 种是 UML 2.5 标准定义的。真实软件工程中,有三种图不在 UML 规范里,但使用频率比很多 UML 图更高。</p><h3 id="14-4-1-系统架构图(Architecture-Diagram)"><a href="#14-4-1-系统架构图(Architecture-Diagram)" class="headerlink" title="14.4.1 系统架构图(Architecture Diagram)"></a>14.4.1 系统架构图(Architecture Diagram)</h3><p>比组件图更自由。展示系统的顶层组件、外部依赖以及数据流方向。不遵守 UML 严格的符号约束——方块、圆圈、数据库图标、云图标都可以用,含义清晰即可。</p><p><strong>什么时候用:</strong> 任何项目的第一个图。项目经理、新成员、外部评审者——所有人都从架构图开始理解系统。</p><h3 id="14-4-2-流程图(Flowchart)"><a href="#14-4-2-流程图(Flowchart)" class="headerlink" title="14.4.2 流程图(Flowchart)"></a>14.4.2 流程图(Flowchart)</h3><p>比活动图更简单。没有 fork/join 的并发语义,没有泳道的组织维度——只有开始、结束、处理步骤、判断分支。它就是画在白板上的那种图。</p><p><strong>什么时候用:</strong> 描述算法逻辑、业务流程或决策树。不追求 UML 的完备语义,追求最大可读性。</p><h3 id="14-4-3-泳道图(Swimlane-Diagram)"><a href="#14-4-3-泳道图(Swimlane-Diagram)" class="headerlink" title="14.4.3 泳道图(Swimlane Diagram)"></a>14.4.3 泳道图(Swimlane Diagram)</h3><p>流程图 + 角色分工。把流程步骤按「谁负责」分配到不同的泳道里,跨泳道的箭头表示交互和交接。</p><p><strong>什么时候用:</strong> 跨团队协作流程。agent-wrapper 中用户、Agent Runtime、LLM Provider、Tool Registry 四个泳道的完整交互流程。</p><h2 id="14-5-UML-在-AI-时代的三个用途"><a href="#14-5-UML-在-AI-时代的三个用途" class="headerlink" title="14.5 UML 在 AI 时代的三个用途"></a>14.5 UML 在 AI 时代的三个用途</h2><p>AI 生成代码之后,UML 的角色从「前置设计工具」变成了「后置理解工具」。三个具体用途:</p><p><strong>用途一:代码理解的可视化层。</strong> 第 13 章的 Understand-Anything 产出知识图谱——文件、函数、类的节点和边。UML 把这些节点组织成有意义的模式——类图把散落的类型组织成领域模型,序列图把函数调用链组织成交互故事。知识图谱是原材料,UML 是成品。</p><p><strong>用途二:AI 生成代码的质量验证。</strong> AI 生成了 2000 行代码。你怎么知道架构没被写烂?生成一张类图——继承链有没有循环引用一目了然。生成一张包图——依赖方向有没有违反 Clean Architecture 的箭头规则。UML 就是 AI 代码的架构 X 光。</p><p><strong>用途三:团队沟通的通用语言。</strong> 不同的人看同一段 AI 代码,脑子里画的图可能完全不一样。UML 给了一套标准化的可视化符号,所有人坐下来看同一张图,对「这段代码长什么样」的理解不会差太远。</p><h2 id="14-6-insight-diagram:一键生成全套-UML-图的-Skill"><a href="#14-6-insight-diagram:一键生成全套-UML-图的-Skill" class="headerlink" title="14.6 insight-diagram:一键生成全套 UML 图的 Skill"></a>14.6 insight-diagram:一键生成全套 UML 图的 Skill</h2><p>前面聊了 17 种图。一个中等规模的项目,手工画完,单位是天。</p><p>insight-diagram 在 <a href="https://goal.rpcx.io/index_cn.html#step-diagram">goal.rpcx.io</a> 上发布,给任意代码库自动生成全套 UML 图、架构图和流程图。不需要你懂 UML——告诉它项目在哪,它分析代码库、让你选图表类型,然后一张张生成。</p><p><img src="/images/image-20260604090531683.png"></p><h3 id="14-6-1-核心理念:代码分析-→-图表选择-→-逐个生成"><a href="#14-6-1-核心理念:代码分析-→-图表选择-→-逐个生成" class="headerlink" title="14.6.1 核心理念:代码分析 → 图表选择 → 逐个生成"></a>14.6.1 核心理念:代码分析 → 图表选择 → 逐个生成</h3><p>四个步骤:</p><p><strong>步骤 1 — 分析代码库。</strong> Skill 读取项目的 <code>CLAUDE.md</code>(项目概览)、各子目录的 <code>CLAUDE.md</code>(模块细节),用 Glob 扫描源码文件结构,用 Grep 搜索关键模式(接口定义、函数签名、依赖注入)。提炼出组件清单、依赖关系图、核心类型、业务流程和部署拓扑。</p><p><strong>步骤 2 — 选择图表。</strong> 多选菜单,17 种图表分四组展示:结构性图形(类图、对象图、组件图、部署图、包图、复合结构图、剖面图)、行为性图形(用例图、活动图、状态机图)、交互图(序列图、通信图、定时图、交互概览图)、实用非 UML(系统架构图、流程图、泳道图)。默认推荐:architecture + sequence + flowchart。</p><p><strong>步骤 3 — 逐个生成。</strong> 每个图表的生成流程:</p><ol><li><strong>先读示例。</strong> Skill 内置 13 个示例 HTML 文件,每种图表类型一个。生成前从示例中提取布局策略(节点间距、分组方式、箭头走向)、节点样式层级(核心节点高亮、普通节点实线边框、可选节点虚线边框)、标注风格(阶段标签、Legend 图例、卡片摘要)和信息密度。</li><li><strong>整理元素和关系。</strong> 根据步骤 1 的分析结果,确定元素和关系。类图:10-15 个核心类型。序列图:2-5 个核心交互场景。每个图有明确关注重点,不追求全量覆盖。</li><li><strong>调用 architecture-diagram skill 渲染。</strong> 使用 Anthropic Claude 视觉风格(暖白背景 <code>#FAF9F6</code>、terracotta/sage/plum/rose 配色、Inter 字体),输出 HTML+SVG。所有元素不得遮盖——箭头在节点下方、间距充足(垂直 ≥40px,水平 ≥30px)、文字不溢出。元素过多则拆分子图或缩小元素。</li><li><strong>保存到 <code>docs/</code> 目录。</strong> 文件命名如 <code>docs/architecture.html</code>、<code>docs/class.html</code>、<code>docs/sequence.html</code>。</li></ol><p><strong>步骤 4 — 报告。</strong> 全部生成完成后输出文件列表和每张图的简要描述。</p><h3 id="14-6-2-图表生成顺序:从宏观到微观"><a href="#14-6-2-图表生成顺序:从宏观到微观" class="headerlink" title="14.6.2 图表生成顺序:从宏观到微观"></a>14.6.2 图表生成顺序:从宏观到微观</h3><p>生成顺序是固定的,不是随机的:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">architecture → component → deployment → package → composite-structure →</span><br><span class="line">profile → class → object → usecase → flowchart → activity → state-machine →</span><br><span class="line">swimlane → sequence → communication → timing → interaction-overview</span><br></pre></td></tr></table></figure><p>从架构图开始,逐步深入到组件、部署、包结构、类的内部结构,再到行为层面的用例、流程、状态、交互。每一步都踩在上一步提取的信息上——生成类图的时候,架构图和组件图已经把系统的主要模块和边界画出来了。</p><h3 id="14-6-3-关键设计决策"><a href="#14-6-3-关键设计决策" class="headerlink" title="14.6.3 关键设计决策"></a>14.6.3 关键设计决策</h3><p><strong>示例驱动,而非提示词驱动。</strong> Skill 不给 LLM 一段「画类图的提示词」——给示例 HTML 文件。LLM 从示例中提取布局模式、色彩方案、节点样式,用自己的内容替换。LLM 做「模仿已有模板然后替换内容」这件事远比做「根据纯文本描述从零生成视觉设计」靠谱。</p><p><strong>专用渲染 Skill 分离。</strong> insight-diagram 不做渲染——它调用 <code>/architecture-diagram</code>(另一个 Skill)生成 HTML+SVG。职责分离:insight-diagram 负责「分析代码、选择图表、整理元素」,architecture-diagram 负责「把元素渲染为 HTML+SVG」。这是 Skill 系统中的组合模式——一个 Skill 依赖另一个 Skill。</p><p><strong>防遮盖规则。</strong> SVG 元素不得互相遮盖。箭头绘制在节点下方(SVG 中先画箭头再画节点)。节点间最小间距 40px(垂直)、30px(水平)。文字不超出节点边界——超长截断或换行。这些规则在生成时强制检查,确保产出图表可直接用于文档和演示,跳过人工微调步骤。</p><p><strong>语言无关。</strong> 支持 Go、Python、TypeScript、Java、Rust 等主流语言。分析代码库使用 Glob + Grep + Read——不依赖语言特定解析器,对所有语言有基本覆盖。需要精确提取「所有类和它们的方法签名」的类图时,建议与 Understand-Anything(第 13 章)组合使用——Understand-Anything 提供精确的结构数据,insight-diagram 负责可视化。</p><h3 id="14-6-4-与-Understand-Anything-的配合"><a href="#14-6-4-与-Understand-Anything-的配合" class="headerlink" title="14.6.4 与 Understand-Anything 的配合"></a>14.6.4 与 Understand-Anything 的配合</h3><p>第 13 章和本章是互补关系:</p><table><thead><tr><th>维度</th><th>Understand-Anything</th><th>insight-diagram</th></tr></thead><tbody><tr><td>产出</td><td>知识图谱(JSON)</td><td>UML/架构/流程 图(HTML+SVG)</td></tr><tr><td>精度</td><td>确定性(Tree-sitter 解析)</td><td>LLM 推断(基于代码阅读)</td></tr><tr><td>用途</td><td>Agent 查询、影响分析、导航</td><td>人类阅读、文档、演示</td></tr><tr><td>目标读者</td><td>AI Agent</td><td>人类团队</td></tr><tr><td>更新方式</td><td>增量(commit hook 触发)</td><td>按需(手动触发重新生成)</td></tr></tbody></table><p>最佳实践:Understand-Anything 产出的知识图谱提供精确的组件清单和依赖关系,insight-diagram 把信息可视化为标准 UML 图形。确定性的结构数据 + LLM 的视觉表达——和 Understand-Anything 的 Tree-sitter + LLM 混合架构是同一设计哲学。</p><h3 id="14-6-5-在-agent-wrapper-上实战"><a href="#14-6-5-在-agent-wrapper-上实战" class="headerlink" title="14.6.5 在 agent-wrapper 上实战"></a>14.6.5 在 agent-wrapper 上实战</h3><p>实际上只生成了架构图、流程图和几个UML图就已经可以充分理解AI生成的代码了,这里我生成了所有的图片,主要是为了让你了解生成的各个图形的效果 。我更多会看架构图、流程图、类图、序列图、泳道图等。</p><p>有些生成的图形连线有些错误,图形有些重叠,你可以给智能体指出错误,让它修复,生成完美的图形。下面有些图生成的不是很完美,我也贴出来了,没有进一步优化。</p><p>agent-wrapper(<code>/Users/smallnest/ai/agent-wrapper</code>)是 Go 语言实现的 AI Agent 包装框架,核心功能包括 Agent 生命周期管理、Skill 注册与调度、Tool 调用链、LLM Provider 抽象和多轮对话状态机。项目包含 20+ 个 Go 源文件,分布在 <code>core/</code>、<code>tools/</code>、<code>plugins/</code>、<code>cli/</code> 等包中。</p><p>使用 insight-diagram 生成全套图表的过程:</p><ol><li><strong>调用 Skill。</strong> <code>/insight-diagram</code>,Skill 读取 <code>CLAUDE.md</code>、扫描源码结构、提取组件和接口。</li><li><strong>选择图表。</strong> 默认推荐三张图(architecture + sequence + flowchart),覆盖「系统怎么组织的、关键交互怎么发生、主流程怎么走」。</li><li><strong>逐个生成。</strong> Skill 依次读取对应示例、整理元素、调用 architecture-diagram 渲染、保存到 <code>docs/</code>。</li><li><strong>人工补充。</strong> 生成的图表作为基础版本,微调后补充到项目文档。</li></ol><h4 id="agent-wrapper-系统架构图(Architecture-Diagram)"><a href="#agent-wrapper-系统架构图(Architecture-Diagram)" class="headerlink" title="agent-wrapper 系统架构图(Architecture Diagram)"></a>agent-wrapper 系统架构图(Architecture Diagram)</h4><p><img src="/images/image-20260604091042656.png"></p><p>这张图把 agent-wrapper 的顶层结构摊开了。上边调用方(Go 代码或 CLI),中间是核心——Registry 管理 8 个 Provider、Orchestrator 驱动多轮对话循环、process 包管理子进程生命周期。下边 8 个 Agent CLI 进程(claude、codex、pi、opencode 等),每个独立子进程,通过 stdin/stdout 与 wrapper 通信。</p><h4 id="agent-wrapper-类图(Class-Diagram)"><a href="#agent-wrapper-类图(Class-Diagram)" class="headerlink" title="agent-wrapper 类图(Class Diagram)"></a>agent-wrapper 类图(Class Diagram)</h4><p><img src="/images/image-20260604093527716.png"></p><p>五个核心类型。<code>Agent</code> 是顶层接口,只有 <code>Run()</code> — 接收 <code>RunInput</code>,输出 <code><-chan Event</code>。<code>Orchestrator</code> 持有 <code>Agent</code>,加 <code>ApprovalHandler</code>、<code>BudgetHandler</code>、<code>ContextCompressor</code> 三个钩子。<code>Registry</code> 用 <code>sync.RWMutex</code> 保护 map。<code>RunInput</code> 的 <code>SessionID</code>、<code>MaxTurns</code>、<code>OutputFormat</code> 决定每次调用的行为。<code>Event</code> 五种类型(TextDelta/ToolCall/ToolResult/TurnEnd/Error)覆盖 agent CLI 的所有输出。</p><h4 id="agent-wrapper-对象图(Object-Diagram)"><a href="#agent-wrapper-对象图(Object-Diagram)" class="headerlink" title="agent-wrapper 对象图(Object Diagram)"></a>agent-wrapper 对象图(Object Diagram)</h4><p><img src="/images/image-20260604094235636.png"></p><p>「用 Claude Code provider 创建并运行一个 Agent」那一瞬间的内存快照。<code>Registry</code> 里注册了 8 个 provider,<code>Get("claude-code")</code> 返回 <code>ClaudeCodeAgent</code> 实例。<code>Orchestrator</code> 持有这个 agent,默认 <code>ChainedCompressor</code>(SlidingWindow + Summary),3 次重试。<code>RunInput</code> 带着 prompt "重构这个文件"、<code>MaxTurns=10</code>、<code>OutputFormat=stream</code>。</p><h4 id="agent-wrapper-组件图(Component-Diagram)"><a href="#agent-wrapper-组件图(Component-Diagram)" class="headerlink" title="agent-wrapper 组件图(Component Diagram)"></a>agent-wrapper 组件图(Component Diagram)</h4><p><img src="/images/image-20260604095935699.png"></p><p>组件之间的契约关系。<code>Registry</code> 对外提供 <code>Register(name, Factory)</code> 和 <code>Get(name) Agent</code> 两个接口(棒棒糖),各 provider 包作为组件注册进来。<code>Orchestrator</code> 消费 <code>Agent</code> 接口,同时插 <code>ApprovalHandler</code> 和 <code>BudgetHandler</code> 两个插座。底层 <code>process.AgentProcess</code> 封装 <code>os/exec.Cmd</code>,提供 stdin/stdout/stderr 管道和优雅关闭(SIGTERM → 5s → SIGKILL)。</p><h4 id="agent-wrapper-部署图(Deployment-Diagram)"><a href="#agent-wrapper-部署图(Deployment-Diagram)" class="headerlink" title="agent-wrapper 部署图(Deployment Diagram)"></a>agent-wrapper 部署图(Deployment Diagram)</h4><p><img src="/images/image-20260604100230653.png"></p><p>代码到物理节点的映射。用户机器运行 CLI(<code>cmd/agent-wrapper</code> 二进制), 运行 <code>Orchestrator.Run()</code>,启动 claude/codex 等子进程。外部节点三个:Anthropic API(claude 后端)、OpenAI API(codex 后端)、Moonshot API(kimi-code 后端)。CLI → Orchestrator 是本地函数调用,Orchestrator → Agent CLI 是子进程 stdio,Agent CLI → 外部 API 是 HTTPS。</p><h4 id="agent-wrapper-包图(Package-Diagram)"><a href="#agent-wrapper-包图(Package-Diagram)" class="headerlink" title="agent-wrapper 包图(Package Diagram)"></a>agent-wrapper 包图(Package Diagram)</h4><p><img src="/images/image-20260604100303916.png"></p><p>顶层 <code>agentwrapper</code> 包入口,依赖 <code>types</code>、<code>harness</code>、<code>process</code>。<code>types/</code> 在最底层——<code>Agent</code> 接口、<code>Event</code>、<code>Message</code>、<code>RunInput</code>,所有其他包都依赖它。<code>harness/</code> 提供可插拔组件:<code>ApprovalHandler</code>、<code>BudgetHandler</code>、<code>ContextCompressor</code>。<code>process/</code> 封装子进程管理。<code>provider/</code> 是独立子树——八个包各自实现 <code>Agent</code> 接口。箭头方向符合 Clean Architecture:外层依赖内层,内层不依赖外层。</p><h4 id="agent-wrapper-复合结构图(Composite-Structure-Diagram)"><a href="#agent-wrapper-复合结构图(Composite-Structure-Diagram)" class="headerlink" title="agent-wrapper 复合结构图(Composite Structure Diagram)"></a>agent-wrapper 复合结构图(Composite Structure Diagram)</h4><p><img src="/images/image-20260604100505711.png"></p><p><code>Orchestrator</code> 的内部结构。四个 Parts:<code>Agent</code>(子进程驱动)、<code>ApprovalHandler</code>(工具审批)、<code>BudgetHandler</code>(token 预算)、<code>ContextCompressor</code>(上下文压缩重试)。<code>Run()</code> 通过 <code>runAgentWithRetry()</code> 端口调 Agent,Event Channel 经审批和预算检查后从 <code>out</code> 端口输出。关键:<code>agent.Run</code> 失败 → <code>IsContextLengthExceeded(err)</code> → <code>compressor.Compress()</code> → 重试最多 <code>maxRetries</code> 次(默认 3)。</p><h4 id="agent-wrapper-剖面图(Profile-Diagram)"><a href="#agent-wrapper-剖面图(Profile-Diagram)" class="headerlink" title="agent-wrapper 剖面图(Profile Diagram)"></a>agent-wrapper 剖面图(Profile Diagram)</h4><p><img src="/images/image-20260604101714963.png"></p><p>为 agent-wrapper 定义的三个自定义 Stereotype。<code><<Provider>></code>——实现 Agent 接口的子进程驱动类,含 <code>binary</code>、<code>protocol</code> 标签值。<code><<Handler>></code>——可插拔运行时钩子,含 <code>hook_point</code>、<code>is_async</code> 标签。<code><<Event>></code>——统一事件类型,含 <code>direction</code> 标签。在 <code>agent.go</code> 里写 <code>// @stereotype Provider binary=claude protocol=stream-json</code>,生成类图时自动识别特殊样式。</p><h4 id="agent-wrapper-流程图(Flowchart)"><a href="#agent-wrapper-流程图(Flowchart)" class="headerlink" title="agent-wrapper 流程图(Flowchart)"></a>agent-wrapper 流程图(Flowchart)</h4><p><img src="/images/image-20260604102024902.png"></p><p>从输入到输出的完整决策树。<code>Registry.Get(provider)</code> → 查表 → 未找到返回 <code>ErrNotRegistered</code>。找到则创建 Agent → <code>NewOrchestrator</code> 配 Approval/Budget/Compressor/Retries。<code>orch.Run()</code> → 校验 prompt → <code>runAgentWithRetry()</code> 启子进程 → Scanner 解析 NDJSON → 每条事件过审批(allow/deny/abort)→ 过预算 → 输出。TurnEnd 检查 <code>stop_reason</code>,Context 超限触发压缩重试。</p><h4 id="agent-wrapper-用例图(Use-Case-Diagram)"><a href="#agent-wrapper-用例图(Use-Case-Diagram)" class="headerlink" title="agent-wrapper 用例图(Use Case Diagram)"></a>agent-wrapper 用例图(Use Case Diagram)</h4><p><img src="/images/image-20260604101840972.png"></p><p>这个图画的不清晰。</p><h4 id="agent-wrapper-活动图(Activity-Diagram)"><a href="#agent-wrapper-活动图(Activity-Diagram)" class="headerlink" title="agent-wrapper 活动图(Activity Diagram)"></a>agent-wrapper 活动图(Activity Diagram)</h4><p><img src="/images/image-20260604102238637.png"></p><p><code>Orchestrator.Run()</code> 的并发控制流。起始 → Fork 两条。主循环:eventCh 接收 → 分发审批/预算/转发 → TextDelta 直接转,ToolCall 走审批,TurnEnd 走预算。子进程监控:goroutine 监听 <code>ctx.Done()</code> → <code>terminate()</code>(SIGTERM → 5s → SIGKILL)。退出时 Join → <code>close(out)</code>。</p><h4 id="agent-wrapper-状态机图(State-Machine-Diagram)"><a href="#agent-wrapper-状态机图(State-Machine-Diagram)" class="headerlink" title="agent-wrapper 状态机图(State Machine Diagram)"></a>agent-wrapper 状态机图(State Machine Diagram)</h4><p><img src="/images/image-20260604102405183.png"></p><p><code>AgentProcess</code> 生命周期。<code>Idle</code> → <code>cmd.Start()</code> → <code>Running</code>。Running + <code>cmd.Wait()</code> → <code>Exited</code>(异常记 ExitCode)。Running + ctx 取消 → <code>Terminating</code> → SIGTERM → 5s 后 SIGKILL → <code>Killed</code>。<code>sync.Once</code> 保证 terminate 只跑一次。</p><h4 id="agent-wrapper-序列图(Sequence-Diagram)"><a href="#agent-wrapper-序列图(Sequence-Diagram)" class="headerlink" title="agent-wrapper 序列图(Sequence Diagram)"></a>agent-wrapper 序列图(Sequence Diagram)</h4><p><img src="/images/image-20260604102512785.png"></p><p><code>Orchestrator.RunSync()</code> 的时间线。<code>CLI</code> → <code>Registry.Get("claude-code")</code> → 返回 <code>ClaudeCodeAgent</code> → <code>NewOrchestrator</code> → <code>orch.RunSync()</code> → <code>agent.Run()</code> → <code>StartProcess()</code> 启动子进程 → NDJSON 到 stdout → Scanner 解析 → 首条 event 带 <code>session_id</code> → 逐条回 Orchestrator → TurnEnd → <code>RunResult{Text, Usage, SessionID}</code>。agent-wrapper 最核心调用链。</p><h4 id="agent-wrapper-通信图(Communication-Diagram)"><a href="#agent-wrapper-通信图(Communication-Diagram)" class="headerlink" title="agent-wrapper 通信图(Communication Diagram)"></a>agent-wrapper 通信图(Communication Diagram)</h4><p><img src="/images/image-20260604102722921.png"></p><p>同一批信息,视角换成「谁在跟谁通信」。<code>CLI</code>→<code>Registry</code>(1:Get)→<code>Orchestrator</code>(2:NewOrchestrator, 3:RunSync)→<code>ClaudeCodeAgent</code>(4:Run)→<code>AgentProcess</code>(5:Start)→<code>Anthropic API</code>(6:HTTPS)。反馈路径:API→Process→Scanner→Agent→Orchestrator channel→CLI。强调拓扑,不强调时间线。</p><p>图画的不好。</p><h4 id="agent-wrapper-定时图(Timing-Diagram)"><a href="#agent-wrapper-定时图(Timing-Diagram)" class="headerlink" title="agent-wrapper 定时图(Timing Diagram)"></a>agent-wrapper 定时图(Timing Diagram)</h4><p><img src="/images/image-20260604103316021.png"></p><p><code>Orchestrator.RunSync()</code> 的时间约束(ms)。0ms 开始,2ms Agent.Run,5ms Process running,10ms 流式输出,12ms API 请求,50ms TurnEnd,55ms 返回。约束:tool_call ≤ 30s、SIGTERM→SIGKILL = 5s。检查长对话超时边界。</p><h4 id="agent-wrapper-交互概览图(Interaction-Overview-Diagram)"><a href="#agent-wrapper-交互概览图(Interaction-Overview-Diagram)" class="headerlink" title="agent-wrapper 交互概览图(Interaction Overview Diagram)"></a>agent-wrapper 交互概览图(Interaction Overview Diagram)</h4><p><img src="/images/image-20260604103110823.png"></p><p>活动图框架嵌入三个交互场景。判断「output format?」→ 三条分支:<code>stream</code>(sd OutputStream,实时输出)、<code>json</code>(sd OutputJSON,<code>Marshal(RunResult)</code> 单条)、<code>stream-json</code>(sd OutputStreamJSON,NDJSON 逐行)。汇聚到 exit。不展开细节,一眼看清分流逻辑。</p><h4 id="agent-wrapper-泳道图(Swimlane-Diagram)"><a href="#agent-wrapper-泳道图(Swimlane-Diagram)" class="headerlink" title="agent-wrapper 泳道图(Swimlane Diagram)"></a>agent-wrapper 泳道图(Swimlane Diagram)</h4><p><img src="/images/image-20260604102857112.png"></p><p>四泳道:User/CLI、Orchestrator、Agent/Provider、External LLM API。User:Registry.Get → RunInput → RunSync/消费 events。Orchestrator:校验 prompt → runAgentWithRetry → processEvent(审批+预算)→ collect result。Agent:startSubprocess → parseNDJSON → emit events → wait exit。API:receive → generate → return tokens/tool_calls。跨泳道标注协议:<code>via interface</code>、<code>via pipe</code>、<code>via HTTPS</code>。</p><h2 id="14-7-本章小结"><a href="#14-7-本章小结" class="headerlink" title="14.7 本章小结"></a>14.7 本章小结</h2><p>UML 十四种图加上三种实用扩展,覆盖软件系统的两个核心维度:结构(系统长什么样)和行为(系统怎么运行)。AI 开始生成代码之后,UML 从「前置设计工具」变成了「后置理解工具」——AI 写代码,图让人看得懂。</p><p>insight-diagram 把画图自动化了。一个 Skill,四步(分析代码 → 选择图表 → 逐图生成 → 报告),17 种图表。示例驱动的生成策略锁住了风格,防遮盖规则保证了产出直接能用。跟 Understand-Anything 配合,就是「确定性结构 + LLM 可视化」的完整链路。</p><p>本章为 agent-wrapper 预留的图表占位区,后续用 insight-diagram 生成全套图表后补充实际图片。</p><p>第 12 到 14 章连续过了三组 AI 编码辅助工具:官方插件、代码知识图谱、UML 可视化。下一章进代码重构——代码写完了,怎么让 AI 帮你把它改好。</p>