AI 改完实现没同步注释和文档,怎么把不一致变成可检查的项
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
多数团队把这个问题归错因了:不是 AI 偷懒不写注释,而是你的注释和文档从来没有被任何机制约束过——它只是一段没人校验的自由文本,人改代码时也会漏,只是人改得慢、漏得少,你没察觉。 AI 把改动速度提高了一个量级,漂移量跟着放大,于是问题在这个时候才浮出来。方向找错了,你会花大量精力去调整给 AI 的交代方式,反复叮嘱它「改完记得更新注释」,短期见效、长期照样烂——因为约束在人的记性里,而不在仓库里。真正的解法是把「说明与实现一致」这件事,从口头要求变成一条能被脚本或 CI 判成红绿的检查。
先说清本篇跟站内相邻两篇的分工:怎么审 AI 写出来的代码本身(逻辑、坏味道、安全问题)在 [AI 代码评审工具怎么选](/learn/ai-daima-pingshen-gongju/) 里讲;怎么在动工之前就把需求写成可验收的规格、让实现从一开始有个对照物,在 [什么是规格驱动开发 SDD?AI 时代的工程](/learn/guige-qudong-kaifa-sdd/) 里讲。这篇只管一件窄事:改动已经发生了,说明落后了,你如何把这种落后变成看得见、判得出、拦得住的东西。
一、先分因:四类漂移的现象不一样,处置完全不同
「注释和代码不一致」是个笼统的说法。混在一起处理必然低效,因为其中有一类根本不该修、该删。按你能观察到的现象分开:
第一类,签名级漂移。 函数参数增删了、类型换了、返回值多了一个字段,而上面的 docstring 或 JSDoc 还写着旧参数。这类最容易被机器抓,因为签名是结构化的,注释里的参数列表也是半结构化的,两边可以对齐比较。
第二类,行为级漂移。 签名一个字没变,里面的逻辑变了:原来失败返回 null,现在抛异常;原来按创建时间排序,现在按权重排序。注释写的还是旧行为。这类机器很难自动判,只能靠测试和示例来间接暴露。
第三类,跨文件引用漂移。 代码里的注释指向 docs/xxx.md 的某一节,或者文档里贴了一段代码片段、写了一个文件路径行号。实现挪了位置、函数改了名,引用就成了死链。这类可以被检查,前提是引用写成机器能解析的形式,而不是「详见架构文档相关章节」这种人话。
第四类,陈旧决策记录。 注释里记着「这里先用轮询,等下个版本换推送」,而推送半年前就上线了;或者一段 // TODO: 兼容旧格式 挂在已经删干净的分支旁边。这类的正确处置往往是删,不是改。它们属于技术债的一种表现形式,留着只会让人误以为当前实现还长那样。
分因的意义在于:你不需要让整个仓库的说明都「保持最新」,那个目标不可达。你只需要让第一类和第三类变成机器可判,让第二类靠测试兜住,让第四类被定期清掉。
二、判别表:从现象反推成因和动作
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 注释里的参数名在代码里搜不到 | 签名级漂移,AI 改签名时只动了实现体 | 对同一函数解析签名参数名与注释参数名做集合差 | 写一条签名对照检查,接进 pre-commit 或 CI,差集非空即失败 |
| 文档里的示例代码复制出来跑不通 | 行为级漂移或依赖接口改名 | 把示例抽成可执行文件,在 CI 里真跑一遍 | 示例改为从测试文件中引用,而不是手写在文档里 |
| 文档里的相对链接 404、代码注释指向的文件不存在 | 跨文件引用漂移,文件被移动或改名 | 用链接检查工具扫全仓 md 的相对路径与锚点 | 引用只写文件路径+稳定锚点,禁止写行号;加链接检查门 |
| 注释描述的分支在代码里已不存在 | 陈旧决策记录,改动时只删代码不删说明 | git log -S 查这段说明提到的关键词最后一次出现在实现里的时间 | 直接删注释,把仍有价值的部分挪到提交信息或决策记录里 |
| 说明看着对,但和实际返回结构对不上一两个字段 | 数据契约漂移,返回值靠 AI 顺手加字段 | 用真实响应体和文档里的字段表逐个对照,或加契约测试 | 把字段表改为从类型定义生成,不再手写 |
| 只有一个人改的模块不一致最严重 | 缺少读者,没有任何人被迫读这段说明 | 看这个文件近半年有没有第二个作者提交 | 判断这份说明是否还有存在必要,多半该删或合并 |
用法是从左往右走:先在现象列里找到你手上的那个,别急着改文字,先做「怎么验证」那一步。这一步的价值是确认成因,很多人跳过它,直接照着代码把注释重写一遍,下次照样漂。
三、三层手段:把说明变成可检查的东西
从便宜到贵,依次上。别一上来就搭最贵的那层。
第一层:结构化对照,能机器判的就机器判
签名和注释的对齐是最划算的一笔投入。Python 侧可以直接用标准库把签名和文档字符串都拿到手,做一次集合比较:
import inspect
import re
def check_params(fn) -> list[str]:
sig = set(inspect.signature(fn).parameters) - {"self", "cls"}
doc = inspect.getdoc(fn) or ""
return sorted(p for p in sig
if not re.search(rf"(?<!\w){re.escape(p)}(?!\w)", doc))
这段只做一件事:签名里有、文档里没提到的参数名列出来。它不判断描述得对不对,只判断有没有提。这里用前后不接单词字符的方式匹配、而不是直接 p in doc,是因为后者会把参数 id 在 hidden 里的出现算成「提到了」,漏报比误报更难被发现。粗糙,但它能拦住最高频的那类漂移,而且零维护成本。TypeScript 侧对应的做法是用类型定义生成接口文档,而不是手写字段表——手写的字段表迟早会漂,生成的不会。
同一层里还有一个动作:把注释里的「魔法值」换成对常量的引用。写「超时后重试,见配置」而不是把具体秒数抄一遍。抄下来的数字是漂移的主要来源之一,它没有任何机制保证跟配置同步。
第二层:让示例可执行,用测试给行为兜底
行为级漂移没法靠文本比较发现,只能靠运行。有效的做法是:文档里不写手抄的示例,改为引用测试文件里的真实用例。测试跑得过,示例就是对的;实现行为一变,测试先红,你在文档漂之前就知道了。
如果不方便引用,退一步:把文档里的代码块抽出来单独跑。很多语言生态里有现成的「文档测试」机制,没有的话自己写十几行脚本抽取加执行也够用。判定标准是:这段示例是不是有一条路径会在 CI 里被真正执行到。 只要答案是「没有」,它就一定会在某个时间点变成假的。
行为契约要是靠 HTTP 接口对外的,加一条最小的契约验证。比如用 curl 打一下真实响应,再把响应结构和你声明的字段表比对:
curl -s -o /tmp/resp.json -w '%{http_code}\n' "$API_BASE/items"
python -c "import json;d=json.load(open('/tmp/resp.json'));d=d[0] if isinstance(d,list) else d;print(sorted(d))"
第一行把响应体落盘、把状态码单独打出来(-w 只输出状态码,不会污染 json 文件);第二行取顶层字段名,顶层是数组时退一步取第一个元素的字段名。拿到实际字段列表,跟文档里那份对一遍。这个动作两分钟,能挡掉一整类「文档说有 total、实际叫 count」的扯皮。真正接入 CI 时把它写成断言即可。
第三层:改仓库里的约定,让 AI 的默认行为对你有利
前两层是拦截,这一层是减少产生。AI 改代码时同步不同步说明,很大程度取决于它拿到的改动范围有多明确。改动范围一散,它就顾不上边角。范围如何界定和收紧,[一句需求换来 30 个文件的改动,AI 的手](/learn/ai-gaidong-fanwei-shikong/) 讲得更细,这里只补跟说明相关的两条:
一是把「说明和实现在同一个提交里」变成硬规矩。同一次改动里,实现改了、docstring 没改,评审直接打回。这条规矩靠人执行会松,靠 diff 检查执行才稳:在 CI 里判断本次改动是否触及了带 docstring 的函数体,如果触及而 docstring 未变,给一个警告级别的提示,不阻塞但可见。
git diff --stat origin/main...HEAD
git diff -U0 origin/main...HEAD -- '*.py' | grep -v '^+++' | grep -c '^+'
第二条数的是新增行数,中间那道 grep -v '^+++' 不能省:diff 每个文件都有一行 +++ b/path 的头,直接 grep -c '^+' 会把文件数量算进新增行里,改动涉及的文件越多虚高越明显。先用这两条看清改动面积和形状,再决定要不要上自动判定。面积小的时候人工看更快,别为了自动化而自动化。
二是把项目约定写进仓库里被 AI 每次都读到的那份文件,写成可执行的判据,不要写成态度。「注释要及时更新」是态度,AI 无法据此判断做没做到;「修改函数体时若参数增删,必须同步 docstring 的参数列表」是判据,它能对照检查。写判据不写态度,这是让约定生效的关键差别。
顺带说一句区域问题:海外那批 AI 编程工具里,有不少并未把中国大陆列入官方支持区域,能不能用、以什么方式用,要以各家官网的当前服务条款和支持区域说明为准,别照着旧帖子办事。绕开区域限制的路子我不介绍也不背书。好消息是上面这些检查手段跟你用哪个工具无关,全是仓库侧的东西——换模型、换工具、甚至完全不用 AI,这套检查照旧生效。
四、什么情况下别再折腾
止损判断比修复技巧更省命。以下几种情况,停手。
说明的读者是零的时候。 一段注释半年没人读、这个模块只有一个人碰、没有外部调用方,那么把它修准确的收益接近零。正确动作是删掉,或者压缩成一句「这里做什么」。留着一段自己都不确定准不准的说明,比没有说明更坏——它会误导下一个人,包括误导 AI,让它照着错的描述改出错的实现。
回滚点:AI 一次性重写文档,且你无法逐段核对的时候。 让 AI 批量对齐一批陈旧注释,是个高危动作。它会把不确定的地方补成看起来通顺的内容,而通顺和正确是两码事。这类幻觉性补全的识别方式在 [大模型为什么会「胡说八道」?幻觉是什么、怎么](/learn/damoxing-huanjue/) 里有系统讲法。判断线很简单:如果这一批改动你没有能力逐段核对,就别合,git restore 或者把那次提交丢掉,改成分小批做、每批都核。
换条路:三次以上试图让说明追上实现却又漂了的时候。 说明一直追不上,通常意味着这个东西的形态错了。散在代码里的自然语言描述,本质上是一份没有编译器的第二套实现。换路的方向是让它不需要维护:类型定义代替字段表、测试用例代替示例、生成代替手写、常量引用代替抄写数值。做不到生成的部分,就削减到只保留意图和取舍——「为什么这么做」这类内容不会因为实现改动而失效,「怎么做」才会。
改动量超过评审能力的时候。 一次带上百个文件的说明对齐提交,评审必然走过场。宁可分十次做。这也是文档改动和代码改动最好分开提交的原因:混在一起,评审的注意力会全被代码吸走。
五、避坑清单
坑一:让 AI「顺手把注释也更新一下」。 为什么会踩:这句话听起来是零成本的顺手活。实际上它会在没有依据的地方编出合理的描述,而你因为觉得是顺手活,不会仔细看。怎么避:注释更新当成独立改动做,单独 diff、单独看,或者干脆只要求它删掉已确认过时的部分,不要求它补写新的。
坑二:文档里贴代码片段。 为什么会踩:写的时候最省事,复制粘贴就完了。之后实现变一次,片段就假了,而且没有任何信号告诉你它假了。怎么避:片段改为引用真实文件的路径加稳定锚点,或者引用测试用例;引用绝不写行号,行号是最脆的定位方式,加一行就全错。
坑三:把配置值、限额、阈值抄进注释。 为什么会踩:读注释的人省了一次跳转。代价是这个数字有了两个源头,其中一个永远不会被更新。怎么避:注释里只写「见某配置项」,具体数值留在唯一来源里。各家产品的规则和口径本来也会调整,抄下来的那份必然先过期。
坑四:在冲突里让 AI 自动合并文档。 为什么会踩:文档冲突看起来比代码冲突无害,就随手让 AI 处理。实际上它常把两个版本的描述都保留下来,形成互相矛盾的两段话,比任何一版都糟。怎么避:文档冲突人工判,先确定哪一版对应当前实现,再删另一版;冲突处理的整体纪律见 [让 AI 帮你解 git 冲突](/learn/git-chongtu-ai-jiejue/)。
坑五:一次性上重型文档检查工具,然后全员绕过。 为什么会踩:检查太严、噪音太多,第一周就有人加忽略标记,两周后忽略标记比检查项还多,工具形同虚设。怎么避:第一版检查只判一件最高频的事(比如参数名缺失),失败必须能在两分钟内修好;跑稳一个月再加第二条。
坑六:只在合并前检查,不在本地检查。 为什么会踩:CI 门槛看着够用。可是人在 CI 红了之后才回头改,上下文已经丢了,改起来是二次成本,久了就想绕。怎么避:同一套脚本先在本地钩子里跑一遍,本地拦住的比远端拦住的便宜得多。
坑七:把「说明不一致」当成个人不认真的问题去说。 为什么会踩:现象上确实是某个人漏了。但你批评的是记性,而记性不可靠是常态,尤其在改动速度上来之后。怎么避:每次发现不一致,问的第一句话是「这条为什么没被任何检查拦住」,把结论落到检查上,而不是落到人身上。
收束
这件事的难点从来不是写文档的意愿,而是说明缺少一个约束它的对照物。代码有编译器和测试盯着,说明什么都没有,于是它必然朝着熵增的方向走——AI 只是把这个过程加速了。你能做的是给说明找对照物:签名对照签名、示例对照测试、引用对照真实路径,剩下那些找不到对照物的部分,要么削减到只写意图,要么删掉。
给你一份自检清单,逐条问自己有没有:
- 仓库里是否至少有一条自动检查,专门判「实现改了说明没改」,且它现在是绿的;
- 文档里的每段示例代码,是否都有一条能在 CI 里被执行到的路径;
- 文档和注释里的跨文件引用,是否都不含行号,并且被链接检查覆盖;
- 配置值、阈值、限额这类数字,是否在仓库里只有一个源头;
- 项目约定文件里写的是可对照的判据,而不是「要及时更新」这类态度;
- 最近三个月里,你删掉的陈旧说明是否多过你新增的说明;
- 一次说明对齐的改动,是否小到你能逐段核对完。
七条里有四条是「删」和「减」的方向,这不是偷懒。能不维护的说明才是可靠的说明。