
最近在技术社区看到一个问题“Has anyone made a legacy codebase more legible to AI coding agents? 有没有人真的让旧代码库对AI编程助手变得更容易读懂” 我盯着这个问题想了很久因为过去一年我们团队一直在做这件事把一个有二十多年历史、经历过四轮技术栈叠加、内部人自称“祖传屎山”的电商系统逐步打造成了能让 AI coding agent 快速定位上下文、正确改代码的仓库。这篇文章就是想把这套实操过程完整记录下来交代我们当时遇到的具体问题、踩过的坑以及最后沉淀下来的方法。这篇文章跟前一段时间流行的各种“AI 辅助开发”经验贴不太一样。很多经验贴默认代码库是干净、模块化、命名规范的新项目而我要讲的是另一条路线在不推翻重写、不搞大重构的前提下怎么把一堆历史包袱变成 AI 能读懂的信息结构。适合人群是技术负责人、后端架构师、以及对 AI 编程工具抱有期望但屡屡受挫的资深工程师。先说明我不讨论跑分和理论只讲我们实际怎么做的以及做完之后发生了哪些肉眼可见的变化。1. 我观察到的一个刺眼现实AI 写代码很强但读老代码很弱很长一段时间里我们团队处于一种“AI 很强但用不上”的别扭状态。新项目里Copilot、Claude 写代码效率惊人一个半天就能搭出雏形可一旦让 AI 接手老系统的需求它就频繁给出让人摸不着头脑的方案代码 review 环节大量返工。时间长了大家心里其实已经有结论这堆老代码本身就有问题AI 读不懂不怪它。为了搞清楚“读不懂”到底是什么意思我做了一个实验。我把同一个需求交给 AI但命令它先不要写代码而是用自己的话复述系统的运行逻辑。结果暴露出几个非常典型的痛点语义混乱。老代码里充满了 doWork、handleStuff、data1、data2 这类命名。有一个方法叫 check实际作用是“校验用户地址是否为可配送区域并尝试从备选仓库列表里选一个最近仓库”。这种命名和实现之间的巨大鸿沟让 AI 的推理链基本走不下去因为它只能靠调用关系去反推意图命名本身提供不了任何语义锚点。死代码和僵尸依赖严重干扰搜索视野。老系统经历过多次人员交替保留了大量从未被调用的工具类、注释掉的历史逻辑、“看起来还在用实际上只有测试代码引用”的模块。AI 在搜索上下文时很容易被这些无效信息勾住。你可以想象成一个人在堆满旧家具的房间里找钥匙钥匙明明就在桌上但眼前全是杂物视觉系统很难快速定位。全局状态和隐式约定完全没有书面化。比如我们有一个全局静态线程池所有异步任务都往里面丢这个约束只存在于老员工的脑子里。代码里看不出任何迹象AI 也看不到。于是它多次“合理地”创建新的线程池结果给线上环境带来线程竞争问题。这本质上是信息缺失而不是模型能力不足。调用链深到离谱片段式阅读漏掉隐藏钩子。电商系统里一个最简单的“创建订单”动作从 Controller 到 Service 到 DAO中间还要经过十几个 Kafka 消息回调、缓存更新、库存扣减的钩子漏掉任何一个环节都会出事。AI 一次只能看到局部文件在没有地图的情况下它根本不知道这些隐式钩子的存在。所以先别急着骂 AI 蠢。这就像让一个刚入职的程序员直接看一个没有文档的老项目而且还规定他不能问同事只能自己硬着头皮猜。人类遇到看不懂的地方至少可以找老同事问问AI 没有这个能力它的全部信息来源就是代码本身和我们能塞进提示词的上下文。有一个关键点很容易被忽视人类可以靠长期记忆和团队沟通补足上下文AI 只能靠有限的上下文窗口和文件内容。要让编码代理真正在老代码上落地必须先解决可读性问题再谈生成效率。我这里说的“可读性”不是给代码换一套好看的排版而是把人类脑中的隐式知识显式地、结构化地搬到代码库里让 AI 能够通过文件内容本身建立准确的系统模型。2. 我没有急着重构而是先花两周把“代码地图”画了出来很多人听到“让 AI 读懂老代码”第一反应是“重命名、加注释、清理死代码”。这个直觉没错但顺序错了。如果一边理解业务一边大动干戈很容易改坏功能还把团队代码 review 的时间拉到不可接受的水平。我当时定下的原则很简单先做纯分析不做任何行为修改先让代码库的“导航系统”可读再考虑局部优化。这阶段做的事可以归纳成三件依赖扫描、数据流标注、风险地带识别。我们大概投入了两个人两周的精力产出物是一份名叫 codebase map 的 Markdown 文档。这份文档没有用任何商业收费工具完全靠开源静态分析手段和少量人工整理拼出来的。2.1 用脚本做一次不改变代码的“健康扫描”我们没有用特别复杂的商业工具而是先用现成的开源静态分析手段给整个代码库跑了一遍“体检”。具体做了四件事用 AST 解析出所有函数、类、模块之间的调用关系用正则把 SQL 查询语句里的表名抓出来和具体 DAO 方法做关联抽取 HTTP 路由入口、定时任务入口、消息队列消费者入口做成一份“系统入口清单”扫描测试文件和 Fixture识别出没有被非测试代码引用的孤立模块。这些信息单独看不稀奇但组合起来就是一份粗糙的“调用地图”哪些路由会触发哪些 ServiceService 又依赖哪些 DAODAO 落到哪些表。我们把这个地图输出成文档并且写了一个脚本定期增量更新保证它不会随着代码演进快速过时。数据不会说谎扫描结果让我倒吸一口凉气整个代码库 2400 个可调用实体里有 260 多个孤立模块意思是不被任何线上路径引用还有 40 多个方法从定义开始到今天一个调用方都没有。这些东西每天都在白白占用代码索引空间更是 AI 搜索上下文时最大的噪音来源。如果没有这个扫描过程我根本不敢让 AI 在上面做任何自动化重构因为噪音太多代理随便搜一个词都会出来几十个无关结果。2.2 给关键模块做“数据流标注”光有调用关系还不够。AI 理解一段代码最缺的是“数据从哪来、到哪里去”的上下文。所以第二步我们挑出了订单链路、用户链路、库存链路这三条最核心的业务流用人工方式补齐了数据流标注入口HTTP Controller 从哪拿参数token 怎么校验落库Service 层会把哪些参数组装成 DO哪个 DAO 负责写库旁路哪些情况下会发 MQ 消息消息消费端在哪里缓存哪些查询有 Cache 层失效是主动删还是过期自动失效。有一类典型问题就是这样被发现的订单状态更新的 Service 方法叫 updateOrderStatus看着很正常但内部会先更新本地数据库再发一条 MQ 消息而 MQ 消费者又会反向调用同名的另一个 updateOrderStatus。AI 如果只看 Service 层代码很可能误以为自己找到了幂等逻辑实际上它正站在一个“通过 MQ 形成闭环”的循环链路上。我们把这类“同形不同义”的命名单独列出来在数据流标注里增加了醒目警告。后面 AI 再碰到同类问题时会先去看标注里的警告而不是直接改代码。2.3 风险地带是给 AI 设置的“减速带”人类老手进到一个复杂系统会本能地知道哪些地方要小心订单状态别乱改、库存扣减必须走统一接口、全局配置不能动。AI 不知道。所以我们在 codebase map 里专门加了一节“风险地带”把那些不影响静态编译、但违反全局约束就会炸的模块列出来并写明为什么危险。举一个真实例子。我们有一个全局静态的 HttpClient 单例所有模块都复用它的连接池。AI 第一次看到某个模块自己 new HttpClient很可能觉得这样更独立、更合理但最终会导致整个实例数量膨胀、端口资源被耗尽。这种风险不会在代码编译期暴露只有对系统整体架构有了解的人才知道。我们把这些风险写清楚相当于在 AI 的推理路径上放了一个减速带让它在动手之前先看到规则。效果也很直接人类工程师在给 AI 派任务时不再需要把背景知识一条条写进 prompt只需要让 AI 先读 codebase map再让它自己定位具体文件。相当于先给 AI 配了一份地图和交通标志再让它进入城市而不是让它在陌生街道里靠瞎猜。扫描产物和后续动作的对应关系可以用下面这张表概括扫描发现在地图上的反映后续改造动作死代码多识别出 260 个孤立模块分三批清理部分标记 deprecated同名方法冲突两个 updateOrderStatus 分别标注来源重命名消除歧义全局单例约束不可见“风险地带”记录 HttpClient 单例在 AGENTS.md 中加全局约定数据流不可见订单链路数据流标注在目录 README 中写明缓存和 MQ 钩子3. 在不改变业务行为的前提下让代码本身对 AI 更友好地图有了代码库本体还是那副旧模样。接下来是大家最关心的部分在不重构架构、不影响业务行为的前提下有哪些小改动可以显著提升 AI 的可读性我按实际效果从高到低挑几个我们认为最值得做的事情展开说。3.1 用行为标签替代长篇注释传统注释喜欢解释“这段代码在干嘛”但 AI 读代码时更缺的是“这段代码有什么副作用、在什么约束下可以被修改”。所以我们在关键方法头上加了一种结构化注释格式内部叫“行为标签”。一段典型标签长这样# behavior: idempotent, retryable, not thread-safe # side-effects: updates order_status, sends MQ_ORDER_CHANGED # constraints: must run inside ORDER_TX; requires auth context刚开始有同事觉得这像文档噪音但跑了几次任务后团队很快认同了。原因是 AI 对这种结构化信息非常敏感它比大段自然语言注释更容易被正确检索和理解。对人也一样代码 review 时可以对照标签逐项核对改动是否越界。这个标签体系后来慢慢扩展出了 release-critical、reflection-loaded 等标记本质上等于给关键代码加了一层机器可读的行为契约。3.2 去掉“命中率极低”的死代码和重复实现扫描出 260 个孤立模块后我们没有马上全删而是分三批做了清理先删没有任何引用的工具类再合并功能和命名一致的重复实现最后把测试还在引用、但生产路径已经不用了的模块在代码里标记为 deprecated防止 AI 搜索到后又当成有效代码使用。这里要特别提醒清理死代码不能只靠脚本结果。有些模块静态扫描出来没有引用但可能被反射调用或者通过 SPI 机制加载。我们就差点删掉一个看似无用的工具类后来 grep 才发现它在 XML 里有 Bean 定义属于被容器加载的隐藏依赖。正确的姿势是用脚本圈定候选清单人工逐一确认再合并删除删完立刻跑全量测试。清理带来的收益比想象中大AI 在搜索调用链时上下文里少了几百个无意义文件直接降低了误判率。还有一个容易被忽略的红利是 Token 成本——很多工具在正式让代理改代码之前都会把相关文件内容拉进上下文窗口噪音文件越少开销越低。3.3 统一“同一件事”的实现方式老代码里最让 AI 抓狂的是同一个逻辑有好几种实现。拿数据库连接举例有的模块自己 new Connection有的用 DbUtils有的走 ORM 框架。AI 看到多种写法会认为它们都是合法模式于是新生成的代码风格南辕北辙。我们在短时间内不可能把所有模块全部重写成统一风格于是换了个思路先在代码地图和文档里明确“标准模式是哪一种”然后在容易混淆的旧模块里加简单的标记说明“这里是历史写法新代码请走标准模式”。这样既不需要大改又能引导 AI 输出一致性更强的代码。说白了我们不追求立刻修好每一段历史代码但必须给 AI 一个清晰的“仿写目标”。3.4 命名优化少做“顺眼”重构多做“消除歧义”重构命名优化这事的度很难拿捏。如果每个变量名都按现代风格改一遍review 成本极高还可能引发更多冲突。我们对命名的原则是如果两个模块的名字会让 AI 混淆就必须改如果只是风格不好但不会混淆暂时不动。前面提到的两个 updateOrderStatus一个改成 updateOrderStatusFromPayments另一个改成 updateOrderStatusFromMqConsumer改完 AI 定位目标的精度马上提升。另一个容易被忽略的点是局部变量名。老代码里大量使用 data、result、list、handler 这类名字。AI 在分析数据流时经常被这些无意义变量名打断推理因为它很难区分哪个 data 是订单、哪个 data 是库存。我们把真正关键链路里的这类变量做了局部重命名范围控制在一个方法内部风险极低收益却很大。这类改动不用开评审大会跟着正常代码 review 流程走就行。3.5 给动态加载的边界类打上入口标记反射、SPI、注解处理器这类动态加载逻辑是静态扫描的盲区也是 AI 理解代码时最容易漏掉的部分。我们遇到过一个案例某个支付回调处理类静态分析显示它没有任何直接调用方看起来完全是死代码。但实际它通过 Spring 的事件监听机制被动态注册线上支付结果全靠它处理。AI 如果没有额外提示很可能在“清理死代码”的任务里把它误删。我们的处理方法是在这些边界类顶部加上# entry: loaded by reflection from config.xml之类的标记。这个改动很小但对 AI 理解“这个类不是没人用而是被框架动态装配”非常关键。同时我们在 codebase map 的扫描条目里也增加了对反射入口的自动识别规则让后续新增的动态加载类不至于再被遗忘。4. 上下文库三层结构把“公司记忆”写进 Agent 可见的文档代码本身改到一定程度后AI 还缺一层信息就是这些年来这个系统是怎么运作的、有哪些不成文的规定。传统做法是把这些写进设计文档但设计文档一旦不维护就成了新的“死文档”比没有还糟。我们换了一种思路把上下文拆成三层直接嵌入代码库目录结构里跟随仓库一起进版本管理让 AI 一开始就能读到。4.1 顶层入口文档AI 的“入职手册”我们在代码库根目录放了一份 AGENTS.md作用相当于给 AI 的入职手册。内容分成四段第一段写系统是什么、用什么技术栈、部署方式是单体还是微服务第二段给一份模块列表每个模块用一句话说清职责和负责人第三段列三条最重要的全局约定比如事务怎么开、日志怎么打、配置怎么改第四段是“常见路径速查表”比如想改订单状态先看哪些文件、想新增一个消费端先看哪些文件、想动库存必须先读哪份规范。内容之外长度控制更重要。我们不追求把这本手册写全只写“AI 每次干活前都必须知道的东西”因为信息量太长会稀释注意力也会白白占用上下文窗口。实测下来这份文档控制在 150 行以内效果最好。如果超过 300 行AI 经常会在无关细节里打转反而不如让它直接读代码。4.2 目录级 README按需加载而不是全量塞入如果只有一总文档AI 在改某个模块时还是要把全仓库的大背景都读一遍。这个成本很高而且很多信息跟当前任务毫无关系。于是我们把上下文拆到目录级在每个核心业务目录下放一个十分精简的 README只描述这个目录内的套路、入口、容易被误解的地方。代理在进入某个模块时可以只读取对应目录的 README而不是盲目加载整个仓库的知识。这个设计很像人类团队里的按需文档你新加入一个子团队只需要先看这个子团队的交接文档不需要把公司所有历史都读完。更重要的是它直接控制了 Token 成本。如果我们把所有信息都堆在根目录 AGENTS.md 里一次任务可能就要消耗一根完整的上下文窗口拆成目录级之后代理可以精准地在任务周边加载相关信息成本翻了几倍都稳得住。4.3 错误模式清单把 AI 犯过的错变成“前车之鉴”上下文库里最有价值的部分是我们后来逐渐积累的“错误模式清单”。每当我们发现 AI 因为缺少上下文而犯了某个错误就在这个清单里加一条写明错误场景、正确做法、以及为什么会错。这些条目不需要很长每条两三行就够。举个例子清单里有一条是“不要动 shared/httpclient 单例全系统连接池复用新建实例会导致端口资源耗尽。如果确实需要独立连接池请先与基础设施组确认。” 加了这条之后代理再改相关代码会自动先把这条约束纳入考虑。还有一条是“订单查询不要绕过 OrderCache因为历史数据只有缓存删过才刷新绕过会拿到过期数据。” 这种“踩坑反馈闭环”是我们觉得可读性改造里最有杠杆的部分因为代码库会持续变化AI 也在持续踩坑只有把每次踩坑的信息沉淀下来可读性才不会倒退。4.4 上下文库的维护节奏很多人担心文档写了不更新很快就废掉。我们的解决方案是把上下文维护嵌入到常规开发流程中每次代码 review 时如果发现改动的模块有对应的目录 README必须在同一个 PR 里更新 README 的相关描述每次 AI 犯错导致返工至少追加一条错误模式记录或者修订对应目录的风险提示。这不需要额外开文档维护专项会议只是把“更新上下文”当作和“写测试”同等地位的事情。这样做的结果是上下文没有变成静态的文档而是一直在跟着代码演进。5. 实测复盘用订单字段下线任务验证前与后的差异讲了不少方法可能还是有点虚。我拿一个真实任务来复盘前后差别。任务其实不复杂把订单模块里一个废弃字段下线同时保证历史数据查询不受影响。听起来很简单但老系统里这个字段被 6 个 Service、3 个定时任务和 40 多张报表间接引用是一个标准的三层深调用链。这种任务在遗留系统里很典型改动面广、隐藏依赖多非常适合用来验证可读性改造的成果。改造之前我们直接把任务扔给 AI。它很快给出一份看似完整的方案删字段、改 DTO、改 Mapper、新增启动时数据迁移脚本。但人工 review 后发现它漏掉两个关键点其一报表模块通过一段动态 SQL 拼装的查询引用这个字段普通的静态搜索根本搜不到AI 完全没感知其二有一个定时任务会把旧值回填到该字段AI 没注意到这个回填逻辑导致迁移脚本会和新任务产生竞写冲突。这个方案最后被退回AI 在没有任何提示的情况下很难发现这些隐藏依赖。做完前面几章说的可读性改造后我们重新执行同一类任务。流程变成了三步先让 AI 读根目录 AGENTS.md再让它读订单模块的目录 README最后让它用“分析模式”输出一份影响面清单而不是直接改代码。这次 AI 把动态 SQL 和定时任务回填都列进了影响范围原因是这两个“隐藏钩子”的信息我们已经写进了目录级 README 的风险提示里。最终改动在人工 review 下只微调了两处就通过了耗时也从原来的半天缩短到一小时左右。客观地说并不是每个任务都这么顺利。我们也遇到过上下文文档没覆盖到的边缘情况比如一个 2015 年留下的硬编码枚举不在任何现有文档里AI 照旧漏掉。我们的处理方式很简单先补进错误模式清单再在枚举所在目录的 README 里加一句警告。这就是可读化改造的真实状态它不是一次性工作而是持续积累反馈的过程。你不可能在第一个月就写出覆盖所有情况的文档但系统会随着每次踩坑不断进化三个季度之后AI 能比多数新人更快地上手这个系统。改造前后的对比也可以给几个粗略的指标任务平均 review 返工率从 50% 左右降到 20% 左右单次任务的上下文整理时间从半小时缩短到几分钟AI 在“影响面分析”阶段就能发现一半以上的隐藏依赖剩下的一半靠人去兜底。这些数字不算惊艳但在一个二十年的老系统里已经是肉眼可见的改善了。6. 六条可落地的建议以及成本和时间评估如果你也想在自己团队的遗留代码库上做这件事下面这六条是我认为比市面上大部分泛泛而谈的“AI 辅助开发经验”更实在的路径。先做分析不做修改。给自己两周时间只做依赖扫描、数据流标注、风险识别产出 codebase map。不要在这段时间里顺手改任何代码否则你根本分不清效果来自分析还是来自重构。分析阶段的产出就是后续所有改造的决策依据。找一个高价值薄片先试点。别想着一次覆盖所有模块。选一个业务关键、AI 高频参与、而且你已经比较了解的模块把它的目录 README、风险提示和行为标签先补齐跑两三个任务验证效果。有成效之后其他模块自然会照做。让 AI 先给影响面分析再允许它动手改代码。这相当于给 AI 加了一道强制推理输出能显著减少漏考虑。我们会要求代理先回答几个固定问题这个改动会影响哪些入口哪些副作用会被触发是否需要同步更新文档它回答不上来就不许动代码。每次 AI 犯错都追问一个“这个错误是否源于上下文缺失”。如果是就补一条错误模式记录并更新对应 README。这会把失败转化为数据资产让整个团队的经验持续沉淀。很多团队做可读性改造做到一半没效果就是因为缺了这个反馈闭环。严格控制上下文文件的长度。根目录 AGENTS.md 不超过 150 行目录 README 不超过 60 行错误模式单条不超过三五行。信息过长时 AI 的注意力会被稀释过短则覆盖不了真实情况。要学会像写诗歌一样删字而不是像写 wiki 一样堆内容。每周留一个固定时间做代码库卫生。把新增的死代码、重复实现和过时注释顺手清掉。可读性改造不是一次性项目它需要像保洁一样定期维护否则被长时间遗忘的代码很快会重新变成不可读状态。成本方面我们团队两个人投入两周时间建立第一版地图和上下文库之后每周大约再花半天维护。Token 消耗没有想象中高因为采用的是按需加载模块级上下文而不是把所有文档塞进提示词整体估算下来比以前靠 AI 凭空猜测、反复返工要划算得多。这里没算进去的最大成本是人力的耐心因为前两周几乎看不到产出纯粹在“画地图”很多管理者容易被这个阶段劝退。最后再分享一个观察。AI 编码代理正在变得更强但真正限制它们在复杂老系统上发挥价值的通常不是参数规模而是信息结构。代码库可读性本质上是一个数据工程问题。谁把历史、约束和依赖组织得越清晰谁的 AI 越像一个“带了三年代码经验的同事”而不是一个动不动就闯祸的实习生。而且这个杠杆是确定性很高的你投入多少整理它回报多少效率不会像新技术选型那样充满不确定性。如果你还在犹豫我的建议是挑一个模块花一个下午把它的上下文写好再跑一次真实任务亲自对比一下。那种差异会直接说服你自己。