一说起AI编程,很多人先想到的是新项目里搭脚手架、写CRUD,那确实爽。但真正能让需求开发效率翻倍的地方,恰恰是那些你既离不开又很头疼的老项目——代码堆了六七年、文档早就失传、改一个需求要翻十几个文件才能定位。我最初在老项目里用AI编程,直接复制粘贴需求描述给大模型,结果它给我生成了一套风格完全不同的新模块,连数据库表名都是自己编的。直到把AI当成"需要喂上下文、需要任务书、需要验证流程"的协作对象之后,效率才真正开始起飞。这篇实战指南,就是我在多个老项目里跑出来的方法论,给每天跟历史代码打交道的后端开发、全栈工程师和技术负责人做参考。
1. 为什么新项目跑得欢,老项目翻车多:AI编程的存量代码困境
1.1 AI在老项目上失效的三个直接原因
先说结论。AI编程工具在新项目里表现好,是因为新项目的一切都是"规范"的:分层清晰、命名统一、依赖明确。老项目恰恰相反,每个角落里都藏着历史补丁、临时方案和"当时不知道为什么要这么写"的代码。我总结下来,AI在老项目上失效通常是三个原因叠加。
第一,AI看不见隐性依赖。老系统里的数据往往不是单一来源,同一份订单金额可能同时存在于订单主表、订单明细、冗余快照表里,各自还因为历史原因有偏差。AI如果没有被告知"这里必须用哪张表、哪个字段、哪个口径",它一定会挑一个看起来最合理的,而这往往就是错的。类似的隐性依赖还包括缓存键、消息队列的topic、定时任务的执行顺序,这些东西不会写进需求文档,但AI默认它们不存在。
第二,业务语言和代码语言之间缺一个翻译官。老项目的需求描述通常是"做一个毛利报表",但落实到代码里,"毛利"到底是用出库单明细的cost_amount字段,还是需要减去对应的入库成本,还是用SKU扩展表里的sku_cost字段?这中间隔着好几层业务判断。你直接把原始需求丢给AI,它只会根据通用行业习惯猜,猜出来的结果大概率不符合你们系统的历史口径。
第三,老项目的约束全是"特殊情况"。老代码里常见这种逻辑:现网某个客户的订单Type=99是历史脏数据,做需求时要跳过;某个接口必须兼容第三方老调用的字段命名;某个定时任务的调度时间被改过三次,日志里还留着兼容逻辑。这些特殊规则不会在代码注释里写清,却在需求实现时一票否决。AI不知道这些,生成代码时天然倾向于"通用、整洁、标准",于是频繁踩雷。
1.2 老项目开发效率提升的真正杠杆点在哪儿
很多团队导入AI编程后,第一感受是"写代码变快了",但看整个需求交付周期,似乎只快了20%。为什么?因为老项目里的时间大头根本不在"写代码"。
一个老需求从提出来到上线,真实时间分布大概是:找代码、读懂现有逻辑占40%,设计改动方案占20%,写代码只占20%,验证和返工占20%。AI再强,它帮你压缩的也只是那20%的"写代码"环节。所以要让老项目需求开发效率真正翻倍,必须把AI的能力往前挪:让它帮你找代码、帮你读代码、帮你确认字段口径、帮你列出潜在影响面。这些才是杠杆点。
我后来给自己定了一条规矩:AI编程在老项目里,我的角色永远是"出任务书的人+验收的人",AI是"执行人"。任务书写得越清楚,AI产出的代码越接近可用状态。反过来,任务书写得越模糊,后续验证和返工的时间就越长。这个道理放在人类开发者身上也一样,只是AI比人类更吃这一套——它不会主动说"等等,你的需求有个歧义"。
2. 需求开发第一步不是让AI写代码,而是把需求翻译成任务
2.1 老项目需求的"隐性知识"收集清单
接一个老项目需求,我的习惯是先打开一个文档,把下面这张清单过一遍。这些信息不一定全部用得上,但凡是填不出来的项目,就是我需要去翻代码、问业务方确认的地方。
| 信息类别 | 要搞清楚的问题 | 为什么老项目特别需要 |
|---|---|---|
| 需求目标 | 这个需求最终要解决什么业务问题 | 老需求经常是"用户随口一句",需要还原真实意图 |
| 涉及模块 | 要改的入口、Service、Mapper、定时任务 | 老项目模块边界混乱,动不动就是跨模块调用 |
| 数据来源 | 数据在哪些表,用哪个字段,口径是什么 | 历史冗余字段极多,选错字段是最高发的错误 |
| 历史约定 | 是否有历史补丁、脏数据、兼容逻辑 | 老项目遍地特殊if,不确认就是定时炸弹 |
| 权限模型 | 是否需要走权限过滤,过滤逻辑在哪 | 通用查询里忘了拼权限条件,属于生产事故级别 |
| 输出要求 | 接口格式、报表字段、导出格式 | AI默认会按"标准做法"设计,容易偏离实际 |
| 验收标准 | 什么算做完、什么数据算正确 | 老数据口径需要人工核验,不是能跑通就行 |
以我最近做的一个仓配管理系统需求为例,客户要"按仓库和SKU维度导出月度毛利明细"。这句话看着简单,但填完清单后发现:毛利里的成本,必须取出库明细里的成本快照字段cost_amount,而不是去SKU表查最新的参考成本,因为这是财务历史口径;仓库过滤字段隐藏在权限过滤器里,查询接口必须复用公共权限逻辑;且成本快照对2023年之前的历史单据是不准确的,报表后台要加一行说明。这些信息,需求文档上一个字都没写。
2.2 需求拆解表:一个能直接喂给AI的结构化任务书
信息收集完之后,我会把它整理成一张"需求拆解表",这才是真正喂给AI的东西。这张表比直接粘贴原始需求有效得多,因为它把模糊的业务需求翻译成了AI能够理解和执行的条目。
【需求任务书】 1. 需求目标:按仓库+SKU维度导出指定时间区间内的毛利明细 2. 业务规则: - 成本金额取 outbound_order_detail.cost_amount - 毛利 = 销售金额 - 成本金额,销售金额取 outbound_order_detail.sell_amount - 2023-01-01之前的订单毛利不保证精确,报表导出时附加备注列 3. 涉及模块: - 入口:OrderReportController#exportMonthlyProfit - 核心逻辑:ProfitReportService(新增方法) - 数据来源:outbound_order、outbound_order_detail、sku_extend 4. 约束条件: - 必须复用 UserWarehouseFilter 做仓库权限过滤 - 不得修改老接口 OrderReportController#exportDailyReport 的任何逻辑 - 不得新建数据库表 - 导出格式:CSV,UTF-8 with BOM 5. 验收标准:以财务提供的2024-06手工统计结果为基准,报表全字段一致你可能会觉得这个任务书太啰嗦,但实测下来,喂给AI的约束越具体,它生成的方案就越贴近项目现状。比如你明确说了"复用UserWarehouseFilter",AI就会主动去翻这个过滤器的用法,而不是自己写一个新的where条件。这个细节非常关键,因为老系统的权限过滤往往散布在多个地方,AI不知道的情况下一定会漏。
2.3 任务边界声明:防止AI"超范围发挥"
AI在老项目里最容易犯的错,不是做得不够,而是做得太多。你让它加一个报表导出接口,它顺手帮你重构了一个公共工具方法;你让它查一下订单状态,它给你新增了一个枚举类。这些"锦上添花"在老项目里全是风险。
所以我在需求任务书里永远有一节叫"禁止事项"。常见的禁止项包括:不要新建表、不要修改公共方法签名、不要动参数校验框架、不要升级依赖版本、不要改变既有接口的返回结构。为什么我要这么强硬?因为老项目的公共方法往往被几十个调用方依赖,AI不会知道其中一个调用方在凌晨定时任务里跑,改了之后编译能过,但运行时就出错。
有一次我在另一个项目里让AI实现一个简单的导出功能,它自己"贴心"地把一个公共Result类的构造方法重载了,理由是"这样调用更简洁"。结果三个老接口的返回序列化结构全变了,测试阶段才发现。所以我的经验是:在任务书里明确写"只允许改动我列出的文件,其他文件一律禁止修改"。这听起来有点蠢,但AI真的需要这种愚蠢而清晰的边界。
3. 给AI喂老代码的正确姿态:上下文工程实战
3.1 哪些代码必须先读,哪些可以直接忽略
AI编程的效果,很大程度上取决于你喂给它的上下文质量。这不是说把整个项目塞进去就好,老项目代码量动辄几十万行,全塞进去不仅上下文装不下,还会让AI的注意力被无关代码稀释。我的做法是把代码分成三类:必读、选读、不读。
必读的是这次需求直接涉及的接口契约、核心Service入口、数据访问层的Mapper或SQL。选读的是公共工具类、和本次改动相关的辅助方法、相近模块的相似实现。不读的是那些跟需求毫无关系的日志处理、监控埋点、历史兼容代码。选读部分可以等AI提出"这里我需要看一下XX"时再补充给它,效果反而更好。
一个判断技巧是:如果一段代码你在做需求时需要断点单步追踪,那它就是必读;如果你只是为了保险才打开看看,那它多半是选读。我通常只喂必读部分,选读部分作为"按需检索"的资源,等AI明确问到再提供。
3.2 三件套上下文打包法:接口契约、数据字典、调用链路
喂给AI的老代码,我习惯打包成"三件套":接口契约、数据字典、调用链路。这三样东西分别回答AI最关心的三个问题:我要对齐什么?我能用哪些数据?我该在哪里下手?
接口契约就是本次需求涉及的Controller方法、Service方法、DTO、返回结构体的定义。我一般直接把方法签名和关键类代码贴进去,让AI知道对接方式。数据字典则是相关的表结构、字段注释、枚举含义,老项目的字段命名经常是历史遗留,比如order_status和order_state同时存在,含义却不一样,不给数据字典AI很容易用错。调用链路就是代码入口到数据库的完整调用路线,包括中间经过了哪些Service、Utils、缓存层,有没有定时任务或消息监听也在使用这些数据。
具体喂法可以这样组织:
【上下文-接口契约】 OrderReportController#exportMonthlyProfit 当前签名: public void exportMonthlyProfit(HttpServletResponse response, @RequestParam String warehouseCode, @RequestParam String startDate, @RequestParam String endDate) 返回值:直接写CSV到response输出流 【上下文-数据字典】 outbound_order:出库单主表,字段 order_id, warehouse_code, order_date, status(1已出库/2作废) outbound_order_detail:出库单明细,字段 detail_id, order_id, sku_code, sell_amount, cost_amount sku_extend:SKU扩展表,字段 sku_code, sku_name, spec, unit 注意:cost_amount是出库时的成本快照,与sk同步的实时成本无关 【上下文-调用链路】 Controller → ProfitReportService.queryProfitList() → OutboundOrderMapper.selectOutboundOrders() → UserWarehouseFilter.doFilter()(仓库权限过滤)把这部分贴给AI之后,它生成的代码质量会明显上一个档次,因为AI不需要再靠猜测去填那些信息空档。
3.3 上下文放不下的取舍策略
老项目经常碰到一个问题:三件套整理出来发现有好几千行,超过了上下文窗口能装下的量。这时候我的处理原则是:先放"约定和规则",不放"实现细节"。数据字典的建表语句可以精简成字段清单,调用链路的完整代码可以缩成伪代码,但业务口径、约束条件、禁止事项一个都不能省。
还有一种更省上下文的办法:让AI先读"骨架"。我先把接口签名的空方法、Mapper接口、以及数据字典贴进去,让它基于这些给出一个实现方案,方案里它对某个字段或方法有疑问时,自然会在回复里询问或要求补充,这样就不需要我把所有代码一次性倒给它了。
我踩过的教训是:上下文太多和太少,失败率一样高。太多的时候AI会盯着一堆无关细节过度设计,太少的时候它又开始瞎编。正确姿势是**"够用的最小集"**——它一旦进入"理解业务口径"的状态,就不要再往里面堆代码了。
4. 老项目接入AI编程的完整工作流:以一个报表需求为例
4.1 需求背景与存量代码摸底
光讲理论没有代入感,我用前面说的仓配管理系统"月度毛利明细报表"需求来复盘一遍完整流程。这个系统维护了快六年,代码风格前后有三拨人写过,有的Service层方法有两百多行,有的数据查询直接用原生SQL拼接,还有一个统一的仓库权限拦截器挂在所有查询入口上。
我接到需求后的摸底结果是这样的:现有代码里已经有一个每日出库报表的Controller和Service,方法和我要新增的月度毛利报表在结构上非常相似;数据库层面,订单主表和明细表是一对多关系,但明细表没有按月份建索引,查询时靠order_date字段过滤。财务方提供了一份6月份手工统计的毛利数据作为校验基准。
这个摸底过程我并没有完全靠人工翻代码,而是用了AI辅助定位——我把模块目录结构丢给AI,让它在指定的几个类里找出"所有已存在的报表相关Controller方法",它的回答帮我锁定了一个已经被前任开发者废弃但还没删掉的旧报表接口。这个接口虽然不能复用,但里面有一段"按仓库过滤+按日期范围导出CSV"的历史实现,成为我这次需求的重要参考样本。
4.2 我的一次完整AI辅助开发过程(步骤复盘)
整个开发过程我分了五步,顺序很重要,不要跳步。
第一步,让AI先复述需求。我把需求任务书和上下文三件套丢给它,要求它用三句话描述自己将要做什么,并列出关键字段口径。这一步能筛掉一半以上的需求误解。AI第一次复述时就没提到"2023年之前的毛利不保证精确"这条备忘规则,说明它根本没把那个备注当成业务约束,我纠正之后,它后续的所有实现都把这个条件带上了。
第二步,让AI给方案,不给代码。我明确要求它先输出实现思路,包括新增哪些方法、复用哪些类、SQL怎么组织、权限过滤在哪一步接入。这个方案我用五分钟审了一遍,调整了一处:AI原本打算新增一个专门的Mapper方法去联查三张表,但我坚持用现有Mapper已有的selectOutboundOrders方法,再做内存组装,因为老系统里这个Mapper方法已经被性能优化过,新联查容易踩大表join的坑。
第三步,确认方案后,进入逐段编写。AI把CSV导出工具类、Service核心计算逻辑、Controller接口分三段输出,每段我都只做局部审查再合并。注意不要让AI一次性输出一个几百行的完整类,那样出了问题定位成本太高。分段的另一个好处是,AI每一段都能基于前一段的实际代码继续写,不会自己脑补一个不存在的接口。
第四步,本地跑通+手工核数。这是整个流程里最不能省的一步。我把AI生成的代码跑起来,用SQL直接查库里的原始数据,手工计算了几条样本数据,和报表导出的结果逐字段比对。第一轮比对发现成本金额差了0.01,原因是数据库里的cost_amount是decimal(18,2),而AI在计算毛利时用了double做中间运算,导致精度漂移。我把这个问题反馈给AI,它很迅速地把中间计算改成BigDecimal,复用老代码里已有的金额计算工具方法。
第五步,全量回归。老项目改接口,最怕影响存量功能。我让AI列出本次改动涉及的全部代码引用点,然后针对这些引用点跑一遍相关的老用例。同时把新接口挂到测试环境,抽样对比了三个仓库、六个月的数据,全部对齐之后才提交代码评审。
4.3 AI生成代码后的四道验证关卡
总结下来,老项目里AI生成代码我只信四道关卡。第一道是编译与启动,这关最简单但能筛掉语法、依赖、注入错误。第二道是数据口径比验,拿真实数据和SQL手工计算做对比,这是老项目特有的,新项目反而不容易遇到。第三道是调用影响面检查,用代码搜索确认改动的类没有影响到其他入口。第四道是Review清单,我手头有一份针对老项目的清单,包含几个必查项:是否保留了既有的null判断、是否修改了公共方法、是否新增了未在任务书承诺外的新文件、是否有自动格式化导致的全文件diff。
这四道关卡看起来麻烦,但正是它们让AI产出从"能跑"变成"能上线"。我在实际使用中发现,只要任务书和上下文给得到位,AI生成的代码通常不会出现逻辑级错误,真正的错误几乎全在这四道关卡里被拦下的细枝末节——类型精度、权限漏配、老接口返回结构变化。这些都是老项目独有的坑。
5. 老项目提示词设计的几个关键技巧:从"能用"到"好用"
5.1 约束词的正确写法:把业务约束翻译成代码约束
很多人给AI下达需求时会写"注意金额精度""注意权限""注意性能",这种写法太虚,AI不会因此改变行为。真正的做法是把业务约束翻译成可检查的代码约束。以金额精度为例,不要写"注意精度",要写"所有金额计算必须使用BigDecimal,禁止使用double或float,金额舍入使用ROUND_HALF_UP保留两位"。
同样的道理,权限约束要写成"查询入口必须调用UserWarehouseFilter#buildWarehouseCondition,禁止在Mapper里自行拼接warehouse_code条件";历史兼容约束要写成"当order_date < 2023-01-01时,cost_amount字段不可靠,导出文件需增加备注列注明'历史数据毛利仅供参考'"。
我把这些写成"正面约束+反面约束"的组合:先告诉AI应该怎么做,再告诉它不要怎么做。比如"复用现有Mapper方法(正面);禁止新增联查SQL(反面)"。我在实测中发现,单独给正面约束AI经常会在细节处跑偏,加上反面约束后跑偏率明显下降,因为它少了很多"自由发挥"的空间。
5.2 报错处理:让AI沿着你的排查思路迭代
老项目开发中报错是家常便饭,AI编程的一个核心优势是它可以陪你迅速排查报错。但把报错直接丢给AI,让它"帮我看看",效果往往一般。正确的做法是把报错连同你的排查思路一起给它,缩小它的注意范围。
我常用的模板是:
【报错信息】 java.lang.NullPointerException at ProfitReportServiceImpl.queryProfitList(Line 87) 【上下文】Line 87对应的代码是:String cost = detail.getCostAmount().toString(); 【我的分析】这个NullPointerException大概率是2023年之前的历史明细cost_amount字段为null, 我怀疑需要在计算毛利前统一判空并默认0。 【请你做】先解释最可能的原因,再给出两种修复方案,说明各自的适用场景。这么做有两个好处。第一,AI不会大海捞针地去猜,它的排查会沿着你的假设聚焦,效率高很多。第二,它能帮你验证自己的判断对不对——如果AI认为最可能的原因和你的猜测不一致,这本身就是一次很好的交叉验证。我在老项目里排查一个"定时任务偶发报错"时,就是靠着给AI喂了我怀疑的缓存空值逻辑,它指出了我漏掉的上游数据更新时机,问题才真正定位。
5.3 要代码也要说明,一次对话拿到可维护的产出
老项目做需求,代码写完不是结束,还要维护。所以我通常要求AI在交付代码时同时输出一段"改动说明",包括:新增了哪些文件、修改了哪些文件、每个修改点的原因、潜在风险有哪些、建议在哪个环节补充测试。这个说明就是我后续做Code Review和评审的输入。
有一次AI生成的代码里修改了一个老工具类的时间格式化方式,表面上看是"小优化",但这段代码被一个导出任务和两个接收第三方回调的接口共用。AI在改动说明里把这条红风险写了出来,我Review时直接决定回退这个改动,保持老格式处理逻辑不变。如果没有这个说明,这类隐藏风险很容易被忽略。
你也可以让AI产出一个简化的"变更影响覆盖范围"清单,比如"方法A被调用方包括X、Y、Z,本次改动会影响X"。这相当于让AI帮你提前做了一次影响力分析,对于老项目这种"一切皆耦合"的环境非常实用。
6. 老项目AI开发最容易踩的坑,以及我的兜底办法
6.1 变更影响面:AI只改了一处,但影响了三处
老项目最典型的耦合场景是一个方法被多个入口共用,AI不知道,你有时候也会漏掉。我做过一个需求是调整订单备注的校验逻辑,AI识别到的调用方只有三个,它更新了任务书里提到的Controller部分。结果上线后,另一个没在明面上的消息消费端在解析订单备注时抛了异常——那个消费端通过消息订阅间接调到了同一段校验逻辑。
现在的兜底办法是分两层:第一层,任务书里明确让AI列出所有调用本次改动方法的代码位置,它列得越全,我的检查范围就越广;第二层,我坚持做全局引用搜索,不依赖AI的判断。老项目里没有任何工具能完全替代IDE的"Find Usages"。这个搜索动作很机械,但确实救了我很多次。
更稳妥的做法是把周期性的公共方法改动全部视为高风险改动:如果AI坚持要改一个公共方法,我会要求它先给出"不修改公共方法、通过新增方法实现需求"的替代方案。只有当替代方案确实不合理时,才允许动公共方法,而且必须配上额外的回归验证。
6.2 新旧代码风格冲突:让AI生成的代码融入老代码
老项目代码风格和新项目差异极大。有的老系统还在用Java 8,日期操作全用Date和SimpleDateFormat,Mapper是手写XML加ResultMap;而AI默认会生成Java 17风格的代码、LocalDateTime、Lombok注解。我在一个老系统里就遇到过这种情况,AI生成的代码在自己的测试里能编译,合并到老项目后因为项目还在用Java 8直接编译失败,还牵动了Maven依赖变更,浪费了整整半天。
解决方案是在任务书里加一段"代码风格基线"。写明本项目的Java版本、是否使用Lombok、日期处理规范、数据库访问方式、异常处理习惯。甚至要写明"禁止使用record、禁止使用var、禁止使用List.of等Java 9+特性",因为这个项目被绑定在Java 8。这个风格描述通常只有三四行,但能避免大量返工。
另外,我建议在任务书里要求AI参考现有的相似实现风格。比如老项目里已有的Service方法都先做参数校验、再查库、再组装返回,那就让AI"参考OutboundOrderServiceImpl中现有方法的代码结构,保持同样的顺序和命名习惯"。AI对"模仿样例代码风格"这件事执行得很好,关键是要给它一个样例。
6.3 性能与兼容性:AI默认的"标准做法"在老项目里可能不标准
老项目的性能瓶颈通常不在代码本身,而在于数据量和历史数据特征。AI生成代码时默认数据量是几千几万行,但老系统的一个订单明细表少说也有几百万行。AI推荐的做法——比如"直接在SQL里join三张表再导出",在这种规模下可能就是灾难。
我在毛利报表需求里就遇到过一次:AI给出方案是在Mapper里写一个大的join查询,我检查后发现那个按月份过滤的order_date没有索引,全表扫描的代价不可接受。最后我让它改成"先按仓库+时间范围查出主表订单,再分批查询明细,最后在内存里组装",这样既避免了长时间锁表,也不会拖垮数据库连接池。
处理这类问题的经验是:如果需求涉及的数据量超过几十万行,就要在任务书里明确标注,并告诉AI"必须优先考虑分批处理、避免大表join、禁止全表扫描"。如果老系统数据库版本比较老,某些新写法支持不了,也要提前说明。比如有一个老项目还在用MySQL 5.7,AI建议使用"窗口函数ROW_NUMBER",但实际上该环境不支持,这种只在特定版本才有坑的问题,最好在任务书里直接封死。
说实话,用了AI编程之后,我最大的感觉不是"写代码轻松了",而是"对老代码的理解和敬畏反而更深了"。因为你要把需求拆得足够细、把上下文整理得足够清楚,才能让AI发挥效力;而这个过程本身,就是对一个老项目底层逻辑的系统性梳理。现在每接到一个老项目新需求,我的流程已经固定:先写任务书,再喂三件套,然后让AI给方案、给代码、给出变更说明,最后用四道关卡验收。这个流程跑顺之后,交付一个中型需求的时间从三四天压到了两天以内。如果你也在老项目里被AI生成的代码坑过,不妨回去检查一下你的任务书和上下文准备环节——大多数翻车,问题都出在那一步。