posts/matt-pocock-architecture-skill.md
代码有点乱时,92 万次安装的 Skill 先画诊断
代码有点乱,是最容易把 Coding Agent 用坏的一句话。
你把这五个字扔过去,模型很可能会搜几个文件,看见一段 543 行的热键代码,再顺手给你拆成状态、事件、效果、工厂和七八个新目录。终端一片绿,文件也变短了。过两周想加个功能,得先搞清楚哪一个抽象才是真正的入口。
这次我没让它这么干。
Matt Pocock 的 improve-codebase-architecture 在 skills.sh 上现场显示 920,093 次安装,GitHub 仓库有 260,762 个 Star。安装命令只有一行。
npx skills add https://github.com/mattpocock/skills --skill improve-codebase-architecture
好家伙,92 万人装的东西,最重要的动作不是改代码,而是先把这次到底该改什么画出来。
我拿一个真实的小项目跑了一遍。它是本地离线语音输入法 xiaodao-ime(开源在 GitHub),有二十多次连续提交,最近的改动落在热键、设置、平台兼容和菜单栏交互。不是为了给文章搭的玩具仓库,热键模块里真的混着录音状态、预览、转写、改写和粘贴流程。
这篇只讲一次实测。诊断怎么把「有点乱」变成可执行的改造,和它在哪些地方主动踩了刹车。
它先找热点,不给所有旧代码判刑
我没有把项目根目录丢进去让它自由发挥,而是按它的规则先看 git log。
最近三十条提交里,app.py 出现 12 次,xiaodao_ime/hotkey.py 出现 9 次,设置、录音、平台实现也反复被碰到。这才是值得优先看的地方。半年没人动的文件就算丑,也不该排在前面。重构的回报来自下一次修改,不来自今天整理完文件夹的舒适感。
接着它扫描模块间的摩擦,随后交出一份临时 HTML 报告。报告没有落进仓库,里面给了三个候选。
- 把选区改写里的剪贴板协议收进一个模块,Strong
- 把热键状态机再拆一层,Worth exploring
- 把菜单栏启动过程抽成新的模块,Speculative
报告最有用的地方,是每一项都有 Before 和 After。它不说「降低耦合」这种漂在空中的话,而是把调用者到底知道了什么,哪条 seam 在漏,测试应该从哪里穿过去,摆到同一张图里。

第一项过了 deletion test。把剪贴板事务删掉后,原剪贴板、哨兵值、失败清理和延迟恢复,会重新散回改写流程的调用者里。它不是为了文件数好看而存在,删掉会把复杂度摊得更开。
所以这不是「把一个函数挪到另一个文件」。它有自己的 interface,调用方只该知道两件事,抓到了什么文本,要不要替换。其余协议都应该留在 implementation 里。
543 行的热键模块,反而没被大拆
这就是我最想写的一点。
HotkeyController 有 543 行,第一眼看过去很像一个适合大手术的对象。它要处理单击和按住两种录音方式,听写和改写两个通道,实时预览,取消逻辑,以及异步转写后的收尾。很多自动重构会从这里开始切。
报告没有把它列为首选。
原因并不玄。当前只有一个真实的键盘事件来源,测试已经能从 HotkeyController 的 interface 驱动状态变化。此时再新建一个事件 port,只会多出一个调用者必须理解的 interface。根据这套方法的判断,一只 adapter 只是想象中的 seam,两只 adapter 才有资格成为真的 seam。
厉害了,这个结论听起来没那么炫,但救命。
很多所谓架构优化,问题不在于拆得不够细,问题在于把未来可能存在的变化提前做成今天必须维护的结构。没有第二种事件来源,没有第二个 UI 适配器,也没有测试替身需要它时,先别为抽象买单。
菜单栏启动流程也是同样处理。它依赖 macOS 的 rumps 和权限检查,确实长,也确实挤在一起。但现阶段没有第二套 UI adapter,硬拆只会把启动顺序和异常处理藏到更多地方。报告给了 Speculative,我同意它先不动。
真正要收口的,是剪贴板归还协议
改写功能的旧流程里有一处很具体的泄漏。
grab_selection() 返回两样东西,选中的文字和原剪贴板。随后 HotkeyController 要自己记住 original_owned,判断什么情况下调用 paste_text(result, restore_to=original),什么情况下又要 restore_clipboard(original)。网络改写失败、没有选区、粘贴失败,都得由调用方把这串协议补齐。
调用方明明只想表达「把这段文字改写成结果」,却被迫记住剪贴板的所有权和归还时机。这个 interface 太宽了,模块也就浅。
我把它改成 SelectionCapture。
capture = capture_selection()
if capture.text:
capture.replace(result)
capture.restore()
它内部保存原剪贴板,负责替换后的延迟恢复,也负责失败时的立即归还。restore() 可以安全重复调用,成功替换后的调用不会提前覆盖刚粘进输入框的文本。

改动落在三个文件,paster.py、hotkey.py 和 test_paster.py。实际 diff 是 66 行新增、27 行删除。更关键的是,HotkeyController 少掉了 original_owned 和一段 finally 清理分支,调用点只留下业务动作。
这才是深模块有用的样子。不是把事情塞进一个大盒子,而是用更小的 interface 藏住调用者本来不该背的协议。调用者得到 leverage,维护者得到 locality,测试也终于能把 interface 当测试面。
我没有再造一个 ClipboardPort。平台层已经有 macOS 和 Windows 两个 adapter,测试里还有 FakeBackend。对这次改造来说,再包一层只会把既有 seam 绕远。保留已有 seam,把事务本身做深,够了。
诊断之后,改动只走一小步
这次验证也没有停在一张漂亮报告上。
我先跑了项目自己的 .venv 离线测试,再改代码,然后重新跑热键状态机、剪贴板流程和润色与设置三组测试。新的测试覆盖了两个关键结果,没有选区时原剪贴板会归还,替换成功后文本能先粘进当前输入框,延迟结束后才归还旧内容。py_compile 和 git diff --check 也都通过。
这个顺序很值得抄。
先看近期提交,给扫描划范围
先看诊断报告,只挑一个 Strong 候选
把调用者不该知道的协议收进模块
从新 interface 跑回归,而不是盯内部函数
它不能保证每次都找对候选。项目越没有领域词汇,诊断越容易泛。报告里的 Before 和 After 也只是把判断显影出来,不会自动替团队决定业务取舍。
但它给 Coding Agent 加了一道很实用的刹车。
代码有点乱,不等于现在就该全面重构。先问哪块会继续变,哪条 seam 真的在漏,删掉一个模块后复杂度会不会重新散开。能回答这三件事,再改一处。
你手里那段最想让 AI 大拆的代码,里面有没有一小段更适合先收口的协议?