第2157篇:模型注册表的工程设计——LLM版本发布的审批与回滚
2026/4/30大约 7 分钟
第2157篇:模型注册表的工程设计——LLM版本发布的审批与回滚
适读人群:负责LLM系统版本管理和发布的工程师 | 阅读时长:约18分钟 | 核心价值:建立完整的LLM版本发布流程,实现有审批、可追踪、能快速回滚的版本管理体系
公司的AI产品上线了一个新版Prompt,当天下午开始收到用户投诉,说AI开始给出奇怪的建议。
紧急排查,发现是Prompt里加了一条新规则,在某些特定场景下会触发意外行为。
问题是:谁改的Prompt?改了什么?测试通过了吗?怎么快速回滚到上一个版本?
四个问题,没有一个能直接回答。因为Prompt是直接在配置文件里改的,没有审批,没有测试门控,没有版本记录。
这就是为什么需要模型注册表。
什么是LLM注册表
LLM注册表(Model Registry)不只是存储Prompt和模型配置,它是一个治理系统:
注册表的核心功能:
1. 版本化存储 → 每次变更都有唯一版本,可以精确回溯
2. 发布审批 → 新版本必须经过测试门控和人工审批才能上生产
3. 环境管理 → 同一个版本在dev/staging/prod有不同的发布状态
4. 变更记录 → 谁在什么时候做了什么变更,完整的审计日志
5. 快速回滚 → 一键回滚到任意历史版本注册表数据模型设计
/**
* LLM配置版本(注册表中的核心实体)
*
* 一个版本包含:模型名称、Prompt模板、参数配置等
*/
@Entity
@Table(name = "llm_config_version")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LlmConfigVersion {
@Id
@GeneratedValue
private Long id;
// 版本的逻辑标识
private String configName; // 如 "customer-service-main"
private Integer majorVersion; // 主版本,重大变更时递增
private Integer minorVersion; // 次版本,小改动时递增
private Integer patchVersion; // 补丁版本,热修复时递增
// 实际配置内容
@Column(columnDefinition = "TEXT")
private String systemPrompt;
@Column(columnDefinition = "TEXT")
private String userPromptTemplate;
private String modelName; // 如 "gpt-4o"
private Double temperature;
private Integer maxTokens;
@Column(columnDefinition = "JSON")
private String additionalParams; // 其他配置,JSON格式
// 版本元数据
private String createdBy;
private Instant createdAt;
private String changeDescription; // 这次改了什么
@Column(columnDefinition = "TEXT")
private String testResults; // 评估结果摘要,JSON格式
// 发布状态
@Enumerated(EnumType.STRING)
private VersionStatus status; // DRAFT, TESTING, APPROVED, DEPRECATED
private String contentHash; // 配置内容的哈希,用于快速检测变更
/**
* 获取版本字符串表示
*/
public String getVersionString() {
return String.format("v%d.%d.%d", majorVersion, minorVersion, patchVersion);
}
}
/**
* 环境发布记录(版本在某个环境的部署状态)
*/
@Entity
@Table(name = "llm_deployment")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LlmDeployment {
@Id
@GeneratedValue
private Long id;
private String configName;
private String environment; // dev, staging, prod
private Long activeVersionId; // 当前激活的版本
private Long previousVersionId; // 上一个版本(用于快速回滚)
private String deployedBy;
private Instant deployedAt;
private String deploymentNote;
@Enumerated(EnumType.STRING)
private DeploymentStatus deploymentStatus; // ACTIVE, ROLLING_BACK, ROLLED_BACK
}
/**
* 审批记录
*/
@Entity
@Table(name = "llm_version_approval")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LlmVersionApproval {
@Id
@GeneratedValue
private Long id;
private Long versionId;
private String environment;
private String requestedBy;
private Instant requestedAt;
private String reviewedBy;
private Instant reviewedAt;
@Enumerated(EnumType.STRING)
private ApprovalStatus status; // PENDING, APPROVED, REJECTED
private String reviewComment;
}版本发布工作流实现
/**
* LLM版本发布服务
*
* 实现从创建到上线的完整发布流程
*/
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class LlmVersionReleaseService {
private final LlmConfigVersionRepository versionRepository;
private final LlmDeploymentRepository deploymentRepository;
private final LlmVersionApprovalRepository approvalRepository;
private final LlmEvaluationService evaluationService;
private final NotificationService notificationService;
/**
* 创建新版本(草稿状态)
*/
public LlmConfigVersion createVersion(CreateVersionRequest request) {
// 确定版本号
Optional<LlmConfigVersion> latest = versionRepository
.findLatestByConfigName(request.getConfigName());
int major = 1, minor = 0, patch = 0;
if (latest.isPresent()) {
LlmConfigVersion prev = latest.get();
switch (request.getVersionType()) {
case MAJOR -> { major = prev.getMajorVersion() + 1; }
case MINOR -> { major = prev.getMajorVersion(); minor = prev.getMinorVersion() + 1; }
case PATCH -> {
major = prev.getMajorVersion();
minor = prev.getMinorVersion();
patch = prev.getPatchVersion() + 1;
}
}
}
// 计算内容哈希
String content = request.getSystemPrompt() + request.getUserPromptTemplate() + request.getModelName();
String hash = DigestUtils.sha256Hex(content);
// 检查是否与已有版本重复(内容完全相同)
if (versionRepository.findByContentHash(hash).isPresent()) {
throw new DuplicateVersionException("配置内容与已有版本完全相同,请确认是否真的需要创建新版本");
}
LlmConfigVersion version = LlmConfigVersion.builder()
.configName(request.getConfigName())
.majorVersion(major)
.minorVersion(minor)
.patchVersion(patch)
.systemPrompt(request.getSystemPrompt())
.userPromptTemplate(request.getUserPromptTemplate())
.modelName(request.getModelName())
.temperature(request.getTemperature())
.maxTokens(request.getMaxTokens())
.createdBy(request.getCreatedBy())
.createdAt(Instant.now())
.changeDescription(request.getChangeDescription())
.status(VersionStatus.DRAFT)
.contentHash(hash)
.build();
LlmConfigVersion saved = versionRepository.save(version);
log.info("新版本创建: {}/{}", request.getConfigName(), saved.getVersionString());
return saved;
}
/**
* 对版本运行评估测试(测试门控)
*
* 评估通过才能进入审批流程
*/
public TestGateResult runTestGate(Long versionId, String datasetName) {
LlmConfigVersion version = versionRepository.findById(versionId)
.orElseThrow(() -> new NotFoundException("版本不存在: " + versionId));
if (version.getStatus() != VersionStatus.DRAFT) {
throw new IllegalStateException("只有DRAFT状态的版本才能运行测试");
}
log.info("开始测试版本 {}/{}", version.getConfigName(), version.getVersionString());
// 更新状态为测试中
version.setStatus(VersionStatus.TESTING);
versionRepository.save(version);
// 执行评估
// 这里需要用该版本的配置运行LLM并评估
Map<String, Double> metrics = runEvaluationWithVersion(version, datasetName);
// 判断是否通过测试门控
boolean passed = isTestGatePassed(metrics);
// 记录测试结果
version.setTestResults(serializeMetrics(metrics));
version.setStatus(passed ? VersionStatus.APPROVED : VersionStatus.DRAFT);
versionRepository.save(version);
log.info("测试完成: {}/{} -> {}",
version.getConfigName(), version.getVersionString(), passed ? "通过" : "未通过");
return TestGateResult.builder()
.versionId(versionId)
.metrics(metrics)
.passed(passed)
.summary(generateTestSummary(metrics, passed))
.build();
}
/**
* 请求部署审批
*/
public LlmVersionApproval requestApproval(Long versionId, String environment,
String requestedBy, String reason) {
LlmConfigVersion version = versionRepository.findById(versionId)
.orElseThrow(() -> new NotFoundException("版本不存在"));
if (version.getStatus() != VersionStatus.APPROVED) {
throw new IllegalStateException("版本尚未通过测试,无法申请部署审批");
}
// Production环境需要人工审批
if ("prod".equals(environment)) {
LlmVersionApproval approval = LlmVersionApproval.builder()
.versionId(versionId)
.environment(environment)
.requestedBy(requestedBy)
.requestedAt(Instant.now())
.status(ApprovalStatus.PENDING)
.build();
LlmVersionApproval saved = approvalRepository.save(approval);
// 通知审批人
notificationService.notifyApprovers(
String.format("请审批:%s %s 上线到%s环境\n变更说明:%s\n测试结果:%s",
version.getConfigName(), version.getVersionString(),
environment, version.getChangeDescription(), version.getTestResults()),
approval.getId()
);
return saved;
} else {
// Dev/Staging可以自动批准
return autoApprove(versionId, environment, requestedBy);
}
}
/**
* 审批通过,执行部署
*/
public LlmDeployment deploy(Long approvalId, String reviewedBy, String comment) {
LlmVersionApproval approval = approvalRepository.findById(approvalId)
.orElseThrow(() -> new NotFoundException("审批记录不存在"));
if (approval.getStatus() != ApprovalStatus.PENDING) {
throw new IllegalStateException("审批已完结");
}
// 更新审批记录
approval.setReviewedBy(reviewedBy);
approval.setReviewedAt(Instant.now());
approval.setStatus(ApprovalStatus.APPROVED);
approval.setReviewComment(comment);
approvalRepository.save(approval);
// 执行部署
return executeDeployment(approval.getVersionId(), approval.getEnvironment(), reviewedBy);
}
/**
* 回滚到上一个版本
*/
public LlmDeployment rollback(String configName, String environment, String operatedBy) {
LlmDeployment current = deploymentRepository
.findActiveByConfigNameAndEnvironment(configName, environment)
.orElseThrow(() -> new NotFoundException("没有找到当前部署记录"));
if (current.getPreviousVersionId() == null) {
throw new IllegalStateException("没有可回滚的历史版本");
}
log.warn("执行回滚: {}/{} by {}", configName, environment, operatedBy);
// 更新部署记录
current.setDeploymentStatus(DeploymentStatus.ROLLED_BACK);
deploymentRepository.save(current);
// 创建新的部署记录(指向上一个版本)
LlmDeployment rollbackDeployment = LlmDeployment.builder()
.configName(configName)
.environment(environment)
.activeVersionId(current.getPreviousVersionId())
.previousVersionId(current.getActiveVersionId())
.deployedBy(operatedBy)
.deployedAt(Instant.now())
.deploymentNote("回滚操作:从版本" + current.getActiveVersionId() + "回滚到" + current.getPreviousVersionId())
.deploymentStatus(DeploymentStatus.ACTIVE)
.build();
LlmDeployment saved = deploymentRepository.save(rollbackDeployment);
// 清除配置缓存,使新配置立即生效
clearConfigCache(configName, environment);
// 记录告警
notificationService.notifyRollback(configName, environment, operatedBy);
return saved;
}
private boolean isTestGatePassed(Map<String, Double> metrics) {
// 测试门控标准:
// 1. 综合分不低于0.70
// 2. 通过率不低于0.65
// 3. 安全性分数不低于0.90(硬性要求)
return metrics.getOrDefault("overall_score", 0.0) >= 0.70
&& metrics.getOrDefault("pass_rate", 0.0) >= 0.65
&& metrics.getOrDefault("safety_score", 1.0) >= 0.90;
}
private LlmDeployment executeDeployment(Long versionId, String environment, String deployedBy) {
// 找到当前的部署(如果有)
Optional<LlmDeployment> currentDeployment = deploymentRepository
.findActiveByConfigNameAndEnvironment(
versionRepository.findById(versionId).get().getConfigName(), environment);
Long previousVersionId = currentDeployment.map(LlmDeployment::getActiveVersionId).orElse(null);
// 将旧的部署标记为非活跃
currentDeployment.ifPresent(d -> {
d.setDeploymentStatus(DeploymentStatus.ROLLED_BACK);
deploymentRepository.save(d);
});
LlmConfigVersion version = versionRepository.findById(versionId).get();
LlmDeployment deployment = LlmDeployment.builder()
.configName(version.getConfigName())
.environment(environment)
.activeVersionId(versionId)
.previousVersionId(previousVersionId)
.deployedBy(deployedBy)
.deployedAt(Instant.now())
.deploymentStatus(DeploymentStatus.ACTIVE)
.build();
LlmDeployment saved = deploymentRepository.save(deployment);
// 清除缓存,使新配置生效
clearConfigCache(version.getConfigName(), environment);
log.info("部署成功: {}/{} -> {}", version.getConfigName(), environment, version.getVersionString());
return saved;
}
private void clearConfigCache(String configName, String environment) {
// 实际实现:清除应用的内存缓存,或发送缓存失效事件
log.info("清除配置缓存: {}/{}", configName, environment);
}
private Map<String, Double> runEvaluationWithVersion(LlmConfigVersion version, String datasetName) {
// 实际评估实现
return Map.of("overall_score", 0.82, "pass_rate", 0.78, "safety_score", 0.95);
}
private String serializeMetrics(Map<String, Double> metrics) {
try { return new ObjectMapper().writeValueAsString(metrics); }
catch (Exception e) { return "{}"; }
}
private String generateTestSummary(Map<String, Double> metrics, boolean passed) {
return String.format("测试%s:综合分=%.2f, 通过率=%.1f%%",
passed ? "通过" : "未通过",
metrics.getOrDefault("overall_score", 0.0),
metrics.getOrDefault("pass_rate", 0.0) * 100);
}
private LlmVersionApproval autoApprove(Long versionId, String environment, String requestedBy) {
return executeDeployment(versionId, environment, requestedBy) != null
? LlmVersionApproval.builder().status(ApprovalStatus.APPROVED).build()
: null;
}
}配置获取(运行时)
/**
* 运行时配置获取服务
*
* 应用代码通过这个服务获取当前生效的LLM配置
* 内置缓存,避免每次调用都查数据库
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ActiveConfigService {
private final LlmDeploymentRepository deploymentRepository;
private final LlmConfigVersionRepository versionRepository;
@Value("${spring.profiles.active:dev}")
private String currentEnvironment;
// 本地缓存,5分钟过期
private final Cache<String, LlmConfigVersion> configCache =
Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();
/**
* 获取当前生效的配置
*/
public LlmConfigVersion getActiveConfig(String configName) {
return configCache.get(configName, name -> loadConfig(name));
}
private LlmConfigVersion loadConfig(String configName) {
LlmDeployment deployment = deploymentRepository
.findActiveByConfigNameAndEnvironment(configName, currentEnvironment)
.orElseThrow(() -> new NotFoundException(
"没有找到配置: " + configName + " @ " + currentEnvironment));
return versionRepository.findById(deployment.getActiveVersionId())
.orElseThrow(() -> new NotFoundException("版本不存在: " + deployment.getActiveVersionId()));
}
/**
* 手动使缓存失效(回滚时调用)
*/
public void invalidateCache(String configName) {
configCache.invalidate(configName);
log.info("配置缓存已失效: {}", configName);
}
}