1. 先说清楚:祖传代码难读,难在哪
接手一个跑了七八年、换过四五拨人的老项目,第一次打开仓库的那一刻,多数人的反应是先把终端关掉,去泡杯茶。目录里躺着几千个文件,命名风格三代同堂:有大驼峰的、有下划线的、还有一堆tmp、test2、new_final_v3。README 停留在三年前,写的是"本项目已完成基础框架搭建"。你想问的问题很简单——这个接口被谁调用了、那个定时任务到底干了什么——但答案散在十几个文件里,散在没人记得的 commit message 里。
Zread这类工具要解决的,就是这个场景:把一个结构混乱、注释稀薄、文档缺失的代码仓库,自动读一遍,然后产出一份人能看懂的说明文档。它不负责重构代码,也不承诺帮你修 bug,它只做一件事——把"代码里写了什么"翻译成"人能读的句子",并尽量把模块边界、调用关系、关键流程讲清楚。适合谁用?三类人最直接受益:刚接手老项目的接手人、要做技术交接的离职方、以及要给一堆历史服务补文档却排不出人力的团队。
但得先把话说前面:这类工具生成的是"初稿",不是"定稿"。它能把 60 分的活干到 60 分,剩下那 40 分——业务为什么这么设计、这段代码为什么不能动、这个字段为什么是魔法值——只能靠人补。把它当"接手老项目的加速器"是合理的,把它当"躺平神器"会失望。下面我按自己实际趟过的流程,把整条链路拆开讲。
1.1 三种"看不懂",对应三种不同的解法
老项目让人头疼的"看不懂",其实是三件不同的事,混在一起谈就容易找错工具。
第一种是结构性看不懂:文件太多,不知道从哪进、哪些是核心、哪些是死代码。这种问题靠工具最有效,因为它本质是"信息压缩",把几千个文件压成一张模块清单,机器干这个比人快得多。
第二种是语义性看不懂:单个函数能读懂每一行,但不知道它为什么这么写。比如一个if (status === 3)后面跟着一段奇怪的补偿逻辑。这类东西代码里没有答案,只有 commit 历史或者当事人脑子里有。工具能做的顶多是把它标出来提醒你去问人。
第三种是演进性看不懂:这段代码是历史包袱还是当前主力?改动它风险多大?这个要靠调用频次、覆盖率、近半年变更热区来判断,属于"数据分析"而不是"读代码"。
我把 Zread 定位在第一类的主力、第二类的辅助、第三类的旁观者。认清这个边界,后面所有的操作预期都会顺很多。
1.2 Zread 在这条链路上负责哪一段
按我对这类工具的理解,它的工作流大致是:扫描仓库文件、按语言做语法层面的解析、抽取出符号(类、函数、接口)以及它们之间的引用关系、再结合大模型把"结构 + 代码片段"转成自然语言描述。这里面真正值钱的不是最后那段文字,而是中间那层结构信息——调用图、依赖图、入口清单。文字是结构的表达,结构才是根基。
所以我在用的时候,会刻意分开看两样东西:一是它产出的模块划分和依赖关系对不对,二是它写的文字描述准不准。前者错了,后者必然一错到底,因为描述是基于错误的结构生成的。这也是为什么我后面要花不少篇幅讲"先体检、再切块"——你喂给它的结构越干净,它吐出来的文字越靠谱。指望它自己从一个 5 万文件的仓库里无中生有地找出正确边界,那是为难它。
2. 整体思路拆解:为什么要"先体检、再切块、后生成"
很多人第一次用这类工具的方式是:zread ./my-legacy-project回车,然后等着看奇迹。结果大概率是拿到一份读完更困惑的文档——因为它把整个仓库揉成了一团,模型的上下文装不下,只能挑挑拣拣,最后给你一份"本项目使用 MVC 架构、包含若干模块"这种正确的废话。
我踩过这个坑之后,改成三段式,效果好很多:仓库体检 → 结构切片 → 分块生成 → 人工缝合。这套流程看着麻烦,实际上第一次跑可能要一两个小时,之后就是增量维护,成本很低。
2.1 一次性丢给模型为什么必然翻车
原因很朴素:上下文窗口是有限的,而代码的信息密度极高。一个 3000 行的业务文件,token 量可能上万,一个大点的仓库轻松几十万 token。当输入超出窗口,工具只能做截断或者抽样,被截掉的那部分等于不存在。于是你看到的现象就是:文档里重点描述了根目录下那几个小工具类,真正的核心业务模块只字未提——因为它在第 800 个文件,被截断了。
还有一个更隐蔽的问题:样本偏差会传染。老项目里往往有大量复制粘贴的代码,同一个CommonUtil出现在十几个地方。如果不做去重和归并,模型会把"这个工具类很重要"当成结论写进文档,实际上它只是个被复制了十几次的残渣。切片的价值就在于:你可以按模块维度控制输入,一个模块一个模块过,每块都在窗口内,且互不干扰。
2.2 三段式流程:仓库体检 → 结构切片 → 分块生成
体检这一步不生成任何文档,只做统计:语言分布、文件数、代码行、目录树深度、可疑文件(超大文件、零引用文件、命名异常文件)。这步我宁愿自己用脚本先跑一遍,因为我要的是"我认可的数字",而不是工具给我的黑箱结论。用脚本的好处是可以随时加规则,比如"把vendor、node_modules、dist直接排除"。
切片是把仓库按业务边界切成若干块。切片的依据可以是目录、可以是入口函数向下追两层调用、也可以是 API 路由表。切完给每块起个名字,写在一份清单里——这份清单本身就已经是很有价值的文档雏形了。
生成是逐块喂给工具,每次让它专注于一个块,并要求它输出固定格式:模块职责、对外接口、内部关键流程、外部依赖、已知疑点。固定格式非常关键,因为格式统一了你才能横向对比、才能拼成一份完整文档。最后一步缝合是人做的,把各块的产物按层次拼起来,补上跨块的关系和业务背景。
2.3 参数怎么定:分块大小与上下文的取舍
分块大小没有标准答案,但有个经验区间。块太大,模型注意力被稀释,写出来的东西平均而空洞;块太小,跨文件的调用关系断掉,读起来像一堆孤岛。我的经验值是单块 8 到 25 个源文件、总量控制在 3000 到 8000 行代码(不含空行和注释)。低于 8 个文件的块,我一般合并进相邻块;超过 25 个文件的块,我会按"入口 → 服务 → 数据访问"再切一层。
另外有个容易忽略的参数是输出详细度。同一块代码,让它写"一句话概括"和写"每个公开方法逐一说明",产出差别很大。我通常分两轮:第一轮让它只输出结构骨架(模块、类、方法签名、依赖),第二轮针对骨架里标注的重点方法单独要求详细描述。这样能避免它平均用力,把篇幅浪费在 getter/setter 上。
注意:以上参数是基于我在几个中等规模仓库上的实践经验总结,不同工具的具体参数名和取值范围有差异,配置时以你手上版本的实际文档为准。核心思路是"控制单次输入的信息量",这个跟具体工具无关。
3. 实操第一步:给项目做一次"体检"
体检的目标是搞清楚三件事:这仓库有多少东西、哪些是垃圾、哪些是真正的入口。别小看这一步,我在一个历史项目上光靠体检就省了两天——统计出来 40% 的.java文件从来没有被任何地方 import 过,全是历史遗留。这部分直接排除在分析范围之外,后面的工作量瞬间小了一半。
3.1 语言分布与文件规模摸底
先看语言分布,这决定了你要配哪些解析器、以及工具能不能吃下。一个仓库里如果混着 Java、Python、Shell、还有一堆 SQL 脚本,那就得分开处理,别指望一次全过。
下面这段脚本我几乎每个新接手的仓库都会跑一遍,纯标准库,不依赖任何东西:
import os from collections import Counter, defaultdict IGNORE_DIRS = {'.git', 'node_modules', 'vendor', 'dist', 'build', 'target', '__pycache__', '.idea', 'venv', 'test_fixtures'} CODE_EXT = {'.java': 'Java', '.py': 'Python', '.js': 'JS', '.ts': 'TS', '.go': 'Go', '.sql': 'SQL', '.sh': 'Shell', '.xml': 'XML'} def walk(root): for cur, dirs, files in os.walk(root): dirs[:] = [d for d in dirs if d not in IGNORE_DIRS] for f in files: yield os.path.join(cur, f) stat = Counter() lines = defaultdict(int) big_files = [] for path in walk('.'): ext = os.path.splitext(path)[1].lower() lang = CODE_EXT.get(ext) if not lang: continue try: with open(path, encoding='utf-8', errors='ignore') as fh: n = sum(1 for _ in fh) except OSError: continue stat[lang] += 1 lines[lang] += n if n > 1500: big_files.append((n, path)) for lang, cnt in stat.most_common(): print(f'{lang:8} 文件数 {cnt:5} 总行数 {lines[lang]:8}') print('\n超长文件(>1500 行,重点人工看):') for n, p in sorted(big_files, reverse=True)[:20]: print(f' {n:6} {p}')跑完你会得到两组数字,各有各的用处。
文件数告诉你工作量。如果某个语言就三五个文件,那基本是脚本或者配置,不用花心思;主力语言往往是几百上千个文件,这才是分析重点。
超长文件清单是我最看重的。经验上,一个超过 1500 行的业务文件,八成是个"什么都往里塞"的上帝类,改它的风险极高,读它的价值也极高。这类文件我会单独拎出来人工过一遍,不指望工具能把它讲明白,但我会让工具把它标在文档最显眼的位置,提醒后来人"这里是个雷区"。
提示:忽略目录一定要在第一步就配好。我见过有人忘了排除
node_modules,结果工具花了四十分钟分析了几万个第三方文件,产出的文档里大段在介绍某个前端库的内部实现,非常荒诞。
3.2 识别入口与"事实上的核心模块"
体检的第二件事,是找入口。程序总有起点:Web 服务的路由注册、定时任务的调度配置、CLI 的参数解析、消息队列的消费者声明。找到入口,就有了"顺着往下追"的绳索,这比漫无目的地按目录扫要高效得多。
我的做法是先用关键词粗筛一遍,再人工确认:
# Web 框架的入口线索(按你的技术栈挑几个关键词) grep -rIl --exclude-dir={node_modules,vendor,dist,build,target} \ -E 'RestController|@RequestMapping|app\.(get|post)\(|router\.(get|post)\(|http\.HandleFunc' . | head -40 # 定时任务 / 后台任务 grep -rIl --exclude-dir={node_modules,vendor,dist,build,target} \ -E '@Scheduled|@KafkaListener|@RabbitListener|cron|schedule\(' . | head -40 # 启动类 / 主函数 grep -rIl --exclude-dir={node_modules,vendor,dist,build,target} \ -E 'public static void main|if __name__ == .__main__.|func main\(' . | head -20输出可能有点杂,但方向是对的。拿到候选清单后,我的判断标准很土:能被部署入口直接牵连到的模块,才算核心模块。做法是挑最像入口的那三五个文件,人工往下一层层看调用,看到第二层或第三层,基本就能圈出这个项目真正的业务主干。剩下的目录,要么是工具库,要么是早就没人调的死代码。
这一步做完,你应该能在一张纸上画出五六条主干流程,每条对应一个入口。这张纸上的东西,就是后面切片的依据——清单是手写的,比任何自动生成的图都可靠,因为里面包含了你的判断。
4. 实操第二步:切块与生成文档
到这一步,你手上有了一份排除了垃圾目录的文件清单,和几条主干流程。接下来把仓库切成若干块,一块一块喂给 Zread。切块的粒度我前面说了经验区间,这里补一个更实用的判断法:一个块最好能用一个短句概括它的职责。如果概括不出来,说明切得太杂;如果概括出来是"用户相关的所有东西",说明切得太粗。
4.1 依赖图与切块边界
切块最怕的是把有强耦合的两个模块切开,导致每个块都在说"这里调用了一个外部方法,具体做什么不清楚"。避免这个问题的办法是先手搓一张粗粒度的依赖关系,不用多精确,能看出"谁离不开谁"就够了。
我用的办法是统计跨目录的 import 次数:
import os, re from collections import Counter # 把包名映射成顶层目录,统计 A 目录 import B 目录的次数 pattern = re.compile(r'^\s*(?:import|from)\s+([\w.]+)', re.M) edge = Counter() for cur, dirs, files in os.walk('src'): dirs[:] = [d for d in dirs if d not in {'test', 'tests'}] src_mod = os.path.relpath(cur, 'src').split(os.sep)[0] for f in files: if not f.endswith(('.py', '.java', '.ts', '.js')): continue with open(os.path.join(cur, f), encoding='utf-8', errors='ignore') as fh: text = fh.read() for m in pattern.findall(text): dst_mod = m.split('.')[0] if dst_mod != src_mod: edge[(src_mod, dst_mod)] += 1 for (a, b), c in edge.most_common(30): print(f'{a:20} -> {b:20} {c:4} 次')跑出来你会看到几张"高频连线",它们就是不能拆散的边界。我的规则是:互相引用超过 20 次的目录,要么合并成一个块,要么在文档里明确写成"强耦合,需一起阅读"。这一条能省掉后面大量的"这里看不明白"。
另外提醒一句,脚本统计的是静态 import,动态加载、反射调用、配置文件里的类名全抓不到。所以它只能作为参考,不能作为唯一依据。真正的边界还是得靠人对业务的理解——这也是为什么我一直强调,工具是加速器不是决策者。
4.2 生成配置示例(结构化输入)
切好块之后,组织一份清单文件,把"每块包含哪些路径、职责是什么、依赖哪些其他块"写清楚。这份清单可以手工维护,也可以用脚本从目录结构生成初稿再改。我更喜欢后者,省事。
# blocks.yaml —— 分析范围与块定义(示例骨架,字段名按你所用工具调整) version: 1 project: root: ./legacy-app exclude: - "**/node_modules/**" - "**/vendor/**" - "**/dist/**" - "**/target/**" - "**/*Test.java" - "**/migrations/**" defaults: language: auto detail_level: outline # 第一轮只要骨架:outline;第二轮再改 detailed max_files_per_block: 25 include_patterns: ["**/*.java", "**/*.xml"] blocks: - name: order-core role: 订单主流程(下单、支付回调、状态流转) paths: - src/main/java/**/order/** - src/main/java/**/payment/callback/** notes: 状态机复杂,status 字段有多套枚举并存,需重点标注 depends_on: [user-core, inventory-core] - name: user-core role: 用户与鉴权 paths: - src/main/java/**/user/** - src/main/java/**/auth/** depends_on: [] - name: inventory-core role: 库存扣减与回滚 paths: - src/main/java/**/inventory/** notes: 存在定时对账任务,注意与实时扣减的关系 depends_on: []这份配置里最值钱的其实是role和notes两栏。前者是你给工具下的"命题作文",有了它,生成的内容不会跑偏到"本模块使用了面向对象设计"这种废话上;后者是你把人工判断提前注入,比如"status 有多套枚举并存"——工具看到这句提示,会更倾向于去找枚举定义并在文档里列出来。
depends_on也别省。它决定了文档里"外部依赖"那一节写什么,也方便你后面检查有没有漏掉跨块关系。
4.3 输出结构的组织方式
我要求生成的文档统一按这个骨架走,五个小节,顺序固定:
| 小节 | 内容 | 谁来保证质量 |
|---|---|---|
| 模块职责 | 一句话定位 + 三条主要能力 | 工具生成,人工校准 |
| 对外接口 | 入口函数/路由/消息主题,含参数含义 | 工具生成,人工补业务语义 |
| 关键流程 | 挑 2 到 4 条主干链路,按步骤描述 | 工具生成,人工验证 |
| 外部依赖 | 依赖的模块、中间件、外部服务 | 工具生成 + 脚本核对 |
| 已知疑点 | 魔法值、异常吞掉、TODO、超长方法 | 工具标记,人工判断 |
固定骨架的好处,一是拼装方便,二是缺口一眼就能看出来。哪个模块的"关键流程"只有一句话,那这块大概率没分析透,得回头看看是不是文件被漏掉了,或者切块方式有问题。我一般会拿着这份骨架当成检查表挨个过,比从头读一遍生成的文字高效得多。
5. 生成结果怎么用:从"能看"到"敢改"
文档生成出来了,别急着发群里。我的习惯是先做两件事:交叉验证和补业务语义。前者保证"文档说的和代码做的一致",后者保证"读文档的人能做出正确决策"。这两步不做,文档的寿命大概只有一周——一周后大家发现它不准,就再也没人打开了。
5.1 交叉验证:文档和代码对不对得上
验证的成本比想象中低,因为不需要逐字核对,只需要抽查三类高风险内容。
第一类是接口清单。文档里列了哪些对外接口,拿去和实际的路由注册、API 定义文件对一遍,数量对不上就说明有遗漏。这一步用 diff 就能做,把文档里的接口路径抽出来排个序,跟代码里 grep 出来的排个序,比一比。
第二类是调用关系。挑文档里说的三条主干流程,每条随机选两个环节,去代码里确认这个方法确实被调用了、确实是这条链路。发现对不上的地方,八成是动态调用或者切面拦截导致的,这类地方要在文档里加个注:"此处存在 AOP 拦截,实际执行顺序需另行确认"。
第三类是数据模型。字段名、类型、是否可空,这类信息工具很容易看错(尤其是 ORM 注解和数据库 DDL 不一致的时候)。我的做法是把实体类字段和建表语句各列一份,人工对一遍差异。这一步花的时间不多,但能避免后来人踩"文档说非空、代码里其实可为空"这种坑。
注意:交叉验证要抽着做,不要全量做。全量核对的时间成本接近自己重读一遍代码,那就失去了用工具的意义。我的经验是抽查比例 15% 左右,如果抽查发现错误率超过三成,说明切块或提示词有问题,得回头调整,而不是硬着头皮逐条改。
5.2 补上机器写不出来的那部分
工具生成的文字有个通病:准确但无用。它会把orderStatus的取值全列出来,但不会告诉你"3 这个状态是历史遗留,新流程已经不用了,但老数据里还有几十万条"。这类信息代码里没有,只有人和 commit 历史里有。
所以我每份文档都会手动加一节"历史包袱与雷区",写三到五条。内容来自三个地方:git log 里反复回滚的提交、代码里的注释掉的旧逻辑、以及找老同事问出来的口头知识。
# 找出近两年改动最频繁的文件,这些地方往往最脆弱 git log --since="2 years ago" --name-only --pretty=format: \ | grep -v '^$' | sort | uniq -c | sort -rn | head -20这个清单非常有用。改动频繁的文件,通常意味着两种可能:要么是需求热点(那就值得详细写文档),要么是 bug 高发区(那更值得写)。无论哪种,都该在文档里被重点标注,而不是和几百个文件平铺在一起。
还有一类补充是**"不要动"清单**。老项目里总有一些代码,看起来丑、看起来可以优化,但动了就出事。比如某个字段的顺序依赖了序列化框架的默认行为、某个 SQL 依赖了数据库的隐式排序。这些必须在文档里写得明明白白,用加粗,用"禁止修改"四个字。工具永远不会告诉你这些,它只会诚实地描述现状,甚至可能建议你"此处可以重构"——这个建议在老项目里往往是有害的。
6. 常见问题与排查速查
前面讲的是顺利路径,实际操作中会遇到一堆状况。我把踩过的整理成一张表,遇到问题先查表,比重新摸索快。
6.1 生成内容"大而空"的四种成因
这是最高频的抱怨:"生成的文档洋洋洒洒几千字,读完还是不知道这模块干嘛的。"拆开看,原因基本是四种。
一是输入太杂。一个块里塞了业务代码、工具类、DTO、常量定义,模型分不清主次,只能平均描述。解法是切块时把纯数据类和常量类单独归到"辅助类型"块,让业务块只放有逻辑的代码。
二是没有给定视角。你不告诉它"这个模块的核心是订单状态流转",它就按代码顺序从第一个类讲到最后一个类。解法就是在配置里写role和notes,把命题作文出好。
三是代码本身就没逻辑。有些模块确实就是一层薄薄的透传,Controller调Service调Mapper,每层都没干什么。这种情况下"空洞"是事实,不是工具的错。我的处理方式是合并成一个块,文档里一句话说清"本模块为标准三层透传,无业务逻辑",然后重点写它操作了哪几张表。
四是详细度参数设错了。第一轮用detailed,模型会在有限上下文里平均分配篇幅,每个方法都写两句套话。正确的做法是先用outline拿骨架,再针对重点方法单独追问。
6.2 问题排查速查表
| 现象 | 最可能的原因 | 处理办法 |
|---|---|---|
| 核心模块完全没出现在文档里 | 被exclude规则误伤,或文件数超上限被截断 | 打印实际参与分析的文件清单,逐个核对 |
| 文档里大段介绍第三方库 | 忽略目录没配全 | 补上vendor、node_modules、dist、third_party |
| 同一条流程出现两次,描述还不一致 | 跨块重复收录了同一个文件 | 检查各块paths是否有交集 |
接口参数说明全是String、Object | 泛型/包装类未被解析,或缺少 DTO 定义 | 把 DTO 所在目录一并纳入该块 |
| 调用关系明显错误 | 框架的动态绑定(反射、切面、SPI)未被识别 | 人工补注,不做强行纠正 |
| 生成耗时极长、反复超时 | 单块过大 | 拆分块,单块控制在 25 个文件内 |
| 中文描述夹生、术语不统一 | 未指定术语表 | 配置里给一份术语对照,如order -> 订单 |
| 每次生成结果差异很大 | 未固定分析范围,文件顺序随机 | 固定blocks.yaml,固定输入清单 |
表里最后一条值得单独说。有些人抱怨"同样的代码跑两次结果不一样",这很正常——大模型本身有随机性,加上文件遍历顺序如果不固定,输入顺序一变,关注重点就变。解决办法是把分析范围固化成配置文件并纳入版本管理。blocks.yaml一旦确定,就跟代码一起提交,谁要重新生成文档就用同一份配置,结果至少是可比对的。这一步做了,文档才有"可复现"的属性,才谈得上长期维护。
提示:遇到"明明代码里有这个方法,文档里就是没有"的情况,先别怀疑工具,去确认一下这个方法所在的文件是不是在
include_patterns里。我吃过一次亏,正则写成了**/*.java,结果 Windows 下路径分隔符不一样,全都没匹配上,白折腾半小时。
7. 团队落地:文档怎么才能不老
单次生成一份文档不难,难的是三个月后它还准。我见过太多团队的"文档工程",轰轰烈烈搞一周,之后再也无人更新,最后变成仓库里一个docs/目录,里面躺着 2021 年的 Markdown。要让文档活下去,靠的不是自觉,是机制。
7.1 增量更新与流水线挂钩
我的做法是把"生成文档"变成一个可以低成本重跑的动作,而不是一次性的运动。具体来说分三层。
第一层是范围固定。blocks.yaml进版本库,谁改了模块边界谁更新它,走正常的代码评审流程。这样文档的"目录结构"是受控的,不会因为某个人随手加了个目录就变形。
第二层是触发条件明确。不是每次提交都重新生成(成本太高,也没必要),而是设置两个触发器:一是某个块内的文件变更超过阈值(比如单块累计改动超过 300 行),二是每月定时跑一次全量。前者保证热点模块的文档跟得上,后者兜底。
第三层是产物可追溯。生成的文档头部带上三个信息:生成时间、对应的 commit 短哈希、使用的配置版本。这三个信息看着不起眼,但在排查"文档和代码对不上"的时候能救命——直接看哈希就知道文档落后了多少个提交。
# 生成时把版本信息写进文档头(示例) COMMIT=$(git rev-parse --short HEAD) DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) CFG_HASH=$(sha256sum blocks.yaml | cut -c1-12) echo "> 生成于 ${DATE} / commit ${COMMIT} / 配置 ${CFG_HASH}" > docs/HEADER.md这套机制跑顺之后,文档维护的实际人力投入大概每月一两个小时,主要是审核自动生成的 diff 和补充业务变更说明。比起从零写文档,这个成本是可以接受的。
7.2 我在实际项目里立的几条硬规矩
最后分享几条规矩,都是用教训换来的。
规矩一:文档只写"是什么"和"要注意什么",不写"应该怎么改"。工具很喜欢在结尾加一句"建议对上述方法进行重构以提升可维护性",这种话在老项目文档里是有害的,会诱导新人去动不该动的地方。我在提示里明确要求去掉所有建议类表述,只做描述。
规矩二:疑点必须显式列出,不许藏。魔法值、被吞掉的异常、注释掉的旧逻辑、TODO,全部集中到"已知疑点"一节。这一节允许写得粗糙,允许只有一句话,但必须存在。它的作用是给后来人一个"这里要小心"的信号,比任何漂亮的架构图都有用。
规矩三:文档和代码放在同一个仓库,跟着代码一起评审。放到外部知识库里的文档,三个月后必然和代码脱节,因为没人在改代码的时候会想起来去改它。同仓库的好处是,改动了核心模块,评审时至少有人会问一句"文档更新了吗"。
规矩四:不追求覆盖率 100%。我通常只覆盖主干流程和核心模块,大概占全部代码的 60% 到 70%,剩下的边角料、工具类、一次性脚本,一律不进文档。追求全覆盖的结果是文档臃肿到没人看,反而不如精简版有用。
我个人在几个历史项目上跑下来的体会是,这类工具真正的价值不在"生成文档"这个动作本身,而在于它逼着你把仓库的结构想清楚。为了给它一份能吃的输入,你必须先排除垃圾、找出入口、划定边界、标注雷区——这一圈走下来,你对项目的理解已经涨了一大截,文档只是这个过程的副产品。所以别把顺序搞反了,先想着"我要把这个项目搞明白",工具是路上的拐杖,不是代步车。真要说有什么小技巧,那就是第一次跑千万别贪全,挑一条最核心的主干流程,从入口到数据落库完整走一遍,把这一条链路生成得清清楚楚,你对这套流程的信心和对工具的信任度,都会比跑十个模块更扎实。