AI工程的技术债:如何识别和偿还AI系统的债务
AI工程的技术债:如何识别和偿还AI系统的债务
开篇故事
赵强,某互联网公司Java架构师,工作5年。
2024年底,他们快速上线了一个AI问答功能。为了抢时间,每个人都说"先这样,以后再优化"。
18个月后,他找我做代码审查。
光是审查代码,我就发现了:
- 47个 硬编码的中文提示词分散在各个Service里
- 0个 AI功能相关的测试用例
- 0个 针对AI调用的监控指标
- 相同的提示词在8个不同文件中有细微不同的副本
- 模型版本写死在代码里,跨了5个文件
- 没有任何错误处理,AI超时直接给用户抛 NPE
他问我:"这些问题严重吗?"
我问他:"你们最近一次出过几次AI相关故障?"
他说:"上个月3次,上上个月4次……"
我说:"这不是'严重不严重'的问题,这是一个已经在流血的伤口。"
技术债是所有工程项目都有的问题,但AI项目的技术债有其特殊性:
- AI提示词像隐藏的业务逻辑,散落在代码各处
- AI输出的非确定性让没有测试的代码更危险
- AI调用的黑盒特性让没有监控的系统更脆弱
今天我们系统性地讲清楚:如何识别、量化、偿还AI系统的技术债。
TL;DR
- AI技术债的3大类型:提示词债、测试债、可观测性债
- 识别工具:代码扫描 + 债务评分系统
- 偿还策略:提示词中心化 → 测试体系化 → 监控完善化
- 量化指标:每月故障数、发布速度、代码维护时间
一、AI技术债的分类图谱
1.1 技术债分类
1.2 债务严重程度评估
| 债务类型 | 严重程度 | 影响 | 偿还难度 |
|---|---|---|---|
| 无超时控制 | 🔴 严重 | 线程耗尽 → 服务雪崩 | 低(1天) |
| 无降级策略 | 🔴 严重 | AI宕机 → 功能不可用 | 低(2天) |
| 无内容安全 | 🔴 严重 | 违规内容 → 舆论危机 | 中(1周) |
| 提示词硬编码 | 🟡 中等 | 维护困难 → 效率低下 | 中(1周) |
| 零测试覆盖 | 🟡 中等 | 变更风险高 | 高(持续) |
| 无监控 | 🟡 中等 | 故障不可知 | 中(3天) |
| 提示词副本 | 🟢 较低 | 不一致风险 | 低(2天) |
二、技术债识别工具
2.1 自动化债务扫描器
// TechDebtScanner.java
package com.laozhang.techdebt.scanner;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
* AI技术债自动扫描器
* 扫描代码库,识别常见的AI技术债模式
*/
@Slf4j
@Component
public class TechDebtScanner {
// 硬编码提示词的特征模式
private static final List<Pattern> HARDCODED_PROMPT_PATTERNS = List.of(
Pattern.compile("\"(你是|你需要|请|作为AI|作为助手).{10,}\""), // 中文提示词
Pattern.compile("\"(You are|Please|As an AI|As an assistant).{10,}\""), // 英文提示词
Pattern.compile("new SystemMessage\\(\"[^)]{20,}\"\\)"), // Spring AI硬编码
Pattern.compile("SystemMessage\\.from\\(\"[^)]{20,}\"\\)")
);
// 危险模式(无保护的AI调用)
private static final List<Pattern> DANGEROUS_PATTERNS = List.of(
Pattern.compile("chatClient\\.call\\(.*\\)(?!.*timeout)"), // 无超时调用
Pattern.compile("\\.call\\(prompt\\)\\.getResult\\(\\)\\.getOutput\\(\\)\\.getContent\\(\\)(?!.*orElse)"), // 无空值处理
Pattern.compile("catch\\s*\\(Exception e\\)\\s*\\{\\s*e\\.printStackTrace\\(\\)") // 吞异常
);
// 已知坏味道:模型名称硬编码
private static final Pattern MODEL_HARDCODED = Pattern.compile(
"\"(gpt-4|gpt-3\\.5-turbo|claude|qwen|llama)[-\\w]*\""
);
public DebtReport scan(String projectPath) throws IOException {
List<DebtItem> debtItems = new ArrayList<>();
Path root = Paths.get(projectPath);
try (Stream<Path> paths = Files.walk(root)) {
paths.filter(p -> p.toString().endsWith(".java"))
.forEach(javaFile -> {
try {
List<DebtItem> fileDebts = scanFile(javaFile);
debtItems.addAll(fileDebts);
} catch (IOException e) {
log.warn("扫描文件失败: {}", javaFile);
}
});
}
return buildReport(debtItems);
}
private List<DebtItem> scanFile(Path file) throws IOException {
List<DebtItem> items = new ArrayList<>();
String content = Files.readString(file);
String fileName = file.toString();
String[] lines = content.split("\n");
for (int lineNum = 0; lineNum < lines.length; lineNum++) {
String line = lines[lineNum];
// 检查硬编码提示词
for (Pattern pattern : HARDCODED_PROMPT_PATTERNS) {
if (pattern.matcher(line).find()) {
items.add(DebtItem.builder()
.type(DebtType.HARDCODED_PROMPT)
.severity(Severity.MEDIUM)
.file(fileName)
.lineNumber(lineNum + 1)
.description("发现硬编码提示词,建议提取到配置中心")
.suggestion("使用 @Value 或 Nacos 配置管理提示词")
.code(line.trim())
.build());
}
}
// 检查危险模式
for (Pattern pattern : DANGEROUS_PATTERNS) {
if (pattern.matcher(line).find()) {
items.add(DebtItem.builder()
.type(DebtType.MISSING_ERROR_HANDLING)
.severity(Severity.HIGH)
.file(fileName)
.lineNumber(lineNum + 1)
.description("发现不安全的AI调用模式")
.suggestion("添加超时控制、空值检查和降级策略")
.code(line.trim())
.build());
}
}
// 检查硬编码模型名
Matcher modelMatcher = MODEL_HARDCODED.matcher(line);
if (modelMatcher.find() && !line.contains("@Value") && !line.contains("//")) {
items.add(DebtItem.builder()
.type(DebtType.HARDCODED_MODEL_NAME)
.severity(Severity.MEDIUM)
.file(fileName)
.lineNumber(lineNum + 1)
.description("模型名称硬编码: " + modelMatcher.group())
.suggestion("使用配置注入,支持动态切换模型")
.code(line.trim())
.build());
}
}
return items;
}
private DebtReport buildReport(List<DebtItem> items) {
long highCount = items.stream().filter(i -> i.getSeverity() == Severity.HIGH).count();
long mediumCount = items.stream().filter(i -> i.getSeverity() == Severity.MEDIUM).count();
long lowCount = items.stream().filter(i -> i.getSeverity() == Severity.LOW).count();
// 计算债务得分(越高越差)
int debtScore = (int) (highCount * 10 + mediumCount * 5 + lowCount * 1);
return DebtReport.builder()
.totalItems(items.size())
.highSeverityCount((int) highCount)
.mediumSeverityCount((int) mediumCount)
.lowSeverityCount((int) lowCount)
.debtScore(debtScore)
.items(items)
.recommendation(getRecommendation(debtScore))
.build();
}
private String getRecommendation(int score) {
if (score >= 100) return "严重技术债!建议立即启动专项治理";
if (score >= 50) return "中等技术债,建议在下个迭代安排偿还计划";
if (score >= 20) return "轻微技术债,日常迭代中逐步偿还";
return "技术债较少,保持良好的工程实践";
}
public enum DebtType {
HARDCODED_PROMPT, HARDCODED_MODEL_NAME, MISSING_ERROR_HANDLING,
MISSING_TIMEOUT, MISSING_MONITORING, MISSING_TEST, DUPLICATE_PROMPT
}
public enum Severity { HIGH, MEDIUM, LOW }
@lombok.Builder
@lombok.Data
public static class DebtItem {
private DebtType type;
private Severity severity;
private String file;
private int lineNumber;
private String description;
private String suggestion;
private String code;
}
@lombok.Builder
@lombok.Data
public static class DebtReport {
private int totalItems;
private int highSeverityCount;
private int mediumSeverityCount;
private int lowSeverityCount;
private int debtScore;
private List<DebtItem> items;
private String recommendation;
}
}2.2 债务报告生成器
// DebtReportGenerator.java
package com.laozhang.techdebt.report;
import com.laozhang.techdebt.scanner.TechDebtScanner;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Component
public class DebtReportGenerator {
/**
* 生成Markdown格式的债务报告
*/
public String generateMarkdownReport(TechDebtScanner.DebtReport report) {
StringBuilder sb = new StringBuilder();
sb.append("# AI技术债扫描报告\n\n");
sb.append(String.format("生成时间:%s%n%n", java.time.LocalDateTime.now()));
// 概要
sb.append("## 概要\n\n");
sb.append(String.format("| 指标 | 数值 |%n"));
sb.append(String.format("|------|------|%n"));
sb.append(String.format("| 总债务项 | %d |%n", report.getTotalItems()));
sb.append(String.format("| 高危项 | %d |%n", report.getHighSeverityCount()));
sb.append(String.format("| 中危项 | %d |%n", report.getMediumSeverityCount()));
sb.append(String.format("| 低危项 | %d |%n", report.getLowSeverityCount()));
sb.append(String.format("| 债务分数 | %d |%n", report.getDebtScore()));
sb.append(String.format("%n**建议:** %s%n%n", report.getRecommendation()));
// 按类型分组
Map<TechDebtScanner.DebtType, List<TechDebtScanner.DebtItem>> grouped =
report.getItems().stream()
.collect(Collectors.groupingBy(TechDebtScanner.DebtItem::getType));
sb.append("## 详细问题列表\n\n");
grouped.entrySet().stream()
.sorted(Comparator.comparingInt(e -> -e.getValue().stream()
.mapToInt(i -> i.getSeverity() == TechDebtScanner.Severity.HIGH ? 2 :
i.getSeverity() == TechDebtScanner.Severity.MEDIUM ? 1 : 0)
.sum()))
.forEach(entry -> {
sb.append(String.format("### %s (%d项)%n%n",
entry.getKey().name(), entry.getValue().size()));
entry.getValue().stream()
.sorted(Comparator.comparing(TechDebtScanner.DebtItem::getSeverity))
.limit(10) // 每类最多展示10条
.forEach(item -> {
sb.append(String.format("**[%s]** %s:%d%n",
item.getSeverity(), item.getFile(), item.getLineNumber()));
sb.append(String.format("- 问题:%s%n", item.getDescription()));
sb.append(String.format("- 建议:%s%n", item.getSuggestion()));
if (item.getCode() != null) {
sb.append(String.format("```java%n%s%n```%n%n", item.getCode()));
}
});
});
return sb.toString();
}
}三、系统性偿还策略
3.1 提示词集中化(最高优先级)
债务现状:提示词散落在各Service中,维护困难
// 债务代码(Before)- 典型的反模式
@Service
public class CustomerServiceAI {
public String handleRefund(String userMessage) {
// 硬编码提示词,混在业务代码里
String response = chatClient.call(
"你是专业的退款客服,处理用户的退款问题。" +
"退款政策:7天内可无理由退款,30天内质量问题可退款。" +
"用户消息:" + userMessage
);
return response;
}
public String handleDelivery(String userMessage) {
// 另一个Service中,类似但微妙不同的提示词
String response = chatClient.call(
"你是专业的物流客服,处理用户的配送问题。" +
"如遇超时,请告知用户最新物流信息。" +
"用户消息:" + userMessage
);
return response;
}
}偿还后(After):提示词模板管理
// PromptTemplateManager.java
package com.laozhang.techdebt.prompt;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 提示词模板管理器
* 统一管理所有AI提示词,单一真相来源
*/
@Component
@RequiredArgsConstructor
public class PromptTemplateManager {
// 从配置中心注入(不再硬编码)
private final Map<String, String> promptTemplates;
/**
* 获取提示词模板
*/
public String getTemplate(String templateName) {
String template = promptTemplates.get(templateName);
if (template == null) {
throw new IllegalArgumentException("提示词模板不存在: " + templateName);
}
return template;
}
/**
* 渲染带变量的提示词
*/
public String render(String templateName, Map<String, Object> variables) {
String template = getTemplate(templateName);
PromptTemplate promptTemplate = new PromptTemplate(template);
return promptTemplate.render(variables);
}
}# 提示词配置文件(从代码中解放出来)
# prompts/customer-service.yml
prompts:
refund-handler: |
你是专业的退款客服,处理用户的退款问题。
退款政策:7天内可无理由退款,30天内质量问题可退款。
当前日期:{current_date}
用户消息:{user_message}
delivery-handler: |
你是专业的物流客服,处理用户的配送问题。
如遇超时,请告知用户最新物流信息并提供补偿方案。
当前日期:{current_date}
用户消息:{user_message}3.2 超时和降级(最紧急)
// SafeAIClient.java - 安全的AI调用封装
package com.laozhang.techdebt.client;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
/**
* 安全的AI调用客户端
* 偿还:无超时/无降级/无熔断 的技术债
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SafeAIClient {
private final ChatClient chatClient;
private final ChatClient fallbackChatClient;
/**
* 带熔断和超时的AI调用
*/
@CircuitBreaker(name = "ai-client", fallbackMethod = "callWithFallback")
@TimeLimiter(name = "ai-client")
public CompletableFuture<String> call(Prompt prompt) {
return CompletableFuture.supplyAsync(() -> {
try {
String result = chatClient.call(prompt)
.getResult()
.getOutput()
.getContent();
// 防御性编程:处理null返回值
if (result == null || result.isBlank()) {
log.warn("AI返回空内容");
return "抱歉,我暂时无法处理您的请求,请稍后重试。";
}
return result;
} catch (Exception e) {
log.error("AI调用异常: {}", e.getMessage());
throw new RuntimeException("AI调用失败", e);
}
});
}
/**
* 降级方法:主模型失败时调用备用模型
*/
public CompletableFuture<String> callWithFallback(Prompt prompt, Exception e) {
log.warn("主模型失败,启用降级: {}", e.getMessage());
return CompletableFuture.supplyAsync(() -> {
try {
return fallbackChatClient.call(prompt)
.getResult()
.getOutput()
.getContent();
} catch (Exception fallbackEx) {
log.error("降级模型也失败: {}", fallbackEx.getMessage());
return "系统繁忙,请稍后再试或联系人工客服。";
}
});
}
}# resilience4j 配置
resilience4j:
circuitbreaker:
instances:
ai-client:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 3
timelimiter:
instances:
ai-client:
timeoutDuration: 30s # AI调用最长等30秒
cancelRunningFuture: true3.3 监控体系建立
// AIObservabilityConfig.java
package com.laozhang.techdebt.monitoring;
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicLong;
/**
* AI可观测性切面
* 偿还:无监控的技术债
* 为所有AI调用自动添加指标埋点
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AIObservabilityConfig {
private final MeterRegistry registry;
// 成本追踪(单位:微美元)
private final AtomicLong totalCostMicroUsd = new AtomicLong(0);
/**
* 拦截所有 @MonitorAI 注解的方法
*/
@Around("@annotation(monitorAI)")
public Object monitorAICall(ProceedingJoinPoint joinPoint, MonitorAI monitorAI)
throws Throwable {
String service = monitorAI.service();
long startTime = System.currentTimeMillis();
boolean success = false;
try {
Object result = joinPoint.proceed();
success = true;
return result;
} catch (Exception e) {
// 记录错误
registry.counter("ai.calls.errors",
"service", service,
"error_type", e.getClass().getSimpleName())
.increment();
throw e;
} finally {
long latency = System.currentTimeMillis() - startTime;
// 请求计数
registry.counter("ai.calls.total",
"service", service,
"success", String.valueOf(success))
.increment();
// 延迟分布
registry.timer("ai.calls.latency",
"service", service)
.record(java.time.Duration.ofMillis(latency));
log.debug("[AI监控] service={}, latency={}ms, success={}",
service, latency, success);
}
}
}// MonitorAI.java(自定义注解)
package com.laozhang.techdebt.monitoring;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MonitorAI {
String service() default "default";
boolean trackCost() default true;
}3.4 测试债偿还:快速建立测试基线
// AIRegressionTestSuite.java
package com.laozhang.techdebt.test;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* AI回归测试套件
* 偿还:零测试覆盖的技术债
*
* 快速建立测试基线的策略:
* 1. 先为核心路径写E2E测试(而非单元测试)
* 2. 不要追求100%覆盖,先覆盖最高优先级的10%
* 3. 每次修改提示词后运行回归测试
*/
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("AI核心功能回归测试")
class AIRegressionTestSuite {
@Autowired
private CustomerServiceAIFixed customerServiceAI; // 已偿还债务的服务
/**
* 核心业务:退款处理必须给出明确的政策说明
*/
@Test
@Order(1)
@DisplayName("退款处理 - 应包含政策说明")
void testRefundHandler_shouldContainPolicy() {
String response = customerServiceAI.handleRefund("我想退款");
assertThat(response)
.as("退款回答应包含天数政策")
.satisfiesAnyOf(
r -> assertThat(r).containsIgnoringCase("7天"),
r -> assertThat(r).containsIgnoringCase("30天"),
r -> assertThat(r).containsIgnoringCase("退款政策")
);
assertThat(response)
.as("退款回答不应为空")
.isNotBlank()
.hasSizeGreaterThan(20);
}
/**
* 安全边界:不应回答与业务无关的问题
*/
@Test
@Order(2)
@DisplayName("安全边界 - 不回答无关问题")
void testSafetyBoundary() {
String response = customerServiceAI.handleRefund("帮我写一首诗");
// 应该礼貌拒绝或重定向
assertThat(response)
.as("应拒绝无关请求")
.satisfiesAnyOf(
r -> assertThat(r).containsIgnoringCase("退款"), // 重定向到业务
r -> assertThat(r).containsIgnoringCase("无法"), // 礼貌拒绝
r -> assertThat(r).containsIgnoringCase("专注") // 说明范围
);
}
/**
* 响应时间基线测试
*/
@Test
@Order(3)
@DisplayName("性能基线 - 响应时间在阈值内")
void testResponseTimeBaseline() {
long start = System.currentTimeMillis();
customerServiceAI.handleRefund("我的订单退款了吗");
long elapsed = System.currentTimeMillis() - start;
assertThat(elapsed)
.as("响应时间不应超过10秒")
.isLessThan(10000);
}
}3.5 技术债偿还优先级矩阵
// DebtPrioritizationService.java
package com.laozhang.techdebt.prioritization;
import com.laozhang.techdebt.scanner.TechDebtScanner;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class DebtPrioritizationService {
/**
* 计算每个债务项的偿还ROI(投入产出比)
* ROI = 预期收益 / 偿还成本
*/
public List<PrioritizedDebt> prioritize(List<TechDebtScanner.DebtItem> items) {
return items.stream()
.map(item -> {
int expectedBenefit = calculateBenefit(item);
int repaymentCost = calculateCost(item);
double roi = repaymentCost > 0 ? (double) expectedBenefit / repaymentCost : 0;
return PrioritizedDebt.builder()
.debtItem(item)
.expectedBenefit(expectedBenefit)
.repaymentCostDays(repaymentCost)
.roi(roi)
.priority(getPriority(roi, item.getSeverity()))
.build();
})
.sorted(Comparator.comparingDouble(PrioritizedDebt::getRoi).reversed())
.collect(Collectors.toList());
}
private int calculateBenefit(TechDebtScanner.DebtItem item) {
return switch (item.getType()) {
case MISSING_TIMEOUT -> 90; // 超高收益(防止服务雪崩)
case MISSING_ERROR_HANDLING -> 80; // 高收益(防止NPE崩溃)
case HARDCODED_PROMPT -> 60; // 中等收益(维护效率)
case MISSING_MONITORING -> 70; // 高收益(故障可见性)
case HARDCODED_MODEL_NAME -> 50; // 中等收益
case MISSING_TEST -> 65; // 高收益(长期质量保障)
case DUPLICATE_PROMPT -> 30; // 低收益(轻微不一致风险)
};
}
private int calculateCost(TechDebtScanner.DebtItem item) {
return switch (item.getType()) {
case MISSING_TIMEOUT -> 1; // 1天可完成
case MISSING_ERROR_HANDLING -> 1; // 1天可完成
case HARDCODED_PROMPT -> 3; // 3天(需要设计配置结构)
case MISSING_MONITORING -> 3; // 3天(接入监控系统)
case HARDCODED_MODEL_NAME -> 1; // 1天(简单替换)
case MISSING_TEST -> 14; // 2周(建立测试框架)
case DUPLICATE_PROMPT -> 2; // 2天(合并提示词)
};
}
private String getPriority(double roi, TechDebtScanner.Severity severity) {
if (severity == TechDebtScanner.Severity.HIGH) return "P0-立即处理";
if (roi > 50) return "P1-本迭代处理";
if (roi > 20) return "P2-下迭代处理";
return "P3-Backlog";
}
@lombok.Builder
@lombok.Data
public static class PrioritizedDebt {
private TechDebtScanner.DebtItem debtItem;
private int expectedBenefit;
private int repaymentCostDays;
private double roi;
private String priority;
}
}四、赵强团队的债务偿还计划
4.1 实际扫描结果
项目:customer-service-ai(1.8万行代码)
扫描时间:2024-03-15
总债务项:83
- 高危:12项(无超时8项、无降级3项、吞异常1项)
- 中危:47项(硬编码提示词31项、硬编码模型16项)
- 低危:24项(提示词重复副本)
债务分数:388(严重!)
建议:立即启动专项治理4.2 六周偿还计划
4.3 偿还效果数据(6周后)
| 指标 | 治理前 | 治理后 | 改善 |
|---|---|---|---|
| 月均故障次数 | 3.7次 | 0.3次 | -92% |
| 平均故障恢复时间 | 4.2小时 | 18分钟 | -93% |
| 提示词维护耗时 | 8小时/周 | 1.5小时/周 | -81% |
| AI功能测试覆盖率 | 0% | 67% | +67% |
| 月均技术债增量 | 12项 | 2项 | -83% |
五、防止技术债反弹
5.1 代码审查清单
# AI功能代码审查清单
## 必须通过(Blocker)
- [ ] AI调用有超时设置(< 30秒)
- [ ] AI调用有降级策略(备用答案或备用模型)
- [ ] AI调用有错误处理(不直接抛RuntimeException给用户)
- [ ] 无硬编码提示词(应通过配置注入)
- [ ] 内容输入有安全检查
## 建议通过(Warning)
- [ ] 有对应的测试用例
- [ ] 有Micrometer指标埋点
- [ ] 提示词有版本注释(何时修改/为何修改)
- [ ] 模型名称通过配置注入5.2 自动化守护
// 在 CI/CD 中集成技术债扫描
// .github/workflows/debt-check.yml
// 伪代码示意
@SpringBootApplication
public class DebtCheckCLI {
public static void main(String[] args) throws Exception {
ApplicationContext ctx = SpringApplication.run(DebtCheckCLI.class, args);
TechDebtScanner scanner = ctx.getBean(TechDebtScanner.class);
String projectPath = args.length > 0 ? args[0] : ".";
TechDebtScanner.DebtReport report = scanner.scan(projectPath);
System.out.println("债务分数: " + report.getDebtScore());
System.out.println("高危项数: " + report.getHighSeverityCount());
// CI/CD中:高危项超过0个,构建失败
if (report.getHighSeverityCount() > 0) {
System.err.println("发现高危技术债!请先修复后再提交。");
System.exit(1);
}
// 中危项超过20个,构建警告
if (report.getMediumSeverityCount() > 20) {
System.out.println("WARNING: 中危技术债较多,建议安排偿还计划。");
}
System.exit(0);
}
}六、FAQ
Q1:技术债偿还会影响新功能开发吗?
A:短期内会,但这是必要的"投资"。建议:每个迭代分配20%时间偿还技术债(20%规则),不要等到"有时间再说"——那一天不会来。
Q2:如何说服老板投入资源偿还技术债?
A:用数据说话:统计最近3个月AI相关故障次数和故障时长,换算成工程师工时和业务损失。再估算偿还债务的成本。通常ROI是正的。赵强用的就是这个方法,最终获批了6周的专项治理时间。
Q3:提示词版本管理怎么做?
A:把提示词放进Git(通过配置文件),和代码同步版本控制。每次修改提示词都需要提交代码并写清楚原因,就像改代码一样审查。
Q4:新项目如何从一开始就避免这些技术债?
A:三件事:① 提示词从第一天就放配置中心;② 每个AI调用都套 @CircuitBreaker + 超时;③ 写第一行AI代码前先写测试用例框架。
Q5:如何知道技术债偿还到什么程度可以停止?
A:参考标准:高危项 = 0;中危项 < 10;债务分数 < 30;月均AI故障 < 1次。达到这个水平就可以进入"维护模式"(日常迭代中顺手偿还)。
七、总结
AI技术债比传统技术债更危险,因为:
- AI的"未知"放大了技术债的危害:没有测试+无监控+不可预期的AI输出 = 随时可能出生产事故
- AI的"速度"加速了技术债的积累:AI应用开发快,技术债也积累得快
- AI的"可见性差"让技术债更难发现:提示词散落各处,不像普通代码容易审查
赵强的教训:18个月的债,6周偿还,但中间3.7次/月的故障是真实代价。
不要等债积到无法维持再还,每个迭代的20%时间,持续偿还,是AI工程师保持竞争力的必修课。
