AI 一进老代码库就迷路?两个 Skill 理清它
AI 一进老代码库就迷路?两个 Skill 理清它
让 AI 写代码,真正的难关,常出在它第一次踏进一个已有的代码库。这库往往累积已久,结构早已混乱,它就在文件堆里打转。Matt Pocock 公开了两个 skill,正好治这个毛病。
- codebase-design
,管代码怎么分块,做的是深模块。
- domain-modeling
,管词怎么定准,定的是领域术语。

一、没有记忆的 AI 同事,爱迷路
团队来了一位新同事,是一位 AI 助手。面试时它应答如流,什么需求都接得住。可真到了上岗第一天,把它领进项目,它站在原地不动了。
让它改一个取消订单的 bug。它翻了十分钟,回头问,取消逻辑在哪个文件?指给它。它又翻,回头再问,这里改了,会不会影响别处?耐着性子答。第三次,它问的还是同一件事。
原因不复杂。AI 助手没有长期记忆,每次新对话,它都等于第一天上班的新人。代码库结构早已混乱,文件互相纠缠,它就在里面打转。
Matt Pocock 在 GitHub 上公开了一整套给 AI 用的 skill。其中有两个,专门治这件事。
- codebase-design
,管代码怎么分块。它讲的是深模块(deep module),接口小,实现大。接口越窄,调用者要记的东西越少;实现越厚,复杂细节越不外泄。
- domain-modeling
,管词怎么定准。代码里的「账户」,有人指客户,有人指登录用户,各说各话。它把这些歧义逐一定准,写进项目术语表(
CONTEXT.md),让每个词只有一个确定的含义。
一个给代码画边界,一个给概念编词典。
先把话说在前面,这两个 skill 都不直接改代码。codebase-design 是一把纯尺子,什么都不产出,只把「深不深」「接口该收多窄」的标准讲清楚,供别的 skill 查。improve-codebase-architecture 扫完代码库,拿它挑该加深的模块;tdd 在接口没定时,拿它定接口。开发者自己重构时想按深模块来,也能直接调它给建议。domain-modeling 不一样,它在项目里写两个文件,CONTEXT.md 术语表和 docs/adr/ 下的决策记录,但同样不碰业务代码。

二、先认识「深模块」
这两个 skill 背后,站着同一个词,深模块(deep module)。
这个词不算新。斯坦福的 John Ousterhout 讲了它很多年,2018 年写进了《A Philosophy of Software Design》(中译名《软件设计的哲学》)。
任何一个模块,都能拆成两半。
接口(interface),调用者为了正确使用它,必须知道的一切。
实现(implementation),藏在接口后面,真正干活的代码。
深模块和浅模块的差别,一句话说不清,看这组对比就明白。
- 深模块
,接口小,实现大。像冰山,水面只露出一小角,底下压着一大块。
- 浅模块
,接口大得吓人,实现薄得可怜。像包装纸,外面印满注意事项,拆开一看,里面几乎没干活,只是把参数原样转给了下一个函数。
深模块带来两个好处,作者各起了一个名字。
杠杆(leverage),调用者学会一小块接口,就能撬动背后一大片行为。
局部性(locality),相关的改动、缺陷、知识、验证,都收在同一个地方,改一次,处处生效。

三、管代码的 codebase-design,先立一条规矩
第一个 skill,codebase-design,管代码怎么分块。它最较真的一条规矩,是用词不许随便换。
- module
不能叫 component,也不能叫 service。
- interface
不能叫 API,也不能叫 signature。
- seam
不能叫 boundary。
为什么这么死板?原作者把理由写在文档第一段,只有一句。
Consistent language is the whole point. 一致的语言,才是这件事的全部意义。
道理很直白。让 AI 和开发者之间、也让 AI 的每一次对话之间,都用同一套词指代同一个概念,这套方法才能生效。词一乱,沟通就失真。
这套词里,最反直觉的是 interface。它的范围,比 TypeScript 里那个 interface 关键字宽得多,也远不止一组公开方法。
它指的是,调用者想正确使用一个模块,必须记住的全部内容。
类型签名只是其中一小块,另有这些。
-
不变量(invariant,程序运行中始终不能打破的规矩,比如购物车商品数量永远不能是负数)
-
调用顺序
-
会抛什么错
-
需要哪些配置
-
性能怎么样
另有一个容易被忽略的词,seam(接缝)。它来自 Michael Feathers 在《修改代码的艺术》一书里提出的概念,指程序里「不用改那一处代码,就能改变它行为」的位置。
拿 processOrder(order, paymentGateway) 来说,paymentGateway 就是一条缝。想让它真扣钱,传真实的支付网关进去;测试时想让它别真扣,传个假的进去。行为变了,processOrder 自己的代码一个字没动。
adapter(适配器),就是坐在某条 seam 上、去满足某个接口的那个具体实现。还是上面那条缝,StripeGateway 真连银行扣钱,FakeGateway 假装扣了只做记录,它俩都满足「能扣款」这个接口,就是两个 adapter。

四、怎么判断一个模块够不够深
设计接口时,可以反复问自己三个问题。
-
方法能不能再少几个?
-
参数能不能再做简单点?
-
复杂度能不能再往里藏一点?
另有一个更实用的招,叫删除测试。想象把这个模块整个删掉。
这里有个小故事。公司里有两位员工。
-
一位是传声筒,他的工作是把别人说的话,原样转给下一个人。把他裁掉,工作照常运转,什么都不会乱。
-
另一位是老师傅,几十年的经验都装在他脑子里,他一走,一大片活儿都没人会干。
删除测试就是这一套。
-
删掉一个模块,如果它的复杂度也跟着消失,那它就是个传声筒,删掉正好。
-
如果它的复杂度会在 N 个调用者那里重新长出来,那它一直在扛真实的分量,值得留。
这套判断的好处在于,它衡量的标准只有一条,调用者要背多少东西。一个模块内部尽可以堆满小零件,但只要它们不暴露到接口上,调用者只学一个入口,就能撬动背后一大片行为。

五、深模块天生好测
深模块的另一面,是它天然好测。办法有三条。
第一条,接受依赖,别自己 new。
这里讲个修水管的故事。水管工来修水管,如果水管是能拧下来的接头,他带着自己的工具就能工作,换个接头就行。如果水管是焊死在墙里的,想修就得拆墙。
代码同理。processOrder(order, paymentGateway) 这种写法好测,传入一个假的支付网关就能测。processOrder(order) 这种写法,函数内部自己 new 一个 StripeGateway,测试时根本换不掉它,几乎没法测。
第二条,返回结果,别改现场。
calculateDiscount 返回一个值,测试只看返回值对不对。applyDiscount 直接往购物车里减钱,测完还得把现场还原回去。
第三条,表面积小。
方法少了,要写的测试就少;参数少了,搭测试环境也省力。
seam 是好东西,却不可滥用。原作者补过一句提醒,一个 adapter 只能算「假想的 seam」,两个 adapter 才算「真正的 seam」。意思是,不要为一个还没发生的变化,提前抽个接口出来放着。

六、Design It Twice,让三个 agent 比一比
这个 skill 最深的一招,叫 Design It Twice(设计两次)。名字同样来自 Ousterhout。逻辑很朴素,拿出的第一个设计,多半仍有改进空间,所以不要只做一个方案。
做法像一场小型比赛。同时放三个子 agent 出去,每个给一个相反的约束。
-
第一个,尽力压缩接口,目标只留一两个入口。
-
第二个,尽力追求灵活,尽量多兼容几种用法。
-
第三个,只照顾最常出现的那类调用者,把默认场景做到最顺手。
三个设计摆在一起,从三个角度比较,深度够不够、局部性聚不聚、seam 放在哪里最合适。最后给出推荐,而且敢下结论,不模棱两可。

七、管词的 domain-modeling
结构这半边交给 codebase-design。另一半,词,交给 domain-modeling。
它做的事很具体,而且很较真。
这里有个常见的小故事。团队里两个人聊「账户」。
-
一个人说的是客户账户。
-
另一个人说的是登录用户。
两人聊了许久,都以为对方明白自己的意思,结果做出来的功能对不上。
domain-modeling 专门治这个。当用到的词,和 CONTEXT.md 里已有的定义冲突,它当场指出。当提到 account(账户),它追问指的是 Customer(客户)还是 User(用户)。当描述某个功能怎么运作,它去翻代码,确认实情到底是不是这样。
先解释一下 CONTEXT.md。它是放在项目里的一份文档,记录这个项目的术语、约定和背景,让 AI 每次进门,都能快速搞清楚「这里的词是什么意思、有什么规矩」。
词一旦定准,就地写进 CONTEXT.md。这份文件是纯术语表,不要往里写实现细节,也不要把规格说明写进去。
建 ADR 更要克制。ADR 是 Architecture Decision Record(架构决策记录),用来记那些重要、又不那么容易看懂的架构决定。三条同时满足才动笔。
-
这个决定难以逆转,改起来代价大。
-
不记下来,后来读代码的人会纳闷为什么这么干。
-
它真是权衡出来的,存在别的选项,而挑了这一个。
三条缺一条,就跳过。

八、先定词,再拆块
真正用起来,这两个 skill 是一条线。先让 domain-modeling 把词定准,术语写入 CONTEXT.md;再让 codebase-design 照着这些词去分块、开 seam、定接口。一个定语义,一个定结构。
以下单模块为例,走一遍。
词先定下来。
order是订单。
customer是客户。
cancellation是取消。
结构再跟上,把取消逻辑藏进一个深模块,接口只留一个 cancel(orderId, reason),实现里那堆校验、退款、通知,全藏在接口后面。
结果就是,调用者只学一个方法,测试只打一个接口,将来改取消逻辑,也只改一个地方。

九、它俩在整套流程里的位置
这两个 skill 有点特别。它们不占主流程上某个固定环节,被别的 skill 按需拉出来用。
Matt 的主流程,一条线跑下来。
-
grill-with-docs 把问题问清楚。
-
to-spec 写规格。
-
to-tickets 拆任务。
-
implement 施工。
-
code-review 收尾。
domain-modeling 主要跟在 grill-with-docs 后面。grill-with-docs 的做法,就是跑一轮提问,同时用上 domain-modeling 这套方法。它在对话途中把术语磨准,就地更新 CONTEXT.md,遇到难以逆转的决定才建 ADR。
codebase-design 主要被 improve-codebase-architecture 请出。improve-codebase-architecture 扫描代码库,找出还能往深做的模块,把候选写进一份 HTML 报告。tdd 在接口形状还没定的时候,也把 codebase-design 当参考词典查。
所以,这两个 skill,是整条流程里共用的「词典」和「尺子」。一个定词,一个定结构。词不乱,块不散,AI 从进门到交付,每一步才走得清楚。

小结
回到开头那个新来的 AI 同事。
第一天,它站在代码库门口打转,反复问取消逻辑在哪个文件。递给它两样东西,一本词典(CONTEXT.md),一把尺子(深模块)。
第二天,把它领进同一个项目,让它改一下取消逻辑。它没再问从哪开始。
让 AI 帮你写深模块,归结起来一句话,小接口藏大行为,术语先定准,再分块。
-
如果代码里接口越写越多、参数越传越长,先上 codebase-design。
-
如果团队里同一个词各说各话,评审总在概念上纠缠,先上 domain-modeling。
两个都装上,一个管结构,一个管词。值得收藏,下次让 AI 进代码库之前,先看一眼。

附录 术语对照与来源说明
|
术语 |
说明 |
|---|---|
|
skill |
交给 AI 的一份操作规程 |
|
深模块(deep module) |
接口小、实现大的模块,像冰山 |
|
浅模块 |
接口大、实现薄的模块,像包装纸 |
|
接口(interface) |
调用者为了正确使用模块必须知道的一切 |
|
实现(implementation) |
藏在接口后面、真正干活的代码 |
|
杠杆(leverage) |
学会一小块接口,就能撬动一大片行为 |
|
局部性(locality) |
相关改动、缺陷、知识都收在同一个地方 |
|
seam(接缝) |
不用改那一处代码、就能改变其行为的位置 |
|
adapter(适配器) |
坐在 seam 上、满足某个接口的具体实现 |
|
删除测试 |
想象删掉模块,看复杂度是否跟着消失 |
|
Design It Twice |
一次出三个反向约束的设计,再比较取舍 |
|
CONTEXT.md |
记录项目术语、约定和背景的文档 |
|
ADR |
架构决策记录,记那些难以逆转的重要决定 |
素材来源 本文基于 Matt Pocock 的 skills 仓库中 codebase-design 与 domain-modeling 两个 skill 的源码整理,为独立写作,非原文翻译。
#深模块 #代码设计 #AgentSkill #ClaudeCode #MattPocock #接口设计 #领域建模 #CONTEXTmd #ADR #可测试性
更多推荐




所有评论(0)