☰
代码折叠怎么做:让 Agent 先看接口,再看实现
2026/10/5 2:27:25 网站建设 项目流程

一、全文给出去之后

你需要改一个很小的行为:shop项目的订单取消,只有在订单存在时才允许取消——订单不存在时,现在的代码抛了一个难以理解的异常。你要把这个行为改成明确的 404。

为了"让它看清楚",你把src/orders/services/order_service.py全文交给了 Agent。这个文件八百多行,里面有订单取消、订单创建、退款计算、库存回滚、通知发送,还有三段历史遗留的兼容代码。

它给出的改动里有三个问题。第一,它把取消流程里的一个"重复校验"删了,理由是"这段逻辑和上面重复"——实际上那段校验是为了兼容一个旧版本的调用方,删掉会破坏那个路径。第二,它顺手把退款计算里的一个魔法数字提取成了常量,改动本身没错,但那行代码属于另一个功能,和本次任务无关。第三,它没有发现取消成功之后还有一条外部通知(发到消息队列),因为在八百行里,这条通知藏在一个名叫_finalize的辅助方法里,名字看不出来和取消有关。

三个问题有一个共同来源:你要它改的是"契约",而它看到的是"实现"。当八百行实现同时摆在面前,它无法区分哪些是这次任务相关的约定、哪些只是碰巧住在一起的代码;同时,真正重要的信息(有一条外部通知)被埋在细节里,没有获得足够的分量。

这篇讲的是怎么把代码"折"起来给:先给模块结构和公开接口,让它知道有哪些约定;实现只在需要的时候展开。关键不在这套交付形式叫什么名字,而在于一个底线——折叠不能把改变行为的细节藏起来。

换个角度看,这件事的难点不在于"少给代码",而在于"把该说的说清楚"。把八百行删成二十行树状图,谁都会;把散落在八百行里的三条关键约定(外部通知、事务边界、旧调用方兼容)提到第一层,才是真正需要判断力的部分。这篇文章的篇幅,大部分会花在这件事上。

二、先把几个词讲明白

接口:一个模块对外承诺的能力。比如"cancel_order(order_id)会取消订单",调用者只需要知道这句话,不需要知道内部怎么实现。生活里的类比是餐厅菜单:菜单告诉你有什么菜、多少钱,不告诉你后厨怎么炒。

契约:接口附带的条件与保证。包括前置条件(调用前必须满足什么)、后置条件(调用后保证什么)、以及失败时的行为(抛什么异常、返回什么错误)。菜单上"这道菜要等二十分钟"也是契约的一部分。

实现:接口背后真正的代码。它可以随时重构、替换,而契约保持不变。后厨今天换了个灶台,菜单不用改。

代码折叠:把"给模型看的材料"分成两层——第一层是模块结构、文件职责、公开接口和契约;第二层是实现细节。默认只给第一层,需要时再展开第二层。它和编辑器的"折叠代码"是同一个想法:先看骨架,再决定展开哪里。

副作用:一次调用除了返回值之外,对外部世界造成的影响:写数据库、发消息、调外部接口、改缓存、写日志。它是契约里最容易被漏掉、也最容易在折叠时被藏掉的部分。

按需展开:不是"永远不给实现",而是"由任务决定展开哪一部分"。展开的触发条件通常有两个:要修改某个函数的行为,或者要确认某个边界行为(重试、并发、事务)到底怎么做的。

阅读包:把上面这些东西整理成一份文件——包含哪些、排除了哪些、为什么排除、需要时从哪里展开。没有这份说明,折叠就会变成"藏"。

三、为什么"先接口后实现"更可靠

3.1 接口是这次任务真正要讨论的对象

大多数改动任务,本质是在讨论契约:这个接口在什么条件下做什么、失败时返回什么。实现是达成契约的手段。当你把全文给出去,讨论的焦点就被拉到了实现层——模型会开始评论写法、提取常量、合并分支,这些话题看起来专业,但和你要的"订单不存在返回 404"没有关系。

先给接口,讨论就容易停在正确的层面上:契约里有没有写"订单不存在"的行为?没有的话,这次任务就是要补上它;有的话,就要检查实现是否符合。

这里还有一层实际收益:契约层面的讨论更容易被验收。"订单不存在返回 404"是一条能写成测试的要求;"把重复校验合并掉"不是——它只能被评价,不能被验证。你把讨论固定在契约层,验收条件也就自然浮出来了。

3.2 折叠同时在做减法:减少噪音

八百行实现里,和本次判断直接相关的可能只有二十行。其余的行数提供的是"可能性":可能被顺手优化、可能被误判为重复、可能引入风格模仿。折叠的作用是把这些可能性先收起来,只在需要时打开一个窗口。

这和我们上一节讲的"按任务选材料"是同一件事,只是对象换成了代码。区别在于:日志和文档可以按主题挑,代码只能按结构挑——所以需要一个折叠方案,而不是全靠手工挑选。

3.3 折叠的三条底线

折叠不是把代码藏起来那么简单。有三类信息一旦藏起来,折叠就从"减负"变成了"埋雷":

第一,副作用。任何写数据库、发消息、调外部服务的行为,都必须在第一层可见。藏起来的副作用会让模型(和你)做出错误的判断——以为改的是纯计算,实际上触发了一串外部动作。

第二,异常语义。函数失败时抛什么异常、什么情况下返回空值、什么情况下不会返回。这些属于契约的一部分,不能藏在第二层。

第三,事务、重试、并发和功能开关。它们不改变"接口长什么样",但会改变行为的时间与次数:同一段代码在事务内还是事务外执行、失败后会不会自动重试、有没有被开关关掉。这些细节在评审时是必须看到的。

底线可以总结成一句话:折叠删掉的是"怎么写",不能删掉"会发生什么"。

3.4 接口声明本身也需要维护

折叠方案的可靠性取决于第一层的准确性。这里有一个循环:接口清单是从代码和测试提炼的,代码改了而清单没改,清单就会误导——而且它比"没有清单"更危险,因为它带着权威感。

所以折叠方案要绑定一个维护动作:任何改变契约的改动,都要同步更新接口清单。判断标准是"这次改动会不会让调用者的预期失效":改了返回值、改了异常、加了副作用、改了事务范围,都要更新;纯粹的内部重构不用。这个动作和写代码注释不同,它不属于"顺手美化",而属于交付的一部分——把契约变化写进声明文件,和改测试一样重要。

有一个轻量的检查可以放进评审:改动里出现了新的副作用(发消息、写缓存、调外部接口)时,问一句"接口清单更新了吗"。一次问、一次改,习惯就建立起来了。

3.5 三段式流程:概览、方案、展开

把折叠落到工作流上,可以固定成三段,每一段都有明确的产出,避免"给完材料就等它改代码"这种一锤子买卖。

第一段是概览。给它第一层材料和任务描述,要求它产出两样东西:对任务的理解(一句话)、以及它认为需要展开的位置清单。这一段不改代码。为什么先要这个?因为"要展开哪里"本身就是对任务理解的一次检验——如果它要求展开的文件和你预期完全不同,通常意味着任务描述或者结构说明有歧义。

第二段是方案。基于第一层材料和少量已展开内容,让它给出改动方案:改哪些文件、每个文件改什么、怎么验证。这一段仍然不改代码。方案阶段的价值在于把"改什么"和"怎么写"分开评审——前者更重要的是你的判断,后者可以慢慢打磨。

第三段是执行。按批准的方案改代码,遇到需要更多展开的情况就停下来要材料。这一段里,如果它需要展开新的部分,应该说明"为什么需要";这个说明既是对你的交代,也会进入阅读包的记录。

三段式的成本比"直接改"多花一轮对话,换来的是一次对齐和一份可复用的记录。对于小改动,可以合并成两段(概览 + 执行);对于涉及多个文件或者有兼容风险的改动,三段都值得保留。

四、完整例子:给一次修改做两层阅读包

4.1 第一层:结构、职责与接口

任务还是那个:POST /orders/{id}/cancel在订单不存在时返回 404。第一层材料包含三部分:模块结构、每个文件的职责、公开接口与契约。

模块结构部分,只列这次任务相关的路径和一句话职责:

src/orders/ api/routes.py # 把 HTTP 请求映射到用例调用;负责状态码与响应结构 domain/order.py # 订单实体与状态规则;不含 IO services/order_service.py # 用例编排:取订单、改状态、写审计、发通知 repositories/order_repo.py # 持久化接口:get / save / transaction tests/orders/ # 订单相关测试,含取消用例

接口与契约部分,写成一份清单,每一条都要包含前置条件、后置条件、失败行为和副作用:

classOrderRepo(Protocol):defget(self,order_id:str)->Order|None:...# 前置:无# 后置:返回订单对象;不存在时返回 None# 副作用:无defsave(self,order:Order)->None:...# 前置:order 必须带有效 id# 后置:订单状态与版本号被覆盖写入# 副作用:写数据库defcancel_order(repo:OrderRepo,audit:Audit,notify:Notifier,order_id:str)->Order:# 前置:order_id 非空# 后置:状态变为 CANCELLED;审计事件恰好一条;通知恰好一条# 失败:订单不存在 -> OrderNotFound;状态不允许 -> OrderStateError# 副作用:写数据库、写审计、发通知(全部在同一事务内)...

注意最后那行注释里"全部在同一事务内"这句。它不属于接口签名,但属于契约,因为删掉它,行为就变了。这类信息正是折叠文档存在的意义——它把散落在实现里的重要约定提炼到第一层。

4.2 让它在第一层给出方案

把这两份材料和任务描述一起交给 Agent,要求它先不要改代码,只回答三件事:这次要改哪些文件、每个文件改什么、有没有它认为需要进一步查看的实现。

预期它给出的回答大概是这样(示例):

1. 需要改 api/routes.py:把 OrderNotFound 映射为 404(目前可能没有这个分支)。 2. 需要改 services/order_service.py 中读取订单之后的分支:把找不到订单的情况 转成 OrderNotFound。 3. domain/order.py 的规则不需要改:状态不允许的语义已经是 OrderStateError。 4. 需要查看:cancel_order 里异常从哪里抛出、是否被吞掉;routes 里现有的 异常映射表。

第 4 条是这个流程最有价值的部分。它明确说出了自己需要展开什么,而不是在全文里随便翻。你把cancel_order的实现展开一段给它,问题就闭环了——这就是"按需展开"。

4.3 第二层:只展开需要的那一段

如果它要确认的是"订单不存在时异常在哪里产生",展开的范围大约是二十行:

defcancel_order(repo,audit,notify,order_id):order=repo.get(order_id)iforderisNone:raiseOrderNotFound(order_id)# ← 这里已经有明确异常ensure_cancellable(order.status)withrepo.transaction():order.status=OrderStatus.CANCELLED repo.save(order)audit.append("order.cancelled",order_id)notify.send("order.cancelled",order_id)returnorder

看完这段,问题的性质就清楚了:领域和用例层的行为已经是对的,缺的是路由层把OrderNotFound映射成 404。改动的范围从"可能涉及多个文件"缩小到"一个文件里加一个映射分支"。

把它和"直接给八百行全文"对比,差别在两点:一是需要判断的信息只有二十行,注意力集中;二是那二十行是因为它明确要求才展开的,展开范围有依据,不是你猜的。

4.4 第一层材料怎么来

读完例子之后,一个现实问题浮出水面:这份接口清单从哪来?多数项目并没有现成的文档。三个来源按成本从低到高排:

第一个来源是代码本身。接口清单可以从类型注解、函数签名和 docstring 里抽出来;缺少的契约信息(副作用、事务、失败行为)需要你补几行注释。这项工作的成本不高,收益是长期的。

第二个来源是测试。测试里常常写着契约:什么情况下抛什么异常、失败时数据应该是什么状态。把测试名和断言提炼成契约描述,比重新推导快得多。

第三个来源是本次任务。如果接口清单只有前两份材料,契约部分会很稀薄;可以在做任务的过程中把新确认的约定补进去。这个动作有一个副作用是好的:它让文档随任务生长,而不是靠一次性的"文档日"。

无论从哪里来,接口清单都遵守同样的格式:签名、前置、后置、失败、副作用。五行写清一个函数,比写一段散文有效得多。

4.5 阅读包文件长什么样

把上面的做法固化成一份文件,放在docs/reading-pack/cancel-order.md,结构如下:

## 本次任务 POST /orders/{id}/cancel 在订单不存在时返回 404。 ## 第一层:结构与接口 - 模块结构:<路径与职责清单> - 公开接口与契约:<签名 + 前置/后置/失败/副作用> ## 第二层:按需展开 - 展开规则:模型明确提出需要时提供;每次注明展开范围与理由 - 已展开记录:services/order_service.py: cancel_order(第 41–58 行) ## 排除项与理由 - services/order_service.py 其余部分:与本次判断无关 - domains 的退款计算:属于另一个用例 - 历史兼容分支:本次不改动,避免破坏旧调用方 ## 折叠底线检查 [ ] 所有副作用已在第一层说明 [ ] 异常语义已在第一层说明 [ ] 事务/重试/开关已在第一层说明

这份文件有两个作用。对本次任务,它是材料包;对下一次改动,它是可复用的起点——把任务、接口清单和排除项换成新的,结构照旧。

4.6 一次对照记录

同一任务、同一仓库版本,用两种给法各跑一遍,记录结果(示例记录,用于演示流程):

观察项给全文给两层阅读包
是否需要额外探索不需要,但找出无关改动二处一次按需展开(20 行)
改动范围接口映射、退款计算常量、删除旧校验接口异常映射一处
是否发现外部通知副作用没有(藏在辅助方法里)在第一层契约中已标注
评审必须说明的问题三处:兼容分支、无关改动、漏掉的副作用一处:确认通知在事务内
往返轮次两轮(回滚无关改动、修复兼容分支)一轮

表格里最值得注意的是第三行和第四行。给全文的那次,"漏掉副作用"不是因为它看不见代码,而是因为副作用所在的位置在结构上离"取消"很远;折叠方案把这条信息从实现里提到了契约里,它就一定会被看到。这就是第一层材料的核心价值:它不是减少信息,而是把重要的信息从细节中提升出来。

另外,别把这张表当成"折叠一定更好"的证据。它的意义是演示怎么记录、记录什么。你的项目结构、任务大小、模型的读取方式不同,结论可能不同——重要的是有记录,而不是有立场。

4.7 什么时候不该折叠

有三种情况,折叠会让事情变慢:

第一种,修改的目标就是一个很短的实现。比如你要改的是一段十行的校验逻辑,读它的时间和读它的接口清单差不多,这时候直接给实现更直接。折叠是为了处理"相关信息占比很低"的场景,不是所有场景。

第二种,任务本身就是"理解实现"。比如你怀疑某处有并发问题,需要逐行看它怎么加锁、怎么处理重试;又比如你要重构一段代码,必须看清它所有的分支。这类任务的产出就是理解,实现必须完整展开。

第三种,第一层材料还没准备好。硬要折叠,你得到的只是一份目录树,反而增加了一轮沟通。这种情况下,先给实现跑完这次任务,然后把这次获得的契约理解回填成第一层材料——阅读包从任务里长出来,比从零开始设计容易得多。

4.8 用一条命令把第一层材料生成一半

第一层材料里最机械的那部分——有哪些公开函数、签名是什么、首行说明写了什么——不需要手抄。用 Python 标准库里的ast模块解析一遍源文件就能列出来,不用装任何东西。

把这个脚本存成tools/iface.py,和代码一起走版本:

# tools/iface.py:把模块的公开接口打印成一页清单,供第一层材料使用importastimportsysdeffirst_line(node):doc=(ast.get_docstring(node)or"").strip().splitlines()returndoc[0]ifdocelse"(无说明)"defshow(node,indent=""):ifisinstance(node,ast.ClassDef):print(f"{indent}class{node.name}--{first_line(node)}")forsubinnode.body:ifisinstance(sub,ast.FunctionDef)andnotsub.name.startswith("_"):show(sub,indent+" ")else:args=[a.argforainnode.args.args]ifargsandargs[0]in("self","cls"):args=args[1:]print(f"{indent}def{node.name}({', '.join(args)}) --{first_line(node)}")forpathinsys.argv[1:]:tree=ast.parse(open(path,encoding="utf-8").read())print(f"##{path}")fornodeintree.body:ifisinstance(node,(ast.FunctionDef,ast.ClassDef))andnotnode.name.startswith("_"):show(node)

运行方式和输出(示例):

$ python tools/iface.py src/orders/services/order_service.py src/orders/domain/order.py ## src/orders/services/order_service.py class OrderService -- 订单用例编排:取消、支付、查询 def cancel(order_id) -- 取消待处理订单,并写一条审计事件 def pay(order_id) -- 把待处理订单标记为已支付 ## src/orders/domain/order.py class Order -- 订单聚合根 def can_cancel() -- 只有待处理状态允许取消 def load_order(raw) -- 从持久化数据还原订单

这张清单解决了"有哪些接口"的问题,但它只覆盖第一层材料的一半。签名和首行说明来自代码本身,剩下那部分——前置条件、副作用、失败语义、事务范围——没有任何工具能自动生成,只能由你或模型从测试、调用点和实际运行行为里提炼。所以脚本的定位是去掉抄写工作,不是去掉判断工作:清单生成之后,你仍然要逐行回答"这一行的事实够不够支撑这次改动"。

怎么检查生成的清单能不能用:挑其中一个函数,只看这一行,问自己两个问题——调用它之后系统里会多出什么、什么情况下它会失败。两个都答得上来,说明这一行的说明写够了;答不上来,就在清单里补一行副作用或者失败语义,而这一行恰好是第一层材料里最值钱的部分。

五、反例与代价:四种折错的姿势

下面四种做法都有一个共同特征:它们都借用了"折叠"的名义,却只完成了形式上的折叠。判断折叠是否有效的标准只有一条——看第一层材料是否仍然承载了"会发生什么"。

5.1 反例一:只折叠出"文件列表"

做法:第一层只给目录树和文件名,不给接口和契约,理由是"让它先了解结构"。

它为什么看起来能行:文件树确实提供了结构信息,而且生成成本几乎为零,tree命令一条就够。

最后的代价是"结构有了,约定没有"。文件名很少能说明行为:order_service.py告诉你这里有用例编排,但不会告诉你取消流程里有外部通知、事务边界在哪里、异常怎么命名。于是它要么向你索要(多一轮往返),要么按命名猜(风险更高)。这里的判别标准很简单:第一层材料能不能让一个不熟悉项目的人说出"这个功能的契约是什么"?不能,就说明折叠折掉了关键部分,只是折法看起来"很整齐"。

5.2 反例二:折叠时藏掉了副作用和失败行为

做法:第一层给了函数签名和一句简单描述,比如"取消订单;返回订单对象"。

它为什么看起来能行:签名和一句话描述是很多人对"接口文档"的全部理解,很多文档站也确实在这个粒度上。

最后的代价是判断依据缺失。签名里写不出"会发一条通知"“失败时抛特定异常”“整个过程在事务里”。而这三条恰恰是评审和修改的核心:一个改动如果没有考虑到通知和事务,它在测试里可能是绿的,在上线后却会引发一串问题。第一层材料的作用是把这些信息从实现里提出来,如果提不出来,折叠的价值就丢失了。

5.3 反例三:折叠之后又一次性全量展开

做法:先给接口,然后"既然它要改这个文件,干脆把整个文件都展开"。

它为什么看起来能行:逻辑上说得通——要改的文件当然要给它看全文,省得来回要。

最后的代价是折叠的收益被抵消。全量展开之后,注意力问题、噪音问题、顺手改动问题全部回来了;而且这一次连"按需"都没有了——你没有给展开范围设边界。更合理的做法是把展开和修改范围绑定:改哪个函数,展开哪个函数及其直接依赖的那一段;需要上下文时再补一点。展开的记录要留在阅读包里(哪个文件、哪几行、为什么),这既方便下一次复用,也让评审人知道模型看到了什么。

5.4 反例四:折叠材料过期

做法:第一层材料做得很漂亮,但此后再没更新;接口改了、事务边界变了,清单还是旧的。

它为什么看起来能行:第一版通常很准确,而且更新文档看起来像"额外工作",没有任何测试会因为文档过期而变红。

最后的代价是它开始系统性地误导人。旧的契约会被当成现行约定,模型据此做的判断全部偏离——而且偏离得很"合理",因为它是照着材料做的。这类问题最难被发现的时刻,恰恰是它最危险的时候:材料看起来专业、格式整齐,没有人会怀疑它。解决办法是把更新绑定到"契约变化"这个事件上(前面 3.4 节的标准),并在评审清单里加一条检查。

六、落地步骤:为一个模块做阅读包

七步做完大约需要半小时到一小时,取决于模块大小。第一次会觉得慢,因为它要求你把"以为知道"的东西写下来;第二次开始,材料的复用会把这部分时间直接省回来。下面每一步都写了"做什么、为什么、怎么检查"。

第一步,选定模块边界。以"这次任务可能触碰的范围"为界,而不是以整个仓库为界。为什么?阅读包的价值来自集中;范围一大,它就退化成目录树。怎么检查:模块清单能不能在一屏内看完。

第二步,写结构清单。每个路径一行:路径 + 一句话职责。职责要写"对谁负责",不要写"实现了什么算法"。为什么?职责是这个文件在系统里的位置,对判断改动范围最有用。怎么检查:随机挑一个文件,问"这次改动要不要碰它",凭这行职责能不能答上来。

第三步,写接口与契约清单。每个公开函数五行:签名、前置、后置、失败、副作用。为什么五行比一段话好?因为它逼你分别回答"会发生什么"和"什么时候不会发生"。怎么检查:五条里有没有空着的;空着的那条是真的没有,还是你不知道。

第四步,写排除项和理由。列出这次不看的部分,并写一句为什么。为什么排除也要写理由?因为下一次任务很可能需要它,理由能帮下一个人判断"我这次的情况是不是类似"。怎么检查:排除项能不能对应到"改动范围之外"这个结论。

第五步,约定展开规则。明确两件事:展开由谁发起(模型提出要求或你判断需要)、展开记录怎么留(文件、行号、理由)。为什么?因为"按需"必须有触发条件,否则执行起来会退化成全量。怎么检查:阅读包里有没有一段"已展开记录",哪怕这次是空的。

第六步,跑一次折叠底线检查。三条底线逐条过:副作用、异常语义、事务/重试/开关。为什么单独设这一步?因为这三类信息最容易在写第一层时被漏掉,而漏掉它们的代价最大。怎么检查:把第一层材料给一个不了解这个模块的同事,问他"这个功能失败时会怎样、会影响哪些外部系统"。

第七步,把阅读包和任务绑定。阅读包放在任务单旁边,命名带任务名;任务结束后回填"已展开记录"和"实际改动范围"。为什么回填?因为下一次做同类任务时,上一次的展开记录就是最好的起点。怎么检查:两份相邻任务的材料能否互相参照。

可复制的检查清单:

[ ] 模块清单在一屏内,每个文件一句话职责 [ ] 接口清单含签名、前置、后置、失败、副作用 [ ] 排除项写了理由 [ ] 展开规则明确:谁发起、如何记录 [ ] 三条底线检查通过:副作用 / 异常 / 事务 [ ] 已展开记录已回填(含未展开也可以写"无") [ ] 契约若发生变化,清单同步更新

阅读包和前面几篇的产物是一套:任务单定义"要做什么、怎么验收",材料包定义"这次给哪些材料",阅读包定义"代码材料怎么分层呈现"。三者可以放在同一个目录下,命名规则一致,比如docs/tasks/2026-09-29-cancel-404/下面放task.md、materials.md、reading-pack.md。任务结束时三份文件一起归档,下次同类任务整体复用。

第一次做阅读包时,建议挑一个"改动经常出意外"的模块,因为反馈会最明显。做完之后对比两组数字:这次的往返轮次、以及上次同类任务在"给全文"方式下的往返轮次。如果数字没有变化,说明折叠的问题可能不在形式上,而在第一层内容里——回去检查三条底线,通常是副作用或者事务边界没写清楚。

七、常见问题

问:接口清单和普通的 API 文档有什么区别?

关注点不同。对外的 API 文档通常描述协议:路径、参数、状态码、示例,读者是外部调用者;接口清单描述内部模块的契约:前置条件、后置条件、失败语义、副作用和事务范围,读者是本次任务的模型和你自己。两者可以共享一部分内容,但接口清单必须包含副作用和失败语义——这两项在对外文档里经常可以省略,因为外部调用者只关心协议层面;但在修改内部代码时,它们决定了行为是否正确。

问:折叠之后模型看不到实现,会不会写出不兼容的代码?

这个风险真实存在,处理方式是两条。第一,在任务里明确写出展开义务:如果需要查看某个函数的实现来判断行为,必须提出请求并说明要看什么,而不是凭签名推断。第二,用验收条件兜住:涉及兼容性的行为(异常类型、返回值、副作用次数)都写进验收条件,模型即使推断错了,测试也会拦下来。两条合起来的效果是:它不需要靠猜,也不被允许靠猜。

问:遗留代码没有类型注解、没有文档,怎么提炼接口清单?

按三个来源依次找:测试里对行为的断言、调用点上对返回值的使用方式、以及运行时观察(跑一遍看它实际做了什么)。这三样拼起来,通常能还原出大部分契约;剩下的部分需要你在改动时逐步确认。一个务实的策略是:先为"这次任务会碰到的函数"补清单,不必一次覆盖整个模块。清单随任务生长,比起憋一份大而全的文档更可持续。

问:维护阅读包的成本高吗?谁来维护?

初次成本在于把契约写清楚,之后每次任务增量很小。维护者是任务的执行者——无论是人还是 Agent——因为"契约变了就更新清单"是交付的一部分。有一个轻量做法可以显著降低成本:把清单放在代码旁边的注释或者独立的 Markdown 文件里,改代码的同一个提交就改清单,避免出现"文档在另一个仓库、另一个流程"的割裂。

问:小项目、单文件,也需要折叠吗?

不需要。折叠的价值随着"文件长度"和"与任务无关内容的占比"上升。经验判断:如果这次要改的内容集中在一个函数内、文件不超过一两百行、其余内容与任务无关的程度不高,直接把文件给出去更快。相反,如果文件很长、历史遗留多、或者同一个文件里住着好几个不相干的用例,折叠就值得做——哪怕只是把"这次要改的函数"和"其余部分"分开给。

问:怎么判断该展开哪一段?

用问题驱动,而不是用文件驱动。具体方式是:先写下"我需要确认什么",再据此决定展开范围。常见的三类问题各有对应的展开范围——"失败时会怎样"看抛出异常前后几行;"会不会影响外部系统"看副作用调用链;“重复执行会不会有问题"看事务边界与幂等处理。展开永远是为了回答一个具体问题,而不是为了"看完整一点”。

问:折叠材料和"让模型自己检索代码"应该怎么配合?

可以配合,顺序是先给第一层、再允许检索。第一层(结构 + 契约 + 排除项)定义了它该去哪找、不该去哪找;检索动作则补上第一层没有覆盖的细节。配合时加一个要求:让它把检索到的关键内容带来源(文件、行号、版本)写进输出,方便你核对它看到的是不是当前版本——这一点和检索结果需要时间和版本元数据是同一个道理。如果没有第一层,直接让它检索,你就会回到"它到底看了什么"这个不可控的状态。

问:第一层材料里要不要放测试?

要放,至少放测试名和关键断言。测试是契约最直接的证据:test_cancel_missing_order_returns_404这个名字就把一条契约说清楚了;断言里的状态码、状态变化、审计条数,都是书面约定。把测试放进第一层还有一个额外好处:它天然和验收条件对齐——如果验收条件里有某条测试覆盖不到,这个缺口会在对照时暴露出来。至于测试的实现细节(夹具怎么搭、数据怎么造),留在第二层按需展开。

问:怎么避免"折叠"最后变成"只给文档、不给代码"?

用三条底线检查加一个习惯来兜。三条底线(副作用、异常语义、事务与重试)在第一层必须写明;习惯是"凡是要改动的地方,实现必须展开到能读懂行为为止"——只要涉及修改,就不是"看看签名就能改"的事。还有一个信号值得警惕:如果模型在没有看到任何实现的情况下就给出了完整的代码改动方案,而任务描述里又没有明确说"先给方案不改代码",那说明它开始凭推断改代码了,应当要求它先说明需要展开什么。

问:同一个模块如果一天内要改两次,阅读包要重做吗?

不用重做,只要改任务部分。阅读包的结构(模块清单、接口与契约、排除项、展开规则)是稳定的,随任务变的只有"本次任务"和"已展开记录"两节。做法是把阅读包当成一个模板加实例的组合:模板部分是长期的,实例部分是单次任务的。重复使用同一个模板还有额外收益——你能观察到"这个模块的任务总是要求展开同一段代码",那说明那段代码可能就是下一个值得重构或者补文档的对象。

问:如果接口清单和实现不一致,以哪个为准?

以当前分支的实现和测试为准,并且把这次不一致当成一个必须处理的发现。处理方式有两种:如果实现是正确的、清单过期了,就更新清单;如果实现偏离了原本的设计意图,那这次任务要讨论的是"要不要把实现改回来",而不是悄悄按实现走。无论哪种情况,都不要把不一致留在原地——你此刻看到了它,下一个读到这份材料的人和模型看不到这种不一致,他们只会照着材料理解。把发现写进任务记录,是让这次判断沉淀下来的最便宜的方式。

问:给模型看的接口清单,和给我自己看的,应该是同一份吗?

可以同一份,也可以分层。因为"对模型有用"和"对人有用"的信息偏好不完全一样:模型需要明确的事实(签名、副作用、失败行为、允许修改的范围),人还需要背景和取舍(为什么当初这样设计、哪些地方在计划中要改)。一个实用的做法是同一份文件里分层写:上面是事实清单(双方都读),下面是背景说明(人读,模型也会读到,但不影响事实部分)。最忌讳的是同一份文件里既有事实又有推测,而且没有区分——一旦推测被当作事实使用,这份材料就从帮助变成了误导。

问:模块被重命名或者拆分了,阅读包怎么办?

把重命名和拆分本身当成一次契约变更来处理:在同一个改动里更新阅读包,而不是等下次任务顺手改。具体动作有两件:更新结构清单和路径引用;检查接口清单里的函数有没有换位置或者换了名字——位置信息(哪个文件、多少行)在折叠材料里是最容易过期的部分,所以它应该只在"按需展开记录"里出现,而不是写死在结构说明里。一个实用习惯是把结构清单写成"路径 + 职责",而不是"路径 + 行号";行号只在展开记录里出现,并且带上日期,提醒读者它是某个时刻的快照。

问:如果团队里有人不喜欢写这些材料,怎么推进?

从收益最容易看见的地方开始,而不是从规范开始。选一个"改动经常出意外"的模块,做一次阅读包,用它跑一次真实任务,记录两次的往返轮次和改动范围。把对比放到评审会上,比讲道理有效。另外把动作降到最低:第一版不需要覆盖整个模块,只覆盖这次要碰的函数和它的直接依赖;排除项可以只写三行;契约段落允许先粗糙,遇到缺口再补。材料是长出来的,一开始就要求完整,往往会导致它永远停留在第一版。

问:模型输出里的"展开请求"应该怎么验收?

把它当成方案的一部分来验收,而不是当成聊天内容。具体做法是要求它把展开请求写成固定格式:要看哪个文件、哪几行、想确认什么行为。你收到之后回答两件事:给不给(给的话附上片段)、以及它要确认的行为的正确答案是什么(如果这一行为已经有明确约定)。这样做有两个好处:一是展开范围有记录,下次可以复用;二是它的疑问会被翻译成契约层面的结论,这些结论正好可以回填到接口清单里——一次任务下来,第一层材料反而更完整了。

问:接口清单要写到什么程度才算够用?

标准是"够这次判断"。具体检验方法是:拿清单去回答三个问题——这次改动会影响什么行为?失败时会怎样?有没有外部影响?三个都能答,清单够用;有一个答不上来,就补对应的一行。反过来,如果清单里写了很多这次用不到的细节(比如完整的数据结构定义、历史版本对比),它就该精简——第一层材料的目标是支撑判断,不是穷举事实。不同的任务对"够用"的要求不同,所以接口清单也需要随任务生长,而不是一次写死。

问:自动生成的接口清单能替代人工整理吗?

不能,但能把人工整理的范围砍掉一半。自动生成的部分是"结构事实":有哪些公开函数、参数叫什么、顺序如何、有没有一行说明。这些内容从代码里读出来比手抄准确,也不会漏。人工要补的是"行为约定":前置条件、副作用、失败时会抛什么、事务范围到哪里结束、可重试还是不可重试。这些内容代码里通常没有,测试里只有一部分,只能靠提炼。判断一份清单是否被人工作业覆盖过,方法很简单:如果它只能回答"有哪些函数",说明它还是自动生成的半成品;如果它能回答"调用之后系统里会多出什么",才算完成。

八、动手练习与小结

练习:为一个真实模块做两层阅读包

选一个你最近改动过的模块(最好是那种"文件很长、改动常常出意外"的模块),按下面五步做一份阅读包。

第一步,写结构清单。列出这次的模块边界内所有文件,每个文件一句话职责。写完自检:这些职责里有没有写成"实现了某某算法"的?有的话改成"对谁负责什么"。

第二步,写接口与契约清单。挑出会被本次任务触碰的公开函数,每个写五行:签名、前置、后置、失败、副作用。写不出来的那几行,就是你需要通过读测试或者读实现补齐的部分。

第三步,写排除项与理由。把这次不打算看的部分列出来,每项写一句理由。这一步常常会发现"其实我还不知道它有没有关系"——那就把它从排除项挪进待确认项,不确认完不开工。

第四步,做一次真实任务。用这份阅读包完成一个小改动,记录三件事:模型主动要求展开了哪些内容、实际改动落在哪些文件、有没有出现"绕过契约"的行为。

第五步,回填和归档。把展开记录、改动范围、遇到的问题写回阅读包,并检查契约部分是否需要更新。这份文件现在就变成了下次任务的起点。

做完的产出是:一份两层阅读包(第一层 + 展开规则与记录),以及一份本次任务的对照记录。当你有三四个模块的阅读包之后,你会发现一个变化:新任务开始时不再需要"重新介绍项目",材料从文件里复制即可。

如果练习过程中发现某个函数的契约怎么写都不对,先停下来检查一件事:是不是把它想得太大。一个函数如果同时承担取数据、判断规则、写库和发通知,它的契约就会长到写不下。这种情况下的正确动作不是"再写详细一点",而是在契约里把职责拆开——取数据的部分失败时返回空,规则部分失败时抛业务异常,写库和通知属于同一个事务。契约写清楚了,实现该怎么拆也就自然清楚了。

练习里最难的一步通常是第二步——写契约。写不出来的那些行,恰好就是这次任务最不确定的地方:可能是你不知道副作用有哪些,也可能是团队没有约定失败行为。把"写不出来"当成一个信号:先把它变成一个问题(比如"这个接口失败时抛什么?"),去代码或者测试里找答案,找不到就问人。阅读包的价值,有一半就来自这个逼你把模糊变清楚的过程。

小结

这一篇讲的是给代码"折"出一个合适的呈现方式,核心有四点。第一,接口承载契约,实现只是达成契约的手段,讨论改动应该从契约层开始。第二,折叠不能藏掉会改变行为的信息:副作用、异常语义、事务与重试、功能开关,四条都必须在第一层可见。第三,按需展开要有触发条件和记录:由具体问题驱动,展开范围与理由写进阅读包,避免退化成全量。第四,第一层材料需要维护,契约变化时同步更新;否则它会从"帮助"变成"误导"。

把它和前后篇连起来看:上一篇处理"给哪些材料",这一篇处理"同一个模块的材料怎么分层给"——前者决定材料的集合,后者决定材料的呈现顺序。下一篇继续沿着这条线走:当一次修改可能影响多个模块时,怎么找到真正受影响的代码——也就是从入口到副作用的完整链路,而不是靠搜索关键词碰运气。

最后补一个容易忽略的收益:第一层材料写下来之后,“改动范围"这件事第一次变得可评审。以前评审靠印象——“这个改动看起来只碰了一个文件吧”;现在对照的是书面清单:接口契约里写着哪些副作用、排除项里写着哪些不碰。评审的对话从"我觉得"变成"清单上写着”,这类变化看起来不起眼,但它决定了同样的流程能不能被交给别人执行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询