第1966篇:技术债与AI功能叠加的危险——在混乱代码库中引入AI的风险控制
第1966篇:技术债与AI功能叠加的危险——在混乱代码库中引入AI的风险控制
我见过一段让我印象深刻的Java代码。
一个Service类,三千多行,里面有各种版本的注释(部分是英文,部分是中文,有几段是拼音缩写),有大量被注释掉但没有删除的"旧逻辑",有几处TODO是五年前写的,方法命名风格至少换过三次……
然后有人在这个类里加了一段AI调用代码,用来"智能化"其中一个业务分支的决策逻辑。
上线两周后,这个AI模块开始偶发性地返回不合理结果。排查了三天,最终发现是因为这个Service类里有一段隐藏的"特殊客户逻辑",会在某些条件下修改传入AI的上下文参数,而这段逻辑本来是十年前某个已经离职的同学为了应付一个紧急需求临时加的,整个团队没有任何人知道它的存在。
AI放大了一个已经存在了十年的隐患。
这件事让我认真思考:在技术债累累的代码库中引入AI,到底有哪些特有的风险?应该怎么控制?
AI让技术债的危害变得非线性
普通的技术债会导致开发效率下降、bug增加、新功能难以迭代。这些影响是相对线性的——债越多,效率下降越多。
但当你在技术债严重的代码库里引入AI时,风险变得非线性:
风险一:AI的行为难以解释,技术债让解释更难。
一个AI模块输出了一个意料之外的结果,你想排查原因。在干净的代码库里,你至少能追踪数据流:输入是什么,经过了哪些处理,最终什么被传给了AI。
在技术债严重的代码库里,数据在到达AI之前可能经过了十几个隐藏的转换,很多转换是"顺手加的"没有任何文档。你不知道AI收到的数据和你以为的数据是否一致。
风险二:技术债会污染AI的训练数据或上下文。
很多AI应用需要从系统中提取特征作为输入,或者把历史数据用于Fine-tuning。如果这些数据本身因为技术债而存在质量问题(不一致的格式、错误的历史记录、冗余的重复数据),AI会学到错误的模式,而你可能在很久之后才意识到。
风险三:AI功能的迭代会和技术债产生耦合,越改越难改。
第一版AI功能上线后,会有需求迭代。但如果AI模块和那些充满技术债的旧代码深度耦合,每次迭代都要同时处理AI逻辑和旧代码问题,工作量比预期大几倍,而且很难预期改动的副作用。
风险四:AI功能的测试难度被技术债放大。
测试AI功能本来就比测试确定性代码难——AI的输出有概率性,边界条件多。在技术债严重的代码库里,你还要应对不可预期的旧逻辑,这两种复杂性叠加,会让测试几乎失去有效性。
在引入AI之前,必须做的三个评估
我现在形成了一个习惯:在任何项目引入AI功能之前,先做这三个评估。
评估一:上下文可信度评估
AI功能的效果高度依赖它接收到的输入质量。在混乱代码库里,这个问题尤其突出。
做法是数据流追踪:从AI调用点出发,向上追踪它接收的每一个参数的来源,画出数据流图。
// 一个检查数据流清洁度的工具类
public class DataFlowAuditor {
/**
* 检查一个对象在传递给AI之前,经过了多少次可能的修改
* 如果修改路径太复杂,是一个警告信号
*/
public DataFlowReport audit(Class<?> aiInputType, String aiCallSite) {
List<String> modificationPoints = findModificationPoints(aiInputType, aiCallSite);
List<String> hiddenTransformations = findImplicitTransformations(aiInputType);
List<String> dataQualityIssues = checkHistoricalDataQuality(aiInputType);
int riskScore = calculateRiskScore(
modificationPoints.size(),
hiddenTransformations.size(),
dataQualityIssues.size()
);
return DataFlowReport.builder()
.modificationPoints(modificationPoints)
.hiddenTransformations(hiddenTransformations)
.dataQualityIssues(dataQualityIssues)
.riskScore(riskScore) // 0-100, 越高风险越大
.recommendation(generateRecommendation(riskScore))
.build();
}
private int calculateRiskScore(int modCount, int hiddenCount, int qualityIssueCount) {
// 超过5个修改点:风险开始显著上升
// 有隐式转换:高风险
// 有历史数据质量问题:直接高风险
return Math.min(100, modCount * 5 + hiddenCount * 15 + qualityIssueCount * 20);
}
}如果数据流追踪发现输入经过了超过5-6个不明确的转换点,我会建议先清理数据流,再引入AI。
评估二:降级路径完整性评估
AI功能必须有清晰的降级路径——当AI不可用或输出不合理时,系统应该如何行为?
在干净的代码库里,降级通常意味着"用旧逻辑兜底"。在技术债严重的代码库里,这个"旧逻辑"本身可能就有问题,或者没有人清楚它的完整行为。
评估问题:
- 当AI返回异常时,系统会做什么?这个行为是被明确定义的,还是"让旧代码继续跑"?
- 旧的"默认行为"有没有已知的bug或不一致?
- 如果AI输出了一个"语法正确但业务不合理"的结果,系统有没有防御机制?
// 防御性AI调用包装,包含多层兜底
public class DefensiveAiWrapper<I, O> {
private final AiService<I, O> aiService;
private final Function<I, O> fallbackFunction;
private final Validator<O> outputValidator;
private final CircuitBreaker circuitBreaker;
public O callWithDefense(I input) {
// 第一层:输入验证
if (!isInputSafe(input)) {
log.warn("AI input validation failed, using fallback");
return fallbackFunction.apply(input);
}
try {
// 第二层:熔断器保护
return circuitBreaker.executeSupplier(() -> {
O aiOutput = aiService.call(input);
// 第三层:输出验证
if (!outputValidator.isValid(aiOutput)) {
log.warn("AI output validation failed: {}", aiOutput);
return fallbackFunction.apply(input);
}
return aiOutput;
});
} catch (CallNotPermittedException e) {
// 熔断器打开时直接走降级
log.info("Circuit breaker open, using fallback");
return fallbackFunction.apply(input);
} catch (Exception e) {
// 其他异常也走降级
log.error("AI call failed, using fallback: {}", e.getMessage());
return fallbackFunction.apply(input);
}
}
private boolean isInputSafe(I input) {
// 检查输入中是否有可能导致AI产生不可预期输出的异常值
// 例如:空字段、超长字符串、格式异常的日期等
return inputSafetyChecker.check(input);
}
}评估三:测试覆盖率和可观测性评估
在引入AI之前,被AI影响的业务路径,有没有足够的测试覆盖?有没有足够的监控?
如果一个核心业务路径只有30%的测试覆盖率,在这个路径里引入AI,你根本无法知道AI是否破坏了原有行为。
最低要求:
- AI直接影响的业务逻辑,单元测试覆盖率不低于80%
- 有端到端的smoke test,覆盖最常用的业务场景
- 有完整的监控告警:AI调用成功率、响应时间、业务指标的变化
技术债清理的优先级:不是全清再上AI
说到这里,有人可能会说:那就先把所有技术债清完,再引入AI。
这不现实,也不必要。
技术债的清理和AI功能引入可以并行,但需要分清楚哪些技术债必须先清,哪些可以暂时共存。
必须先清的技术债(AI引入的前置条件):
AI数据流上的混乱代码:AI的输入数据经过的所有处理逻辑,必须是清晰可理解的,否则无法保证AI收到正确的输入。
无降级路径的旧逻辑:如果"当AI失败时回退到旧逻辑"这条路本身就不可靠,必须先修复。
严重影响数据质量的技术债:如果AI需要用到的历史数据有大量脏数据(重复记录、格式不一致、异常值),必须先做数据清洗。
可以暂时共存的技术债(AI引入后逐步清理):
- AI模块不涉及的代码路径里的技术债
- 对AI输入输出没有影响的历史遗留代码
- 性能问题(可以在AI上线后统一优化)
一个实战经验:AI功能的"隔离区"设计
我在一个有大量技术债的项目里做的一个有效做法:给AI功能设计一个隔离区。
具体是:AI功能所在的模块,对其依赖的上游接口做严格定义,所有进入AI模块的数据必须经过一个统一的转换层,这个转换层负责:
- 从旧系统里提取数据,转成标准化格式
- 做数据清洗和校验
- 记录转换前后的数据,便于追踪问题
这样AI模块自身的边界就很清晰,旧代码的混乱不会直接污染AI的运行环境。
// AI功能隔离区的统一入口
@Service
public class AiContextBuilder {
/**
* 把来自旧系统的各种格式的数据,转换成AI可以稳定处理的标准格式
* 这个类是AI模块和旧系统之间的"防腐层"
*/
public AiContext buildContext(String orderId) {
// 从各个旧系统接口取数(这些接口可能返回奇怪的格式)
RawOrderData rawOrder = legacyOrderService.getOrder(orderId);
RawCustomerData rawCustomer = legacyCustomerService.getCustomer(rawOrder.getCustomerId());
// 标准化转换
AiContext context = AiContext.builder()
.orderId(orderId)
.orderAmount(normalizeAmount(rawOrder.getAmount())) // 处理各种金额格式
.customerTier(normalizeCustomerTier(rawCustomer.getLevel())) // 统一客户等级枚举
.itemCount(safeParseInt(rawOrder.getItems(), 0)) // 处理null和格式异常
.orderAgeHours(calculateOrderAge(rawOrder.getCreateTime())) // 统一时间格式
.build();
// 记录转换,便于问题追踪
contextBuildLog.record(orderId, rawOrder, rawCustomer, context);
return context;
}
private BigDecimal normalizeAmount(Object rawAmount) {
if (rawAmount == null) return BigDecimal.ZERO;
if (rawAmount instanceof BigDecimal) return (BigDecimal) rawAmount;
if (rawAmount instanceof String) {
try {
return new BigDecimal(((String) rawAmount).replace(",", ""));
} catch (NumberFormatException e) {
log.warn("Invalid amount format: {}, using 0", rawAmount);
return BigDecimal.ZERO;
}
}
return BigDecimal.valueOf(((Number) rawAmount).doubleValue());
}
}这个"防腐层"(Anti-Corruption Layer,领域驱动设计里的概念)的思想非常适合AI化改造场景。它让AI模块可以独立演进,同时把旧系统的复杂性控制在一个明确的边界之内。
技术债和AI功能不是非此即彼的关系,关键是知道哪些债是AI的直接风险,哪些债可以容忍。不分青红皂白地"先把技术债清完"会无限推迟AI价值的落地;不管不顾地在混乱代码上叠加AI会放大风险到不可控。精细化的风险识别和分类处理,才是在复杂现实中推进AI化的正确姿势。
