接手没人维护的老系统,AI 该从哪三条线切进去

2026-07-29

数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。

多数人把这件事归错因了:以为老系统看不懂是”文档缺失”,于是第一件事就是让 AI 通读代码生成一份架构文档。真正卡住你的不是文档,是事实缺失——你不知道这堆代码里哪些还在跑、哪些数据是活的、哪些动作会往系统外面写东西。 AI 生成的架构文档读起来很顺,但它描述的是”代码写成什么样”,不是”线上正在发生什么”。这两者在一个没人维护的系统里往往差得很远,而你要改的、要背锅的,是后者。

先说清本篇和站内两篇相近文章的分工:把一个大仓塞不进模型、AI 只能看到局部导致答非所问,那是上下文投喂策略的问题,见 大仓库上下文不足;多包仓库里 AI 认错包、改到隔壁模块,那是仓库结构与定位的问题,见 monorepo 里 AI 犯迷糊。这篇不重复那两件事,它只管一个场景:代码你全都能给它看,但没有活人能告诉你这系统在干嘛。此时按什么顺序问、按什么顺序验。

一、先分因:你面对的”没人维护”是哪一种

“没人维护”至少有四种,处置成本差一个数量级,先分清再动手。

第一种是人走了但系统健康:代码能构建,依赖能装上,测试还能跑,只是没人熟。这种最好办,AI 的收益也最大。

第二种是代码在但环境没了:构建脚本引用的内网源、私有镜像、证书全失效。你会遇到装依赖时的证书链校验失败(自签或内网中间证书没进信任链)、拉包超时的 ETIMEDOUT、连接被重置的 ECONNRESET。这类问题跟”看懂代码”无关,是补基础设施。

第三种是仓库与线上不一致:仓库里的分支不是线上跑的那份,或者线上是手工改过的。这种最危险,你读的每一行都可能是假的。

第四种是系统已经半死:一部分入口早就没流量了,但代码还在,把它当成活的去理解会白白消耗大量精力。

判别方法可以做成一张表,每一格都要求你拿到证据再往下走。

现象大概率成因怎么验证处置动作
构建报证书或依赖拉不下来环境断了,不是代码问题换一台干净机器复现;单独 curl 一次依赖源看返回码先修环境,此阶段不要让 AI 读代码
仓库最后提交时间远早于线上最后变更线上被手工改过,或有另一份分支比对线上产物的构建时间戳与仓库最近提交时间;查发布记录以线上产物为准,先把线上那份取回入库
某个模块目录一年多零提交可能已死,也可能是稳定件查访问日志里对应路径有无请求;查数据库表最近写入时间无流量则标记”疑似死代码”,暂不投入理解成本
AI 说得头头是道但引用的函数搜不到模型在补全不存在的实现用 grep 逐个核对它提到的符号名要求它只引用能给出文件路径与行号的内容
同名函数多处定义,AI 每次说法不同存在多份历史副本全仓搜同名符号,看构建脚本实际打包了哪一份先确定构建入口,删掉无关副本的干扰
改一处就有别的功能坏存在隐式副作用或共享状态查是否有定时任务、触发器、消息消费者读同一份数据转到副作用线,先摸全写路径
本地跑得通线上就是不对环境变量或配置来源不一致打印两边实际生效的配置键集合做差集按配置差异定位,别改业务代码

这张表的用法是:先把现象对上,再决定要不要开始”理解代码”。很多团队的浪费就在于跳过这一步,直接进入第二步。

二、入口线:AI 能帮你列全,活没活必须你来定

入口线要回答的问题只有一个:外部世界通过哪些点进入这个系统。HTTP 路由、定时任务、消息队列消费者、命令行脚本、数据库触发器、外部回调,都是入口。

AI 在这条线上能做到的是穷举与归类。你把路由注册文件、任务配置、消费者注册处丢给它,让它输出一张清单,要求每一条都带文件路径和行号。这件事它做得比人快也比人全,尤其是那种入口散落在十几个文件里的老项目。

它做不到的是判断哪个入口还活着。代码里注册了三十个路由,线上可能只有六个有流量。这个判断只能靠运行期证据。最直接的是访问日志:

head -1 access.log                 # 先看一行,确认路径落在第几列
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -50

第二条命令里的 $7 是按 nginx 默认 combined 格式算的:请求行 "GET /path HTTP/1.1" 被空格拆成三段,路径正好排第七。老系统的日志格式常被改过,也可能是 JSON 一行一条,列号未必是 7。所以先跑 head -1 数一遍,再决定 awk 取第几列;是 JSON 就换成 jq -r '.request_uri' 这类取法。带查询串的路径记得先按 ? 截断,否则同一个接口会被参数拆成几百行,统计不出来。

对定时任务,去机器上看实际的 crontab 与调度平台配置,不要信代码注释里写的周期。对消息消费者,看队列的堆积与消费速率。对外部回调,看有没有真实来源 IP 的请求记录。

拿到”活入口清单”之后,再让 AI 只针对这几个入口往下追调用链。范围一收窄,它的准确率会明显上升,你也不用再为几十个死路由付出理解成本。

顺手做一件事:给每个活入口用 curl 打一次,记录返回码。401 或 403 通常说明请求走到了鉴权环节,429 通常说明前面挂了限流,500 通常说明路由确实转到了业务代码而且执行时抛了异常——抛异常也是”这条路径在跑”的证据。

curl -s -o /dev/null -w '%{http_code} %{time_total}\n' https://example.internal/some/path

这里要留个心眼:状态码只是弱证据,不能当结论用。老系统常有统一异常处理,把什么错都包成 200 再在 body 里塞 code;反过来,前面的网关、WAF、反向代理也可能直接代答 403 或 502,请求压根没到应用。所以拿到返回码之后,要用同一次请求在服务端日志里找到对应的那条记录,两边对上了才算数。真正稳的判据是”这次请求在应用日志里留下了痕迹”,不是”curl 回了几”。响应头里的 ServerVia 之类字段也能帮你判断是谁在应答。

如果你打算让 AI 帮忙梳理调用链,注意控制它一次读多少文件。读得太散会出现前后说法不一致,这个坑在 AI 读不到文件时的排查 里有更细的展开。

三、数据流线:AI 能画草图,字段语义只能人来钉

第二条线是数据流:一次请求进来,数据从哪读、写到哪、经过几次转换。

AI 在这条线上能给你一份候选草图:从入口函数往下,把涉及的表名、缓存键、外部调用列出来。它对代码路径的追踪是可靠的,因为那是纯静态信息。

它不可靠的地方在字段语义。老系统里最常见的情况是字段名早就不代表含义了:叫 status 的列里塞了三种业务状态,叫 type 的列被复用了两轮,某个布尔字段的 true 在两年前的一次上线后含义反转过。AI 只能看到列名和取值,它会按字面意思解释,然后你就基于一个错误的语义去改代码。

钉死语义的办法是拿真实数据反推,而不是读代码猜。取一批线上样本,按取值做分布统计,再拿几条你能人工确认的业务记录去对照。

python -c "
import collections, csv, sys
c = collections.Counter(r['status'] for r in csv.DictReader(sys.stdin))
print(c.most_common(20))
" < sample.csv

命令里的 status 是列名,换字段要改成实际的表头;导出的 CSV 如果没有表头行,DictReader 会把第一条数据当成列名,得先补上表头或改用 csv.reader 按下标取。样本量别太小,几百条起步,否则低频取值根本不出现在你眼前,而恰恰是那些低频值最容易在改动后炸出来。

分布本身就会告诉你很多事:如果一个”状态字段”里 99% 是同一个值,剩下的散落着几个只出现过个位数次的值,那多半是历史遗留的一次性数据,不是当前流程会产生的状态。反过来,如果几个取值各占三成,说明它们都是活跃分支,都得覆盖。

拿到分布后,让 AI 做的是”解释这个分布与代码里的写入点是否对得上”。这是个校验任务,比让它凭空解释字段含义靠谱得多。对不上的地方就是你要重点看的地方,往往那里藏着另一条写入路径。

数据流线还有一个必查项:谁在写。除了代码里的写入点,还要查有没有别的服务直连了同一个库、有没有人在跑手工脚本、有没有数据库触发器。一个共享库的老系统,代码只是写入方之一。

四、副作用线:这条线漏了,改坏的就不只是代码

第三条线是副作用:哪些操作会对系统外部产生不可撤销的影响。发短信、发邮件、推消息、扣款、调用第三方写接口、往对象存储写文件、往下游队列投消息,都算。

这条线是三条线里 AI 最容易漏的,原因不难理解:副作用往往藏在很深的调用层级,或者通过配置、事件订阅这种间接方式触发,静态追踪追不全。而它一旦漏了,后果又是三条线里最重的——数据流搞错了大不了读到脏数据,副作用搞错了就是真的发出去了。

所以这条线的做法要反过来:不从入口往下追,而是从”能产生外部影响的能力”往上倒查。先列出这个系统可能用到的外发通道:HTTP 客户端、邮件库、消息队列生产者、短信网关、文件写入、支付相关 SDK。然后全仓搜这些通道的调用点,再往上找是谁调的。

grep -rn --include='*.py' -E 'requests\.(post|put|delete|patch|request)|\.session\.(post|put)|smtplib|boto3\.(client|resource)' .

这条命令按 Python 项目写的,换语言要换关键字,但更要紧的是别把它当成穷举。这类正则天生会漏两类写法:一是二次封装,团队自己包了个 http_client.send(),底下才是 requests,字面上搜不到;二是动态派发,方法名从配置里拼出来。所以搜完还要反过来做一次——从依赖清单入手,把 requirements.txtpackage.json 里所有带网络、邮件、短信、支付语义的库列出来,逐个搜它的 import 点,再顺着 import 往上找调用方。两条路都走一遍,剩下的缺口才小到可以接受。

搜出来的每一处,都要人工确认三件事:这段代码现在还会被执行吗、它作用于生产环境还是被配置关掉了、它是幂等的吗。AI 可以帮你把这三个问题的候选答案写出来,但确认必须由你完成,因为判据在配置和运行环境里,不在代码里。

倒查完之后做一张”危险操作清单”,标出每一项的触发条件。后面任何改动,都拿这张清单过一遍。改动范围一旦越过清单上的项,就得升级为需要评审的变更,这一点和 AI 改动范围失控 里说的边界约定是同一个道理。

关于工具选择顺带说一句:这类任务对模型的长程一致性要求高,有人会想用海外的编码工具。要注意官方对中国大陆有区域限制、不支持直连,市面上存在第三方中转但可靠性与合规性各不相同,这里不做任何推荐。用哪家都一样要遵守上面的规矩——AI 给候选,人给结论。

五、什么情况下别再折腾

理解老系统是个成本无底洞,必须提前设好线。下面几个信号出现,就该停下来换路子,而不是继续加班读代码。

仓库与线上对不上,且拿不回线上那份。 这时候你读的一切都可能是错的。止损动作是停止理解,转为从外部黑盒摸行为:把入口的输入输出录下来,用行为等价的方式重写关键路径。

追了两天,核心数据流仍然有断点。 断点通常意味着存在你还没发现的写入方——另一个服务、一个手工脚本、一个触发器。这时候继续读代码的边际收益已经接近零,应该转去查权限:谁有这个库的写权限,从人和账号倒推,比从代码正推快得多。

每次让 AI 解释同一段逻辑,说法都不一样。 这不是提问方式的问题,是它掌握的信息不足以支撑结论,它在填空。止损动作是把问题拆小到单个函数级别,或者干脆放弃让它解释,改成让它列事实(调用了谁、被谁调用)。

改一处坏三处,且你说不清为什么。 这说明隐式耦合超出了你目前的认知范围。回滚点应该定在这里:立刻把改动全部退回,回到最后一个已知可用的版本,然后转做隔离——在老系统外面加一层,新逻辑走新路径,老逻辑原样保留。这比在内部动刀安全得多。

理解成本已经超过重写关键路径的成本。 判断依据很朴素:如果这个系统的活入口只有几个、数据流也不复杂,只是代码写得乱,那么按行为等价重写往往比考古便宜。老系统的价值在于它承载的业务规则,不在于它的代码。把规则抠出来,代码可以扔。

设一个明确的时间盒。给理解阶段定上限,到点了就按上面的分支走,不要靠”再看看就懂了”这种感觉推进。

六、避坑清单

坑一:让 AI 一上来就生成架构文档。 为什么会踩——生成的文档格式规整、术语齐全,读起来像真的,人天然会信。怎么避——把产出物换成”事实清单”,每条必须带文件路径和行号,你能一条条 grep 核对。核不上的直接划掉,别留在文档里污染后续判断。

坑二:把死代码当活代码理解。 为什么会踩——代码在仓库里就显得重要,没有运行期证据的话,你分不出哪些早就没人调了。怎么避——理解任何模块之前先要一个流量证据:日志里有请求,或者表里有近期写入。两样都没有,先标记后置。

坑三:信字段名的字面意思。 为什么会踩——名字是当年取的,语义是这些年改的,两者早就脱节,而 AI 只能按字面解释。怎么避——用真实数据的取值分布反推,再拿人工可确认的样本对照。

坑四:漏掉非代码入口。 为什么会踩——调度平台的任务、数据库触发器、别的服务的直连,都不在这个仓库里,静态分析看不见。怎么避——把”谁能碰这份数据”当成独立一问,从权限和运维配置去查,而不是只从代码查。

坑五:在还没摸清副作用时就动手改。 为什么会踩——本地测试跑通了会给人一种安全感,但本地往往把外发通道关掉了,副作用在测试里根本不出现。怎么避——先出危险操作清单,改动前逐条比对;对涉及外发的路径,先加一层显式开关再改逻辑。

坑六:多轮对话越问越偏。 为什么会踩——前面几轮的错误结论会留在上下文里,后面它会顺着错的往下推,越推越自洽。怎么避——每确认一批事实就重开一轮,只带上已核实的结论进去,不带推测。这类污染的表现和处理方式,上下文污染 里写得更细。

坑七:把测试通过当成理解正确。 为什么会踩——老系统的测试往往覆盖率低、断言弱,甚至有一批本来就是空跑的。怎么避——先抽查几个测试,故意改坏被测逻辑看它会不会红。不会红的测试没有信息量,参考 测试全绿线上照样爆 里的判断方法。

收尾

这三条线的顺序不能换。先入口,因为它决定了范围;再数据流,因为它决定了正确性;最后副作用,因为它决定了风险上限。跳过前两条直接看副作用,你会漏;只看前两条不看副作用,你会出事。

AI 在整个过程里的定位很清楚:它负责穷举、归类、交叉校验,负责把你要核的东西列全;结论由你拿证据下。凡是需要运行期证据的判断——这个入口活没活、这个字段现在是什么意思、这段代码线上会不会执行——都不在它的能力边界里,硬让它答就是给自己埋雷。

动手前对一遍这份自检清单:

  • 我确认过仓库这份就是线上跑的那份了吗
  • 活入口清单有运行期证据支撑吗
  • 关键字段的语义是拿真实数据核过的,还是按名字猜的
  • 危险操作清单列全了吗,每一项的触发条件写清楚了吗
  • 理解阶段的时间盒定了吗,到点走哪条分支想好了吗
  • 回滚点在哪,回滚要几分钟,试过一次吗

六条里有两条答不上来,就先别改代码。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。