第2470篇:遗留系统的AI改造策略——如何给老系统加上AI能力而不重写
第2470篇:遗留系统的AI改造策略——如何给老系统加上AI能力而不重写
适读人群:架构师、技术负责人、资深工程师 | 阅读时长:约18分钟 | 核心价值:渐进式给遗留系统添加AI能力的实战策略,避免大规模重写风险
公司里总有一些服务,没人敢动,但又不得不依赖。
我见过一个ERP系统,核心模块写于2008年,用的是Struts 1.x加上Oracle存储过程。Spring还没普及的时候写的。这个系统跑着公司几乎所有的财务流程,换掉?代价太大,风险太高。加功能?每次改都战战兢兢,不知道改了什么会触发什么。
但业务在变,他们需要给这个系统加上AI能力——至少要能做一些智能的数据分析和预测。
不重写,怎么加AI?
遗留系统改造的三种策略
策略一:Strangler Fig(绞杀者模式)
把遗留系统的功能逐步迁移到新系统,同时通过路由层让两个系统并存。新AI能力在新系统上开发,旧功能逐渐被新功能替代。
适合:计划长期替换、有资源投入的情况
策略二:Anti-Corruption Layer(防腐层)
在遗留系统外面包一层API,把遗留系统的数据模型和接口翻译成现代化的接口,AI能力基于这个翻译层来构建。
适合:遗留系统相对稳定、不计划重写、只想加新能力
策略三:Sidecar Pattern(边车模式)
给遗留系统加一个"伴生服务",这个服务监听遗留系统的事件或数据,独立运行AI逻辑,通过异步方式将结果回写给遗留系统。
适合:无法修改遗留系统代码、需要最小侵入性
我这篇文章重点讲策略二和策略三,因为这是大多数团队面临的现实情况。
策略二:防腐层实现
1. 数据适配层
遗留系统的数据模型往往很"古老"——大量的冗余字段、神秘的状态码、不一致的命名:
/**
* 防腐层:把遗留系统的ERP数据模型转换为领域友好的模型
* 遗留系统的表结构和字段命名是无法改变的
*/
@Component
public class LegacyERPAdapter {
private final LegacyERPDataSource legacyDataSource;
/**
* 从ERP获取财务凭证,转换为标准财务事件模型
*/
public List<FinancialEvent> getFinancialEvents(
LocalDate startDate, LocalDate endDate) {
// 遗留系统的查询(存储过程调用)
String legacyQuery = """
{CALL GET_VOUCHERS(?, ?, ?)}
""";
List<Map<String, Object>> rawData = legacyDataSource.callProcedure(
legacyQuery,
formatLegacyDate(startDate), // 遗留系统用yyyyMMdd格式
formatLegacyDate(endDate),
"Y" // 遗留系统用Y/N表示boolean
);
return rawData.stream()
.map(this::convertToFinancialEvent)
.filter(Objects::nonNull)
.collect(toList());
}
private FinancialEvent convertToFinancialEvent(Map<String, Object> legacyRow) {
try {
// 遗留系统用数字代码表示凭证类型
String voucherType = decodeLegacyVoucherType(
((BigDecimal) legacyRow.get("VCHR_TYPE")).intValue()
);
// 遗留系统的金额是字符串,有时候有前导空格
BigDecimal amount = parseLegacyAmount(
(String) legacyRow.get("VCHR_AMT")
);
// 遗留系统的部门编码需要映射到新的组织结构
String departmentId = departmentCodeMapper.mapToNewId(
(String) legacyRow.get("DEPT_CD")
);
return FinancialEvent.builder()
.id((String) legacyRow.get("VCHR_NO"))
.type(voucherType)
.amount(amount)
.departmentId(departmentId)
.createdAt(parseLegacyTimestamp((String) legacyRow.get("CREATE_TIME")))
.createdBy((String) legacyRow.get("OPER_ID"))
.build();
} catch (Exception e) {
log.warn("转换遗留凭证数据失败: {}", legacyRow, e);
return null; // 跳过无法转换的数据
}
}
private String decodeLegacyVoucherType(int code) {
// 遗留系统从未有文档说明这些编码含义
// 通过业务访谈和数据分析还原出来的映射
return switch (code) {
case 1 -> "CASH_PAYMENT";
case 2 -> "BANK_TRANSFER";
case 3 -> "EXPENSE_REIMBURSEMENT";
case 4 -> "PAYROLL";
case 10 -> "ADJUSTMENT";
default -> "UNKNOWN_" + code;
};
}
}2. AI能力层(建立在防腐层之上)
@Service
public class FinancialAnomalyDetectionService {
private final LegacyERPAdapter erpAdapter;
private final ChatClient chatClient;
private final StatisticalAnomalyDetector statisticalDetector;
/**
* 对遗留ERP系统的财务数据做AI异常检测
* 遗留系统本身不做任何改动
*/
public FinancialAnomalyReport detectAnomalies(LocalDate analysisDate) {
// 通过防腐层获取数据(遗留系统不知道发生了什么)
List<FinancialEvent> events = erpAdapter.getFinancialEvents(
analysisDate.minusDays(90), // 3个月数据作为基线
analysisDate
);
// 统计异常检测
List<StatisticalAnomaly> statAnomalies = statisticalDetector.detect(events);
if (statAnomalies.isEmpty()) {
return FinancialAnomalyReport.noAnomalies(analysisDate);
}
// LLM分析异常的业务含义
String analysisPrompt = buildFinancialAnalysisPrompt(statAnomalies, events);
ChatResponse response = chatClient.call(new Prompt(
List.of(
new SystemMessage(FINANCIAL_ANALYST_PROMPT),
new UserMessage(analysisPrompt)
)
));
return buildReport(analysisDate, statAnomalies, response.getResult().getOutput().getContent());
}
private static final String FINANCIAL_ANALYST_PROMPT = """
你是一个经验丰富的财务审计专家,专注于发现财务数据中的异常模式。
分析时重点关注:
1. 不寻常的大额支付(超过历史平均的3倍以上)
2. 不符合业务规律的时间模式(比如下班后、周末的大额操作)
3. 特定操作人员的行为偏离(某人的操作金额突然增大)
4. 异常的账户活动(几乎不活跃的账户突然有大量操作)
对于每个异常,要说明:业务上可能的解释(正常的)和需要关注的风险点(异常的)。
""";
}策略三:Sidecar Pattern实现
对于完全无法修改代码的遗留系统(比如第三方的SAP、Oracle EBS等),用Sidecar模式:
@Service
public class CDCSidecarProcessor {
private final ChatClient chatClient;
private final AnalysisResultRepository resultRepository;
/**
* 监听数据库变更事件(通过Debezium CDC捕获)
* 遗留系统不知道这个服务的存在
*/
@KafkaListener(topics = "legacy-db.orders", groupId = "ai-sidecar")
public void processOrderChange(CDCEvent event) {
if (event.getOperation() == CDC_INSERT || event.getOperation() == CDC_UPDATE) {
// 异步处理,不阻塞遗留系统的主流程
processAsyncWithRetry(event);
}
}
private void processAsyncWithRetry(CDCEvent event) {
try {
Map<String, Object> orderData = event.getAfterData();
// 基于变更数据做AI分析
if (isHighValueOrder(orderData)) {
// 高额订单,做风险评估
String riskAnalysis = analyzeOrderRisk(orderData);
resultRepository.save(AnalysisResult.builder()
.sourceId(event.getSourceId())
.sourceTable("orders")
.analysisType("RISK_ASSESSMENT")
.result(riskAnalysis)
.analyzedAt(Instant.now())
.build());
}
} catch (Exception e) {
log.error("处理CDC事件失败,将重试: {}", event, e);
// 不让异常影响Kafka消费,但记录下来
}
}
private String analyzeOrderRisk(Map<String, Object> orderData) {
// 构建分析上下文
String context = buildOrderRiskContext(orderData);
return chatClient.call(new Prompt(
List.of(
new SystemMessage("你是一个风控专家,分析订单的潜在风险。"),
new UserMessage(context)
)
)).getResult().getOutput().getContent();
}
}给遗留系统加AI能力的完整路线图
关键原则:永远从只读开始。先让AI"看"遗留系统的数据,给出分析和建议,但不要让AI直接修改遗留系统的数据。这个阶段验证AI的分析是否准确可信。
踩坑经验
坑一:遗留系统的数据质量往往很差。历史遗留数据有大量的脏数据:NULL值存了特殊字符串("N/A"、"-"、"无"),日期格式不统一,金额有时候是字符串有时候是数字……AI分析前必须先做数据清洗,否则分析结果一文不值。
坑二:不要低估防腐层的工作量。看起来只是"写个适配器",但深入进去会发现遗留系统的数据模型有大量的隐式业务规则,这些规则没有文档,只存在于业务人员的脑子里。理清这些规则,往往需要反复访谈和数据验证,可能要花掉你整个项目的1/3时间。
