第2300篇:AI系统的蓝绿部署——零停机切换LLM服务版本
2026/4/30大约 6 分钟
第2300篇:AI系统的蓝绿部署——零停机切换LLM服务版本
适读人群:负责AI服务上线和版本管理的工程师 | 阅读时长:约14分钟 | 核心价值:掌握AI服务特有的发布挑战和蓝绿部署的工程实现
AI服务的发布比普通服务危险得多。
普通服务的一次发布,如果有bug,回滚就行了——代码逻辑的变化是确定的,回滚后行为会恢复。但AI服务的发布还可能涉及:更换了LLM模型(比如从Claude Sonnet 3到Claude Sonnet 3.5)、修改了提示词、修改了RAG的检索策略。
这三种变更都有一个共同特点:即使没有代码bug,输出质量也可能发生变化,而且不容易通过自动化测试完全覆盖。一旦上线发现问题,"回滚"到旧版本的决策成本比代码bug要高——你不知道到底是新模型的问题还是提示词的问题,也不知道改回去是不是真的更好。
蓝绿部署给了你一个关键能力:在完全上线前,用真实流量的一小部分来验证新版本。
AI系统蓝绿部署的特殊性
传统蓝绿部署关注的是可用性:蓝(旧版本)和绿(新版本)同时运行,流量在两者之间切换,确保无停机。
AI系统的蓝绿部署除了可用性,还需要关注质量:
- 新模型的回答质量是否和旧模型相当甚至更好?
- 新提示词是否能正确处理各种边界情况?
- 切换后的用户满意度有没有变化?
这些质量指标不能靠功能测试覆盖,需要用A/B测试或金丝雀发布的方式,在真实流量上验证。
流量路由实现
在Java API网关层实现流量分配:
@Component
public class BlueGreenTrafficRouter {
private final AtomicInteger greenTrafficPercent = new AtomicInteger(0);
private final AiServiceClient blueClient; // 蓝(当前生产)
private final AiServiceClient greenClient; // 绿(新版本)
// 控制哪些用户被路由到绿环境
private final Set<String> greenBetaUsers = ConcurrentHashMap.newKeySet();
public AiResponse route(AiRequest request) {
boolean useGreen = shouldUseGreen(request);
// 记录路由决策(用于后续A/B分析)
metricsCollector.record(
request.getRequestId(),
useGreen ? "green" : "blue",
request.getUserId()
);
AiServiceClient client = useGreen ? greenClient : blueClient;
try {
AiResponse response = client.complete(request);
response.setEnvironment(useGreen ? "green" : "blue");
return response;
} catch (Exception e) {
if (useGreen) {
// 绿环境失败,自动降级到蓝环境
log.warn("绿环境请求失败,降级到蓝环境: requestId={}", request.getRequestId());
metricsCollector.recordGreenFailure(request.getRequestId());
return blueClient.complete(request);
}
throw e;
}
}
private boolean shouldUseGreen(AiRequest request) {
// 规则1:Beta用户始终走绿环境(允许特定用户提前体验)
if (greenBetaUsers.contains(request.getUserId())) {
return true;
}
// 规则2:按比例随机路由(保证会话粘性:同一用户同一会话不切换)
int hash = Math.abs((request.getUserId() + request.getConversationId()).hashCode());
return (hash % 100) < greenTrafficPercent.get();
}
/**
* 动态调整绿环境流量比例(由管理接口或自动化系统调用)
*/
public void setGreenTrafficPercent(int percent) {
int clamped = Math.max(0, Math.min(100, percent));
log.info("调整绿环境流量比例: {}% -> {}%", greenTrafficPercent.get(), clamped);
greenTrafficPercent.set(clamped);
}
/**
* 紧急回滚:把所有流量切回蓝环境
*/
public void rollbackToBlue() {
log.warn("执行紧急回滚,所有流量切回蓝环境");
greenTrafficPercent.set(0);
alertService.sendAlert("AI服务蓝绿回滚", "已将100%流量切回蓝环境");
}
}质量评估:AI特有的发布验证
常规服务的发布验证看错误率、延迟。AI服务还需要评估回答质量:
@Service
public class AiReleaseQualityEvaluator {
/**
* 对比蓝绿两个环境的回答质量
* 使用LLM-as-Judge方式评分
*/
public QualityComparisonReport compareEnvironments(
String samplePeriodHours,
int sampleSize) {
// 采样蓝绿环境的请求对
List<RequestPair> pairs = metricsStore.sampleMatchedPairs(
samplePeriodHours, sampleSize
);
// 并行评估每对请求的质量(用AI判断哪个回答更好)
List<CompletableFuture<PairEvaluationResult>> futures = pairs.stream()
.map(pair -> CompletableFuture.supplyAsync(
() -> evaluatePair(pair), evaluatorExecutor
))
.collect(Collectors.toList());
List<PairEvaluationResult> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
// 统计:绿环境更好/相当/更差的比例
long greenBetter = results.stream().filter(r -> r.getWinner() == Environment.GREEN).count();
long blueBetter = results.stream().filter(r -> r.getWinner() == Environment.BLUE).count();
long tied = results.stream().filter(r -> r.getWinner() == null).count();
return QualityComparisonReport.builder()
.totalSamples(results.size())
.greenBetterPercent((double) greenBetter / results.size())
.blueBetterPercent((double) blueBetter / results.size())
.tiedPercent((double) tied / results.size())
.recommendation(determineRecommendation(greenBetter, blueBetter, results.size()))
.build();
}
private PairEvaluationResult evaluatePair(RequestPair pair) {
// 用Claude作为评判者
String judgePrompt = """
你是一个AI回答质量评判者。对于同一个用户问题,比较两个AI的回答哪个更好。
用户问题:%s
回答A:%s
回答B:%s
请评判哪个回答更好,给出理由,最后输出:
- "A更好" 或 "B更好" 或 "相当"
""".formatted(pair.getQuestion(), pair.getBlueAnswer(), pair.getGreenAnswer());
String judgment = judgeClient.complete(judgePrompt);
Environment winner = null;
if (judgment.contains("A更好")) winner = Environment.BLUE;
else if (judgment.contains("B更好")) winner = Environment.GREEN;
return new PairEvaluationResult(pair, winner, judgment);
}
}自动化发布决策
基于质量指标自动化发布流程:
@Service
public class AutomatedReleaseController {
@Scheduled(fixedDelay = 600000) // 每10分钟检查一次
public void checkAndAdvanceRelease() {
ReleaseState state = releaseStateStore.getCurrentState();
if (state.getStatus() != ReleaseStatus.IN_PROGRESS) return;
int currentGreenPercent = trafficRouter.getGreenPercent();
// 收集过去30分钟的指标
ReleaseMetrics greenMetrics = metricsCollector.getMetrics("green", 30);
ReleaseMetrics blueMetrics = metricsCollector.getMetrics("blue", 30);
// 安全检查
if (isUnhealthy(greenMetrics, blueMetrics)) {
log.error("绿环境指标异常,触发自动回滚");
trafficRouter.rollbackToBlue();
notifyTeam("自动回滚:绿环境错误率超过阈值");
return;
}
// 质量检查(每100%需要足够样本)
if (hasEnoughSamples(greenMetrics)) {
QualityComparisonReport qualityReport = evaluator.compareEnvironments("0.5", 50);
if (qualityReport.getGreenBetterPercent() < 0.35) {
// 绿环境质量明显不如蓝环境
log.warn("绿环境质量不达标,暂停发布");
notifyTeam("需要人工决策:绿环境质量评分低于阈值");
return;
}
}
// 逐步放量:5% -> 10% -> 25% -> 50% -> 100%
int[] milestones = {5, 10, 25, 50, 100};
for (int milestone : milestones) {
if (currentGreenPercent < milestone) {
log.info("发布进展:{}% -> {}%", currentGreenPercent, milestone);
trafficRouter.setGreenTrafficPercent(milestone);
if (milestone == 100) {
completeRelease();
}
break;
}
}
}
private boolean isUnhealthy(ReleaseMetrics green, ReleaseMetrics blue) {
// 绿环境错误率比蓝环境高2倍以上,视为不健康
if (green.getErrorRate() > blue.getErrorRate() * 2 &&
green.getErrorRate() > 0.01) {
return true;
}
// 绿环境P99延迟比蓝环境高50%以上
if (green.getP99LatencyMs() > blue.getP99LatencyMs() * 1.5) {
return true;
}
return false;
}
}提示词版本的回滚
除了服务版本,提示词的变更也需要独立的版本管理和回滚能力:
@Service
public class PromptVersionManager {
private final PromptRepository promptRepository;
private final Map<String, String> activePromptVersions = new ConcurrentHashMap<>();
/**
* 发布新版本提示词(支持即时回滚)
*/
public PromptVersion publishPromptVersion(
String promptKey, String newContent, String author, String changeSummary) {
// 保存新版本
PromptVersion newVersion = PromptVersion.builder()
.promptKey(promptKey)
.content(newContent)
.version(generateVersion())
.author(author)
.changeSummary(changeSummary)
.publishedAt(Instant.now())
.build();
promptRepository.save(newVersion);
// 更新活跃版本
String previousVersion = activePromptVersions.put(promptKey, newVersion.getVersion());
log.info("提示词发布: key={}, version={}, previousVersion={}",
promptKey, newVersion.getVersion(), previousVersion);
return newVersion;
}
/**
* 回滚到上一版本(一键操作)
*/
public void rollbackPrompt(String promptKey) {
String currentVersion = activePromptVersions.get(promptKey);
Optional<PromptVersion> previous = promptRepository.findPreviousVersion(
promptKey, currentVersion
);
previous.ifPresent(prev -> {
activePromptVersions.put(promptKey, prev.getVersion());
log.warn("提示词回滚: key={}, from={}, to={}",
promptKey, currentVersion, prev.getVersion());
alertService.sendAlert("提示词回滚",
String.format("提示词[%s]已回滚到版本%s", promptKey, prev.getVersion()));
});
}
}AI系统的蓝绿部署,本质上是把"一次大的、不可预测的变更"变成"多次小的、可监控的变更"。每次只改一件事(只换模型,或只改提示词,而不是同时改),加上完善的质量监控,可以把发布风险降到可接受的水平。
