提示词版本管理系统:构建企业级提示词工程基础设施
提示词版本管理系统:构建企业级提示词工程基础设施
一个让人崩溃的周一早晨
王芳是一家电商公司的 Java 工程师,工作了 4 年,去年转型做 AI 应用开发。
她们团队做了一个智能商品推荐系统,核心是一段精心调优的提示词。这个提示词花了整整两周时间,通过上百次手动测试才达到了让产品满意的推荐质量——转化率从 2.1% 提升到了 4.7%,直接效果。
那段提示词保存在哪里?代码里的一个字符串常量。
某个周五下午,她的同事小张觉得"优化"一下提示词,改了几个词,"让表达更流畅"。他直接修改了代码并提交,测试环境没问题,周末发布上线了。
周一早上,数据分析师发现推荐转化率从 4.7% 跌回了 2.3%。
排查了半天,才发现是提示词改了。但改了什么?原来那个 4.7% 的提示词长什么样?没有记录,无从查起。
Git 历史里倒是有一条提交记录,但哪个版本是最好的,没有数据支撑,没有效果指标,只能靠印象猜。
王芳花了一周时间,重新做实验,重新调优,才把转化率恢复到 4.5%——还差了 0.2 个百分点,不知道怎么也恢复不回来。
"如果提示词能像代码一样管理就好了。"
这篇文章,就是解决这个问题的。
为什么提示词需要像代码一样管理
很多团队的提示词管理现状:
// 写在代码里
private static final String RECOMMEND_PROMPT = "你是一个商品推荐助手...";
// 或者写在配置文件里
ai.prompt.recommend=你是一个商品推荐助手...
// 或者写在数据库里某个没人知道的字段这些方式有一个共同的问题:提示词的修改历史、效果数据和上线决策是分离的,无法关联分析。
企业级提示词管理需要解决以下问题:
| 问题 | 传统方案的痛点 | 版本管理系统的解法 |
|---|---|---|
| 版本追溯 | 只能看 Git 历史,没有效果数据 | 每个版本关联指标,可对比 |
| 快速回滚 | 需要修改代码重新发布 | 一键切换到历史版本 |
| 环境隔离 | dev/prod 用同一个字符串 | 各环境独立版本 |
| A/B 测试 | 需要额外开发功能 | 内置 A/B 测试支持 |
| 效果分析 | 提示词变更与效果无法关联 | 自动记录每版本指标 |
| 团队协作 | 谁改了不知道,为什么改不知道 | 审批记录、变更说明 |
数据库设计
建表 SQL
-- 提示词模板表
CREATE TABLE prompt_template (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE COMMENT '提示词唯一名称,如 product-recommend-v2',
description TEXT COMMENT '用途描述',
category VARCHAR(50) NOT NULL COMMENT '分类',
tags JSON COMMENT '标签,如 ["推荐","电商"]',
created_by BIGINT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_category (category),
INDEX idx_created_by (created_by)
) COMMENT '提示词模板表';
-- 提示词版本表
CREATE TABLE prompt_version (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
template_id BIGINT NOT NULL COMMENT '关联模板ID',
version VARCHAR(20) NOT NULL COMMENT '语义版本号,如 1.2.3',
content MEDIUMTEXT NOT NULL COMMENT '提示词内容,支持 {{variable}} 占位符',
variables JSON COMMENT '变量定义,如 [{"name":"product","type":"string","required":true}]',
status ENUM('draft','pending_approval','active','deprecated') NOT NULL DEFAULT 'draft',
environment ENUM('dev','test','prod') NOT NULL DEFAULT 'dev',
change_note TEXT COMMENT '本次变更说明',
created_by BIGINT NOT NULL,
approved_by BIGINT,
approved_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_template_version_env (template_id, version, environment),
INDEX idx_template_status (template_id, status),
FOREIGN KEY (template_id) REFERENCES prompt_template(id)
) COMMENT '提示词版本表';
-- 效果指标表
CREATE TABLE prompt_metrics (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
version_id BIGINT NOT NULL COMMENT '版本ID',
metric_name VARCHAR(50) NOT NULL COMMENT '指标名,如 conversion_rate, satisfaction_score',
metric_value DOUBLE NOT NULL COMMENT '指标值',
sample_count BIGINT NOT NULL DEFAULT 0 COMMENT '样本数量',
recorded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_version_metric (version_id, metric_name),
FOREIGN KEY (version_id) REFERENCES prompt_version(id)
) COMMENT '提示词效果指标表';
-- A/B 测试表
CREATE TABLE prompt_ab_test (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
test_name VARCHAR(100) NOT NULL,
version_a_id BIGINT NOT NULL COMMENT '对照组(现有版本)',
version_b_id BIGINT NOT NULL COMMENT '实验组(新版本)',
traffic_split_percent INT NOT NULL DEFAULT 10 COMMENT 'B组流量百分比(0-100)',
status ENUM('draft','running','completed','stopped') NOT NULL DEFAULT 'draft',
start_at DATETIME,
end_at DATETIME,
created_by BIGINT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (version_a_id) REFERENCES prompt_version(id),
FOREIGN KEY (version_b_id) REFERENCES prompt_version(id)
) COMMENT 'A/B测试表';
-- 审批记录表
CREATE TABLE prompt_approval (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
version_id BIGINT NOT NULL,
status ENUM('pending','approved','rejected') NOT NULL DEFAULT 'pending',
reviewer_id BIGINT,
comment TEXT COMMENT '审批意见',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (version_id) REFERENCES prompt_version(id)
) COMMENT '审批记录表';核心实体类
// src/main/java/com/company/prompt/entity/PromptVersion.java
package com.company.prompt.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "prompt_version")
@Data
public class PromptVersion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "template_id", nullable = false)
private Long templateId;
@Column(nullable = false, length = 20)
private String version;
@Column(nullable = false, columnDefinition = "MEDIUMTEXT")
private String content;
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "json")
private List<VariableDefinition> variables;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PromptStatus status = PromptStatus.DRAFT;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Environment environment = Environment.DEV;
@Column(name = "change_note")
private String changeNote;
@Column(name = "created_by", nullable = false)
private Long createdBy;
@Column(name = "approved_by")
private Long approvedBy;
@Column(name = "approved_at")
private LocalDateTime approvedAt;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
public enum PromptStatus {
DRAFT, PENDING_APPROVAL, ACTIVE, DEPRECATED
}
public enum Environment {
DEV, TEST, PROD
}
@Data
public static class VariableDefinition {
private String name; // 变量名
private String type; // 类型: string/number/list
private boolean required; // 是否必填
private String description;// 说明
private String defaultValue; // 默认值
}
}版本控制 API 实现
服务层
// src/main/java/com/company/prompt/service/PromptVersionService.java
package com.company.prompt.service;
import com.company.prompt.dto.*;
import com.company.prompt.entity.PromptVersion;
import com.company.prompt.entity.PromptVersion.Environment;
import com.company.prompt.entity.PromptVersion.PromptStatus;
import com.company.prompt.repository.PromptVersionRepository;
import com.company.prompt.exception.PromptVersionException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
@Service
@RequiredArgsConstructor
@Slf4j
public class PromptVersionService {
private final PromptVersionRepository versionRepository;
private final PromptApprovalService approvalService;
private final PromptAuditService auditService;
private static final Pattern SEMANTIC_VERSION_PATTERN =
Pattern.compile("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$");
/**
* 创建新版本(初始状态为 DRAFT)
*/
@Transactional
public PromptVersion createVersion(CreateVersionRequest request, Long operatorId) {
// 校验语义版本号
if (!SEMANTIC_VERSION_PATTERN.matcher(request.getVersion()).matches()) {
throw new PromptVersionException("版本号必须符合语义版本规范,如 1.2.3");
}
// 检查版本号是否已存在
boolean exists = versionRepository.existsByTemplateIdAndVersionAndEnvironment(
request.getTemplateId(), request.getVersion(), request.getEnvironment()
);
if (exists) {
throw new PromptVersionException(
String.format("版本 %s 在 %s 环境已存在", request.getVersion(), request.getEnvironment())
);
}
// 校验变量占位符与变量定义一致
validateVariables(request.getContent(), request.getVariables());
PromptVersion version = new PromptVersion();
version.setTemplateId(request.getTemplateId());
version.setVersion(request.getVersion());
version.setContent(request.getContent());
version.setVariables(request.getVariables());
version.setStatus(PromptStatus.DRAFT);
version.setEnvironment(request.getEnvironment());
version.setChangeNote(request.getChangeNote());
version.setCreatedBy(operatorId);
PromptVersion saved = versionRepository.save(version);
auditService.record("CREATE_VERSION", saved.getId(), operatorId);
log.info("创建提示词版本: templateId={}, version={}, env={}, operator={}",
request.getTemplateId(), request.getVersion(), request.getEnvironment(), operatorId);
return saved;
}
/**
* 获取当前激活版本(带缓存)
*/
@Cacheable(value = "prompt-active", key = "#templateId + ':' + #environment")
public Optional<PromptVersion> getActiveVersion(Long templateId, Environment environment) {
return versionRepository.findByTemplateIdAndStatusAndEnvironment(
templateId, PromptStatus.ACTIVE, environment
);
}
/**
* 激活版本(需要审批或直接激活,取决于环境)
*/
@Transactional
@CacheEvict(value = "prompt-active", key = "#templateId + ':' + #environment")
public PromptVersion activateVersion(Long versionId, Long operatorId, Environment environment) {
PromptVersion version = versionRepository.findById(versionId)
.orElseThrow(() -> new PromptVersionException("版本不存在: " + versionId));
// 生产环境必须经过审批
if (environment == Environment.PROD && !approvalService.isApproved(versionId)) {
throw new PromptVersionException("生产环境版本必须先通过审批");
}
// 将当前激活版本标记为 DEPRECATED
versionRepository.findByTemplateIdAndStatusAndEnvironment(
version.getTemplateId(), PromptStatus.ACTIVE, environment
).ifPresent(old -> {
old.setStatus(PromptStatus.DEPRECATED);
versionRepository.save(old);
log.info("版本 {} 已废弃", old.getVersion());
});
// 激活新版本
version.setStatus(PromptStatus.ACTIVE);
PromptVersion saved = versionRepository.save(version);
auditService.record("ACTIVATE_VERSION", versionId, operatorId);
log.info("激活提示词版本: versionId={}, templateId={}, version={}, env={}",
versionId, version.getTemplateId(), version.getVersion(), environment);
return saved;
}
/**
* 回滚到历史版本
*/
@Transactional
public PromptVersion rollback(Long templateId, String targetVersion,
Environment environment, Long operatorId) {
PromptVersion target = versionRepository
.findByTemplateIdAndVersionAndEnvironment(templateId, targetVersion, environment)
.orElseThrow(() -> new PromptVersionException(
String.format("目标版本 %s 不存在", targetVersion)
));
log.warn("执行提示词回滚: templateId={}, targetVersion={}, env={}, operator={}",
templateId, targetVersion, environment, operatorId);
// 生产环境回滚记录特殊审计
auditService.record("ROLLBACK_VERSION", target.getId(), operatorId,
"回滚到版本: " + targetVersion);
return activateVersion(target.getId(), operatorId, environment);
}
/**
* 渲染提示词(替换变量占位符)
*/
public String render(Long templateId, Environment environment,
java.util.Map<String, Object> variables) {
PromptVersion version = getActiveVersion(templateId, environment)
.orElseThrow(() -> new PromptVersionException(
"未找到激活版本: templateId=" + templateId
));
return renderContent(version.getContent(), variables);
}
/**
* 变量替换:支持 {{variableName}} 格式
*/
private String renderContent(String content, java.util.Map<String, Object> variables) {
if (variables == null || variables.isEmpty()) {
return content;
}
String result = content;
for (java.util.Map.Entry<String, Object> entry : variables.entrySet()) {
String placeholder = "{{" + entry.getKey() + "}}";
result = result.replace(placeholder, String.valueOf(entry.getValue()));
}
// 检查是否还有未替换的必填变量
if (result.contains("{{")) {
log.warn("提示词中存在未替换的变量占位符: {}", result);
}
return result;
}
/**
* 校验提示词内容中的变量与变量定义一致
*/
private void validateVariables(String content,
List<PromptVersion.VariableDefinition> definitions) {
if (definitions == null) return;
Pattern placeholderPattern = Pattern.compile("\\{\\{(\\w+)\\}\\}");
java.util.Set<String> usedVariables = new java.util.HashSet<>();
java.util.regex.Matcher matcher = placeholderPattern.matcher(content);
while (matcher.find()) {
usedVariables.add(matcher.group(1));
}
java.util.Set<String> definedVariables = definitions.stream()
.map(PromptVersion.VariableDefinition::getName)
.collect(java.util.stream.Collectors.toSet());
// 找出使用了但未定义的变量
usedVariables.stream()
.filter(v -> !definedVariables.contains(v))
.findFirst()
.ifPresent(v -> {
throw new PromptVersionException("使用了未定义的变量: " + v);
});
}
}环境隔离:dev/test/prod 的版本管理
// src/main/java/com/company/prompt/service/PromptRenderService.java
package com.company.prompt.service;
import com.company.prompt.entity.PromptVersion.Environment;
import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Profiles;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class PromptRenderService {
private final PromptVersionService versionService;
@org.springframework.core.env.Environment
private org.springframework.core.env.Environment springEnvironment;
/**
* 自动根据 Spring Profile 选择对应的提示词环境
*/
public String renderForCurrentEnv(Long templateId, Map<String, Object> variables) {
Environment env = detectEnvironment();
return versionService.render(templateId, env, variables);
}
/**
* 指定环境渲染(用于测试目的)
*/
public String renderForEnv(Long templateId, Environment env, Map<String, Object> variables) {
return versionService.render(templateId, env, variables);
}
private Environment detectEnvironment() {
if (springEnvironment.acceptsProfiles(Profiles.of("prod"))) {
return Environment.PROD;
} else if (springEnvironment.acceptsProfiles(Profiles.of("test"))) {
return Environment.TEST;
} else {
return Environment.DEV;
}
}
}A/B 测试:同一功能测试多个提示词版本
// src/main/java/com/company/prompt/service/PromptAbTestService.java
package com.company.prompt.service;
import com.company.prompt.entity.PromptAbTest;
import com.company.prompt.entity.PromptVersion;
import com.company.prompt.repository.PromptAbTestRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
@Service
@RequiredArgsConstructor
@Slf4j
public class PromptAbTestService {
private final PromptAbTestRepository abTestRepository;
private final PromptVersionService versionService;
private final PromptMetricsService metricsService;
/**
* A/B 测试路由:根据流量配置决定使用哪个版本
*
* @param templateId 模板ID
* @param userId 用户ID(用于一致性路由:同一用户始终看到同一版本)
* @param variables 变量
* @return 渲染后的提示词 + 使用的版本信息
*/
public AbTestResult routeAndRender(Long templateId, String userId, Map<String, Object> variables) {
Optional<PromptAbTest> runningTest = abTestRepository
.findRunningTestByTemplateId(templateId);
if (runningTest.isEmpty()) {
// 没有运行中的 A/B 测试,使用默认激活版本
String content = versionService.render(templateId, PromptVersion.Environment.PROD, variables);
return new AbTestResult(content, "A", null);
}
PromptAbTest test = runningTest.get();
// 一致性路由:同一用户始终在同一组
// 使用用户ID的哈希值,确保分配稳定
boolean isGroupB = isInGroupB(userId, test.getTrafficSplitPercent());
Long versionId = isGroupB ? test.getVersionBId() : test.getVersionAId();
String group = isGroupB ? "B" : "A";
PromptVersion version = versionService.findById(versionId);
String renderedContent = versionService.renderContent(version.getContent(), variables);
log.debug("A/B测试路由: userId={}, testId={}, group={}, versionId={}",
userId, test.getId(), group, versionId);
return new AbTestResult(renderedContent, group, test.getId());
}
/**
* 记录 A/B 测试的效果反馈
*/
public void recordFeedback(Long testId, String group, String metricName, double value) {
metricsService.record(testId, group, metricName, value);
}
/**
* 基于哈希的一致性路由算法
* 相同 userId + testId 的组合,每次路由结果相同
*/
private boolean isInGroupB(String userId, int splitPercent) {
// 使用稳定哈希,而非随机数
int hash = Math.abs(userId.hashCode() % 100);
return hash < splitPercent;
}
public record AbTestResult(String promptContent, String group, Long testId) {}
}A/B 测试效果追踪
// src/main/java/com/company/prompt/service/PromptMetricsService.java
package com.company.prompt.service;
import com.company.prompt.entity.PromptMetrics;
import com.company.prompt.repository.PromptMetricsRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class PromptMetricsService {
private final PromptMetricsRepository metricsRepository;
/**
* 异步记录指标(不阻塞主流程)
*/
@Async
public void recordAsync(Long versionId, String metricName, double value) {
try {
PromptMetrics metrics = new PromptMetrics();
metrics.setVersionId(versionId);
metrics.setMetricName(metricName);
metrics.setMetricValue(value);
metricsRepository.save(metrics);
} catch (Exception e) {
log.error("记录指标失败: versionId={}, metric={}", versionId, metricName, e);
}
}
/**
* 获取版本指标汇总
*/
public Map<String, MetricsSummary> getVersionMetrics(Long versionId) {
List<PromptMetrics> metrics = metricsRepository.findByVersionId(versionId);
return metrics.stream()
.collect(Collectors.groupingBy(
PromptMetrics::getMetricName,
Collectors.collectingAndThen(
Collectors.toList(),
list -> new MetricsSummary(
list.stream().mapToDouble(PromptMetrics::getMetricValue).average().orElse(0),
list.stream().mapToDouble(PromptMetrics::getMetricValue).max().orElse(0),
list.stream().mapToDouble(PromptMetrics::getMetricValue).min().orElse(0),
(long) list.size()
)
)
));
}
/**
* 比较 A/B 两个版本的指标
*/
public AbTestMetricsComparison compareVersions(Long versionAId, Long versionBId) {
Map<String, MetricsSummary> metricsA = getVersionMetrics(versionAId);
Map<String, MetricsSummary> metricsB = getVersionMetrics(versionBId);
return new AbTestMetricsComparison(metricsA, metricsB);
}
public record MetricsSummary(double avg, double max, double min, long count) {}
public record AbTestMetricsComparison(
Map<String, MetricsSummary> groupA,
Map<String, MetricsSummary> groupB
) {}
}审批流程:生产提示词变更的审批机制
// src/main/java/com/company/prompt/service/PromptApprovalService.java
package com.company.prompt.service;
import com.company.prompt.entity.PromptApproval;
import com.company.prompt.entity.PromptVersion;
import com.company.prompt.entity.PromptVersion.PromptStatus;
import com.company.prompt.repository.PromptApprovalRepository;
import com.company.prompt.repository.PromptVersionRepository;
import com.company.prompt.notification.NotificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class PromptApprovalService {
private final PromptApprovalRepository approvalRepository;
private final PromptVersionRepository versionRepository;
private final NotificationService notificationService;
/**
* 提交审批申请(DRAFT -> PENDING_APPROVAL)
*/
@Transactional
public PromptApproval submitForApproval(Long versionId, Long submitterId) {
PromptVersion version = versionRepository.findById(versionId)
.orElseThrow(() -> new RuntimeException("版本不存在"));
if (version.getStatus() != PromptStatus.DRAFT) {
throw new RuntimeException("只有草稿状态的版本可以提交审批");
}
// 更新版本状态
version.setStatus(PromptStatus.PENDING_APPROVAL);
versionRepository.save(version);
// 创建审批记录
PromptApproval approval = new PromptApproval();
approval.setVersionId(versionId);
approval.setStatus(PromptApproval.ApprovalStatus.PENDING);
PromptApproval saved = approvalRepository.save(approval);
// 通知审批人
notificationService.notifyApprovers(
"提示词版本待审批",
String.format("版本 %s 已提交审批,请及时处理。\n变更说明:%s",
version.getVersion(), version.getChangeNote()),
getApproverIds()
);
log.info("提交审批: versionId={}, submitterId={}", versionId, submitterId);
return saved;
}
/**
* 审批通过
*/
@Transactional
public void approve(Long approvalId, Long reviewerId, String comment) {
PromptApproval approval = approvalRepository.findById(approvalId)
.orElseThrow(() -> new RuntimeException("审批记录不存在"));
approval.setStatus(PromptApproval.ApprovalStatus.APPROVED);
approval.setReviewerId(reviewerId);
approval.setComment(comment);
approval.setUpdatedAt(LocalDateTime.now());
approvalRepository.save(approval);
// 更新版本为可激活状态(但不自动激活,由运维手动操作)
PromptVersion version = versionRepository.findById(approval.getVersionId()).get();
version.setApprovedBy(reviewerId);
version.setApprovedAt(LocalDateTime.now());
versionRepository.save(version);
// 通知申请人
notificationService.notifyVersionOwner(version.getCreatedBy(),
"提示词版本审批通过,可以激活上线了!");
log.info("审批通过: approvalId={}, versionId={}, reviewer={}",
approvalId, approval.getVersionId(), reviewerId);
}
/**
* 审批拒绝(版本回到 DRAFT 状态)
*/
@Transactional
public void reject(Long approvalId, Long reviewerId, String reason) {
PromptApproval approval = approvalRepository.findById(approvalId)
.orElseThrow(() -> new RuntimeException("审批记录不存在"));
approval.setStatus(PromptApproval.ApprovalStatus.REJECTED);
approval.setReviewerId(reviewerId);
approval.setComment(reason);
approvalRepository.save(approval);
// 版本回到草稿状态
PromptVersion version = versionRepository.findById(approval.getVersionId()).get();
version.setStatus(PromptStatus.DRAFT);
versionRepository.save(version);
notificationService.notifyVersionOwner(version.getCreatedBy(),
"提示词版本审批被拒绝,原因:" + reason);
log.warn("审批拒绝: approvalId={}, reason={}", approvalId, reason);
}
public boolean isApproved(Long versionId) {
return approvalRepository.existsByVersionIdAndStatus(
versionId, PromptApproval.ApprovalStatus.APPROVED
);
}
private List<Long> getApproverIds() {
// 从配置或数据库获取审批人列表
return List.of(1001L, 1002L); // 示例
}
}提示词模板:变量占位符的标准化设计
// src/main/java/com/company/prompt/template/PromptTemplateEngine.java
package com.company.prompt.template;
import com.company.prompt.entity.PromptVersion.VariableDefinition;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 提示词模板引擎
* 支持以下语法:
* {{variable}} - 简单变量替换
* {{variable:default}} - 带默认值的变量
* {{#list}}...{{/list}} - 列表循环(未来扩展)
*/
@Component
public class PromptTemplateEngine {
// 匹配 {{variableName}} 或 {{variableName:defaultValue}}
private static final Pattern VARIABLE_PATTERN =
Pattern.compile("\\{\\{(\\w+)(?::([^}]*))?\\}\\}");
/**
* 渲染提示词,支持默认值
*/
public String render(String template, Map<String, Object> variables) {
Matcher matcher = VARIABLE_PATTERN.matcher(template);
StringBuilder result = new StringBuilder();
while (matcher.find()) {
String varName = matcher.group(1);
String defaultValue = matcher.group(2); // 可能为 null
Object value = variables.get(varName);
String replacement;
if (value != null) {
replacement = escapeForRegex(String.valueOf(value));
} else if (defaultValue != null) {
replacement = escapeForRegex(defaultValue);
} else {
// 没有值也没有默认值,保留原占位符(后续可检测未填充变量)
replacement = Matcher.quoteReplacement(matcher.group(0));
}
matcher.appendReplacement(result, replacement);
}
matcher.appendTail(result);
return result.toString();
}
/**
* 验证变量完整性:检查所有必填变量是否都已提供
*/
public void validateRequired(List<VariableDefinition> definitions,
Map<String, Object> variables) {
for (VariableDefinition def : definitions) {
if (def.isRequired() && !variables.containsKey(def.getName())) {
throw new IllegalArgumentException("缺少必填变量: " + def.getName());
}
}
}
/**
* 从模板内容提取所有变量名
*/
public List<String> extractVariables(String template) {
Matcher matcher = VARIABLE_PATTERN.matcher(template);
List<String> variables = new java.util.ArrayList<>();
while (matcher.find()) {
variables.add(matcher.group(1));
}
return variables;
}
private String escapeForRegex(String value) {
return Matcher.quoteReplacement(value);
}
}示例提示词模板
你是{{company_name}}的智能客服助手。
用户信息:
- 用户名:{{user_name:匿名用户}}
- 会员等级:{{member_level:普通会员}}
- 历史订单数:{{order_count:default=0}}
用户问题:
{{user_question}}
背景信息:
{{context:暂无相关背景信息}}
请根据以上信息,用友好、专业的语气回答用户的问题。
如果问题超出你的能力范围,请诚实告知并引导用户联系人工客服。提示词搜索:按功能/标签/效果检索历史版本
// src/main/java/com/company/prompt/controller/PromptSearchController.java
package com.company.prompt.controller;
import com.company.prompt.dto.PromptSearchRequest;
import com.company.prompt.dto.PromptSearchResult;
import com.company.prompt.service.PromptSearchService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/prompts")
@RequiredArgsConstructor
public class PromptSearchController {
private final PromptSearchService searchService;
/**
* 多维度搜索提示词版本
* GET /api/v1/prompts/search?keyword=推荐&category=电商&minScore=0.8&status=active
*/
@GetMapping("/search")
public Page<PromptSearchResult> search(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(required = false) Double minScore,
@RequestParam(required = false) String status,
@RequestParam(required = false) String tag,
Pageable pageable) {
PromptSearchRequest request = PromptSearchRequest.builder()
.keyword(keyword)
.category(category)
.minScore(minScore)
.status(status)
.tag(tag)
.build();
return searchService.search(request, pageable);
}
/**
* 按效果指标排名:找出历史上效果最好的版本
* GET /api/v1/prompts/{templateId}/best?metric=conversion_rate&limit=5
*/
@GetMapping("/{templateId}/best")
public java.util.List<PromptSearchResult> getBestVersions(
@PathVariable Long templateId,
@RequestParam(defaultValue = "satisfaction_score") String metric,
@RequestParam(defaultValue = "5") int limit) {
return searchService.findBestVersions(templateId, metric, limit);
}
/**
* 版本对比:查看两个版本的差异和指标对比
* GET /api/v1/prompts/compare?versionAId=1&versionBId=2
*/
@GetMapping("/compare")
public PromptVersionComparison compareVersions(
@RequestParam Long versionAId,
@RequestParam Long versionBId) {
return searchService.compare(versionAId, versionBId);
}
}REST API 汇总
// src/main/java/com/company/prompt/controller/PromptManagementController.java
package com.company.prompt.controller;
import com.company.prompt.dto.*;
import com.company.prompt.entity.PromptVersion;
import com.company.prompt.entity.PromptVersion.Environment;
import com.company.prompt.service.*;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/prompts")
@RequiredArgsConstructor
public class PromptManagementController {
private final PromptVersionService versionService;
private final PromptApprovalService approvalService;
private final PromptAbTestService abTestService;
private final PromptMetricsService metricsService;
private final PromptRenderService renderService;
// ===== 版本管理 =====
@PostMapping("/versions")
@PreAuthorize("hasRole('PROMPT_EDITOR')")
public ResponseEntity<PromptVersion> createVersion(
@RequestBody CreateVersionRequest request,
@AuthenticationPrincipal Long userId) {
return ResponseEntity.ok(versionService.createVersion(request, userId));
}
@PostMapping("/versions/{id}/activate")
@PreAuthorize("hasRole('PROMPT_ADMIN')")
public ResponseEntity<PromptVersion> activateVersion(
@PathVariable Long id,
@RequestParam Environment environment,
@AuthenticationPrincipal Long userId) {
return ResponseEntity.ok(versionService.activateVersion(id, userId, environment));
}
@PostMapping("/templates/{templateId}/rollback")
@PreAuthorize("hasRole('PROMPT_ADMIN')")
public ResponseEntity<PromptVersion> rollback(
@PathVariable Long templateId,
@RequestParam String targetVersion,
@RequestParam Environment environment,
@AuthenticationPrincipal Long userId) {
return ResponseEntity.ok(
versionService.rollback(templateId, targetVersion, environment, userId)
);
}
// ===== 审批 =====
@PostMapping("/versions/{id}/submit-approval")
public ResponseEntity<Void> submitApproval(
@PathVariable Long id,
@AuthenticationPrincipal Long userId) {
approvalService.submitForApproval(id, userId);
return ResponseEntity.ok().build();
}
@PostMapping("/approvals/{id}/approve")
@PreAuthorize("hasRole('PROMPT_APPROVER')")
public ResponseEntity<Void> approve(
@PathVariable Long id,
@RequestBody ApproveRequest request,
@AuthenticationPrincipal Long userId) {
approvalService.approve(id, userId, request.comment());
return ResponseEntity.ok().build();
}
// ===== 渲染 =====
@PostMapping("/templates/{templateId}/render")
public ResponseEntity<Map<String, String>> render(
@PathVariable Long templateId,
@RequestParam(defaultValue = "PROD") Environment environment,
@RequestBody Map<String, Object> variables) {
String content = renderService.renderForEnv(templateId, environment, variables);
return ResponseEntity.ok(Map.of("content", content));
}
// ===== 指标 =====
@PostMapping("/versions/{id}/metrics")
public ResponseEntity<Void> recordMetrics(
@PathVariable Long id,
@RequestBody RecordMetricsRequest request) {
metricsService.recordAsync(id, request.metricName(), request.value());
return ResponseEntity.ok().build();
}
@GetMapping("/versions/{id}/metrics")
public ResponseEntity<?> getMetrics(@PathVariable Long id) {
return ResponseEntity.ok(metricsService.getVersionMetrics(id));
}
}FAQ
Q1:提示词内容要加密存储吗?
对于包含业务逻辑的核心提示词,建议加密存储。可以使用 Jasypt 对数据库字段加密,只在读取时解密。同时,数据库访问要按角色控制,不是所有开发人员都需要读生产提示词。
Q2:提示词版本号用数字还是语义版本?
推荐语义版本(1.2.3 格式):主版本号(兼容性变更)、次版本号(功能增强)、补丁版本(效果微调)。这样光看版本号就能判断变更幅度。
Q3:版本太多了怎么清理?
保留策略:每个模板保留最近 20 个版本 + 所有 active 版本 + 所有有效果指标的版本。定期清理 DEPRECATED 状态且没有指标数据的旧版本。
Q4:提示词的 A/B 测试样本量要多大才有统计意义?
一般要求:转化率类指标(低频),每组至少 1000 个样本;满意度类指标(高频),每组至少 200 个样本。可以使用 Apache Commons Math 库计算样本量和显著性。
Q5:多语言场景下,不同语言版本的提示词算同一个模板的不同版本吗?
建议分开管理:product-recommend-zh(中文提示词)和 product-recommend-en(英文提示词)作为两个独立模板,各自有独立的版本历史和效果数据。这样更清晰,也方便各语言独立优化。
总结
提示词版本管理系统是 AI 应用工程化的重要基础设施。核心价值:
- 可追溯:每次变更都有记录,每个版本都有效果数据
- 可回滚:一键切换到历史任意版本,不需要重新发布代码
- 可实验:内置 A/B 测试,用数据说话而不是靠经验判断
- 可管控:生产变更必须经过审批,有流程保障
王芳的团队引入这套系统后,提示词优化的周期从"改了不知道好不好"变成了"数据驱动的快速迭代"。那个消失的 0.2% 转化率?三个月后找回来了,还超过了历史最高值。
提示词版本管理系统成熟度模型
参考 Google SRE 的成熟度思路,提示词工程也有自己的成熟度阶梯:
| 级别 | 特征 | 典型问题 |
|---|---|---|
| L0:混沌 | 提示词写在代码里,没有版本概念 | 改了不知道好不好,出了问题无法回滚 |
| L1:基础管理 | 提示词存入数据库,有基本 CRUD | 没有效果追踪,没有审批流程 |
| L2:版本控制 | 语义版本号、变更记录、可回滚 | 没有实验能力,无法数据驱动优化 |
| L3:数据驱动 | A/B 测试、效果指标追踪、自动推荐最优版本 | 没有团队协作流程 |
| L4:工程化 | 审批流程、环境隔离、CI/CD集成、自动化评估 | 接近业界最佳实践 |
大多数公司目前处于 L0-L1 阶段。本文介绍的系统对应 L3-L4,可以按需裁剪,从 L2 开始建设,逐步演进。
